Files
stack/tests/sem/test_enrich.py
kert 59eb56f659
Some checks failed
CI / skinny-install (aco) (push) Successful in 1m18s
CI / skinny-install (api) (push) Successful in 40s
CI / skinny-install (bcda) (push) Successful in 35s
CI / skinny-install (bib) (push) Successful in 38s
CI / skinny-install (cli) (push) Successful in 46s
CI / skinny-install (conf) (push) Successful in 36s
CI / skinny-install (opps) (push) Successful in 38s
CI / skinny-install (pfs) (push) Successful in 47s
CI / skinny-install (rex) (push) Successful in 35s
Infra CI / notebooks (push) Successful in 3m17s
CI / lint-test (push) Failing after 3m30s
CI / skinny-install (bls) (push) Successful in 34s
CI / skinny-install (ccw) (push) Successful in 45s
CI / skinny-install (cms) (push) Successful in 32s
CI / skinny-install (perf) (push) Successful in 43s
Deploy / build-scan-report (push) Has been cancelled
Infra CI / docs (push) Failing after 20s
Infra CI / api (push) Successful in 16s
Infra CI / mc (push) Successful in 12s
Package Supply Chain / pkg-supply-chain (push) Successful in 1m27s
Infra CI / zotero (push) Successful in 6m10s
chore: hw provisioning, test coverage, deps
2026-04-09 22:26:31 -04:00

151 lines
5.2 KiB
Python

"""Tests for sem.enrich — Ruff and ty diagnostic attachment."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from sem.enrich import _find_node, _run_tool, attach_ruff, attach_ty
from sem.nodes import NodeKind, SemanticNode, Span
def _make_node(start: int, end: int, node_id: str = "n") -> SemanticNode:
return SemanticNode(
node_id=node_id,
kind=NodeKind.FUNCTION,
span=Span(start_line=start, end_line=end),
module="test",
)
class TestFindNode:
def test_exact_match(self):
nodes = [_make_node(1, 5, "a"), _make_node(10, 20, "b")]
assert _find_node(nodes, 3) is nodes[0]
assert _find_node(nodes, 15) is nodes[1]
def test_tightest_span(self):
outer = _make_node(1, 20, "outer")
inner = _make_node(5, 10, "inner")
assert _find_node([outer, inner], 7) is inner
def test_no_match(self):
nodes = [_make_node(1, 5)]
assert _find_node(nodes, 100) is None
def test_boundary_lines(self):
node = _make_node(10, 20, "n")
assert _find_node([node], 10) is node
assert _find_node([node], 20) is node
assert _find_node([node], 9) is None
assert _find_node([node], 21) is None
class TestRunTool:
def test_returns_empty_on_file_not_found(self):
result = _run_tool(["nonexistent_binary_xyz_123"])
assert result == []
@patch(
"sem.enrich.subprocess.run",
side_effect=__import__("subprocess").TimeoutExpired(cmd=["x"], timeout=1),
)
def test_returns_empty_on_timeout(self, mock_run):
result = _run_tool(["some_cmd"])
assert result == []
@patch("sem.enrich.subprocess.run")
def test_returns_empty_on_no_output(self, mock_run):
mock_run.return_value = MagicMock(stdout="", returncode=0)
result = _run_tool(["some_cmd"])
assert result == []
@patch("sem.enrich.subprocess.run")
def test_returns_empty_on_invalid_json(self, mock_run):
mock_run.return_value = MagicMock(stdout="not json at all", returncode=0)
result = _run_tool(["some_cmd"])
assert result == []
@patch("sem.enrich.subprocess.run")
def test_returns_parsed_json(self, mock_run):
import json
mock_run.return_value = MagicMock(
stdout=json.dumps([{"code": "E501", "location": {"row": 5}}]),
returncode=0,
)
result = _run_tool(["some_cmd"])
assert len(result) == 1
assert result[0]["code"] == "E501"
class TestAttachRuff:
@patch("sem.enrich._run_tool")
def test_attaches_ruff_codes_to_matching_node(self, mock_tool, tmp_path):
mock_tool.return_value = [
{"code": "E501", "location": {"row": 3}},
]
node = _make_node(1, 10, "func")
result = attach_ruff([node], tmp_path / "test.py")
assert "E501" in result[0].ruff_codes
@patch("sem.enrich._run_tool")
def test_skips_diag_without_line_or_code(self, mock_tool, tmp_path):
mock_tool.return_value = [
{"code": "", "location": {"row": 3}},
{"code": "E501", "location": {"row": 0}},
{"location": {"row": 3}},
]
node = _make_node(1, 10, "func")
result = attach_ruff([node], tmp_path / "test.py")
assert result[0].ruff_codes == []
@patch("sem.enrich._run_tool")
def test_no_duplicate_codes(self, mock_tool, tmp_path):
mock_tool.return_value = [
{"code": "E501", "location": {"row": 3}},
{"code": "E501", "location": {"row": 4}},
]
node = _make_node(1, 10, "func")
attach_ruff([node], tmp_path / "test.py")
assert node.ruff_codes.count("E501") == 1
class TestAttachTy:
@patch("sem.enrich._run_tool")
def test_attaches_ty_codes_to_matching_node(self, mock_tool, tmp_path):
mock_tool.return_value = [
{"code": "possibly-none", "location": {"row": 5}},
]
node = _make_node(1, 10, "func")
result = attach_ty([node], tmp_path / "test.py")
assert "possibly-none" in result[0].ty_codes
@patch("sem.enrich._run_tool")
def test_uses_rule_field_as_fallback(self, mock_tool, tmp_path):
mock_tool.return_value = [
{"rule": "type-error", "location": {"row": 5}},
]
node = _make_node(1, 10, "func")
result = attach_ty([node], tmp_path / "test.py")
assert "type-error" in result[0].ty_codes
@patch("sem.enrich._run_tool")
def test_skips_diag_without_line_or_code(self, mock_tool, tmp_path):
mock_tool.return_value = [
{"code": "", "location": {"row": 3}},
{"location": {"row": 0}},
]
node = _make_node(1, 10, "func")
attach_ty([node], tmp_path / "test.py")
assert node.ty_codes == []
@patch("sem.enrich._run_tool")
def test_no_duplicate_codes(self, mock_tool, tmp_path):
mock_tool.return_value = [
{"code": "possibly-none", "location": {"row": 3}},
{"code": "possibly-none", "location": {"row": 4}},
]
node = _make_node(1, 10, "func")
attach_ty([node], tmp_path / "test.py")
assert node.ty_codes.count("possibly-none") == 1