relenv 0.21.2__py3-none-any.whl → 0.22.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- relenv/__init__.py +14 -2
- relenv/__main__.py +12 -6
- relenv/_resources/xz/config.h +148 -0
- relenv/_resources/xz/readme.md +4 -0
- relenv/build/__init__.py +28 -30
- relenv/build/common/__init__.py +50 -0
- relenv/build/common/_sysconfigdata_template.py +72 -0
- relenv/build/common/builder.py +907 -0
- relenv/build/common/builders.py +163 -0
- relenv/build/common/download.py +324 -0
- relenv/build/common/install.py +609 -0
- relenv/build/common/ui.py +432 -0
- relenv/build/darwin.py +128 -14
- relenv/build/linux.py +292 -74
- relenv/build/windows.py +123 -169
- relenv/buildenv.py +48 -17
- relenv/check.py +10 -5
- relenv/common.py +492 -165
- relenv/create.py +147 -7
- relenv/fetch.py +16 -4
- relenv/manifest.py +15 -7
- relenv/python-versions.json +350 -0
- relenv/pyversions.py +817 -30
- relenv/relocate.py +101 -55
- relenv/runtime.py +457 -282
- relenv/toolchain.py +9 -3
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/METADATA +1 -1
- relenv-0.22.1.dist-info/RECORD +48 -0
- tests/__init__.py +2 -0
- tests/_pytest_typing.py +45 -0
- tests/conftest.py +42 -36
- tests/test_build.py +426 -9
- tests/test_common.py +373 -48
- tests/test_create.py +149 -6
- tests/test_downloads.py +19 -15
- tests/test_fips_photon.py +6 -3
- tests/test_module_imports.py +44 -0
- tests/test_pyversions_runtime.py +177 -0
- tests/test_relocate.py +45 -39
- tests/test_relocate_module.py +257 -0
- tests/test_runtime.py +1968 -6
- tests/test_verify_build.py +477 -34
- relenv/build/common.py +0 -1707
- relenv-0.21.2.dist-info/RECORD +0 -35
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/WHEEL +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/entry_points.txt +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/licenses/LICENSE.md +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/licenses/NOTICE +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.1.dist-info}/top_level.txt +0 -0
tests/test_build.py
CHANGED
|
@@ -1,27 +1,45 @@
|
|
|
1
1
|
# Copyright 2022-2025 Broadcom.
|
|
2
|
-
# SPDX-License-Identifier: Apache-2
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import pathlib
|
|
4
6
|
|
|
5
7
|
import pytest
|
|
6
8
|
|
|
7
|
-
from relenv.build.common import
|
|
8
|
-
from relenv.common import
|
|
9
|
+
from relenv.build.common import Dirs, get_dependency_version
|
|
10
|
+
from relenv.build.common.builder import Builder
|
|
11
|
+
from relenv.build.common.download import Download, verify_checksum
|
|
12
|
+
from relenv.build.common.ui import (
|
|
13
|
+
BuildStats,
|
|
14
|
+
LineCountHandler,
|
|
15
|
+
load_build_stats,
|
|
16
|
+
save_build_stats,
|
|
17
|
+
update_build_stats,
|
|
18
|
+
)
|
|
19
|
+
from relenv.common import DATA_DIR, RelenvException, toolchain_root_dir, work_dirs
|
|
20
|
+
|
|
21
|
+
# mypy: ignore-errors
|
|
9
22
|
|
|
10
23
|
|
|
11
24
|
@pytest.fixture
|
|
12
|
-
def fake_download(tmp_path):
|
|
25
|
+
def fake_download(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
13
26
|
download = tmp_path / "fake_download"
|
|
14
27
|
download.write_text("This is some file contents")
|
|
15
28
|
return download
|
|
16
29
|
|
|
17
30
|
|
|
18
31
|
@pytest.fixture
|
|
19
|
-
def fake_download_md5(fake_download):
|
|
32
|
+
def fake_download_md5(fake_download: pathlib.Path) -> str:
|
|
20
33
|
return hashlib.sha1(fake_download.read_bytes()).hexdigest()
|
|
21
34
|
|
|
22
35
|
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def fake_download_sha256(fake_download: pathlib.Path) -> str:
|
|
38
|
+
return hashlib.sha256(fake_download.read_bytes()).hexdigest()
|
|
39
|
+
|
|
40
|
+
|
|
23
41
|
@pytest.mark.skip_unless_on_linux
|
|
24
|
-
def test_builder_defaults_linux():
|
|
42
|
+
def test_builder_defaults_linux() -> None:
|
|
25
43
|
builder = Builder(version="3.10.10")
|
|
26
44
|
assert builder.arch == "x86_64"
|
|
27
45
|
assert builder.arch == "x86_64"
|
|
@@ -29,15 +47,414 @@ def test_builder_defaults_linux():
|
|
|
29
47
|
assert builder.prefix == DATA_DIR / "build" / "3.10.10-x86_64-linux-gnu"
|
|
30
48
|
assert builder.sources == DATA_DIR / "src"
|
|
31
49
|
assert builder.downloads == DATA_DIR / "download"
|
|
32
|
-
assert
|
|
50
|
+
assert builder.toolchain == toolchain_root_dir() / builder.triplet
|
|
33
51
|
assert callable(builder.build_default)
|
|
34
52
|
assert callable(builder.populate_env)
|
|
35
53
|
assert builder.recipies == {}
|
|
36
54
|
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
@pytest.mark.skip_unless_on_linux
|
|
57
|
+
def test_builder_toolchain_lazy_loading(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
58
|
+
"""Test that toolchain is only fetched when accessed (lazy loading)."""
|
|
59
|
+
import relenv.common
|
|
60
|
+
|
|
61
|
+
call_count = {"count": 0}
|
|
62
|
+
|
|
63
|
+
def mock_get_toolchain(arch=None, root=None):
|
|
64
|
+
call_count["count"] += 1
|
|
65
|
+
# Return a fake path instead of actually extracting
|
|
66
|
+
return pathlib.Path(f"/fake/toolchain/{arch or 'default'}")
|
|
67
|
+
|
|
68
|
+
# Patch where get_toolchain is actually imported and used (in relenv.common)
|
|
69
|
+
monkeypatch.setattr(relenv.common, "get_toolchain", mock_get_toolchain)
|
|
70
|
+
|
|
71
|
+
# Create builder - should NOT call get_toolchain yet
|
|
72
|
+
builder = Builder(version="3.10.10", arch="aarch64")
|
|
73
|
+
assert call_count["count"] == 0, "get_toolchain should not be called during init"
|
|
74
|
+
|
|
75
|
+
# Access toolchain property - should call get_toolchain once
|
|
76
|
+
toolchain = builder.toolchain
|
|
77
|
+
assert (
|
|
78
|
+
call_count["count"] == 1
|
|
79
|
+
), "get_toolchain should be called when property is accessed"
|
|
80
|
+
assert toolchain == pathlib.Path("/fake/toolchain/aarch64")
|
|
81
|
+
|
|
82
|
+
# Access again - should use cached value, not call again
|
|
83
|
+
toolchain2 = builder.toolchain
|
|
84
|
+
assert call_count["count"] == 1, "get_toolchain should only be called once (cached)"
|
|
85
|
+
assert toolchain == toolchain2
|
|
86
|
+
|
|
87
|
+
# Change arch - should reset cache
|
|
88
|
+
builder.set_arch("x86_64")
|
|
89
|
+
assert builder._toolchain is None, "Changing arch should reset toolchain cache"
|
|
90
|
+
|
|
91
|
+
# Access after arch change - should call get_toolchain again
|
|
92
|
+
toolchain3 = builder.toolchain
|
|
93
|
+
assert (
|
|
94
|
+
call_count["count"] == 2
|
|
95
|
+
), "get_toolchain should be called again after arch change"
|
|
96
|
+
assert toolchain3 == pathlib.Path("/fake/toolchain/x86_64")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_verify_checksum(fake_download: pathlib.Path, fake_download_md5: str) -> None:
|
|
39
100
|
assert verify_checksum(fake_download, fake_download_md5) is True
|
|
40
101
|
|
|
41
102
|
|
|
42
|
-
def
|
|
103
|
+
def test_verify_checksum_sha256(
|
|
104
|
+
fake_download: pathlib.Path, fake_download_sha256: str
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Test SHA-256 checksum validation."""
|
|
107
|
+
assert verify_checksum(fake_download, fake_download_sha256) is True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_verify_checksum_failed(fake_download: pathlib.Path) -> None:
|
|
43
111
|
pytest.raises(RelenvException, verify_checksum, fake_download, "no")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_verify_checksum_none(fake_download: pathlib.Path) -> None:
|
|
115
|
+
"""Test that verify_checksum returns False when checksum is None."""
|
|
116
|
+
assert verify_checksum(fake_download, None) is False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_verify_checksum_invalid_length(fake_download: pathlib.Path) -> None:
|
|
120
|
+
"""Test that invalid checksum length raises error."""
|
|
121
|
+
with pytest.raises(RelenvException, match="Invalid checksum length"):
|
|
122
|
+
verify_checksum(fake_download, "abc123") # 6 chars, not 40 or 64
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_get_dependency_version_openssl_linux() -> None:
|
|
126
|
+
"""Test getting OpenSSL version for Linux platform."""
|
|
127
|
+
result = get_dependency_version("openssl", "linux")
|
|
128
|
+
assert result is not None
|
|
129
|
+
assert isinstance(result, dict)
|
|
130
|
+
assert "version" in result
|
|
131
|
+
assert "url" in result
|
|
132
|
+
assert "sha256" in result
|
|
133
|
+
assert isinstance(result["version"], str)
|
|
134
|
+
assert "openssl" in result["url"].lower()
|
|
135
|
+
assert "{version}" in result["url"]
|
|
136
|
+
assert isinstance(result["sha256"], str)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_get_dependency_version_sqlite_all_platforms() -> None:
|
|
140
|
+
"""Test getting SQLite version for various platforms."""
|
|
141
|
+
for platform in ["linux", "darwin", "win32"]:
|
|
142
|
+
result = get_dependency_version("sqlite", platform)
|
|
143
|
+
assert result is not None, f"SQLite should be available for {platform}"
|
|
144
|
+
assert isinstance(result, dict)
|
|
145
|
+
assert "version" in result
|
|
146
|
+
assert "url" in result
|
|
147
|
+
assert "sha256" in result
|
|
148
|
+
assert "sqliteversion" in result, "SQLite should have sqliteversion field"
|
|
149
|
+
assert isinstance(result["version"], str)
|
|
150
|
+
assert "sqlite" in result["url"].lower()
|
|
151
|
+
assert isinstance(result["sha256"], str)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_get_dependency_version_xz_all_platforms() -> None:
|
|
155
|
+
"""Test getting XZ version for various platforms."""
|
|
156
|
+
# XZ 5.5.0+ removed MSBuild support, so Windows uses a fallback version
|
|
157
|
+
# and XZ is not in JSON for win32
|
|
158
|
+
for platform in ["linux", "darwin"]:
|
|
159
|
+
result = get_dependency_version("xz", platform)
|
|
160
|
+
assert result is not None, f"XZ should be available for {platform}"
|
|
161
|
+
assert isinstance(result, dict)
|
|
162
|
+
assert "version" in result
|
|
163
|
+
assert "url" in result
|
|
164
|
+
assert "sha256" in result
|
|
165
|
+
assert isinstance(result["version"], str)
|
|
166
|
+
assert "xz" in result["url"].lower()
|
|
167
|
+
assert isinstance(result["sha256"], str)
|
|
168
|
+
|
|
169
|
+
# Windows should return None (uses hardcoded fallback in windows.py)
|
|
170
|
+
result = get_dependency_version("xz", "win32")
|
|
171
|
+
assert result is None, "XZ should not be in JSON for win32 (uses fallback)"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_get_dependency_version_nonexistent() -> None:
|
|
175
|
+
"""Test that nonexistent dependency returns None."""
|
|
176
|
+
result = get_dependency_version("nonexistent-dep", "linux")
|
|
177
|
+
assert result is None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_get_dependency_version_wrong_platform() -> None:
|
|
181
|
+
"""Test that requesting unsupported platform returns None."""
|
|
182
|
+
# Try to get OpenSSL for a platform that doesn't exist
|
|
183
|
+
result = get_dependency_version("openssl", "nonexistent-platform")
|
|
184
|
+
assert result is None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Build stats tests
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_build_stats_save_load(
|
|
191
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Test saving and loading build statistics."""
|
|
194
|
+
monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
|
|
195
|
+
|
|
196
|
+
# Save some stats
|
|
197
|
+
stats = {
|
|
198
|
+
"python": BuildStats(avg_lines=100, samples=1, last_lines=100),
|
|
199
|
+
"openssl": BuildStats(avg_lines=200, samples=2, last_lines=180),
|
|
200
|
+
}
|
|
201
|
+
save_build_stats(stats)
|
|
202
|
+
|
|
203
|
+
# Load them back
|
|
204
|
+
loaded = load_build_stats()
|
|
205
|
+
assert loaded["python"]["avg_lines"] == 100
|
|
206
|
+
assert loaded["python"]["samples"] == 1
|
|
207
|
+
assert loaded["python"]["last_lines"] == 100
|
|
208
|
+
assert loaded["openssl"]["avg_lines"] == 200
|
|
209
|
+
assert loaded["openssl"]["samples"] == 2
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_build_stats_load_nonexistent(
|
|
213
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Test loading stats when file doesn't exist returns empty dict."""
|
|
216
|
+
monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
|
|
217
|
+
loaded = load_build_stats()
|
|
218
|
+
assert loaded == {}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_build_stats_update_new_step(
|
|
222
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Test updating stats for a new build step."""
|
|
225
|
+
monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
|
|
226
|
+
|
|
227
|
+
# Update a new step
|
|
228
|
+
update_build_stats("python", 100)
|
|
229
|
+
|
|
230
|
+
# Load and verify
|
|
231
|
+
stats = load_build_stats()
|
|
232
|
+
assert stats["python"]["avg_lines"] == 100
|
|
233
|
+
assert stats["python"]["samples"] == 1
|
|
234
|
+
assert stats["python"]["last_lines"] == 100
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_build_stats_update_existing_step(
|
|
238
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Test updating stats for an existing step uses exponential moving average."""
|
|
241
|
+
monkeypatch.setattr("relenv.build.common.ui.DATA_DIR", tmp_path)
|
|
242
|
+
|
|
243
|
+
# Initial value
|
|
244
|
+
update_build_stats("python", 100)
|
|
245
|
+
|
|
246
|
+
# Update with new value
|
|
247
|
+
update_build_stats("python", 200)
|
|
248
|
+
|
|
249
|
+
# Load and verify exponential moving average: 0.7 * 200 + 0.3 * 100 = 170
|
|
250
|
+
stats = load_build_stats()
|
|
251
|
+
assert stats["python"]["avg_lines"] == 170
|
|
252
|
+
assert stats["python"]["samples"] == 2
|
|
253
|
+
assert stats["python"]["last_lines"] == 200
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# LineCountHandler tests
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_line_count_handler() -> None:
|
|
260
|
+
"""Test LineCountHandler increments shared dict correctly."""
|
|
261
|
+
shared_dict = {}
|
|
262
|
+
handler = LineCountHandler("test", shared_dict)
|
|
263
|
+
|
|
264
|
+
# Create a log record
|
|
265
|
+
record = logging.LogRecord(
|
|
266
|
+
name="test",
|
|
267
|
+
level=logging.INFO,
|
|
268
|
+
pathname="",
|
|
269
|
+
lineno=0,
|
|
270
|
+
msg="test message",
|
|
271
|
+
args=(),
|
|
272
|
+
exc_info=None,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Emit first record
|
|
276
|
+
handler.emit(record)
|
|
277
|
+
assert shared_dict["test"] == 1
|
|
278
|
+
|
|
279
|
+
# Emit second record
|
|
280
|
+
handler.emit(record)
|
|
281
|
+
assert shared_dict["test"] == 2
|
|
282
|
+
|
|
283
|
+
# Emit third record
|
|
284
|
+
handler.emit(record)
|
|
285
|
+
assert shared_dict["test"] == 3
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_line_count_handler_multiple_steps() -> None:
|
|
289
|
+
"""Test LineCountHandler tracks multiple steps independently."""
|
|
290
|
+
shared_dict = {}
|
|
291
|
+
handler1 = LineCountHandler("step1", shared_dict)
|
|
292
|
+
handler2 = LineCountHandler("step2", shared_dict)
|
|
293
|
+
|
|
294
|
+
record = logging.LogRecord(
|
|
295
|
+
name="test",
|
|
296
|
+
level=logging.INFO,
|
|
297
|
+
pathname="",
|
|
298
|
+
lineno=0,
|
|
299
|
+
msg="test",
|
|
300
|
+
args=(),
|
|
301
|
+
exc_info=None,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
handler1.emit(record)
|
|
305
|
+
handler1.emit(record)
|
|
306
|
+
handler2.emit(record)
|
|
307
|
+
|
|
308
|
+
assert shared_dict["step1"] == 2
|
|
309
|
+
assert shared_dict["step2"] == 1
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Dirs class tests
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@pytest.mark.skip_unless_on_linux
|
|
316
|
+
def test_dirs_initialization() -> None:
|
|
317
|
+
"""Test Dirs class initialization."""
|
|
318
|
+
dirs = Dirs(work_dirs(), "python", "x86_64", "3.10.0")
|
|
319
|
+
assert dirs.name == "python"
|
|
320
|
+
assert dirs.arch == "x86_64"
|
|
321
|
+
assert dirs.version == "3.10.0"
|
|
322
|
+
assert "python_build" in dirs.tmpbuild
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_dirs_triplet_darwin(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
326
|
+
"""Test Dirs._triplet property for darwin platform."""
|
|
327
|
+
monkeypatch.setattr("sys.platform", "darwin")
|
|
328
|
+
dirs = Dirs(work_dirs(), "test", "arm64", "3.10.0")
|
|
329
|
+
assert dirs._triplet == "arm64-macos"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_dirs_triplet_win32(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
333
|
+
"""Test Dirs._triplet property for win32 platform."""
|
|
334
|
+
monkeypatch.setattr("sys.platform", "win32")
|
|
335
|
+
dirs = Dirs(work_dirs(), "test", "amd64", "3.10.0")
|
|
336
|
+
assert dirs._triplet == "amd64-win"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@pytest.mark.skip_unless_on_linux
|
|
340
|
+
def test_dirs_triplet_linux() -> None:
|
|
341
|
+
"""Test Dirs._triplet property for linux platform."""
|
|
342
|
+
dirs = Dirs(work_dirs(), "test", "x86_64", "3.10.0")
|
|
343
|
+
assert dirs._triplet == "x86_64-linux-gnu"
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@pytest.mark.skip_unless_on_linux
|
|
347
|
+
def test_dirs_prefix() -> None:
|
|
348
|
+
"""Test Dirs.prefix property."""
|
|
349
|
+
dirs = Dirs(work_dirs(), "test", "x86_64", "3.10.0")
|
|
350
|
+
assert "3.10.0-x86_64-linux-gnu" in str(dirs.prefix)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@pytest.mark.skip_unless_on_linux
|
|
354
|
+
def test_dirs_to_dict() -> None:
|
|
355
|
+
"""Test Dirs.to_dict() method."""
|
|
356
|
+
dirs = Dirs(work_dirs(), "test", "x86_64", "3.10.0")
|
|
357
|
+
d = dirs.to_dict()
|
|
358
|
+
assert "root" in d
|
|
359
|
+
assert "prefix" in d
|
|
360
|
+
assert "downloads" in d
|
|
361
|
+
assert "logs" in d
|
|
362
|
+
assert "sources" in d
|
|
363
|
+
assert "build" in d
|
|
364
|
+
assert "toolchain" in d
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@pytest.mark.skip_unless_on_linux
|
|
368
|
+
def test_dirs_pickle() -> None:
|
|
369
|
+
"""Test Dirs serialization/deserialization."""
|
|
370
|
+
dirs = Dirs(work_dirs(), "python", "x86_64", "3.10.0")
|
|
371
|
+
|
|
372
|
+
# Get state
|
|
373
|
+
state = dirs.__getstate__()
|
|
374
|
+
assert state["name"] == "python"
|
|
375
|
+
assert state["arch"] == "x86_64"
|
|
376
|
+
|
|
377
|
+
# Create new instance and restore state
|
|
378
|
+
dirs2 = Dirs.__new__(Dirs)
|
|
379
|
+
dirs2.__setstate__(state)
|
|
380
|
+
assert dirs2.name == "python"
|
|
381
|
+
assert dirs2.arch == "x86_64"
|
|
382
|
+
assert dirs2.tmpbuild == dirs.tmpbuild
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Download class tests
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_download_copy() -> None:
|
|
389
|
+
"""Test Download.copy() creates independent copy."""
|
|
390
|
+
d1 = Download(
|
|
391
|
+
"test",
|
|
392
|
+
"http://example.com/{version}/test.tar.gz",
|
|
393
|
+
version="1.0.0",
|
|
394
|
+
checksum="abc123",
|
|
395
|
+
)
|
|
396
|
+
d2 = d1.copy()
|
|
397
|
+
|
|
398
|
+
# Verify copy has same values
|
|
399
|
+
assert d2.name == d1.name
|
|
400
|
+
assert d2.url_tpl == d1.url_tpl
|
|
401
|
+
assert d2.version == d1.version
|
|
402
|
+
assert d2.checksum == d1.checksum
|
|
403
|
+
|
|
404
|
+
# Verify it's a different object
|
|
405
|
+
assert d2 is not d1
|
|
406
|
+
|
|
407
|
+
# Verify modifying copy doesn't affect original
|
|
408
|
+
d2.version = "2.0.0"
|
|
409
|
+
assert d1.version == "1.0.0"
|
|
410
|
+
assert d2.version == "2.0.0"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_download_fallback_url() -> None:
|
|
414
|
+
"""Test Download.fallback_url property."""
|
|
415
|
+
d = Download(
|
|
416
|
+
"test",
|
|
417
|
+
"http://main.com/{version}/test.tar.gz",
|
|
418
|
+
fallback_url="http://backup.com/{version}/test.tar.gz",
|
|
419
|
+
version="1.0.0",
|
|
420
|
+
)
|
|
421
|
+
assert d.fallback_url == "http://backup.com/1.0.0/test.tar.gz"
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_download_no_fallback() -> None:
|
|
425
|
+
"""Test Download.fallback_url returns None when not configured."""
|
|
426
|
+
d = Download("test", "http://example.com/{version}/test.tar.gz", version="1.0.0")
|
|
427
|
+
assert d.fallback_url is None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_download_signature_url() -> None:
|
|
431
|
+
"""Test Download.signature_url property."""
|
|
432
|
+
d = Download(
|
|
433
|
+
"test",
|
|
434
|
+
"http://example.com/{version}/test.tar.gz",
|
|
435
|
+
signature="http://example.com/{version}/test.tar.gz.asc",
|
|
436
|
+
version="1.0.0",
|
|
437
|
+
)
|
|
438
|
+
assert d.signature_url == "http://example.com/1.0.0/test.tar.gz.asc"
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_download_signature_url_error() -> None:
|
|
442
|
+
"""Test Download.signature_url raises error when not configured."""
|
|
443
|
+
from relenv.common import ConfigurationError
|
|
444
|
+
|
|
445
|
+
d = Download("test", "http://example.com/test.tar.gz")
|
|
446
|
+
with pytest.raises(ConfigurationError, match="Signature template not configured"):
|
|
447
|
+
_ = d.signature_url
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def test_download_destination_setter() -> None:
|
|
451
|
+
"""Test Download.destination setter with None value."""
|
|
452
|
+
d = Download("test", "http://example.com/test.tar.gz")
|
|
453
|
+
|
|
454
|
+
# Set to a path
|
|
455
|
+
d.destination = "/tmp/downloads"
|
|
456
|
+
assert d.destination == pathlib.Path("/tmp/downloads")
|
|
457
|
+
|
|
458
|
+
# Set to None
|
|
459
|
+
d.destination = None
|
|
460
|
+
assert d.destination == pathlib.Path()
|