"""Tests for graphify/benchmark.py.""" from __future__ import annotations import json import pytest import networkx as nx from networkx.readwrite import json_graph from graphify.benchmark import run_benchmark, print_benchmark, _query_subgraph_tokens, _SAMPLE_QUESTIONS, _safe, _hr def _make_graph() -> nx.Graph: G = nx.Graph() G.add_node("n1", label="authentication", source_file="auth.py", source_location="L1", community=0) G.add_node("n2", label="api_handler", source_file="api.py", source_location="L5", community=0) G.add_node("n3", label="main_entry", source_file="main.py", source_location="L1", community=1) G.add_node("n4", label="error_handler", source_file="errors.py", source_location="L1", community=1) G.add_node("n5", label="database_layer", source_file="db.py", source_location="L1", community=2) G.add_edge("n1", "n2", relation="calls", confidence="INFERRED") G.add_edge("n2", "n3", relation="imports", confidence="EXTRACTED") G.add_edge("n3", "n4", relation="uses", confidence="EXTRACTED") G.add_edge("n5", "n2", relation="provides", confidence="EXTRACTED") return G def _write_graph(G: nx.Graph, path) -> None: data = json_graph.node_link_data(G, edges="links") path.write_text(json.dumps(data)) # --- _query_subgraph_tokens --- def test_query_returns_positive_for_matching_question(): G = _make_graph() tokens = _query_subgraph_tokens(G, "how does authentication work") assert tokens > 0 def test_query_returns_zero_for_no_match(): G = _make_graph() tokens = _query_subgraph_tokens(G, "xyzzy plugh zorkmid") assert tokens == 0 def test_query_bfs_expands_neighbors(): G = _make_graph() # "authentication" matches n1, BFS depth=3 should reach n2, n3, n4 tokens_deep = _query_subgraph_tokens(G, "authentication", depth=3) tokens_shallow = _query_subgraph_tokens(G, "authentication", depth=1) assert tokens_deep >= tokens_shallow def test_query_keeps_short_non_english_terms(): G = nx.Graph() G.add_node("frontend", label="前端", source_file="docs/前端.md", source_location="L1", community=0) tokens = _query_subgraph_tokens(G, "前端", depth=1) assert tokens > 0 # --- run_benchmark --- def test_run_benchmark_returns_reduction(tmp_path): G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) result = run_benchmark(str(graph_file), corpus_words=10_000) assert "reduction_ratio" in result assert result["reduction_ratio"] > 1.0 def test_run_benchmark_corpus_tokens_proportional(tmp_path): G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) r1 = run_benchmark(str(graph_file), corpus_words=1_000) r2 = run_benchmark(str(graph_file), corpus_words=10_000) # corpus_tokens scales linearly with corpus_words (within integer-division rounding) assert abs(r2["corpus_tokens"] - r1["corpus_tokens"] * 10) <= r1["corpus_tokens"] def test_run_benchmark_per_question_list(tmp_path): G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) result = run_benchmark(str(graph_file), corpus_words=5_000, questions=["how does authentication work", "what is the main entry"]) assert len(result["per_question"]) >= 1 for p in result["per_question"]: assert "question" in p assert "query_tokens" in p assert "reduction" in p def test_run_benchmark_estimates_corpus_if_no_words(tmp_path): G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) result = run_benchmark(str(graph_file), corpus_words=None) assert result["corpus_words"] > 0 def test_run_benchmark_error_on_empty_graph(tmp_path): G = nx.Graph() graph_file = tmp_path / "empty.json" _write_graph(G, graph_file) result = run_benchmark(str(graph_file), corpus_words=1_000) assert "error" in result def test_run_benchmark_includes_node_edge_counts(tmp_path): G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) result = run_benchmark(str(graph_file), corpus_words=5_000) assert result["nodes"] == G.number_of_nodes() assert result["edges"] == G.number_of_edges() # --- print_benchmark --- def test_print_benchmark_no_crash(tmp_path, capsys): G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) result = run_benchmark(str(graph_file), corpus_words=5_000) print_benchmark(result) out = capsys.readouterr().out assert "reduction" in out.lower() assert "x" in out def test_print_benchmark_error_message(capsys): print_benchmark({"error": "test error message"}) out = capsys.readouterr().out assert "test error message" in out # --- cp1252 / Windows-console encoding compatibility (regression for #?) --- # print_benchmark previously crashed on Windows consoles (cp1252) because it # unconditionally printed U+2500 and U+2192. _safe() falls back to ASCII when # stdout cannot encode the glyph. def test_safe_returns_unicode_when_encodable(): import io, sys real_stdout = sys.stdout try: sys.stdout = io.TextIOWrapper(io.BytesIO(), encoding="utf-8") assert _safe("→", "->") == "→" assert _hr(5) == "─" * 5 finally: sys.stdout = real_stdout def test_safe_falls_back_when_unencodable(): import io, sys real_stdout = sys.stdout try: sys.stdout = io.TextIOWrapper(io.BytesIO(), encoding="cp1252") assert _safe("→", "->") == "->" assert _hr(5) == "-" * 5 finally: sys.stdout = real_stdout def test_print_benchmark_survives_cp1252_stdout(tmp_path, monkeypatch, capsys): """Regression: U+2500 / U+2192 used to crash with UnicodeEncodeError on cp1252.""" import io, sys G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) result = run_benchmark(str(graph_file), corpus_words=5_000) # Replace stdout with a strict cp1252 stream — same behaviour as the # legacy Windows console that surfaced this bug. cp1252_stdout = io.TextIOWrapper(io.BytesIO(), encoding="cp1252", errors="strict") monkeypatch.setattr(sys, "stdout", cp1252_stdout) print_benchmark(result) # must not raise UnicodeEncodeError cp1252_stdout.flush() written = cp1252_stdout.buffer.getvalue().decode("cp1252") assert "reduction" in written.lower() # ASCII fallbacks must be present, fancy glyphs must not. assert "─" not in written assert "→" not in written def test_run_benchmark_rejects_oversized_graph(monkeypatch, tmp_path): """#F4: run_benchmark must refuse to read a graph.json that exceeds the size cap before parsing it into memory.""" G = _make_graph() graph_file = tmp_path / "graph.json" _write_graph(G, graph_file) monkeypatch.setattr("graphify.security._MAX_GRAPH_FILE_BYTES", 8) with pytest.raises(ValueError, match="exceeds"): run_benchmark(str(graph_file))