Migration help for user-defined tools

With user-defined tools and the UserTool API, it is now possible to connect user-defined tools more intuitively and use them with more of ecu.test’s features.

New features of user-defined tools

Support of ports

User-defined tools now enable implementing ports on user-defined tool connections. They are no longer restricted to job calls: for supported port types, it is possible to implement features for more convenience in test cases. For example, Read Image and Touch input test steps are available for user-defined Image ports. Supported port types and features can be found in the UserTool API.

Intuitive interface

The interface was developed to enable easy and maintainable implementations. User-defined tools can be realized with little code. Optionally, they can be extended by various functionalities. Jobs and properties can now be used more intuitively.

Independent Python modules

User-defined tools do not require ecu.test-specific imports. Thus they can be developed, maintained and used independently.

Adaptations for user-defined tool adapters

Existing user-defined tool adapters based on the deprecated Tooladapter API can be upgraded to a user-defined tool based on the new UserTool API. For that, a number of adaptations are necessary.

The following overview shows the main differences.

Overview changes

Method/Functionality

Deprecated user-defined tool adapters

User-defined tool

Registration in ecu.test

  • Variables TOOLNAME, TOOLCAPS

  • IsInstalled() and CreateToolAdapter()

  • Inheritance from ToolAdapter

  • Required method GetVersion(), may return None

  • Variables TOOLNAME and TOOLCAPS should not be used. The tool name is to be defined in GetToolName() now.

  • IsInstalled() and CreateToolAdapter() should not be implemented.

  • Variable MODULE_TYPE = 'USER_TOOL'

  • IsInstalled() is now optional as a classmethod of the UserTool class.

  • Required method GetVersion(), must return a string value

  • A file __init__.py needs to exist in the user tool’s directory.

__init__()

  • Must define ToolDescriptor in self._toolDesc

  • Called at ecu.test startup und when reloading the user libraries.

  • Must accept new property parameter properties: dict[str, str | bool].

  • The possible properties are no longer defined within the ToolDescriptor, but by GetProperties() method.

  • Called at configuration start

Tool properties

  • Defined in __init__() as PropertyDescriptors in the ToolDescriptor

  • Property name defined with name. Different displayName possible.

  • Access to values via self.GetProperty() and self.GetTypedProperty()

  • Defined as return dictionary of class method GetProperties(), see UserTools.

  • Property name defined by dictionary key. The property is displayed with the same name in the TBC.

  • Admitted types are ‘BOOL’, ‘STRING’, ‘CHOICE’. Other types, e.g. ‘INT’, need to be registered as ‘STRING’ and casted upon usage.

  • Access via parameter properties: dict[str, str | bool] in __init__()

Tool jobs

  • Defined with parameters in __init__() via JobDescriptors in the ToolDescriptor

  • Methods to be called must match the registered job name with “Job” prefix

  • Registered as jobs via return values of class method GetJobs(), see UserTools.

  • The properties of the tool job are generated using the docstring and the Python type hints of the method.

Configuration start

  • Set up tool connection within OnConfigure()

  • Set up tool connection within __init__()

Configuration stop

  • Disconnect tool in OnUnconfigure()

  • Disconnect tool in Dispose()

Example for the adaptations from a UserToolAdapter to a UserTool

The following side-by-side comparison shows an example tool adapter and its matching UserTool.

Migration notes are added as single-line or in-line comments.

Migration example

UserToolAdapter

UserTool

########################################################################
##
# Imports needed for every Tooladapter
from tts.core.toolingFramework.interface.Descriptors import \
    ToolDescriptor, PropertyDescriptor, JobDescriptor
from tts.core.toolingFramework.interface.Properties import \
    PropertyTypeId
from tts.core.toolingFramework.interface.Capabilities import \
    Toolcaps
##
from tts.core.toolingFramework.interface.AbstractAdapter import \
    ToolAdapter
from tts.core.toolingFramework.interface.Exceptions import \
    ToollibError
from tts.core.toolingFramework.interface.Proxy import \
    ToolAdapterProxy
##
########################################################################

TOOLNAME = 'AdvancedExampleAdapter'
TOOLCAPS = Toolcaps.GetNull()





def IsInstalled():
    """
    .. seealso:: :meth:`BasicExampleAdapter.IsInstalled()`
    """
    return True


def CreateToolAdapter(host, port):  # pylint: disable=unused-argument
    """
    .. seealso:: :meth:`BasicExampleAdapter.CreateToolAdapter()`
    """
    return ToolAdapterProxy(AdvancedExampleAdapter())


