atdd 0.2.1__py3-none-any.whl
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.
- atdd/__init__.py +6 -0
- atdd/__main__.py +4 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- atdd-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test Python code complexity stays within acceptable thresholds.
|
|
3
|
+
|
|
4
|
+
Validates:
|
|
5
|
+
- Cyclomatic complexity < 10 per function
|
|
6
|
+
- Nesting depth < 4 levels
|
|
7
|
+
- Function length < 50 lines
|
|
8
|
+
- No overly complex functions
|
|
9
|
+
|
|
10
|
+
Inspired by: .claude/utils/coder/complexity.py
|
|
11
|
+
But: Self-contained, no utility dependencies
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Path constants
|
|
21
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
22
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Complexity thresholds
|
|
26
|
+
MAX_CYCLOMATIC_COMPLEXITY = 10
|
|
27
|
+
MAX_NESTING_DEPTH = 4
|
|
28
|
+
MAX_FUNCTION_LINES = 50
|
|
29
|
+
MAX_FUNCTION_PARAMS = 6
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_python_files() -> List[Path]:
|
|
33
|
+
"""Find all Python source files (excluding tests)."""
|
|
34
|
+
if not PYTHON_DIR.exists():
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
files = []
|
|
38
|
+
for py_file in PYTHON_DIR.rglob("*.py"):
|
|
39
|
+
if '/test/' in str(py_file) or py_file.name.startswith('test_'):
|
|
40
|
+
continue
|
|
41
|
+
if '__pycache__' in str(py_file):
|
|
42
|
+
continue
|
|
43
|
+
if py_file.name == '__init__.py':
|
|
44
|
+
continue
|
|
45
|
+
files.append(py_file)
|
|
46
|
+
|
|
47
|
+
return files
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_functions(file_path: Path) -> List[Tuple[str, int, str]]:
|
|
51
|
+
"""
|
|
52
|
+
Extract functions from Python file.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of (function_name, line_number, function_body) tuples
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
59
|
+
content = f.read()
|
|
60
|
+
except Exception:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
functions = []
|
|
64
|
+
lines = content.split('\n')
|
|
65
|
+
|
|
66
|
+
i = 0
|
|
67
|
+
while i < len(lines):
|
|
68
|
+
line = lines[i]
|
|
69
|
+
|
|
70
|
+
# Match function definition: def function_name(...)
|
|
71
|
+
func_match = re.match(r'^\s*(async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(', line)
|
|
72
|
+
|
|
73
|
+
if func_match:
|
|
74
|
+
func_name = func_match.group(2)
|
|
75
|
+
start_line = i + 1 # Line numbers are 1-based
|
|
76
|
+
indent = len(line) - len(line.lstrip())
|
|
77
|
+
|
|
78
|
+
# Extract function body
|
|
79
|
+
body_lines = [line]
|
|
80
|
+
i += 1
|
|
81
|
+
|
|
82
|
+
# Find end of function (next line with same or less indentation that's not blank)
|
|
83
|
+
while i < len(lines):
|
|
84
|
+
current_line = lines[i]
|
|
85
|
+
|
|
86
|
+
# Skip blank lines and comments
|
|
87
|
+
if not current_line.strip() or current_line.strip().startswith('#'):
|
|
88
|
+
body_lines.append(current_line)
|
|
89
|
+
i += 1
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
current_indent = len(current_line) - len(current_line.lstrip())
|
|
93
|
+
|
|
94
|
+
# If indentation is same or less and not blank, function ended
|
|
95
|
+
if current_indent <= indent and current_line.strip():
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
body_lines.append(current_line)
|
|
99
|
+
i += 1
|
|
100
|
+
|
|
101
|
+
function_body = '\n'.join(body_lines)
|
|
102
|
+
functions.append((func_name, start_line, function_body))
|
|
103
|
+
else:
|
|
104
|
+
i += 1
|
|
105
|
+
|
|
106
|
+
return functions
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def calculate_cyclomatic_complexity(function_body: str) -> int:
|
|
110
|
+
"""
|
|
111
|
+
Calculate cyclomatic complexity of a function.
|
|
112
|
+
|
|
113
|
+
Cyclomatic complexity = number of decision points + 1
|
|
114
|
+
|
|
115
|
+
Decision points:
|
|
116
|
+
- if, elif
|
|
117
|
+
- for, while
|
|
118
|
+
- and, or (in conditions)
|
|
119
|
+
- except
|
|
120
|
+
- case (match statement)
|
|
121
|
+
"""
|
|
122
|
+
complexity = 1 # Base complexity
|
|
123
|
+
|
|
124
|
+
# Count decision keywords
|
|
125
|
+
keywords = ['if', 'elif', 'for', 'while', 'except', 'case']
|
|
126
|
+
for keyword in keywords:
|
|
127
|
+
# Match keyword as whole word
|
|
128
|
+
pattern = r'\b' + keyword + r'\b'
|
|
129
|
+
complexity += len(re.findall(pattern, function_body))
|
|
130
|
+
|
|
131
|
+
# Count boolean operators in conditions
|
|
132
|
+
# (simplified - count 'and' and 'or' in lines with 'if', 'elif', 'while')
|
|
133
|
+
condition_lines = [line for line in function_body.split('\n')
|
|
134
|
+
if re.search(r'\b(if|elif|while)\b', line)]
|
|
135
|
+
|
|
136
|
+
for line in condition_lines:
|
|
137
|
+
complexity += len(re.findall(r'\band\b', line))
|
|
138
|
+
complexity += len(re.findall(r'\bor\b', line))
|
|
139
|
+
|
|
140
|
+
return complexity
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def calculate_nesting_depth(function_body: str) -> int:
|
|
144
|
+
"""
|
|
145
|
+
Calculate maximum nesting depth in a function.
|
|
146
|
+
|
|
147
|
+
Counts nested blocks (if, for, while, with, try, etc.)
|
|
148
|
+
"""
|
|
149
|
+
max_depth = 0
|
|
150
|
+
current_depth = 0
|
|
151
|
+
base_indent = None
|
|
152
|
+
|
|
153
|
+
for line in function_body.split('\n'):
|
|
154
|
+
stripped = line.strip()
|
|
155
|
+
|
|
156
|
+
# Skip blank lines and comments
|
|
157
|
+
if not stripped or stripped.startswith('#'):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# Calculate indentation
|
|
161
|
+
indent = len(line) - len(line.lstrip())
|
|
162
|
+
|
|
163
|
+
# Set base indent from first non-empty line
|
|
164
|
+
if base_indent is None:
|
|
165
|
+
base_indent = indent
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# Calculate depth relative to function start
|
|
169
|
+
relative_indent = indent - base_indent
|
|
170
|
+
|
|
171
|
+
# Each 4 spaces = 1 level (standard Python indentation)
|
|
172
|
+
current_depth = relative_indent // 4
|
|
173
|
+
|
|
174
|
+
# Check if line introduces a new block
|
|
175
|
+
if stripped.endswith(':') and any(
|
|
176
|
+
stripped.startswith(kw) for kw in
|
|
177
|
+
['if', 'elif', 'else', 'for', 'while', 'with', 'try', 'except', 'finally', 'def', 'class']
|
|
178
|
+
):
|
|
179
|
+
max_depth = max(max_depth, current_depth + 1)
|
|
180
|
+
else:
|
|
181
|
+
max_depth = max(max_depth, current_depth)
|
|
182
|
+
|
|
183
|
+
return max_depth
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def count_function_lines(function_body: str) -> int:
|
|
187
|
+
"""
|
|
188
|
+
Count lines of code in function (excluding blank lines and comments).
|
|
189
|
+
"""
|
|
190
|
+
lines = function_body.split('\n')
|
|
191
|
+
code_lines = 0
|
|
192
|
+
|
|
193
|
+
for line in lines:
|
|
194
|
+
stripped = line.strip()
|
|
195
|
+
# Skip blank lines and pure comment lines
|
|
196
|
+
if stripped and not stripped.startswith('#'):
|
|
197
|
+
code_lines += 1
|
|
198
|
+
|
|
199
|
+
return code_lines
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def count_function_parameters(function_body: str) -> int:
|
|
203
|
+
"""
|
|
204
|
+
Count number of parameters in function definition.
|
|
205
|
+
"""
|
|
206
|
+
# Extract first line (function signature)
|
|
207
|
+
first_line = function_body.split('\n')[0]
|
|
208
|
+
|
|
209
|
+
# Extract parameters from signature
|
|
210
|
+
match = re.search(r'def\s+\w+\s*\((.*?)\)', first_line)
|
|
211
|
+
if not match:
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
params = match.group(1).strip()
|
|
215
|
+
|
|
216
|
+
# No parameters
|
|
217
|
+
if not params:
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
# Split by comma (simple counting)
|
|
221
|
+
# This is simplified - doesn't handle complex default values perfectly
|
|
222
|
+
param_list = [p.strip() for p in params.split(',')]
|
|
223
|
+
|
|
224
|
+
# Filter out 'self' and 'cls'
|
|
225
|
+
param_list = [p for p in param_list if not p.startswith('self') and not p.startswith('cls')]
|
|
226
|
+
|
|
227
|
+
return len(param_list)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@pytest.mark.coder
|
|
231
|
+
def test_cyclomatic_complexity_under_threshold():
|
|
232
|
+
"""
|
|
233
|
+
SPEC-CODER-COMPLEXITY-0001: Functions have acceptable cyclomatic complexity.
|
|
234
|
+
|
|
235
|
+
Cyclomatic complexity measures the number of independent paths through code.
|
|
236
|
+
High complexity indicates code that is:
|
|
237
|
+
- Hard to test
|
|
238
|
+
- Hard to understand
|
|
239
|
+
- More likely to contain bugs
|
|
240
|
+
|
|
241
|
+
Threshold: < 10 (industry standard)
|
|
242
|
+
|
|
243
|
+
Given: All Python functions
|
|
244
|
+
When: Calculating cyclomatic complexity
|
|
245
|
+
Then: Complexity < 10 for all functions
|
|
246
|
+
"""
|
|
247
|
+
python_files = find_python_files()
|
|
248
|
+
|
|
249
|
+
if not python_files:
|
|
250
|
+
pytest.skip("No Python files found")
|
|
251
|
+
|
|
252
|
+
violations = []
|
|
253
|
+
|
|
254
|
+
for py_file in python_files:
|
|
255
|
+
functions = extract_functions(py_file)
|
|
256
|
+
|
|
257
|
+
for func_name, line_num, func_body in functions:
|
|
258
|
+
# Skip very small functions (< 3 lines)
|
|
259
|
+
if count_function_lines(func_body) < 3:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
complexity = calculate_cyclomatic_complexity(func_body)
|
|
263
|
+
|
|
264
|
+
if complexity > MAX_CYCLOMATIC_COMPLEXITY:
|
|
265
|
+
rel_path = py_file.relative_to(REPO_ROOT)
|
|
266
|
+
violations.append(
|
|
267
|
+
f"{rel_path}:{line_num}\\n"
|
|
268
|
+
f" Function: {func_name}\\n"
|
|
269
|
+
f" Complexity: {complexity} (max: {MAX_CYCLOMATIC_COMPLEXITY})\\n"
|
|
270
|
+
f" Suggestion: Break into smaller functions"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if violations:
|
|
274
|
+
pytest.fail(
|
|
275
|
+
f"\\n\\nFound {len(violations)} complexity violations:\\n\\n" +
|
|
276
|
+
"\\n\\n".join(violations[:10]) +
|
|
277
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@pytest.mark.coder
|
|
282
|
+
def test_nesting_depth_under_threshold():
|
|
283
|
+
"""
|
|
284
|
+
SPEC-CODER-COMPLEXITY-0002: Functions have acceptable nesting depth.
|
|
285
|
+
|
|
286
|
+
Deep nesting makes code:
|
|
287
|
+
- Hard to read
|
|
288
|
+
- Hard to test
|
|
289
|
+
- More error-prone
|
|
290
|
+
|
|
291
|
+
Threshold: < 4 levels
|
|
292
|
+
|
|
293
|
+
Given: All Python functions
|
|
294
|
+
When: Calculating nesting depth
|
|
295
|
+
Then: Depth < 4 for all functions
|
|
296
|
+
"""
|
|
297
|
+
python_files = find_python_files()
|
|
298
|
+
|
|
299
|
+
if not python_files:
|
|
300
|
+
pytest.skip("No Python files found")
|
|
301
|
+
|
|
302
|
+
violations = []
|
|
303
|
+
|
|
304
|
+
for py_file in python_files:
|
|
305
|
+
functions = extract_functions(py_file)
|
|
306
|
+
|
|
307
|
+
for func_name, line_num, func_body in functions:
|
|
308
|
+
depth = calculate_nesting_depth(func_body)
|
|
309
|
+
|
|
310
|
+
if depth > MAX_NESTING_DEPTH:
|
|
311
|
+
rel_path = py_file.relative_to(REPO_ROOT)
|
|
312
|
+
violations.append(
|
|
313
|
+
f"{rel_path}:{line_num}\\n"
|
|
314
|
+
f" Function: {func_name}\\n"
|
|
315
|
+
f" Nesting depth: {depth} (max: {MAX_NESTING_DEPTH})\\n"
|
|
316
|
+
f" Suggestion: Extract nested logic into separate functions"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if violations:
|
|
320
|
+
pytest.fail(
|
|
321
|
+
f"\\n\\nFound {len(violations)} nesting depth violations:\\n\\n" +
|
|
322
|
+
"\\n\\n".join(violations[:10]) +
|
|
323
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@pytest.mark.coder
|
|
328
|
+
def test_function_length_under_threshold():
|
|
329
|
+
"""
|
|
330
|
+
SPEC-CODER-COMPLEXITY-0003: Functions are not too long.
|
|
331
|
+
|
|
332
|
+
Long functions are:
|
|
333
|
+
- Hard to understand
|
|
334
|
+
- Hard to test
|
|
335
|
+
- Likely doing too much (SRP violation)
|
|
336
|
+
|
|
337
|
+
Threshold: < 50 lines of code
|
|
338
|
+
|
|
339
|
+
Given: All Python functions
|
|
340
|
+
When: Counting lines of code
|
|
341
|
+
Then: Length < 50 for all functions
|
|
342
|
+
"""
|
|
343
|
+
python_files = find_python_files()
|
|
344
|
+
|
|
345
|
+
if not python_files:
|
|
346
|
+
pytest.skip("No Python files found")
|
|
347
|
+
|
|
348
|
+
violations = []
|
|
349
|
+
|
|
350
|
+
for py_file in python_files:
|
|
351
|
+
functions = extract_functions(py_file)
|
|
352
|
+
|
|
353
|
+
for func_name, line_num, func_body in functions:
|
|
354
|
+
lines = count_function_lines(func_body)
|
|
355
|
+
|
|
356
|
+
if lines > MAX_FUNCTION_LINES:
|
|
357
|
+
rel_path = py_file.relative_to(REPO_ROOT)
|
|
358
|
+
violations.append(
|
|
359
|
+
f"{rel_path}:{line_num}\\n"
|
|
360
|
+
f" Function: {func_name}\\n"
|
|
361
|
+
f" Lines: {lines} (max: {MAX_FUNCTION_LINES})\\n"
|
|
362
|
+
f" Suggestion: Break into smaller functions"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if violations:
|
|
366
|
+
pytest.fail(
|
|
367
|
+
f"\\n\\nFound {len(violations)} function length violations:\\n\\n" +
|
|
368
|
+
"\\n\\n".join(violations[:10]) +
|
|
369
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@pytest.mark.coder
|
|
374
|
+
def test_function_parameter_count_under_threshold():
|
|
375
|
+
"""
|
|
376
|
+
SPEC-CODER-COMPLEXITY-0004: Functions don't have too many parameters.
|
|
377
|
+
|
|
378
|
+
Too many parameters indicate:
|
|
379
|
+
- Function doing too much
|
|
380
|
+
- Poor abstraction
|
|
381
|
+
- Hard to call/test
|
|
382
|
+
|
|
383
|
+
Threshold: < 6 parameters
|
|
384
|
+
|
|
385
|
+
Given: All Python functions
|
|
386
|
+
When: Counting parameters
|
|
387
|
+
Then: Parameters < 6 for all functions
|
|
388
|
+
"""
|
|
389
|
+
python_files = find_python_files()
|
|
390
|
+
|
|
391
|
+
if not python_files:
|
|
392
|
+
pytest.skip("No Python files found")
|
|
393
|
+
|
|
394
|
+
violations = []
|
|
395
|
+
|
|
396
|
+
for py_file in python_files:
|
|
397
|
+
functions = extract_functions(py_file)
|
|
398
|
+
|
|
399
|
+
for func_name, line_num, func_body in functions:
|
|
400
|
+
param_count = count_function_parameters(func_body)
|
|
401
|
+
|
|
402
|
+
if param_count > MAX_FUNCTION_PARAMS:
|
|
403
|
+
rel_path = py_file.relative_to(REPO_ROOT)
|
|
404
|
+
violations.append(
|
|
405
|
+
f"{rel_path}:{line_num}\\n"
|
|
406
|
+
f" Function: {func_name}\\n"
|
|
407
|
+
f" Parameters: {param_count} (max: {MAX_FUNCTION_PARAMS})\\n"
|
|
408
|
+
f" Suggestion: Use parameter objects or reduce responsibilities"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if violations:
|
|
412
|
+
pytest.fail(
|
|
413
|
+
f"\\n\\nFound {len(violations)} parameter count violations:\\n\\n" +
|
|
414
|
+
"\\n\\n".join(violations[:10]) +
|
|
415
|
+
(f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
|
416
|
+
)
|