From 729e9d34b477be53f8091c237fa58d5ee93ef476 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 6 Jun 2026 23:29:42 +0300 Subject: [PATCH] doc: improve documentaiton about listeters --- .github/workflows/CI.yml | 2 +- README.md | 58 ++++++++++++++++++++++++++++++++++---- atest/ListenerCore.py | 7 +++-- atest/ListenerExample.py | 33 ++++++++++++++++++++++ atest/run.py | 8 ++++++ atest/tests_listener.robot | 6 +++- 6 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 atest/ListenerExample.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bee9044..77c683e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10.19, 3.14] + python-version: ["3.10", "3.14"] rf-version: [6.1.1, 7.4.2] steps: diff --git a/README.md b/README.md index e817915..a6cbdf3 100644 --- a/README.md +++ b/README.md @@ -156,15 +156,63 @@ Then Library can be imported in Robot Framework side like this: Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py ``` +# Listener + +PLC supports +[library listeners](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#libraries-as-listeners), +also listener can be defined in the class that defines keywords. PLC will automatically detect +is class is also listener and set the `ROBOT_LIBRARY_LISTENER` as a list. List will contains all +the class instances that are marked as listeners. + +Example: +```python +from robot.running.model import TestCase +from robot.result.model import TestCase as TestCaseResult + +from robotlibcore import DynamicCore, keyword + + +class ListenerExample(DynamicCore): + + ROBOT_LIBRARY_SCOPE = 'GLOBAL' + + def __init__(self): + self.ROBOT_LIBRARY_LISTENER = self + components = [KeywordsWithListener()] + super().__init__(components) + + + +class KeywordsWithListener: + ROBOT_LISTENER_API_VERSION = 3 + + def __init__(self): + self.test = None + + + def start_test(self, data: TestCase, result: TestCaseResult): + self.test = data.name + self.passed = result.passed + + @keyword + def keyword_with_listener(self, name: str, status: bool): + assert name == self.test, f"Test case name {name} does not match expected {self.test}" + assert status == self.passed, f"Test case status {status} does not match expected {self.passed} {type(self.passed)}" + +``` + +In the example, `KeywordsWithListener` acts as a listener and the `start_test` method is +called each time a test starts. + # Translation -PLC supports translation of keywords names and documentation. Translations must be provided in -the `translation` argument in the `HybridCore` or `DynamicCore` `__init__`, either as a -dictionary or through a [Path](https://docs.python.org/3/library/pathlib.html) to a -[JSON](https://www.json.org/json-en.html) file. Providing translation data is optional, also it +PLC supports translation of keywords names and documentation. Translations must be provided in +the `translation` argument in the `HybridCore` or `DynamicCore` `__init__`, either as a +dictionary or through a [Path](https://docs.python.org/3/library/pathlib.html) to a +[JSON](https://www.json.org/json-en.html) file. Providing translation data is optional, also it is not mandatory to provide translation to all keyword. -The keys of the dictionary are the methods names, not the keyword names, which implements keyword. +The keys of the dictionary are the methods names, not the keyword names, which implements keyword. Values are objects which contains two keys: `name` and `doc`. `name` key contains the keyword translated name and `doc` contains keyword translated documentation. Providing `doc` and `name` is optional, i.e. translations data can also provide translations only diff --git a/atest/ListenerCore.py b/atest/ListenerCore.py index b3ca4ee..c06eb58 100644 --- a/atest/ListenerCore.py +++ b/atest/ListenerCore.py @@ -36,7 +36,8 @@ def _start_suite(self, name, attrs): @keyword def first_component(self, arg: str): - assert arg == self.suite_name, f"Suite name '{self.suite_name}' should be detected by listener, but was not." + name = self.suite_name + assert name == arg, f"Test suite name {name} does not match expected {arg}." class SecondComponent: @@ -46,7 +47,9 @@ def __init__(self) -> None: @keyword def second_component(self, arg: str): - assert self.listener.test.name == arg, "Test case name should be detected by listener, but was not." + name = self.listener.test.name + assert name == arg, f"Test case name {name} does not match expected {arg}." + class ExternalListener: diff --git a/atest/ListenerExample.py b/atest/ListenerExample.py new file mode 100644 index 0000000..aacce40 --- /dev/null +++ b/atest/ListenerExample.py @@ -0,0 +1,33 @@ +from robot.running.model import TestCase +from robot.result.model import TestCase as TestCaseResult + +from robotlibcore import DynamicCore, keyword + + +class ListenerExample(DynamicCore): + + ROBOT_LIBRARY_SCOPE = 'GLOBAL' + ROBOT_LISTENER_API_VERSION = 3 + + def __init__(self): + self.ROBOT_LIBRARY_LISTENER = self + components = [KeywordsWithListener()] + super().__init__(components) + + + +class KeywordsWithListener: + ROBOT_LISTENER_API_VERSION = 3 + + def __init__(self): + self.test = None + + + def start_test(self, data: TestCase, result: TestCaseResult): + self.test = data.name + self.passed = result.passed + + @keyword + def keyword_with_listener(self, name: str, status: bool): + assert name == self.test, f"Test case name {name} does not match expected {self.test}" + assert status == self.passed, f"Test case status {status} does not match expected {self.passed} {type(self.passed)}" diff --git a/atest/run.py b/atest/run.py index feb05f6..70503f4 100755 --- a/atest/run.py +++ b/atest/run.py @@ -17,6 +17,7 @@ tests = join(curdir, "tests.robot") tests_types = join(curdir, "tests_types.robot") plugin_api = join(curdir, "plugin_api") +listener_api = join(curdir, "tests_listener.robot") sys.path.insert(0, join(curdir, "..", "src")) python_version = platform.python_version() for variant in library_variants: @@ -58,6 +59,13 @@ if rc > 250: sys.exit(rc) process_output(output) + +output = join(outdir, f"lib-Listener-python-{python_version}-robot-{RF_VERSION}.xml") +rc = run(listener_api, name="Listener", output=output, report=None, log=None, loglevel="debug") +if rc > 250: + sys.exit(rc) +process_output(output) + print("\nCombining results.") library_variants.append("DynamicTypesLibrary") xml_files = [str(xml_file) for xml_file in Path(outdir).glob("*.xml")] diff --git a/atest/tests_listener.robot b/atest/tests_listener.robot index 43bb131..cc8a8ce 100644 --- a/atest/tests_listener.robot +++ b/atest/tests_listener.robot @@ -1,5 +1,6 @@ *** Settings *** Library ListenerCore.py +Library ListenerExample.py *** Test Cases *** @@ -22,4 +23,7 @@ Tests The Suite Name ... to the suite name from _start_suite. ... ... It uses an independent class as listener which is manually set. - First Component Tests Listener + First Component Listener + +Test With Listener Example + Keyword With Listener Test With Listener Example False