fixture_validator.mojo (12679B)
1 from std.collections import List 2 3 from std.os.path import exists 4 from std.pathlib import Path 5 from json import Value, loads 6 7 8 @fieldwise_init 9 struct FixtureValidationIssue(Copyable, Movable): 10 var case_id: String 11 var rule: String 12 var detail: String 13 14 15 def registered_fixture_operators() -> List[String]: 16 var operators = List[String]() 17 operators.append("equals") 18 operators.append("absent") 19 operators.append("present") 20 operators.append("contains") 21 operators.append("not_equals") 22 return operators^ 23 24 25 def _has_key(value: Value, key: String) -> Bool: 26 for candidate in value.object_keys(): 27 if candidate == key: 28 return True 29 return False 30 31 32 def _is_registered_operator(operator: String, registered: List[String]) -> Bool: 33 for candidate in registered: 34 if candidate == operator: 35 return True 36 return False 37 38 39 def validate_fixture_corpus( 40 directory: String, 41 ) raises -> List[FixtureValidationIssue]: 42 var issues = List[FixtureValidationIssue]() 43 var base = Path(directory) 44 var manifest = loads((base / "manifest.json").read_text()) 45 if not _has_key(manifest, "cases"): 46 issues.append( 47 FixtureValidationIssue( 48 case_id="", rule="missing_cases", detail="manifest has no cases" 49 ) 50 ) 51 return issues^ 52 53 var cases = manifest["cases"].array_items() 54 if len(cases) == 0: 55 issues.append( 56 FixtureValidationIssue( 57 case_id="", 58 rule="empty_corpus", 59 detail="manifest declares no cases", 60 ) 61 ) 62 63 var seen = List[String]() 64 var registered = registered_fixture_operators() 65 for entry in cases: 66 var case_id = entry["case_id"].string_value() 67 68 var duplicate = False 69 for prior in seen: 70 if prior == case_id: 71 duplicate = True 72 if duplicate: 73 issues.append( 74 FixtureValidationIssue( 75 case_id=case_id, 76 rule="duplicate_case_id", 77 detail="case_id appears more than once", 78 ) 79 ) 80 seen.append(String(case_id)) 81 82 var relative_path = entry["path"].string_value() 83 var case_path = base / relative_path 84 if not exists(case_path): 85 issues.append( 86 FixtureValidationIssue( 87 case_id=case_id, 88 rule="dangling_path", 89 detail=relative_path, 90 ) 91 ) 92 continue 93 94 var doc = loads(case_path.read_text()) 95 if ( 96 not _has_key(doc, "case_id") 97 or doc["case_id"].string_value() != case_id 98 ): 99 issues.append( 100 FixtureValidationIssue( 101 case_id=case_id, 102 rule="case_id_mismatch", 103 detail="case_id does not match manifest", 104 ) 105 ) 106 if ( 107 not _has_key(doc, "requirements") 108 or len(doc["requirements"].array_items()) == 0 109 ): 110 issues.append( 111 FixtureValidationIssue( 112 case_id=case_id, 113 rule="empty_requirements", 114 detail="no requirement references", 115 ) 116 ) 117 if ( 118 not _has_key(doc, "required_from_step") 119 or doc["required_from_step"].string_value() == "" 120 ): 121 issues.append( 122 FixtureValidationIssue( 123 case_id=case_id, 124 rule="missing_activation_step", 125 detail="required_from_step is empty", 126 ) 127 ) 128 elif _has_key(entry, "required_from_step") and ( 129 doc["required_from_step"].string_value() 130 != entry["required_from_step"].string_value() 131 ): 132 issues.append( 133 FixtureValidationIssue( 134 case_id=case_id, 135 rule="activation_step_mismatch", 136 detail="required_from_step does not match manifest", 137 ) 138 ) 139 if not _has_key(doc, "then") or len(doc["then"].array_items()) == 0: 140 issues.append( 141 FixtureValidationIssue( 142 case_id=case_id, 143 rule="empty_expectations", 144 detail="no expected assertions", 145 ) 146 ) 147 else: 148 for assertion in doc["then"].array_items(): 149 if not _has_key(assertion, "operator"): 150 issues.append( 151 FixtureValidationIssue( 152 case_id=case_id, 153 rule="missing_operator", 154 detail="assertion has no operator", 155 ) 156 ) 157 continue 158 var operator = assertion["operator"].string_value() 159 if not _is_registered_operator(operator, registered): 160 issues.append( 161 FixtureValidationIssue( 162 case_id=case_id, 163 rule="unknown_operator", 164 detail=operator, 165 ) 166 ) 167 if not _has_key(doc, "provenance"): 168 issues.append( 169 FixtureValidationIssue( 170 case_id=case_id, 171 rule="missing_provenance", 172 detail="provenance is required", 173 ) 174 ) 175 if ( 176 not _has_key(doc, "implementation_status") 177 or doc["implementation_status"].string_value() != "planned" 178 ): 179 issues.append( 180 FixtureValidationIssue( 181 case_id=case_id, 182 rule="unexpected_status", 183 detail="new corpus cases must be planned until activated", 184 ) 185 ) 186 187 if _has_key(manifest, "raw_files"): 188 for raw in manifest["raw_files"].array_items(): 189 if not exists(base / raw.string_value()): 190 issues.append( 191 FixtureValidationIssue( 192 case_id="", 193 rule="dangling_raw", 194 detail=raw.string_value(), 195 ) 196 ) 197 198 return issues^ 199 200 201 def validate_requirement_traceability( 202 registry_path: String, manifest_path: String, steps_path: String 203 ) raises -> List[FixtureValidationIssue]: 204 var issues = List[FixtureValidationIssue]() 205 var registry = loads(Path(registry_path).read_text()) 206 var manifest = loads(Path(manifest_path).read_text()) 207 var steps = loads(Path(steps_path).read_text()) 208 209 var step_ids = List[String]() 210 for step in steps["steps"].array_items(): 211 step_ids.append(step["id"].string_value()) 212 213 var case_ids = List[String]() 214 for case_entry in manifest["cases"].array_items(): 215 case_ids.append(case_entry["case_id"].string_value()) 216 217 var requirement_ids = List[String]() 218 var requirements = registry["requirements"].array_items() 219 if len(requirements) == 0: 220 issues.append( 221 FixtureValidationIssue( 222 case_id="", rule="empty_registry", detail="no requirements" 223 ) 224 ) 225 for requirement in requirements: 226 var rid = requirement["id"].string_value() 227 var duplicate = False 228 for prior in requirement_ids: 229 if prior == rid: 230 duplicate = True 231 if duplicate: 232 issues.append( 233 FixtureValidationIssue( 234 case_id=rid, 235 rule="duplicate_requirement", 236 detail="duplicate requirement id", 237 ) 238 ) 239 requirement_ids.append(String(rid)) 240 241 var method = requirement["verification_method"].string_value() 242 if method == "": 243 issues.append( 244 FixtureValidationIssue( 245 case_id=rid, 246 rule="missing_verification_method", 247 detail="verification_method is empty", 248 ) 249 ) 250 var impl_steps = requirement["implementation_steps"].array_items() 251 if len(impl_steps) == 0: 252 issues.append( 253 FixtureValidationIssue( 254 case_id=rid, 255 rule="uncovered_requirement", 256 detail="no implementation steps", 257 ) 258 ) 259 else: 260 for step in impl_steps: 261 var sid = step.string_value() 262 var found = False 263 for known in step_ids: 264 if known == sid: 265 found = True 266 if not found: 267 issues.append( 268 FixtureValidationIssue( 269 case_id=rid, 270 rule="unknown_step", 271 detail=sid, 272 ) 273 ) 274 var fixtures = requirement["fixture_ids"].array_items() 275 for fixture in fixtures: 276 var fid = fixture.string_value() 277 var found = False 278 for known in case_ids: 279 if known == fid: 280 found = True 281 if not found: 282 issues.append( 283 FixtureValidationIssue( 284 case_id=rid, 285 rule="dangling_fixture", 286 detail=fid, 287 ) 288 ) 289 290 var step_seen = List[String]() 291 for step in steps["steps"].array_items(): 292 var sid = step["id"].string_value() 293 var duplicate = False 294 for prior in step_seen: 295 if prior == sid: 296 duplicate = True 297 if duplicate: 298 issues.append( 299 FixtureValidationIssue( 300 case_id=sid, 301 rule="duplicate_step", 302 detail="duplicate step id", 303 ) 304 ) 305 step_seen.append(String(sid)) 306 for dependency in step["dependencies"].array_items(): 307 var did = dependency.string_value() 308 var found = False 309 for known in step_ids: 310 if known == did: 311 found = True 312 if not found: 313 issues.append( 314 FixtureValidationIssue( 315 case_id=sid, 316 rule="dangling_dependency", 317 detail=did, 318 ) 319 ) 320 321 return issues^ 322 323 324 def validate_step_states(path: String) raises -> List[FixtureValidationIssue]: 325 var issues = List[FixtureValidationIssue]() 326 var doc = loads(Path(path).read_text()) 327 if not _has_key(doc, "states"): 328 issues.append( 329 FixtureValidationIssue( 330 case_id="", rule="missing_states", detail="no states declared" 331 ) 332 ) 333 return issues^ 334 335 var states = List[String]() 336 for entry in doc["states"].array_items(): 337 var state = entry.string_value() 338 var duplicate = False 339 for prior in states: 340 if prior == state: 341 duplicate = True 342 if duplicate: 343 issues.append( 344 FixtureValidationIssue( 345 case_id="", rule="duplicate_state", detail=state 346 ) 347 ) 348 states.append(String(state)) 349 350 for required in ["PASSED", "NOT_RUN", "NOT_APPLICABLE"]: 351 var found = False 352 for state in states: 353 if state == required: 354 found = True 355 if not found: 356 issues.append( 357 FixtureValidationIssue( 358 case_id="", rule="missing_state", detail=required 359 ) 360 ) 361 362 if ( 363 not _has_key(doc, "passed_requires_executed_evidence") 364 or not doc["passed_requires_executed_evidence"].bool_value() 365 ): 366 issues.append( 367 FixtureValidationIssue( 368 case_id="", 369 rule="passed_without_evidence", 370 detail="PASSED must require executed evidence", 371 ) 372 ) 373 374 if not _has_key(doc, "rules") or len(doc["rules"].array_items()) == 0: 375 issues.append( 376 FixtureValidationIssue( 377 case_id="", 378 rule="missing_state_rules", 379 detail="state rules must be documented", 380 ) 381 ) 382 383 return issues^