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\n jobs:\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