From 8f23ffea83373bb25ff3a51bf959ecb73cf78750 Mon Sep 17 00:00:00 2001 From: Rainer Poisel Date: Sat, 17 Aug 2024 11:44:08 +0200 Subject: [PATCH 1/6] refactor: make CONSOLE logging level labgrid-specific MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add this logging level without changing Python’s internal logging module. This maintains architectural integrity and clarifies that the level is labgrid-specific. Signed-off-by: Rainer Poisel --- labgrid/logging.py | 12 ++++++------ labgrid/pytestplugin/hooks.py | 6 +++--- labgrid/remote/client.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/labgrid/logging.py b/labgrid/logging.py index a3807ecf1..3ce6cf8c5 100644 --- a/labgrid/logging.py +++ b/labgrid/logging.py @@ -17,9 +17,9 @@ def basicConfig(**kwargs): root.handlers[0].setFormatter(StepFormatter(indent=indent, parent=parent)) -logging.CONSOLE = logging.INFO - 5 -assert(logging.CONSOLE > logging.DEBUG) -logging.addLevelName(logging.CONSOLE, "CONSOLE") +CONSOLE = logging.INFO - 5 +assert(CONSOLE > logging.DEBUG) +logging.addLevelName(CONSOLE, "CONSOLE") # Use composition instead of inheritance class StepFormatter: @@ -106,11 +106,11 @@ def notify(self, event): for part in parts: data = self.vt100_replace_cr_nl(part) - logger.log(logging.CONSOLE, self._create_message(event, data), extra=extra) + logger.log(CONSOLE, self._create_message(event, data), extra=extra) elif state == "start" and step.args and "data" in step.args: data = self.vt100_replace_cr_nl(step.args["data"]) - logger.log(logging.CONSOLE, self._create_message(event, data), extra=extra) + logger.log(CONSOLE, self._create_message(event, data), extra=extra) def flush(self): if self.lastevent is None: @@ -122,7 +122,7 @@ def flush(self): for source, logger in self.loggers.items(): data = self.vt100_replace_cr_nl(self.bufs[source]) if data: - logger.log(logging.CONSOLE, self._create_message(self.lastevent, data), extra=extra) + logger.log(CONSOLE, self._create_message(self.lastevent, data), extra=extra) self.bufs[source] = b"" diff --git a/labgrid/pytestplugin/hooks.py b/labgrid/pytestplugin/hooks.py index f69507250..ebb35b8cc 100644 --- a/labgrid/pytestplugin/hooks.py +++ b/labgrid/pytestplugin/hooks.py @@ -6,7 +6,7 @@ from .. import Environment from ..consoleloggingreporter import ConsoleLoggingReporter from ..util.helper import processwrapper -from ..logging import StepFormatter, StepLogger +from ..logging import CONSOLE, StepFormatter, StepLogger LABGRID_ENV_KEY = pytest.StashKey[Environment]() @@ -37,13 +37,13 @@ def set_cli_log_level(level): if verbosity > 3: # enable with -vvvv set_cli_log_level(logging.DEBUG) elif verbosity > 2: # enable with -vvv - set_cli_log_level(logging.CONSOLE) + set_cli_log_level(CONSOLE) elif verbosity > 1: # enable with -vv set_cli_log_level(logging.INFO) def configure_pytest_logging(config, plugin): - plugin.log_cli_handler.formatter.add_color_level(logging.CONSOLE, "blue") + plugin.log_cli_handler.formatter.add_color_level(CONSOLE, "blue") plugin.log_cli_handler.setFormatter(StepFormatter( color=config.option.lg_colored_steps, parent=plugin.log_cli_handler.formatter, diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 5ab4f0683..a5feaa3c4 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -46,7 +46,7 @@ from ..util.proxy import proxymanager from ..util.helper import processwrapper from ..driver import Mode, ExecutionError -from ..logging import basicConfig, StepLogger +from ..logging import basicConfig, CONSOLE, StepLogger class Error(Exception): @@ -2041,7 +2041,7 @@ def main(): if args.verbose: logging.getLogger().setLevel(logging.INFO) if args.verbose > 1: - logging.getLogger().setLevel(logging.CONSOLE) + logging.getLogger().setLevel(CONSOLE) if args.debug or args.verbose > 2: logging.getLogger().setLevel(logging.DEBUG) From 2639dc5db82734556bada07ef43367e369c217ac Mon Sep 17 00:00:00 2001 From: Rainer Poisel Date: Sat, 17 Aug 2024 11:43:52 +0200 Subject: [PATCH 2/6] refactor: use typing hints where possible in pytest plugin Signed-off-by: Rainer Poisel --- labgrid/config.py | 5 +++-- labgrid/environment.py | 6 +++--- labgrid/logging.py | 2 +- labgrid/py.typed | 1 + labgrid/pytestplugin/__init__.py | 10 ++++++++++ labgrid/pytestplugin/fixtures.py | 21 ++++++++++++++------- labgrid/pytestplugin/hooks.py | 21 +++++++++++++++------ labgrid/pytestplugin/py.typed | 0 8 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 labgrid/py.typed create mode 100644 labgrid/pytestplugin/py.typed diff --git a/labgrid/config.py b/labgrid/config.py index 9b09dcb5e..8e40bcbf1 100644 --- a/labgrid/config.py +++ b/labgrid/config.py @@ -5,6 +5,7 @@ """ import os import warnings +from typing import Dict from yaml import YAMLError import attr @@ -262,7 +263,7 @@ def get_imports(self): return imports - def get_paths(self): + def get_paths(self) -> Dict[str, str]: """Helper function that returns the subdict of all paths Returns: @@ -275,7 +276,7 @@ def get_paths(self): return paths - def get_images(self): + def get_images(self) -> Dict[str, str]: """Helper function that returns the subdict of all images Returns: diff --git a/labgrid/environment.py b/labgrid/environment.py index abff96ce1..2e4271c0d 100644 --- a/labgrid/environment.py +++ b/labgrid/environment.py @@ -1,5 +1,5 @@ import os -from typing import Optional +from typing import Callable, Optional import attr from .target import Target @@ -9,10 +9,10 @@ @attr.s(eq=False) class Environment: """An environment encapsulates targets.""" - config_file = attr.ib( + config_file: str = attr.ib( default="config.yaml", validator=attr.validators.instance_of(str) ) - interact = attr.ib(default=input, repr=False) + interact: Callable[[str], str] = attr.ib(default=input, repr=False) def __attrs_post_init__(self): self.targets = {} diff --git a/labgrid/logging.py b/labgrid/logging.py index 3ce6cf8c5..3ae42b3c2 100644 --- a/labgrid/logging.py +++ b/labgrid/logging.py @@ -22,7 +22,7 @@ def basicConfig(**kwargs): logging.addLevelName(CONSOLE, "CONSOLE") # Use composition instead of inheritance -class StepFormatter: +class StepFormatter(logging.Formatter): def __init__(self, *args, indent=True, color=None, parent=None, **kwargs): self.formatter = parent or logging.Formatter(*args, **kwargs) self.indent = indent diff --git a/labgrid/py.typed b/labgrid/py.typed new file mode 100644 index 000000000..b648ac923 --- /dev/null +++ b/labgrid/py.typed @@ -0,0 +1 @@ +partial diff --git a/labgrid/pytestplugin/__init__.py b/labgrid/pytestplugin/__init__.py index 63324bdfc..a790be480 100644 --- a/labgrid/pytestplugin/__init__.py +++ b/labgrid/pytestplugin/__init__.py @@ -1,2 +1,12 @@ from .fixtures import pytest_addoption, env, target, strategy from .hooks import pytest_configure, pytest_collection_modifyitems, pytest_cmdline_main + +__all__ = [ + "pytest_addoption", + "env", + "target", + "strategy", + "pytest_configure", + "pytest_collection_modifyitems", + "pytest_cmdline_main", +] diff --git a/labgrid/pytestplugin/fixtures.py b/labgrid/pytestplugin/fixtures.py index f881377c3..e4365ab4d 100644 --- a/labgrid/pytestplugin/fixtures.py +++ b/labgrid/pytestplugin/fixtures.py @@ -1,8 +1,12 @@ import os import subprocess +from typing import Iterator + import pytest +from .. import Environment, Target from ..exceptions import NoResourceFoundError, NoDriverFoundError +from ..strategy import Strategy from ..remote.client import UserError from ..resource.remote import RemotePlace from ..util.ssh import sshmanager @@ -12,7 +16,9 @@ # pylint: disable=redefined-outer-name -def pytest_addoption(parser): +def pytest_addoption(parser: pytest.Parser, pluginmanager: pytest.PytestPluginManager) -> None: + del pluginmanager # unused + group = parser.getgroup('labgrid') group.addoption( '--env-config', @@ -57,7 +63,7 @@ def pytest_addoption(parser): @pytest.fixture(scope="session") -def env(request, record_testsuite_property): +def env(request: pytest.FixtureRequest, record_testsuite_property) -> Iterator[Environment]: """Return the environment configured in the supplied configuration file. It contains the targets contained in the configuration file. """ @@ -74,7 +80,8 @@ def env(request, record_testsuite_property): try: target = env.get_target(target_name) except UserError as e: - pytest.exit(e) + pytest.exit(str(e)) + assert target, "could not get target from environment" try: remote_place = target.get_resource(RemotePlace, wait_avail=False) remote_name = remote_place.name @@ -117,7 +124,7 @@ def env(request, record_testsuite_property): @pytest.fixture(scope="session") -def target(env): +def target(env: Environment) -> Target: """Return the default target `main` configured in the supplied configuration file.""" target = env.get_target() @@ -128,13 +135,13 @@ def target(env): @pytest.fixture(scope="session") -def strategy(request, target): +def strategy(request: pytest.FixtureRequest, target: Target) -> Strategy: """Return the Strategy of the default target `main` configured in the supplied configuration file.""" try: - strategy = target.get_driver("Strategy") + strategy: Strategy = target.get_driver("Strategy") except NoDriverFoundError as e: - pytest.exit(e) + pytest.exit(str(e)) state = request.config.option.lg_initial_state if state is not None: diff --git a/labgrid/pytestplugin/hooks.py b/labgrid/pytestplugin/hooks.py index ebb35b8cc..4fa7c09a9 100644 --- a/labgrid/pytestplugin/hooks.py +++ b/labgrid/pytestplugin/hooks.py @@ -1,19 +1,22 @@ import os import warnings import logging +from typing import List, Optional + import pytest +from _pytest.logging import ColoredLevelFormatter, LoggingPlugin from .. import Environment from ..consoleloggingreporter import ConsoleLoggingReporter from ..util.helper import processwrapper from ..logging import CONSOLE, StepFormatter, StepLogger -LABGRID_ENV_KEY = pytest.StashKey[Environment]() +LABGRID_ENV_KEY = pytest.StashKey[Optional[Environment]]() @pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config): - def set_cli_log_level(level): +def pytest_cmdline_main(config: pytest.Config): + def set_cli_log_level(level: int): nonlocal config try: @@ -27,6 +30,7 @@ def set_cli_log_level(level): current_level = int(logging.getLevelName(current_level)) except ValueError: current_level = None + assert current_level is None or isinstance(current_level, int), "unexpected type of current log level" # If no level was set previously (via ini or cli) or current_level is # less verbose than level, set to new level. @@ -34,6 +38,7 @@ def set_cli_log_level(level): config.option.log_cli_level = str(level) verbosity = config.getoption("verbose") + assert isinstance(verbosity, int), "unexpected verbosity option type" if verbosity > 3: # enable with -vvvv set_cli_log_level(logging.DEBUG) elif verbosity > 2: # enable with -vvv @@ -42,7 +47,8 @@ def set_cli_log_level(level): set_cli_log_level(logging.INFO) -def configure_pytest_logging(config, plugin): +def configure_pytest_logging(config: pytest.Config, plugin: LoggingPlugin): + assert isinstance(plugin.log_cli_handler.formatter, ColoredLevelFormatter), "unexpected type of log_cli_handler.formatter" plugin.log_cli_handler.formatter.add_color_level(CONSOLE, "blue") plugin.log_cli_handler.setFormatter(StepFormatter( color=config.option.lg_colored_steps, @@ -61,12 +67,13 @@ def configure_pytest_logging(config, plugin): plugin.report_handler.setFormatter(StepFormatter(parent=caplog_formatter)) @pytest.hookimpl(trylast=True) -def pytest_configure(config): +def pytest_configure(config: pytest.Config): StepLogger.start() config.add_cleanup(StepLogger.stop) logging_plugin = config.pluginmanager.getplugin('logging-plugin') if logging_plugin: + assert isinstance(logging_plugin, LoggingPlugin), "unexpected type of logging-plugin" configure_pytest_logging(config, logging_plugin) config.addinivalue_line("markers", @@ -97,9 +104,11 @@ def pytest_configure(config): processwrapper.enable_logging() @pytest.hookimpl() -def pytest_collection_modifyitems(config, items): +def pytest_collection_modifyitems(session: pytest.Session, config: pytest.Config, items: List[pytest.Item]): """This function matches function feature flags with those found in the environment and disables the item if no match is found""" + del session # unused + env = config.stash[LABGRID_ENV_KEY] if not env: diff --git a/labgrid/pytestplugin/py.typed b/labgrid/pytestplugin/py.typed new file mode 100644 index 000000000..e69de29bb From 50fdc83f2924f20db7975c235d7cef13ee0a908a Mon Sep 17 00:00:00 2001 From: Rainer Poisel Date: Sun, 18 Aug 2024 14:25:57 +0200 Subject: [PATCH 3/6] chore: ruff format and lint test_fixtures.py Signed-off-by: Rainer Poisel --- tests/test_fixtures.py | 44 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 4ed9fba39..ea22662e1 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,12 +1,14 @@ import os + import pexpect import pytest + @pytest.fixture def short_env(tmpdir): p = tmpdir.join("config.yaml") p.write( -""" + """ targets: test1: drivers: {} @@ -17,73 +19,83 @@ def short_env(tmpdir): ) return p + @pytest.fixture def short_test(tmpdir): t = tmpdir.join("test.py") t.write( -""" + """ def test(env): assert True """ ) return t + def test_config(short_test): - with pexpect.spawn(f'pytest --traceconfig {short_test}') as spawn: + with pexpect.spawn(f"pytest --traceconfig {short_test}") as spawn: spawn.expect(pexpect.EOF) - assert b'labgrid.pytestplugin' in spawn.before + assert b"labgrid.pytestplugin" in spawn.before spawn.close() assert spawn.exitstatus == 0 + def test_env_fixture(short_env, short_test): - with pexpect.spawn(f'pytest --lg-env {short_env} {short_test}') as spawn: + with pexpect.spawn(f"pytest --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 + def test_env_fixture_no_logging(short_env, short_test): - with pexpect.spawn(f'pytest -p no:logging --lg-env {short_env} {short_test}') as spawn: + with pexpect.spawn(f"pytest -p no:logging --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0, spawn.before + def test_env_old_fixture(short_env, short_test): - with pexpect.spawn(f'pytest --env-config {short_env} {short_test}') as spawn: + with pexpect.spawn(f"pytest --env-config {short_env} {short_test}") as spawn: spawn.expect("deprecated option --env-config") spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 + def test_env_env_fixture(short_env, short_test): - env=os.environ.copy() - env['LG_ENV'] = short_env - with pexpect.spawn(f'pytest {short_test}') as spawn: + env = os.environ.copy() + env["LG_ENV"] = short_env + with pexpect.spawn(f"pytest {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 + def test_env_with_junit(short_env, short_test, tmpdir): - x = tmpdir.join('junit.xml') - with pexpect.spawn(f'pytest --junitxml={x} --lg-env {short_env} {short_test}') as spawn: + x = tmpdir.join("junit.xml") + with pexpect.spawn(f"pytest --junitxml={x} --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 + def test_help(short_test): - with pexpect.spawn(f'pytest --help {short_test}') as spawn: + with pexpect.spawn(f"pytest --help {short_test}") as spawn: spawn.expect(pexpect.EOF) - assert b'--lg-coordinator=COORDINATOR_ADDRESS' in spawn.before + assert b"--lg-coordinator=COORDINATOR_ADDRESS" in spawn.before spawn.close() assert spawn.exitstatus == 0 + def test_help_coordinator(short_test): - with pexpect.spawn(f'pytest --lg-coordinator=127.0.0.1:20408 --help {short_test}') as spawn: + with pexpect.spawn(f"pytest --lg-coordinator=127.0.0.1:20408 --help {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 + def test_log_without_capturing(short_env, short_test, tmpdir): - with pexpect.spawn(f'pytest -vv -s --lg-env {short_env} {short_test}') as spawn: + with pexpect.spawn(f"pytest -vv -s --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() print(spawn.before) From fb978620df8ec3db8dd40a17df95b30230c68cac Mon Sep 17 00:00:00 2001 From: Rainer Poisel Date: Sun, 18 Aug 2024 15:26:22 +0200 Subject: [PATCH 4/6] test: add typing hints to fixtures tests Signed-off-by: Rainer Poisel --- tests/test_fixtures.py | 45 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index ea22662e1..7793618c2 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,38 +1,37 @@ import os +import pathlib import pexpect import pytest @pytest.fixture -def short_env(tmpdir): - p = tmpdir.join("config.yaml") - p.write( - """ +def short_env(tmp_path: pathlib.Path) -> pathlib.Path: + p = tmp_path / "config.yaml" + p.write_text(""" targets: test1: drivers: {} test2: role: foo resources: {} -""" - ) + main: + drivers: {} +""") return p @pytest.fixture -def short_test(tmpdir): - t = tmpdir.join("test.py") - t.write( - """ +def short_test(tmp_path: pathlib.Path) -> pathlib.Path: + t = tmp_path / "test.py" + t.write_text(""" def test(env): assert True -""" - ) +""") return t -def test_config(short_test): +def test_config(short_test: pathlib.Path) -> None: with pexpect.spawn(f"pytest --traceconfig {short_test}") as spawn: spawn.expect(pexpect.EOF) assert b"labgrid.pytestplugin" in spawn.before @@ -40,21 +39,21 @@ def test_config(short_test): assert spawn.exitstatus == 0 -def test_env_fixture(short_env, short_test): +def test_env_fixture(short_env: pathlib.Path, short_test: pathlib.Path) -> None: with pexpect.spawn(f"pytest --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 -def test_env_fixture_no_logging(short_env, short_test): +def test_env_fixture_no_logging(short_env: pathlib.Path, short_test: pathlib.Path) -> None: with pexpect.spawn(f"pytest -p no:logging --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0, spawn.before -def test_env_old_fixture(short_env, short_test): +def test_env_old_fixture(short_env: pathlib.Path, short_test: pathlib.Path) -> None: with pexpect.spawn(f"pytest --env-config {short_env} {short_test}") as spawn: spawn.expect("deprecated option --env-config") spawn.expect(pexpect.EOF) @@ -62,24 +61,24 @@ def test_env_old_fixture(short_env, short_test): assert spawn.exitstatus == 0 -def test_env_env_fixture(short_env, short_test): +def test_env_env_fixture(short_env: pathlib.Path, short_test: pathlib.Path) -> None: env = os.environ.copy() - env["LG_ENV"] = short_env + env["LG_ENV"] = str(short_env) with pexpect.spawn(f"pytest {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 -def test_env_with_junit(short_env, short_test, tmpdir): - x = tmpdir.join("junit.xml") +def test_env_with_junit(short_env: pathlib.Path, short_test: pathlib.Path, tmp_path: pathlib.Path) -> None: + x = tmp_path / "junit.xml" with pexpect.spawn(f"pytest --junitxml={x} --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 -def test_help(short_test): +def test_help(short_test: pathlib.Path) -> None: with pexpect.spawn(f"pytest --help {short_test}") as spawn: spawn.expect(pexpect.EOF) assert b"--lg-coordinator=COORDINATOR_ADDRESS" in spawn.before @@ -87,14 +86,14 @@ def test_help(short_test): assert spawn.exitstatus == 0 -def test_help_coordinator(short_test): +def test_help_coordinator(short_test: pathlib.Path) -> None: with pexpect.spawn(f"pytest --lg-coordinator=127.0.0.1:20408 --help {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0 -def test_log_without_capturing(short_env, short_test, tmpdir): +def test_log_without_capturing(short_env: pathlib.Path, short_test: pathlib.Path) -> None: with pexpect.spawn(f"pytest -vv -s --lg-env {short_env} {short_test}") as spawn: spawn.expect(pexpect.EOF) spawn.close() From 9fcd05e97f19e374726b5538634c773a4482b7bb Mon Sep 17 00:00:00 2001 From: Rainer Poisel Date: Sun, 18 Aug 2024 15:36:03 +0200 Subject: [PATCH 5/6] docs: update CHANGES.rst file to mention typing hints in pytest components Signed-off-by: Rainer Poisel --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index b2c4ce84c..b0cc008db 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,7 @@ more performant and the import times are shorter. New Features in 24.1 ~~~~~~~~~~~~~~~~~~~~ - All components can be installed into the same virtualenv again. +- The ``pytest`` specific parts of the labgrid framework now have typing hints. Bug fixes in 24.1 ~~~~~~~~~~~~~~~~~ From 200d9150ff83918445a02f6f383e98a4af716a73 Mon Sep 17 00:00:00 2001 From: Rainer Poisel Date: Sun, 18 Aug 2024 15:30:10 +0200 Subject: [PATCH 6/6] test: add pytester based tests for the pytest labgrid fixtures Signed-off-by: Rainer Poisel --- tests/conftest.py | 2 + tests/test_fixtures.py | 156 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index b56b212fa..24b633e0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,8 @@ psutil = pytest.importorskip("psutil") +pytest_plugins = ["pytester"] + @pytest.fixture(scope="session") def curses_init(): """ curses only reads the terminfo DB once on the first import, so make diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 7793618c2..679fdcdc9 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,5 +1,7 @@ import os import pathlib +from dataclasses import dataclass, field +from typing import Dict, List, Optional import pexpect import pytest @@ -99,3 +101,157 @@ def test_log_without_capturing(short_env: pathlib.Path, short_test: pathlib.Path spawn.close() print(spawn.before) assert spawn.exitstatus == 0 + + +@dataclass +class Scenario: + config: Optional[str] + test: str + exitcode: pytest.ExitCode + lines: List[str] = field(default_factory=list) + outcome: Dict[str, int] = field(default_factory=dict) + + +COMPLETE_CONFIG = """ +targets: + main: + resources: + RawSerialPort: + port: '/dev/ttyUSB0' + drivers: + ManualPowerDriver: {} + SerialDriver: {} + BareboxDriver: {} + ShellDriver: + prompt: 'root@\\w+:[^ ]+ ' + login_prompt: ' login: ' + username: 'root' + BareboxStrategy: {} + test1: + drivers: {} + test2: + resources: {} +""" + + +@pytest.mark.parametrize( + "scenario", + [ + Scenario( + config=COMPLETE_CONFIG, + test=""" +import pytest +from labgrid import Environment, Target + + +def test_env_fixture(env: Environment) -> None: + assert (target1 := env.get_target('test1')) + assert isinstance(target1, Target) + assert env.get_target('test2') + assert not env.get_target('test3') +""", + exitcode=pytest.ExitCode.OK, + outcome={"passed": 1}, + ), + Scenario( + config=COMPLETE_CONFIG, + test=""" +import pytest +from labgrid import Target + + +def test_target_fixture(target: Target) -> None: + assert target +""", + exitcode=pytest.ExitCode.OK, + outcome={"passed": 1}, + ), + Scenario( + config=COMPLETE_CONFIG, + test=""" +import pytest +from labgrid.strategy import Strategy + + +def test_strategy_fixture(strategy: Strategy) -> None: + assert strategy +""", + exitcode=pytest.ExitCode.OK, + outcome={"passed": 1}, + ), + Scenario( + config=None, + test=""" +import pytest +from labgrid import Environment, Target +from labgrid.strategy import Strategy + + +def test_env_fixture(env: Environment) -> None: + del env # unused +""", + exitcode=pytest.ExitCode.OK, + lines=["*SKIPPED*missing environment config*", "*1 skipped*"], + ), + Scenario( + config=""" +targets: + test1: + drivers: {} +""", + test=""" +import pytest +from labgrid import Target + + +def test_target_fixture(target: Target) -> None: + assert target +""", + exitcode=pytest.ExitCode.TESTS_FAILED, + lines=["*UserError*Using target fixture without*", "*ERROR*"], + ), + Scenario( + config=""" +targets: + main: + drivers: {} +""", + test=""" +import pytest +from labgrid.strategy import Strategy + + +def test_strategy_fixture(strategy: Strategy) -> None: + assert strategy +""", + exitcode=pytest.ExitCode.INTERRUPTED, + lines=["*no Strategy driver found in Target*", "*no tests ran*"], + ), + ], +) +def test_fixtures_with_pytester(pytester: pytest.Pytester, scenario: Scenario) -> None: + pytester.makefile( + ".ini", + pytest=f"""[pytest] +python_files = test_*.py +python_functions = test_* +python_classes = Test* *_test +addopts = --strict-markers +testpaths = . +""", + ) + + if scenario.config: + pytester.makefile(".yaml", config=scenario.config) + + pytester.makepyfile(test_sample=scenario.test) + + args = ["-s", "-vvv"] + if scenario.config: + args += ["--lg-env", "config.yaml"] + result = pytester.runpytest(*args) + assert result.ret == scenario.exitcode + if scenario.lines: + result.stdout.fnmatch_lines(scenario.lines) + if scenario.outcome: + result.assert_outcomes(**scenario.outcome)