atdd 0.4.5__py3-none-any.whl → 0.4.7__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/cli.py +71 -12
- atdd/coach/commands/inventory.py +91 -3
- atdd/coach/commands/registry.py +475 -202
- atdd/coach/utils/config.py +131 -0
- atdd/coach/utils/train_spec_phase.py +97 -0
- atdd/coach/validators/shared_fixtures.py +68 -1
- atdd/coach/validators/test_train_registry.py +189 -0
- atdd/coder/validators/test_train_infrastructure.py +236 -2
- atdd/planner/schemas/train.schema.json +125 -2
- atdd/planner/validators/test_train_validation.py +667 -2
- atdd/tester/validators/test_train_backend_e2e.py +371 -0
- atdd/tester/validators/test_train_frontend_e2e.py +292 -0
- atdd/tester/validators/test_train_frontend_python.py +282 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/METADATA +1 -1
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/RECORD +19 -13
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/WHEEL +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/entry_points.txt +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/licenses/LICENSE +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ATDD Configuration Loader.
|
|
3
|
+
|
|
4
|
+
Loads configuration from .atdd/config.yaml for train validation and enforcement.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_atdd_config(repo_root: Path) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Load .atdd/config.yaml configuration file.
|
|
15
|
+
|
|
16
|
+
The config file controls:
|
|
17
|
+
- FastAPI template enforcement (Section 11)
|
|
18
|
+
- Train validation behavior
|
|
19
|
+
- Custom path conventions
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
repo_root: Repository root path
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Parsed configuration dict, or empty dict if file doesn't exist
|
|
26
|
+
|
|
27
|
+
Example config:
|
|
28
|
+
trains:
|
|
29
|
+
enforce_fastapi_template: true
|
|
30
|
+
backend_runner_paths:
|
|
31
|
+
- python/trains/runner.py
|
|
32
|
+
- python/trains/{train_id}/runner.py
|
|
33
|
+
frontend_allowed_roots:
|
|
34
|
+
- web/src/
|
|
35
|
+
- web/components/
|
|
36
|
+
"""
|
|
37
|
+
config_path = repo_root / ".atdd" / "config.yaml"
|
|
38
|
+
|
|
39
|
+
if not config_path.exists():
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with open(config_path) as f:
|
|
44
|
+
config = yaml.safe_load(f)
|
|
45
|
+
return config if config else {}
|
|
46
|
+
except Exception:
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_train_config(repo_root: Path) -> Dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Get train-specific configuration.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
repo_root: Repository root path
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Train configuration dict with defaults applied
|
|
59
|
+
"""
|
|
60
|
+
config = load_atdd_config(repo_root)
|
|
61
|
+
train_config = config.get("trains", {})
|
|
62
|
+
|
|
63
|
+
# Apply defaults
|
|
64
|
+
defaults = {
|
|
65
|
+
"enforce_fastapi_template": False,
|
|
66
|
+
"backend_runner_paths": [
|
|
67
|
+
"python/trains/runner.py",
|
|
68
|
+
"python/trains/{train_id}/runner.py"
|
|
69
|
+
],
|
|
70
|
+
"frontend_allowed_roots": [
|
|
71
|
+
"web/src/",
|
|
72
|
+
"web/components/",
|
|
73
|
+
"web/pages/"
|
|
74
|
+
],
|
|
75
|
+
"frontend_python_paths": [
|
|
76
|
+
"python/streamlit/",
|
|
77
|
+
"python/apps/"
|
|
78
|
+
],
|
|
79
|
+
"e2e_backend_pattern": "e2e/{theme}/test_{train_id}*.py",
|
|
80
|
+
"e2e_frontend_pattern": "web/e2e/{train_id}/*.spec.ts"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Merge with defaults
|
|
84
|
+
for key, default_value in defaults.items():
|
|
85
|
+
if key not in train_config:
|
|
86
|
+
train_config[key] = default_value
|
|
87
|
+
|
|
88
|
+
return train_config
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_validation_config(repo_root: Path) -> Dict[str, Any]:
|
|
92
|
+
"""
|
|
93
|
+
Get validation-specific configuration.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
repo_root: Repository root path
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Validation configuration with defaults
|
|
100
|
+
"""
|
|
101
|
+
config = load_atdd_config(repo_root)
|
|
102
|
+
validation_config = config.get("validation", {})
|
|
103
|
+
|
|
104
|
+
defaults = {
|
|
105
|
+
"strict_mode": False,
|
|
106
|
+
"warn_on_missing_tests": True,
|
|
107
|
+
"warn_on_missing_code": True,
|
|
108
|
+
"require_primary_wagon": False
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for key, default_value in defaults.items():
|
|
112
|
+
if key not in validation_config:
|
|
113
|
+
validation_config[key] = default_value
|
|
114
|
+
|
|
115
|
+
return validation_config
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_feature_enabled(repo_root: Path, feature: str) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Check if a specific feature is enabled in config.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
repo_root: Repository root path
|
|
124
|
+
feature: Feature name to check
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if feature is enabled, False otherwise
|
|
128
|
+
"""
|
|
129
|
+
config = load_atdd_config(repo_root)
|
|
130
|
+
features = config.get("features", {})
|
|
131
|
+
return features.get(feature, False)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Train First-Class Spec v0.6 Rollout Phase Controller.
|
|
3
|
+
|
|
4
|
+
Manages the phased rollout of train validation rules:
|
|
5
|
+
- Phase 1 (WARNINGS_ONLY): All new validators emit warnings only
|
|
6
|
+
- Phase 2 (BACKEND_ENFORCEMENT): Backend validators become strict
|
|
7
|
+
- Phase 3 (FULL_ENFORCEMENT): All validators become strict
|
|
8
|
+
|
|
9
|
+
Usage in validators:
|
|
10
|
+
from atdd.coach.utils.train_spec_phase import TrainSpecPhase, should_enforce
|
|
11
|
+
|
|
12
|
+
if should_enforce(TrainSpecPhase.BACKEND_ENFORCEMENT):
|
|
13
|
+
assert condition, "Error message"
|
|
14
|
+
else:
|
|
15
|
+
if not condition:
|
|
16
|
+
warnings.warn("Warning message")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from enum import IntEnum
|
|
20
|
+
from typing import Optional
|
|
21
|
+
import warnings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TrainSpecPhase(IntEnum):
|
|
25
|
+
"""
|
|
26
|
+
Rollout phases for Train First-Class Spec v0.6.
|
|
27
|
+
|
|
28
|
+
Phases are ordered by strictness level:
|
|
29
|
+
- WARNINGS_ONLY (1): All new validators emit warnings, no assertions
|
|
30
|
+
- BACKEND_ENFORCEMENT (2): Backend validators (0022-0025, 0031-0033) strict
|
|
31
|
+
- FULL_ENFORCEMENT (3): All validators strict
|
|
32
|
+
"""
|
|
33
|
+
WARNINGS_ONLY = 1
|
|
34
|
+
BACKEND_ENFORCEMENT = 2
|
|
35
|
+
FULL_ENFORCEMENT = 3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Current rollout phase - update this to advance through phases
|
|
39
|
+
CURRENT_PHASE = TrainSpecPhase.WARNINGS_ONLY
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def should_enforce(validator_phase: TrainSpecPhase) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Check if a validator should enforce strict mode.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
validator_phase: The phase at which this validator becomes strict
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if current phase >= validator_phase (should enforce)
|
|
51
|
+
False if current phase < validator_phase (should warn only)
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
# This validator becomes strict in Phase 2
|
|
55
|
+
if should_enforce(TrainSpecPhase.BACKEND_ENFORCEMENT):
|
|
56
|
+
assert backend_test_exists, "Backend test required"
|
|
57
|
+
else:
|
|
58
|
+
if not backend_test_exists:
|
|
59
|
+
warnings.warn("Backend test missing (warning only)")
|
|
60
|
+
"""
|
|
61
|
+
return CURRENT_PHASE >= validator_phase
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_current_phase() -> TrainSpecPhase:
|
|
65
|
+
"""Get the current rollout phase."""
|
|
66
|
+
return CURRENT_PHASE
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_phase_name(phase: Optional[TrainSpecPhase] = None) -> str:
|
|
70
|
+
"""Get human-readable name for a phase."""
|
|
71
|
+
phase = phase or CURRENT_PHASE
|
|
72
|
+
return {
|
|
73
|
+
TrainSpecPhase.WARNINGS_ONLY: "Phase 1: Warnings Only",
|
|
74
|
+
TrainSpecPhase.BACKEND_ENFORCEMENT: "Phase 2: Backend Enforcement",
|
|
75
|
+
TrainSpecPhase.FULL_ENFORCEMENT: "Phase 3: Full Enforcement",
|
|
76
|
+
}.get(phase, "Unknown Phase")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def emit_phase_warning(
|
|
80
|
+
spec_id: str,
|
|
81
|
+
message: str,
|
|
82
|
+
validator_phase: TrainSpecPhase = TrainSpecPhase.BACKEND_ENFORCEMENT
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Emit a deprecation/validation warning with phase context.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
spec_id: The SPEC ID (e.g., "SPEC-TRAIN-VAL-0022")
|
|
89
|
+
message: The warning message
|
|
90
|
+
validator_phase: Phase when this becomes an error
|
|
91
|
+
"""
|
|
92
|
+
phase_name = get_phase_name(validator_phase)
|
|
93
|
+
warnings.warn(
|
|
94
|
+
f"[{spec_id}] {message} (will become error in {phase_name})",
|
|
95
|
+
category=UserWarning,
|
|
96
|
+
stacklevel=3
|
|
97
|
+
)
|
|
@@ -14,11 +14,12 @@ Validators should use:
|
|
|
14
14
|
import json
|
|
15
15
|
import yaml
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Dict, Any, List, Tuple
|
|
17
|
+
from typing import Dict, Any, List, Tuple, Optional
|
|
18
18
|
import pytest
|
|
19
19
|
|
|
20
20
|
import atdd
|
|
21
21
|
from atdd.coach.utils.repo import find_repo_root
|
|
22
|
+
from atdd.coach.utils.config import load_atdd_config, get_train_config
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# Path constants
|
|
@@ -192,6 +193,72 @@ def trains_registry() -> Dict[str, Any]:
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
|
|
196
|
+
@pytest.fixture(scope="module")
|
|
197
|
+
def trains_registry_with_groups() -> Dict[str, Dict[str, List[Dict]]]:
|
|
198
|
+
"""
|
|
199
|
+
Load trains registry preserving full group structure for theme validation.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Trains data with full nesting preserved:
|
|
203
|
+
{"0-commons": {"00-commons-nominal": [train1, train2], ...}, ...}
|
|
204
|
+
|
|
205
|
+
This fixture is used for validating theme derivation from group keys.
|
|
206
|
+
"""
|
|
207
|
+
trains_file = PLAN_DIR / "_trains.yaml"
|
|
208
|
+
if trains_file.exists():
|
|
209
|
+
with open(trains_file) as f:
|
|
210
|
+
data = yaml.safe_load(f)
|
|
211
|
+
return data.get("trains", {})
|
|
212
|
+
return {}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@pytest.fixture(scope="module")
|
|
216
|
+
def train_files() -> List[Tuple[Path, Dict]]:
|
|
217
|
+
"""
|
|
218
|
+
Load all train YAML files with their data.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of (path, train_data) tuples for all train files in plan/_trains/
|
|
222
|
+
"""
|
|
223
|
+
trains_dir = PLAN_DIR / "_trains"
|
|
224
|
+
train_files_data = []
|
|
225
|
+
|
|
226
|
+
if trains_dir.exists():
|
|
227
|
+
for train_file in sorted(trains_dir.glob("*.yaml")):
|
|
228
|
+
if not train_file.name.startswith("_"):
|
|
229
|
+
try:
|
|
230
|
+
with open(train_file) as f:
|
|
231
|
+
train_data = yaml.safe_load(f)
|
|
232
|
+
if train_data:
|
|
233
|
+
train_files_data.append((train_file, train_data))
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
return train_files_data
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.fixture(scope="module")
|
|
241
|
+
def atdd_config() -> Dict[str, Any]:
|
|
242
|
+
"""
|
|
243
|
+
Load .atdd/config.yaml configuration.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Configuration dict with train and validation settings
|
|
247
|
+
"""
|
|
248
|
+
return load_atdd_config(REPO_ROOT)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@pytest.fixture(scope="module")
|
|
252
|
+
def train_config() -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
Load train-specific configuration with defaults.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Train configuration dict with defaults applied
|
|
258
|
+
"""
|
|
259
|
+
return get_train_config(REPO_ROOT)
|
|
260
|
+
|
|
261
|
+
|
|
195
262
|
@pytest.fixture(scope="module")
|
|
196
263
|
def wagons_registry() -> Dict[str, Any]:
|
|
197
264
|
"""
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Coach validators: Train registry gap validation.
|
|
3
|
+
|
|
4
|
+
Train First-Class Spec v0.6 Section 14: Inventory Reporting
|
|
5
|
+
|
|
6
|
+
Validates:
|
|
7
|
+
- SPEC-TRAIN-VAL-0037: Inventory reports train gaps
|
|
8
|
+
- SPEC-TRAIN-VAL-0038: Gap report format validation
|
|
9
|
+
|
|
10
|
+
Ensures the inventory command properly reports gaps in train implementation
|
|
11
|
+
across backend, frontend, and frontend_python platforms.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, Any, List
|
|
17
|
+
|
|
18
|
+
from atdd.coach.utils.repo import find_repo_root
|
|
19
|
+
from atdd.coach.commands.inventory import RepositoryInventory
|
|
20
|
+
from atdd.coach.utils.train_spec_phase import (
|
|
21
|
+
TrainSpecPhase,
|
|
22
|
+
should_enforce,
|
|
23
|
+
emit_phase_warning
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Path constants
|
|
28
|
+
REPO_ROOT = find_repo_root()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(scope="module")
|
|
32
|
+
def train_inventory() -> Dict[str, Any]:
|
|
33
|
+
"""Generate train inventory with gap reporting."""
|
|
34
|
+
inventory = RepositoryInventory(REPO_ROOT)
|
|
35
|
+
return inventory.scan_trains()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.platform
|
|
39
|
+
def test_inventory_reports_train_gaps(train_inventory):
|
|
40
|
+
"""
|
|
41
|
+
SPEC-TRAIN-VAL-0037: Inventory reports train gaps
|
|
42
|
+
|
|
43
|
+
Given: Inventory scan of trains
|
|
44
|
+
When: Checking gap reporting fields
|
|
45
|
+
Then: Inventory includes missing_test_* and missing_code_* arrays
|
|
46
|
+
|
|
47
|
+
Section 14: Gap Reporting
|
|
48
|
+
"""
|
|
49
|
+
required_gap_fields = [
|
|
50
|
+
"missing_test_backend",
|
|
51
|
+
"missing_test_frontend",
|
|
52
|
+
"missing_test_frontend_python",
|
|
53
|
+
"missing_code_backend",
|
|
54
|
+
"missing_code_frontend",
|
|
55
|
+
"missing_code_frontend_python"
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
missing_fields = []
|
|
59
|
+
for field in required_gap_fields:
|
|
60
|
+
if field not in train_inventory:
|
|
61
|
+
missing_fields.append(field)
|
|
62
|
+
|
|
63
|
+
if missing_fields:
|
|
64
|
+
pytest.fail(
|
|
65
|
+
f"Inventory missing gap reporting fields:\n " +
|
|
66
|
+
"\n ".join(missing_fields) +
|
|
67
|
+
"\n\nExpected fields for Train First-Class Spec v0.6 Section 14"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Verify all gap fields are lists
|
|
71
|
+
for field in required_gap_fields:
|
|
72
|
+
value = train_inventory.get(field)
|
|
73
|
+
assert isinstance(value, list), \
|
|
74
|
+
f"Gap field '{field}' should be a list, got {type(value).__name__}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.platform
|
|
78
|
+
def test_gap_report_format_validation(train_inventory):
|
|
79
|
+
"""
|
|
80
|
+
SPEC-TRAIN-VAL-0038: Gap report format validation
|
|
81
|
+
|
|
82
|
+
Given: Inventory with gap reporting
|
|
83
|
+
When: Checking gap report structure
|
|
84
|
+
Then: Gaps summary exists with correct format
|
|
85
|
+
|
|
86
|
+
Section 14: Gap Report Format
|
|
87
|
+
"""
|
|
88
|
+
# Check for gaps summary
|
|
89
|
+
assert "gaps" in train_inventory, \
|
|
90
|
+
"Inventory should include 'gaps' summary object"
|
|
91
|
+
|
|
92
|
+
gaps = train_inventory["gaps"]
|
|
93
|
+
|
|
94
|
+
# Validate structure
|
|
95
|
+
assert isinstance(gaps, dict), \
|
|
96
|
+
"gaps should be a dictionary"
|
|
97
|
+
|
|
98
|
+
assert "test" in gaps, \
|
|
99
|
+
"gaps should include 'test' category"
|
|
100
|
+
|
|
101
|
+
assert "code" in gaps, \
|
|
102
|
+
"gaps should include 'code' category"
|
|
103
|
+
|
|
104
|
+
# Validate test gaps structure
|
|
105
|
+
test_gaps = gaps["test"]
|
|
106
|
+
assert isinstance(test_gaps, dict), \
|
|
107
|
+
"gaps.test should be a dictionary"
|
|
108
|
+
|
|
109
|
+
required_platforms = ["backend", "frontend", "frontend_python"]
|
|
110
|
+
for platform in required_platforms:
|
|
111
|
+
assert platform in test_gaps, \
|
|
112
|
+
f"gaps.test should include '{platform}'"
|
|
113
|
+
assert isinstance(test_gaps[platform], int), \
|
|
114
|
+
f"gaps.test.{platform} should be an integer count"
|
|
115
|
+
|
|
116
|
+
# Validate code gaps structure
|
|
117
|
+
code_gaps = gaps["code"]
|
|
118
|
+
assert isinstance(code_gaps, dict), \
|
|
119
|
+
"gaps.code should be a dictionary"
|
|
120
|
+
|
|
121
|
+
for platform in required_platforms:
|
|
122
|
+
assert platform in code_gaps, \
|
|
123
|
+
f"gaps.code should include '{platform}'"
|
|
124
|
+
assert isinstance(code_gaps[platform], int), \
|
|
125
|
+
f"gaps.code.{platform} should be an integer count"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@pytest.mark.platform
|
|
129
|
+
def test_gap_counts_match_arrays(train_inventory):
|
|
130
|
+
"""
|
|
131
|
+
Verify gap counts match corresponding array lengths.
|
|
132
|
+
|
|
133
|
+
Given: Inventory with gap reporting
|
|
134
|
+
When: Comparing counts to arrays
|
|
135
|
+
Then: Counts equal array lengths
|
|
136
|
+
"""
|
|
137
|
+
gaps = train_inventory.get("gaps", {})
|
|
138
|
+
|
|
139
|
+
# Test gaps
|
|
140
|
+
test_gaps = gaps.get("test", {})
|
|
141
|
+
assert test_gaps.get("backend", 0) == len(train_inventory.get("missing_test_backend", [])), \
|
|
142
|
+
"gaps.test.backend count mismatch"
|
|
143
|
+
assert test_gaps.get("frontend", 0) == len(train_inventory.get("missing_test_frontend", [])), \
|
|
144
|
+
"gaps.test.frontend count mismatch"
|
|
145
|
+
assert test_gaps.get("frontend_python", 0) == len(train_inventory.get("missing_test_frontend_python", [])), \
|
|
146
|
+
"gaps.test.frontend_python count mismatch"
|
|
147
|
+
|
|
148
|
+
# Code gaps
|
|
149
|
+
code_gaps = gaps.get("code", {})
|
|
150
|
+
assert code_gaps.get("backend", 0) == len(train_inventory.get("missing_code_backend", [])), \
|
|
151
|
+
"gaps.code.backend count mismatch"
|
|
152
|
+
assert code_gaps.get("frontend", 0) == len(train_inventory.get("missing_code_frontend", [])), \
|
|
153
|
+
"gaps.code.frontend count mismatch"
|
|
154
|
+
assert code_gaps.get("frontend_python", 0) == len(train_inventory.get("missing_code_frontend_python", [])), \
|
|
155
|
+
"gaps.code.frontend_python count mismatch"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.platform
|
|
159
|
+
def test_gap_train_ids_are_valid(train_inventory):
|
|
160
|
+
"""
|
|
161
|
+
Verify train IDs in gap arrays are valid.
|
|
162
|
+
|
|
163
|
+
Given: Inventory with gap arrays
|
|
164
|
+
When: Checking train IDs
|
|
165
|
+
Then: All train IDs in gaps are in train_ids list
|
|
166
|
+
"""
|
|
167
|
+
all_train_ids = set(train_inventory.get("train_ids", []))
|
|
168
|
+
|
|
169
|
+
if not all_train_ids:
|
|
170
|
+
pytest.skip("No trains found in inventory")
|
|
171
|
+
|
|
172
|
+
gap_fields = [
|
|
173
|
+
"missing_test_backend",
|
|
174
|
+
"missing_test_frontend",
|
|
175
|
+
"missing_test_frontend_python",
|
|
176
|
+
"missing_code_backend",
|
|
177
|
+
"missing_code_frontend",
|
|
178
|
+
"missing_code_frontend_python"
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
invalid_ids = []
|
|
182
|
+
for field in gap_fields:
|
|
183
|
+
gap_ids = train_inventory.get(field, [])
|
|
184
|
+
for train_id in gap_ids:
|
|
185
|
+
if train_id not in all_train_ids:
|
|
186
|
+
invalid_ids.append(f"{field}: {train_id}")
|
|
187
|
+
|
|
188
|
+
assert not invalid_ids, \
|
|
189
|
+
f"Gap arrays contain invalid train IDs:\n " + "\n ".join(invalid_ids)
|