iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0
Software Development

或躍在淵的CAE: 讓咱們用Python會一會ANSA + LS-DYNA系列 第 18

[Day18] - Box Drop Project精進計畫(10) - Test

  • 分享至 

  • xImage
  •  

太忙了,哪有那個美國時間寫test啦!寫test的時間,都可以寫兩個function了。

這次的project沒很難呀,把以前寫過的兜一兜就好了,不會出錯啦!

呵呵,類似的話,是不是聽起來很熟悉呀?

說實話,我們也曾經是不太喜歡寫test的一群,尤其有時候要mockmock去的時候,很容易耗盡腦力,還不如拿來寫code勒。

但隨著日漸肥大的code,以及參與project的兄弟越來越多時,不寫test可謂寸步難行呀。code裡一堆潛藏的bug,也沒辦法CI/CD,到最後反而得花更多時間。

所以我們試圖找到一個平衡點,盡量先寫剛剛好夠用的test,之後有時間再慢慢增加。

針對box drop project,我們也是秉持著剛剛好的原則,可能不會每個function都寫test,但希望大致的脈絡都有被檢測到。

或許諸位會覺得這個project到現在應該沒什麼錯吧?其實我們藏了一個對ANSA來說不算錯的小細節來與各位分享(註1)。

Why not pytest

這個問題真是戳中我們的痛處啊,我們也是pytest的愛好者啊!

但是一來pytest還不在ANSA附帶的third-party package內,如果客戶端環境沒網路又不能插USB,怎麼裝pytest呀?怎麼收集test的資訊呢?

二來,我們還沒找到辦法可以順利的讓pytest執行。我們試了很多方法,像是sys.path.appendsubprocess、從command line呼叫,甚至整個pytest搬進site-packages等,就是沒辦法work(崩潰)。

所以截至目前為止,我們還是使用Python內建的unittest來寫test。如果諸位有更好的處理方法,衷心希望能跟我們分享,感激不盡。

TestBoxDropBase

TestBoxDropBase中建立_setup_tear_down,並分別於setUptearDown中呼叫。每個test function執行前會呼叫setUp,執行後則會呼叫tearDown

我們這樣安排的目的,是希望TestBoxDropBase可以被後續的Test class所繼承。如果該Test class有需要特別的setUptearDown,可以overwrite_setup_tear_down

  • _setup目前沒有需要設定的部份。
  • _tear_down收集ANSA內所有的Entity,並刪除。
# tests.py
class TestBoxDropBase(unittest.TestCase):
    deck = constants.LSDYNA

    def setUp(self):
        self._setup()

    def tearDown(self):
        self._tear_down()

    def _setup(self):
        pass

    def _tear_down(self):
        ents = base.CollectEntities(self.deck, None, LSDYNAType.ALL)
        base.DeleteEntity(ents)

TestCreators

TestCreators我們測試了create_matcreate_propcreate_setcreate_contact四個function,下面我們以create_mat為例來說明。

create_mat有關的test functiontest_create_mattest_create_mat_given_nametest_create_mat_given_vals三個。

  • test_create_mat
    • 呼叫create_mat建立Entity,參數都使用預設值。
    • 檢查Entity _id是否為1,及_name內是否含有auto字串。
  • test_create_mat_given_name
    • 給定name來呼叫create_mat建立Entity
    • 檢查Entity_name是否與給定的一樣,及其內應該是不含有auto字串。
  • test_create_mat_given_vals
    • 給定E,並置入vals中來呼叫create_mat建立Entity
    • 檢查Entity card values中的E是否與給定的一樣。
