atdd 0.4.7__py3-none-any.whl → 0.6.0__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/coach/schemas/config.schema.json +63 -0
- atdd/coach/utils/coverage_phase.py +97 -0
- atdd/coach/validators/shared_fixtures.py +154 -0
- atdd/coder/conventions/coverage.convention.yaml +85 -0
- atdd/coder/validators/conftest.py +5 -0
- atdd/coder/validators/test_hierarchy_coverage.py +361 -0
- atdd/planner/conventions/coverage.convention.yaml +95 -0
- atdd/planner/validators/test_hierarchy_coverage.py +433 -0
- atdd/tester/conventions/coverage.convention.yaml +114 -0
- atdd/tester/validators/test_hierarchy_coverage.py +604 -0
- {atdd-0.4.7.dist-info → atdd-0.6.0.dist-info}/METADATA +1 -1
- {atdd-0.4.7.dist-info → atdd-0.6.0.dist-info}/RECORD +16 -8
- {atdd-0.4.7.dist-info → atdd-0.6.0.dist-info}/WHEEL +0 -0
- {atdd-0.4.7.dist-info → atdd-0.6.0.dist-info}/entry_points.txt +0 -0
- {atdd-0.4.7.dist-info → atdd-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {atdd-0.4.7.dist-info → atdd-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tester hierarchy coverage validation.
|
|
3
|
+
|
|
4
|
+
ATDD Hierarchy Coverage Spec v0.1 - Section 3: Tester Coverage Rules
|
|
5
|
+
|
|
6
|
+
Validates:
|
|
7
|
+
- Acceptance <-> Tests (COVERAGE-TEST-3.1)
|
|
8
|
+
- Contract <-> Wagon (COVERAGE-TEST-3.2)
|
|
9
|
+
- Telemetry <-> Wagon (COVERAGE-TEST-3.3)
|
|
10
|
+
- Telemetry tracking manifest (COVERAGE-TEST-3.4)
|
|
11
|
+
|
|
12
|
+
Architecture:
|
|
13
|
+
- Uses shared fixtures from atdd.coach.validators.shared_fixtures
|
|
14
|
+
- Phased rollout via atdd.coach.utils.coverage_phase
|
|
15
|
+
- Exception handling via .atdd/config.yaml coverage.exceptions
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
import yaml
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, List, Set, Tuple, Any, Optional
|
|
24
|
+
|
|
25
|
+
from atdd.coach.utils.repo import find_repo_root
|
|
26
|
+
from atdd.coach.utils.coverage_phase import (
|
|
27
|
+
CoveragePhase,
|
|
28
|
+
should_enforce,
|
|
29
|
+
emit_coverage_warning
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Path constants
|
|
34
|
+
REPO_ROOT = find_repo_root()
|
|
35
|
+
PLAN_DIR = REPO_ROOT / "plan"
|
|
36
|
+
CONTRACTS_DIR = REPO_ROOT / "contracts"
|
|
37
|
+
TELEMETRY_DIR = REPO_ROOT / "telemetry"
|
|
38
|
+
PYTHON_DIR = REPO_ROOT / "python"
|
|
39
|
+
SUPABASE_DIR = REPO_ROOT / "supabase"
|
|
40
|
+
TEST_DIR = REPO_ROOT / "test"
|
|
41
|
+
E2E_DIR = REPO_ROOT / "e2e"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ============================================================================
|
|
45
|
+
# HELPER FUNCTIONS
|
|
46
|
+
# ============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_acceptance_references_in_tests() -> Set[str]:
|
|
50
|
+
"""
|
|
51
|
+
Scan test files for acceptance URN references.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Set of acceptance URNs found in test files
|
|
55
|
+
"""
|
|
56
|
+
urn_pattern = re.compile(
|
|
57
|
+
r'acc:[a-z][a-z0-9\-]*:[A-Z0-9]+-[A-Z0-9]+-\d{3}(?:-[a-z0-9-]+)?',
|
|
58
|
+
re.IGNORECASE
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
found_urns: Set[str] = set()
|
|
62
|
+
|
|
63
|
+
# Scan Python tests
|
|
64
|
+
if PYTHON_DIR.exists():
|
|
65
|
+
for test_file in PYTHON_DIR.rglob("test_*.py"):
|
|
66
|
+
try:
|
|
67
|
+
content = test_file.read_text(encoding="utf-8")
|
|
68
|
+
matches = urn_pattern.findall(content)
|
|
69
|
+
found_urns.update(matches)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Scan TypeScript tests
|
|
74
|
+
if SUPABASE_DIR.exists():
|
|
75
|
+
for test_file in SUPABASE_DIR.rglob("*.test.ts"):
|
|
76
|
+
try:
|
|
77
|
+
content = test_file.read_text(encoding="utf-8")
|
|
78
|
+
matches = urn_pattern.findall(content)
|
|
79
|
+
found_urns.update(matches)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Scan E2E tests
|
|
84
|
+
if E2E_DIR.exists():
|
|
85
|
+
for test_file in E2E_DIR.rglob("*.test.ts"):
|
|
86
|
+
try:
|
|
87
|
+
content = test_file.read_text(encoding="utf-8")
|
|
88
|
+
matches = urn_pattern.findall(content)
|
|
89
|
+
found_urns.update(matches)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
# Scan Dart tests
|
|
94
|
+
if TEST_DIR.exists():
|
|
95
|
+
for test_file in TEST_DIR.rglob("*_test.dart"):
|
|
96
|
+
try:
|
|
97
|
+
content = test_file.read_text(encoding="utf-8")
|
|
98
|
+
matches = urn_pattern.findall(content)
|
|
99
|
+
found_urns.update(matches)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
return found_urns
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_contract_status(contract_path: Path) -> Optional[str]:
|
|
107
|
+
"""
|
|
108
|
+
Extract status from contract schema x-artifact-metadata.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Status string or None if not found
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
with open(contract_path) as f:
|
|
115
|
+
data = json.load(f)
|
|
116
|
+
metadata = data.get("x-artifact-metadata", {})
|
|
117
|
+
return metadata.get("status")
|
|
118
|
+
except Exception:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_telemetry_signal_status(signal_path: Path) -> Optional[str]:
|
|
123
|
+
"""
|
|
124
|
+
Extract status from telemetry signal file.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Status string or None if not found
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
with open(signal_path) as f:
|
|
131
|
+
data = json.load(f)
|
|
132
|
+
return data.get("status")
|
|
133
|
+
except Exception:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ============================================================================
|
|
138
|
+
# COVERAGE-TEST-3.1: Acceptance <-> Tests Coverage
|
|
139
|
+
# ============================================================================
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.tester
|
|
143
|
+
def test_all_acceptances_have_tests(all_acceptance_urns, coverage_exceptions):
|
|
144
|
+
"""
|
|
145
|
+
COVERAGE-TEST-3.1: Every acceptance has at least one test.
|
|
146
|
+
|
|
147
|
+
Given: All acceptance URNs from WMBT files
|
|
148
|
+
When: Scanning test files for URN references
|
|
149
|
+
Then: Every acceptance URN is referenced by at least one test
|
|
150
|
+
"""
|
|
151
|
+
if not all_acceptance_urns:
|
|
152
|
+
pytest.skip("No acceptance URNs found in plan/")
|
|
153
|
+
|
|
154
|
+
allowed_acceptances = set(coverage_exceptions.get("acceptance_without_tests", []))
|
|
155
|
+
|
|
156
|
+
# Find all acceptance references in tests
|
|
157
|
+
test_references = find_acceptance_references_in_tests()
|
|
158
|
+
|
|
159
|
+
# Normalize URNs for comparison (case-insensitive)
|
|
160
|
+
test_references_lower = {urn.lower() for urn in test_references}
|
|
161
|
+
|
|
162
|
+
violations = []
|
|
163
|
+
|
|
164
|
+
for acceptance_urn in all_acceptance_urns:
|
|
165
|
+
# Skip allowed exceptions
|
|
166
|
+
if acceptance_urn in allowed_acceptances:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
# Check if acceptance is referenced (case-insensitive)
|
|
170
|
+
if acceptance_urn.lower() not in test_references_lower:
|
|
171
|
+
violations.append(acceptance_urn)
|
|
172
|
+
|
|
173
|
+
if violations:
|
|
174
|
+
if should_enforce(CoveragePhase.PLANNER_TESTER_ENFORCEMENT):
|
|
175
|
+
pytest.fail(
|
|
176
|
+
f"COVERAGE-TEST-3.1: Acceptances without tests ({len(violations)}):\n " +
|
|
177
|
+
"\n ".join(violations[:20]) +
|
|
178
|
+
(f"\n ... and {len(violations) - 20} more" if len(violations) > 20 else "") +
|
|
179
|
+
"\n\nAdd tests or coverage.exceptions.acceptance_without_tests"
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
for violation in violations[:10]:
|
|
183
|
+
emit_coverage_warning(
|
|
184
|
+
"COVERAGE-TEST-3.1",
|
|
185
|
+
f"Acceptance without test: {violation}",
|
|
186
|
+
CoveragePhase.PLANNER_TESTER_ENFORCEMENT
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ============================================================================
|
|
191
|
+
# COVERAGE-TEST-3.2: Contract <-> Wagon Coverage
|
|
192
|
+
# ============================================================================
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.tester
|
|
196
|
+
def test_all_contracts_referenced(wagon_manifests, coverage_exceptions):
|
|
197
|
+
"""
|
|
198
|
+
COVERAGE-TEST-3.2a: Every contract schema referenced by wagon.
|
|
199
|
+
|
|
200
|
+
Given: Contract JSON files in contracts/
|
|
201
|
+
When: Checking wagon produce/consume references
|
|
202
|
+
Then: Every contract is referenced by at least one wagon
|
|
203
|
+
"""
|
|
204
|
+
if not CONTRACTS_DIR.exists():
|
|
205
|
+
pytest.skip("contracts/ directory does not exist")
|
|
206
|
+
|
|
207
|
+
allowed_contracts = set(coverage_exceptions.get("contracts_unreferenced", []))
|
|
208
|
+
|
|
209
|
+
# Build set of all contract references from wagons
|
|
210
|
+
wagon_contract_refs: Set[str] = set()
|
|
211
|
+
|
|
212
|
+
for path, manifest in wagon_manifests:
|
|
213
|
+
for produce_item in manifest.get("produce", []):
|
|
214
|
+
contract = produce_item.get("contract")
|
|
215
|
+
if contract:
|
|
216
|
+
wagon_contract_refs.add(contract)
|
|
217
|
+
|
|
218
|
+
for consume_item in manifest.get("consume", []):
|
|
219
|
+
contract = consume_item.get("contract")
|
|
220
|
+
if contract:
|
|
221
|
+
wagon_contract_refs.add(contract)
|
|
222
|
+
|
|
223
|
+
# Find all contract files
|
|
224
|
+
violations = []
|
|
225
|
+
|
|
226
|
+
for contract_file in CONTRACTS_DIR.rglob("*.json"):
|
|
227
|
+
# Skip non-schema files
|
|
228
|
+
if contract_file.name.startswith("_"):
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# Check status
|
|
232
|
+
status = get_contract_status(contract_file)
|
|
233
|
+
if status in ("draft", "external", "deprecated"):
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# Build contract URN from path
|
|
237
|
+
relative_path = contract_file.relative_to(CONTRACTS_DIR)
|
|
238
|
+
# contracts/domain/resource.json -> contract:domain:resource
|
|
239
|
+
parts = list(relative_path.parts)
|
|
240
|
+
if len(parts) >= 2:
|
|
241
|
+
domain = parts[0]
|
|
242
|
+
resource = parts[-1].replace(".json", "")
|
|
243
|
+
# Handle nested paths (category)
|
|
244
|
+
if len(parts) > 2:
|
|
245
|
+
category = "/".join(parts[1:-1])
|
|
246
|
+
resource = f"{category}/{resource}"
|
|
247
|
+
|
|
248
|
+
contract_urn = f"contract:{domain}:{resource}"
|
|
249
|
+
|
|
250
|
+
# Skip allowed exceptions
|
|
251
|
+
if contract_urn in allowed_contracts:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Check if referenced
|
|
255
|
+
is_referenced = any(
|
|
256
|
+
contract_urn in ref or ref.endswith(f":{resource}")
|
|
257
|
+
for ref in wagon_contract_refs
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if not is_referenced:
|
|
261
|
+
violations.append(
|
|
262
|
+
f"{contract_file.relative_to(REPO_ROOT)}: not referenced by any wagon"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if violations:
|
|
266
|
+
if should_enforce(CoveragePhase.PLANNER_TESTER_ENFORCEMENT):
|
|
267
|
+
pytest.fail(
|
|
268
|
+
f"COVERAGE-TEST-3.2a: Contracts not referenced:\n " +
|
|
269
|
+
"\n ".join(violations[:20]) +
|
|
270
|
+
(f"\n ... and {len(violations) - 20} more" if len(violations) > 20 else "") +
|
|
271
|
+
"\n\nAdd to wagon produce/consume or coverage.exceptions.contracts_unreferenced"
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
for violation in violations[:10]:
|
|
275
|
+
emit_coverage_warning(
|
|
276
|
+
"COVERAGE-TEST-3.2a",
|
|
277
|
+
violation,
|
|
278
|
+
CoveragePhase.PLANNER_TESTER_ENFORCEMENT
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@pytest.mark.tester
|
|
283
|
+
def test_all_contract_refs_exist(wagon_manifests):
|
|
284
|
+
"""
|
|
285
|
+
COVERAGE-TEST-3.2b: Every wagon contract ref has schema file.
|
|
286
|
+
|
|
287
|
+
Given: Wagon produce/consume with contract fields
|
|
288
|
+
When: Checking for corresponding files
|
|
289
|
+
Then: Every contract reference has a schema file
|
|
290
|
+
"""
|
|
291
|
+
if not CONTRACTS_DIR.exists():
|
|
292
|
+
pytest.skip("contracts/ directory does not exist")
|
|
293
|
+
|
|
294
|
+
violations = []
|
|
295
|
+
|
|
296
|
+
for path, manifest in wagon_manifests:
|
|
297
|
+
wagon_slug = manifest.get("wagon", path.parent.name)
|
|
298
|
+
|
|
299
|
+
all_contract_refs = []
|
|
300
|
+
|
|
301
|
+
for produce_item in manifest.get("produce", []):
|
|
302
|
+
contract = produce_item.get("contract")
|
|
303
|
+
if contract:
|
|
304
|
+
all_contract_refs.append((contract, "produce"))
|
|
305
|
+
|
|
306
|
+
for consume_item in manifest.get("consume", []):
|
|
307
|
+
contract = consume_item.get("contract")
|
|
308
|
+
if contract:
|
|
309
|
+
all_contract_refs.append((contract, "consume"))
|
|
310
|
+
|
|
311
|
+
for contract_ref, ref_type in all_contract_refs:
|
|
312
|
+
# Parse contract URN: contract:domain:resource
|
|
313
|
+
if contract_ref.startswith("contract:"):
|
|
314
|
+
parts = contract_ref.split(":")
|
|
315
|
+
if len(parts) >= 3:
|
|
316
|
+
domain = parts[1]
|
|
317
|
+
resource = ":".join(parts[2:]) # Handle nested resources
|
|
318
|
+
|
|
319
|
+
# Try to find the contract file
|
|
320
|
+
contract_path = CONTRACTS_DIR / domain / f"{resource}.json"
|
|
321
|
+
contract_path_nested = CONTRACTS_DIR / domain / resource / "index.json"
|
|
322
|
+
|
|
323
|
+
if not contract_path.exists() and not contract_path_nested.exists():
|
|
324
|
+
violations.append(
|
|
325
|
+
f"{wagon_slug}: {ref_type} contract '{contract_ref}' - file not found"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if violations:
|
|
329
|
+
if should_enforce(CoveragePhase.PLANNER_TESTER_ENFORCEMENT):
|
|
330
|
+
pytest.fail(
|
|
331
|
+
f"COVERAGE-TEST-3.2b: Contract references without files:\n " +
|
|
332
|
+
"\n ".join(violations)
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
for violation in violations:
|
|
336
|
+
emit_coverage_warning(
|
|
337
|
+
"COVERAGE-TEST-3.2b",
|
|
338
|
+
violation,
|
|
339
|
+
CoveragePhase.PLANNER_TESTER_ENFORCEMENT
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ============================================================================
|
|
344
|
+
# COVERAGE-TEST-3.3: Telemetry <-> Wagon Coverage
|
|
345
|
+
# ============================================================================
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@pytest.mark.tester
|
|
349
|
+
def test_all_telemetry_referenced(wagon_manifests, coverage_exceptions):
|
|
350
|
+
"""
|
|
351
|
+
COVERAGE-TEST-3.3a: Every telemetry signal referenced by wagon.
|
|
352
|
+
|
|
353
|
+
Given: Telemetry signal files in telemetry/
|
|
354
|
+
When: Checking wagon produce references
|
|
355
|
+
Then: Every telemetry signal is referenced by at least one wagon
|
|
356
|
+
"""
|
|
357
|
+
if not TELEMETRY_DIR.exists():
|
|
358
|
+
pytest.skip("telemetry/ directory does not exist")
|
|
359
|
+
|
|
360
|
+
allowed_telemetry = set(coverage_exceptions.get("telemetry_unreferenced", []))
|
|
361
|
+
|
|
362
|
+
# Build set of all telemetry references from wagons
|
|
363
|
+
wagon_telemetry_refs: Set[str] = set()
|
|
364
|
+
|
|
365
|
+
for path, manifest in wagon_manifests:
|
|
366
|
+
for produce_item in manifest.get("produce", []):
|
|
367
|
+
telemetry = produce_item.get("telemetry")
|
|
368
|
+
if telemetry:
|
|
369
|
+
if isinstance(telemetry, list):
|
|
370
|
+
wagon_telemetry_refs.update(telemetry)
|
|
371
|
+
else:
|
|
372
|
+
wagon_telemetry_refs.add(telemetry)
|
|
373
|
+
|
|
374
|
+
# Find all telemetry directories (each dir represents a telemetry URN)
|
|
375
|
+
violations = []
|
|
376
|
+
|
|
377
|
+
for domain_dir in TELEMETRY_DIR.iterdir():
|
|
378
|
+
if not domain_dir.is_dir() or domain_dir.name.startswith("_"):
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
for resource_dir in domain_dir.iterdir():
|
|
382
|
+
if not resource_dir.is_dir():
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
# Check for signal files
|
|
386
|
+
signal_files = list(resource_dir.glob("*.json"))
|
|
387
|
+
if not signal_files:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# Check status of first signal
|
|
391
|
+
status = get_telemetry_signal_status(signal_files[0])
|
|
392
|
+
if status in ("draft", "external", "deprecated"):
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
# Build telemetry URN
|
|
396
|
+
telemetry_urn = f"telemetry:{domain_dir.name}:{resource_dir.name}"
|
|
397
|
+
|
|
398
|
+
# Skip allowed exceptions
|
|
399
|
+
if telemetry_urn in allowed_telemetry:
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
# Check if referenced
|
|
403
|
+
is_referenced = any(
|
|
404
|
+
telemetry_urn in ref or ref.endswith(f":{resource_dir.name}")
|
|
405
|
+
for ref in wagon_telemetry_refs
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if not is_referenced:
|
|
409
|
+
violations.append(
|
|
410
|
+
f"{resource_dir.relative_to(REPO_ROOT)}: not referenced by any wagon"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if violations:
|
|
414
|
+
if should_enforce(CoveragePhase.PLANNER_TESTER_ENFORCEMENT):
|
|
415
|
+
pytest.fail(
|
|
416
|
+
f"COVERAGE-TEST-3.3a: Telemetry not referenced:\n " +
|
|
417
|
+
"\n ".join(violations[:20]) +
|
|
418
|
+
(f"\n ... and {len(violations) - 20} more" if len(violations) > 20 else "") +
|
|
419
|
+
"\n\nAdd to wagon produce or coverage.exceptions.telemetry_unreferenced"
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
for violation in violations[:10]:
|
|
423
|
+
emit_coverage_warning(
|
|
424
|
+
"COVERAGE-TEST-3.3a",
|
|
425
|
+
violation,
|
|
426
|
+
CoveragePhase.PLANNER_TESTER_ENFORCEMENT
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@pytest.mark.tester
|
|
431
|
+
def test_all_telemetry_refs_exist(wagon_manifests):
|
|
432
|
+
"""
|
|
433
|
+
COVERAGE-TEST-3.3b: Every wagon telemetry ref has signal files.
|
|
434
|
+
|
|
435
|
+
Given: Wagon produce with telemetry fields
|
|
436
|
+
When: Checking for corresponding directories
|
|
437
|
+
Then: Every telemetry reference has signal files
|
|
438
|
+
"""
|
|
439
|
+
if not TELEMETRY_DIR.exists():
|
|
440
|
+
pytest.skip("telemetry/ directory does not exist")
|
|
441
|
+
|
|
442
|
+
violations = []
|
|
443
|
+
|
|
444
|
+
for path, manifest in wagon_manifests:
|
|
445
|
+
wagon_slug = manifest.get("wagon", path.parent.name)
|
|
446
|
+
|
|
447
|
+
for produce_item in manifest.get("produce", []):
|
|
448
|
+
telemetry = produce_item.get("telemetry")
|
|
449
|
+
if not telemetry:
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
telemetry_refs = telemetry if isinstance(telemetry, list) else [telemetry]
|
|
453
|
+
|
|
454
|
+
for telemetry_ref in telemetry_refs:
|
|
455
|
+
# Parse telemetry URN: telemetry:domain:resource[.category]
|
|
456
|
+
if telemetry_ref.startswith("telemetry:"):
|
|
457
|
+
parts = telemetry_ref.split(":")
|
|
458
|
+
if len(parts) >= 3:
|
|
459
|
+
domain = parts[1]
|
|
460
|
+
resource = parts[2]
|
|
461
|
+
|
|
462
|
+
# Handle category suffix (resource.category)
|
|
463
|
+
if "." in resource:
|
|
464
|
+
resource_parts = resource.split(".")
|
|
465
|
+
resource = resource_parts[0]
|
|
466
|
+
category = "/".join(resource_parts[1:])
|
|
467
|
+
telemetry_path = TELEMETRY_DIR / domain / resource / category
|
|
468
|
+
else:
|
|
469
|
+
telemetry_path = TELEMETRY_DIR / domain / resource
|
|
470
|
+
|
|
471
|
+
# Check if directory exists with signal files
|
|
472
|
+
if not telemetry_path.exists():
|
|
473
|
+
violations.append(
|
|
474
|
+
f"{wagon_slug}: telemetry '{telemetry_ref}' - directory not found"
|
|
475
|
+
)
|
|
476
|
+
elif not list(telemetry_path.glob("*.json")):
|
|
477
|
+
violations.append(
|
|
478
|
+
f"{wagon_slug}: telemetry '{telemetry_ref}' - no signal files"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
if violations:
|
|
482
|
+
if should_enforce(CoveragePhase.PLANNER_TESTER_ENFORCEMENT):
|
|
483
|
+
pytest.fail(
|
|
484
|
+
f"COVERAGE-TEST-3.3b: Telemetry references without files:\n " +
|
|
485
|
+
"\n ".join(violations)
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
for violation in violations:
|
|
489
|
+
emit_coverage_warning(
|
|
490
|
+
"COVERAGE-TEST-3.3b",
|
|
491
|
+
violation,
|
|
492
|
+
CoveragePhase.PLANNER_TESTER_ENFORCEMENT
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ============================================================================
|
|
497
|
+
# COVERAGE-TEST-3.4: Telemetry Tracking Manifest
|
|
498
|
+
# ============================================================================
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@pytest.mark.tester
|
|
502
|
+
def test_telemetry_manifest_complete():
|
|
503
|
+
"""
|
|
504
|
+
COVERAGE-TEST-3.4: Tracking manifest signals all exist as files.
|
|
505
|
+
|
|
506
|
+
Given: telemetry/_tracking_manifest.yaml if present
|
|
507
|
+
When: Checking listed signals
|
|
508
|
+
Then: All signals in manifest have corresponding files
|
|
509
|
+
"""
|
|
510
|
+
manifest_path = TELEMETRY_DIR / "_tracking_manifest.yaml"
|
|
511
|
+
|
|
512
|
+
if not manifest_path.exists():
|
|
513
|
+
pytest.skip("No telemetry tracking manifest found")
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
with open(manifest_path) as f:
|
|
517
|
+
manifest_data = yaml.safe_load(f)
|
|
518
|
+
except Exception as e:
|
|
519
|
+
pytest.fail(f"Failed to load tracking manifest: {e}")
|
|
520
|
+
|
|
521
|
+
violations = []
|
|
522
|
+
|
|
523
|
+
signals = manifest_data.get("signals", [])
|
|
524
|
+
for signal_entry in signals:
|
|
525
|
+
if isinstance(signal_entry, dict):
|
|
526
|
+
signal_path = signal_entry.get("path")
|
|
527
|
+
signal_urn = signal_entry.get("urn")
|
|
528
|
+
else:
|
|
529
|
+
signal_path = signal_entry
|
|
530
|
+
signal_urn = signal_entry
|
|
531
|
+
|
|
532
|
+
if signal_path:
|
|
533
|
+
full_path = TELEMETRY_DIR / signal_path
|
|
534
|
+
if not full_path.exists():
|
|
535
|
+
violations.append(f"{signal_urn or signal_path}: file not found")
|
|
536
|
+
|
|
537
|
+
if violations:
|
|
538
|
+
if should_enforce(CoveragePhase.PLANNER_TESTER_ENFORCEMENT):
|
|
539
|
+
pytest.fail(
|
|
540
|
+
f"COVERAGE-TEST-3.4: Tracking manifest signals missing:\n " +
|
|
541
|
+
"\n ".join(violations)
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
for violation in violations:
|
|
545
|
+
emit_coverage_warning(
|
|
546
|
+
"COVERAGE-TEST-3.4",
|
|
547
|
+
violation,
|
|
548
|
+
CoveragePhase.PLANNER_TESTER_ENFORCEMENT
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# ============================================================================
|
|
553
|
+
# COVERAGE SUMMARY
|
|
554
|
+
# ============================================================================
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@pytest.mark.tester
|
|
558
|
+
def test_tester_coverage_summary(
|
|
559
|
+
all_acceptance_urns,
|
|
560
|
+
wagon_manifests,
|
|
561
|
+
coverage_thresholds
|
|
562
|
+
):
|
|
563
|
+
"""
|
|
564
|
+
COVERAGE-TEST-SUMMARY: Report tester coverage statistics.
|
|
565
|
+
|
|
566
|
+
This test always passes but reports coverage metrics for visibility.
|
|
567
|
+
"""
|
|
568
|
+
# Count acceptance coverage
|
|
569
|
+
test_references = find_acceptance_references_in_tests()
|
|
570
|
+
test_references_lower = {urn.lower() for urn in test_references}
|
|
571
|
+
|
|
572
|
+
covered_acceptances = sum(
|
|
573
|
+
1 for urn in all_acceptance_urns
|
|
574
|
+
if urn.lower() in test_references_lower
|
|
575
|
+
)
|
|
576
|
+
total_acceptances = len(all_acceptance_urns)
|
|
577
|
+
|
|
578
|
+
# Calculate coverage percentage
|
|
579
|
+
coverage_pct = (covered_acceptances / total_acceptances * 100) if total_acceptances > 0 else 0
|
|
580
|
+
threshold = coverage_thresholds.get("min_acceptance_coverage", 80)
|
|
581
|
+
|
|
582
|
+
# Count contracts
|
|
583
|
+
total_contracts = 0
|
|
584
|
+
if CONTRACTS_DIR.exists():
|
|
585
|
+
total_contracts = len(list(CONTRACTS_DIR.rglob("*.json")))
|
|
586
|
+
|
|
587
|
+
# Count telemetry
|
|
588
|
+
total_telemetry = 0
|
|
589
|
+
if TELEMETRY_DIR.exists():
|
|
590
|
+
for domain_dir in TELEMETRY_DIR.iterdir():
|
|
591
|
+
if domain_dir.is_dir() and not domain_dir.name.startswith("_"):
|
|
592
|
+
total_telemetry += sum(1 for _ in domain_dir.iterdir() if _.is_dir())
|
|
593
|
+
|
|
594
|
+
# Report summary
|
|
595
|
+
summary = (
|
|
596
|
+
f"\n\nTester Coverage Summary:\n"
|
|
597
|
+
f" Acceptances covered: {covered_acceptances}/{total_acceptances} ({coverage_pct:.1f}%)\n"
|
|
598
|
+
f" Coverage threshold: {threshold}%\n"
|
|
599
|
+
f" Total contracts: {total_contracts}\n"
|
|
600
|
+
f" Total telemetry domains: {total_telemetry}"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# This test always passes - it's informational
|
|
604
|
+
assert True, summary
|