Skip to content

Commit 08ffd0d

Browse files
committed
persist repository metadata in artifacts.yaml
1 parent 98dbf50 commit 08ffd0d

File tree

6 files changed

+99
-23
lines changed

6 files changed

+99
-23
lines changed

tests/unit/artifact/test_copr_repository.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,31 @@ def test_fetch_contents_enables_repository(
4848
mock_from_file.assert_called_once()
4949
assert result == []
5050
assert provider.repository is mock_repo
51+
52+
53+
def test_enumerate_artifacts(mock_copr, mock_package_manager, artifact_provider, tmppath):
54+
mock_repo = MagicMock()
55+
mock_repo.repo_ids = ['group_teemtee-stable-fedora-42-x86_64']
56+
mock_guest = MagicMock()
57+
mock_guest.package_manager = mock_package_manager
58+
59+
mock_package_manager.list_packages.return_value = [
60+
'tmt-1.69.0-1.fc42.noarch',
61+
'tmt-all-0:1.69.0-1.fc42.noarch',
62+
]
63+
64+
provider = artifact_provider("copr.repository:@teemtee/stable")
65+
66+
with patch(
67+
'tmt.steps.prepare.artifact.providers.copr_repository.Repository.from_file_path',
68+
return_value=mock_repo,
69+
):
70+
provider.fetch_contents(mock_guest, tmppath / "artifacts")
71+
72+
provider.enumerate_artifacts(mock_guest)
73+
74+
assert len(provider.artifacts) == 2
75+
assert provider.artifacts[0].version.name == 'tmt'
76+
assert provider.artifacts[0].version.version == '1.69.0'
77+
assert provider.artifacts[1].version.name == 'tmt-all'
78+
assert provider.artifacts[1].version.epoch == 0

tmt/steps/prepare/artifact/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ def go(
265265
guest.package_manager.install_repository(repo)
266266
logger.debug(f"Installed repository '{repo.name}'.")
267267

268+
# Enumerate artifacts from installed repositories.
269+
for provider in providers:
270+
provider.enumerate_artifacts(guest)
271+
268272
# Persist artifact metadata to YAML
269273
self._save_artifacts_metadata(providers)
270274

tmt/steps/prepare/artifact/providers/__init__.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import configparser
66
from abc import ABC, abstractmethod
77
from collections.abc import Iterator, Sequence
8-
from functools import cached_property
98
from re import Pattern
109
from shlex import quote
1110
from typing import Any, Optional
@@ -86,6 +85,32 @@ def from_rpm_meta(cls, rpm_meta: dict[str, Any]) -> Self:
8685
epoch=rpm_meta.get("epoch", 0),
8786
)
8887

88+
@classmethod
89+
def from_nevra(
90+
cls, nevra: str
91+
) -> Self: # TODO: move this to `tmt.package_managers.PackageManager.list_packages`
92+
"""
93+
Version constructed from a NEVRA string as returned by ``dnf repoquery``.
94+
95+
Example usage:
96+
97+
.. code-block:: python
98+
99+
version_info = RpmVersion.from_nevra("curl-0:8.11.1-7.fc42.x86_64")
100+
version_info = RpmVersion.from_nevra("curl-8.11.1-7.fc42.x86_64")
101+
"""
102+
nvr_epoch, sep, arch = nevra.rpartition('.')
103+
if not sep:
104+
raise ValueError(f"Cannot parse arch from NEVRA '{nevra}'.")
105+
parts = nvr_epoch.rsplit('-', 2)
106+
if len(parts) != 3:
107+
raise ValueError(f"Cannot parse NVR from NEVRA '{nevra}'.")
108+
name, ev, release = parts
109+
epoch_str, sep, version = ev.partition(':')
110+
epoch = int(epoch_str) if sep else 0
111+
version = version if sep else epoch_str
112+
return cls(name=name, version=version, release=release, arch=arch, epoch=epoch)
113+
89114
@classmethod
90115
def from_filename(cls, filename: str) -> Self:
91116
"""
@@ -177,6 +202,7 @@ def __init__(self, raw_id: str, repository_priority: int, logger: tmt.log.Logger
177202
self.sanitized_id = tmt.utils.sanitize_name(raw_id, allow_slash=False)
178203

179204
self.id = self._extract_provider_id(raw_id)
205+
self._artifacts: list[ArtifactInfo] = []
180206

181207
@classmethod
182208
@abstractmethod
@@ -191,16 +217,12 @@ def _extract_provider_id(cls, raw_id: str) -> ArtifactProviderId:
191217

192218
raise NotImplementedError
193219

194-
@cached_property
220+
@property
195221
@abstractmethod
196222
def artifacts(self) -> Sequence[ArtifactInfo]:
197223
"""
198224
Collect all artifacts available from this provider.
199225
200-
The method is left for derived classes to implement with respect
201-
to the actual artifact provider they implement. The list of
202-
artifacts will be cached, and is treated as read-only.
203-
204226
:returns: a list of provided artifacts.
205227
"""
206228

@@ -300,6 +322,39 @@ def get_repositories(self) -> list['Repository']:
300322
"""
301323
return []
302324

325+
def enumerate_artifacts(
326+
self, guest: Guest
327+
) -> None: # TODO: refactor this once the NEVRA parsing is centralized.
328+
"""
329+
Enumerate artifacts available from this provider after its repositories
330+
have been installed on the guest.
331+
"""
332+
for repository in self.get_repositories():
333+
try:
334+
nevras = guest.package_manager.list_packages(repository)
335+
except Exception:
336+
self.logger.warning(
337+
f"Failed to enumerate packages from repository '{repository.name}'."
338+
)
339+
continue
340+
for nevra in nevras:
341+
nevra = nevra.strip()
342+
if not nevra:
343+
continue
344+
try:
345+
self._artifacts.append(
346+
ArtifactInfo(
347+
version=RpmVersion.from_nevra(nevra),
348+
provider=self,
349+
location=repository.name,
350+
)
351+
)
352+
except ValueError:
353+
self.logger.warning(f"Could not parse NEVRA '{nevra}'.")
354+
self.logger.debug(
355+
f"Enumerated {len(self._artifacts)} packages from repository '{repository.name}'."
356+
)
357+
303358
# B027: "... is an empty method in an abstract base class, but has
304359
# no abstract decorator" - expected, it's a default implementation
305360
# provided for subclasses. It is acceptable to do nothing.

tmt/steps/prepare/artifact/providers/copr_repository.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import re
66
from collections.abc import Sequence
7-
from functools import cached_property
87
from re import Pattern
98
from typing import Optional
109

@@ -91,11 +90,9 @@ def _extract_provider_id(cls, raw_id: str) -> ArtifactProviderId:
9190

9291
return value
9392

94-
@cached_property
93+
@property
9594
def artifacts(self) -> Sequence[ArtifactInfo]:
96-
# Copr repository provider does not enumerate individual artifacts.
97-
# The repository is enabled and packages are available through the package manager.
98-
return []
95+
return self._artifacts
9996

10097
def _download_artifact(self, artifact: ArtifactInfo, guest: Guest, destination: Path) -> None:
10198
"""This provider only enables repositories; it does not download individual RPMs."""

tmt/steps/prepare/artifact/providers/repository.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
from collections.abc import Sequence
6-
from functools import cached_property
76
from re import Pattern
87
from typing import Optional
98
from urllib.parse import urlparse
@@ -57,12 +56,9 @@ def _extract_provider_id(cls, raw_id: str) -> ArtifactProviderId:
5756
raise ValueError("Missing repository URL.")
5857
return value
5958

60-
@cached_property
59+
@property
6160
def artifacts(self) -> Sequence[ArtifactInfo]:
62-
# Repository provider does not enumerate individual artifacts.
63-
# The repository is installed and packages are available through the package manager.
64-
# There is no need to download individual artifact files.
65-
return []
61+
return self._artifacts
6662

6763
def _download_artifact(self, artifact: ArtifactInfo, guest: Guest, destination: Path) -> None:
6864
"""This provider only discovers repos; it does not download individual RPMs."""

tmt/steps/prepare/artifact/providers/repository_url.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
from collections.abc import Sequence
6-
from functools import cached_property
76
from re import Pattern
87
from typing import Optional
98

@@ -57,12 +56,9 @@ def _extract_provider_id(cls, raw_id: str) -> ArtifactProviderId:
5756
raise ValueError("Missing repository baseurl.")
5857
return value
5958

60-
@cached_property
59+
@property
6160
def artifacts(self) -> Sequence[ArtifactInfo]:
62-
# Repository provider does not enumerate individual artifacts.
63-
# The repository is installed and packages are available through the package manager.
64-
# There is no need to download individual artifact files.
65-
return []
61+
return self._artifacts
6662

6763
def _download_artifact(
6864
self, artifact: ArtifactInfo, guest: Guest, destination: tmt.utils.Path

0 commit comments

Comments
 (0)