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
151 lines
5.2 KiB
Python
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
|