Migrationshilfe für benutzerdefinierte Tools

Durch benutzerdefinierte Tools und die UserTool API können eigene Tools künftig intuitiver und umfassender in ecu.test angebunden werden.

Vorteile benutzerdefinierter Tools

Unterstützung von Ports

Mit den neuen benutzerdefinierten Tools ist es nun möglich, Ports auf benutzerdefinierten Toolanbindungen zu implementieren. Somit sind diese Toolanbindungen nicht mehr auf Jobaufrufe beschränkt: für unterstützte Porttypen können Features auf Portebene bereitgestellt werden, die Testfallimplementierungen deutlich erleichtern. Beispielsweise sind für benutzerdefinierte Image-Ports Bild-Lesen- und Touch-Input-Schritte verfügbar. Unterstützte Ports und Funktionalitäten können der UserTool API entnommen werden.

Intuitives Interface

Das Interface wurde mit dem Ziel wartbarer Implementierungen entwickelt. Anbindungen können bereits mit wenig Code realisiert werden und optional um viele Funktionalitäten erweitert werden. Jobs und Eigenschaften können nun einfacher bereitgestellt werden.

Eigenständige Python-Module

Für benutzerdefinierte Tools sind keine ecu.test-spezifischen Imports mehr nötig. Damit können sie auch außerhalb von ecu.test entwickelt, gewartet und genutzt werden.

Anpassungen für benutzerdefinierte Tooladapter

Bestehende benutzerdefinierte Tooladapter, die über die abgekündigte Tooladapter API implementiert wurden, können in ein benutzerdefiniertes Tool auf Basis der UserTool API umgewandelt werden. Dafür sind einige Anpassungen nötig.

Die folgende Übersicht zeigt wesentliche Unterschiede zwischen beiden Anbindungen.

Übersicht der Änderungen

Methode/Funktionalität

Benutzerdefinierter Tooladapter (deprecated)

Benutzerdefiniertes Tool

Registrierung in ecu.test

  • Variablen TOOLNAME, TOOLCAPS

  • IsInstalled() und CreateToolAdapter()

  • Vererbung von ToolAdapter

  • Notwendige Methode GetVersion(), darf None zurückgeben

  • Variablen TOOLNAME und TOOLCAPS sollen nicht genutzt werden. Der Toolname wird nun über GetToolName() definiert.

  • IsInstalled() und CreateToolAdapter() sollen nicht implementiert werden.

  • Variable MODULE_TYPE = 'USER_TOOL' muss gesetzt sein.

  • IsInstalled() ist nun optional und eine @classmethod der UserTool-Klasse.

  • Notwendige Methode GetVersion(), muss einen String-Wert zurückgeben

  • Im Verzeichnis des UserTools muss eine Datei __init__.py existieren.

__init__()

  • Muss ToolDescriptor in self._toolDesc definieren

  • Wird in ecu.test-Aufstart und beim Aktualisieren der Nutzerbibliotheken aufgerufen.

  • Muss den neuen Parameter properties: dict[str, str | bool] in der Signatur enthalten.

  • Die Tooleigenschaften werden nicht mehr im ToolDescriptor, sondern über die Methode GetProperties() definiert.

  • Wird bei Konfigurationsstart aufgerufen.

Tooleigenschaften

  • Werden in __init__() als PropertyDescriptors im ToolDescriptor definiert.

  • Name der Eigenschaft definiert mit name. Abweichende Anzeigenamen sind über displayName möglich.

  • Zugriff auf Werte über self.GetProperty() und self.GetTypedProperty()

  • Definiert als Rückgabe-Dictionary der Klassenmethode GetProperties(), siehe auch UserTools.

  • Name der Eigenschaft durch den Dictionary-Key definiert. Die Eigenschaft wird unter demselben Namen in der TBC angezeigt.

  • Zulässige Typen sind ‚BOOL‘, ‚STRING‘, ‚CHOICE‘. Andere Typen wie ‚INT‘, müssen als ‚STRING‘ deklariert und bei Nutzung in den korrekten Datentyp umgewandelt werden.

  • Zugriff über den Parameter properties: dict[str, str | bool] der __init__()-Methode

Tooljobs

  • Werden in __init__() mit JobDescriptors im ToolDescriptor definiert.

  • Methoden für den Aufruf müssen als Name den angebenen Jobnamen und dem zusätzlichen Präfix „Job“ haben.

  • Werden über den Rückgabewert der Methode GetJobs() als Jobs registriert, siehe UserTools.

  • Die Eigenschaften und Parameter werden aus dem Docstring und den Type Hints der Methode generiert.

Konfigurationsstart

  • Toolverbindung wird in OnConfigure() initialisiert.

  • Toolverbindung wird in __init__() intialisiert.

Konfigurationsstop

  • Toolverbindung wird in OnUnconfigure() getrennt.

  • Toolverbindung wird in Dispose() getrennt.

Bespiel für die Anpassungen von UserToolAdapter zu UserTool

Die folgende Vergleichsansicht zeigt einen benutzerdefiniert Tooladapter und ein benutzerdefiniertes Tool mit den entsprechenden Inhalten.

Änderungshinweise befinden sich als Kommentare an den jeweiligen Codestellen.

Migrationsbeispiel

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'