From fd001d7e7af8be03b89a063f4e4b1d8c004ec3d8 Mon Sep 17 00:00:00 2001 From: asemraza Date: Thu, 13 Aug 2026 20:11:08 +0200 Subject: [PATCH] Exempt lifecycle and construction methods from test gaps setUp, tearDown, constructors and their xUnit/unittest variants are exercised implicitly by every test that touches their class. Listing them as test gaps inflates the gap count and pushes noise into the Untested summary, which erodes trust in the panel for real gaps. Partial fix; the container-resolution linkage itself is tracked in issue 850 and in issue 851. Part of #850 --- code_review_graph/changes.py | 13 +++++++++++++ tests/test_changes.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/code_review_graph/changes.py b/code_review_graph/changes.py index 2165fba1..f97d35ab 100644 --- a/code_review_graph/changes.py +++ b/code_review_graph/changes.py @@ -20,6 +20,17 @@ logger = logging.getLogger(__name__) + +# Lifecycle and construction methods are exercised implicitly by every test +# that touches their class, so listing them as test gaps is noise that +# inflates the gap count and the Untested summary (#850). Names cover +# PHPUnit/xUnit and Python unittest conventions. +_TEST_GAP_EXEMPT_NAMES = frozenset({ + "setUp", "tearDown", "setUpBeforeClass", "tearDownAfterClass", + "setup_method", "teardown_method", "setUpClass", "tearDownClass", + "__construct", "__init__", "__destruct", +}) + _GIT_TIMEOUT = int(os.environ.get("CRG_GIT_TIMEOUT", "30")) # seconds, configurable _SAFE_GIT_REF = re.compile(r"^[A-Za-z0-9_.~^/@{}\-]+$") @@ -471,6 +482,8 @@ def analyze_changes( for node in changed_funcs: if node.is_test: continue + if node.name in _TEST_GAP_EXEMPT_NAMES: + continue # TESTED_BY edges are stored as source=production, target=test by the # parser, so a changed production function finds its tests by source. # See: #515 diff --git a/tests/test_changes.py b/tests/test_changes.py index c0d4f75f..bdf4dbf5 100644 --- a/tests/test_changes.py +++ b/tests/test_changes.py @@ -403,6 +403,31 @@ def test_analyze_changes_with_flows(self): ) assert len(result["affected_flows"]) >= 1 + def test_analyze_changes_lifecycle_methods_not_test_gaps(self): + """Lifecycle and construction methods are exempt from gaps (#850).""" + self._add_func("setUp", path="thing_test_helper.py", + line_start=1, line_end=5) + self._add_func("tearDown", path="thing_test_helper.py", + line_start=6, line_end=10) + self._add_func("__construct", path="thing.php", + line_start=1, line_end=5) + self._add_func("realMethod", path="thing.php", + line_start=6, line_end=20) + + result = analyze_changes( + self.store, + changed_files=["thing_test_helper.py", "thing.php"], + changed_ranges={ + "thing_test_helper.py": [(1, 10)], + "thing.php": [(1, 20)], + }, + ) + gap_names = {g["name"] for g in result["test_gaps"]} + assert "setUp" not in gap_names + assert "tearDown" not in gap_names + assert "__construct" not in gap_names + assert "realMethod" in gap_names + def test_analyze_changes_review_priorities_ordered(self): """Review priorities are ordered by descending risk score.""" # Create several functions with varying risk levels.