1- """Unit tests for the PagePackageConfig.clear_content_dir purge flag.
1+ """Unit tests for the PagePackageConfig.clear_content_dir purge flag, and for
2+ the deterministic ordering of packages.json (#172).
23
34Regression guard for #42: WtSite.create_page_package used to unconditionally
45shutil.rmtree() the content directory. clear_content_dir defaults to True (no
56behaviour change), but setting it to False must keep any existing content.
7+
8+ Regression guard for #172: WtSite.create_page_package wrote the "pages" array
9+ of packages.json in an order that varied between runs whenever
10+ config.include_files was True, causing spurious diffs.
611"""
712
13+ import json
814import threading
915
1016import pytest
@@ -27,6 +33,51 @@ def _make_fake_wtsite():
2733 return ws
2834
2935
36+ def _make_file_page (ws , title ):
37+ """A File: page whose dump() does not attempt a real file download."""
38+ page = WtPage (wtSite = ws , title = title , do_init = False )
39+ page .exists = True
40+ page .dump = lambda config , _title = title : package .PagePackagePage (
41+ name = _title .split (":" )[- 1 ], namespace = "NS_FILE" , slots = {}
42+ )
43+ return page
44+
45+
46+ def _find_order_sensitive_file_titles ():
47+ """Two File: titles for which list(set([a, b])) != list(set([b, a])) under
48+ the current process's (randomized) string hash seed. Used to reproduce the
49+ hash-order dependent bug without relying on a specific PYTHONHASHSEED."""
50+ for i in range (1000 ):
51+ for j in range (i + 1 , 1000 ):
52+ a , b = f"File:F{ i } .png" , f"File:F{ j } .png"
53+ if list ({a , b }) != list ({b , a }):
54+ return a , b
55+ raise RuntimeError ("could not find an order-sensitive pair of file titles" )
56+
57+
58+ def _make_bundle_and_config (tmp_path , subdir , titles , include_files = True ):
59+ target = tmp_path / subdir
60+ target .mkdir (parents = True , exist_ok = True )
61+ bundle = package .PagePackageBundle (
62+ packages = {
63+ "TestPkg" : package .PagePackage (
64+ globalID = "org.test.TestPkg" ,
65+ description = "test package" ,
66+ version = "0.0.1" ,
67+ baseURL = "https://example.org/" ,
68+ )
69+ }
70+ )
71+ return package .PagePackageConfig (
72+ name = "TestPkg" ,
73+ config_path = target / "packages.json" ,
74+ content_path = target / "content" ,
75+ bundle = bundle ,
76+ titles = titles ,
77+ include_files = include_files ,
78+ )
79+
80+
3081def _make_config (tmp_path , clear_content_dir ):
3182 bundle = package .PagePackageBundle (
3283 packages = {
@@ -84,3 +135,99 @@ def test_create_page_package_honours_clear_content_dir(tmp_path, clear_content_d
84135 assert not marker .exists ()
85136 else :
86137 assert marker .exists ()
138+
139+
140+ def test_create_page_package_per_page_file_order_is_deterministic (tmp_path ):
141+ """Regression test for #172: the order in which a page's file references
142+ are discovered must not change where they end up in packages.json."""
143+ file_a , file_b = _find_order_sensitive_file_titles ()
144+ extra_file = "File:Extra.png"
145+
146+ def _run (subdir , reversed_refs ):
147+ ws = _make_fake_wtsite ()
148+ item1 = WtPage (wtSite = ws , title = "Item:OSW1" , do_init = False )
149+ item2 = WtPage (wtSite = ws , title = "Item:OSW2" , do_init = False )
150+ order = [file_b , file_a ] if reversed_refs else [file_a , file_b ]
151+ item1 .find_file_page_refs_in_slots = lambda slots = None : list (order )
152+ item2 .find_file_page_refs_in_slots = lambda slots = None : [extra_file ]
153+
154+ offline_pages = {
155+ "Item:OSW1" : item1 ,
156+ "Item:OSW2" : item2 ,
157+ file_a : _make_file_page (ws , file_a ),
158+ file_b : _make_file_page (ws , file_b ),
159+ extra_file : _make_file_page (ws , extra_file ),
160+ }
161+
162+ config = _make_bundle_and_config (
163+ tmp_path , subdir , titles = ["Item:OSW1" , "Item:OSW2" ]
164+ )
165+ ws .create_page_package (
166+ WtSite .CreatePagePackageParam (
167+ config = config ,
168+ offline_pages = offline_pages ,
169+ debug = False ,
170+ )
171+ )
172+ return json .loads (config .config_path .read_text (encoding = "utf-8" ))
173+
174+ data_1 = _run ("run1" , reversed_refs = False )
175+ data_2 = _run ("run2" , reversed_refs = True )
176+
177+ names_1 = [p ["name" ] for p in data_1 ["packages" ]["TestPkg" ]["pages" ]]
178+ names_2 = [p ["name" ] for p in data_2 ["packages" ]["TestPkg" ]["pages" ]]
179+
180+ assert names_1 == names_2
181+
182+ # Item:OSW1's own files sit between its dump and Item:OSW2's dump, and are
183+ # in sorted order.
184+ expected_file_names = sorted ([file_a .split (":" )[- 1 ], file_b .split (":" )[- 1 ]])
185+ assert names_1 [1 :3 ] == expected_file_names
186+
187+
188+ def test_create_page_package_shared_file_always_attaches_to_first_page (tmp_path ):
189+ """Regression test for #172: a file referenced by several pages must
190+ always be attached to the same (first-configured) page, regardless of the
191+ order in which get_page's parallel fetch happens to complete."""
192+ shared_file = "File:Shared.png"
193+
194+ def _run (subdir , reverse_fetch_order ):
195+ ws = _make_fake_wtsite ()
196+ item1 = WtPage (wtSite = ws , title = "Item:OSW1" , do_init = False )
197+ item2 = WtPage (wtSite = ws , title = "Item:OSW2" , do_init = False )
198+ item1 .find_file_page_refs_in_slots = lambda slots = None : [shared_file ]
199+ item2 .find_file_page_refs_in_slots = lambda slots = None : [shared_file ]
200+
201+ pages_by_title = {
202+ "Item:OSW1" : item1 ,
203+ "Item:OSW2" : item2 ,
204+ shared_file : _make_file_page (ws , shared_file ),
205+ }
206+
207+ def fake_get_page (param ):
208+ titles = list (param .titles )
209+ if reverse_fetch_order :
210+ titles = list (reversed (titles ))
211+ selected = [pages_by_title [t ] for t in titles ]
212+ return WtSite .GetPageResult (pages = selected , errors = [])
213+
214+ ws .get_page = fake_get_page
215+
216+ config = _make_bundle_and_config (
217+ tmp_path , subdir , titles = ["Item:OSW1" , "Item:OSW2" ]
218+ )
219+ ws .create_page_package (
220+ WtSite .CreatePagePackageParam (config = config , debug = False )
221+ )
222+ return json .loads (config .config_path .read_text (encoding = "utf-8" ))
223+
224+ data_normal = _run ("normal" , reverse_fetch_order = False )
225+ data_reversed = _run ("reversed" , reverse_fetch_order = True )
226+
227+ names_normal = [p ["name" ] for p in data_normal ["packages" ]["TestPkg" ]["pages" ]]
228+ names_reversed = [p ["name" ] for p in data_reversed ["packages" ]["TestPkg" ]["pages" ]]
229+
230+ assert names_normal == names_reversed
231+ # Shared.png must be attached to Item:OSW1 (first in config.titles), even
232+ # when Item:OSW2 is fetched first.
233+ assert names_normal == ["OSW1" , "Shared.png" , "OSW2" ]
0 commit comments