relenv 0.21.1__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.
Files changed (49) hide show
  1. relenv/__init__.py +14 -2
  2. relenv/__main__.py +12 -6
  3. relenv/_resources/xz/config.h +148 -0
  4. relenv/_resources/xz/readme.md +4 -0
  5. relenv/build/__init__.py +28 -30
  6. relenv/build/common/__init__.py +50 -0
  7. relenv/build/common/_sysconfigdata_template.py +72 -0
  8. relenv/build/common/builder.py +907 -0
  9. relenv/build/common/builders.py +163 -0
  10. relenv/build/common/download.py +324 -0
  11. relenv/build/common/install.py +609 -0
  12. relenv/build/common/ui.py +432 -0
  13. relenv/build/darwin.py +128 -14
  14. relenv/build/linux.py +296 -78
  15. relenv/build/windows.py +259 -44
  16. relenv/buildenv.py +48 -17
  17. relenv/check.py +10 -5
  18. relenv/common.py +499 -163
  19. relenv/create.py +147 -7
  20. relenv/fetch.py +16 -4
  21. relenv/manifest.py +15 -7
  22. relenv/python-versions.json +329 -0
  23. relenv/pyversions.py +817 -30
  24. relenv/relocate.py +101 -55
  25. relenv/runtime.py +452 -253
  26. relenv/toolchain.py +9 -3
  27. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/METADATA +1 -1
  28. relenv-0.22.0.dist-info/RECORD +48 -0
  29. tests/__init__.py +2 -0
  30. tests/_pytest_typing.py +45 -0
  31. tests/conftest.py +42 -36
  32. tests/test_build.py +426 -9
  33. tests/test_common.py +311 -48
  34. tests/test_create.py +149 -6
  35. tests/test_downloads.py +19 -15
  36. tests/test_fips_photon.py +6 -3
  37. tests/test_module_imports.py +44 -0
  38. tests/test_pyversions_runtime.py +177 -0
  39. tests/test_relocate.py +45 -39
  40. tests/test_relocate_module.py +257 -0
  41. tests/test_runtime.py +1802 -6
  42. tests/test_verify_build.py +500 -34
  43. relenv/build/common.py +0 -1609
  44. relenv-0.21.1.dist-info/RECORD +0 -35
  45. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/WHEEL +0 -0
  46. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/entry_points.txt +0 -0
  47. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
  48. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/licenses/NOTICE +0 -0
  49. {relenv-0.21.1.dist-info → relenv-0.22.0.dist-info}/top_level.txt +0 -0
tests/test_create.py CHANGED
@@ -12,12 +12,12 @@ from relenv.common import arches
12
12
  from relenv.create import CreateException, chdir, create
13
13
 
14
14
 
15
- def test_chdir(tmp_path):
15
+ def test_chdir(tmp_path: pathlib.Path) -> None:
16
16
  with chdir(str(tmp_path)):
17
17
  assert pathlib.Path(os.getcwd()) == tmp_path
18
18
 
19
19
 
20
- def test_create(tmp_path):
20
+ def test_create(tmp_path: pathlib.Path) -> None:
21
21
  to_be_archived = tmp_path / "to_be_archived"
22
22
  to_be_archived.mkdir()
23
23
  test_file = to_be_archived / "testfile"
@@ -34,21 +34,164 @@ def test_create(tmp_path):
34
34
  assert (to_dir / to_be_archived.name / test_file.name) in to_dir.glob("**/*")
35
35
 
36
36
 
37
- def test_create_tar_doesnt_exist(tmp_path):
37
+ def test_create_tar_doesnt_exist(tmp_path: pathlib.Path) -> None:
38
38
  tar_file = tmp_path / "fake_archive"
39
39
  with patch("relenv.create.archived_build", return_value=tar_file):
40
40
  with pytest.raises(CreateException):
41
41
  create("foo", dest=tmp_path)
42
42
 
43
43
 
44
- def test_create_directory_exists(tmp_path):
44
+ def test_create_directory_exists(tmp_path: pathlib.Path) -> None:
45
45
  (tmp_path / "foo").mkdir()
46
46
  with pytest.raises(CreateException):
47
47
  create("foo", dest=tmp_path)
48
48
 
49
49
 
50
- def test_create_arches_directory_exists(tmp_path):
51
- mocked_arches = {key: [] for key in arches.keys()}
50
+ def test_create_arches_directory_exists(tmp_path: pathlib.Path) -> None:
51
+ mocked_arches: dict[str, list[str]] = {key: [] for key in arches.keys()}
52
52
  with patch("relenv.create.arches", mocked_arches):
