claude-dev-env 1.77.0 → 1.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.test.mjs +3 -2
- package/hooks/blocking/CLAUDE.md +5 -2
- package/hooks/blocking/code_rules_dead_module_constant.py +215 -59
- package/hooks/blocking/code_rules_dead_split_branch.py +225 -0
- package/hooks/blocking/code_rules_docstrings.py +951 -6
- package/hooks/blocking/code_rules_enforcer.py +64 -0
- package/hooks/blocking/code_rules_naming_collection.py +76 -1
- package/hooks/blocking/code_rules_paired_test.py +517 -0
- package/hooks/blocking/code_rules_string_magic.py +71 -1
- package/hooks/blocking/code_rules_test_assertions.py +159 -1
- package/hooks/blocking/convergence_gate_blocker.py +24 -15
- package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +89 -2
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +105 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +262 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +226 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +339 -0
- package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_whitespace_indentation_magic.py +74 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +71 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/blocking_check_limits.py +102 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
- package/hooks/hooks_constants/paired_test_coverage_constants.py +35 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +2 -0
- package/rules/docstring-prose-matches-implementation.md +56 -53
- package/rules/env-var-table-code-drift.md +24 -0
- package/rules/file-global-constants.md +2 -2
- package/rules/package-inventory-stale-entry.md +4 -4
- package/rules/paired-test-coverage.md +35 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Tests for check_docstring_documents_unreferenced_parameter.
|
|
2
|
+
|
|
3
|
+
A parameter named in the ``Args:`` block but referenced nowhere in the body is
|
|
4
|
+
dead: the function does not read it, yet the docstring describes behavior keyed
|
|
5
|
+
to it. The common shape is a flag a caller wired in before the real logic moved
|
|
6
|
+
up a level, leaving the parameter and its Args line claiming a behavior the body
|
|
7
|
+
does not implement.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_enforcer_module() -> ModuleType:
|
|
18
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
20
|
+
assert spec is not None
|
|
21
|
+
assert spec.loader is not None
|
|
22
|
+
module = importlib.util.module_from_spec(spec)
|
|
23
|
+
spec.loader.exec_module(module)
|
|
24
|
+
return module
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_documents_unreferenced_parameter(content: str, file_path: str) -> list[str]:
|
|
31
|
+
return code_rules_enforcer.check_docstring_documents_unreferenced_parameter(content, file_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
35
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PRODUCTION_FILE_PATH = "/project/src/per_theme_loop.py"
|
|
39
|
+
TEST_FILE_PATH = "/project/src/test_per_theme_loop.py"
|
|
40
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _function_with_dead_documented_flag() -> str:
|
|
44
|
+
return (
|
|
45
|
+
"def run_per_theme_loop_and_finalize(themes: list, is_no_notify: bool) -> int:\n"
|
|
46
|
+
' """Run the per-theme loop and finalize the report.\n'
|
|
47
|
+
"\n"
|
|
48
|
+
" Args:\n"
|
|
49
|
+
" themes: The themes to process.\n"
|
|
50
|
+
" is_no_notify: When True, suppresses opening the HTML report.\n"
|
|
51
|
+
"\n"
|
|
52
|
+
" Returns:\n"
|
|
53
|
+
" The resolved exit code.\n"
|
|
54
|
+
' """\n'
|
|
55
|
+
" exit_code = 0\n"
|
|
56
|
+
" for each_theme in themes:\n"
|
|
57
|
+
" exit_code = max(exit_code, each_theme.run())\n"
|
|
58
|
+
" render_and_write_html_report(themes)\n"
|
|
59
|
+
" return exit_code\n"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_flag_documented_parameter_never_referenced() -> None:
|
|
64
|
+
issues = check_documents_unreferenced_parameter(
|
|
65
|
+
_function_with_dead_documented_flag(), PRODUCTION_FILE_PATH
|
|
66
|
+
)
|
|
67
|
+
assert any("is_no_notify" in each for each in issues), (
|
|
68
|
+
f"Expected dead 'is_no_notify' flag, got: {issues!r}"
|
|
69
|
+
)
|
|
70
|
+
assert len(issues) == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_should_not_flag_parameter_referenced_in_body() -> None:
|
|
74
|
+
source = (
|
|
75
|
+
"def run_per_theme_loop_and_finalize(themes: list, is_no_notify: bool) -> int:\n"
|
|
76
|
+
' """Run the per-theme loop and finalize the report.\n'
|
|
77
|
+
"\n"
|
|
78
|
+
" Args:\n"
|
|
79
|
+
" themes: The themes to process.\n"
|
|
80
|
+
" is_no_notify: When True, suppresses opening the HTML report.\n"
|
|
81
|
+
"\n"
|
|
82
|
+
" Returns:\n"
|
|
83
|
+
" The resolved exit code.\n"
|
|
84
|
+
' """\n'
|
|
85
|
+
" exit_code = 0\n"
|
|
86
|
+
" for each_theme in themes:\n"
|
|
87
|
+
" exit_code = max(exit_code, each_theme.run())\n"
|
|
88
|
+
" if not is_no_notify:\n"
|
|
89
|
+
" open_html_report(themes)\n"
|
|
90
|
+
" return exit_code\n"
|
|
91
|
+
)
|
|
92
|
+
issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
|
|
93
|
+
assert issues == [], f"Referenced parameter must not be flagged, got: {issues!r}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_should_not_flag_when_kwargs_present() -> None:
|
|
97
|
+
source = (
|
|
98
|
+
"def render(themes: list, is_no_notify: bool, **overrides) -> int:\n"
|
|
99
|
+
' """Render the report.\n'
|
|
100
|
+
"\n"
|
|
101
|
+
" Args:\n"
|
|
102
|
+
" themes: The themes to process.\n"
|
|
103
|
+
" is_no_notify: When True, suppresses opening the report.\n"
|
|
104
|
+
' """\n'
|
|
105
|
+
" settings = dict(overrides)\n"
|
|
106
|
+
" for each_theme in themes:\n"
|
|
107
|
+
" each_theme.render(settings)\n"
|
|
108
|
+
" return 0\n"
|
|
109
|
+
)
|
|
110
|
+
issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
|
|
111
|
+
assert issues == [], f"**kwargs functions must be skipped, got: {issues!r}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_should_skip_private_function() -> None:
|
|
115
|
+
source = (
|
|
116
|
+
"def _drive(themes: list, is_no_notify: bool) -> int:\n"
|
|
117
|
+
' """Drive internally.\n'
|
|
118
|
+
"\n"
|
|
119
|
+
" Args:\n"
|
|
120
|
+
" themes: The themes to process.\n"
|
|
121
|
+
" is_no_notify: When True, suppresses the report.\n"
|
|
122
|
+
' """\n'
|
|
123
|
+
" exit_code = 0\n"
|
|
124
|
+
" for each_theme in themes:\n"
|
|
125
|
+
" exit_code = max(exit_code, each_theme.run())\n"
|
|
126
|
+
" return exit_code\n"
|
|
127
|
+
)
|
|
128
|
+
issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
|
|
129
|
+
assert issues == [], f"Private functions exempt, got: {issues!r}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_should_skip_test_file() -> None:
|
|
133
|
+
issues = check_documents_unreferenced_parameter(
|
|
134
|
+
_function_with_dead_documented_flag(), TEST_FILE_PATH
|
|
135
|
+
)
|
|
136
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
140
|
+
issues = check_documents_unreferenced_parameter(
|
|
141
|
+
_function_with_dead_documented_flag(), HOOK_INFRASTRUCTURE_PATH
|
|
142
|
+
)
|
|
143
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
147
|
+
issues = check_documents_unreferenced_parameter("def fetch(\n", PRODUCTION_FILE_PATH)
|
|
148
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_validate_content_surfaces_unreferenced_parameter() -> None:
|
|
152
|
+
issues = validate_content(
|
|
153
|
+
_function_with_dead_documented_flag(), PRODUCTION_FILE_PATH, old_content=""
|
|
154
|
+
)
|
|
155
|
+
matching_issues = [
|
|
156
|
+
each for each in issues if "is_no_notify" in each and "never references" in each
|
|
157
|
+
]
|
|
158
|
+
assert matching_issues, (
|
|
159
|
+
f"Expected validate_content to surface the dead-parameter issue, got: {issues!r}"
|
|
160
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Tests for check_module_docstring_scope_omits_data_schema_constants (Category O).
|
|
2
|
+
|
|
3
|
+
A module whose one-line docstring scopes its contents to user-facing text while
|
|
4
|
+
the body also defines serialization field keys, run-metadata schema keys, or
|
|
5
|
+
runtime config under-describes the module — the Category O module-responsibility
|
|
6
|
+
drift. The gate fires only when the docstring claims a user-facing-text scope and
|
|
7
|
+
acknowledges no data-schema or runtime-config category, so broadening the summary
|
|
8
|
+
to name the data-schema keys and runtime config clears it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_enforcer_module() -> ModuleType:
|
|
19
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
21
|
+
assert spec is not None
|
|
22
|
+
assert spec.loader is not None
|
|
23
|
+
module = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(module)
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_scope(content: str, file_path: str) -> list[str]:
|
|
32
|
+
return code_rules_enforcer.check_module_docstring_scope_omits_data_schema_constants(
|
|
33
|
+
content, file_path
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PRODUCTION_FILE_PATH = "/project/stp_contrast_fix/config/messages.py"
|
|
38
|
+
TEST_FILE_PATH = "/project/stp_contrast_fix/config/test_messages.py"
|
|
39
|
+
|
|
40
|
+
USER_FACING_SUMMARY = '"""User-facing strings: CLI flag names, help text, and log messages."""\n'
|
|
41
|
+
DATA_SCHEMA_BODY = (
|
|
42
|
+
"from typing import Final\n"
|
|
43
|
+
'CLI_FLAG_EXECUTE: Final[str] = "--execute"\n'
|
|
44
|
+
'JSONL_FIELD_STP_PATH: Final[str] = "stp_path"\n'
|
|
45
|
+
'RUN_METADATA_CLI_ARG_KEY_LIMIT: Final[str] = "limit"\n'
|
|
46
|
+
'STDOUT_ENCODING: Final[str] = "utf-8"\n'
|
|
47
|
+
'MAIN_LOGGING_FORMAT_STRING: Final[str] = "%(message)s"\n'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_flags_user_facing_summary_over_data_schema_constants() -> None:
|
|
52
|
+
issues = check_scope(USER_FACING_SUMMARY + DATA_SCHEMA_BODY, PRODUCTION_FILE_PATH)
|
|
53
|
+
assert len(issues) == 1
|
|
54
|
+
assert "JSONL_FIELD_STP_PATH" in issues[0]
|
|
55
|
+
assert "RUN_METADATA_CLI_ARG_KEY_LIMIT" in issues[0]
|
|
56
|
+
assert "module-responsibility drift" in issues[0]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_passes_when_summary_acknowledges_data_schema_scope() -> None:
|
|
60
|
+
acknowledging_summary = (
|
|
61
|
+
'"""User-facing strings plus per-theme JSONL field keys, run-metadata '
|
|
62
|
+
'schema keys, and runtime config."""\n'
|
|
63
|
+
)
|
|
64
|
+
assert check_scope(acknowledging_summary + DATA_SCHEMA_BODY, PRODUCTION_FILE_PATH) == []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_passes_when_module_has_no_data_schema_constants() -> None:
|
|
68
|
+
strings_only_body = (
|
|
69
|
+
"from typing import Final\n"
|
|
70
|
+
'CLI_FLAG_EXECUTE: Final[str] = "--execute"\n'
|
|
71
|
+
'CLI_FLAG_LIMIT: Final[str] = "--limit"\n'
|
|
72
|
+
)
|
|
73
|
+
assert check_scope(USER_FACING_SUMMARY + strings_only_body, PRODUCTION_FILE_PATH) == []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_passes_when_summary_does_not_claim_user_facing_scope() -> None:
|
|
77
|
+
non_user_facing_summary = '"""Theme-database column names and SQL templates."""\n'
|
|
78
|
+
assert check_scope(non_user_facing_summary + DATA_SCHEMA_BODY, PRODUCTION_FILE_PATH) == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_test_files_are_exempt() -> None:
|
|
82
|
+
assert check_scope(USER_FACING_SUMMARY + DATA_SCHEMA_BODY, TEST_FILE_PATH) == []
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Behavioral tests for the public-function paired-test coverage check.
|
|
2
|
+
|
|
3
|
+
Drives ``check_public_function_missing_paired_test`` over a real temporary
|
|
4
|
+
filesystem whose path carries no ``test``/``hooks``/``config`` segment, so the
|
|
5
|
+
module under test is classified as ordinary production code while its on-disk
|
|
6
|
+
paired test files are read exactly as they would be in a real package.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
_BLOCKING_DIRECTORY = Path(__file__).resolve().parent
|
|
19
|
+
if str(_BLOCKING_DIRECTORY) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_BLOCKING_DIRECTORY))
|
|
21
|
+
|
|
22
|
+
from code_rules_paired_test import ( # noqa: E402
|
|
23
|
+
check_public_function_missing_paired_test,
|
|
24
|
+
check_test_file_omits_module_public_function,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_TWO_PUBLIC_FUNCTIONS = "def alpha():\n return 1\n\n\ndef beta():\n return 2\n"
|
|
28
|
+
_THREE_PUBLIC_FUNCTIONS = (
|
|
29
|
+
"def alpha():\n return 1\n\n\n"
|
|
30
|
+
"def beta():\n return 2\n\n\n"
|
|
31
|
+
"def gamma():\n return 3\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def neutral_package_directory() -> Iterator[Path]:
|
|
37
|
+
"""Yield a ``pkg`` directory under a neutral temp root with a ``tests`` child.
|
|
38
|
+
|
|
39
|
+
The root prefix carries no ``test`` segment, so the module path the check
|
|
40
|
+
receives is classified as production code rather than a test file.
|
|
41
|
+
"""
|
|
42
|
+
with tempfile.TemporaryDirectory(prefix="paircov_") as root_name:
|
|
43
|
+
package_directory = Path(root_name) / "pkg"
|
|
44
|
+
(package_directory / "tests").mkdir(parents=True)
|
|
45
|
+
yield package_directory
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _write_test_file(package_directory: Path, filename: str, body: str) -> None:
|
|
49
|
+
(package_directory / "tests" / filename).write_text(body, encoding="utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_flags_public_function_absent_from_established_suite(
|
|
53
|
+
neutral_package_directory: Path,
|
|
54
|
+
) -> None:
|
|
55
|
+
_write_test_file(
|
|
56
|
+
neutral_package_directory,
|
|
57
|
+
"test_mod.py",
|
|
58
|
+
"from pkg.mod import alpha\n\ndef test_alpha():\n assert alpha() == 1\n",
|
|
59
|
+
)
|
|
60
|
+
all_issues = check_public_function_missing_paired_test(
|
|
61
|
+
_TWO_PUBLIC_FUNCTIONS, str(neutral_package_directory / "mod.py")
|
|
62
|
+
)
|
|
63
|
+
assert len(all_issues) == 1
|
|
64
|
+
assert "beta" in all_issues[0]
|
|
65
|
+
assert "paired test suite" in all_issues[0]
|
|
66
|
+
assert "alpha" not in all_issues[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_clean_when_suite_covers_every_public_function(
|
|
70
|
+
neutral_package_directory: Path,
|
|
71
|
+
) -> None:
|
|
72
|
+
_write_test_file(
|
|
73
|
+
neutral_package_directory,
|
|
74
|
+
"test_mod.py",
|
|
75
|
+
"from pkg.mod import alpha, beta\n\ndef test_both():\n assert alpha() and beta()\n",
|
|
76
|
+
)
|
|
77
|
+
all_issues = check_public_function_missing_paired_test(
|
|
78
|
+
_TWO_PUBLIC_FUNCTIONS, str(neutral_package_directory / "mod.py")
|
|
79
|
+
)
|
|
80
|
+
assert all_issues == []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_skips_module_without_dedicated_test_file(
|
|
84
|
+
neutral_package_directory: Path,
|
|
85
|
+
) -> None:
|
|
86
|
+
all_issues = check_public_function_missing_paired_test(
|
|
87
|
+
_TWO_PUBLIC_FUNCTIONS, str(neutral_package_directory / "mod.py")
|
|
88
|
+
)
|
|
89
|
+
assert all_issues == []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_skips_when_suite_covers_no_public_function(
|
|
93
|
+
neutral_package_directory: Path,
|
|
94
|
+
) -> None:
|
|
95
|
+
_write_test_file(
|
|
96
|
+
neutral_package_directory,
|
|
97
|
+
"test_mod.py",
|
|
98
|
+
"def test_unrelated():\n assert 1 + 1 == 2\n",
|
|
99
|
+
)
|
|
100
|
+
all_issues = check_public_function_missing_paired_test(
|
|
101
|
+
_TWO_PUBLIC_FUNCTIONS, str(neutral_package_directory / "mod.py")
|
|
102
|
+
)
|
|
103
|
+
assert all_issues == []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_flags_public_surface_when_suite_exercises_only_private_helper(
|
|
107
|
+
neutral_package_directory: Path,
|
|
108
|
+
) -> None:
|
|
109
|
+
module_source = (
|
|
110
|
+
"def _aarrggbb_to_css(value):\n return value\n\n\n"
|
|
111
|
+
"def render_table():\n return _aarrggbb_to_css('x')\n\n\n"
|
|
112
|
+
"def render_summary():\n return 2\n"
|
|
113
|
+
)
|
|
114
|
+
_write_test_file(
|
|
115
|
+
neutral_package_directory,
|
|
116
|
+
"test_mod.py",
|
|
117
|
+
"from pkg.mod import _aarrggbb_to_css\n\n"
|
|
118
|
+
"def test_helper():\n assert _aarrggbb_to_css('x') == 'x'\n",
|
|
119
|
+
)
|
|
120
|
+
all_issues = check_public_function_missing_paired_test(
|
|
121
|
+
module_source, str(neutral_package_directory / "mod.py")
|
|
122
|
+
)
|
|
123
|
+
assert len(all_issues) == 2
|
|
124
|
+
flagged_names = " ".join(all_issues)
|
|
125
|
+
assert "render_table" in flagged_names
|
|
126
|
+
assert "render_summary" in flagged_names
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_counts_coverage_across_sibling_test_files(
|
|
130
|
+
neutral_package_directory: Path,
|
|
131
|
+
) -> None:
|
|
132
|
+
_write_test_file(
|
|
133
|
+
neutral_package_directory,
|
|
134
|
+
"test_mod.py",
|
|
135
|
+
"from pkg.mod import alpha\n\ndef test_alpha():\n assert alpha() == 1\n",
|
|
136
|
+
)
|
|
137
|
+
_write_test_file(
|
|
138
|
+
neutral_package_directory,
|
|
139
|
+
"test_extra.py",
|
|
140
|
+
"from pkg.mod import beta\n\ndef test_beta():\n assert beta() == 2\n",
|
|
141
|
+
)
|
|
142
|
+
all_issues = check_public_function_missing_paired_test(
|
|
143
|
+
_TWO_PUBLIC_FUNCTIONS, str(neutral_package_directory / "mod.py")
|
|
144
|
+
)
|
|
145
|
+
assert all_issues == []
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_main_and_private_functions_are_never_required(
|
|
149
|
+
neutral_package_directory: Path,
|
|
150
|
+
) -> None:
|
|
151
|
+
module_source = (
|
|
152
|
+
"def alpha():\n return 1\n\n\n"
|
|
153
|
+
"def main():\n return 0\n\n\n"
|
|
154
|
+
"def _helper():\n return 2\n"
|
|
155
|
+
)
|
|
156
|
+
_write_test_file(
|
|
157
|
+
neutral_package_directory,
|
|
158
|
+
"test_mod.py",
|
|
159
|
+
"from pkg.mod import alpha\n\ndef test_alpha():\n assert alpha() == 1\n",
|
|
160
|
+
)
|
|
161
|
+
all_issues = check_public_function_missing_paired_test(
|
|
162
|
+
module_source, str(neutral_package_directory / "mod.py")
|
|
163
|
+
)
|
|
164
|
+
assert all_issues == []
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_exempts_hook_infrastructure_and_test_paths(
|
|
168
|
+
neutral_package_directory: Path,
|
|
169
|
+
) -> None:
|
|
170
|
+
_write_test_file(
|
|
171
|
+
neutral_package_directory,
|
|
172
|
+
"test_mod.py",
|
|
173
|
+
"from pkg.mod import alpha\n\ndef test_alpha():\n assert alpha() == 1\n",
|
|
174
|
+
)
|
|
175
|
+
hook_path = "/repo/hooks/blocking/code_rules_paired_test.py"
|
|
176
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
177
|
+
assert check_public_function_missing_paired_test(_TWO_PUBLIC_FUNCTIONS, hook_path) == []
|
|
178
|
+
assert check_public_function_missing_paired_test(_TWO_PUBLIC_FUNCTIONS, test_path) == []
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _write_module(package_directory: Path, body: str) -> None:
|
|
182
|
+
(package_directory / "mod.py").write_text(body, encoding="utf-8")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_SUITE_COVERS_ALPHA_BETA = (
|
|
186
|
+
"from pkg.mod import alpha, beta\n\n"
|
|
187
|
+
"def test_pair():\n assert alpha() == 1 and beta() == 2\n"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_flags_module_public_function_when_test_suite_omits_it(
|
|
192
|
+
neutral_package_directory: Path,
|
|
193
|
+
) -> None:
|
|
194
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
195
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
196
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
197
|
+
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
198
|
+
)
|
|
199
|
+
assert len(all_issues) == 1
|
|
200
|
+
assert "gamma" in all_issues[0]
|
|
201
|
+
assert "mod.py" in all_issues[0]
|
|
202
|
+
assert "alpha" not in all_issues[0]
|
|
203
|
+
assert "beta" not in all_issues[0]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_clean_when_test_suite_covers_every_module_public_function(
|
|
207
|
+
neutral_package_directory: Path,
|
|
208
|
+
) -> None:
|
|
209
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
210
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
211
|
+
suite_covers_all = (
|
|
212
|
+
"from pkg.mod import alpha, beta, gamma\n\n"
|
|
213
|
+
"def test_all():\n assert alpha() and beta() and gamma()\n"
|
|
214
|
+
)
|
|
215
|
+
all_issues = check_test_file_omits_module_public_function(suite_covers_all, test_path)
|
|
216
|
+
assert all_issues == []
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_skips_when_test_suite_covers_no_module_public_function(
|
|
220
|
+
neutral_package_directory: Path,
|
|
221
|
+
) -> None:
|
|
222
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
223
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
224
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
225
|
+
"def test_unrelated():\n assert 1 + 1 == 2\n", test_path
|
|
226
|
+
)
|
|
227
|
+
assert all_issues == []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_skips_test_file_with_no_paired_production_module(
|
|
231
|
+
neutral_package_directory: Path,
|
|
232
|
+
) -> None:
|
|
233
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
234
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
235
|
+
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
236
|
+
)
|
|
237
|
+
assert all_issues == []
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_ignores_a_non_stem_matched_written_file(
|
|
241
|
+
neutral_package_directory: Path,
|
|
242
|
+
) -> None:
|
|
243
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
244
|
+
helper_path = str(neutral_package_directory / "helper.py")
|
|
245
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
246
|
+
_SUITE_COVERS_ALPHA_BETA, helper_path
|
|
247
|
+
)
|
|
248
|
+
assert all_issues == []
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_resolves_production_module_beside_the_test_file(
|
|
252
|
+
neutral_package_directory: Path,
|
|
253
|
+
) -> None:
|
|
254
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
255
|
+
beside_test_path = str(neutral_package_directory / "test_mod.py")
|
|
256
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
257
|
+
_SUITE_COVERS_ALPHA_BETA, beside_test_path
|
|
258
|
+
)
|
|
259
|
+
assert len(all_issues) == 1
|
|
260
|
+
assert "gamma" in all_issues[0]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_judges_post_edit_content_over_stale_on_disk_test(
|
|
264
|
+
neutral_package_directory: Path,
|
|
265
|
+
) -> None:
|
|
266
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
267
|
+
_write_test_file(
|
|
268
|
+
neutral_package_directory,
|
|
269
|
+
"test_mod.py",
|
|
270
|
+
_SUITE_COVERS_ALPHA_BETA,
|
|
271
|
+
)
|
|
272
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
273
|
+
post_edit_covers_all = (
|
|
274
|
+
"from pkg.mod import alpha, beta, gamma\n\n"
|
|
275
|
+
"def test_all():\n assert alpha() and beta() and gamma()\n"
|
|
276
|
+
)
|
|
277
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
278
|
+
post_edit_covers_all, test_path
|
|
279
|
+
)
|
|
280
|
+
assert all_issues == []
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_counts_coverage_across_sibling_test_files_on_test_write(
|
|
284
|
+
neutral_package_directory: Path,
|
|
285
|
+
) -> None:
|
|
286
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
287
|
+
_write_test_file(
|
|
288
|
+
neutral_package_directory,
|
|
289
|
+
"test_extra.py",
|
|
290
|
+
"from pkg.mod import gamma\n\ndef test_gamma():\n assert gamma() == 3\n",
|
|
291
|
+
)
|
|
292
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
293
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
294
|
+
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
295
|
+
)
|
|
296
|
+
assert all_issues == []
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_test_side_message_names_module_and_function_without_line_prefix(
|
|
300
|
+
neutral_package_directory: Path,
|
|
301
|
+
) -> None:
|
|
302
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
303
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
304
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
305
|
+
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
306
|
+
)
|
|
307
|
+
assert len(all_issues) == 1
|
|
308
|
+
assert not all_issues[0].startswith("Line ")
|
|
309
|
+
assert "mod.py" in all_issues[0]
|
|
310
|
+
assert "gamma" in all_issues[0]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_test_side_omission_blocks_on_any_test_file_edit(
|
|
314
|
+
neutral_package_directory: Path,
|
|
315
|
+
) -> None:
|
|
316
|
+
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
317
|
+
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
318
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
319
|
+
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
320
|
+
)
|
|
321
|
+
assert len(all_issues) == 1
|
|
322
|
+
assert "gamma" in all_issues[0]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_skips_when_paired_production_module_is_exempt(
|
|
326
|
+
neutral_package_directory: Path,
|
|
327
|
+
) -> None:
|
|
328
|
+
(neutral_package_directory / "__init__.py").write_text(
|
|
329
|
+
_THREE_PUBLIC_FUNCTIONS, encoding="utf-8"
|
|
330
|
+
)
|
|
331
|
+
beside_test_path = str(neutral_package_directory / "test___init__.py")
|
|
332
|
+
suite_covers_all = (
|
|
333
|
+
"from pkg import alpha, beta, gamma\n\n"
|
|
334
|
+
"def test_all():\n assert alpha() and beta() and gamma()\n"
|
|
335
|
+
)
|
|
336
|
+
all_issues = check_test_file_omits_module_public_function(
|
|
337
|
+
suite_covers_all, beside_test_path
|
|
338
|
+
)
|
|
339
|
+
assert all_issues == []
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
8
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
9
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
10
|
+
|
|
11
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
12
|
+
TEST_FILE_PATH = "packages/app/tests/test_foo.py"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_should_flag_allowed_target_assigned_from_forbidden_callee() -> None:
|
|
16
|
+
source = (
|
|
17
|
+
"def offset() -> None:\n"
|
|
18
|
+
" is_inside_allowed = _point_hits_any_forbidden(px, py, rects)\n"
|
|
19
|
+
" return is_inside_allowed\n"
|
|
20
|
+
)
|
|
21
|
+
issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
|
|
22
|
+
assert any("is_inside_allowed" in each_issue for each_issue in issues), (
|
|
23
|
+
f"Expected the allowed/forbidden contradiction flagged, got: {issues}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_flag_attribute_callee_with_antonym() -> None:
|
|
28
|
+
source = (
|
|
29
|
+
"def gate() -> None:\n is_enabled = self._is_disabled(state)\n return is_enabled\n"
|
|
30
|
+
)
|
|
31
|
+
issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
|
|
32
|
+
assert any("is_enabled" in each_issue for each_issue in issues), (
|
|
33
|
+
f"Expected the enabled/disabled contradiction flagged, got: {issues}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_not_flag_when_callee_polarity_matches_target() -> None:
|
|
38
|
+
source = (
|
|
39
|
+
"def gate() -> None:\n"
|
|
40
|
+
" is_allowed = _point_inside_any_allowed_rect(px, py, rects)\n"
|
|
41
|
+
" return is_allowed\n"
|
|
42
|
+
)
|
|
43
|
+
issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
|
|
44
|
+
assert issues == [], f"Matching polarity must pass, got: {issues}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_should_not_flag_neutral_callee() -> None:
|
|
48
|
+
source = (
|
|
49
|
+
"def gate() -> None:\n"
|
|
50
|
+
" is_allowed = _point_inside_any_rect(px, py, rects)\n"
|
|
51
|
+
" return is_allowed\n"
|
|
52
|
+
)
|
|
53
|
+
issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
|
|
54
|
+
assert issues == [], f"Neutral callee must pass, got: {issues}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_should_not_flag_substring_only_token_match() -> None:
|
|
58
|
+
source = (
|
|
59
|
+
"def gate() -> None:\n"
|
|
60
|
+
" is_disallowed_count = compute_blocked_total(rects)\n"
|
|
61
|
+
" return is_disallowed_count\n"
|
|
62
|
+
)
|
|
63
|
+
issues = code_rules_enforcer.check_polarity_name_contradiction(source, PRODUCTION_FILE_PATH)
|
|
64
|
+
assert issues == [], (
|
|
65
|
+
f"'disallowed' embeds 'allowed' as a substring, not a whole token; must not flag, got: {issues}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_skip_in_test_files() -> None:
|
|
70
|
+
source = (
|
|
71
|
+
"def offset() -> None:\n"
|
|
72
|
+
" is_inside_allowed = _point_hits_any_forbidden(px, py, rects)\n"
|
|
73
|
+
" return is_inside_allowed\n"
|
|
74
|
+
)
|
|
75
|
+
issues = code_rules_enforcer.check_polarity_name_contradiction(source, TEST_FILE_PATH)
|
|
76
|
+
assert issues == [], f"Test files exempt, got: {issues}"
|