Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ git remote add upstream https://github.com/Graphify-Labs/graphify.git
git fetch upstream
git checkout -b my-feature-branch upstream/v8 # Always branch off upstream v8, never commit directly to v8

# Set up the environment
uv sync
# Set up the environment (--all-extras installs the optional grammars and SDKs,
# matching CI; without it the tests that need them are skipped)
uv sync --all-extras
uv run pre-commit install

# Run the test suite to confirm your setup
Expand Down
19 changes: 19 additions & 0 deletions tests/test_erlang_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@



import importlib.util as _ilu
import sys


Expand All @@ -16,8 +17,18 @@



import pytest

from graphify.extract import extract

# tree-sitter-language-pack is an optional extra, not installed by a default
# `uv sync`. Skip the grammar tests when it is absent; the missing-parser
# test below still runs because it simulates the absent grammar itself.
_needs_erlang = pytest.mark.skipif(
_ilu.find_spec("tree_sitter_language_pack") is None,
reason="tree-sitter-language-pack not installed (optional [erlang] extra)",
)




Expand All @@ -37,6 +48,7 @@ def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]:
}


@_needs_erlang
def test_erlang_functions_resolve_by_name_and_arity(tmp_path):
source = tmp_path / "worker.erl"
source.write_text(
Expand All @@ -56,6 +68,7 @@ def test_erlang_functions_resolve_by_name_and_arity(tmp_path):
assert ("run/1", "helper/2") not in _edge_labels(result, "calls")


@_needs_erlang
def test_erlang_attributes_includes_and_remote_calls(tmp_path):
header = tmp_path / "worker.hrl"
header.write_text("-define(HEADER_VALUE, 2).\n", encoding="utf-8")
Expand Down Expand Up @@ -94,6 +107,7 @@ def test_erlang_attributes_includes_and_remote_calls(tmp_path):
assert any(edge["relation"] == "imports_from" for edge in result["edges"])


@_needs_erlang
def test_erlang_fixture_uses_normal_extract_path(tmp_path):
result = extract([FIXTURE], cache_root=tmp_path)

Expand All @@ -102,6 +116,7 @@ def test_erlang_fixture_uses_normal_extract_path(tmp_path):
assert ('run/0', 'helper/0') in _edge_labels(result, "calls")


@_needs_erlang
def test_erlang_local_fun_reference_links_to_the_function(tmp_path):
"""`fun helper/1` is a reference to a local function — the idiomatic way to
pass a callback to `lists:map`, `spawn`, etc. It names exactly one function
Expand All @@ -122,6 +137,7 @@ def test_erlang_local_fun_reference_links_to_the_function(tmp_path):
assert ("run/0", "helper/1") in _edge_labels(result, "indirect_call")


@_needs_erlang
def test_erlang_local_fun_reference_respects_arity(tmp_path):
"""Fail-closed: `fun helper/1` must bind only to the arity-1 clause, never to
a same-named function of a different arity (#3993)."""
Expand All @@ -140,6 +156,7 @@ def test_erlang_local_fun_reference_respects_arity(tmp_path):
)


@_needs_erlang
def test_erlang_direct_call_stays_a_call_not_indirect(tmp_path):
"""Positive control: a direct call keeps emitting a `calls` edge (not
`indirect_call`), unchanged by the fun-reference handling (#3993)."""
Expand All @@ -157,6 +174,7 @@ def test_erlang_direct_call_stays_a_call_not_indirect(tmp_path):
assert ("run/0", "helper/1") not in _edge_labels(result, "indirect_call")


@_needs_erlang
def test_erlang_call_inside_anonymous_fun_still_captured(tmp_path):
"""Positive control: a direct call made inside an anonymous `fun ... end`
body must still be walked and resolved (#3993)."""
Expand All @@ -173,6 +191,7 @@ def test_erlang_call_inside_anonymous_fun_still_captured(tmp_path):
assert ("run/0", "helper/1") in _edge_labels(result, "calls")


@_needs_erlang
def test_erlang_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path):
source = tmp_path / 'broken.erl'
source.write_text('-module(broken).\nvalid() -> ok.\n% ghost() -> ok.\ninvalid(\n', encoding="utf-8")
Expand Down
5 changes: 5 additions & 0 deletions tests/test_ollama_retry_cap.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@

from unittest.mock import MagicMock

import pytest

