Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tests] JavaScript: extract searchindex.js-format test fixtures. #12102

Open
wants to merge 74 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
cf53a7c
[tests] JavaScript: extract searchindex.js-format test fixtures
jayaddison Mar 15, 2024
311c4f0
[tests] JavaScript: write Sphinx sources that produce acceptable sear…
jayaddison Mar 15, 2024
e0bee37
[tests] linting: add a ruff exclude filter for the JS fixture source …
jayaddison Mar 15, 2024
4340bbe
[tests] nitpick: reduce the changeset diff by retaining an index vari…
jayaddison Mar 16, 2024
40fc985
Add CHANGES.rst entry
jayaddison Mar 16, 2024
7e22d78
[tests] fixup: regenerate multiterm test search index
jayaddison Mar 16, 2024
8f753a8
[tests] refactor: use a separate index fixture directory per root
jayaddison Mar 16, 2024
dd1fde8
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Mar 16, 2024
695037d
[search] Refactor search code to improve testability
jayaddison Mar 16, 2024
58b5e90
[tests] expect duplicate result due to #11961
jayaddison Mar 16, 2024
c37bb53
[tests] run 'js-beautify -r ...' on each searchindex.js file
jayaddison Mar 16, 2024
8ac1b6c
[tests] Fixup: regenerate cpp searchindex.js
jayaddison Mar 16, 2024
e31a7bd
Merge branch 'issue-12099-prep/extract-js-search-index-fixtures' into…
jayaddison Mar 16, 2024
fbaff49
[utils] Add script to regenerate JavaScript test fixtures
jayaddison Mar 16, 2024
d619a52
[tests] nitpick: relocate project-related JavaScript files away from …
jayaddison Mar 16, 2024
b33d331
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Mar 18, 2024
55b6072
Merge branch 'issue-12099-utils/fixture-regeneration' into issue-1209…
jayaddison Mar 18, 2024
ee4316d
[tests] Regenerate test fixtures by running 'python utils/generate_js…
jayaddison Mar 18, 2024
0634502
Merge branch 'issue-12099-prep/extract-js-search-index-fixtures' into…
jayaddison Mar 18, 2024
0bc558a
[utils] Add script to regenerate JavaScript test fixtures
jayaddison Mar 16, 2024
a74bc15
[utils] Remove beautification step for generated searchindex.js files.
jayaddison Mar 22, 2024
a037867
Merge branch 'issue-12099-utils/fixture-regeneration' into refactor/e…
jayaddison Mar 22, 2024
68a6504
[tests] Regenerate searchindex.js files.
jayaddison Mar 22, 2024
564de92
Merge branch 'issue-12099-utils/fixture-regeneration' into issue-1209…
jayaddison Mar 22, 2024
64ecd41
[linting] Reformat using 'ruff format'.
jayaddison Mar 22, 2024
34752b1
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Mar 22, 2024
1addb80
[tests] Regenerate searchindex.js files.
jayaddison Mar 22, 2024
76cc8d5
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Mar 25, 2024
d66b7a2
Merge branch 'refactor/extract-search-result-gathering' into issue-12…
jayaddison Mar 26, 2024
01a4509
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Mar 26, 2024
262dcec
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 4, 2024
52ee7c4
[utils] Cleanup: remove unused function.
jayaddison Apr 4, 2024
916e5af
[tests] Add test case to confirm that JavaScript search fixtures rema…
jayaddison Apr 4, 2024
de6f7a0
[docs] Add debugging tip for JavaScript searchindex.js fixtures.
jayaddison Apr 4, 2024
ef8e4b0
[tests] Refactor: rewrite searchindex test using pytest parametrize.
jayaddison Apr 4, 2024
450aafe
[tests] Refactor: use temporary directory when regenerating search in…
jayaddison Apr 4, 2024
94b9fc5
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 4, 2024
d885288
Revert "[tests] nitpick: relocate project-related JavaScript files aw…
jayaddison Apr 4, 2024
6987c48
[tests] karma config: explicitly add language_data.js alongside docum…
jayaddison Apr 4, 2024
fac7694
[tests] nitpick: relocate file-read operations to same-code-line as h…
jayaddison Apr 4, 2024
a464bdf
[tests] nitpick / consistency: use current-file as base for both proj…
jayaddison Apr 4, 2024
5b6a479
[tests] refactor / cleanup: re-use existing TESTS_ROOT testutil const…
jayaddison Apr 4, 2024
fc76aef
[tests] search: indicate full path of search index file when it fails…
jayaddison Apr 4, 2024
e9614b9
[docs] fixup for JS search-index fixture paths.
jayaddison Apr 4, 2024
4d62b15
[docs] contributing: rewrite paragraph about JavaScript test fixtures.
jayaddison Apr 4, 2024
38bfc34
[docs] contributing: rewrite (again) paragraph about JavaScript test …
jayaddison Apr 4, 2024
51f9a4d
[tests] karma config: nitpick: move served files to top of files option.
jayaddison Apr 4, 2024
8c6c669
[search] nitpick: remove empty line from query function.
jayaddison Apr 4, 2024
5827576
[search] typo fixup: 'if' instead of 'is'.
jayaddison Apr 4, 2024
9773c0c
[utils] js fixtures: use '_build' as output dir instead of 'build'.
jayaddison Apr 4, 2024
66328bc
[utils] update path navigation steps during JavaScript fixture genera…
jayaddison Apr 4, 2024
7370d20
[tests] search: re-constrain test coverage for multiterm test case to…
jayaddison Apr 4, 2024
2ef85ba
[search] Rectification: rename misleadingly-named 'searchTerms' varia…
jayaddison Apr 4, 2024
cd1a368
[search] Rectification: test cases: rename misleadingly-named 'search…
jayaddison Apr 4, 2024
7e4b562
[tests] search: add end-to-end / search-result-aggregation test.
jayaddison Apr 4, 2024
ec4f236
[tests] search: fixup: initialize index-term variables from Search._i…
jayaddison Apr 4, 2024
9b709d7
[tests] search: migrate partial-match search to use fixture data.
jayaddison Apr 4, 2024
3d108c1
[tests] search: re-constrain cpp and partial test cases to term-searc…
jayaddison Apr 4, 2024
1b59d03
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 4, 2024
d0125c7
[tests] search: use Search.parseQuery function to match query terms t…
jayaddison Apr 4, 2024
fa64b98
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 5, 2024
0086771
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 5, 2024
3a6c784
[utils] js fixtures: create destination parent directory if it does n…
jayaddison Apr 5, 2024
14f1b98
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 6, 2024
d7e0e82
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 10, 2024
08460ca
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 13, 2024
2297164
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 17, 2024
9f5df2c
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 25, 2024
59d497d
Merge branch 'master' into issue-12099-prep/extract-js-search-index-f…
jayaddison Apr 28, 2024
cc8ee12
Fixup: relocate CHANGES entry for this feature to in-development sect…
jayaddison May 4, 2024
2cc1476
Fixup: add self-credit to CHANGES entry.
jayaddison May 4, 2024
7e92842
Tests: attempt to add explanatory content to JS test fixture source p…
jayaddison May 4, 2024
9c0f2b2
Tests: regenerate JavaScript fixtures by running 'python utils/genera…
jayaddison May 4, 2024
7acb3cd
Tests: compare regenerated-vs-existing search indices byte-for-byte i…
jayaddison May 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -31,6 +31,7 @@ doc/_build/
doc/locale/
tests/.coverage
tests/build/
tests/js/roots/*/_build
tests/test-server.lock
utils/regression_test.js

Expand Down
1 change: 1 addition & 0 deletions .ruff.toml
Expand Up @@ -9,6 +9,7 @@ exclude = [
".tox",
".venv",
"tests/roots/*",
"tests/js/roots/*",
"build/*",
"doc/_build/*",
"sphinx/search/*",
Expand Down
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -159,6 +159,7 @@ Testing
servers. As a side-effect, this removes the need for test server lockfiles,
meaning that any remaining ``tests/test-server.lock`` files can safely be
deleted.
* karma: refactor HTML search tests to use fixtures generated by Sphinx.
jayaddison marked this conversation as resolved.
Show resolved Hide resolved

Release 7.2.6 (released Sep 13, 2023)
=====================================
Expand Down
6 changes: 6 additions & 0 deletions doc/internals/contributing.rst
Expand Up @@ -338,3 +338,9 @@ Debugging tips
Minified files in ``sphinx/search/minified-js/*.js`` are generated from
non-minified ones using ``uglifyjs`` (installed via npm), with ``-m``
option to enable mangling.

* The ``searchindex.js`` files found in the ``tests/js/fixtures/*`` directories
are generated by using the standard Sphinx HTML builder on the corresponding
input projects found in ``tests/js/roots/*``. The fixtures provide test data
used by the Sphinx JavaScript unit tests, and can be regenerated by running
the ``utils/generate_js_fixtures.py`` script.
2 changes: 2 additions & 0 deletions karma.conf.js
Expand Up @@ -15,7 +15,9 @@ module.exports = function(config) {

// list of files / patterns to load in the browser
files: [
{ pattern: 'tests/js/fixtures/**/*.js', included: false, served: true },
'tests/js/documentation_options.js',
'tests/js/language_data.js',
'sphinx/themes/basic/static/doctools.js',
'sphinx/themes/basic/static/searchtools.js',
'sphinx/themes/basic/static/sphinx_highlight.js',
Expand Down
31 changes: 20 additions & 11 deletions sphinx/themes/basic/static/searchtools.js
Expand Up @@ -268,16 +268,7 @@ const Search = {
else Search.deferQuery(query);
},

