"""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") == []