import graphify.llm as llm

# The client is the openai SDK; it ships with the optional [ollama] extra.
pytest.importorskip("openai")


def _capture_client_kwargs(monkeypatch):
captured: dict = {}
Expand Down
22 changes: 22 additions & 0 deletions tests/test_r_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@



import importlib.util as _ilu
import sys


Expand All @@ -16,8 +17,18 @@



import pytest

from graphify.extract import extract, extract_r

# tree-sitter-language-pack is an optional extra, not installed by a default
# `uv sync`. Skip the grammar tests when it is absent; the missing-parser
# test below still runs because it simulates the absent grammar itself.
_needs_r = pytest.mark.skipif(
_ilu.find_spec("tree_sitter_language_pack") is None,
reason="tree-sitter-language-pack not installed (optional [r] extra)",
)




Expand All @@ -37,6 +48,7 @@ def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]:
}


@_needs_r
def test_r_functions_and_calls_are_extracted(tmp_path):
source = tmp_path / "analysis.R"
source.write_text(
Expand All @@ -53,6 +65,7 @@ def test_r_functions_and_calls_are_extracted(tmp_path):
assert ("run()", "helper()") in _edge_labels(result, "calls")


@_needs_r
def test_r_assignments_sources_and_classes_are_extracted(tmp_path):
helper = tmp_path / "helpers.R"
helper.write_text("shared <- function(x) x\n", encoding="utf-8")
Expand Down Expand Up @@ -92,6 +105,7 @@ def test_r_assignments_sources_and_classes_are_extracted(tmp_path):
assert any(edge["relation"] == "imports_from" for edge in result["edges"])


@_needs_r
def test_r_fixture_uses_normal_extract_path(tmp_path):
result = extract([FIXTURE], cache_root=tmp_path)

Expand All @@ -100,6 +114,7 @@ def test_r_fixture_uses_normal_extract_path(tmp_path):
assert ('run()', 'double()') in _edge_labels(result, "calls")


@_needs_r
def test_r_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path):
source = tmp_path / 'broken.R'
source.write_text('valid <- function() 1\n"ghost <- function() 2"\n# hidden <- function() 3\nbroken(\n', encoding="utf-8")
Expand All @@ -111,6 +126,7 @@ def test_r_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path):
assert labels.isdisjoint({'ghost()', 'hidden()'})


@_needs_r
def test_r_namespaced_r6class_extracts_the_class_body(tmp_path):
"""`R6::R6Class(...)` is the idiomatic library()-free way to define an R6
class. The qualified name never matched the class-constructor set, so the
Expand All @@ -133,6 +149,7 @@ class collapsed to a plain variable and every method was dropped."""
assert ("Counter", "report()") in methods


@_needs_r
def test_r_namespaced_setrefclass_is_recognised(tmp_path):
"""A namespace-qualified `methods::setRefClass` declares a class too."""
source = tmp_path / "acc.R"
Expand All @@ -148,6 +165,7 @@ def test_r_namespaced_setrefclass_is_recognised(tmp_path):
assert ("Acc", "add()") in _edge_labels(result, "method")


