relenv 0.21.2__py3-none-any.whl → 0.22.0__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 +489 -165
- relenv/create.py +147 -7
- relenv/fetch.py +16 -4
- relenv/manifest.py +15 -7
- relenv/python-versions.json +329 -0
- relenv/pyversions.py +817 -30
- relenv/relocate.py +101 -55
- relenv/runtime.py +452 -282
- relenv/toolchain.py +9 -3
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/METADATA +1 -1
- relenv-0.22.0.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 +311 -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 +1802 -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.0.dist-info}/WHEEL +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
- {relenv-0.21.2.dist-info → relenv-0.22.0.dist-info}/top_level.txt +0 -0
tests/test_common.py
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
# Copyright 2022-2025 Broadcom.
|
|
2
|
-
# SPDX-License-Identifier: Apache-2
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import os
|
|
4
6
|
import pathlib
|
|
7
|
+
import pickle
|
|
5
8
|
import platform
|
|
6
9
|
import shutil
|
|
7
10
|
import subprocess
|
|
8
11
|
import sys
|
|
9
12
|
import tarfile
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
from typing import BinaryIO, Callable, Literal, Optional
|
|
10
15
|
from unittest.mock import patch
|
|
11
16
|
|
|
12
17
|
import pytest
|
|
13
18
|
|
|
19
|
+
import relenv.common
|
|
14
20
|
from relenv.common import (
|
|
15
21
|
MODULE_DIR,
|
|
16
22
|
SHEBANG_TPL_LINUX,
|
|
17
23
|
SHEBANG_TPL_MACOS,
|
|
18
24
|
RelenvException,
|
|
25
|
+
Version,
|
|
26
|
+
addpackage,
|
|
19
27
|
archived_build,
|
|
28
|
+
download_url,
|
|
20
29
|
extract_archive,
|
|
21
30
|
format_shebang,
|
|
22
31
|
get_download_location,
|
|
23
32
|
get_toolchain,
|
|
24
33
|
get_triplet,
|
|
34
|
+
list_archived_builds,
|
|
35
|
+
makepath,
|
|
25
36
|
relative_interpreter,
|
|
26
37
|
runcmd,
|
|
27
38
|
sanitize_sys_path,
|
|
@@ -29,21 +40,46 @@ from relenv.common import (
|
|
|
29
40
|
work_dirs,
|
|
30
41
|
work_root,
|
|
31
42
|
)
|
|
43
|
+
from tests._pytest_typing import mark_skipif, parametrize
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _mock_ppbt_module(
|
|
47
|
+
monkeypatch: pytest.MonkeyPatch, triplet: str, archive_path: pathlib.Path
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Provide a lightweight ppbt.common stub so get_toolchain() skips the real extraction.
|
|
51
|
+
"""
|
|
52
|
+
stub_package = ModuleType("ppbt")
|
|
53
|
+
stub_common = ModuleType("ppbt.common")
|
|
54
|
+
setattr(stub_package, "common", stub_common)
|
|
55
|
+
|
|
56
|
+
# pytest will clean these entries up automatically via monkeypatch
|
|
57
|
+
monkeypatch.setitem(sys.modules, "ppbt", stub_package)
|
|
58
|
+
monkeypatch.setitem(sys.modules, "ppbt.common", stub_common)
|
|
59
|
+
|
|
60
|
+
setattr(stub_common, "ARCHIVE", archive_path)
|
|
32
61
|
|
|
62
|
+
def fake_extract_archive(dest: str, archive: str) -> None:
|
|
63
|
+
dest_path = pathlib.Path(dest)
|
|
64
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
(dest_path / triplet).mkdir(parents=True, exist_ok=True)
|
|
33
66
|
|
|
34
|
-
|
|
67
|
+
setattr(stub_common, "extract_archive", fake_extract_archive)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_get_triplet_linux() -> None:
|
|
35
71
|
assert get_triplet("aarch64", "linux") == "aarch64-linux-gnu"
|
|
36
72
|
|
|
37
73
|
|
|
38
|
-
def test_get_triplet_darwin():
|
|
74
|
+
def test_get_triplet_darwin() -> None:
|
|
39
75
|
assert get_triplet("x86_64", "darwin") == "x86_64-macos"
|
|
40
76
|
|
|
41
77
|
|
|
42
|
-
def test_get_triplet_windows():
|
|
78
|
+
def test_get_triplet_windows() -> None:
|
|
43
79
|
assert get_triplet("amd64", "win32") == "amd64-win"
|
|
44
80
|
|
|
45
81
|
|
|
46
|
-
def test_get_triplet_default():
|
|
82
|
+
def test_get_triplet_default() -> None:
|
|
47
83
|
machine = platform.machine().lower()
|
|
48
84
|
plat = sys.platform
|
|
49
85
|
if plat == "win32":
|
|
@@ -56,12 +92,12 @@ def test_get_triplet_default():
|
|
|
56
92
|
pytest.fail(f"Do not know how to test for '{plat}' platform")
|
|
57
93
|
|
|
58
94
|
|
|
59
|
-
def test_get_triplet_unknown():
|
|
95
|
+
def test_get_triplet_unknown() -> None:
|
|
60
96
|
with pytest.raises(RelenvException):
|
|
61
97
|
get_triplet("aarch64", "oijfsdf")
|
|
62
98
|
|
|
63
99
|
|
|
64
|
-
def test_archived_build():
|
|
100
|
+
def test_archived_build() -> None:
|
|
65
101
|
dirs = work_dirs()
|
|
66
102
|
build = archived_build()
|
|
67
103
|
try:
|
|
@@ -70,23 +106,23 @@ def test_archived_build():
|
|
|
70
106
|
pytest.fail("Archived build value not relative to build dir")
|
|
71
107
|
|
|
72
108
|
|
|
73
|
-
def test_work_root_when_passed_relative_path():
|
|
109
|
+
def test_work_root_when_passed_relative_path() -> None:
|
|
74
110
|
name = "foo"
|
|
75
111
|
assert work_root(name) == pathlib.Path(name).resolve()
|
|
76
112
|
|
|
77
113
|
|
|
78
|
-
def test_work_root_when_passed_full_path():
|
|
114
|
+
def test_work_root_when_passed_full_path() -> None:
|
|
79
115
|
name = "/foo/bar"
|
|
80
116
|
if sys.platform == "win32":
|
|
81
117
|
name = "D:/foo/bar"
|
|
82
118
|
assert work_root(name) == pathlib.Path(name)
|
|
83
119
|
|
|
84
120
|
|
|
85
|
-
def test_work_root_when_nothing_passed():
|
|
121
|
+
def test_work_root_when_nothing_passed() -> None:
|
|
86
122
|
assert work_root() == MODULE_DIR
|
|
87
123
|
|
|
88
124
|
|
|
89
|
-
def test_work_dirs_attributes():
|
|
125
|
+
def test_work_dirs_attributes() -> None:
|
|
90
126
|
dirs = work_dirs()
|
|
91
127
|
checkfor = [
|
|
92
128
|
"root",
|
|
@@ -100,118 +136,215 @@ def test_work_dirs_attributes():
|
|
|
100
136
|
assert hasattr(dirs, attr)
|
|
101
137
|
|
|
102
138
|
|
|
103
|
-
def test_runcmd_success():
|
|
139
|
+
def test_runcmd_success() -> None:
|
|
104
140
|
ret = runcmd(["echo", "foo"])
|
|
105
141
|
assert ret.returncode == 0
|
|
106
142
|
|
|
107
143
|
|
|
108
|
-
def test_runcmd_fail():
|
|
144
|
+
def test_runcmd_fail() -> None:
|
|
109
145
|
with pytest.raises(RelenvException):
|
|
110
146
|
runcmd([sys.executable, "-c", "import sys;sys.exit(1)"])
|
|
111
147
|
|
|
112
148
|
|
|
113
|
-
def test_work_dir_with_root_module_dir():
|
|
149
|
+
def test_work_dir_with_root_module_dir() -> None:
|
|
114
150
|
ret = work_dir("fakedir")
|
|
115
151
|
assert ret == MODULE_DIR / "_fakedir"
|
|
116
152
|
|
|
117
153
|
|
|
118
|
-
def test_work_dir_with_root_given(tmp_path):
|
|
154
|
+
def test_work_dir_with_root_given(tmp_path: pathlib.Path) -> None:
|
|
119
155
|
ret = work_dir("fakedir", root=tmp_path)
|
|
120
156
|
assert ret == tmp_path / "fakedir"
|
|
121
157
|
|
|
122
158
|
|
|
123
|
-
def test_get_toolchain(tmp_path):
|
|
159
|
+
def test_get_toolchain(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
124
160
|
data_dir = tmp_path / "data"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
triplet = "aarch64-linux-gnu"
|
|
162
|
+
monkeypatch.setattr(relenv.common, "DATA_DIR", data_dir, raising=False)
|
|
163
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
164
|
+
monkeypatch.setattr(
|
|
165
|
+
relenv.common, "get_triplet", lambda machine=None, plat=None: triplet
|
|
166
|
+
)
|
|
167
|
+
monkeypatch.setenv("RELENV_TOOLCHAIN_CACHE", str(data_dir / "toolchain"))
|
|
168
|
+
archive_path = tmp_path / "dummy-toolchain.tar.xz"
|
|
169
|
+
archive_path.write_bytes(b"")
|
|
170
|
+
_mock_ppbt_module(monkeypatch, triplet, archive_path)
|
|
171
|
+
ret = get_toolchain(arch="aarch64")
|
|
172
|
+
assert ret == data_dir / "toolchain" / triplet
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_get_toolchain_linux_existing(tmp_path: pathlib.Path) -> None:
|
|
134
176
|
data_dir = tmp_path / "data"
|
|
135
|
-
|
|
177
|
+
triplet = "x86_64-linux-gnu"
|
|
178
|
+
toolchain_path = data_dir / "toolchain" / triplet
|
|
179
|
+
toolchain_path.mkdir(parents=True)
|
|
180
|
+
with patch("relenv.common.DATA_DIR", data_dir), patch(
|
|
181
|
+
"sys.platform", "linux"
|
|
182
|
+
), patch("relenv.common.get_triplet", return_value=triplet), patch.dict(
|
|
183
|
+
os.environ,
|
|
184
|
+
{"RELENV_TOOLCHAIN_CACHE": str(data_dir / "toolchain")},
|
|
185
|
+
):
|
|
136
186
|
ret = get_toolchain()
|
|
137
|
-
|
|
138
|
-
assert "data" in str(ret)
|
|
139
|
-
else:
|
|
140
|
-
assert f"{data_dir}/toolchain" in str(ret)
|
|
187
|
+
assert ret == toolchain_path
|
|
141
188
|
|
|
142
189
|
|
|
143
|
-
|
|
144
|
-
|
|
190
|
+
def test_get_toolchain_no_arch(
|
|
191
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
192
|
+
) -> None:
|
|
193
|
+
data_dir = tmp_path / "data"
|
|
194
|
+
triplet = "x86_64-linux-gnu"
|
|
195
|
+
monkeypatch.setattr(relenv.common, "DATA_DIR", data_dir, raising=False)
|
|
196
|
+
monkeypatch.setattr(sys, "platform", "linux")
|
|
197
|
+
monkeypatch.setattr(
|
|
198
|
+
relenv.common, "get_triplet", lambda machine=None, plat=None: triplet
|
|
199
|
+
)
|
|
200
|
+
monkeypatch.setenv("RELENV_TOOLCHAIN_CACHE", str(data_dir / "toolchain"))
|
|
201
|
+
archive_path = tmp_path / "dummy-toolchain.tar.xz"
|
|
202
|
+
archive_path.write_bytes(b"")
|
|
203
|
+
_mock_ppbt_module(monkeypatch, triplet, archive_path)
|
|
204
|
+
ret = get_toolchain()
|
|
205
|
+
assert ret == data_dir / "toolchain" / triplet
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
WriteMode = Literal["w:gz", "w:xz", "w:bz2", "w"]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@parametrize(
|
|
212
|
+
("suffix", "mode"),
|
|
213
|
+
(
|
|
214
|
+
(".tgz", "w:gz"),
|
|
215
|
+
(".tar.gz", "w:gz"),
|
|
216
|
+
(".tar.xz", "w:xz"),
|
|
217
|
+
(".tar.bz2", "w:bz2"),
|
|
218
|
+
(".tar", "w"),
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
def test_extract_archive(tmp_path: pathlib.Path, suffix: str, mode: WriteMode) -> None:
|
|
145
222
|
to_be_archived = tmp_path / "to_be_archived"
|
|
146
223
|
to_be_archived.mkdir()
|
|
147
224
|
test_file = to_be_archived / "testfile"
|
|
148
225
|
test_file.touch()
|
|
149
|
-
tar_file = tmp_path / "fake_archive"
|
|
226
|
+
tar_file = tmp_path / f"fake_archive{suffix}"
|
|
150
227
|
to_dir = tmp_path / "extracted"
|
|
151
|
-
with tarfile.open(str(tar_file),
|
|
228
|
+
with tarfile.open(str(tar_file), mode=mode) as tar:
|
|
152
229
|
tar.add(str(to_be_archived), to_be_archived.name)
|
|
153
230
|
extract_archive(str(to_dir), str(tar_file))
|
|
154
231
|
assert to_dir.exists()
|
|
155
232
|
assert (to_dir / to_be_archived.name / test_file.name) in to_dir.glob("**/*")
|
|
156
233
|
|
|
157
234
|
|
|
158
|
-
def test_get_download_location(tmp_path):
|
|
235
|
+
def test_get_download_location(tmp_path: pathlib.Path) -> None:
|
|
159
236
|
url = "https://test.com/1.0.0/test-1.0.0.tar.xz"
|
|
160
237
|
loc = get_download_location(url, str(tmp_path))
|
|
161
238
|
assert loc == str(tmp_path / "test-1.0.0.tar.xz")
|
|
162
239
|
|
|
163
240
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
241
|
+
def test_download_url_writes_file(tmp_path: pathlib.Path) -> None:
|
|
242
|
+
dest = tmp_path / "downloads"
|
|
243
|
+
dest.mkdir()
|
|
244
|
+
data = b"payload"
|
|
245
|
+
|
|
246
|
+
def fake_fetch(
|
|
247
|
+
url: str,
|
|
248
|
+
fp: BinaryIO,
|
|
249
|
+
backoff: int,
|
|
250
|
+
timeout: float,
|
|
251
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
fp.write(data)
|
|
254
|
+
|
|
255
|
+
with patch("relenv.common.fetch_url", side_effect=fake_fetch):
|
|
256
|
+
path = download_url("https://example.com/a.txt", dest)
|
|
257
|
+
|
|
258
|
+
assert pathlib.Path(path).read_bytes() == data
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_download_url_failure_cleans_up(tmp_path: pathlib.Path) -> None:
|
|
262
|
+
dest = tmp_path / "downloads"
|
|
263
|
+
dest.mkdir()
|
|
264
|
+
created = dest / "a.txt"
|
|
265
|
+
|
|
266
|
+
def fake_fetch(
|
|
267
|
+
url: str,
|
|
268
|
+
fp: BinaryIO,
|
|
269
|
+
backoff: int,
|
|
270
|
+
timeout: float,
|
|
271
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
272
|
+
) -> None:
|
|
273
|
+
raise RelenvException("fail")
|
|
274
|
+
|
|
275
|
+
with patch("relenv.common.get_download_location", return_value=str(created)), patch(
|
|
276
|
+
"relenv.common.fetch_url", side_effect=fake_fetch
|
|
277
|
+
), patch("relenv.common.log") as log_mock:
|
|
278
|
+
with pytest.raises(RelenvException):
|
|
279
|
+
download_url("https://example.com/a.txt", dest)
|
|
280
|
+
log_mock.error.assert_called()
|
|
281
|
+
assert not created.exists()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _extract_shell_snippet(tpl: str) -> str:
|
|
285
|
+
rendered = format_shebang("python3", tpl)
|
|
286
|
+
lines = rendered.splitlines()[1:] # skip #!/bin/sh
|
|
287
|
+
snippet: list[str] = []
|
|
288
|
+
for line in lines:
|
|
289
|
+
if line.startswith("'''"):
|
|
290
|
+
break
|
|
291
|
+
snippet.append(line)
|
|
292
|
+
return "\n".join(snippet)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@mark_skipif(shutil.which("shellcheck") is None, reason="Test needs shellcheck")
|
|
296
|
+
def test_shebang_tpl_linux() -> None:
|
|
297
|
+
sh = _extract_shell_snippet(SHEBANG_TPL_LINUX)
|
|
167
298
|
proc = subprocess.Popen(["shellcheck", "-s", "sh", "-"], stdin=subprocess.PIPE)
|
|
299
|
+
assert proc.stdin is not None
|
|
168
300
|
proc.stdin.write(sh.encode())
|
|
169
301
|
proc.communicate()
|
|
170
302
|
assert proc.returncode == 0
|
|
171
303
|
|
|
172
304
|
|
|
173
|
-
@
|
|
174
|
-
def test_shebang_tpl_macos():
|
|
175
|
-
sh =
|
|
305
|
+
@mark_skipif(shutil.which("shellcheck") is None, reason="Test needs shellcheck")
|
|
306
|
+
def test_shebang_tpl_macos() -> None:
|
|
307
|
+
sh = _extract_shell_snippet(SHEBANG_TPL_MACOS)
|
|
176
308
|
proc = subprocess.Popen(["shellcheck", "-s", "sh", "-"], stdin=subprocess.PIPE)
|
|
309
|
+
assert proc.stdin is not None
|
|
177
310
|
proc.stdin.write(sh.encode())
|
|
178
311
|
proc.communicate()
|
|
179
312
|
assert proc.returncode == 0
|
|
180
313
|
|
|
181
314
|
|
|
182
|
-
def test_format_shebang_newline():
|
|
315
|
+
def test_format_shebang_newline() -> None:
|
|
183
316
|
assert format_shebang("python3", SHEBANG_TPL_LINUX).endswith("\n")
|
|
184
317
|
|
|
185
318
|
|
|
186
|
-
def test_relative_interpreter_default_location():
|
|
319
|
+
def test_relative_interpreter_default_location() -> None:
|
|
187
320
|
assert relative_interpreter(
|
|
188
321
|
"/tmp/relenv", "/tmp/relenv/bin", "/tmp/relenv/bin/python3"
|
|
189
322
|
) == pathlib.Path("..", "bin", "python3")
|
|
190
323
|
|
|
191
324
|
|
|
192
|
-
def test_relative_interpreter_pip_dir_location():
|
|
325
|
+
def test_relative_interpreter_pip_dir_location() -> None:
|
|
193
326
|
assert relative_interpreter(
|
|
194
327
|
"/tmp/relenv", "/tmp/relenv", "/tmp/relenv/bin/python3"
|
|
195
328
|
) == pathlib.Path("bin", "python3")
|
|
196
329
|
|
|
197
330
|
|
|
198
|
-
def test_relative_interpreter_alternate_location():
|
|
331
|
+
def test_relative_interpreter_alternate_location() -> None:
|
|
199
332
|
assert relative_interpreter(
|
|
200
333
|
"/tmp/relenv", "/tmp/relenv/bar/bin", "/tmp/relenv/bin/python3"
|
|
201
334
|
) == pathlib.Path("..", "..", "bin", "python3")
|
|
202
335
|
|
|
203
336
|
|
|
204
|
-
def test_relative_interpreter_interpreter_not_relative_to_root():
|
|
337
|
+
def test_relative_interpreter_interpreter_not_relative_to_root() -> None:
|
|
205
338
|
with pytest.raises(ValueError):
|
|
206
339
|
relative_interpreter("/tmp/relenv", "/tmp/relenv/bar/bin", "/tmp/bin/python3")
|
|
207
340
|
|
|
208
341
|
|
|
209
|
-
def test_relative_interpreter_scripts_not_relative_to_root():
|
|
342
|
+
def test_relative_interpreter_scripts_not_relative_to_root() -> None:
|
|
210
343
|
with pytest.raises(ValueError):
|
|
211
344
|
relative_interpreter("/tmp/relenv", "/tmp/bar/bin", "/tmp/relenv/bin/python3")
|
|
212
345
|
|
|
213
346
|
|
|
214
|
-
def test_sanitize_sys_path():
|
|
347
|
+
def test_sanitize_sys_path() -> None:
|
|
215
348
|
if sys.platform.startswith("win"):
|
|
216
349
|
path_prefix = "C:\\"
|
|
217
350
|
separator = "\\"
|
|
@@ -237,3 +370,133 @@ def test_sanitize_sys_path():
|
|
|
237
370
|
new_sys_path = sanitize_sys_path(sys_path)
|
|
238
371
|
assert new_sys_path != sys_path
|
|
239
372
|
assert new_sys_path == expected
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_version_parse_and_str() -> None:
|
|
376
|
+
version = Version("3.10.4")
|
|
377
|
+
assert version.major == 3
|
|
378
|
+
assert version.minor == 10
|
|
379
|
+
assert version.micro == 4
|
|
380
|
+
assert str(version) == "3.10.4"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_version_equality_and_hash_handles_missing_parts() -> None:
|
|
384
|
+
left = Version("3.10")
|
|
385
|
+
right = Version("3.10.0")
|
|
386
|
+
assert left == right
|
|
387
|
+
assert isinstance(hash(left), int)
|
|
388
|
+
assert isinstance(hash(right), int)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_version_comparisons() -> None:
|
|
392
|
+
assert Version("3.9") < Version("3.10")
|
|
393
|
+
assert Version("3.10.1") > Version("3.10.0")
|
|
394
|
+
assert Version("3.11") >= Version("3.11.0")
|
|
395
|
+
assert Version("3.12.2") <= Version("3.12.2")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_version_parse_string_too_many_parts() -> None:
|
|
399
|
+
with pytest.raises(RuntimeError):
|
|
400
|
+
Version.parse_string("1.2.3.4")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_work_dirs_pickle_roundtrip(tmp_path: pathlib.Path) -> None:
|
|
404
|
+
data_dir = tmp_path / "data"
|
|
405
|
+
with patch("relenv.common.DATA_DIR", data_dir):
|
|
406
|
+
dirs = work_dirs(tmp_path)
|
|
407
|
+
restored = pickle.loads(pickle.dumps(dirs))
|
|
408
|
+
assert restored.root == dirs.root
|
|
409
|
+
assert restored.toolchain == dirs.toolchain
|
|
410
|
+
assert restored.download == dirs.download
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_work_dirs_with_data_dir_root(tmp_path: pathlib.Path) -> None:
|
|
414
|
+
data_dir = tmp_path / "data"
|
|
415
|
+
with patch("relenv.common.DATA_DIR", data_dir):
|
|
416
|
+
dirs = work_dirs(data_dir)
|
|
417
|
+
assert dirs.build == data_dir / "build"
|
|
418
|
+
assert dirs.logs == data_dir / "logs"
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_list_archived_builds(tmp_path: pathlib.Path) -> None:
|
|
422
|
+
data_dir = tmp_path / "data"
|
|
423
|
+
build_dir = data_dir / "build"
|
|
424
|
+
build_dir.mkdir(parents=True)
|
|
425
|
+
archive = build_dir / "3.10.0-x86_64-linux-gnu.tar.xz"
|
|
426
|
+
archive.write_bytes(b"")
|
|
427
|
+
with patch("relenv.common.DATA_DIR", data_dir):
|
|
428
|
+
builds = list_archived_builds()
|
|
429
|
+
assert ("3.10.0", "x86_64", "linux-gnu") in builds
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_addpackage_reads_paths(tmp_path: pathlib.Path) -> None:
|
|
433
|
+
sitedir = tmp_path
|
|
434
|
+
module_dir = tmp_path / "package"
|
|
435
|
+
module_dir.mkdir()
|
|
436
|
+
pth_file = sitedir / "example.pth"
|
|
437
|
+
pth_file.write_text(f"{module_dir.name}\n")
|
|
438
|
+
result = addpackage(str(sitedir), pth_file.name)
|
|
439
|
+
assert result == [str(module_dir.resolve())]
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_sanitize_sys_path_with_editable_paths(tmp_path: pathlib.Path) -> None:
|
|
443
|
+
base = tmp_path / "base"
|
|
444
|
+
base.mkdir()
|
|
445
|
+
known_path = base / "lib"
|
|
446
|
+
known_path.mkdir()
|
|
447
|
+
editable_file = known_path / "__editable__.demo.pth"
|
|
448
|
+
editable_file.touch()
|
|
449
|
+
extra_path = str(known_path / "extra")
|
|
450
|
+
with patch.object(sys, "prefix", str(base)), patch.object(
|
|
451
|
+
sys, "base_prefix", str(base)
|
|
452
|
+
), patch.dict(os.environ, {}, clear=True), patch(
|
|
453
|
+
"relenv.common.addpackage", return_value=[extra_path]
|
|
454
|
+
):
|
|
455
|
+
sanitized = sanitize_sys_path([str(known_path)])
|
|
456
|
+
assert extra_path in sanitized
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_makepath_oserror() -> None:
|
|
460
|
+
with patch("os.path.abspath", side_effect=OSError):
|
|
461
|
+
result, case = makepath("foo", "Bar")
|
|
462
|
+
expected = os.path.join("foo", "Bar")
|
|
463
|
+
assert result == expected
|
|
464
|
+
assert case == os.path.normcase(expected)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def test_copyright_headers() -> None:
|
|
468
|
+
"""Verify all Python source files have the correct copyright header."""
|
|
469
|
+
expected_header = (
|
|
470
|
+
"# Copyright 2022-2025 Broadcom.\n" "# SPDX-License-Identifier: Apache-2.0\n"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Find all Python files in relenv/ and tests/
|
|
474
|
+
root = MODULE_DIR.parent
|
|
475
|
+
python_files: list[pathlib.Path] = []
|
|
476
|
+
for directory in ("relenv", "tests"):
|
|
477
|
+
dir_path = root / directory
|
|
478
|
+
if dir_path.exists():
|
|
479
|
+
python_files.extend(dir_path.rglob("*.py"))
|
|
480
|
+
|
|
481
|
+
# Skip generated and cache files
|
|
482
|
+
python_files = [
|
|
483
|
+
f
|
|
484
|
+
for f in python_files
|
|
485
|
+
if "__pycache__" not in f.parts
|
|
486
|
+
and ".nox" not in f.parts
|
|
487
|
+
and "build" not in f.parts
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
failures = []
|
|
491
|
+
for py_file in python_files:
|
|
492
|
+
with open(py_file, "r", encoding="utf-8") as f:
|
|
493
|
+
content = f.read()
|
|
494
|
+
|
|
495
|
+
if not content.startswith(expected_header):
|
|
496
|
+
# Read first two lines for error message
|
|
497
|
+
lines = content.split("\n", 2)
|
|
498
|
+
actual = "\n".join(lines[:2]) + "\n" if len(lines) >= 2 else content
|
|
499
|
+
failures.append(f"{py_file.relative_to(root)}: {actual!r}")
|
|
500
|
+
|
|
501
|
+
if failures:
|
|
502
|
+
pytest.fail("Files with incorrect copyright headers:\n" + "\n".join(failures))
|