Files
tinyproject-monorepo/prg/sjaur/tests/test_search.py
T
2026-10-02 15:29:35 +02:00

202 lines
7.3 KiB
Python

"""Tests for the search engine: ranking, fuzzy matching, and @maintainer."""
from __future__ import annotations
from sjaur.query import parse_query
from sjaur.search import MAINTAINER_MATCH_THRESHOLD, SearchEngine
def names(results):
return [result.package.name for result in results]
def test_exact_name_match_first(engine):
results = engine.search(parse_query("firefox"))
assert names(results)[0] == "firefox"
# All firefox variants are found.
assert {"firefox", "firefox-nightly", "firefox-wayland"} <= set(names(results))
def test_name_prefix_ranks_above_substring(engine):
results = engine.search(parse_query("fire"))
top = names(results)
assert "firefox" in top[:3] # prefix match
# The substring-only "firefox-extension-uorigin" must not outrank the
# prefix matches.
assert top.index("firefox") < top.index("firefox-extension-uorigin")
def test_typo_still_finds_package(engine):
results = engine.search(parse_query("firfox"))
assert "firefox" in names(results)
def test_description_match(engine):
results = engine.search(parse_query("office productivity"))
assert "libreoffice-fresh" in names(results)
def test_description_tokens_all_must_appear(engine):
results = engine.search(parse_query("productivity suite"))
assert names(results)[0] == "libreoffice-fresh"
def test_maintainer_field_contributes_to_normal_search(engine):
results = engine.search(parse_query("alice"))
assert "fzf" in names(results)
def test_maintainer_operator_fuzzy(engine):
results = engine.search(parse_query("@max_musterman"))
found = names(results)
# Fuzzy: similar maintainers match despite the typo.
assert "python-pytest" in found # max_musterman123
assert "firefox-wayland" in found # max_mustermann
# Unrelated maintainers must not appear.
assert "firefox" not in found
assert "chromium" not in found
def test_maintainer_operator_with_package_query(engine):
results = engine.search(parse_query("@jonas browser"))
found = names(results)
assert {"firefox", "chromium"} <= set(found)
# Maintained by others.
assert "python-pytest" not in found
def test_maintainer_query_boosts_maintainer_matches(engine):
# "jonas" maintains firefox and chromium; both rank top for the plain
# text search, and the @-combined search scores at least as high overall.
plain = engine.search(parse_query("jonas"))
combined = engine.search(parse_query("@jonas jonas"))
assert names(plain)[0] == "firefox" # shorter name wins the prefix tie
assert names(combined)[0] in ("firefox", "chromium")
assert combined[0].score >= plain[0].score
def test_maintainer_below_threshold_is_excluded(engine):
results = engine.search(parse_query("@zzzzzzzzzz"))
assert results == []
def test_orphan_packages_have_no_maintainer_matches(engine):
# An empty (orphaned) maintainer cannot fuzzy-match an @query, but the
# package itself is still reachable through a normal search.
results = engine.search(parse_query("@orph"))
found = names(results)
assert "python-editor" in found # maintainer "orphan"
assert "libreoffice-fresh" not in found
assert "libreoffice-fresh" in names(engine.search(parse_query("office")))
def test_combined_scoring_formula(engine):
# With a maintainer query, maintainer similarity carries most of the
# weight but the package score still orders results.
results = engine.search(parse_query("@jonas standalone"))
firefox = next(r for r in results if r.package.name == "firefox")
chromium = next(r for r in results if r.package.name == "chromium")
assert firefox.score > 0 and chromium.score > 0
# firefox's description contains "standalone", chromium's does not.
assert firefox.score > chromium.score
def test_provides_field_is_searched(engine):
results = engine.search(parse_query("chromium-browser"))
assert names(results)[0] == "chromium"
def test_dependencies_are_searched_with_lower_priority(engine):
results = engine.search(parse_query("pytest"))
# python-pytest matches by name; anything matching only via depends must
# not outrank it.
assert names(results)[0] == "python-pytest"
def test_empty_results(engine):
assert engine.search(parse_query("zzzznotapackagezzzz")) == []
def test_empty_query_returns_popular_packages(engine):
results = engine.search(parse_query(""))
top = names(results)
assert len(top) > 0
# Most-voted package first.
votes = [r.package.votes for r in results]
assert votes == sorted(votes, reverse=True)
def test_results_are_deterministic(engine):
first = engine.search(parse_query("web browser"))
second = engine.search(parse_query("web browser"))
assert names(first) == names(second)
assert [r.score for r in first] == [r.score for r in second]
def test_limit(engine):
results = engine.search(parse_query("firefox"), limit=2)
assert len(results) == 2
def test_whitespace_only_query_behaves_like_empty(engine):
results = engine.search(parse_query(" "))
assert results == engine.search(parse_query(""))
def test_maintainer_threshold_is_fuzzy_not_exact():
# The threshold must be low enough that small typos still match.
assert MAINTAINER_MATCH_THRESHOLD < 60.0
def test_index_is_built_once_and_reused(engine):
# Repeated searches must not rebuild the index (same engine object).
first = engine.search(parse_query("firefox"))
assert engine._names is not None
second = engine.search(parse_query("firefox"))
assert names(first) == names(second)
def test_repo_and_aur_packages_of_same_name_are_both_kept():
from conftest import make_package
aur = make_package("python-editor", votes=42)
repo = make_package("python-editor", votes=0)
object.__setattr__(repo, "repository", "extra")
combined = SearchEngine([aur, repo])
assert len(combined.packages) == 2
# Both survive a search and rank identically (votes tie-break first).
results = combined.search(parse_query("python-editor"))
assert [r.package.repository for r in results if r.package.name == "python-editor"] == [
"aur",
"extra",
]
def test_quick_filter_returns_matching_subset(engine):
from conftest import make_package
from sjaur.search import SearchResult
base = engine.search(parse_query("firefox"))
narrowed = engine.quick_filter(base, "firefo")
assert {r.package.name for r in narrowed} <= {r.package.name for r in base}
assert "firefox" in {r.package.name for r in narrowed}
# Fuzzy-only hits survive filtering by the same text (no flicker),
# and correcting the typo keeps real name matches while dropping
# unrelated fuzzy noise.
typo = engine.search(parse_query("firfox"))
assert engine.quick_filter(typo, "firfox") != []
corrected = engine.quick_filter(typo, "firefox")
assert "firefox" in {r.package.name for r in corrected}
assert "ffbox" not in {r.package.name for r in corrected}
# Multi-word queries require every token to be present.
web = engine.search(parse_query("web"))
assert engine.quick_filter(web, "web browser") != []
# Empty text keeps everything; unknown packages are dropped.
assert engine.quick_filter(base, "") == base
stranger = SearchResult(package=make_package("not-in-engine"), score=1.0)
assert engine.quick_filter([stranger], "firefox") == []