class AdvancedExampleAdapter(ToolAdapter):  # pylint: disable=no-toollib
    """
    Sample implementation of a ToolAdapter with additional feature.
    """




    def __init__(self):
        ToolAdapter.__init__(self)






































        self._toolDesc = ToolDescriptor(
            TOOLNAME,
            [],  # must be an empty list
            0,





            # List of PropertyDescriptors:













            [
                # definition of first property group
                PropertyDescriptor.CreateInstance(
                    name='choiceExampleGroup',
                    displayName='First property group',
                    type=PropertyTypeId.group,
                ),
                # choice group inside the property group
                # 'choiceExampleGroup'
                PropertyDescriptor.CreateInstance(
                    name='choiceParameter',
                    displayName='Choice group parameter',
                    description='An example parameter',
                    type=PropertyTypeId.choiceGroup,
                    validReferences=None,
                    default='Value A',
                    domain=['Value A', 'Value B'],
                    group='choiceExampleGroup'
                ),
                # property which is only displayed if 'Value A' is
                # selected in the previous choice group
                PropertyDescriptor.CreateInstance(
                    name='exampleCheckbox',
                    displayName='Check box for value A',
                    type=PropertyTypeId.bool,
                    default=True,
                    group='choiceParameter',
                    groupValue='Value A'
                ),
                # Example property with int value
                PropertyDescriptor.CreateInstance(
                    name='intParameter',
                    displayName='Example integer',
                    type=PropertyTypeId.int,
                    default=0,
                ),
            ],

            # List of ToolJobs:
            [
                # Tooljob with a complex return/result type
                JobDescriptor('GetCountryLanguageAndCurrency', [
                    PropertyDescriptor.CreateInstance(
                        name='country',
                        displayName='Country',
                        description=None,
                        type=PropertyTypeId.string,
                        domain=[
                            'Germany',
                            'Great Britain',
                            'France'
                        ],
                        default='France'
                    ),
                ],
                    'Demonstrates a grouped input parameter as well as '
                    'complex return types.',
                    # PropertyDescriptor for the return/result: For a
                    # complex return type the type of the result property
                    # should be a set to PropertyTypeId.object.
                    PropertyDescriptor.CreateInstance(
                    name='result',
                    description=None,
                    type=PropertyTypeId.object),
                ),
            ]
        )

    def OnConfigure(self):
        """
        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. Raise a ToollibError
        to signal an error.
        If OnConfigure fails, OnUnconfigure will not be called.
        Use "pass" if no external tool is used.
        """
        print(
            f'Setting up tool connection for test bench settings choice '
            f'parameter {self.GetProperty("choiceParameter")} '
            f'and integer parameter '
            f'{self.GetTypedProperty("intParameter")}.')

    def OnUnconfigure(self):
        """
        This method is called on tool shutdown, if the previous call to
        OnConfigure did not fail. It can be used to stop any tool needed
        by this ToolAdapter.
        Raise a ToollibError to signal an error. Use "pass" if no
        external tool is used.
        """
        print('Disconnecting tool.')

    def IsBroken(self):
        """
        This method is called before test execution and on configuration
        window update. Return True if any of the external tools used by
        this ToolAdapter cannot be used anymore (e.g connection lost,
        tool locked up). Otherwise or if no tool is used, return False.

        :return: True if tool is broken (not usable)
        :rtype: bool
        """
        return False

    def JobGetCountryLanguageAndCurrency(self, country):
        """
        Example Tooljob with a complex return type, in this example a
        tuple.
        For complex return values the type of the corresponding
        PropertyDescriptor for the return value needs to be set to
        PropertyTypeId.object.
        """
        if country == 'Germany':
            return ['German', 'Euro']
        if country == 'Great Britain':
            return ['English', 'Pound sterling']
        if country == 'France':
            return ['French', 'Euro']
        raise ValueError('Unknown country given')







    def GetVersion(self):
        """
        You can return the version of the tool you are connecting via
        this tool adapter as a string. The version will be documented
        in the test report.

        :return: version
        :rtype: Optional<str>
        """
        return None












from __future__ import annotations

from typing import Literal

from collections.abc import Callable

#########################################################################
##
# No UserTool-specific imports needed
#
#
#
#
#
#
#
#
#
#
#
#
#
##
#########################################################################

# TOOLNAME and TOOLCAPS should no longer be set.
# The tool name now has to be defined in GetToolName()

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

# IsInstalled() should no longer be implemented.






# CreateToolAdapter() should no longer be implemented.






