from __future__ import annotations import gzip import io import json import sqlite3 import tempfile import threading import unittest import zipfile from pathlib import Path from unittest.mock import patch from urllib.parse import urlencode from fastapi.testclient import TestClient import app import build_fulltext_db def create_fts_database(path: Path, rows: list[tuple[int, str]]) -> None: with sqlite3.connect(path) as connection: connection.execute("CREATE TABLE documents (doc_id TEXT PRIMARY KEY, summary TEXT NOT NULL)") connection.execute("CREATE TABLE postings (token_hash BLOB PRIMARY KEY, docs BLOB NOT NULL, doc_count INTEGER NOT NULL)") connection.execute("CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)") connection.execute("INSERT INTO metadata VALUES ('tokenizer_version', ?)", (app.TOKENIZER_VERSION,)) connection.execute("CREATE VIRTUAL TABLE content_fts USING fts5(content, tokenize='trigram', detail='full')") connection.execute("CREATE TABLE snippet_anchors (token_hash BLOB NOT NULL, doc_number INTEGER NOT NULL, char_offset INTEGER NOT NULL, PRIMARY KEY (token_hash, doc_number)) WITHOUT ROWID") for number, content in rows: connection.execute("INSERT INTO documents VALUES (?, ?)", (f"CCRD:{number}", content[:700])) connection.execute("INSERT INTO content_fts(rowid, content) VALUES (?, ?)", (number, content)) connection.executemany( "INSERT INTO snippet_anchors VALUES (?, ?, ?)", ((token_hash, number, char_offset) for token_hash, char_offset in build_fulltext_db.snippet_anchors(content).items()), ) for token in build_fulltext_db.index_tokens(content) | build_fulltext_db.literal_tokens(content): token_hash = __import__("hashlib").sha256(token.encode("utf-8")).digest()[:16] existing = connection.execute("SELECT docs FROM postings WHERE token_hash = ?", (token_hash,)).fetchone() numbers = [number] if existing is None else sorted(build_doc_numbers(existing[0]) | {number}) payload = build_fulltext_db.encode_doc_ids(numbers) connection.execute( "INSERT OR REPLACE INTO postings VALUES (?, ?, ?)", (token_hash, payload, len(numbers)), ) def build_doc_numbers(payload: bytes) -> set[int]: return app.decode_doc_numbers(payload) class FulltextIntegrationTests(unittest.TestCase): def test_match_cache_is_complete_isolated_mode_scoped_and_expiring(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) create_fts_database(root / "CCRD.sqlite3", [(1, "文化 革命"), (2, "文化革命")]) databases = app.FulltextDatabases(root) original = databases._search_source_connection try: with patch.object(databases, "_search_source_connection", wraps=original) as search, \ patch.object(app.time, "monotonic", return_value=100) as clock: first = databases.search_source("CCRD", "文化 革命") first.clear() self.assertEqual(databases.search_source("CCRD", "文化 革命"), {1, 2}) self.assertEqual(search.call_count, 1) self.assertEqual(databases.search_source("CCRD", "文化 革命", True), {1}) self.assertEqual(search.call_count, 2) clock.return_value = 401 self.assertEqual(databases.search_source("CCRD", "文化 革命"), {1, 2}) self.assertEqual(search.call_count, 3) self.assertEqual(databases.match_inflight, {}) finally: databases.close() def test_match_cache_obeys_entry_and_total_id_budgets_without_truncation(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) create_fts_database(root / "CCRD.sqlite3", [(1, "正文")]) databases = app.FulltextDatabases(root) try: with patch.object(app, "FULLTEXT_MATCH_CACHE_MAX_ENTRIES", 2), \ patch.object(app, "FULLTEXT_MATCH_CACHE_MAX_TOTAL_IDS", 3), \ patch.object(databases, "_search_source_connection", return_value={1, 2}): for query in ("甲", "乙", "丙"): self.assertEqual(databases.search_source("CCRD", query), {1, 2}) self.assertEqual(len(databases.match_cache), 1) self.assertLessEqual(sum(len(value[1]) for value in databases.match_cache.values()), 3) databases.match_cache.clear() with patch.object(databases, "_search_source_connection", return_value={1}) as search: for query in ("甲", "乙", "丙"): self.assertEqual(databases.search_source("CCRD", query), {1}) self.assertEqual(len(databases.match_cache), 2) databases.match_cache.clear() with patch.object(databases, "_search_source_connection", return_value={1, 2, 3, 4}) as search: for _ in range(2): self.assertEqual(databases.search_source("CCRD", "oversized"), {1, 2, 3, 4}) self.assertEqual(search.call_count, 2) self.assertEqual(databases.match_cache, {}) self.assertEqual(databases.match_inflight, {}) finally: databases.close() def test_concurrent_source_queries_share_complete_work_and_failures_release(self): import concurrent.futures with tempfile.TemporaryDirectory() as directory: root = Path(directory) create_fts_database(root / "CCRD.sqlite3", [(1, "正文")]) databases = app.FulltextDatabases(root) entered = threading.Event() joined = threading.Event() release = threading.Event() calls = [] def execute(*_args): calls.append(1) entered.set() self.assertTrue(release.wait(5)) return {1, 2, 3} try: with patch.object(databases, "_search_source_connection", side_effect=execute): with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: first = executor.submit(databases.search_source, "CCRD", "query") try: self.assertTrue(entered.wait(5)) pending = next(iter(databases.match_inflight.values())) original_wait = pending.wait def wait(timeout=None): joined.set() return original_wait(timeout) with patch.object(pending, "wait", side_effect=wait): second = executor.submit(databases.search_source, "CCRD", "query") self.assertTrue(joined.wait(5)) self.assertEqual(len(calls), 1) finally: release.set() self.assertEqual(first.result(timeout=5), {1, 2, 3}) self.assertEqual(second.result(timeout=5), {1, 2, 3}) self.assertEqual(len(calls), 1) self.assertEqual(databases.match_inflight, {}) with patch.object(databases, "_search_source_connection", side_effect=[RuntimeError("failed"), {9}]) as search: with self.assertRaises(RuntimeError): databases.search_source("CCRD", "failure") self.assertEqual(databases.match_inflight, {}) self.assertEqual(databases.search_source("CCRD", "failure"), {9}) self.assertEqual(search.call_count, 2) finally: databases.close() def test_fulltext_databases_can_load_in_memory(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "CCRD.sqlite3" create_fts_database(path, [(1, "开头 文化革命 正文")]) databases = app.FulltextDatabases(Path(directory), in_memory=True) try: self.assertEqual(databases.search_source("CCRD", "文化革命", exact=True), {1}) self.assertEqual(databases.search_source("CCRD", "文化 革命", exact=False), {1}) finally: databases.close() def test_short_latin_exact_and_latin_wildcards_do_not_miss_substrings(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "CCRD.sqlite3" create_fts_database(path, [(1, "smartphone abc123xyz"), (2, "alpha beta"), (3, "unrelated")]) databases = app.FulltextDatabases(Path(directory)) try: self.assertEqual(databases.search_source("CCRD", "abc", exact=True), {1}) self.assertEqual(databases.search_source("CCRD", "phone", exact=True), {1}) self.assertEqual(databases.search_source("CCRD", "abc*xyz", exact=True), {1}) self.assertEqual(databases.search_source("CCRD", "a?c*xyz", exact=True), {1}) finally: databases.close() def test_builder_creates_complete_reusable_database(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) corpus = root / "corpus" corpus.mkdir() (corpus / "one.txt").write_text("开头 文化革命 正文", encoding="utf-8") (corpus / "two.txt").write_text("全角ABC和手机", encoding="utf-8") records = [ {"doc_id": "TEST:1", "storage_root": "corpus", "storage_rel_path": "one.txt"}, {"doc_id": "TEST:2", "storage_root": "corpus", "storage_rel_path": "two.txt"}, ] with patch.object(build_fulltext_db, "BASE_DIR", root), patch.object(build_fulltext_db, "FULLTEXT_DIR", root / "fulltext"): build_fulltext_db.FULLTEXT_DIR.mkdir() build_fulltext_db.build_source_db("TEST", records) database = build_fulltext_db.FULLTEXT_DIR / "TEST.sqlite3" first_bytes = database.read_bytes() self.assertTrue(build_fulltext_db.database_ready(database, 2)) with sqlite3.connect(database) as connection: self.assertEqual(connection.execute("SELECT COUNT(*) FROM documents").fetchone()[0], 2) self.assertEqual(connection.execute("SELECT COUNT(*) FROM content_fts").fetchone()[0], 2) self.assertGreater(connection.execute("SELECT COUNT(*) FROM snippet_anchors").fetchone()[0], 0) self.assertEqual(connection.execute("SELECT value FROM metadata WHERE key='tokenizer_version'").fetchone()[0], app.TOKENIZER_VERSION) self.assertGreater(connection.execute("SELECT COUNT(*) FROM postings").fetchone()[0], 0) build_fulltext_db.build_source_db("TEST", records) self.assertEqual(database.read_bytes(), first_bytes) def test_builder_rejects_invalid_document_ids_without_publishing_database(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) corpus = root / "corpus" corpus.mkdir() (corpus / "one.txt").write_text("正文", encoding="utf-8") record = {"doc_id": "invalid", "storage_root": "corpus", "storage_rel_path": "one.txt"} with patch.object(build_fulltext_db, "BASE_DIR", root), patch.object(build_fulltext_db, "FULLTEXT_DIR", root / "fulltext"): build_fulltext_db.FULLTEXT_DIR.mkdir() with self.assertRaises(ValueError): build_fulltext_db.build_source_db("TEST", [record]) self.assertFalse((build_fulltext_db.FULLTEXT_DIR / "TEST.sqlite3").exists()) def test_builder_does_not_publish_database_with_missing_files(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) record = {"doc_id": "TEST:1", "storage_root": "corpus", "storage_rel_path": "missing.txt"} with patch.object(build_fulltext_db, "BASE_DIR", root), patch.object(build_fulltext_db, "FULLTEXT_DIR", root / "fulltext"): build_fulltext_db.FULLTEXT_DIR.mkdir() with self.assertRaises(RuntimeError): build_fulltext_db.build_source_db("TEST", [record]) self.assertFalse((build_fulltext_db.FULLTEXT_DIR / "TEST.sqlite3").exists()) self.assertFalse((build_fulltext_db.FULLTEXT_DIR / "TEST.sqlite3.tmp").exists()) def test_atomic_database_replacement_reopens_and_increments_generation(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) path = root / "CCRD.sqlite3" create_fts_database(path, [(1, "旧数据库文化")]) databases = app.FulltextDatabases(root) first_generation = databases.generation replacement = root / "replacement.sqlite3" create_fts_database(replacement, [(2, "新数据库革命资料")]) replacement.replace(path) databases.refresh() try: self.assertGreater(databases.generation, first_generation) self.assertEqual(databases.search_source("CCRD", "革命"), {2}) self.assertEqual(databases.search_source("CCRD", "文化"), set()) finally: databases.close() def test_refresh_builds_replacement_without_blocking_existing_queries(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) path = root / "CCRD.sqlite3" create_fts_database(path, [(1, "旧数据库文化")]) databases = app.FulltextDatabases(root, in_memory=True) replacement = root / "replacement.sqlite3" create_fts_database(replacement, [(2, "新数据库革命资料")]) replacement.replace(path) opening = threading.Event() release = threading.Event() original_open = databases._open_candidate def blocked_open(candidate_path): opening.set() self.assertTrue(release.wait(2)) return original_open(candidate_path) thread = threading.Thread(target=databases.refresh) try: with patch.object(databases, "_open_candidate", side_effect=blocked_open): thread.start() self.assertTrue(opening.wait(2)) self.assertEqual(databases.search_source("CCRD", "文化"), {1}) release.set() thread.join(2) self.assertFalse(thread.is_alive()) self.assertEqual(databases.search_source("CCRD", "革命"), {2}) finally: release.set() thread.join(2) databases.close() def test_active_query_keeps_retired_connection_alive_until_release(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) path = root / "CCRD.sqlite3" create_fts_database(path, [(1, "旧数据库文化")]) databases = app.FulltextDatabases(root, in_memory=True) entered = threading.Event() release = threading.Event() result = [] original_search = databases._search_source_connection def blocked_search(connection, query, exact, state): entered.set() self.assertTrue(release.wait(2)) return original_search(connection, query, exact, state) thread = threading.Thread(target=lambda: result.append(databases.search_source("CCRD", "文化"))) try: with patch.object(databases, "_search_source_connection", side_effect=blocked_search): thread.start() self.assertTrue(entered.wait(2)) replacement = root / "replacement.sqlite3" create_fts_database(replacement, [(2, "新数据库革命资料")]) replacement.replace(path) databases.refresh() self.assertTrue(databases.retired_connections) release.set() thread.join(2) self.assertFalse(thread.is_alive()) self.assertEqual(result, [{1}]) self.assertFalse(databases.retired_connections) self.assertEqual(databases.match_cache, {}) self.assertEqual(databases.search_source("CCRD", "革命"), {2}) finally: release.set() thread.join(2) databases.close() class HttpIntegrationTests(unittest.TestCase): def test_fulltext_in_memory_defaults_enabled(self): self.assertTrue(app.FULLTEXT_IN_MEMORY) @classmethod def setUpClass(cls): app.load_data() cls.original_databases = app.fulltext_databases app.fulltext_databases = app.FulltextDatabases(Path("/tmp/ccrd-test-no-index")) cls.client = TestClient(app.app) @classmethod def tearDownClass(cls): app.fulltext_databases.close() app.fulltext_databases = cls.original_databases def test_search_http_validation_and_option_isolation(self): valid = self.client.post("/api/search", json={"q": "文化 革命", "fulltext": False, "page_size": 1}) self.assertEqual(valid.status_code, 200) self.assertEqual(valid.json()["page_size"], 1) self.assertEqual(self.client.post("/api/search", json={"q": "x" * 257}).status_code, 422) self.assertEqual(self.client.post("/api/search", json={"page": 0}).status_code, 422) self.assertEqual(self.client.post("/api/search", json={"page_size": 201}).status_code, 422) self.assertEqual(self.client.post("/api/search", json={"min_size": -1}).status_code, 422) self.assertEqual(self.client.post("/api/search/missing", json={}).status_code, 404) def test_preview_snippet_and_download_http_contracts(self): doc_id, record = next(iter(app.record_map.items())) preview = self.client.get(f"/api/preview/{doc_id}") self.assertEqual(preview.status_code, 200) self.assertEqual(preview.json()["doc_id"], doc_id) snippet = self.client.get(f"/api/snippet/{doc_id}", params={"q": "文化", "exact": "false"}) self.assertEqual(snippet.status_code, 200) self.assertIn("snippet", snippet.json()) download = self.client.get(f"/api/download/{doc_id}") self.assertEqual(download.status_code, 200) self.assertTrue(download.headers["content-type"].startswith("text/plain")) self.assertIn("attachment", download.headers["content-disposition"]) self.assertEqual(download.content, app.get_doc_storage_path(record).read_bytes()) self.assertEqual(self.client.get("/api/preview/missing:1").status_code, 404) self.assertEqual(self.client.get("/api/snippet/missing:1").status_code, 404) self.assertEqual(self.client.get("/api/download/missing:1").status_code, 404) self.assertEqual(self.client.get(f"/api/snippet/{doc_id}", params={"q": "x" * 257}).status_code, 422) def test_json_and_form_zip_are_valid_and_enforce_limits(self): selected = sorted(app.records, key=lambda record: record["size"])[:2] ids = [record["doc_id"] for record in selected] for response in ( self.client.post("/api/zip", json={"doc_ids": ids}), self.client.post( "/api/zip-download", content=urlencode([("doc_id", ids[0]), ("doc_id", ids[1]), ("doc_id", ids[0])]), headers={"Content-Type": "application/x-www-form-urlencoded"}, ), ): self.assertEqual(response.status_code, 200) self.assertEqual(response.headers["content-type"], "application/zip") self.assertIn("attachment", response.headers["content-disposition"]) self.assertEqual(response.headers["cache-control"], "no-store") self.assertEqual(response.headers["content-encoding"], "identity") self.assertEqual(int(response.headers["content-length"]), len(response.content)) self.assertTrue(response.content.startswith(b"PK")) self.assertNotIn(b"PK\x06\x06", response.content) with zipfile.ZipFile(io.BytesIO(response.content)) as archive: self.assertEqual(len(archive.namelist()), 2) self.assertEqual(archive.testzip(), None) with self.assertRaises(zipfile.BadZipFile): with zipfile.ZipFile(io.BytesIO(response.content[:-22])) as truncated: truncated.testzip() self.assertEqual(self.client.post("/api/zip", json={"doc_ids": []}).status_code, 400) self.assertEqual(self.client.post("/api/zip", json={"doc_ids": ["x"] * 501}).status_code, 422) too_many = [("doc_id", f"CCRD:{index}") for index in range(501)] self.assertEqual(self.client.post( "/api/zip-download", content=urlencode(too_many), headers={"Content-Type": "application/x-www-form-urlencoded"}, ).status_code, 400) self.assertEqual(self.client.post("/api/zip-download", json={"doc_id": ids}).status_code, 415) self.assertEqual(self.client.get("/api/zip-download").status_code, 400) legacy = self.client.post( "/api/zip-download", content=urlencode([("doc_id", ids[0]), ("doc_id", ids[1])]), headers={"Content-Type": "application/x-www-form-urlencoded"}, follow_redirects=False, ) self.assertEqual(legacy.status_code, 303) self.assertTrue(legacy.headers["location"].startswith("/api/zip-download/")) legacy_download = self.client.get(legacy.headers["location"]) self.assertTrue(legacy_download.content.startswith(b"PK")) prepared = self.client.post("/api/zip-prepare", json={"doc_ids": ids}) self.assertEqual(prepared.status_code, 200) self.assertEqual(prepared.json()["file_count"], 2) download_url = prepared.json()["download_url"] for _attempt in range(2): downloaded = self.client.get(download_url, headers={"Accept-Encoding": "gzip"}) self.assertEqual(downloaded.status_code, 200) self.assertEqual(downloaded.headers["content-encoding"], "identity") self.assertTrue(downloaded.content.startswith(b"PK")) with zipfile.ZipFile(io.BytesIO(downloaded.content)) as archive: self.assertEqual(len(archive.namelist()), 2) self.assertIsNone(archive.testzip()) self.assertEqual(self.client.get("/api/zip-download/invalid-token").status_code, 404) def test_zip_disambiguates_duplicate_display_paths_and_deduplicates_ids(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) first = root / "first.txt" second = root / "second.txt" first.write_text("first", encoding="utf-8") second.write_text("second", encoding="utf-8") original_map = app.record_map app.record_map = { "CCRD:1": {"source": "CCRD", "display_name": 'same:name*?"<>|', "display_rel_path": "same/name.txt"}, "CCRD:2": {"source": "CCRD", "display_name": 'same:name*?"<>|', "display_rel_path": "same/name.txt"}, } paths = {"CCRD:1": first, "CCRD:2": second} try: with patch.object(app, "get_doc_storage_path", side_effect=lambda record: paths[next(key for key, value in app.record_map.items() if value is record)]): response = app.zip_response(["CCRD:1", "CCRD:1", "CCRD:2"]) self.assertEqual(response.status_code, 200) with open(response.path, "rb") as archive_file: payload = archive_file.read() self.assertNotIn(b"PK\x06\x06", payload) with zipfile.ZipFile(io.BytesIO(payload)) as archive: self.assertEqual(len(set(archive.namelist())), 2) self.assertTrue(all(not any(character in name for character in ':*?"<>|\\') for name in archive.namelist())) self.assertTrue(all(len(name.encode("utf-8")) <= 220 for name in archive.namelist())) self.assertEqual(sorted(archive.read(name).decode() for name in archive.namelist()), ["first", "second"]) finally: app.record_map = original_map def test_static_status_and_cors_contracts(self): self.assertEqual(self.client.get("/manifest.json").status_code, 200) self.assertIn("javascript", self.client.get("/sw.js").headers["content-type"]) status = self.client.get("/api/index-status") self.assertEqual(status.status_code, 200) self.assertEqual(status.json()["loaded_sources"], []) cors = self.client.options( "/api/search", headers={"Origin": "https://example.com", "Access-Control-Request-Method": "POST"}, ) self.assertEqual(cors.status_code, 200) self.assertEqual(cors.headers["access-control-allow-origin"], "*") self.assertEqual(self.client.get("/data/fulltext/CCRD.sqlite3").status_code, 404) self.assertEqual(self.client.get("/data/fulltext/CCRD.sqlite3.tmp").status_code, 404) def test_ping_is_empty_and_not_cached(self): response = self.client.get("/api/ping") self.assertEqual(response.status_code, 204) self.assertEqual(response.content, b"") self.assertEqual(response.headers["cache-control"], "no-store") def test_sources_extensions_folders_random_and_spa_routes(self): sources = self.client.get("/api/sources") self.assertEqual(sources.status_code, 200) self.assertEqual({item["slug"] for item in sources.json()}, {"CCRD", "CW"}) self.assertEqual(self.client.get("/api/extensions").json(), [{"name": "txt", "count": 47250}]) self.assertEqual(self.client.get("/api/extensions", params={"source": "CCRD"}).json(), [{"name": "txt", "count": 35586}]) folders = self.client.get("/api/folders/CCRD") self.assertEqual(folders.status_code, 200) self.assertIsInstance(folders.json(), list) contents = self.client.get("/api/folders/CCRD/contents") self.assertEqual(contents.status_code, 200) self.assertEqual(contents.json()["current_path"], "") random_doc = self.client.get("/api/random", params={"source": "CW"}) self.assertEqual(random_doc.status_code, 200) self.assertEqual(random_doc.json()["Source"], "CW") self.assertEqual(self.client.get("/api/random", params={"source": "missing"}).status_code, 404) # `sources` is the canonical filter and must not be silently ignored. self.assertEqual(self.client.get("/api/random", params={"sources": "missing"}).status_code, 404) self.assertEqual(self.client.get("/api/random", params={"sources": ["CW"]}).json()["Source"], "CW") self.assertEqual( self.client.get("/api/random", params=[("sources", "CCRD"), ("sources", "CW")]).status_code, 200 ) for route in ("/", "/CCRD", "/CW"): page = self.client.get(route) self.assertEqual(page.status_code, 200) self.assertIn("text/html", page.headers["content-type"]) self.assertIn("initial-sidebar-data", page.text) def test_unknown_sources_and_unknown_api_paths_return_json_404(self): for path in ("/api/folders/missing", "/api/folders/missing/contents"): response = self.client.get(path) self.assertEqual(response.status_code, 404, path) self.assertIn("application/json", response.headers["content-type"], path) self.assertEqual(response.json().get("error"), "source not found", path) for path in ("/api/nope", "/api/search", "/api/folders"): response = self.client.get(path) self.assertEqual(response.status_code, 404, path) self.assertIn("application/json", response.headers["content-type"], path) self.assertEqual(response.json().get("error"), "not found", path) def test_search_cache_ttl_is_longer_than_burst_idle_gap(self): self.assertGreaterEqual(app.SEARCH_CACHE_TTL_SECONDS, 300) def test_search_cache_returns_identical_payload(self): app.search_response_cache.clear() body = {"q": "server-timing-unique-query", "fulltext": False, "page_size": 1} first = self.client.post("/api/search", json=body) second = self.client.post("/api/search", json=body) self.assertEqual(first.status_code, 200) self.assertEqual(first.json(), second.json()) def test_missing_physical_files_return_stable_errors(self): original_map = app.record_map app.record_map = { "CCRD:missing": { "display_name": "missing", "display_rel_path": "missing.txt", "source_name": "CCRD", "storage_root": "CCRD", "storage_rel_path": "does-not-exist.txt", } } try: for endpoint in ("preview", "snippet", "download"): response = self.client.get(f"/api/{endpoint}/CCRD:missing") self.assertEqual(response.status_code, 404) self.assertIn("missing file", response.json()["error"]) finally: app.record_map = original_map class CorpusIntegrityTests(unittest.TestCase): def test_metadata_counts_and_every_storage_file_exists(self): payload = json.loads(gzip.decompress((Path(app.BASE_DIR) / "data/search_data.json.gz").read_bytes())) records = payload["records"] self.assertEqual(len(records), 47250) counts = {source["slug"]: source["count"] for source in payload["sources"]} self.assertEqual(counts, {"CCRD": 35586, "CW": 11664}) doc_ids = {record["doc_id"] for record in records} self.assertEqual(len(doc_ids), len(records)) missing = [ record["doc_id"] for record in records if not (Path(app.BASE_DIR) / record["storage_root"] / record["storage_rel_path"]).is_file() ] self.assertEqual(missing, []) # Every source TXT must be strict UTF-8 after the CW:28 conversion. A GB18030 # file here would be indexed by the builder only through its fallback path. non_utf8 = [] for record in records: path = Path(app.BASE_DIR) / record["storage_root"] / record["storage_rel_path"] try: path.read_bytes().decode("utf-8") except UnicodeDecodeError: non_utf8.append(record["doc_id"]) self.assertEqual(non_utf8, []) self.assertEqual(sum(record["source"] == "CCRD" for record in records), 35586) self.assertEqual(sum(record["source"] == "CW" for record in records), 11664) archive_keys = [(record["source"], record["display_rel_path"], record["doc_id"]) for record in records] self.assertEqual(len(archive_keys), len(set(archive_keys)))