mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
e5f263ba98
Three independent Windows compatibility fixes shipped together because they
all surface during the same first /graphify run on Windows.
graphify/benchmark.py
print_benchmark() unconditionally printed U+2500 (box-drawing) and U+2192
(rightwards arrow), which UnicodeEncodeError'd on stdouts that can't encode
them — most notably the legacy Windows console at cp1252. New _safe()
helper falls back to ASCII when the active stdout encoding can't carry the
glyph; _hr() uses it. Two regression tests cover both paths and prove
print_benchmark survives a cp1252-strict stream.
graphify/extract.py
ProcessPoolExecutor on Windows uses spawn, so worker subprocesses
re-import the calling __main__. When the caller is `python -c "..."` or a
script without an `if __name__ == "__main__":` guard, the workers
recursively spawn themselves and the pool dies. The user-visible failure
was a 290-line traceback ending in BrokenProcessPool, hiding the actual
cause. _extract_parallel now catches BrokenProcessPool, prints a one-line
warning that names the __main__-guard idiom, and returns False so the
public extract() routes to the existing _extract_sequential fallback. Two
tests cover the parallel-returns-False contract and the sequential
fallback wiring.
graphify/skill-windows.md
Every `python -c "..."` block (30 in total) is replaced with a
Write+run+delete pattern using PowerShell's literal here-string @'...'@.
The old form was a quote-escaping minefield: any double-quote inside the
Python source had to be backslash-escaped for the shell, and PowerShell's
parser ate them inconsistently — failing on f-strings like
`f'AST: {len(result["nodes"])} nodes'`. The new form passes Python source
to disk literally, so what the model writes is what Python sees. The AST
step's script template now includes an explicit `if __name__ == "__main__":`
guard so multi-core extraction works even before the runtime fallback above
kicks in. All 31 resulting heredoc blocks parse cleanly under
`ast.parse`.
Co-authored-by: Nauman Hameed <Nauman.Hameed@enghouse.com>
166 lines
6.2 KiB
Python
166 lines
6.2 KiB
Python
"""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
|
|
|
|
|
|
# --- 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
|