53
53
  with pytest.raises(CreateException):
54
54
  create("foo", dest=tmp_path)
55
+
56
+
57
+ def test_create_with_minor_version(tmp_path: pathlib.Path) -> None:
58
+ """Test that minor version (e.g., '3.12') resolves to latest micro version."""
59
+ import argparse
60
+ import sys
61
+
62
+ from relenv.create import main
63
+ from relenv.pyversions import Version
64
+
65
+ # Mock python_versions to return some test versions
66
+ all_versions = {
67
+ Version("3.11.5"): "aaa111",
68
+ Version("3.12.5"): "abc123",
69
+ Version("3.12.6"): "def456",
70
+ Version("3.12.7"): "ghi789",
71
+ Version("3.13.1"): "zzz999",
72
+ }
73
+
74
+ def mock_python_versions(minor: str | None = None) -> dict[Version, str]:
75
+ """Mock that filters versions by minor version like the real function."""
76
+ if minor is None:
77
+ return all_versions
78
+ # Filter versions matching the minor version
79
+ mv = Version(minor)
80
+ return {
81
+ v: h
82
+ for v, h in all_versions.items()
83
+ if v.major == mv.major and v.minor == mv.minor
84
+ }
85
+
86
+ # Create a fake archive
87
+ to_be_archived = tmp_path / "to_be_archived"
88
+ to_be_archived.mkdir()
89
+ test_file = to_be_archived / "testfile"
90
+ test_file.touch()
91
+ tar_file = tmp_path / "fake_archive"
92
+ with tarfile.open(str(tar_file), "w:xz") as tar:
93
+ tar.add(str(to_be_archived), to_be_archived.name)
94
+
95
+ # Use appropriate architecture for the platform
96
+ test_arch = "amd64" if sys.platform == "win32" else "x86_64"
97
+ args = argparse.Namespace(name="test_env", arch=test_arch, python="3.12")
98
+
99
+ with chdir(str(tmp_path)):
100
+ with patch("relenv.create.python_versions", side_effect=mock_python_versions):
101
+ with patch("relenv.create.archived_build", return_value=tar_file):
102
+ with patch("relenv.create.build_arch", return_value=test_arch):
103
+ main(args)
104
+
105
+ to_dir = tmp_path / "test_env"
106
+ assert to_dir.exists()
107
+
108
+
109
+ def test_create_with_full_version(tmp_path: pathlib.Path) -> None:
110
+ """Test that full version (e.g., '3.12.7') still works."""
111
+ import argparse
112
+ import sys
113
+
114
+ from relenv.create import main
115
+ from relenv.pyversions import Version
116
+
117
+ # Mock python_versions to return some test versions
118
+ all_versions = {
119
+ Version("3.11.5"): "aaa111",
120
+ Version("3.12.5"): "abc123",
121
+ Version("3.12.6"): "def456",
122
+ Version("3.12.7"): "ghi789",
123
+ Version("3.13.1"): "zzz999",
124
+ }
125
+
126
+ def mock_python_versions(minor: str | None = None) -> dict[Version, str]:
127
+ """Mock that filters versions by minor version like the real function."""
128
+ if minor is None:
129
+ return all_versions
130
+ # Filter versions matching the minor version
131
+ mv = Version(minor)
132
+ return {
133
+ v: h
134
+ for v, h in all_versions.items()
135
+ if v.major == mv.major and v.minor == mv.minor
136
+ }
137
+
138
+ # Create a fake archive
139
+ to_be_archived = tmp_path / "to_be_archived"
140
+ to_be_archived.mkdir()
141
+ test_file = to_be_archived / "testfile"
142
+ test_file.touch()
143
+ tar_file = tmp_path / "fake_archive"
144
+ with tarfile.open(str(tar_file), "w:xz") as tar:
145
+ tar.add(str(to_be_archived), to_be_archived.name)
146
+
147
+ # Use appropriate architecture for the platform
148
+ test_arch = "amd64" if sys.platform == "win32" else "x86_64"
149
+ args = argparse.Namespace(name="test_env", arch=test_arch, python="3.12.7")
150
+
151
+ with chdir(str(tmp_path)):
152
+ with patch("relenv.create.python_versions", side_effect=mock_python_versions):
153
+ with patch("relenv.create.archived_build", return_value=tar_file):
154
+ with patch("relenv.create.build_arch", return_value=test_arch):
155
+ main(args)
156
+
157
+ to_dir = tmp_path / "test_env"
158
+ assert to_dir.exists()
159
+
160
+
161
+ def test_create_with_unknown_minor_version(tmp_path: pathlib.Path) -> None:
162
+ """Test that unknown minor version produces an error."""
163
+ import argparse
164
+ import sys
165
+
166
+ from relenv.create import main
167
+ from relenv.pyversions import Version
168
+
169
+ # Mock python_versions to return empty dict for unknown version
170
+ all_versions = {
171
+ Version("3.11.5"): "aaa111",
172
+ Version("3.12.5"): "abc123",
173
+ Version("3.12.6"): "def456",
174
+ Version("3.12.7"): "ghi789",
175
+ Version("3.13.1"): "zzz999",
176
+ }
177
+
178
+ # Use appropriate architecture for the platform
179
+ test_arch = "amd64" if sys.platform == "win32" else "x86_64"
180
+ args = argparse.Namespace(name="test_env", arch=test_arch, python="3.99")
181
+
182
+ def mock_python_versions(minor: str | None = None) -> dict[Version, str]:
183
+ """Mock that filters versions by minor version like the real function."""
184
+ if minor is None:
185
+ return all_versions
186
+ # Filter versions matching the minor version
187
+ mv = Version(minor)
188
+ return {
189
+ v: h
190
+ for v, h in all_versions.items()
191
+ if v.major == mv.major and v.minor == mv.minor
192
+ }
193
+
194
+ with patch("relenv.create.python_versions", side_effect=mock_python_versions):
195
+ with patch("relenv.create.build_arch", return_value=test_arch):
196
+ with pytest.raises(SystemExit):
197
+ main(args)
tests/test_downloads.py CHANGED
@@ -1,22 +1,24 @@
1
1
  # Copyright 2022-2025 Broadcom.
