Skip to content

Commit b46f5c7

Browse files
committed
Used command output instead of exceptions
1 parent fe7bf3e commit b46f5c7

7 files changed

Lines changed: 193 additions & 76 deletions

File tree

tmt/package_managers/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,16 @@ def check_presence(self, *installables: Installable) -> dict[Installable, bool]:
270270

271271
raise NotImplementedError
272272

273+
def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
274+
"""
275+
Extract failed package names from package manager error output.
276+
277+
:param output: Error output (stdout or stderr) from the package manager.
278+
:returns: An iterator of package names that failed to install.
279+
"""
280+
281+
raise NotImplementedError
282+
273283
def install(
274284
self,
275285
*installables: Installable,

tmt/package_managers/apk.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from collections.abc import Iterator
23
from typing import Optional, Union
34

45
import tmt.utils
@@ -34,6 +35,11 @@
3435
}
3536

3637

38+
# Compiled regex patterns for APK error messages
39+
_UNABLE_TO_LOCATE_PATTERN = re.compile(r'unable to locate package\s+([^\s]+)', re.IGNORECASE)
40+
_NO_SUCH_PACKAGE_PATTERN = re.compile(r'ERROR:\s+([^\s:]+):\s+No such package', re.IGNORECASE)
41+
42+
3743
class ApkEngine(PackageManagerEngine):
3844
install_command = Command('add')
3945

@@ -189,3 +195,11 @@ def install_debuginfo(
189195
options: Optional[Options] = None,
190196
) -> CommandOutput:
191197
raise tmt.utils.GeneralError("There is no support for debuginfo packages in apk.")
198+
199+
def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
200+
"""
201+
Extract failed package names from APK error output.
202+
"""
203+
for pattern in [_UNABLE_TO_LOCATE_PATTERN, _NO_SUCH_PACKAGE_PATTERN]:
204+
for match in pattern.finditer(output):
205+
yield match.group(1)

tmt/package_managers/apt.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from collections.abc import Iterator
23
from typing import Optional, Union
34

45
import tmt.utils
@@ -79,6 +80,13 @@
7980
""" # noqa: E501
8081

8182

83+
# Compiled regex patterns for APT error messages
84+
_UNABLE_TO_LOCATE_PATTERN = re.compile(r'Unable to locate package\s+([^\s]+)', re.IGNORECASE)
85+
_E_UNABLE_TO_LOCATE_PATTERN = re.compile(
86+
r'E:\s+Unable to locate package\s+([^\s]+)', re.IGNORECASE
87+
)
88+
89+
8290
class AptEngine(PackageManagerEngine):
8391
install_command = Command('install')
8492

@@ -281,3 +289,11 @@ def install_debuginfo(
281289
options: Optional[Options] = None,
282290
) -> CommandOutput:
283291
raise tmt.utils.GeneralError("There is no support for debuginfo packages in apt.")
292+
293+
def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
294+
"""
295+
Extract failed package names from APT error output.
296+
"""
297+
for pattern in [_UNABLE_TO_LOCATE_PATTERN, _E_UNABLE_TO_LOCATE_PATTERN]:
298+
for match in pattern.finditer(output):
299+
yield match.group(1)

tmt/package_managers/dnf.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from collections.abc import Iterator
23
from typing import TYPE_CHECKING, Any, Optional, cast
34

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

203204

205+
# Compiled regex patterns for DNF/YUM error messages
206+
_NO_PACKAGE_PROVIDES_PATTERN = re.compile(r'no package provides\s+([^\s\n]+)', re.IGNORECASE)
207+
_UNABLE_TO_FIND_MATCH_PATTERN = re.compile(r'Unable to find a match:\s+([^\s\n]+)', re.IGNORECASE)
208+
_NO_MATCH_FOR_ARGUMENT_PATTERN = re.compile(r'No match for argument:\s+([^\s\n]+)', re.IGNORECASE)
209+
_NO_PACKAGE_AVAILABLE_PATTERN = re.compile(r'No package\s+([^\s\n]+)\s+available', re.IGNORECASE)
210+
211+
204212
# ignore[type-arg]: TypeVar in package manager registry annotations is
205213
# puzzling for type checkers. And not a good idea in general, probably.
206214
@provides_package_manager('dnf') # type: ignore[arg-type]
@@ -260,6 +268,19 @@ def check_presence(self, *installables: Installable) -> dict[Installable, bool]:
260268

261269
return results
262270

271+
def extract_package_name_from_package_manager_output(self, output: str) -> Iterator[str]:
272+
"""
273+
Extract failed package names from DNF/YUM error output.
274+
"""
275+
for pattern in [
276+
_NO_PACKAGE_PROVIDES_PATTERN,
277+
_UNABLE_TO_FIND_MATCH_PATTERN,
278+
_NO_MATCH_FOR_ARGUMENT_PATTERN,
279+
_NO_PACKAGE_AVAILABLE_PATTERN,
280+
]:
281+
for match in pattern.finditer(output):
282+
yield match.group(1)
283+
263284

264285
class Dnf5Engine(DnfEngine):
265286
_base_command = Command('dnf5')

tmt/steps/discover/__init__.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -281,21 +281,24 @@ def required_tests(self) -> list[TestOrigin]:
281281
return tests
282282

283283
@property
284-
def dependencies_to_tests(self) -> dict[str, list[str]]:
284+
def dependencies_to_tests(self) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
285285
"""
286-
Dictionary mapping dependencies to tests.
286+
A tuple containing two dictionaries mapping dependencies to tests (required & recommended)
287287
"""
288288

289-
dependencies_to_tests: dict[str, list[str]] = {}
289+
required_dependencies_to_tests: dict[str, list[str]] = {}
290+
recommended_dependencies_to_tests: dict[str, list[str]] = {}
290291

291292
for test_origin in self.tests(enabled=True):
292293
test = test_origin.test
293294
test_name = test.name
294-
# Collect all dependencies (required + recommended)
295-
for dependency in [*test.require, *test.recommend]:
296-
dependencies_to_tests.setdefault(str(dependency), []).append(test_name)
295+
# Collect dependencies separately for required and recommended
296+
for dependency in test.require:
297+
required_dependencies_to_tests.setdefault(str(dependency), []).append(test_name)
298+
for dependency in test.recommend:
299+
recommended_dependencies_to_tests.setdefault(str(dependency), []).append(test_name)
297300

298-
return dependencies_to_tests
301+
return required_dependencies_to_tests, recommended_dependencies_to_tests
299302

300303
def load(self) -> None:
301304
"""

