mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
test: skip optional-extra tests when the extra is not installed (#4190)
Grammar and SDK tests for erlang, r, solidity, vbnet, ollama and the sql encoding case failed instead of skipping on a default uv sync. Guard them with the existing _needs_* skipif pattern and point CONTRIBUTING at uv sync --all-extras to match CI.
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit d7f443eeb1)
This commit is contained in:
1 parent
748de12553
commit
96c39d6a88
7 files changed
+94
-5
No files matched your search
+3
-2
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
|
||||
|
||||
import importlib.util as _ilu
|
||||
import sys
|
||||
|
||||
|
||||
@@ -16,8 +17,18 @@ from pathlib import Path
|
||||
|
||||
|
||||
|
||||
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)",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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)."""
|
||||
@@ -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)."""
|
||||
@@ -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)."""
|
||||
@@ -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")
|
||||
|
||||
@@ -7,8 +7,13 @@ from __future__ import annotations
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
|
||||
|
||||
import importlib.util as _ilu
|
||||
import sys
|
||||
|
||||
|
||||
@@ -16,8 +17,18 @@ from pathlib import Path
|
||||
|
||||
|
||||
|
||||
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)",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -133,6 +149,7 @@ def test_r_namespaced_r6class_extracts_the_class_body(tmp_path):
|
||||
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"
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
|
||||
|
||||
import importlib.util as _ilu
|
||||
import sys
|
||||
|
||||
|
||||
@@ -16,8 +17,18 @@ from pathlib import Path
|
||||
|
||||
|
||||
|
||||
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)",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -22,6 +22,7 @@ These tests write bytes directly, so they pin the behaviour on every platform.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -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()"}),
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
|
||||
|
||||
import importlib.util as _ilu
|
||||
import sys
|
||||
|
||||
|
||||
@@ -16,8 +17,18 @@ from pathlib import Path
|
||||
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
# tree-sitter-vb-dotnet 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_vbnet = pytest.mark.skipif(
|
||||
_ilu.find_spec("tree_sitter_vb_dotnet") is None,
|
||||
reason="tree-sitter-vb-dotnet not installed (optional [vbnet] extra)",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +48,7 @@ def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]:
|
||||
}
|
||||
|
||||
|
||||
@_needs_vbnet
|
||||
def test_vbnet_class_methods_and_case_insensitive_calls(tmp_path):
|
||||
source = tmp_path / "Counter.vb"
|
||||
source.write_text(
|
||||
@@ -59,6 +71,7 @@ def test_vbnet_class_methods_and_case_insensitive_calls(tmp_path):
|
||||
assert ("Run()", "Helper()") in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
@_needs_vbnet
|
||||
def test_vbnet_module_qualified_call_resolves_to_the_module_method(tmp_path):
|
||||
# A call qualified by a type/module name (Helpers.Log()) — the idiomatic way
|
||||
# to reach a shared Module Sub — was dropped entirely: the invocation
|
||||
@@ -85,6 +98,7 @@ def test_vbnet_module_qualified_call_resolves_to_the_module_method(tmp_path):
|
||||
assert ("Run()", "Log()") in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
@_needs_vbnet
|
||||
def test_vbnet_call_through_a_value_receiver_stays_unresolved(tmp_path):
|
||||
# A call through a value receiver (a local variable) has no statically known
|
||||
# owner, so it must NOT be guessed into a false edge (fail-closed).
|
||||
@@ -107,6 +121,7 @@ def test_vbnet_call_through_a_value_receiver_stays_unresolved(tmp_path):
|
||||
assert ("Run()", "Save()") not in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
@_needs_vbnet
|
||||
def test_vbnet_types_members_relationships_and_partial_calls(tmp_path):
|
||||
first = tmp_path / "Counter.vb"
|
||||
first.write_text(
|
||||
@@ -160,6 +175,7 @@ def test_vbnet_types_members_relationships_and_partial_calls(tmp_path):
|
||||
assert ("Run()", "Changed") in _edge_labels(result, "handles")
|
||||
|
||||
|
||||
@_needs_vbnet
|
||||
def test_vbnet_fixture_uses_normal_extract_path(tmp_path):
|
||||
result = extract([FIXTURE], cache_root=tmp_path)
|
||||
|
||||
@@ -168,6 +184,7 @@ def test_vbnet_fixture_uses_normal_extract_path(tmp_path):
|
||||
assert ('Run()', 'Helper()') in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
@_needs_vbnet
|
||||
def test_vbnet_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path):
|
||||
source = tmp_path / 'Broken.vb'
|
||||
source.write_text("Class Broken\n Sub Valid()\n End Sub\nEnd Class\n???\n' Sub Ghost()\n", encoding="utf-8")
|
||||
@@ -190,6 +207,7 @@ def test_vbnet_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys
|
||||
assert 'pip install "graphifyy[vbnet]"' in capsys.readouterr().err
|
||||
|
||||
|
||||
@_needs_vbnet
|
||||
def test_vbnet_enum_members_emit_case_of_not_contains(tmp_path):
|
||||
"""A VB.NET enum member is a discriminant case, so it must get a `case_of`
|
||||
edge like every other language with enums (Java #1719, C#, Swift, Scala),
|
||||
|
||||
Reference in new issue
Block a user