class AdvancedExampleAdapterUserTool:  # Inheritance no longer needed
    """
    Sample implementation of a ToolAdapter with additional feature.
    """

    # The __init__() method now requires parameter "properties".
    # It may contain startup functionality as it is now called on tool
    # startup rather than ecu.test start.
    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.
        """
        # Description of properties and jobs are now handled within the
        # separate methods GetToolname(), GetProperties() and GetJobs()

        # The settings for properties in the TBC can now be accessed with
        # the "properties" dict.
        self._properties = properties

        # Properties can be accessed from the dictionary.
        try:
            int(self._properties["Example integer"])
        except ValueError:
            raise ValueError(
                f'Configured value '
                f'{self._properties["Example integer"]} for parameter '
                f'"intParameter" can not be converted to an integer.'
            )

        # OnConfigure() no longer needed:
        # Startup functionality previously called in OnConfigure()
        # may be moved to __init__().
        print(
            f'Setting up tool connection for test bench settings choice '
            f'parameter {self._properties["Choice group parameter"]} '
            f'and integer parameter '
            f'{self._properties["Example integer"]}.'
        )

    # New method to indicate tool name
    @classmethod
    def GetToolName(cls) -> str:
        """
        The name of the tool. This name is displayed in the test bench
        configuration.
        """
        return 'AdvancedExampleAdapter_UserTool'

    # New optional method to define properties
    @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 user tool when the configuration is started.
        Available property types are STRING, BOOL and CHOICE.

        """

        # Properties are now described as dictionaries.
        # The complexity of PropertyDescriptors is no longer needed.

        return {
            'First property group': {





                'Choice group parameter': {
                    'Description': 'An example parameter',
                    'Type': 'CHOICE',
                    'Choices': ['Value A', 'Value B'],
                    'Default': 'Value A',
                },






                    'Check box for value A': {
                        'Description': 'Additional choice for Value A. '
                                       'Will only be evaluated for '
                                       'Choice group parameter Value A',
                        'Type': 'BOOL',
                        'Default': True,
                        # Property is always shown;
                        # groupValues not supported.
                },
            },
            'Example integer': {
                    'Description': 'An example integer',
                    # Not supported types, e.g. previously 'int',
                    # need to be cast to the correct type upon usage.
                    'Type': 'STRING',
                    'Default': '0',
                },
        }

    # New optional method to define jobs
    @classmethod
    def GetJobs(cls) -> list[Callable]:
        """
        Provides a list of methods that are offered in ecu.test as a
        tool job for this user tool. The implementation is optional.

        Note: The properties of the tool job are generated using the
           docstring and the Python type hints of the method.

        :return: List of job methods.

        """
        # A method is now interpreted as a job once it is returned by
        # this method. A job method's name no longer needs to start with
        # "Job".
        # Also, jobs no longer need to be described with JobDescriptors.
        # Instead, the job descriptions are derived from the job method's
        # docstring.

        return [cls.GetCountryLanguageAndCurrency]









    # OnConfigure() is no longer used.
    # Any startup sequences for the tool connection should be moved
    # to __init__()











    # Renamed method from OnUnconfigure()
    def Dispose(self) -> None:
        """
        This method is called on tool shutdown. It can be used to stop
        any tool needed by this ToolAdapter.
        Can be omitted if no external tool is used.
        """
        print('Disconnecting tool.')



    def IsBroken(self) -> bool:
        """
        This method is called before test execution and on configuration
        window update. Return True if any of the external tools used by
        this ToolAdapter cannot be used anymore (e.g connection lost,
        tool locked up). Otherwise or if no tool is used, return False.

        :return: True if tool is broken (not usable)
        :rtype: bool
        """
        return False

    def GetCountryLanguageAndCurrency(  # Prefix "Job" no longer needed
        self,
        country: Literal['Germany', 'Great Britain', 'France'] = 'France'
    ) -> list[str]:  # Domain defined with type hints
        """
        Demonstrates a grouped input parameter as well as complex return
        types.

        :param country: Country whose information are to be checked.
                        Supported values are 'Germany', 'Great Britain',
                        'France'
        :return:
        """
        if country == 'Germany':
            return ['German', 'Euro']
        if country == 'Great Britain':
            return ['English', 'Pound sterling']
        if country == 'France':
            return ['French', 'Euro']
        raise ValueError('Unknown country given')

    # GetVersion() now always requires a string return value.
    def GetVersion(self) -> str:
        """
        Version of the external tool. This version will be documented
        in the test report.
        """
        return '1.0.0'