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).

launch.json
{
    "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.

TestExcavator.py
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.

TestExcavator.py
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.