From 6159892adf368aee2e4d12e1824c485871e61ed7 Mon Sep 17 00:00:00 2001 From: Jan Richter Date: Thu, 1 Aug 2024 15:53:15 +0200 Subject: [PATCH 1/2] Runnable identifier This commit introduces a way how to explicitly specify runnable identifier through Job API or runnable recipe. This change shouldn't affect current behaviour and if the identifier is not specified it will be generated based on the `runner.identifier_format` config variable. Reference: #5964 Signed-off-by: Jan Richter --- avocado/core/nrunner/runnable.py | 47 +++++++++++-------- avocado/schemas/runnable-recipe.schema.json | 4 ++ examples/jobs/custom_exec_test.py | 9 +++- .../recipes/runnable/exec_test_sleep_3.json | 2 +- .../nrunner/recipes/runnable/identifier.json | 1 + .../nrunner/recipes/runnables/true_false.json | 6 ++- selftests/check.py | 2 +- selftests/functional/resolver.py | 2 +- selftests/unit/runnable.py | 18 ++++++- 9 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 examples/nrunner/recipes/runnable/identifier.json diff --git a/avocado/core/nrunner/runnable.py b/avocado/core/nrunner/runnable.py index 78bff90f8b..606330239e 100644 --- a/avocado/core/nrunner/runnable.py +++ b/avocado/core/nrunner/runnable.py @@ -89,7 +89,7 @@ class Runnable: execute a runnable. """ - def __init__(self, kind, uri, *args, config=None, **kwargs): + def __init__(self, kind, uri, *args, config=None, identifier=None, **kwargs): self.kind = kind #: The main reference to what needs to be run. This is free #: form, but commonly set to the path to a file containing the @@ -113,6 +113,7 @@ def __init__(self, kind, uri, *args, config=None, **kwargs): #: expressing assets that the test will require in order to run. self.assets = kwargs.pop("assets", None) self.kwargs = kwargs + self._identifier = identifier def __repr__(self): fmt = ( @@ -156,27 +157,29 @@ def identifier(self): Since this is formatter, combined values can be used. Example: "{uri}-{args}". """ - fmt = self.config.get("runner.identifier_format", "{uri}") + if not self._identifier: + fmt = self.config.get("runner.identifier_format", "{uri}") - # Optimize for the most common scenario - if fmt == "{uri}": - return self.uri + # Optimize for the most common scenario + if fmt == "{uri}": + return self.uri - # For args we can use the entire list of arguments or with a specific - # index. - args = "-".join(self.args) - if "args" in fmt and "[" in fmt: - args = self.args + # For args we can use the entire list of arguments or with a specific + # index. + args = "-".join(self.args) + if "args" in fmt and "[" in fmt: + args = self.args - # For kwargs we can use the entire list of values or with a specific - # index. - kwargs = "-".join(str(self.kwargs.values())) - if "kwargs" in fmt and "[" in fmt: - kwargs = self.kwargs + # For kwargs we can use the entire list of values or with a specific + # index. + kwargs = "-".join(str(self.kwargs.values())) + if "kwargs" in fmt and "[" in fmt: + kwargs = self.kwargs - options = {"uri": self.uri, "args": args, "kwargs": kwargs} + options = {"uri": self.uri, "args": args, "kwargs": kwargs} + self._identifier = fmt.format(**options) - return fmt.format(**options) + return self._identifier @property def config(self): @@ -255,7 +258,7 @@ def _validate_recipe(cls, recipe): """ if not cls._validate_recipe_json_schema(recipe): # This is a simplified validation of the recipe - allowed = set(["kind", "uri", "args", "kwargs", "config"]) + allowed = set(["kind", "uri", "args", "kwargs", "config", "identifier"]) if not "kind" in recipe: raise RunnableRecipeInvalidError('Missing required property "kind"') if not set(recipe.keys()).issubset(allowed): @@ -279,6 +282,7 @@ def from_dict(cls, recipe_dict): recipe_dict.get("uri"), *recipe_dict.get("args", ()), config=config, + identifier=recipe_dict.get("identifier"), **recipe_dict.get("kwargs", {}), ) @@ -296,12 +300,14 @@ def from_recipe(cls, recipe_path): return cls.from_dict(recipe_dict) @classmethod - def from_avocado_config(cls, kind, uri, *args, config=None, **kwargs): + def from_avocado_config( + cls, kind, uri, *args, config=None, identifier=None, **kwargs + ): """Creates runnable with only essential config for runner of specific kind.""" if not config: config = {} config = cls.filter_runnable_config(kind, config) - return cls(kind, uri, *args, config=config, **kwargs) + return cls(kind, uri, *args, config=config, identifier=identifier, **kwargs) @classmethod def get_configuration_used_by_kind(cls, kind): @@ -423,6 +429,7 @@ def get_dict(self): if self.uri is not None: recipe["uri"] = self.uri recipe["config"] = self.config + recipe["identifier"] = self.identifier if self.args is not None: recipe["args"] = self.args kwargs = self.kwargs.copy() diff --git a/avocado/schemas/runnable-recipe.schema.json b/avocado/schemas/runnable-recipe.schema.json index f0b24a690f..d8c3e63b38 100644 --- a/avocado/schemas/runnable-recipe.schema.json +++ b/avocado/schemas/runnable-recipe.schema.json @@ -28,6 +28,10 @@ "config": { "description": "Avocado settings that should be applied to this runnable. At least the ones declared as CONFIGURATION_USED in the runner specific for this kind should be present", "type": "object" + }, + "identifier": { + "description": "ID of runnable which will be used for identification during the runtime and in logs. If this is not specified it will be autogenerated based on `runner.identifier_format` config value", + "type": "string" } }, "additionalProperties": false, diff --git a/examples/jobs/custom_exec_test.py b/examples/jobs/custom_exec_test.py index ad5c57402e..19ca7526b0 100755 --- a/examples/jobs/custom_exec_test.py +++ b/examples/jobs/custom_exec_test.py @@ -18,10 +18,17 @@ # here, 'Hello World!' is appended to the uri (/usr/bin/echo) echo = Runnable("exec-test", "/usr/bin/echo", "Hello World!") +# here, echo-hello-world is used as id of this runnable +id_test = Runnable( + "exec-test", "/usr/bin/echo", "Hello World!", identifier="echo-hello-world" +) + # the execution of examples/tests/sleeptest.sh takes around 2 seconds # and the output of the /usr/bin/echo test is available at the # job-results/latest/test-results/exec-test-2-_usr_bin_echo/stdout file. -suite = TestSuite(name="exec-test", tests=[sleeptest, echo]) +# and the last test is the same as the echo, but you can notice that +# the identifier in avocado output is more specific to what this test is doing. +suite = TestSuite(name="exec-test", tests=[sleeptest, echo, id_test]) with Job(test_suites=[suite]) as j: sys.exit(j.run()) diff --git a/examples/nrunner/recipes/runnable/exec_test_sleep_3.json b/examples/nrunner/recipes/runnable/exec_test_sleep_3.json index 23113758ba..b49f0521bc 100644 --- a/examples/nrunner/recipes/runnable/exec_test_sleep_3.json +++ b/examples/nrunner/recipes/runnable/exec_test_sleep_3.json @@ -1 +1 @@ -{"kind": "exec-test", "uri": "/bin/sleep", "args": ["3"]} +{"kind": "exec-test", "uri": "/bin/sleep", "identifier": "sleep-test", "args": ["3"]} diff --git a/examples/nrunner/recipes/runnable/identifier.json b/examples/nrunner/recipes/runnable/identifier.json new file mode 100644 index 0000000000..5f820bf454 --- /dev/null +++ b/examples/nrunner/recipes/runnable/identifier.json @@ -0,0 +1 @@ +{"kind": "noop", "identifier": "test-noop"} diff --git a/examples/nrunner/recipes/runnables/true_false.json b/examples/nrunner/recipes/runnables/true_false.json index 82f1650e60..eba3d44274 100644 --- a/examples/nrunner/recipes/runnables/true_false.json +++ b/examples/nrunner/recipes/runnables/true_false.json @@ -1,7 +1,9 @@ [ {"kind": "exec-test", - "uri": "/bin/true"}, + "uri": "/bin/true", + "identifier": "true-test"}, {"kind": "exec-test", - "uri": "/bin/false"} + "uri": "/bin/false", + "identifier": "false-test"} ] diff --git a/selftests/check.py b/selftests/check.py index 2eccc9e621..a78a3df93c 100755 --- a/selftests/check.py +++ b/selftests/check.py @@ -27,7 +27,7 @@ "job-api-7": 1, "nrunner-interface": 70, "nrunner-requirement": 28, - "unit": 670, + "unit": 671, "jobs": 11, "functional-parallel": 307, "functional-serial": 7, diff --git a/selftests/functional/resolver.py b/selftests/functional/resolver.py index f5df67a449..b3a19bdea4 100644 --- a/selftests/functional/resolver.py +++ b/selftests/functional/resolver.py @@ -183,7 +183,7 @@ def test_runnables_recipe(self): ================== asset: 1 exec-test: 3 -noop: 1 +noop: 2 package: 1 python-unittest: 1 sysinfo: 1""" diff --git a/selftests/unit/runnable.py b/selftests/unit/runnable.py index aef40977d9..824ea0d333 100644 --- a/selftests/unit/runnable.py +++ b/selftests/unit/runnable.py @@ -83,6 +83,7 @@ def test_get_dict(self): "uri": "_uri_", "args": ("arg1", "arg2"), "config": {"runner.identifier_format": "{uri}"}, + "identifier": "_uri_", }, ) @@ -92,6 +93,7 @@ def test_get_json(self): '{"kind": "noop", ' '"uri": "_uri_", ' '"config": {"runner.identifier_format": "{uri}"}, ' + '"identifier": "_uri_", ' '"args": ["arg1", "arg2"]}' ) self.assertEqual(runnable.get_json(), expected) @@ -144,6 +146,19 @@ def test_config(self): runnable.config.get("runner.identifier_format"), "{uri}-{args[0]}" ) + def test_identifier(self): + open_mocked = unittest.mock.mock_open( + read_data=( + '{"kind": "exec-test", "uri": "/bin/sh", ' + '"args": ["/etc/profile"], ' + '"config": {"runner.identifier_format": "{uri}-{args[0]}"}, ' + '"identifier": "exec-test-1"}' + ) + ) + with unittest.mock.patch("avocado.core.nrunner.runnable.open", open_mocked): + runnable = Runnable.from_recipe("fake_path") + self.assertEqual(runnable.identifier, "exec-test-1") + class RunnableFromCommandLineArgs(unittest.TestCase): def test_noop(self): @@ -219,10 +234,11 @@ def test_runnable_to_recipe_uri(self): self.assertEqual(loaded_runnable.uri, "/bin/true") def test_runnable_to_recipe_args(self): - runnable = Runnable("exec-test", "/bin/sleep", "0.01") + runnable = Runnable("exec-test", "/bin/sleep", "0.01", identifier="exec-test-1") open_mocked = unittest.mock.mock_open(read_data=runnable.get_json()) with unittest.mock.patch("avocado.core.nrunner.runnable.open", open_mocked): loaded_runnable = Runnable.from_recipe("fake_path") self.assertEqual(loaded_runnable.kind, "exec-test") self.assertEqual(loaded_runnable.uri, "/bin/sleep") self.assertEqual(loaded_runnable.args, ("0.01",)) + self.assertEqual(loaded_runnable.identifier, "exec-test-1") From 331cd09048c7011766dba35862ccd782b16428cd Mon Sep 17 00:00:00 2001 From: Jan Richter Date: Mon, 5 Aug 2024 11:50:43 +0200 Subject: [PATCH 2/2] Indetifier usage in list After this commit the `avocado list` command will print runnable identifier instead of url and `avocado -V list` will add the identifier as another column in the list matrix. It will be helpful when users will define their own identifiers after change in e7228afad. Signed-off-by: Jan Richter --- avocado/plugins/list.py | 19 ++++++++++++++++--- selftests/functional/resolver.py | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/avocado/plugins/list.py b/avocado/plugins/list.py index da2a00517d..dace3fcbe5 100644 --- a/avocado/plugins/list.py +++ b/avocado/plugins/list.py @@ -52,7 +52,13 @@ def _prepare_matrix_for_display(matrix, verbose=False): type_label = TERM_SUPPORT.healthy_str(kind) if verbose: colored_matrix.append( - (type_label, item[1], item[2], _get_tags_as_string(item[3] or {})) + ( + type_label, + item[1], + item[2], + item[3], + _get_tags_as_string(item[4] or {}), + ) ) else: colored_matrix.append((type_label, item[1])) @@ -65,6 +71,7 @@ def _display(self, suite, matrix): header = ( TERM_SUPPORT.header_str("Type"), TERM_SUPPORT.header_str("Test"), + TERM_SUPPORT.header_str("Uri"), TERM_SUPPORT.header_str("Resolver"), TERM_SUPPORT.header_str("Tag(s)"), ) @@ -140,10 +147,16 @@ def _get_resolution_matrix(suite): if verbose: tags = runnable.tags or {} test_matrix.append( - (runnable.kind, runnable.uri, resolution.origin, tags) + ( + runnable.kind, + runnable.identifier, + runnable.uri, + resolution.origin, + tags, + ) ) else: - test_matrix.append((runnable.kind, runnable.uri)) + test_matrix.append((runnable.kind, runnable.identifier)) return test_matrix @staticmethod diff --git a/selftests/functional/resolver.py b/selftests/functional/resolver.py index b3a19bdea4..6b0eea5ee8 100644 --- a/selftests/functional/resolver.py +++ b/selftests/functional/resolver.py @@ -153,7 +153,7 @@ def test_runnable_recipe_origin(self): cmd_line = f"{AVOCADO} -V list {test_path}" result = process.run(cmd_line) self.assertIn( - b"python-unittest selftests/unit/test.py:TestClassTestUnit.test_long_name runnable-recipe\n", + b"python-unittest selftests/unit/test.py:TestClassTestUnit.test_long_name selftests/unit/test.py:TestClassTestUnit.test_long_name runnable-recipe\n", result.stdout, )