Initial commit: SearXNG CLI Toolkit v1.6.0
CI / test (3.8) (push) Canceled after 0s
CI / test (3.9) (push) Canceled after 0s
CI / test (3.10) (push) Canceled after 0s
CI / test (3.11) (push) Canceled after 0s
CI / test (3.12) (push) Canceled after 0s

Multi-instance failover, exponential-backoff retry, SQLite cache, batch mode, domain filter, cross-engine dedup, result sorting, CSV export, structured logging, enhanced Markdown conversion, 155 pytest tests, Gitea Actions CI
This commit is contained in:
Metona Team
2026-08-01 17:02:34 +08:00
commit 0468dd4e9d
17 changed files with 4791 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
"""Tests for scripts/cache.py — SQLite-backed result cache.
Covers: cache key stability, key whitelist (format/auth excluded),
get/put roundtrip, TTL expiry, caller-shorter-TTL override, clear, stats,
and the ttl<=0 disable behavior.
"""
import sqlite3
import time
import cache
# ----- _make_key -----
def test_make_key_stable():
p = {"q": "test", "engines": "google,bing", "format": "json"}
k1 = cache._make_key(p)
k2 = cache._make_key(p)
assert k1 == k2
assert len(k1) == 64 # SHA-256 hex digest length
def test_make_key_excludes_format():
"""format is transient (output shape, not result set) — must not affect key."""
p1 = {"q": "test", "format": "json"}
p2 = {"q": "test", "format": "brief"}
assert cache._make_key(p1) == cache._make_key(p2)
def test_make_key_differs_on_query():
assert cache._make_key({"q": "python"}) != cache._make_key({"q": "rust"})
def test_make_key_differs_on_engines():
assert cache._make_key({"q": "x", "engines": "google"}) != \
cache._make_key({"q": "x", "engines": "bing"})
def test_make_key_differs_on_time_range():
assert cache._make_key({"q": "x", "time_range": "day"}) != \
cache._make_key({"q": "x", "time_range": "year"})
# ----- get / put -----
def test_put_get_roundtrip(isolated_cache):
params = {"q": "hello", "format": "json"}
result = {"results": [{"title": "Hi", "url": "https://example.com"}],
"number_of_results": 1}
cache.put(params, result, ttl_seconds=60)
got = cache.get(params, ttl_seconds=60)
assert got is not None
assert got["results"][0]["title"] == "Hi"
def test_get_miss_when_empty(isolated_cache):
assert cache.get({"q": "nope"}, ttl_seconds=60) is None
def test_get_expired(isolated_cache):
"""Entry older than TTL is a miss."""
params = {"q": "old", "format": "json"}
cache.put(params, {"results": []}, ttl_seconds=60)
# Backdate the entry so it's past TTL
with cache._connect(cache._cache_path()) as conn:
conn.execute(
"UPDATE search_cache SET created_at = ? WHERE key = ?",
(time.time() - 120, cache._make_key(params)),
)
conn.commit()
assert cache.get(params, ttl_seconds=60) is None
def test_get_caller_shorter_ttl_expires(isolated_cache):
"""Caller's shorter TTL overrides a longer stored TTL (immediate effect)."""
params = {"q": "ttl-test", "format": "json"}
cache.put(params, {"results": []}, ttl_seconds=3600)
with cache._connect(cache._cache_path()) as conn:
conn.execute(
"UPDATE search_cache SET created_at = ? WHERE key = ?",
(time.time() - 120, cache._make_key(params)),
)
conn.commit()
# stored TTL=3600 (still fresh by stored clock) but caller asks 60s -> expired
assert cache.get(params, ttl_seconds=60) is None
def test_get_caller_longer_ttl_uses_stored(isolated_cache):
"""Caller's longer TTL does NOT resurrect an expired-by-stored entry."""
params = {"q": "ttl2", "format": "json"}
cache.put(params, {"results": []}, ttl_seconds=60)
with cache._connect(cache._cache_path()) as conn:
conn.execute(
"UPDATE search_cache SET created_at = ? WHERE key = ?",
(time.time() - 120, cache._make_key(params)),
)
conn.commit()
# stored TTL=60 (expired), caller asks 3600 -> effective = min(3600,60)=60 -> expired
assert cache.get(params, ttl_seconds=3600) is None
# ----- clear -----
def test_clear_removes_entries(isolated_cache):
cache.put({"q": "x"}, {"results": []}, ttl_seconds=60)
removed = cache.clear()
assert removed == 1
assert cache.get({"q": "x"}, ttl_seconds=60) is None
def test_clear_empty_returns_zero(isolated_cache):
assert cache.clear() == 0
# ----- stats -----
def test_stats_reports_entries(isolated_cache):
cache.put({"q": "stats-test"}, {"results": []}, ttl_seconds=60)
s = cache.stats()
assert s["entries"] == 1
assert s["size_bytes"] > 0
assert "path" in s
assert s.get("oldest_created_at") is not None
assert s.get("newest_created_at") is not None
def test_stats_empty(isolated_cache):
s = cache.stats()
assert s["entries"] == 0
# ----- ttl<=0 disable -----
def test_get_ttl_zero_is_disabled(isolated_cache):
"""ttl_seconds=0 means caching off -> always miss even if stored."""
cache.put({"q": "disabled"}, {"results": []}, ttl_seconds=60)
assert cache.get({"q": "disabled"}, ttl_seconds=0) is None
def test_put_ttl_zero_is_noop(isolated_cache):
"""put with ttl<=0 should not store anything."""
cache.put({"q": "noop"}, {"results": []}, ttl_seconds=0)
assert cache.get({"q": "noop"}, ttl_seconds=60) is None
assert cache.stats()["entries"] == 0