#tests.py
class TestCreators(TestBoxDropBase):
    def test_create_mat(self):
        mat = create_mat()
        mat_id, mat_name = mat._id, mat._name
        self.assertEqual(mat_id, 1)
        self.assertIn('auto', mat_name)

    def test_create_mat_given_name(self):
        given_mat_name = 'dummy_mat_name'
        mat = create_mat(name=given_mat_name)
        mat_name = mat._name
        self.assertEqual(given_mat_name, mat_name)
        self.assertNotIn('auto', mat_name)

    def test_create_mat_given_vals(self):
        e = 123456
        vals = {'E': e}
        mat = create_mat(vals=vals)
        e_value = mat.get_entity_values(self.deck, ['E'])['E']
        self.assertEqual(e, e_value)

    def test_create_prop(self):
        prop = create_sec()
        prop_id, prop_name = prop._id, prop._name
        self.assertEqual(prop_id, 1)
        self.assertIn('auto', prop_name)

        mat = prop.get_entity_values(self.deck, ['MID'])['MID']
        mat_id, mat_name = mat._id, mat._name
        self.assertEqual(mat_id, 1)
        self.assertIn('auto', mat_name)

    def test_create_prop_given_name(self):
        given_prop_name = 'dummy_prop_name'
        prop = create_sec(name=given_prop_name)
        prop_name = prop._name
        self.assertEqual(given_prop_name, prop_name)
        self.assertNotIn('auto', prop_name)

    def test_create_prop_given_vals(self):
        t1 = 2
        vals = {'T1': t1}
        prop = create_sec(vals=vals)
        t1_value = prop.get_entity_values(self.deck, ['T1'])['T1']
        self.assertEqual(t1, t1_value)

    def test_create_set(self):
        set_ = create_set()
        set_id, set_name = set_._id, set_._name
        self.assertEqual(set_id, 1)
        self.assertIn('auto', set_name)

    def test_create_set_given_name(self):
        given_set_name = 'dummy_set_name'
        set_ = create_set(name=given_set_name)
        set_name = set_._name
        self.assertEqual(given_set_name, set_name)
        self.assertNotIn('auto', set_name)

    def test_create_set_add_prop(self):
        prop = create_sec()
        set_ = create_set(prop)

        prop_ents = base.CollectEntities(self.deck, set_, LSDYNAType.PROPERTY)
        self.assertEqual(len(prop_ents), 1)

        all_ents = base.CollectEntities(self.deck, set_, LSDYNAType.ALL)
        self.assertEqual(len(all_ents), 1)

    def test_create_contact(self):
        set1 = create_set()
        set2 = create_set()
        contact = create_contact(ssid=set1._id,
                                 msid=set2._id,
                                 sstyp=ContactType.TYPE2_PART_SET.value,
                                 mstyp=ContactType.TYPE2_PART_SET.value)
        contact_id, contact_name = contact._id, contact._name
        self.assertEqual(contact_id, 1)
        self.assertIn('auto', contact_name)

TestID

TestID我們測試了test_get_mat_prop_idtest_get_idtest_mix_idtest_get_fit_id_rangetest_get_fit_mix_id_range五個function

  • test_get_mat_prop_id
    • 呼叫十次create_sec
    • 呼叫一次create_mat
    • 因為create_sec會同時建立materialproperty,此時ANSA裡應該有十個property及十一個material
    • 檢查get_mat_prop_id是否能自動傳回下一個materialproperty同時可用的id=12
  • test_get_id
    • 呼叫十次create_set
    • 檢查get_id是否回傳下一個可用id=11
  • test_get_id
    • 呼叫五次create_sec
    • 呼叫十次create_set
    • 檢查get_mix_id是否回傳下一個propertyset同時可用id=11
  • test_get_fit_id_range
    • 呼叫一次create_mat
    • 呼叫一次給予MID=10create_mat
    • 此時ANSA裡應該有兩個materialid分別為110
    • 檢查當需要八個id時,get_fit_id_range能否回傳一個當下可用的range,即210,中間能夠使用的為2~9
  • test_get_fit_mix_id_range
    • 呼叫一次create_mat
    • 呼叫一次給予MID=10create_mat
    • 呼叫一次create_sec
    • 呼叫十一次create_set
    • 此時ANSA裡應該有三個material(id分別為1210)、一個property(id1)及十一個set(id1~11)。
    • 檢查當需要八個id時,test_get_fit_mix_id_range能否回傳一個當下可用的mix range,即1220,中間能夠使用的為12~19
