Automated Vortex Content Testing with Python
Purpose of this tutorial
This tutorial demonstrates how to script an automated test while loading a scene containing a fully working environment and mechanism.
The Excavator Scene from the Demo Scenes is used as input content to the script.
The purpose of the test is to operate the Excavator so that the equipment digs a trench and to measure the output payload while operating.
Visual Studio Code is used as python IDE.
Extending the Content with VHL Interfaces
Some VHL interfaces are already available on the Excavator mechanism, however one is missing to start the equipment without UI, nor Joystick device.
"External Control" and "Earthwork Bucket" interface are already available in the content.
Let's add the "External Engine Control" VHL Interface.
The Python Script
Basic Script "Hello World"
The first lines to define in the test script are the following:
import sys import unittest # the standard unittest python module import vxatp3 # Vortex tools on top of the unittest module import Vortex # Vortex API # definition of the test case class in this script file (derived from TestCase) class TestExcavator(unittest.TestCase): def test_nothing(self): self.assertTrue(True) def main(): # utility when running the file directly # alternative is to use vxatp_run_thisdirectory(...) from a run_all.py script vxatp3.vxatp_run_thisfile('.\\log\\') if __name__ == "__main__": main()
The previous test simply runs a basic test using vxatp3 and unittest as test framework.
The following is the env file and launch.json used in VSCode (default python).
{ "version": "0.2.0", "configurations": [ { "name": "Python: Current File", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal" } ] }
Simulate Script
The following script adds content loading and simulation functionalities.
Note that there can only be one Vortex Application per test run (see setUpClass).
However, load/unload can be done for each sub test.
Note also the current solution needed to ensure that the python3 interpreter used in Vortex embedded is the same as the one used to run this test.
import sys import unittest # the standard unittest python module import vxatp3 # Vortex tools on top of the unittest module import Vortex # Vortex API # definition of the test case class in this script file (derived from TestCase) class TestExcavator(unittest.TestCase): scene_filename = "C:/CM Labs/Vortex Studio Content 2021b/Demo Scenes/Scenario/Excavator Scene/Excavator.vxscene" scene_object = None # called for each test method 'begin' def setUp(self): # edit mode vxatp3.VxATPUtils.requestApplicationModeChangeAndWait(self.application, Vortex.kModeEditing) # load content self.scene_object = self.application.getSimulationFileManager().loadObject(self.scene_filename) self.assertIsNotNone(self.scene_object) self.application.update() print('%s loaded.' % self.scene_filename) # called for each test method 'end' def tearDown(self): # back to edit mode before unloading vxatp3.VxATPUtils.requestApplicationModeChangeAndWait(self.application, Vortex.kModeEditing) # unload content self.application.getSimulationFileManager().unloadObject(self.scene_object) self.scene_object = None # called once for the class 'begin' @classmethod def setUpClass(cls): # workaround having a common python interpreter in the setup file (should be built-in vxatp) cls._config = vxatp3.VxATPConfig('.') # avoid exception from vxatp # only one application per Test cls.application = vxatp3.VxATPConfig.createApplication(cls, cls.__class__.__name__, None) serializer = Vortex.ApplicationConfigSerializer() serializer.load(vxatp3.VxATPConfig.getAppConfigWithGraphics()) # getAppConfigWithGraphics() #getAppConfigWithoutGraphics() config = serializer.getApplicationConfig() # Set the interpreterDirectory to the same that launched this script config.parameterPython3.interpreterDirectory.value = sys.exec_prefix config.apply(cls.application) # cls.application.setSyncMode(Vortex.kSyncNone) cls.application.update() # called once for the class 'end' @classmethod def tearDownClass(cls): cls.application = None @unittest.skip('Test Example 1') def test_nothing(self): self.assertTrue(True) def test_simulate(self): # activate configurations if any # ... # self.application.update() # simulate mode self.assertTrue(vxatp3.VxATPUtils.requestApplicationModeChangeAndWait(self.application, Vortex.kModeSimulating)) for step in range(0,30): self.application.update() def main(): # utility when running the file directly # alternative is to use vxatp_run_thisdirectory(...) from a run_all.py script vxatp3.vxatp_run_thisfile('.\\log\\') if __name__ == "__main__": main()
Payload Script
Finally, the last test method uses the VHL interface to start the engine, drive the boom and dig in order to fill the bucket and measure the payload.
import sys import unittest # the standard unittest python module import vxatp3 # Vortex tools on top of the unittest module import Vortex # Vortex API # definition of the test case class in this script file (derived from TestCase) class TestExcavator(unittest.TestCase): scene_filename = "C:/CM Labs/Vortex Studio Content 2021b/Demo Scenes/Scenario/Excavator Scene/Excavator.vxscene" scene_object = None # called for each test method 'begin' def setUp(self): # edit mode vxatp3.VxATPUtils.requestApplicationModeChangeAndWait(self.application, Vortex.kModeEditing) # load content self.scene_object = self.application.getSimulationFileManager().loadObject(self.scene_filename) self.assertIsNotNone(self.scene_object) self.application.update() print('%s loaded.' % self.scene_filename) # called for each test method 'end' def tearDown(self): # back to edit mode before unloading vxatp3.VxATPUtils.requestApplicationModeChangeAndWait(self.application, Vortex.kModeEditing) # unload content self.application.getSimulationFileManager().unloadObject(self.scene_object) self.scene_object = None # called once for the class 'begin' @classmethod def setUpClass(cls): # workaround having a common python interpreter in the setup file (should be built-in vxatp) cls._config = vxatp3.VxATPConfig('.') # avoid exception from vxatp # only one application per Test cls.application = vxatp3.VxATPConfig.createApplication(cls, cls.__class__.__name__, None) serializer = Vortex.ApplicationConfigSerializer() serializer.load(vxatp3.VxATPConfig.getAppConfigWithGraphics()) # getAppConfigWithGraphics() #getAppConfigWithoutGraphics() config = serializer.getApplicationConfig() # Set the interpreterDirectory to the same that launched this script config.parameterPython3.interpreterDirectory.value = sys.exec_prefix config.apply(cls.application) # cls.application.setSyncMode(Vortex.kSyncNone) cls.application.update() # called once for the class 'end' @classmethod def tearDownClass(cls): cls.application = None @unittest.skip('Test Example 1') def test_nothing(self): self.assertTrue(True) @unittest.skip('Test Example 2') def test_simulate(self): # activate configurations if any # ... # self.application.update() # simulate mode self.assertTrue(vxatp3.VxATPUtils.requestApplicationModeChangeAndWait(self.application, Vortex.kModeSimulating)) for step in range(0,30): self.application.update() def test_payload(self): # activate configurations if any # ... # self.application.update() # find Excavator Interface excavator_object = self.scene_object.findExtensionByName('Excavator').toObject() self.assertIsNotNone(excavator_object) engine_control = excavator_object.findExtensionByName('External Engine Control') self.assertIsNotNone(engine_control) external_control = excavator_object.findExtensionByName('External Control') self.assertIsNotNone(external_control) earthwork_bucket = excavator_object.findExtensionByName('Earthwork Bucket') self.assertIsNotNone(earthwork_bucket) # simulate mode self.assertTrue(vxatp3.VxATPUtils.requestApplicationModeChangeAndWait(self.application, Vortex.kModeSimulating)) for step in range(0,60): self.application.update() # start engine engine_control.getInput("Engine Start Toggle Switch").value = True # setup speed engine_control.getInput("Engine Speed Dial").value = 9 # use external control external_control.getInput("External Override").value = True # warm up for step in range(0,30): self.application.update() print("Throttle : %s" % engine_control.getOutput("Engine Throttle").value) print("RPM : %s" % external_control.getOutput("RPM").value) # operate input_values = [ ["External Stick", 1.0, 3], ["External Boom", 1.0, 3], ["External Stick", -1.0, 3], ["External Bucket", 1.0, 3], ["External Boom", -1.0, 2] ] for input_value in input_values: in_name = input_value[0] in_value = input_value[1] in_duration = input_value[2] external_control.getInput(in_name).value = in_value for step in range(0,30*in_duration): self.application.update() print("Payload : %s" % earthwork_bucket.getOutput("Payload").value) external_control.getInput(in_name).value = 0 # measure payload payload = earthwork_bucket.getOutput("Payload").value self.assertGreater(payload, 1000) def main(): # utility when running the file directly # alternative is to use vxatp_run_thisdirectory(...) from a run_all.py script vxatp3.vxatp_run_thisfile('.\\log\\') if __name__ == "__main__": main()
Log Files
vxatp generates automatically log files that can be opened directly in VS Code.