Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions tmt/package_managers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,16 @@ def check_presence(self, *installables: Installable) -> dict[Installable, bool]:

raise NotImplementedError

def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, decorate with @abc.abstractmethod to make clear to linters this is an abstract method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or not. After seeing the actual methods, how about moving the patterns into classes, making something like _PACKAGE_NAME_IN_PM_OUTPUT_PATTERNS = [...], and the default implementation of this method can do what all the methods do: iterate over a list of patterns, match, yield. They are really repeating the same set of lines, right? The only thing that's different is the actual set of patterns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to classes. Should the compiles themselves be also inside a class?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, why not?

class ApkEngine(PackageManagerEngine):
    install_command = Command('add')


    _engine_class = ApkEngine

    _PACKAGE_NAME_IN_PM_OUTPUT_PATTERNS = [
        re.compile(r'unable to locate package\s+([^\s]+)', re.IGNORECASE),
        re.compile(r'ERROR:\s+([^\s:]+):\s+No such package', re.IGNORECASE)
    ]

"""
Extract failed package names from package manager error output.
:param output: Error output (stdout or stderr) from the package manager.
:returns: An iterator of package names that failed to install.
"""

raise NotImplementedError

def install(
self,
*installables: Installable,
Expand Down
14 changes: 14 additions & 0 deletions tmt/package_managers/apk.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from collections.abc import Iterator
from typing import Optional, Union

import tmt.utils
Expand Down Expand Up @@ -34,6 +35,11 @@
}


# Compiled regex patterns for APK error messages
_UNABLE_TO_LOCATE_PATTERN = re.compile(r'unable to locate package\s+([^\s]+)', re.IGNORECASE)
_NO_SUCH_PACKAGE_PATTERN = re.compile(r'ERROR:\s+([^\s:]+):\s+No such package', re.IGNORECASE)


class ApkEngine(PackageManagerEngine):
install_command = Command('add')

Expand Down Expand Up @@ -189,3 +195,11 @@ def install_debuginfo(
options: Optional[Options] = None,
) -> CommandOutput:
raise tmt.utils.GeneralError("There is no support for debuginfo packages in apk.")

def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
"""
Extract failed package names from APK error output.
"""
for pattern in [_UNABLE_TO_LOCATE_PATTERN, _NO_SUCH_PACKAGE_PATTERN]:
for match in pattern.finditer(output):
yield match.group(1)
16 changes: 16 additions & 0 deletions tmt/package_managers/apt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from collections.abc import Iterator
from typing import Optional, Union

import tmt.utils
Expand Down Expand Up @@ -79,6 +80,13 @@
""" # noqa: E501


# Compiled regex patterns for APT error messages
_UNABLE_TO_LOCATE_PATTERN = re.compile(r'Unable to locate package\s+([^\s]+)', re.IGNORECASE)
_E_UNABLE_TO_LOCATE_PATTERN = re.compile(
r'E:\s+Unable to locate package\s+([^\s]+)', re.IGNORECASE
)


class AptEngine(PackageManagerEngine):
install_command = Command('install')

Expand Down Expand Up @@ -281,3 +289,11 @@ def install_debuginfo(
options: Optional[Options] = None,
) -> CommandOutput:
raise tmt.utils.GeneralError("There is no support for debuginfo packages in apt.")

def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
"""
Extract failed package names from APT error output.
"""
for pattern in [_UNABLE_TO_LOCATE_PATTERN, _E_UNABLE_TO_LOCATE_PATTERN]:
for match in pattern.finditer(output):
yield match.group(1)
21 changes: 21 additions & 0 deletions tmt/package_managers/dnf.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, Optional, cast

from tmt._compat.pathlib import Path
Expand Down Expand Up @@ -201,6 +202,13 @@ def create_repository(self, directory: Path) -> ShellScript:
return ShellScript(f"createrepo {directory}")


# Compiled regex patterns for DNF/YUM error messages
_NO_PACKAGE_PROVIDES_PATTERN = re.compile(r'no package provides\s+([^\s\n]+)', re.IGNORECASE)
_UNABLE_TO_FIND_MATCH_PATTERN = re.compile(r'Unable to find a match:\s+([^\s\n]+)', re.IGNORECASE)
_NO_MATCH_FOR_ARGUMENT_PATTERN = re.compile(r'No match for argument:\s+([^\s\n]+)', re.IGNORECASE)
_NO_PACKAGE_AVAILABLE_PATTERN = re.compile(r'No package\s+([^\s\n]+)\s+available', re.IGNORECASE)


# ignore[type-arg]: TypeVar in package manager registry annotations is
# puzzling for type checkers. And not a good idea in general, probably.
@provides_package_manager('dnf') # type: ignore[arg-type]
Expand Down Expand Up @@ -260,6 +268,19 @@ def check_presence(self, *installables: Installable) -> dict[Installable, bool]:

return results

def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
"""
Extract failed package names from DNF/YUM error output.
"""
for pattern in [
_NO_PACKAGE_PROVIDES_PATTERN,
_UNABLE_TO_FIND_MATCH_PATTERN,
_NO_MATCH_FOR_ARGUMENT_PATTERN,
_NO_PACKAGE_AVAILABLE_PATTERN,
]:
for match in pattern.finditer(output):
yield match.group(1)


class Dnf5Engine(DnfEngine):
_base_command = Command('dnf5')
Expand Down
20 changes: 20 additions & 0 deletions tmt/steps/discover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,26 @@ def required_tests(self) -> list[TestOrigin]:
]
return tests

@property
def dependencies_to_tests(self) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
"""
A tuple containing two dictionaries mapping dependencies to tests (required & recommended)
"""

required_dependencies_to_tests: dict[str, list[str]] = {}
recommended_dependencies_to_tests: dict[str, list[str]] = {}

for test_origin in self.tests(enabled=True):
test = test_origin.test
test_name = test.name
# Collect dependencies separately for required and recommended
for dependency in test.require:
required_dependencies_to_tests.setdefault(str(dependency), []).append(test_name)
for dependency in test.recommend:
recommended_dependencies_to_tests.setdefault(str(dependency), []).append(test_name)

return required_dependencies_to_tests, recommended_dependencies_to_tests

def load(self) -> None:
"""
Load step data from the workdir
Expand Down
Loading