# tests.py
class TestID(TestBoxDropBase):
    def test_get_mat_prop_id(self):
        for _ in range(10):
            create_sec()
        create_mat()
        mat_prop_id = get_mat_prop_id()
        self.assertEqual(mat_prop_id, 12)

    def test_get_id(self):
        for _ in range(10):
            create_set()
        set_id = get_id(LSDYNAType.SET)
        self.assertEqual(set_id, 11)

    def test_mix_id(self):
        for _ in range(5):
            create_sec()
        for _ in range(10):
            create_set()
        mix_id = get_mix_id([LSDYNAType.PROPERTY, LSDYNAType.SET])
        self.assertEqual(mix_id, 11)

    def test_get_fit_id_range(self):
        create_mat()
        create_mat(vals={'MID': 10})
        start, end_ = get_fit_id_range(8, LSDYNAType.MATERIAL)
        self.assertEqual(start, 2)
        self.assertEqual(end_, 10)

    def test_get_fit_mix_id_range(self):
        create_mat()
        create_mat(vals={'MID': 10})
        create_sec()
        for _ in range(11):
            create_set()
        start, end_ = get_fit_mix_id_range(8, [LSDYNAType.MATERIAL,
                                               LSDYNAType.PROPERTY,
                                               LSDYNAType.SET])
        self.assertEqual(start, 12)
        self.assertEqual(end_, 20)

TestMisc

TestMisc我們測試了test_create_boundary_spctest_create_boundary_spc_c_strtest_create_initial_velocitytest_create_ctrl_cardtest_create_ctrl_cardtest_output_file六個function

  • test_create_boundary_spc
    • 呼叫create_set ,建立一個set Entity
    • 呼叫create_boundary_spc,並指定fields為上一步建立的setc=123456,來建立一個boundary_spc_set Entity
    • 檢查EntityNSID_idc是否與給定值相同。
  • test_create_boundary_spc_c_str
    • 步驟與test_create_boundary_spc相同,但此處c = '123456'str型態。
    • 檢查EntityNSID_id是否與給定值相同。
    • 檢查Entityc是否與給定值不同
  • test_create_initial_velocity
    • 呼叫create_set ,建立一個set Entity
    • 呼叫create_boundary_spc,並指定fields為上一步建立的setVZ=-500,來建立一個initial_velocity_set Entity
    • 檢查EntityNSID_idVZ是否與給定值相同。
  • test_create_ctrl_card
    • 呼叫get_card_ent ,得到一個control Entity
    • 呼叫card_handler,並指定參數為上一步所得的control EntityTERMINATIONENDTIM1.5E-2
    • 檢查Control EntityTERMINATION是否為ONTERMINATION_ENDTIM是否與給定值相同。
  • test_create_db_card
    • 呼叫get_card_ent ,得到一個database Entity
    • 呼叫card_handler,並指定參數為上一步所得的database EntityD3PLOTDT2E-4
    • 檢查database EntityD3PLOT是否為OND3PLOT_DT是否與給定值相同。
  • test_output_file
    • 給定filename建立一個pathlib.Path object
    • 呼叫output_file寫出k檔。
    • 透過Path.is_file檢查k檔是否在硬碟中。
    • 建立一個context manager
      • 讀取k檔部份內容,檢查ANSA字串是否在內(註2)。
      • 如果部份內容不存在ANSA字串,則raise AssertionError
      • 最後無論assert成功與否,皆呼叫Path.unlink()刪除k檔。
