1+ import io
2+ import zipfile
13from urllib .parse import urljoin , urlsplit
24from xml .etree import ElementTree as ET
35
2022TWINE_500_WHEEL_URL = urljoin (urljoin (PYTHON_FIXTURES_URL , "packages/" ), TWINE_500_WHEEL_FILENAME )
2123
2224
25+ def _make_build_tagged_wheel (tmp_path , base_wheel_bytes , build_tag , platform = "linux_x86_64" ):
26+ """Repackage a wheel with a PEP 427 build tag and platform in the filename."""
27+ filename = f"shelf_reader-0.1-{ build_tag } -py2-none-{ platform } .whl"
28+ path = tmp_path / filename
29+ with zipfile .ZipFile (io .BytesIO (base_wheel_bytes )) as src , zipfile .ZipFile (path , "w" ) as dst :
30+ for item in src .infolist ():
31+ dst .writestr (item , src .read (item .filename ))
32+ # Pulp deduplicates content by sha256, so identical zip bytes with
33+ # different filenames map to the same content unit. Inject a unique
34+ # marker so each (build_tag, platform) combination gets its own sha256.
35+ dst .writestr (
36+ "shelf_reader/.build_marker" ,
37+ f"build={ build_tag } platform={ platform } \n " ,
38+ )
39+ return str (path ), filename
40+
41+
2342def _index_url (distro , bindings_cfg ):
2443 """Build the index URL using the same origin the API client uses."""
2544 path = urlsplit (distro .base_url ).path
@@ -125,18 +144,18 @@ def test_pinned_version_feeds(
125144
126145 item = _parse_items (_get_feed (distro , "rss/updates.xml" , bindings_cfg ))[0 ]
127146 assert item .findtext ("link" ).endswith ("pypi/shelf-reader/0.1/json" )
128- assert " pypi/shelf-reader/0.1/json#" in item . findtext ( "guid " )
147+ assert item . findtext ( "guid" ). endswith ( " pypi/shelf-reader/0.1/json" )
129148
130149 python_content_factory (TWINE_WHEEL_FILENAME , url = TWINE_WHEEL_URL , repository = repo )
131150 update_titles = _titles (_parse_items (_get_feed (distro , "rss/updates.xml" , bindings_cfg )))
132151 assert update_titles == ["shelf-reader 0.1" ]
133152
134153
135154@pytest .mark .parallel
136- def test_new_file_for_existing_version_updates_guid (
155+ def test_guid_stable_when_adding_file_without_new_build_tag (
137156 bindings_cfg , python_content_factory , python_empty_repo_distro
138157):
139- """Adding a new file for an existing (name, version) produces a new guid and updated date ."""
158+ """Adding a file without a new build tag keeps the GUID stable but updates pubDate ."""
140159 repo , distro = python_empty_repo_distro ()
141160
142161 python_content_factory (PYTHON_EGG_FILENAME , url = PYTHON_EGG_URL , repository = repo )
@@ -145,7 +164,7 @@ def test_new_file_for_existing_version_updates_guid(
145164 assert len (items ) == 1
146165 first_guid = items [0 ].findtext ("guid" )
147166 first_date = items [0 ].findtext ("pubDate" )
148- assert "pypi/shelf-reader/0.1/json" in first_guid
167+ assert first_guid . endswith ( "pypi/shelf-reader/0.1/json" )
149168
150169 python_content_factory (PYTHON_WHEEL_FILENAME , url = PYTHON_WHEEL_URL , repository = repo )
151170
@@ -154,11 +173,60 @@ def test_new_file_for_existing_version_updates_guid(
154173 second_guid = items [0 ].findtext ("guid" )
155174 second_date = items [0 ].findtext ("pubDate" )
156175
157- assert second_guid ! = first_guid
176+ assert second_guid = = first_guid
158177 assert second_date >= first_date
159178
160179 release_items = _parse_items (
161180 _get_feed (distro , "rss/project/shelf-reader/releases.xml" , bindings_cfg )
162181 )
163182 assert len (release_items ) == 1
164- assert release_items [0 ].findtext ("guid" ) == second_guid
183+ assert release_items [0 ].findtext ("guid" ) == first_guid
184+
185+
186+ @pytest .mark .parallel
187+ def test_new_build_tag_changes_guid (
188+ tmp_path , bindings_cfg , python_bindings , monitor_task , python_empty_repo_distro
189+ ):
190+ """A new build tag produces a new GUID; a new arch for the same tag does not."""
191+ repo , distro = python_empty_repo_distro ()
192+
193+ base_wheel = requests .get (PYTHON_WHEEL_URL , timeout = 30 ).content
194+
195+ # Upload build tag 1 (x86_64)
196+ path_1 , name_1 = _make_build_tagged_wheel (tmp_path , base_wheel , "1" )
197+ task = python_bindings .ContentPackagesApi .create (
198+ relative_path = name_1 , file = path_1 , repository = repo .pulp_href
199+ ).task
200+ monitor_task (task )
201+
202+ items = _parse_items (_get_feed (distro , "rss/updates.xml" , bindings_cfg ))
203+ assert len (items ) == 1
204+ guid_after_build1 = items [0 ].findtext ("guid" )
205+ assert "#builds=1" in guid_after_build1
206+
207+ # Upload build tag 2 (x86_64) -- different build, GUID must change
208+ path_2 , name_2 = _make_build_tagged_wheel (tmp_path , base_wheel , "2" )
209+ task = python_bindings .ContentPackagesApi .create (
210+ relative_path = name_2 , file = path_2 , repository = repo .pulp_href
211+ ).task
212+ monitor_task (task )
213+
214+ items = _parse_items (_get_feed (distro , "rss/updates.xml" , bindings_cfg ))
215+ assert len (items ) == 1
216+ guid_after_build2 = items [0 ].findtext ("guid" )
217+ assert guid_after_build2 != guid_after_build1
218+ assert "#builds=1,2" in guid_after_build2
219+
220+ # Upload build tag 2 again with a different arch -- same build tag, GUID must stay
221+ path_2_arm , name_2_arm = _make_build_tagged_wheel (
222+ tmp_path , base_wheel , "2" , platform = "linux_aarch64"
223+ )
224+ task = python_bindings .ContentPackagesApi .create (
225+ relative_path = name_2_arm , file = path_2_arm , repository = repo .pulp_href
226+ ).task
227+ monitor_task (task )
228+
229+ items = _parse_items (_get_feed (distro , "rss/updates.xml" , bindings_cfg ))
230+ assert len (items ) == 1
231+ guid_after_build2_arm = items [0 ].findtext ("guid" )
232+ assert guid_after_build2_arm == guid_after_build2
0 commit comments