tmt/steps/prepare/__init__.py

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -221,61 +221,6 @@ def summary(self) -> None:
221221
preparations = fmf.utils.listed(self.preparations_applied, 'preparation')
222222
self.info('summary', f'{preparations} applied', 'green', shift=1)
223223

224-
def _extract_failed_packages(self, exceptions: list[Exception]) -> set[str]:
225-
"""
226-
Extract package names from installation exceptions.
227-
228-
Returns a list of package names that failed to install.
229-
"""
230-
231-
failed_packages: set[str] = set()
232-
import re
233-
234-
for exc in exceptions:
235-
# Extract failed packages from RunError stderr/stdout
236-
if isinstance(exc, tmt.utils.RunError):
237-
error_text = ''
238-
if exc.stderr:
239-
error_text += exc.stderr + '\n'
240-
if exc.stdout:
241-
error_text += exc.stdout
242-
243-
if error_text:
244-
# dnf, dnf5, yum, apt, apk specific patterns
245-
patterns = [
246-
r'no package provides\s+([^\s\n]+)',
247-
r'Unable to find a match:\s+([^\s\n]+)',
248-
r'No match for argument:\s+([^\s\n]+)',
249-
r'No package\s+([^\s\n]+)\s+available',
250-
r'Unable to locate package\s+([^\s]+)',
251-
r'E:\s+Unable to locate package\s+([^\s]+)',
252-
]
253-
254-
for pattern in patterns:
255-
matches = re.findall(pattern, error_text, re.IGNORECASE)
256-
failed_packages.update(matches)
257-
258-
return failed_packages
259-
260-
def _show_failed_packages_with_tests(self, failed_packages: set[str]) -> None:
261-
"""
262-
Show failed packages and which tests require them.
263-
"""
264-
265-
# Get test dependencies from discover step
266-
dependencies_to_tests = self.plan.discover.dependencies_to_tests
267-
268-
# Show failed packages with their associated tests
269-
self.info('')
270-
self.info('failed packages', '', 'red', shift=1)
271-
272-
for failed_package in failed_packages:
273-
matching_tests = []
274-
if failed_package in dependencies_to_tests:
275-
matching_tests = dependencies_to_tests[failed_package]
276-
sorted_matching_tests = ', '.join(sorted(matching_tests))
277-
self.info(failed_package, f'required by {sorted_matching_tests}', color='red', shift=2)
278-
279224
def go(self, force: bool = False) -> None:
280225
"""
281226
Prepare the guests
@@ -553,9 +498,7 @@ def _record_exception(
553498
self._save_results(self.results)
554499

555500
if exceptions:
556-
failed_packages = self._extract_failed_packages(exceptions)
557-
if failed_packages:
558-
self._show_failed_packages_with_tests(failed_packages)
501+
# TODO: needs a better message...
559502
raise tmt.utils.PrepareError(
560503
'prepare step failed',
561504
causes=exceptions,

0 commit comments

Comments
 (0)