# tests.py
class TestMisc(TestBoxDropBase):
    def test_create_boundary_spc(self):
        c = 123456
        set_ = create_set()
        set_id = set_._id
        fields = ('NSID', 'c')
        boundary_spc = create_boundary_spc(dict(zip(fields, (set_id, c))))
        card_values = boundary_spc.get_entity_values(self.deck, fields)
        spc_nsid, spc_c = card_values['NSID']._id, card_values['c']
        self.assertEqual(spc_nsid, set_id)
        self.assertEqual(spc_c, c)

    def test_create_boundary_spc_c_str(self):
        c = '123456'
        set_ = create_set()
        set_id = set_._id
        fields = ('NSID', 'c')
        boundary_spc = create_boundary_spc(dict(zip(fields, (set_id, c))))
        card_values = boundary_spc.get_entity_values(self.deck, fields)
        spc_nsid, spc_c = card_values['NSID']._id, card_values['c']
        self.assertEqual(spc_nsid, set_id)
        self.assertNotEqual(spc_c, c)

    def test_create_initial_velocity(self):
        vz = -500
        box_set = create_set()
        box_set_id = box_set._id
        fields = ('NSID', 'VZ')
        initial_velocity = create_initial_velocity(
            dict(zip(fields, (box_set_id, vz))))
        card_values = initial_velocity.get_entity_values(self.deck, fields)
        iv_nsid, iv_vz = card_values['NSID']._id, card_values['VZ']
        self.assertEqual(iv_nsid, box_set_id)
        self.assertEqual(iv_vz, vz)

    def test_create_ctrl_card(self):
        endtim = 1.5E-2
        crtl_ent = get_card_ent(ControlCardType.CONTROL)
        ctrl_params = [('TERMINATION', {'ENDTIM': endtim})]
        crtl_ent = card_handler(crtl_ent, ctrl_params)
        fields = ('TERMINATION', 'TERMINATION_ENDTIM')
        card_values = crtl_ent.get_entity_values(self.deck, fields)
        termination, termination_endtim = card_values.values()
        self.assertEqual(termination, 'ON')
        self.assertEqual(termination_endtim, endtim)

    def test_create_db_card(self):
        dt = 2E-4
        db_ent = get_card_ent(ControlCardType.DATABASE)
        db_params = [('D3PLOT', {'DT': dt})]
        db_ent = card_handler(db_ent, db_params)
        fields = ('D3PLOT', 'D3PLOT_DT')
        card_values = db_ent.get_entity_values(self.deck, fields)
        db, db_d3plot = card_values.values()
        self.assertEqual(db, 'ON')
        self.assertEqual(db_d3plot, dt)

    def test_output_file(self):
        filename = 'lsdyna_test_file.k'
        p = Path(output_file(filename, self.deck))
        self.assertTrue(p.is_file())
        with open(p) as f:
            try:
                self.assertIn('ANSA', f.read(30))
            except AssertionError as ex:
                raise ex
            finally:
                p.unlink()

TestPlate

  • overwrite _setup,將產生plate所需參數於此設定。
  • test_create_plate_v1
    • 呼叫create_sec建立一個materialproperty,命名其idplate_mat_prop_id
    • 呼叫create_set,並將剛剛建立的property Entity置入其中。
    • 呼叫MyImportV1建立一個context manager,命名為plate_import_v1
    • plate_import_v1範圍內呼叫plate_import_v1
    • 檢查其產生nodeshell數量是否正確。
# tests.py
class TestPlate(TestBoxDropBase):
    def _setup(self):
        self.l, self.w, self.en1, self.en2 = 100, 100, 10, 10
        self.z_elv, self.move_xy, self.rot_angle = 0, None, None

    def test_create_plate_v1(self):
        plate_prop = create_sec()
        plate_mat_prop_id = plate_prop._id
        plate_set = create_set(plate_prop, 'plate', deck=self.deck)
        plate_import_v1 = MyImportV1()
        with plate_import_v1 as import_v1:
            create_plate_v1(import_v1,
                            self.l,
                            self.w,
                            self.en1,
                            self.en2,
                            plate_mat_prop_id,
                            z_elv=self.z_elv,
                            move_xy=self.move_xy,
                            rot_angle=self.rot_angle,
                            deck=self.deck)

        n_nodes = (self.en1+1)*(self.en2+1)
        n_shells = self.en1*self.en2
        self.assertEqual(len(plate_import_v1.nodes), n_nodes)
        self.assertEqual(len(plate_import_v1.shells), n_shells)

