Skip to content

Commit 9ec7b03

Browse files
authored
Improve test coverage and error reporting for PBS Python upgrade (DataDog#22816)
* Improve test coverage and error reporting for PBS Python upgrade * Add changelog * Simplify tests * Lint
1 parent 0b2f4d0 commit 9ec7b03

3 files changed

Lines changed: 112 additions & 32 deletions

File tree

ddev/changelog.d/22816.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve error reporting and test coverage for macOS Python Build Standalone (PBS) upgrade logic.

ddev/src/ddev/cli/meta/scripts/upgrade_python.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,18 +277,18 @@ def upgrade_macos_python_version(app: Application, new_version: str, tracker: Va
277277
)
278278
return
279279

280-
# Define replacements: (pattern, new_value)
280+
# Define replacements: (label, pattern, new_value)
281281
replacements = [
282-
(r'^(\s*PYTHON_PATCH:\s*)\d+\s*$', rf'\g<1>{new_patch}'),
283-
(r'^(\s*PBS_RELEASE:\s*)\d+\s*$', rf"\g<1>{pbs_info['release']}"),
284-
(r'^(\s*PBS_SHA256__aarch64:\s*)[0-9a-f]+\s*$', rf"\g<1>{pbs_info['aarch64']}"),
285-
(r'^(\s*PBS_SHA256__x86_64:\s*)[0-9a-f]+\s*$', rf"\g<1>{pbs_info['x86_64']}"),
282+
('PYTHON_PATCH', r'^(\s*PYTHON_PATCH:\s*)\d+\s*$', rf'\g<1>{new_patch}'),
283+
('PBS_RELEASE', r'^(\s*PBS_RELEASE:\s*)\d+\s*$', rf"\g<1>{pbs_info['release']}"),
284+
('PBS_SHA256__aarch64', r'^(\s*PBS_SHA256__aarch64:\s*)[0-9a-f]+\s*$', rf"\g<1>{pbs_info['aarch64']}"),
285+
('PBS_SHA256__x86_64', r'^(\s*PBS_SHA256__x86_64:\s*)[0-9a-f]+\s*$', rf"\g<1>{pbs_info['x86_64']}"),
286286
]
287287

288-
for pattern, replacement in replacements:
288+
for label, pattern, replacement in replacements:
289289
content, count = re.subn(pattern, replacement, content, count=1, flags=re.MULTILINE)
290290
if count == 0:
291-
tracker.error(('macOS workflow',), message=f'Could not find pattern: {pattern}')
291+
tracker.error(('macOS workflow',), message=f'Could not find {label} in workflow file')
292292
return
293293

294294
write_file_safely(workflow_file, content, 'macOS workflow', tracker)
@@ -378,13 +378,16 @@ def get_pbs_release_info(app: Application, python_version: str) -> dict[str, str
378378
return None
379379

380380
hashes = {'release': release}
381+
sha_lines = sha_response.text.splitlines()
381382
for arch in ('aarch64', 'x86_64'):
382383
filename = f'cpython-{python_version}+{release}-{arch}-apple-darwin-install_only_stripped.tar.gz'
383-
for line in sha_response.text.splitlines():
384+
for line in sha_lines:
384385
if filename in line:
385386
sha_hash = line.split()[0]
386387
if validate_sha256(sha_hash):
387388
hashes[arch] = sha_hash
389+
else:
390+
app.display_warning(f"Invalid SHA256 hash for {arch}: {sha_hash}")
388391
break
389392

390393
# Verify we found both architectures
Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,44 @@
11
# (C) Datadog, Inc. 2023-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
import pytest
45

6+
AARCH64_HASH = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'
7+
X86_64_HASH = 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5'
58

6-
def test_update_python_version_success(fake_repo, ddev, mocker):
7-
"""Test successful Python version update."""
8-
# Mock the constants that get imported by the upgrade script
9+
PYTHON_SHA256_HASHES = {
10+
'linux_source_sha256': 'c4c066af19c98fb7835d473bebd7e23be84f6e9874d47db9e39a68ee5d0ce35c',
11+
'windows_amd64_sha256': '200ddff856bbff949d2cc1be42e8807c07538abd6b6966d5113a094cf628c5c5',
12+
}
13+
14+
15+
def _make_sha256sums(version, release, archs=('aarch64', 'x86_64')):
16+
"""Build realistic SHA256SUMS content with unrelated entries to verify correct filtering."""
17+
hashes = {'aarch64': AARCH64_HASH, 'x86_64': X86_64_HASH}
18+
lines = []
19+
for arch in archs:
20+
filename = f'cpython-{version}+{release}-{arch}-apple-darwin-install_only_stripped.tar.gz'
21+
lines.append(f'{hashes[arch]} {filename}')
22+
lines.append(f'{"0" * 64} cpython-{version}+{release}-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz')
23+
return '\n'.join(lines)
24+
25+
26+
@pytest.fixture
27+
def mock_python_version(mocker):
928
mocker.patch('ddev.repo.constants.PYTHON_VERSION_FULL', '3.13.7')
1029
mocker.patch('ddev.repo.constants.PYTHON_VERSION', '3.13')
1130

12-
# Mock network calls
13-
mocker.patch('ddev.cli.meta.scripts.upgrade_python.get_latest_python_version', return_value='3.13.9')
31+
32+
@pytest.fixture
33+
def mock_sha256_hashes(mocker):
1434
mocker.patch(
1535
'ddev.cli.meta.scripts.upgrade_python.get_python_sha256_hashes',
16-
return_value={
17-
'linux_source_sha256': 'c4c066af19c98fb7835d473bebd7e23be84f6e9874d47db9e39a68ee5d0ce35c',
18-
'windows_amd64_sha256': '200ddff856bbff949d2cc1be42e8807c07538abd6b6966d5113a094cf628c5c5',
19-
},
36+
return_value=PYTHON_SHA256_HASHES,
2037
)
38+
39+
40+
def test_update_python_version_success(fake_repo, ddev, mocker, mock_python_version, mock_sha256_hashes):
41+
mocker.patch('ddev.cli.meta.scripts.upgrade_python.get_latest_python_version', return_value='3.13.9')
2142
mocker.patch(
2243
'ddev.cli.meta.scripts.upgrade_python.get_pbs_release_info',
2344
return_value={
@@ -34,27 +55,23 @@ def test_update_python_version_success(fake_repo, ddev, mocker):
3455
assert 'Passed: 5' in result.output
3556
assert 'Python version upgraded from 3.13.7 to 3.13.9' in result.output
3657

37-
# Verify constants.py was updated
3858
constants_file = fake_repo.path / 'ddev' / 'src' / 'ddev' / 'repo' / 'constants.py'
3959
contents = constants_file.read_text()
4060
assert "PYTHON_VERSION_FULL = '3.13.9'" in contents
4161
assert "PYTHON_VERSION_FULL = '3.13.7'" not in contents
4262

43-
# Verify Linux Dockerfile was updated
4463
linux_dockerfile = fake_repo.path / '.builders' / 'images' / 'linux-aarch64' / 'Dockerfile'
4564
contents = linux_dockerfile.read_text()
4665
assert 'ENV PYTHON3_VERSION=3.13.9' in contents
4766
assert 'SHA256="c4c066af19c98fb7835d473bebd7e23be84f6e9874d47db9e39a68ee5d0ce35c"' in contents
4867
assert 'ENV PYTHON3_VERSION=3.13.7' not in contents
4968

50-
# Verify Windows Dockerfile was updated
5169
windows_dockerfile = fake_repo.path / '.builders' / 'images' / 'windows-x86_64' / 'Dockerfile'
5270
contents = windows_dockerfile.read_text()
5371
assert 'ENV PYTHON_VERSION="3.13.9"' in contents
5472
assert '-Hash \'200ddff856bbff949d2cc1be42e8807c07538abd6b6966d5113a094cf628c5c5\'' in contents
5573
assert 'ENV PYTHON_VERSION="3.13.7"' not in contents
5674

57-
# Verify macOS workflow was updated with PBS format
5875
workflow_file = fake_repo.path / '.github' / 'workflows' / 'resolve-build-deps.yaml'
5976
contents = workflow_file.read_text()
6077
assert 'PYTHON_PATCH: 9' in contents
@@ -64,11 +81,7 @@ def test_update_python_version_success(fake_repo, ddev, mocker):
6481
assert 'PBS_SHA256__x86_64: f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5' in contents
6582

6683

67-
def test_update_python_version_already_latest(fake_repo, ddev, mocker):
68-
# Mock the constants that get imported by the upgrade script
69-
mocker.patch('ddev.repo.constants.PYTHON_VERSION_FULL', '3.13.7')
70-
mocker.patch('ddev.repo.constants.PYTHON_VERSION', '3.13')
71-
84+
def test_update_python_version_already_latest(fake_repo, ddev, mocker, mock_python_version):
7285
mocker.patch('ddev.cli.meta.scripts.upgrade_python.get_latest_python_version', return_value='3.13.7')
7386

7487
result = ddev('meta', 'scripts', 'upgrade-python-version')
@@ -86,13 +99,8 @@ def test_update_python_version_no_new_version_found(fake_repo, ddev, mocker):
8699
assert 'Could not find latest Python version' in result.output
87100

88101

89-
def test_update_python_version_invalid_hash_format(fake_repo, ddev, mocker):
90-
# Mock the constants that get imported by the upgrade script
91-
mocker.patch('ddev.repo.constants.PYTHON_VERSION_FULL', '3.13.7')
92-
mocker.patch('ddev.repo.constants.PYTHON_VERSION', '3.13')
93-
102+
def test_update_python_version_invalid_hash_format(fake_repo, ddev, mocker, mock_python_version):
94103
mocker.patch('ddev.cli.meta.scripts.upgrade_python.get_latest_python_version', return_value='3.13.9')
95-
# Hash validation happens inside get_python_sha256_hashes, so it raises ValueError
96104
mocker.patch(
97105
'ddev.cli.meta.scripts.upgrade_python.get_python_sha256_hashes',
98106
side_effect=ValueError('Invalid Linux SHA256 hash format from SBOM: not-a-valid-hash'),
@@ -103,3 +111,71 @@ def test_update_python_version_invalid_hash_format(fake_repo, ddev, mocker):
103111
assert result.exit_code == 1, result.output
104112
assert 'Failed to fetch' in result.output
105113
assert 'Invalid Linux SHA256 hash format' in result.output
114+
115+
116+
def test_upgrade_reports_error_when_pbs_release_unavailable(
117+
fake_repo, ddev, mocker, mock_python_version, mock_sha256_hashes
118+
):
119+
mocker.patch('ddev.cli.meta.scripts.upgrade_python.get_latest_python_version', return_value='3.13.9')
120+
mocker.patch('ddev.cli.meta.scripts.upgrade_python.get_pbs_release_info', return_value=None)
121+
122+
result = ddev('meta', 'scripts', 'upgrade-python-version')
123+
124+
assert result.exit_code == 1, result.output
125+
assert 'Could not find PBS release' in result.output
126+
127+
128+
def test_upgrade_macos_reports_error_on_pattern_mismatch(tmp_path, mocker):
129+
from ddev.cli.meta.scripts.upgrade_python import upgrade_macos_python_version
130+
131+
mock_app = mocker.MagicMock()
132+
mock_app.repo.path = tmp_path
133+
mock_tracker = mocker.MagicMock()
134+
135+
workflow_dir = tmp_path / '.github' / 'workflows'
136+
workflow_dir.mkdir(parents=True)
137+
(workflow_dir / 'resolve-build-deps.yaml').write_text('name: Resolve build deps\njobs:\n build: {}\n')
138+
139+
mocker.patch(
140+
'ddev.cli.meta.scripts.upgrade_python.get_pbs_release_info',
141+
return_value={'release': '20251215', 'aarch64': AARCH64_HASH, 'x86_64': X86_64_HASH},
142+
)
143+
144+
upgrade_macos_python_version(mock_app, '3.13.9', mock_tracker)
145+
146+
mock_tracker.error.assert_called_once()
147+
assert 'Could not find PYTHON_PATCH in workflow file' in mock_tracker.error.call_args[1]['message']
148+
149+
150+
@pytest.mark.parametrize(
151+
'archs, expected',
152+
[
153+
(
154+
('aarch64', 'x86_64'),
155+
{'release': '20251215', 'aarch64': AARCH64_HASH, 'x86_64': X86_64_HASH},
156+
),
157+
(
158+
('aarch64',),
159+
None,
160+
),
161+
],
162+
ids=['both_architectures', 'missing_x86_64'],
163+
)
164+
def test_get_pbs_release_info(mocker, archs, expected):
165+
from ddev.cli.meta.scripts.upgrade_python import get_pbs_release_info
166+
167+
mock_app = mocker.MagicMock()
168+
169+
release_response = mocker.MagicMock()
170+
release_response.text = '{"tag_name": "20251215"}'
171+
172+
sha_response = mocker.MagicMock()
173+
sha_response.text = _make_sha256sums('3.13.9', '20251215', archs=archs)
174+
sha_response.status_code = 200
175+
176+
mocker.patch(
177+
'ddev.cli.meta.scripts.upgrade_python.httpx.get',
178+
side_effect=[release_response, sha_response],
179+
)
180+
181+
assert get_pbs_release_info(mock_app, '3.13.9') == expected

0 commit comments

Comments
 (0)