"""Tests for multi-language AST extraction: JS/TS, Go, Rust.""" from __future__ import annotations import shutil from pathlib import Path import pytest from graphify.extract import extract_js, extract_go, extract_rust, extract FIXTURES = Path(__file__).parent / "fixtures" # ── helpers ────────────────────────────────────────────────────────────────── def _labels(result): return [n["label"] for n in result["nodes"]] def _call_pairs(result): node_by_id = {n["id"]: n["label"] for n in result["nodes"]} return { (node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"])) for e in result["edges"] if e["relation"] == "calls" } def _confidences(result): return {e["confidence"] for e in result["edges"]} # ── TypeScript ──────────────────────────────────────────────────────────────── def test_ts_finds_class(): r = extract_js(FIXTURES / "sample.ts") assert "error" not in r assert "HttpClient" in _labels(r) def test_ts_finds_methods(): r = extract_js(FIXTURES / "sample.ts") labels = _labels(r) assert any("get" in l for l in labels) assert any("post" in l for l in labels) def test_ts_finds_function(): r = extract_js(FIXTURES / "sample.ts") assert any("buildHeaders" in l for l in _labels(r)) def test_ts_emits_calls(): r = extract_js(FIXTURES / "sample.ts") calls = _call_pairs(r) # .post() calls .get() assert any("post" in src and "get" in tgt for src, tgt in calls) def test_ts_calls_are_inferred(): r = extract_js(FIXTURES / "sample.ts") for e in r["edges"]: if e["relation"] == "calls": assert e["confidence"] == "INFERRED" def test_ts_no_dangling_edges(): r = extract_js(FIXTURES / "sample.ts") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: if e["relation"] in ("contains", "method", "calls"): assert e["source"] in node_ids # ── Go ──────────────────────────────────────────────────────────────────────── def test_go_finds_struct(): r = extract_go(FIXTURES / "sample.go") assert "error" not in r assert "Server" in _labels(r) def test_go_finds_methods(): r = extract_go(FIXTURES / "sample.go") labels = _labels(r) assert any("Start" in l for l in labels) assert any("Stop" in l for l in labels) def test_go_finds_constructor(): r = extract_go(FIXTURES / "sample.go") assert any("NewServer" in l for l in _labels(r)) def test_go_emits_calls(): r = extract_go(FIXTURES / "sample.go") # main() calls NewServer and Start assert len(_call_pairs(r)) > 0 def test_go_has_inferred_calls(): r = extract_go(FIXTURES / "sample.go") assert "INFERRED" in _confidences(r) def test_go_no_dangling_edges(): r = extract_go(FIXTURES / "sample.go") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: if e["relation"] in ("contains", "method", "calls"): assert e["source"] in node_ids # ── Rust ────────────────────────────────────────────────────────────────────── def test_rust_finds_struct(): r = extract_rust(FIXTURES / "sample.rs") assert "error" not in r assert "Graph" in _labels(r) def test_rust_finds_impl_methods(): r = extract_rust(FIXTURES / "sample.rs") labels = _labels(r) assert any("add_node" in l for l in labels) assert any("add_edge" in l for l in labels) def test_rust_finds_function(): r = extract_rust(FIXTURES / "sample.rs") assert any("build_graph" in l for l in _labels(r)) def test_rust_emits_calls(): r = extract_rust(FIXTURES / "sample.rs") calls = _call_pairs(r) assert any("build_graph" in src for src, _ in calls) def test_rust_calls_are_inferred(): r = extract_rust(FIXTURES / "sample.rs") for e in r["edges"]: if e["relation"] == "calls": assert e["confidence"] == "INFERRED" def test_rust_no_dangling_edges(): r = extract_rust(FIXTURES / "sample.rs") node_ids = {n["id"] for n in r["nodes"]} for e in r["edges"]: if e["relation"] in ("contains", "method", "calls"): assert e["source"] in node_ids # ── extract() dispatch ──────────────────────────────────────────────────────── def test_extract_dispatches_all_languages(): files = [ FIXTURES / "sample.py", FIXTURES / "sample.ts", FIXTURES / "sample.go", FIXTURES / "sample.rs", ] r = extract(files) source_files = {n["source_file"] for n in r["nodes"] if n["source_file"]} # All four files should contribute nodes assert any("sample.py" in f for f in source_files) assert any("sample.ts" in f for f in source_files) assert any("sample.go" in f for f in source_files) assert any("sample.rs" in f for f in source_files) # ── Cache ───────────────────────────────────────────────────────────────────── def test_cache_hit_returns_same_result(tmp_path): src = FIXTURES / "sample.py" dst = tmp_path / "sample.py" dst.write_bytes(src.read_bytes()) r1 = extract([dst]) r2 = extract([dst]) assert len(r1["nodes"]) == len(r2["nodes"]) assert len(r1["edges"]) == len(r2["edges"]) def test_cache_miss_after_file_change(tmp_path): dst = tmp_path / "a.py" dst.write_text("def foo(): pass\n") r1 = extract([dst]) dst.write_text("def foo(): pass\ndef bar(): pass\n") r2 = extract([dst]) # bar() should appear in the second result labels2 = [n["label"] for n in r2["nodes"]] assert any("bar" in l for l in labels2)