@@ -304,3 +304,40 @@ def test_parallel(app):
304304 app .build ()
305305 index = load_searchindex (app .outdir / 'searchindex.js' )
306306 assert index ['docnames' ] == ['index' , 'nosearch' , 'tocitem' ]
307+
308+
309+ @pytest .mark .sphinx (testroot = 'search' )
310+ def test_search_index_is_deterministic (app ):
311+ lists_not_to_sort = {
312+ # Each element of .titles is related to the element of .docnames in the same position.
313+ # The ordering is deterministic because .docnames is sorted.
314+ '.titles' ,
315+ # Each element of .filenames is related to the element of .docnames in the same position.
316+ # The ordering is deterministic because .docnames is sorted.
317+ '.filenames' ,
318+ }
319+
320+ # In the search index, titles inside .alltitles are stored as a tuple of
321+ # (document_idx, title_anchor). Tuples are represented as lists in JSON,
322+ # but their contents must not be sorted. We cannot sort them anyway, as
323+ # document_idx is an int and title_anchor is a str.
324+ def is_title_tuple_type (item ):
325+ return len (item ) == 2 and isinstance (item [0 ], int ) and isinstance (item [1 ], str )
326+
327+ def assert_is_sorted (item , path ):
328+ err_path = path if path else '<root>'
329+ if isinstance (item , dict ):
330+ assert list (item .keys ()) == sorted (item .keys ()), f'{ err_path } is not sorted'
331+ for key , value in item .items ():
332+ assert_is_sorted (value , f'{ path } .{ key } ' )
333+ elif isinstance (item , list ):
334+ if not is_title_tuple_type (item ) and path not in lists_not_to_sort :
335+ assert item == sorted (item ), f'{ err_path } is not sorted'
336+ for i , child in enumerate (item ):
337+ assert_is_sorted (child , f'{ path } [{ i } ]' )
338+
339+ app .builder .build_all ()
340+ index = load_searchindex (app .outdir / 'searchindex.js' )
341+ # Pretty print the index. Only shown by pytest on failure.
342+ print (f'searchindex.js contents:\n \n { json .dumps (index , indent = 2 )} ' )
343+ assert_is_sorted (index , '' )
0 commit comments