Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 79 additions & 7 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import functools
import importlib.metadata
import os
from collections.abc import Generator, Iterator, Mapping, Sequence
from collections.abc import Generator, Iterator, Mapping
from dataclasses import dataclass, field # noqa: TID251
from typing import IO, Any, Callable, Optional, Protocol, TypeVar, Union

import _pytest.monkeypatch
Expand All @@ -15,7 +16,7 @@
from tmt._compat.typing import ParamSpec
from tmt._compat.typing import TypeAlias as TypeAlias
from tmt.log import Logger as Logger
from tmt.utils import Command, Path, RawCommandElement
from tmt.utils import Command, Path, RawCommand, RawCommandElement

_CLICK_VERSION = tuple(int(_s) for _s in importlib.metadata.version('click').split('.'))

Expand Down Expand Up @@ -149,19 +150,70 @@ def __call__(
pass


@dataclass
class TmtCliOptions:
_root_options: RawCommand = field(default_factory=list)

def to_options(self) -> Iterator[RawCommandElement]:
yield from self._root_options


@dataclass
class TmtCliRunOptions(TmtCliOptions):
# Run level options
run_id: Optional[Path] = None

# Run subcommands
discover: Optional[RawCommand] = None
provision: Optional[RawCommand] = None
prepare: Optional[RawCommand] = None
execute: Optional[RawCommand] = None
report: Optional[RawCommand] = None
finish: Optional[RawCommand] = None
cleanup: Optional[RawCommand] = None
plans: Optional[RawCommand] = None
tests: Optional[RawCommand] = None
login: Optional[RawCommand] = None
reboot: Optional[RawCommand] = None

def to_options(self) -> Iterator[RawCommandElement]:
yield from super().to_options()
yield "run"
if self.run_id:
yield f"--id={self.run_id}"
for part in [
"discover",
"provision",
"prepare",
"execute",
"report",
"finish",
"cleanup",
"plans",
"tests",
"login",
"reboot",
]:
if (part_opts := getattr(self, part)) is not None:
assert isinstance(part_opts, list)
yield part
yield from part_opts


class CliRunner(click.testing.CliRunner):
options: Optional[TmtCliOptions] = None

def __init__(self) -> None:
if _CLICK_VERSION >= (8, 2, 0):
super().__init__(charset='utf-8', echo_stdin=False)

else:
super().__init__(charset='utf-8', echo_stdin=False, mix_stderr=False)

def invoke( # type: ignore[override]
def _invoke(
self,
*args: RawCommandElement,
*args: str,
command: Optional[click.BaseCommand] = None,
extra_tmt_options: Optional[Sequence[RawCommandElement]] = None,
input: Optional[Union[str, bytes, IO[Any]]] = None,
env: Optional[Mapping[str, Optional[str]]] = None,
catch_exceptions: bool = True,
Expand All @@ -173,11 +225,31 @@ def invoke( # type: ignore[override]
tmt.__main__.import_cli_commands()

command = command or tmt.cli._root.main
options = Command(*(*(extra_tmt_options or []), *args))

return super().invoke(
command,
args=options.to_popen(),
args=args,
input=input,
env=env,
catch_exceptions=catch_exceptions,
color=color,
**kwargs,
)

def invoke( # type: ignore[override]
self,
*args: RawCommandElement,
command: Optional[click.BaseCommand] = None,
input: Optional[Union[str, bytes, IO[Any]]] = None,
env: Optional[Mapping[str, Optional[str]]] = None,
catch_exceptions: bool = True,
color: bool = False,
**kwargs: Any,
) -> click.testing.Result:
options = Command(*(self.options.to_options() if self.options else []), *args)
return self._invoke(
*options.to_popen(),
command=command,
input=input,
env=env,
catch_exceptions=catch_exceptions,
Expand Down
52 changes: 33 additions & 19 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import functools
import os
import pathlib
import shutil
Expand All @@ -11,29 +10,39 @@
import fmf.base
import pytest

from tests import CliRunner, RunTmt
from tests import CliRunner, RunTmt, TmtCliOptions, TmtCliRunOptions
from tmt.base.plan import Plan
from tmt.log import Logger
from tmt.steps.provision import Provision
from tmt.steps.provision.podman import GuestContainer, PodmanGuestData
from tmt.utils import Path, RawCommand
from tmt.utils import Path

if TYPE_CHECKING:
from pytest_container.container import ContainerData


@pytest.fixture(name='_extra_tmt_options')
def _fixture_extra_tmt_options() -> RawCommand:
"""
Collection of extra options to pass to ``tmt`` command.
@pytest.fixture
def _cli_runner() -> CliRunner:
return CliRunner()

This fixture is not to be used directly in tests. It is used by
fixtures to collect additional command-line options for the main
command, ``tmt``: fixtures that do wish to enhance the command would
require this fixture, and add their options to the provided list.
"""

return []
@pytest.fixture
def _tmt_cli_options(_cli_runner: CliRunner) -> TmtCliOptions:
options = TmtCliOptions()
_cli_runner.options = options
return options


@pytest.fixture
def _tmt_cli_run_options(
_cli_runner: CliRunner, _tmt_cli_options: TmtCliOptions
) -> TmtCliRunOptions:
options = TmtCliRunOptions()
# Transform the _tmt_cli_options into a TmtCliRunOptions
# This may break if the fields _tmt_cli_options are not pointer-like attributes
options._root_options = _tmt_cli_options._root_options
_cli_runner.options = options
return options


@pytest.fixture(name='root_logger')
Expand All @@ -47,28 +56,33 @@ def fixture_root_logger(caplog: _pytest.logging.LogCaptureFixture) -> Logger:

@pytest.fixture(name='fmf_root')
def fixture_fmf_root(
_extra_tmt_options: RawCommand, request: _pytest.fixtures.FixtureRequest
_tmt_cli_options: TmtCliOptions, request: _pytest.fixtures.FixtureRequest
) -> Path:
assert isinstance(request.param, Path)

# Let the main tmt command know it's supposed to use the given fmf
# root...
_extra_tmt_options += ['-r', request.param]
_tmt_cli_options._root_options += ['-r', request.param]

# ... but also propagate the path to the test, just like fixtures do.
# The test might wish to work with the path as well.
return request.param


@pytest.fixture(name='run_id')
def fixture_run_id(_tmt_cli_run_options: TmtCliRunOptions, tmppath: Path) -> Path:
run_id = tmppath / "tmt_run"
_tmt_cli_run_options.run_id = run_id
return run_id


@pytest.fixture(name='run_tmt')
def fixture_run_tmt(
_extra_tmt_options: RawCommand, request: _pytest.fixtures.FixtureRequest
) -> RunTmt:
def fixture_run_tmt(_cli_runner: CliRunner) -> RunTmt:
"""
Invoke a ``tmt`` command with given options.
"""

return functools.partial(CliRunner().invoke, extra_tmt_options=_extra_tmt_options)
return _cli_runner.invoke


# Equivalent fixtures to `tmp_path_factory` and `tmp_path` recasting the paths
Expand Down
Loading