2
- # SPDX-License-Identifier: Apache-2
2
+ # SPDX-License-Identifier: Apache-2.0
3
3
  import pathlib
4
4
  import subprocess
5
5
  import sys
6
+
7
+ # mypy: ignore-errors
6
8
  from unittest.mock import patch
7
9
 
8
- from relenv.build.common import Download
10
+ from relenv.build.common.download import Download
9
11
  from relenv.common import RelenvException
10
12
 
11
13
 
12
- def test_download_url():
14
+ def test_download_url() -> None:
13
15
  download = Download(
14
16
  "test", "https://test.com/{version}/test-{version}.tar.xz", version="1.0.0"
15
17
  )
16
18
  assert download.url == "https://test.com/1.0.0/test-1.0.0.tar.xz"
17
19
 
18
20
 
19
- def test_download_url_change_version():
21
+ def test_download_url_change_version() -> None:
20
22
  download = Download(
21
23
  "test", "https://test.com/{version}/test-{version}.tar.xz", version="1.0.0"
22
24
  )
@@ -24,7 +26,7 @@ def test_download_url_change_version():
24
26
  assert download.url == "https://test.com/1.2.2/test-1.2.2.tar.xz"
25
27
 
26
28
 
27
- def test_download_filepath():
29
+ def test_download_filepath() -> None:
28
30
  download = Download(
29
31
  "test",
30
32
  "https://test.com/{version}/test-{version}.tar.xz",
@@ -38,7 +40,7 @@ def test_download_filepath():
38
40
  assert str(download.filepath) == "/tmp/test-1.0.0.tar.xz"
39
41
 
40
42
 
41
- def test_download_filepath_change_destination():
43
+ def test_download_filepath_change_destination() -> None:
42
44
  download = Download(
43
45
  "test",
44
46
  "https://test.com/{version}/test-{version}.tar.xz",
@@ -53,7 +55,7 @@ def test_download_filepath_change_destination():
53
55
  assert str(download.filepath) == "/tmp/foo/test-1.0.0.tar.xz"
54
56
 
55
57
 
56
- def test_download_exists(tmp_path):
58
+ def test_download_exists(tmp_path: pathlib.Path) -> None:
57
59
  download = Download(
58
60
  "test",
59
61
  "https://test.com/{version}/test-{version}.tar.xz",
@@ -65,25 +67,25 @@ def test_download_exists(tmp_path):
65
67
  assert download.exists() is True
66
68
 
67
69
 
68
- def test_validate_md5sum(tmp_path):
70
+ def test_validate_md5sum(tmp_path: pathlib.Path) -> None:
69
71
  fake_md5 = "fakemd5"
70
- with patch("relenv.build.common.verify_checksum") as run_mock:
72
+ with patch("relenv.build.common.download.verify_checksum") as run_mock:
71
73
  assert Download.validate_checksum(str(tmp_path), fake_md5) is True
72
74
  run_mock.assert_called_with(str(tmp_path), fake_md5)
73
75
 
74
76
 
75
- def test_validate_md5sum_failed(tmp_path):
77
+ def test_validate_md5sum_failed(tmp_path: pathlib.Path) -> None:
76
78
  fake_md5 = "fakemd5"
77
79
  with patch(
78
- "relenv.build.common.verify_checksum", side_effect=RelenvException
80
+ "relenv.build.common.download.verify_checksum", side_effect=RelenvException
79
81
  ) as run_mock:
80
82
  assert Download.validate_checksum(str(tmp_path), fake_md5) is False
81
83
  run_mock.assert_called_with(str(tmp_path), fake_md5)
82
84
 
83
85
 
84
- def test_validate_signature(tmp_path):
86
+ def test_validate_signature(tmp_path: pathlib.Path) -> None:
85
87
  sig = "fakesig"
86
- with patch("relenv.build.common.runcmd") as run_mock:
88
+ with patch("relenv.build.common.download.runcmd") as run_mock:
87
89
  assert Download.validate_signature(str(tmp_path), sig) is True
88
90
  run_mock.assert_called_with(
89
91
  ["gpg", "--verify", sig, str(tmp_path)],
@@ -92,9 +94,11 @@ def test_validate_signature(tmp_path):
92
94
  )
93
95
 
94
96
 
95
- def test_validate_signature_failed(tmp_path):
97
+ def test_validate_signature_failed(tmp_path: pathlib.Path) -> None:
96
98
  sig = "fakesig"
97
- with patch("relenv.build.common.runcmd", side_effect=RelenvException) as run_mock:
99
+ with patch(
100
+ "relenv.build.common.download.runcmd", side_effect=RelenvException
101
+ ) as run_mock:
98
102
  assert Download.validate_signature(str(tmp_path), sig) is False
99
103
  run_mock.assert_called_with(
100
104
  ["gpg", "--verify", sig, str(tmp_path)],
tests/test_fips_photon.py CHANGED
@@ -1,9 +1,12 @@
1
- # Copyright 2023-2025 Broadcom.
1
+ # Copyright 2022-2025 Broadcom.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  import os
5
5
  import pathlib
6
+
7
+ # mypy: ignore-errors
6
8
  import subprocess
9
+ from typing import Any
7
10
 
8
11
  import pytest
9
12
 
@@ -12,7 +15,7 @@ from tests.test_verify_build import _install_ppbt
12
15
  from .conftest import get_build_version
13
16
 
14
17
 
15
- def check_test_environment():
18
+ def check_test_environment() -> bool:
16
19
  path = pathlib.Path("/etc/os-release")
17
20
  if path.exists():
18
21
  release = path.read_text()
@@ -28,7 +31,7 @@ pytestmark = [
28
31
  ]
29
32
 
30
33
 
31
- def test_fips_mode(pyexec, build):
34
+ def test_fips_mode(pyexec: str, build: Any) -> None:
32
35
  _install_ppbt(pyexec)
33
36
  env = os.environ.copy()
34
37
  proc = subprocess.run(
@@ -0,0 +1,44 @@
1
+ # Copyright 2022-2025 Broadcom.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ from __future__ import annotations
5
+
6
+ import importlib
7
+ import pathlib
8
+ from typing import TYPE_CHECKING, Any, Callable, List, Sequence, TypeVar, cast
9
+
10
+ import pytest
11
+
12
+ if TYPE_CHECKING:
13
+ from _pytest.mark.structures import ParameterSet
14
+
15
+ F = TypeVar("F", bound=Callable[..., object])
16
+
17
+
18
+ def typed_parametrize(*args: Any, **kwargs: Any) -> Callable[[F], F]:
19
+ """Type-aware wrapper around pytest.mark.parametrize."""
20
+ decorator = pytest.mark.parametrize(*args, **kwargs)
21
+ return cast(Callable[[F], F], decorator)
22
+
23
+
24
+ def _top_level_modules() -> Sequence["ParameterSet"]:
25
+ relenv_dir = pathlib.Path(__file__).resolve().parents[1] / "relenv"
26
+ params: List["ParameterSet"] = []
27
+ for path in sorted(relenv_dir.iterdir()):
28
+ if not path.is_file() or path.suffix != ".py":
29
+ continue
30
+ stem = path.stem
31
+ if stem == "__init__":
32
+ module_name = "relenv"
33
+ else:
34
+ module_name = f"relenv.{stem}"
35
+ params.append(pytest.param(module_name, id=module_name))
36
+ return params
37
+
38
+
39
+ @typed_parametrize("module_name", _top_level_modules())
40
+ def test_import_top_level_module(module_name: str) -> None:
41
+ """
42
+ Ensure each top-level module in the relenv package can be imported.
43
+ """
44
+ importlib.import_module(module_name)
@@ -0,0 +1,177 @@
1
+ # Copyright 2022-2025 Broadcom.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import pathlib
8
+ import subprocess
9
+ from typing import Any, Dict, Sequence
10
+
11
+ import pytest
12
+
13
+ from relenv import pyversions
14
+
15
+
16
+ def test_python_versions_returns_versions() -> None:
17
+ versions: Dict[pyversions.Version, str] = pyversions.python_versions()
18
+ assert versions, "python_versions() should return known versions"
19
+ first_version = next(iter(versions))
20
+ assert isinstance(first_version, pyversions.Version)
21
+ assert isinstance(versions[first_version], str)
22
+
23
+
24
+ def test_python_versions_filters_minor() -> None:
25
+ versions = pyversions.python_versions("3.11")
26
+ assert versions
27
+ assert all(version.major == 3 and version.minor == 11 for version in versions)
28
+ sorted_versions = sorted(versions)
29
+ assert sorted_versions[-1] in versions
30
+
31
+
32
+ def test_release_urls_handles_old_versions() -> None:
33
+ tarball, signature = pyversions._release_urls(pyversions.Version("3.1.3"))
34
+ assert tarball.endswith(".tar.xz")
35
+ assert signature is not None
36
+
37
+
38
+ def test_release_urls_no_signature_before_23() -> None:
39
+ tarball, signature = pyversions._release_urls(pyversions.Version("2.2.3"))
40
+ assert tarball.endswith(".tar.xz")
41
+ assert signature is None
42
+
43
+
44
+ def test_ref_version_and_path_helpers() -> None:
45
+ html = '<a href="download/Python-3.11.9.tgz">Python 3.11.9</a>'
46
+ version = pyversions._ref_version(html)
47
+ assert str(version) == "3.11.9"
48
+ assert pyversions._ref_path(html) == "download/Python-3.11.9.tgz"
49
+
50
+
51
+ def test_digest(tmp_path: pathlib.Path) -> None:
52
+ file = tmp_path / "data.bin"
53
+ file.write_bytes(b"abc")
54
+ assert pyversions.digest(file) == hashlib.sha1(b"abc").hexdigest()
55
+
56
+
57
+ def test_get_keyid_parses_second_line() -> None:
58
+ proc = subprocess.CompletedProcess(
59
+ ["gpg"],
60
+ 1,
61
+ stdout=b"",
62
+ stderr=b"gpg: error\n[GNUPG:] INV_SGNR 0 CB1234\n",
63
+ )
64
+ assert pyversions._get_keyid(proc) == "CB1234"
65
+
66
+
67
+ def test_verify_signature_success(
68
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
69
+ ) -> None:
70
+ called: Dict[str, list[str]] = {}
71
+
72
+ def fake_run(
73
+ cmd: Sequence[str], **kwargs: Any
74
+ ) -> subprocess.CompletedProcess[bytes]:
75
+ called.setdefault("cmd", []).extend(cmd)
76
+ return subprocess.CompletedProcess(cmd, 0, stdout=b"", stderr=b"")
77
+
78
+ monkeypatch.setattr(pyversions.subprocess, "run", fake_run)
79
+ assert pyversions.verify_signature("archive.tgz", "archive.tgz.asc") is True
80
+ assert called["cmd"][0] == "gpg"
81
+
82
+
83
+ def test_verify_signature_failure_with_missing_key(
84
+ monkeypatch: pytest.MonkeyPatch,
85
+ ) -> None:
86
+ responses: list[str] = []
87
+
88
+ def fake_run(
89
+ cmd: Sequence[str], **kwargs: Any
90
+ ) -> subprocess.CompletedProcess[bytes]:
91
+ if len(responses) == 0:
92
+ responses.append("first")
93
+ stderr = b"gpg: error\n[GNUPG:] INV_SGNR 0 ABCDEF12\nNo public key\n"
94
+ return subprocess.CompletedProcess(cmd, 1, stdout=b"", stderr=stderr)
95
+ return subprocess.CompletedProcess(cmd, 0, stdout=b"", stderr=b"")
96
+
97
+ monkeypatch.setattr(pyversions.subprocess, "run", fake_run)
98
+ monkeypatch.setattr(pyversions, "_receive_key", lambda keyid, server: True)
99
+ assert pyversions.verify_signature("archive.tgz", "archive.tgz.asc") is True
100
+
101
+
102
+ def test_sha256_digest(tmp_path: pathlib.Path) -> None:
103
+ """Test SHA-256 digest computation."""
104
+ file = tmp_path / "data.bin"
105
+ file.write_bytes(b"test data")
106
+ assert pyversions.sha256_digest(file) == hashlib.sha256(b"test data").hexdigest()
107
+
108
+
109
+ def test_detect_openssl_versions(monkeypatch: pytest.MonkeyPatch) -> None:
110
+ """Test OpenSSL version detection from GitHub releases."""
111
+ mock_html = """
112
+ <html>
113
+ <a href="/openssl/openssl/releases/tag/openssl-3.5.4">openssl-3.5.4</a>
114
+ <a href="/openssl/openssl/releases/tag/openssl-3.5.3">openssl-3.5.3</a>
115
+ <a href="/openssl/openssl/releases/tag/openssl-3.4.0">openssl-3.4.0</a>
116
+ </html>
117
+ """
118
+
119
+ def fake_fetch(url: str) -> str:
120
+ return mock_html
121
+
122
+ monkeypatch.setattr(pyversions, "fetch_url_content", fake_fetch)
123
+ versions = pyversions.detect_openssl_versions()
124
+ assert isinstance(versions, list)
125
+ assert "3.5.4" in versions
126
+ assert "3.5.3" in versions
127
+ assert "3.4.0" in versions
128
+ # Verify sorting (latest first)
129
+ assert versions[0] == "3.5.4"
130
+
131
+
132
+ def test_detect_sqlite_versions(monkeypatch: pytest.MonkeyPatch) -> None:
133
+ """Test SQLite version detection from sqlite.org."""
134
+ mock_html = """
135
+ <html>
136
+ <a href="2024/sqlite-autoconf-3500400.tar.gz">sqlite-autoconf-3500400.tar.gz</a>
137
+ <a href="2024/sqlite-autoconf-3500300.tar.gz">sqlite-autoconf-3500300.tar.gz</a>
138
+ </html>
139
+ """
140
+
141
+ def fake_fetch(url: str) -> str:
142
+ return mock_html
143
+
144
+ monkeypatch.setattr(pyversions, "fetch_url_content", fake_fetch)
145
+ versions = pyversions.detect_sqlite_versions()
146
+ assert isinstance(versions, list)
147
+ # Should return list of tuples (version, sqliteversion)
148
+ assert len(versions) > 0
149
+ assert isinstance(versions[0], tuple)
150
+ assert len(versions[0]) == 2
151
+ # Check that conversion worked
152
+ version, sqlite_ver = versions[0]
153
+ assert version == "3.50.4.0"
154
+ assert sqlite_ver == "3500400"
155
+
156
+
157
+ def test_detect_xz_versions(monkeypatch: pytest.MonkeyPatch) -> None:
158
+ """Test XZ version detection from tukaani.org."""
159
+ mock_html = """
160
+ <html>
161
+ <a href="xz-5.8.1.tar.gz">xz-5.8.1.tar.gz</a>
162
+ <a href="xz-5.8.0.tar.gz">xz-5.8.0.tar.gz</a>
163
+ <a href="xz-5.6.3.tar.gz">xz-5.6.3.tar.gz</a>
164
+ </html>
165
+ """
166
+
167
+ def fake_fetch(url: str) -> str:
168
+ return mock_html
169
+
170
+ monkeypatch.setattr(pyversions, "fetch_url_content", fake_fetch)
171
+ versions = pyversions.detect_xz_versions()
172
+ assert isinstance(versions, list)
173
+ assert "5.8.1" in versions
174
+ assert "5.8.0" in versions
175
+ assert "5.6.3" in versions
176
+ # Verify sorting (latest first)
177
+ assert versions[0] == "5.8.1"