diff --git a/avocado/core/nrunner/runnable.py b/avocado/core/nrunner/runnable.py index 606330239e..7669f5835b 100644 --- a/avocado/core/nrunner/runnable.py +++ b/avocado/core/nrunner/runnable.py @@ -1,10 +1,12 @@ import base64 import collections +import copy import json import logging import os import subprocess import sys +import warnings import pkg_resources @@ -96,13 +98,14 @@ def __init__(self, kind, uri, *args, config=None, identifier=None, **kwargs): #: test or being the test, or an actual URI with multiple #: parts self.uri = uri + #: This attributes holds default configuration values that the + #: runner has determined that has interest in by setting it in + #: attr:`avocado.core.nrunner.runner.BaseRunner.CONFIGURATION_USED` + self._default_config = self.filter_runnable_config(kind, {}) #: This attributes holds configuration from Avocado proper #: that is passed to runners, as long as a runner declares #: its interest in using them with #: attr:`avocado.core.nrunner.runner.BaseRunner.CONFIGURATION_USED` - self._config = {} - if config is None: - config = self.filter_runnable_config(kind, {}) self.config = config or {} self.args = args self.tags = kwargs.pop("tags", None) @@ -183,7 +186,30 @@ def identifier(self): @property def config(self): - return self._config + if not self._config: + return self._default_config + config_with_defaults = copy.copy(self._default_config) + config_with_defaults.update(self._config) + return config_with_defaults + + @property + def default_config(self): + return self._default_config + + def _config_setter_warning(self, config, default_config=False): + configuration_used = Runnable.get_configuration_used_by_kind(self.kind) + if default_config: + if set(configuration_used) == (set(config.keys())): + return + else: + if set(config.keys()).issubset(set(configuration_used)): + return + LOG.warning( + "The runnable config should have only values " + "essential for its runner. In the next version of " + "avocado, this will raise a ValueError. Please " + "use avocado.core.nrunner.runnable.Runnable.filter_runnable_config" + ) @config.setter def config(self, config): @@ -196,22 +222,31 @@ def config(self, config): :param config: A config dict with new values for Runnable. :type config: dict """ - configuration_used = Runnable.get_configuration_used_by_kind(self.kind) - if not set(configuration_used).issubset(set(config.keys())): - LOG.warning( - "The runnable config should have only values " - "essential for its runner. In the next version of " - "avocado, this will raise a Value Error. Please " - "use avocado.core.nrunner.runnable.Runnable.filter_runnable_config " - "or avocado.core.nrunner.runnable.Runnable.from_avocado_config" - ) + self._config_setter_warning(config) self._config = config + @default_config.setter + def default_config(self, config): + """Sets the default config values based on the runnable kind. + + This is not avocado config, it is a runnable config which is a subset + of avocado config based on `STANDALONE_EXECUTABLE_CONFIG_USED` which + describes essential configuration values for each runner kind. + + These values are used as convenience if other values are not set + in the actual :attr:`config` itself. + + :param config: A config dict with default values for this Runnable. + :type config: dict + """ + self._config_setter_warning(config, True) + self._default_config = config + @classmethod def from_args(cls, args): """Returns a runnable from arguments""" decoded_args = [_arg_decode_base64(arg) for arg in args.get("arg", ())] - return cls.from_avocado_config( + return cls( args.get("kind"), args.get("uri"), *decoded_args, @@ -277,7 +312,7 @@ def from_dict(cls, recipe_dict): """ cls._validate_recipe(recipe_dict) config = ConfigDecoder.decode_set(recipe_dict.get("config", {})) - return cls.from_avocado_config( + return cls( recipe_dict.get("kind"), recipe_dict.get("uri"), *recipe_dict.get("args", ()), @@ -304,9 +339,11 @@ 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) + warnings.warn( + "from_avocado_config() is deprecated, please use the regular " + "class initialization as it has the same behavior.", + DeprecationWarning, + ) return cls(kind, uri, *args, config=config, identifier=identifier, **kwargs) @classmethod @@ -327,7 +364,7 @@ def get_configuration_used_by_kind(cls, kind): if command is not None: command = " ".join(command) configuration_used = STANDALONE_EXECUTABLE_CONFIG_USED.get(command) - return configuration_used + return configuration_used + CONFIGURATION_USED @classmethod def filter_runnable_config(cls, kind, config): @@ -349,7 +386,7 @@ def filter_runnable_config(cls, kind, config): """ whole_config = settings.as_dict() filtered_config = {} - config_items = cls.get_configuration_used_by_kind(kind) + CONFIGURATION_USED + config_items = cls.get_configuration_used_by_kind(kind) for config_item in config_items: filtered_config[config_item] = config.get( config_item, whole_config.get(config_item) diff --git a/avocado/core/suite.py b/avocado/core/suite.py index 88949f526d..286c532406 100644 --- a/avocado/core/suite.py +++ b/avocado/core/suite.py @@ -76,7 +76,9 @@ def resolutions_to_runnables(resolutions, config): if resolution.result != ReferenceResolutionResult.SUCCESS: continue for runnable in resolution.resolutions: - runnable.config = runnable.filter_runnable_config(runnable.kind, config) + runnable.default_config = runnable.filter_runnable_config( + runnable.kind, config + ) result.append(runnable) return result diff --git a/avocado/plugins/sysinfo.py b/avocado/plugins/sysinfo.py index 515d7e4469..a6ee797cfc 100644 --- a/avocado/plugins/sysinfo.py +++ b/avocado/plugins/sysinfo.py @@ -220,7 +220,7 @@ def pre_test_runnables(self, test_runnable, suite_config=None): return [] sysinfo_config = sysinfo.gather_collectibles_config(suite_config) return [ - Runnable.from_avocado_config( + Runnable( "sysinfo", "pre", config=suite_config, @@ -237,7 +237,7 @@ def post_test_runnables(self, test_runnable, suite_config=None): sysinfo_config = sysinfo.gather_collectibles_config(suite_config) return [ ( - Runnable.from_avocado_config( + Runnable( "sysinfo", "post", config=suite_config, @@ -249,7 +249,7 @@ def post_test_runnables(self, test_runnable, suite_config=None): ["pass"], ), ( - Runnable.from_avocado_config( + Runnable( "sysinfo", "post", config=suite_config, diff --git a/examples/nrunner/recipes/runnable/noop_config.json b/examples/nrunner/recipes/runnable/noop_config.json new file mode 100644 index 0000000000..009060233a --- /dev/null +++ b/examples/nrunner/recipes/runnable/noop_config.json @@ -0,0 +1 @@ +{"kind": "noop", "uri": "noop", "config": {"runner.identifier_format": "nothing-op"}} diff --git a/selftests/check.py b/selftests/check.py index 52368ce2ca..ea81932dfa 100755 --- a/selftests/check.py +++ b/selftests/check.py @@ -27,7 +27,7 @@ "job-api-7": 1, "nrunner-interface": 70, "nrunner-requirement": 28, - "unit": 672, + "unit": 678, "jobs": 11, "functional-parallel": 307, "functional-serial": 7, diff --git a/selftests/functional/resolver.py b/selftests/functional/resolver.py index 6b0eea5ee8..f9b64449db 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: 2 +noop: 3 package: 1 python-unittest: 1 sysinfo: 1""" diff --git a/selftests/unit/runnable.py b/selftests/unit/runnable.py index 824ea0d333..303dce5354 100644 --- a/selftests/unit/runnable.py +++ b/selftests/unit/runnable.py @@ -1,5 +1,6 @@ import unittest.mock +import avocado.core.nrunner.runnable as runnable_mod from avocado.core.nrunner.runnable import Runnable @@ -146,6 +147,44 @@ def test_config(self): runnable.config.get("runner.identifier_format"), "{uri}-{args[0]}" ) + def test_config_at_init(self): + runnable = Runnable("exec-test", "/bin/sh") + self.assertEqual( + set(runnable.config.keys()), + set( + [ + "run.keep_tmp", + "runner.exectest.exitcodes.skip", + "runner.exectest.clear_env", + "runner.identifier_format", + ] + ), + ) + + def test_config_warn_if_not_used(self): + runnable = Runnable("exec-test", "/bin/sh") + with unittest.mock.patch.object(runnable_mod.LOG, "warning") as log_mock: + runnable.config = {"some-unused-config": "foo"} + log_mock.assert_called_once() + + def test_config_dont_warn_if_used(self): + with unittest.mock.patch.object(runnable_mod.LOG, "warning") as log_mock: + Runnable("noop", "noop", config={"runner.identifier_format": "noop"}) + log_mock.assert_not_called() + + def test_default_config(self): + runnable = Runnable("noop", "noop") + self.assertEqual( + runnable.default_config.get("runner.identifier_format"), "{uri}" + ) + + def test_default_and_actual_config(self): + runnable = Runnable("noop", "noop", config={"runner.identifier_format": "noop"}) + self.assertEqual(runnable.config.get("runner.identifier_format"), "noop") + self.assertEqual( + runnable.default_config.get("runner.identifier_format"), "{uri}" + ) + def test_identifier(self): open_mocked = unittest.mock.mock_open( read_data=( diff --git a/selftests/unit/suite.py b/selftests/unit/suite.py index 51c967e8b9..af91128439 100644 --- a/selftests/unit/suite.py +++ b/selftests/unit/suite.py @@ -85,6 +85,17 @@ def test_config_runnable(self): runnable = suite.tests[0] self.assertEqual(runnable.config.get("runner.identifier_format"), "NOT FOO") + def test_config_runnable_and_suite(self): + config = { + "resolver.references": [ + "examples/nrunner/recipes/runnable/noop_config.json", + ], + "runner.identifier_format": "NOT FOO", + } + suite = TestSuite.from_config(config) + runnable = suite.tests[0] + self.assertEqual(runnable.config.get("runner.identifier_format"), "nothing-op") + def tearDown(self): self.tmpdir.cleanup()