Benutzerdefinierte Tools

Um ein benutzerdefiniertes Tool zu verwenden, muss dessen Python-Modul im Utilities-Verzeichnis (wie im Menü Optionen ‣ Einstellungen ‣ Verzeichnisse angegeben) liegen. Zudem muss eine Datei __init__.py im Verzeichnis des Python-Moduls existieren.

Anschließend kann das benutzerdefinierte Tool über die Testbenchkonfiguration konfiguriert und die im Tool definierten Tooljobs in den Testfall eingefügt werden. Der konkrete Aufbau eines benutzerdefinierten Tools ist in der Api-Dokumentation api/tools/usertool-api/usertool-api.html erläutert.

Migration von UserToolAdapter

Der veraltete UserToolAdapter wird nicht mehr unterstützt. Eine Anleitung zur Migration auf ein UserTool findet sich unter UserTool-Migrationshilfe.

Beispielimplementierungen

Verschiedene Beispielimplementierungen für benutzerdefinierte Tools sind im Unterverzeichnis Templates/DefaultData/Utilities/ExampleUserTools/ des ecu.test Installationsverzeichnis zu finden:

  • BasicExampleUserTool.py Grundlegendes Beispiel eines UserTools einschließlich eines UserJobs zur Umsetzung dynamischer Parameterbereiche für Jobs.

  • ExampleUserToolBasePort.py Beispiel eines UserTools mit Base-Port durch Implementierung des UserPort Interfaces.

  • ExampleUserToolImagePort.py Beispiel eines UserTools mit Image-Port durch Implementierung der Interfaces UserImagePort und UserTouchInput.

  • ExampleUserToolModelPort.py Beispiel eines UserTools mit Modell-Port durch Implementierung der Interfaces UserModelPort, UserRecorder und UserSimulationManager. Durch die Implementierung eines Modell-Ports wird zudem der modellbasierte Bus-Port für das Tool verfügbar.

Das Minimalbeispiel in BasicExampleUserTool.py enthält den folgenden 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}.'