User-defined tools

To use a user-defined tool, its Python module must be saved in the Utilities directory (as specified in the Options ‣ Settings ‣ Directories menu). A file __init__.py needs to exist in the same directory as the Python module.

The user-defined tool can then be configured in the test bench configuration and the Tooljobs defined in the tool can be inserted into the test case. The specific structure of a user-defined tool is described in the API documentation api/tools/usertool-api/usertool-api.html.

Migration from UserToolAdapter

The deprecated UserToolAdapter is no longer supported. To upgrade to a UserTool, please see the UserTool migration help.

Example implementations

Various example implementations of user-defined tools can be found in the subdirectory Templates/DefaultData/Utilities/ExampleUserTools/ of the ecu.test installation directory:

  • BasicExampleUserTool.py Basic example of a UserTools including a UserJobs to implement dynamic domains of job parameters.

  • ExampleUserToolBasePort.py Example of a UserTool with Base port by implementing the UserPort interface.

  • ExampleUserToolImagePort.py Example of a UserTool with Image port by implementing the interfaces UserImagePort and UserTouchInput.

  • ExampleUserToolModelPort.py Example of a UserTool with Model port by implementing the interfaces UserModelPort, UserRecorder and UserSimulationManager. By implementing a model port, the model-based bus port will be available for the tool.

The minimal example from BasicExampleUserTool.py contains the following code:

"""
This module contains an example of a user-defined Tool. The source code of this example
can be found in the ecu.test installation directory under ``Templates/DefaultData/Utilities``.

A user-defined Tool must be placed in the Utilities directory of the current workspace. If a Tool
is changed, it is necessary to stop the current test bench (if it uses the Tooladapter) and
select *Extras -> Update user libraries* in ecu.test.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, TypedDict, NotRequired, Any, Literal

if TYPE_CHECKING:
    from collections.abc import Callable

# Required so that ecu.test can discover this module and recognize it as a user tool.
MODULE_TYPE = 'USER_TOOL'


class ExampleUserTool:
    def __init__(self, properties: dict[str, str | bool]) -> None:
        """
        This method is called on tool start (test bench start, manual start in
        configuration window). It can be used to start any external tool needed by this
        ToolAdapter. Use `pass` if no external tool is used.

        :param properties: Dictionary with the properties from the test bench configuration for
                           this tool and the configured values. The possible properties are
                           defined by the return value of the :func:`GetProperties` method.
        """
        self._properties = properties
        logging.getLogger('ecu.test').info(f'{self.GetToolName()} initialized')

    @classmethod
    def GetToolName(cls) -> str:
        """
        The name of the tool. This name is displayed in the test bench configuration.
        """
        return 'ExampleUserTool'

    @classmethod
    def GetJobs(cls) -> list[Callable]:
        """
        Returns a list of methods that should be registered on this tool as jobs.
        """
        return [cls.ExampleJob, DynamicJob]

    @classmethod
    def GetProperties(cls) -> dict[str, dict]:
        """
        Properties of the tool. These properties can be configured in the test bench configuration.
        The configured properties are passed to the `__init__` method when the configuration is
        started. Available property types are STRING, TEXT, BOOL and CHOICE. Properties may be
        sorted into groups to improve visual separation in the test bench configuration.
        Informational properties can be marked readonly.
        For BOOL and CHOICE properties, it is possible to define sub properties that are only
        available for a given property value.
        """
        return {
            'Option 1': {
                'Description': 'This is Option 1',
                'Type': 'STRING',
                'Default': 'default value',
            },
            'Group': {
                'Option 2': {
                    'Description': 'This is Option 2',
                    'Type': 'BOOL',
                    'Default': True,
                },
                'Option 3': {
                    'Description': 'This is Option 3',
                    'Type': 'CHOICE',
                    'Choices': ['c1', 'c2', 'c3'],
                    'Default': 'c2',
                },
                'Nested group': {
                    'Option 4': {
                        'Description': 'This is Option 4',
                        'Type': 'STRING',
                        'Default': '4',
                    },
                },
            },
            'Group with sub properties': {
                'Option 5': {
                    'Description': 'This BOOL option has a sub property',
                    'Type': 'BOOL',
                    'Default': True,
                    'Children': {
                        'Sub option 5.1': {
                            'Description': 'This option is visible if Option 5 is checked',
                            'Type': 'TEXT',
                            'ReadOnly': True,
                            'Default': 'Information on\nmultiple lines.\n',
                        },
                    },
                },
                'Option 6': {
                    'Description': 'This CHOICE option has a sub property',
                    'Type': 'CHOICE',
                    'Choices': {
                        'c1': {
                            'Sub option 6.1': {
                                'Description': 'This option is only visible for choice c1',
                                'Type': 'STRING',
                                'Default': 'default value',
                            },
                        },
                        'c2': {
                        },
                    },
                    'Default': 'c2',
                },
            },
        }

    def GetProperty(self, name: str) -> str | bool | None:
        return self._properties.get(name)

    def GetVersion(self) -> str:
        """
        Version of the external tool. This version will be documented in the test report.
        """
        return '1.0.0'

    def ExampleJob(self, paramOne: float = 1.0, paramTwo: float = 2.0) -> float:
        """
        Example job implementation. The function arguments will be automatically converted to job
        parameters based on their type annotations. This docstring will be used as the job's
        description. See `GetJobs()` for how to register this method as a job.

        :param paramOne: Description of paramOne
        :param paramTwo: Description of paramTwo
        :return: Multiplication of paramOne, paramTwo and value from TBC
        """
        try:
            valueFromTBC = float(self._properties['Option 4'])
        except ValueError:
            valueFromTBC = 1.0
        return paramOne * paramTwo * valueFromTBC


class DynamicParameterInformation(TypedDict):
    Choices: NotRequired[list[Any]]
    Default: NotRequired[Any]


class DynamicJob:
    """
    Example implementation of a job that allows the choice options and default values of selected
    parameters to be determined dynamically.
    """

    def __init__(self, parent: ExampleUserTool) -> None:
        self._parent = parent

    @classmethod
    def GetCategory(cls) -> str:
        """
        Optional method to specify the category of the job. Categories are used to group jobs in the
        job panel configuration.

        :return: The category of the job as a string.
        """
        return 'Example Category'

    def GetDynamicParameterInformation(self) -> dict[str, DynamicParameterInformation]:
        """
        This method specifies the choice options for selected parameters.
        :return: Dictionary where the key represents the parameter name and the value contains the
                 dynamically determined choices and default values.
        """
        if self._parent.GetProperty('Option 2'):
            choices = ['a', 'b', 'c']
        else:
            choices = ['x', 'y', 'z']

        return {
            'param2': {
                'Choices': choices,
                'Default': choices[0]
            },
        }

    def Call(self, param1: Literal['on', 'off'], param2: str) -> str:
        """
        This method is executed when the job is executed in ecu.test.

        :param param1: Description of param1
        :param param2: Description of param2
        :return: Output string containing param1 and param2.
        """
        return f'The parameters were set to {param1} and {param2}.'