mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
53da14333e
Co-Authored-By: FatahChan <FatahChan@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
967 lines
36 KiB
Python
967 lines
36 KiB
Python
"""Tests for language extractors: Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Go, Julia, Fortran, JS/TS."""
|
|
from __future__ import annotations
|
|
from pathlib import Path
|
|
import pytest
|
|
from graphify.extract import (
|
|
extract_java, extract_c, extract_cpp, extract_ruby,
|
|
extract_csharp, extract_kotlin, extract_scala, extract_php,
|
|
extract_swift, extract_go, extract_julia, extract_js, extract_fortran,
|
|
extract_groovy,
|
|
)
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
|
|
|
|
def _labels(r):
|
|
return [n["label"] for n in r["nodes"]]
|
|
|
|
def _relations(r):
|
|
return {e["relation"] for e in r["edges"]}
|
|
|
|
def _calls(r):
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
return {
|
|
(node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"]))
|
|
for e in r["edges"] if e["relation"] == "calls"
|
|
}
|
|
|
|
|
|
def _references(r):
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
return [
|
|
(
|
|
node_by_id.get(e["source"], e["source"]),
|
|
node_by_id.get(e["target"], e["target"]),
|
|
e,
|
|
)
|
|
for e in r["edges"] if e["relation"] == "references"
|
|
]
|
|
|
|
|
|
def _edges_with_relation(r, *relations):
|
|
return [e for e in r["edges"] if e["relation"] in relations]
|
|
|
|
|
|
# ── Java ──────────────────────────────────────────────────────────────────────
|
|
|
|
def test_java_no_error():
|
|
r = extract_java(FIXTURES / "sample.java")
|
|
assert "error" not in r
|
|
|
|
def test_java_finds_class():
|
|
r = extract_java(FIXTURES / "sample.java")
|
|
assert any("DataProcessor" in l for l in _labels(r))
|
|
|
|
def test_java_finds_interface():
|
|
r = extract_java(FIXTURES / "sample.java")
|
|
assert any("Processor" in l for l in _labels(r))
|
|
|
|
def test_java_finds_methods():
|
|
r = extract_java(FIXTURES / "sample.java")
|
|
labels = _labels(r)
|
|
assert any("addItem" in l for l in labels)
|
|
assert any("process" in l for l in labels)
|
|
|
|
def test_java_finds_imports():
|
|
r = extract_java(FIXTURES / "sample.java")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
def test_java_import_edges_have_import_context():
|
|
r = extract_java(FIXTURES / "sample.java")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
def test_java_no_dangling_edges():
|
|
r = extract_java(FIXTURES / "sample.java")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids
|
|
|
|
|
|
# ── C ────────────────────────────────────────────────────────────────────────
|
|
|
|
def test_c_no_error():
|
|
r = extract_c(FIXTURES / "sample.c")
|
|
assert "error" not in r
|
|
|
|
def test_c_finds_functions():
|
|
r = extract_c(FIXTURES / "sample.c")
|
|
labels = _labels(r)
|
|
assert any("process" in l for l in labels)
|
|
assert any("main" in l for l in labels)
|
|
|
|
def test_c_finds_includes():
|
|
r = extract_c(FIXTURES / "sample.c")
|
|
assert "imports" in _relations(r)
|
|
|
|
def test_c_emits_calls():
|
|
r = extract_c(FIXTURES / "sample.c")
|
|
assert any(e["relation"] == "calls" for e in r["edges"])
|
|
|
|
def test_c_calls_are_extracted():
|
|
r = extract_c(FIXTURES / "sample.c")
|
|
for e in r["edges"]:
|
|
if e["relation"] == "calls":
|
|
assert e["confidence"] == "EXTRACTED"
|
|
|
|
|
|
def test_c_import_edges_have_import_context():
|
|
r = extract_c(FIXTURES / "sample.c")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
def test_c_call_edges_have_call_context():
|
|
r = extract_c(FIXTURES / "sample.c")
|
|
call_edges = _edges_with_relation(r, "calls")
|
|
assert call_edges
|
|
assert all(e.get("context") == "call" for e in call_edges)
|
|
|
|
|
|
# ── C++ ───────────────────────────────────────────────────────────────────────
|
|
|
|
def test_cpp_no_error():
|
|
r = extract_cpp(FIXTURES / "sample.cpp")
|
|
assert "error" not in r
|
|
|
|
def test_cpp_finds_class():
|
|
r = extract_cpp(FIXTURES / "sample.cpp")
|
|
assert any("HttpClient" in l for l in _labels(r))
|
|
|
|
def test_cpp_finds_methods():
|
|
r = extract_cpp(FIXTURES / "sample.cpp")
|
|
labels = _labels(r)
|
|
# C++ extractor captures the constructor and public-visible methods
|
|
assert any("HttpClient" in l for l in labels)
|
|
|
|
def test_cpp_finds_includes():
|
|
r = extract_cpp(FIXTURES / "sample.cpp")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
def test_cpp_import_edges_have_import_context():
|
|
r = extract_cpp(FIXTURES / "sample.cpp")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
# ── Ruby ─────────────────────────────────────────────────────────────────────
|
|
|
|
def test_ruby_no_error():
|
|
r = extract_ruby(FIXTURES / "sample.rb")
|
|
assert "error" not in r
|
|
|
|
def test_ruby_finds_class():
|
|
r = extract_ruby(FIXTURES / "sample.rb")
|
|
assert any("ApiClient" in l for l in _labels(r))
|
|
|
|
def test_ruby_finds_methods():
|
|
r = extract_ruby(FIXTURES / "sample.rb")
|
|
labels = _labels(r)
|
|
assert any("get" in l for l in labels)
|
|
assert any("post" in l for l in labels)
|
|
|
|
def test_ruby_finds_function():
|
|
r = extract_ruby(FIXTURES / "sample.rb")
|
|
assert any("parse_response" in l for l in _labels(r))
|
|
|
|
|
|
# ── C# ───────────────────────────────────────────────────────────────────────
|
|
|
|
def test_csharp_no_error():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
assert "error" not in r
|
|
|
|
def test_csharp_finds_class():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
assert any("DataProcessor" in l for l in _labels(r))
|
|
|
|
def test_csharp_finds_interface():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
assert any("IProcessor" in l for l in _labels(r))
|
|
|
|
def test_csharp_finds_methods():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
labels = _labels(r)
|
|
assert any("Process" in l for l in labels)
|
|
|
|
def test_csharp_finds_usings():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
assert "imports" in _relations(r)
|
|
|
|
def test_csharp_inherits_edge():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
inherits = [e for e in r["edges"] if e["relation"] == "inherits"]
|
|
assert len(inherits) >= 1
|
|
|
|
def test_csharp_inherits_iprocessor():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
found = any(
|
|
"DataProcessor" in node_by_id.get(e["source"], "") and
|
|
"IProcessor" in node_by_id.get(e["target"], "")
|
|
for e in r["edges"] if e["relation"] == "inherits"
|
|
)
|
|
assert found, "DataProcessor should have inherits edge to IProcessor"
|
|
|
|
|
|
def test_csharp_field_type_references_have_field_context():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
refs = _references(r)
|
|
assert any(
|
|
"DataProcessor" in src and "HttpClient" in tgt and edge.get("context") == "field"
|
|
for src, tgt, edge in refs
|
|
), "DataProcessor field declarations should reference HttpClient with field context"
|
|
|
|
|
|
def test_csharp_call_edges_have_call_context():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
assert any(
|
|
"Process" in node_by_id.get(e["source"], "")
|
|
and "Validate" in node_by_id.get(e["target"], "")
|
|
and e.get("context") == "call"
|
|
for e in r["edges"] if e["relation"] == "calls"
|
|
), "C# call edges should retain call context"
|
|
|
|
|
|
def test_csharp_import_edges_have_import_context():
|
|
r = extract_csharp(FIXTURES / "sample.cs")
|
|
import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
# ── Kotlin ───────────────────────────────────────────────────────────────────
|
|
|
|
def test_kotlin_no_error():
|
|
r = extract_kotlin(FIXTURES / "sample.kt")
|
|
assert "error" not in r
|
|
|
|
def test_kotlin_finds_class():
|
|
r = extract_kotlin(FIXTURES / "sample.kt")
|
|
assert any("HttpClient" in l for l in _labels(r))
|
|
|
|
def test_kotlin_finds_data_class():
|
|
r = extract_kotlin(FIXTURES / "sample.kt")
|
|
assert any("Config" in l for l in _labels(r))
|
|
|
|
def test_kotlin_finds_methods():
|
|
r = extract_kotlin(FIXTURES / "sample.kt")
|
|
labels = _labels(r)
|
|
assert any("get" in l for l in labels)
|
|
assert any("post" in l for l in labels)
|
|
|
|
def test_kotlin_finds_function():
|
|
r = extract_kotlin(FIXTURES / "sample.kt")
|
|
assert any("createClient" in l for l in _labels(r))
|
|
|
|
def test_kotlin_emits_in_file_calls():
|
|
"""Regression test for the call-walker `simple_identifier` /
|
|
`identifier` rename — see graphify-kmp's PythonParityTest."""
|
|
r = extract_kotlin(FIXTURES / "sample.kt")
|
|
calls = _calls(r)
|
|
# In sample.kt: get() and post() both call buildRequest(), and
|
|
# createClient() invokes Config and HttpClient (constructor calls).
|
|
assert (".get()", ".buildRequest()") in calls
|
|
assert (".post()", ".buildRequest()") in calls
|
|
assert ("createClient()", "Config") in calls
|
|
assert ("createClient()", "HttpClient") in calls
|
|
|
|
|
|
# ── Scala ─────────────────────────────────────────────────────────────────────
|
|
|
|
def test_scala_no_error():
|
|
r = extract_scala(FIXTURES / "sample.scala")
|
|
assert "error" not in r
|
|
|
|
def test_scala_finds_class():
|
|
r = extract_scala(FIXTURES / "sample.scala")
|
|
assert any("HttpClient" in l for l in _labels(r))
|
|
|
|
def test_scala_finds_object():
|
|
r = extract_scala(FIXTURES / "sample.scala")
|
|
assert any("HttpClientFactory" in l for l in _labels(r))
|
|
|
|
def test_scala_finds_methods():
|
|
r = extract_scala(FIXTURES / "sample.scala")
|
|
labels = _labels(r)
|
|
assert any("get" in l for l in labels)
|
|
assert any("post" in l for l in labels)
|
|
|
|
|
|
def test_scala_import_edges_have_import_context():
|
|
r = extract_scala(FIXTURES / "sample.scala")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
def test_scala_call_edges_have_call_context():
|
|
r = extract_scala(FIXTURES / "sample.scala")
|
|
call_edges = _edges_with_relation(r, "calls")
|
|
assert call_edges
|
|
assert all(e.get("context") == "call" for e in call_edges)
|
|
|
|
|
|
# ── PHP ───────────────────────────────────────────────────────────────────────
|
|
|
|
def test_php_no_error():
|
|
r = extract_php(FIXTURES / "sample.php")
|
|
assert "error" not in r
|
|
|
|
def test_php_finds_class():
|
|
r = extract_php(FIXTURES / "sample.php")
|
|
assert any("ApiClient" in l for l in _labels(r))
|
|
|
|
def test_php_finds_methods():
|
|
r = extract_php(FIXTURES / "sample.php")
|
|
labels = _labels(r)
|
|
assert any("get" in l for l in labels)
|
|
assert any("post" in l for l in labels)
|
|
|
|
def test_php_finds_function():
|
|
r = extract_php(FIXTURES / "sample.php")
|
|
assert any("parseResponse" in l for l in _labels(r))
|
|
|
|
def test_php_finds_imports():
|
|
r = extract_php(FIXTURES / "sample.php")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
def test_php_import_edges_have_import_context():
|
|
r = extract_php(FIXTURES / "sample.php")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
def test_php_call_edges_have_call_context():
|
|
r = extract_php(FIXTURES / "sample.php")
|
|
call_edges = _edges_with_relation(r, "calls")
|
|
assert call_edges
|
|
assert all(e.get("context") == "call" for e in call_edges)
|
|
|
|
def test_php_finds_static_property_access():
|
|
r = extract_php(FIXTURES / "sample_php_static_prop.php")
|
|
assert "uses_static_prop" in _relations(r)
|
|
|
|
def test_php_static_prop_target_is_holding_class():
|
|
r = extract_php(FIXTURES / "sample_php_static_prop.php")
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
uses_prop = [
|
|
(node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"]))
|
|
for e in r["edges"] if e["relation"] == "uses_static_prop"
|
|
]
|
|
assert any("DefaultPalette" in tgt for _, tgt in uses_prop)
|
|
|
|
def test_php_finds_config_helper_call():
|
|
r = extract_php(FIXTURES / "sample_php_config.php")
|
|
assert "uses_config" in _relations(r)
|
|
|
|
def test_php_config_helper_target_matches_first_segment():
|
|
r = extract_php(FIXTURES / "sample_php_config.php")
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
uses_cfg = [
|
|
(node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"]))
|
|
for e in r["edges"] if e["relation"] == "uses_config"
|
|
]
|
|
assert any("Throttle" in tgt for _, tgt in uses_cfg)
|
|
|
|
def test_php_finds_container_bind():
|
|
r = extract_php(FIXTURES / "sample_php_container.php")
|
|
assert "bound_to" in _relations(r)
|
|
|
|
def test_php_container_bind_links_contract_to_implementation():
|
|
r = extract_php(FIXTURES / "sample_php_container.php")
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
bound = [
|
|
(node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"]))
|
|
for e in r["edges"] if e["relation"] == "bound_to"
|
|
]
|
|
assert any("PaymentGateway" in src and "StripeGateway" in tgt for src, tgt in bound)
|
|
|
|
def test_php_finds_event_listeners():
|
|
r = extract_php(FIXTURES / "sample_php_listen.php")
|
|
assert "listened_by" in _relations(r)
|
|
|
|
def test_php_event_listener_links_event_to_listener():
|
|
r = extract_php(FIXTURES / "sample_php_listen.php")
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
listened = [
|
|
(node_by_id.get(e["source"], e["source"]), node_by_id.get(e["target"], e["target"]))
|
|
for e in r["edges"] if e["relation"] == "listened_by"
|
|
]
|
|
assert any("UserRegistered" in src and "SendWelcomeEmail" in tgt for src, tgt in listened)
|
|
|
|
|
|
# ── Swift ────────────────────────────────────────────────────────────────────
|
|
|
|
def test_swift_no_error():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert "error" not in r
|
|
|
|
def test_swift_finds_class():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("DataProcessor" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_protocol():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("Processor" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_struct():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("Config" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_methods():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
labels = _labels(r)
|
|
assert any("addItem" in l for l in labels)
|
|
assert any("process" in l for l in labels)
|
|
|
|
def test_swift_finds_function():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("createProcessor" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_imports():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
def test_swift_import_edges_have_import_context():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
def test_swift_no_dangling_edges():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids
|
|
|
|
def test_swift_finds_actor():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("CacheManager" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_enum():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("NetworkError" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_enum_methods():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("describe" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_enum_cases():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
labels = _labels(r)
|
|
assert any("timeout" in l for l in labels)
|
|
assert any("connectionFailed" in l for l in labels)
|
|
|
|
def test_swift_enum_cases_have_case_of_edge():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
case_edges = [e for e in r["edges"] if e["relation"] == "case_of"]
|
|
assert len(case_edges) >= 2
|
|
|
|
def test_swift_finds_deinit():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("deinit" in l for l in _labels(r))
|
|
|
|
def test_swift_finds_subscript():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
assert any("subscript" in l for l in _labels(r))
|
|
|
|
def test_swift_extension_methods_attach_to_type():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
method_edges = [e for e in r["edges"] if e["relation"] == "method"]
|
|
found = False
|
|
for e in method_edges:
|
|
src_label = node_by_id.get(e["source"], "")
|
|
tgt_label = node_by_id.get(e["target"], "")
|
|
if "Config" in src_label and "isValid" in tgt_label:
|
|
found = True
|
|
break
|
|
assert found, "extension method isValid should attach to Config"
|
|
|
|
def test_swift_extension_does_not_duplicate_type_node():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
config_nodes = [n for n in r["nodes"] if n["label"] == "Config"]
|
|
assert len(config_nodes) == 1, f"Config should appear once, got {len(config_nodes)}"
|
|
|
|
def test_swift_conformance_edge():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
inherits_edges = [e for e in r["edges"] if e["relation"] == "inherits"]
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
found = False
|
|
for e in inherits_edges:
|
|
src_label = node_by_id.get(e["source"], "")
|
|
tgt_label = node_by_id.get(e["target"], "")
|
|
if "DataProcessor" in src_label and "Processor" in tgt_label:
|
|
found = True
|
|
break
|
|
assert found, "DataProcessor should have inherits edge to Processor"
|
|
|
|
def test_swift_extension_conformance_edge():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
inherits_edges = [e for e in r["edges"] if e["relation"] == "inherits"]
|
|
node_by_id = {n["id"]: n["label"] for n in r["nodes"]}
|
|
found = False
|
|
for e in inherits_edges:
|
|
src_label = node_by_id.get(e["source"], "")
|
|
tgt_label = node_by_id.get(e["target"], "")
|
|
if "DataProcessor" in src_label and "Loggable" in tgt_label:
|
|
found = True
|
|
break
|
|
assert found, "extension should add conformance edge DataProcessor -> Loggable"
|
|
|
|
def test_swift_emits_calls():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
calls = _calls(r)
|
|
assert any("process" in src and "validate" in tgt for src, tgt in calls)
|
|
|
|
def test_swift_call_edges_have_call_context():
|
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
call_edges = _edges_with_relation(r, "calls")
|
|
assert call_edges
|
|
assert all(e.get("context") == "call" for e in call_edges)
|
|
|
|
|
|
# ── Elixir ────────────────────────────────────────────────────────────────────
|
|
|
|
from graphify.extract import extract_elixir
|
|
|
|
def test_elixir_finds_module():
|
|
r = extract_elixir(FIXTURES / "sample.ex")
|
|
assert "error" not in r
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert any("MyApp.Accounts.User" in l for l in labels)
|
|
|
|
def test_elixir_finds_functions():
|
|
r = extract_elixir(FIXTURES / "sample.ex")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert any("create" in l for l in labels)
|
|
assert any("find" in l for l in labels)
|
|
assert any("validate" in l for l in labels)
|
|
|
|
def test_elixir_finds_imports():
|
|
r = extract_elixir(FIXTURES / "sample.ex")
|
|
import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
assert len(import_edges) >= 2
|
|
|
|
|
|
def test_elixir_import_edges_have_import_context():
|
|
r = extract_elixir(FIXTURES / "sample.ex")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
def test_elixir_finds_calls():
|
|
r = extract_elixir(FIXTURES / "sample.ex")
|
|
calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
|
|
labels = {n["id"]: n["label"] for n in r["nodes"]}
|
|
assert any("create" in labels.get(src, "") and "validate" in labels.get(tgt, "") for src, tgt in calls)
|
|
|
|
|
|
def test_elixir_call_edges_have_call_context():
|
|
r = extract_elixir(FIXTURES / "sample.ex")
|
|
call_edges = _edges_with_relation(r, "calls")
|
|
assert call_edges
|
|
assert all(e.get("context") == "call" for e in call_edges)
|
|
|
|
def test_elixir_method_edges():
|
|
r = extract_elixir(FIXTURES / "sample.ex")
|
|
methods = [e for e in r["edges"] if e["relation"] == "method"]
|
|
assert len(methods) >= 3
|
|
|
|
|
|
# ── Objective-C ──────────────────────────────────────────────────────────────
|
|
from graphify.extract import extract_objc
|
|
|
|
|
|
def test_objc_finds_interface():
|
|
r = extract_objc(FIXTURES / "sample.m")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "Animal" in labels
|
|
|
|
|
|
def test_objc_finds_subclass():
|
|
r = extract_objc(FIXTURES / "sample.m")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "Dog" in labels
|
|
|
|
|
|
def test_objc_finds_methods():
|
|
r = extract_objc(FIXTURES / "sample.m")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert any("speak" in l or "fetch" in l or "initWithName" in l for l in labels)
|
|
|
|
|
|
def test_objc_finds_imports():
|
|
r = extract_objc(FIXTURES / "sample.m")
|
|
import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
assert len(import_edges) >= 1
|
|
|
|
|
|
def test_objc_import_edges_have_import_context():
|
|
r = extract_objc(FIXTURES / "sample.m")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
def test_objc_inherits_edge():
|
|
r = extract_objc(FIXTURES / "sample.m")
|
|
inherits = [e for e in r["edges"] if e["relation"] == "inherits"]
|
|
assert len(inherits) >= 1
|
|
|
|
|
|
def test_objc_no_dangling_edges():
|
|
r = extract_objc(FIXTURES / "sample.m")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids, f"Dangling source: {e}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Go
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_go_receiver_methods_share_type_node():
|
|
"""Methods on the same receiver type must share one canonical type node."""
|
|
r = extract_go(FIXTURES / "sample.go")
|
|
server_nodes = [n for n in r["nodes"] if n["label"] == "Server"]
|
|
# Both Start() and Stop() are on *Server — should produce exactly one Server node
|
|
assert len(server_nodes) == 1
|
|
|
|
def test_go_receiver_uses_pkg_scope():
|
|
"""Type node id should be scoped to directory, not file stem."""
|
|
r = extract_go(FIXTURES / "sample.go")
|
|
server_nodes = [n for n in r["nodes"] if n["label"] == "Server"]
|
|
assert server_nodes
|
|
# Should NOT contain the file stem "sample" in the type node id
|
|
assert "sample" not in server_nodes[0]["id"].split(":")[0]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Julia
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_julia_finds_module():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "Geometry" in labels
|
|
|
|
|
|
def test_julia_finds_structs():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "Point" in labels
|
|
assert "Circle" in labels
|
|
|
|
|
|
def test_julia_finds_abstract_type():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "Shape" in labels
|
|
|
|
|
|
def test_julia_finds_functions():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert any("area" in l for l in labels)
|
|
assert any("distance" in l for l in labels)
|
|
|
|
|
|
def test_julia_finds_short_function():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert any("perimeter" in l for l in labels)
|
|
|
|
|
|
def test_julia_finds_imports():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
assert len(import_edges) >= 1
|
|
|
|
|
|
def test_julia_import_edges_have_import_context():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
def test_julia_finds_inherits():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
inherits = [e for e in r["edges"] if e["relation"] == "inherits"]
|
|
assert len(inherits) >= 1
|
|
|
|
|
|
def test_julia_finds_calls():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
call_edges = [e for e in r["edges"] if e["relation"] == "calls"]
|
|
assert len(call_edges) >= 1
|
|
|
|
|
|
def test_julia_call_edges_have_call_context():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
call_edges = _edges_with_relation(r, "calls")
|
|
assert call_edges
|
|
assert all(e.get("context") == "call" for e in call_edges)
|
|
|
|
|
|
def test_julia_no_dangling_edges():
|
|
r = extract_julia(FIXTURES / "sample.jl")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids, f"Dangling source: {e}"
|
|
|
|
|
|
# ── Fortran extractor ────────────────────────────────────────────────────────
|
|
|
|
def test_fortran_finds_module():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
assert "error" not in r
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "geometry" in labels
|
|
|
|
|
|
def test_fortran_finds_subroutines():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert any("circle_area" in l for l in labels)
|
|
assert any("print_area" in l for l in labels)
|
|
|
|
|
|
def test_fortran_finds_function():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert any("distance" in l for l in labels)
|
|
|
|
|
|
def test_fortran_finds_program():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "main" in labels
|
|
|
|
|
|
def test_fortran_finds_use_imports():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
assert len(import_edges) >= 2
|
|
|
|
|
|
def test_fortran_use_edges_have_use_context():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
assert all(e.get("context") == "use" for e in import_edges)
|
|
|
|
|
|
def test_fortran_finds_calls():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
call_edges = [e for e in r["edges"] if e["relation"] == "calls"]
|
|
assert len(call_edges) >= 1
|
|
|
|
|
|
def test_fortran_case_insensitive_names():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert all(l == l.lower() or "(" in l for l in labels if l.endswith(("()", "")) and not "." in l)
|
|
assert "geometry" in labels
|
|
assert "main" in labels
|
|
|
|
|
|
def test_fortran_no_dangling_edges():
|
|
r = extract_fortran(FIXTURES / "sample.f90")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids, f"Dangling source: {e}"
|
|
|
|
|
|
def test_fortran_capital_F_parses_preprocessed():
|
|
r = extract_fortran(FIXTURES / "sample_preprocessed.F90")
|
|
assert "error" not in r
|
|
labels = [n["label"] for n in r["nodes"]]
|
|
assert "shapes" in labels
|
|
assert any("compute_volume" in l for l in labels)
|
|
|
|
|
|
# ── TypeScript dynamic imports ───────────────────────────────────────────────
|
|
|
|
def test_ts_dynamic_import_no_error():
|
|
r = extract_js(FIXTURES / "dynamic_import.ts")
|
|
assert "error" not in r
|
|
|
|
def test_ts_dynamic_import_extracts_edges():
|
|
"""Dynamic import() calls inside functions should produce imports_from edges."""
|
|
r = extract_js(FIXTURES / "dynamic_import.ts")
|
|
dyn_edges = [e for e in r["edges"] if e["relation"] == "imports_from"]
|
|
targets = {e["target"] for e in dyn_edges}
|
|
# Should find: static ./logger, dynamic ./mayaEngine.js, dynamic ./queue.js
|
|
assert any("logger" in t for t in targets), f"Missing static import of logger: {targets}"
|
|
assert any("mayaengine" in t.lower() for t in targets), f"Missing dynamic import of mayaEngine: {targets}"
|
|
assert any("queue" in t.lower() for t in targets), f"Missing dynamic import of queue: {targets}"
|
|
|
|
def test_ts_dynamic_import_confidence():
|
|
"""Dynamic imports should have EXTRACTED confidence (they are deterministic string literals)."""
|
|
r = extract_js(FIXTURES / "dynamic_import.ts")
|
|
dyn_edges = [e for e in r["edges"]
|
|
if e["relation"] == "imports_from"
|
|
and "mayaengine" in e["target"].lower()]
|
|
assert len(dyn_edges) >= 1
|
|
assert dyn_edges[0]["confidence"] == "EXTRACTED"
|
|
|
|
def test_ts_dynamic_import_source_is_function():
|
|
"""Dynamic import edge source should be the enclosing function, not the file."""
|
|
r = extract_js(FIXTURES / "dynamic_import.ts")
|
|
node_labels = {n["id"]: n["label"] for n in r["nodes"]}
|
|
dyn_edges = [e for e in r["edges"]
|
|
if e["relation"] == "imports_from"
|
|
and "mayaengine" in e["target"].lower()]
|
|
assert len(dyn_edges) >= 1
|
|
src_label = node_labels.get(dyn_edges[0]["source"], "")
|
|
assert "processInbound" in src_label, f"Expected processInbound as source, got {src_label}"
|
|
|
|
def test_ts_no_dynamic_import_in_sync_fn():
|
|
"""Functions without dynamic imports should not get spurious imports_from edges."""
|
|
r = extract_js(FIXTURES / "dynamic_import.ts")
|
|
node_ids = {n["label"]: n["id"] for n in r["nodes"]}
|
|
sync_nid = node_ids.get("syncOnly()")
|
|
if sync_nid:
|
|
sync_imports = [e for e in r["edges"]
|
|
if e["source"] == sync_nid and e["relation"] == "imports_from"]
|
|
assert len(sync_imports) == 0
|
|
|
|
def test_ts_dynamic_template_literal_skipped():
|
|
"""Dynamic template literals (with ${}) must not produce an imports_from edge."""
|
|
r = extract_js(FIXTURES / "dynamic_import.ts")
|
|
targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"}
|
|
# loadHandler uses `./handlers/${handlerName}` — no static path, must be absent
|
|
assert not any("handler" in t.lower() and "$" in t for t in targets), \
|
|
f"Garbage edge from dynamic template literal found: {targets}"
|
|
# More robust: no target should contain a brace character
|
|
assert not any("{" in t or "}" in t for t in targets), \
|
|
f"Target contains unresolved template expression: {targets}"
|
|
|
|
def test_ts_static_template_literal_resolved():
|
|
"""Static template literals (no ${}) should resolve the same as a plain string."""
|
|
r = extract_js(FIXTURES / "dynamic_import.ts")
|
|
targets = {e["target"] for e in r["edges"] if e["relation"] == "imports_from"}
|
|
assert any("statichelper" in t.lower() for t in targets), \
|
|
f"Static template literal import not resolved: {targets}"
|
|
|
|
|
|
# ── Markdown ─────────────────────────────────────────────────────────────────
|
|
|
|
from graphify.extract import extract_markdown
|
|
|
|
def test_markdown_no_error():
|
|
r = extract_markdown(FIXTURES / "deploy_guide.md")
|
|
assert "error" not in r
|
|
|
|
def test_markdown_finds_headings():
|
|
r = extract_markdown(FIXTURES / "deploy_guide.md")
|
|
labels = _labels(r)
|
|
assert any("Deploy Guide" in l for l in labels)
|
|
assert any("Prerequisites" in l for l in labels)
|
|
assert any("Full Deploy" in l for l in labels)
|
|
assert any("Rollback" in l for l in labels)
|
|
|
|
def test_markdown_finds_nested_heading():
|
|
"""### Database Migration is nested under ## Full Deploy."""
|
|
r = extract_markdown(FIXTURES / "deploy_guide.md")
|
|
labels = _labels(r)
|
|
assert any("Database Migration" in l for l in labels)
|
|
|
|
def test_markdown_finds_code_blocks():
|
|
r = extract_markdown(FIXTURES / "deploy_guide.md")
|
|
labels = _labels(r)
|
|
assert any("code:bash" in l for l in labels)
|
|
assert any("code:sql" in l for l in labels)
|
|
assert any("code:python" in l for l in labels)
|
|
|
|
def test_markdown_contains_edges():
|
|
"""Headings and code blocks should be connected via 'contains' edges."""
|
|
r = extract_markdown(FIXTURES / "deploy_guide.md")
|
|
assert "contains" in _relations(r)
|
|
contains_edges = [e for e in r["edges"] if e["relation"] == "contains"]
|
|
assert len(contains_edges) >= 5 # file->h1, h1->h2s, h2->h3, h2->codeblocks
|
|
|
|
def test_markdown_no_dangling_edges():
|
|
r = extract_markdown(FIXTURES / "deploy_guide.md")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids, f"Dangling source: {e}"
|
|
|
|
|
|
# ── Groovy ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_groovy_no_error():
|
|
r = extract_groovy(FIXTURES / "sample.groovy")
|
|
assert "error" not in r
|
|
|
|
|
|
def test_groovy_finds_class():
|
|
r = extract_groovy(FIXTURES / "sample.groovy")
|
|
assert any("SampleService" in l for l in _labels(r))
|
|
|
|
|
|
def test_groovy_finds_methods():
|
|
r = extract_groovy(FIXTURES / "sample.groovy")
|
|
labels = _labels(r)
|
|
assert any("process" in l for l in labels)
|
|
assert any("reset" in l for l in labels)
|
|
|
|
|
|
def test_groovy_finds_imports():
|
|
r = extract_groovy(FIXTURES / "sample.groovy")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
def test_groovy_import_edges_have_import_context():
|
|
r = extract_groovy(FIXTURES / "sample.groovy")
|
|
import_edges = _edges_with_relation(r, "imports", "imports_from")
|
|
assert import_edges
|
|
assert all(e.get("context") == "import" for e in import_edges)
|
|
|
|
|
|
def test_groovy_no_dangling_edges():
|
|
r = extract_groovy(FIXTURES / "sample.groovy")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids
|
|
|
|
|
|
def test_groovy_spock_finds_class():
|
|
r = extract_groovy(FIXTURES / "sample_spock.groovy")
|
|
assert any("SampleSpec" in l for l in _labels(r))
|
|
|
|
|
|
def test_groovy_spock_finds_feature_methods():
|
|
r = extract_groovy(FIXTURES / "sample_spock.groovy")
|
|
feature_labels = [l for l in _labels(r) if l.startswith('"')]
|
|
assert len(feature_labels) >= 2
|
|
|
|
|
|
def test_groovy_spock_finds_method_with_apostrophe():
|
|
r = extract_groovy(FIXTURES / "sample_spock.groovy")
|
|
assert any("it's" in l for l in _labels(r))
|
|
|
|
|
|
def test_groovy_spock_preserves_import_edges():
|
|
r = extract_groovy(FIXTURES / "sample_spock.groovy")
|
|
assert "imports" in _relations(r)
|
|
|
|
|
|
def test_groovy_spock_no_dangling_edges():
|
|
r = extract_groovy(FIXTURES / "sample_spock.groovy")
|
|
node_ids = {n["id"] for n in r["nodes"]}
|
|
for e in r["edges"]:
|
|
assert e["source"] in node_ids
|