@_needs_r
def test_r6_self_and_private_method_calls_resolve(tmp_path):
"""R6 methods reach their siblings through `self$` / `private$`, never as a
bare name. Those intra-class calls were dropped because walk_calls only
Expand Down Expand Up @@ -183,6 +201,7 @@ def test_r_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys):
assert 'pip install "graphifyy[r]"' in capsys.readouterr().err


@_needs_r
def test_r_symbol_only_bindings_have_distinct_ids(tmp_path):
source = tmp_path / "ops.R"
source.write_text(
Expand All @@ -203,6 +222,7 @@ def test_r_symbol_only_bindings_have_distinct_ids(tmp_path):
}


@_needs_r
def test_r_symbol_only_function_with_external_call_terminates(tmp_path):
import json
import subprocess
Expand All @@ -224,6 +244,7 @@ def test_r_symbol_only_function_with_external_call_terminates(tmp_path):
assert any(call["callee"] == "is.null" for call in result["raw_calls"])


@_needs_r
def test_r_operator_fallback_does_not_collide_with_ordinary_binding(tmp_path):
from graphify.build import build

Expand All @@ -250,6 +271,7 @@ def test_r_operator_fallback_does_not_collide_with_ordinary_binding(tmp_path):
assert ids_by_order[0] == ids_by_order[1]


@_needs_r
def test_r_operator_scope_keeps_nested_binding_identity(tmp_path):
source = tmp_path / "ops.R"
source.write_text(
Expand Down
17 changes: 17 additions & 0 deletions tests/test_solidity_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@



import importlib.util as _ilu
import sys


Expand All @@ -16,8 +17,18 @@



import pytest

from graphify.extract import extract

# tree-sitter-solidity is an optional extra, not installed by a default
# `uv sync`. Skip the grammar tests when it is absent; the missing-parser
# test below still runs because it simulates the absent grammar itself.
_needs_solidity = pytest.mark.skipif(
_ilu.find_spec("tree_sitter_solidity") is None,
reason="tree-sitter-solidity not installed (optional [solidity] extra)",
)




Expand All @@ -37,6 +48,7 @@ def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]:
}


@_needs_solidity
def test_solidity_contract_members_and_calls_are_extracted(tmp_path):
source = tmp_path / "Counter.sol"
source.write_text(
Expand All @@ -57,6 +69,7 @@ def test_solidity_contract_members_and_calls_are_extracted(tmp_path):
assert ("increment()", "record()") in _edge_labels(result, "calls")


@_needs_solidity
def test_solidity_free_functions_and_their_calls_are_extracted(tmp_path):
# File-level (free) functions — legal since Solidity 0.7 — live outside any
# contract. Before the fix the extractor only descended into named type
Expand All @@ -76,6 +89,7 @@ def test_solidity_free_functions_and_their_calls_are_extracted(tmp_path):
assert ("quarter()", "halve()") in _edge_labels(result, "calls")


@_needs_solidity
def test_solidity_types_imports_inheritance_overloads_and_modifiers(tmp_path):
(tmp_path / "Base.sol").write_text(
"contract Base { function baseRun() internal {} }\n", encoding="utf-8"
Expand Down Expand Up @@ -119,6 +133,7 @@ def test_solidity_types_imports_inheritance_overloads_and_modifiers(tmp_path):
assert len([edge for edge in result["edges"] if edge["relation"] == "imports_from"]) == 2


@_needs_solidity
def test_solidity_enum_values_emit_case_of_not_contains(tmp_path):
"""A Solidity enum value is a discriminant case, so it must get a `case_of`
edge like every other language with enums (Java #1719, C#, Swift, Rust,
Expand Down Expand Up @@ -153,6 +168,7 @@ def test_solidity_enum_values_emit_case_of_not_contains(tmp_path):
assert ("Point", "x") not in case_of


@_needs_solidity
def test_solidity_fixture_uses_normal_extract_path(tmp_path):
result = extract([FIXTURE], cache_root=tmp_path)

Expand All @@ -161,6 +177,7 @@ def test_solidity_fixture_uses_normal_extract_path(tmp_path):
assert ('run()', 'helper()') in _edge_labels(result, "calls")


@_needs_solidity
def test_solidity_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path):
source = tmp_path / 'Broken.sol'
source.write_text('contract Kept { function valid() public {} string constant text = "function Ghost()"; /* function Hidden() {} */', encoding="utf-8")
Expand Down
13 changes: 10 additions & 3 deletions tests/test_source_file_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class came out as ``Order``.
"""
from __future__ import annotations

import importlib.util
import os
from pathlib import Path

Expand Down Expand Up @@ -206,9 +207,15 @@ def test_utf16_python_import_resolves_across_files(tmp_path):


@pytest.mark.parametrize("name, text, want", [
("schema.sql",
"CREATE TABLE Orders (id INT);\nCREATE VIEW RecentOrders AS SELECT id FROM Orders;\n",
{"Orders", "RecentOrders"}),
pytest.param(
"schema.sql",
"CREATE TABLE Orders (id INT);\nCREATE VIEW RecentOrders AS SELECT id FROM Orders;\n",
{"Orders", "RecentOrders"},
marks=pytest.mark.skipif(
importlib.util.find_spec("tree_sitter_sql") is None,
reason="tree-sitter-sql not installed (optional [sql] extra)",
),
),
("Deploy.ps1",
"function Invoke-Deploy {\n Get-Config\n}\nfunction Get-Config {\n return 1\n}\n",
{"Invoke-Deploy()", "Get-Config()"}),
Expand Down
Loading
Loading