Skip to content

Commit 2287b15

Browse files
authored
Implement Brew artifact provider (teemtee#4202)
resolves: teemtee#4174
1 parent 3767f44 commit 2287b15

File tree

3 files changed

+152
-3
lines changed

3 files changed

+152
-3
lines changed

tests/core/about/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export.story: dict json rst template yaml
88
export.test: dict json nitrate polarion template yaml
99
package_managers: apk apt bootc dnf dnf5 mock-dnf mock-dnf5 mock-yum rpm-ostree yum
1010
plan_shapers: max-tests repeat
11-
prepare.artifact.providers: brew koji koji.build koji.nvr koji.task
11+
prepare.artifact.providers: brew brew.build brew.nvr brew.task koji koji.build koji.nvr koji.task
1212
prepare.feature: crb epel fips profile
1313
step.cleanup: tmt
1414
step.discover: fmf shell

tests/unit/artifact/test_brew.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import pytest
2+
3+
from tmt.steps.prepare.artifact.providers.brew import BrewArtifactProvider
4+
5+
6+
@pytest.mark.skip(reason="would be replaced by mocks")
7+
@pytest.mark.integration
8+
def test_brew_valid_build(root_logger):
9+
provider = BrewArtifactProvider("brew.build:3866328", root_logger)
10+
assert len(provider.artifacts) == 21
11+
12+
13+
@pytest.mark.skip(reason="would be replaced by mocks")
14+
@pytest.mark.integration
15+
def test_brew_valid_draft_build(root_logger):
16+
provider = BrewArtifactProvider("brew.build:3525300", root_logger)
17+
assert len(provider.artifacts) == 2
18+
assert any(
19+
"draft_3525300" in artifact._raw_artifact['url'] for artifact in provider.artifacts
20+
), "No artifact URL contains 'draft_3525300'"
21+
22+
23+
@pytest.mark.skip(reason="would be replaced by mocks")
24+
@pytest.mark.integration
25+
def test_brew_valid_nvr(root_logger):
26+
provider = BrewArtifactProvider("brew.nvr:unixODBC-2.3.12-1.el9", root_logger)
27+
assert len(provider.artifacts) == 21
28+
assert provider.build_id == 3866328 # Known build ID for this NVR
29+
30+
31+
@pytest.mark.skip(reason="would be replaced by mocks")
32+
@pytest.mark.integration
33+
def test_brew_invalid_nvr(root_logger):
34+
from tmt.utils import GeneralError
35+
36+
provider = BrewArtifactProvider("brew.nvr:nonexistent-1.0-1.fc43", root_logger)
37+
38+
with pytest.raises(GeneralError, match=r"No build found for NVR 'nonexistent-1\.0-1\.fc43'\."):
39+
_ = provider.build_id
40+
41+
42+
@pytest.mark.skip(reason="would be replaced by mocks")
43+
@pytest.mark.integration
44+
def test_brew_valid_task_id_actual_build(root_logger):
45+
provider = BrewArtifactProvider("brew.task:69098388", root_logger)
46+
assert provider.build_id == 3866328 # Known build ID for this task
47+
assert len(provider.artifacts) == 21
48+
49+
50+
@pytest.mark.skip(reason="would be replaced by mocks")
51+
@pytest.mark.integration
52+
def test_brew_valid_task_id_scratch_build(root_logger):
53+
task_id = 69111304
54+
provider = BrewArtifactProvider(f"brew.task:{task_id}", root_logger)
55+
tasks = list(provider._get_task_children(task_id))
56+
57+
assert len(tasks) == 6
58+
assert task_id in tasks
59+
assert provider.build_id is None
60+
assert len(provider.artifacts) == 12

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

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,103 @@
22
Brew Artifact Provider
33
"""
44

5+
from collections.abc import Sequence
6+
from functools import cached_property
7+
from typing import Any, ClassVar, Optional
8+
from urllib.parse import urljoin
9+
10+
import tmt.log
511
from tmt.steps.prepare.artifact.providers import provides_artifact_provider
6-
from tmt.steps.prepare.artifact.providers.koji import KojiArtifactProvider
12+
from tmt.steps.prepare.artifact.providers.koji import (
13+
KojiArtifactProvider,
14+
KojiBuild,
15+
KojiNvr,
16+
KojiTask,
17+
)
718

819

920
# ignore[type-arg]: TypeVar in provider registry annotations is
1021
# puzzling for type checkers. And not a good idea in general, probably.
1122
@provides_artifact_provider('brew') # type: ignore[arg-type]
12-
class BrewProvider(KojiArtifactProvider):
23+
class BrewArtifactProvider(KojiArtifactProvider):
1324
"""
25+
Provider for downloading artifacts from Brew builds.
26+
1427
Brew builds are just a special case of Koji builds
28+
29+
.. note::
30+
31+
Only RPMs are supported currently.
32+
33+
.. code-block:: python
34+
35+
provider = BrewArtifactProvider("brew.build:123456", logger)
36+
artifacts = provider.fetch_contents(guest, Path("/tmp"))
1537
"""
38+
39+
SUPPORTED_PREFIXES: ClassVar[tuple[str, ...]] = ()
40+
41+
def __new__(cls, raw_provider_id: str, logger: tmt.log.Logger) -> Any:
42+
"""
43+
Create a specific Brew provider based on the ``raw_provider_id`` prefix.
44+
45+
The supported providers are:
46+
:py:class:`BrewBuild`,
47+
:py:class:`BrewTask`,
48+
:py:class:`BrewNvr`.
49+
50+
:raises ValueError: If the prefix is not supported
51+
"""
52+
return cls._dispatch_subclass(raw_provider_id, cls._REGISTRY)
53+
54+
def __init__(self, raw_provider_id: str, logger: tmt.log.Logger):
55+
super().__init__(raw_provider_id, logger)
56+
self._session = self._initialize_session(
57+
api_url="https://brewhub.engineering.redhat.com/brewhub",
58+
top_url="https://download.eng.bos.redhat.com/brew",
59+
)
60+
61+
@cached_property
62+
def build_provider(self) -> Optional['BrewBuild']:
63+
return self._make_build_provider(BrewBuild, "brew.build")
64+
65+
def _rpm_url(self, rpm_meta: dict[str, str]) -> str:
66+
"""Construct Brew RPM URL."""
67+
name = rpm_meta["name"]
68+
version = rpm_meta["version"]
69+
release = rpm_meta["release"]
70+
arch = rpm_meta["arch"]
71+
assert self.build_info is not None
72+
volume = self.build_info["volume_name"]
73+
draft_suffix = f",draft_{self.build_id}" if self.build_info["draft"] else ""
74+
75+
path = (
76+
f"vol/{volume}/packages/{name}/{version}/"
77+
f"{release}{draft_suffix}/{arch}/"
78+
f"{name}-{version}-{release}.{arch}.rpm"
79+
)
80+
81+
return urljoin(self._top_url, path)
82+
83+
84+
@provides_artifact_provider("brew.build") # type: ignore[arg-type]
85+
class BrewBuild(BrewArtifactProvider, KojiBuild):
86+
pass
87+
88+
89+
@provides_artifact_provider("brew.task") # type: ignore[arg-type]
90+
class BrewTask(BrewArtifactProvider, KojiTask):
91+
pass
92+
93+
94+
@provides_artifact_provider("brew.nvr") # type: ignore[arg-type]
95+
class BrewNvr(BrewArtifactProvider, KojiNvr):
96+
pass
97+
98+
99+
BrewArtifactProvider._REGISTRY = {
100+
"brew.build": BrewBuild,
101+
"brew.task": BrewTask,
102+
"brew.nvr": BrewNvr,
103+
}
104+
BrewArtifactProvider.SUPPORTED_PREFIXES = tuple(BrewArtifactProvider._REGISTRY.keys())

0 commit comments

Comments
 (0)