/**
* execute search (requires search index to be loaded)
*/
query: (query) => {
const filenames = Search._index.filenames;
const docNames = Search._index.docnames;
const titles = Search._index.titles;
const allTitles = Search._index.alltitles;
const indexEntries = Search._index.indexentries;

_parseQuery: (query) => {
// stem the search terms and add them to the correct list
const stemmer = new Stemmer();
const searchTerms = new Set();
Expand Down Expand Up @@ -313,6 +304,19 @@ const Search = {
// console.info("required: ", [...searchTerms]);
// console.info("excluded: ", [...excludedTerms]);

return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];
},

/**
* execute search (requires search index to be loaded)
*/
_performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => {
const filenames = Search._index.filenames;
const docNames = Search._index.docnames;
const titles = Search._index.titles;
const allTitles = Search._index.alltitles;
const indexEntries = Search._index.indexentries;

// Collect multiple result groups to be sorted separately and then ordered.
// Each is an array of [docname, title, anchor, descr, score, filename].
const normalResults = [];
Expand Down Expand Up @@ -394,7 +398,12 @@ const Search = {
return acc;
}, []);

results = results.reverse();
return results.reverse();
},

query: (query) => {
const searchParameters = Search._parseQuery(query);
const results = Search._performSearch(...searchParameters);

// for debugging
//Search.lastresults = results.slice(); // a copy
Expand Down
1 change: 1 addition & 0 deletions tests/js/fixtures/cpp/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/js/fixtures/multiterm/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/js/fixtures/partial/searchindex.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions tests/js/language_data.js
@@ -0,0 +1,26 @@
/*
* language_data.js
* ~~~~~~~~~~~~~~~~
*
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments in this file seem a little misleading? This is really just test scaffolding AFAICT

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mm, yep - this is an interesting case - it's an evaluated copy of a template file from the basic theme. That's me attempting to make the JavaScript test environment realistic, but it is duplicative (and a bit confusing).

*
* :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/

var stopwords = [];


/* Non-minified version is copied as a separate JS file, if available */

/**
* Dummy stemmer for languages without stemming rules.
*/
var Stemmer = function() {
this.stemWord = function(w) {
return w;
}
}

Empty file added tests/js/roots/cpp/conf.py
Empty file.
5 changes: 5 additions & 0 deletions tests/js/roots/cpp/index.rst
@@ -0,0 +1,5 @@
This is a sample C++ project used to generate a search engine index fixture.

.. cpp:class:: public Sphinx

The description of Sphinx class.
Empty file.
4 changes: 4 additions & 0 deletions tests/js/roots/multiterm/index.rst
@@ -0,0 +1,4 @@
Main Page
=========

Welcome to the... main page!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally use this opportunity to add a bit of context about what this file tests.

Empty file added tests/js/roots/partial/conf.py
Empty file.
4 changes: 4 additions & 0 deletions tests/js/roots/partial/index.rst
@@ -0,0 +1,4 @@
sphinx_utils module
===================

Useful utilities.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, I would personally use this opportunity to add a bit of context about what this file tests.

93 changes: 53 additions & 40 deletions tests/js/searchtools.js
@@ -1,20 +1,20 @@
describe('Basic html theme search', function() {

function loadFixture(name) {
picnixz marked this conversation as resolved.
Show resolved Hide resolved
req = new XMLHttpRequest();
req.open("GET", `base/tests/js/fixtures/${name}`, false);
req.send(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing you used XMLHttpRequest() because you want to make a synchronous? Should probably add a comment about that.

Apparently using XMLHttpRequest this way is discouraged and might be deprecated at some point, but they've been saying that since 2014. 😉 It's probably fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that's good to know :) Yep; we don't want the test to proceed until the fixture has definitely loaded -- I'm inclined to use a straightforward (even if seemingly dated) approach here.

return req.responseText;
}

describe('terms search', function() {

it('should find "C++" when in index', function() {
index = {
docnames:["index"],
filenames:["index.rst"],
terms:{'c++':0},
titles:["<no title>"],
titleterms:{}
}
Search.setIndex(index);
searchterms = ['c++'];
excluded = [];
terms = index.terms;
titleterms = index.titleterms;
eval(loadFixture("cpp/searchindex.js"));
jayaddison marked this conversation as resolved.
Show resolved Hide resolved

[/* first-ignored */, searchterms, excluded, /*rest-ignored*/] = Search._parseQuery('C++');
terms = Search._index.terms;
titleterms = Search._index.titleterms;

hits = [[
"index",
Expand All @@ -28,22 +28,11 @@ describe('Basic html theme search', function() {
});

it('should be able to search for multiple terms', function() {
index = {
alltitles: {
'Main Page': [[0, 'main-page']],
},
docnames:["index"],
filenames:["index.rst"],
terms:{main:0, page:0},
titles:["Main Page"],
titleterms:{ main:0, page:0 }
}
Search.setIndex(index);

searchterms = ['main', 'page'];
excluded = [];
terms = index.terms;
titleterms = index.titleterms;
eval(loadFixture("multiterm/searchindex.js"));
jayaddison marked this conversation as resolved.
Show resolved Hide resolved

[/* first-ignored */, searchterms, excluded, /*rest-ignored*/] = Search._parseQuery('main page');
terms = Search._index.terms;
titleterms = Search._index.titleterms;
hits = [[
'index',
'Main Page',
Expand All @@ -55,18 +44,11 @@ describe('Basic html theme search', function() {
});

it('should partially-match "sphinx" when in title index', function() {
index = {
docnames:["index"],
filenames:["index.rst"],
terms:{'useful': 0, 'utilities': 0},
titles:["sphinx_utils module"],
titleterms:{'sphinx_utils': 0}
}
Search.setIndex(index);
searchterms = ['sphinx'];
excluded = [];
terms = index.terms;
titleterms = index.titleterms;
eval(loadFixture("partial/searchindex.js"));

[/* first-ignored */, searchterms, excluded, /*rest-ignored*/] = Search._parseQuery('sphinx');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally use underscores in these cases but it's not that big a deal.

Suggested change
[/* first-ignored */, searchterms, excluded, /*rest-ignored*/] = Search._parseQuery('sphinx');
[_, searchterms, excluded, _] = Search._parseQuery('sphinx');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JavaScript's unpacking behaviour here seems somewhat inconsistent to me; the value on the left is a single item from the value on the right-hand-side, and yet the last value (the second _ in your suggestion) is assigned all of the remaining items. I was hoping for a way to retain that information for future readers/copiers in case they attempted to make adjustments that wouldn't work as they expect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, you learn something new every day. ;) I might use an underscore just for the first element then. But as I said above, not a big deal.

terms = Search._index.terms;
titleterms = Search._index.titleterms;

hits = [[
"index",
Expand All @@ -81,6 +63,37 @@ describe('Basic html theme search', function() {

});

describe('aggregation of search results', function() {

it('should combine document title and document term matches', function() {
eval(loadFixture("multiterm/searchindex.js"));

searchParameters = Search._parseQuery('main page');

// fixme: duplicate result due to https://github.com/sphinx-doc/sphinx/issues/11961
hits = [
[
'index',
'Main Page',
'',
null,
15,
'index.rst'
],
[
'index',
'Main Page',
'#main-page',
null,
100,
'index.rst'
]
];
expect(Search._performSearch(...searchParameters)).toEqual(hits);
});

});

});

describe("htmlToText", function() {
Expand Down
20 changes: 20 additions & 0 deletions tests/test_search.py
Expand Up @@ -2,6 +2,7 @@

import json
import warnings
from hashlib import sha256
from io import BytesIO

import pytest
Expand All @@ -10,6 +11,8 @@

from sphinx.search import IndexBuilder

from tests.utils import TESTS_ROOT


class DummyEnvironment:
def __init__(self, version, domains):
Expand Down Expand Up @@ -341,3 +344,20 @@ def assert_is_sorted(item, path):
# Pretty print the index. Only shown by pytest on failure.
print(f'searchindex.js contents:\n\n{json.dumps(index, indent=2)}')
assert_is_sorted(index, '')


@pytest.mark.parametrize('directory', (
directory for directory in (TESTS_ROOT / 'js' / 'roots').iterdir()
))
def test_check_js_search_indexes(make_app, sphinx_test_tempdir, directory):
app = make_app('html', srcdir=directory, builddir=sphinx_test_tempdir / directory.name)
app.build()

fresh_searchindex = (app.outdir / 'searchindex.js')
fresh_hash = sha256(fresh_searchindex.read_bytes()).digest()
jayaddison marked this conversation as resolved.
Show resolved Hide resolved

existing_searchindex = (TESTS_ROOT / 'js' / 'fixtures' / directory.name / 'searchindex.js')
existing_hash = sha256(existing_searchindex.read_bytes()).digest()
jayaddison marked this conversation as resolved.
Show resolved Hide resolved

msg = f"Search index fixture {existing_searchindex} does not match regenerated copy."
assert fresh_hash == existing_hash, msg
26 changes: 26 additions & 0 deletions utils/generate_js_fixtures.py
@@ -0,0 +1,26 @@
#!/usr/bin/env python3

import subprocess
from pathlib import Path

SPHINX_ROOT = Path(__file__).resolve().parent.parent
TEST_JS_FIXTURES = SPHINX_ROOT / 'tests' / 'js' / 'fixtures'
TEST_JS_ROOTS = SPHINX_ROOT / 'tests' / 'js' / 'roots'


def build(srcdir: Path) -> None:
cmd = ('sphinx-build', '-E', '-q', '-b', 'html', f'{srcdir}', f'{srcdir}/_build')
subprocess.run(cmd, check=True, capture_output=True)


for directory in TEST_JS_ROOTS.iterdir():
searchindex = directory / '_build' / 'searchindex.js'
destination = TEST_JS_FIXTURES / directory.name / 'searchindex.js'

print(f'Building {directory} ... ', end='')
build(directory)
print('done')

print(f'Moving {searchindex} to {destination} ... ', end='')
searchindex.replace(destination)
print('done')