TestBox

  • overwrite _setup,將產生box所需參數於此設定。
  • test_create_box_v1
    • 呼叫create_sec建立一個materialproperty,命名其idbox_mat_prop_id
    • 呼叫create_set,並將剛剛建立的property Entity置入其中。
    • 呼叫MyImportV1建立一個context manager,命名為box_import_v1
    • box_import_v1範圍內呼叫create_box_v1
    • 檢查其產生nodesolid數量是否正確。
# tests.py
class TestBox(TestBoxDropBase):
    def _setup(self):
        self.l, self.w, self.h, self.en1, self.en2, self.en3 = 50, 50, 50, 10, 10, 10
        self.z_elv, self.move_xy, self.rot_angle = 5, (50, 20), 45

    def test_create_box_v1(self):
        box_prop = create_sec()
        box_mat_prop_id = box_prop._id
        box_set = create_set(box_prop, 'box', deck=self.deck)
        box_import_v1 = MyImportV1()
        with box_import_v1 as import_v1:
            create_box_v1(import_v1,
                          self.l,
                          self.w,
                          self.h,
                          self.en1,
                          self.en2,
                          self.en3,
                          box_mat_prop_id,
                          z_elv=self.z_elv,
                          move_xy=self.move_xy,
                          rot_angle=self.rot_angle,
                          deck=self.deck)

        n_nodes = (self.en1+1)*(self.en2+1)*(self.en3+1)
        n_solids = self.en1*self.en2*self.en3
        self.assertEqual(len(box_import_v1.nodes), n_nodes)
        self.assertEqual(len(box_import_v1.solids), n_solids)

執行測試

我們測試的方法是透過搭配Python的unittest.main及ANSA的command line interfaceterminal中執行。

tests.py的最後加入下面幾行:

# tests.py
if __name__ == '__main__'
    unittest.main()

接著在terminal中輸入:

ANSA路徑 -execscript "tests.py路徑" -nogui

輸入指令會像:

~/BETA_CAE_Systems/ansa_v23.0.0/ansa64.sh -execscript "./tests.py" -nogui

其中-nogui可以讓我們於不啟動ANSA GUI的情況下使用ANSA。

備註

註1:於create_boundary_spc中的c其實應該是int型態,但如果給str型態,ANSA也是可以接受,不會報錯。這個問題,也是我們在寫test時發現的,本來想直接在[Day06]~[Day07]的box_drop.py裡就修正,但轉念一想,或許直接分享在今天的內容,也會是不錯的學習體驗。

註2:output_file格式大概會像:

$
$ANSA_VERSION;23.0.0;
$
$
$ file created by  A N S A  Wed Sep 01 11:34:15 2022
$
$ output from :
$
$ 
$
$ Settings :
$
$ Output format : R13
$
$ Output : Visible
$
$
$
$
*KEYWORD
*END

提醒

[Day17]我們建立了一個名為_create_entityhelper function,用以幫助大部分的creator呼叫base.CreateEntity。相關修改可以參考creators.py

#creators.py
def _create_entity(type_, fields, deck=None):
    deck = deck or constants.LSDYNA
    return base.CreateEntity(deck, type_, fields)

Code

本日程式碼傳送門


上一篇
[Day17] - Box Drop Project精進計畫(9) - 實作Context Manager用以簡化base.ImportV1使用流程
下一篇
[Day19] - Box Drop Project精進計畫(11) - 使用Streamlit於本機端產生config檔
系列文
或躍在淵的CAE: 讓咱們用Python會一會ANSA + LS-DYNA30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言