Files
FreeCADWasmDev/tools/test_continuous_acceptance.py

177 lines
12 KiB
Python

import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import business_acceptance as evidence
import prepare_business_acceptance_batch as batches
import prepare_implementation_business_batch as planner
import run_freecad_web_scheduler as scheduler
import implementation_first as implementation_policy
class ContinuousAcceptanceTests(unittest.TestCase):
def test_gap_priority_excludes_repeat_validation_without_changing_scope(self):
policy = {"continuousDevelopment": True, "completionFocus": "business-acceptance", "prioritizeCoverageGaps": True}
with patch.object(planner, "load", return_value=policy):
commands = [command for _, command in planner.phase_order({}, 0)]
self.assertEqual(commands[0], ["tools/prepare_business_acceptance_batch.py", "--uncovered-only"])
self.assertNotIn(["tools/prepare_business_acceptance_batch.py", "--priority-only"], commands)
self.assertNotIn(["tools/prepare_failure_reconciliation_batch.py"], commands)
rows = [{"id": name, "module": "Part", "command": name} for name in ("old", "gap", "new-case")]
state = {"pending": rows, "covered": {"Part": {"old", "new-case"}}, "cooling": set(), "receipts": {},
"deferredValidation": {"old"}}
selected = lambda **kwargs: {row["id"] for group in batches.candidates(state, **kwargs) for row in group["rows"]}
self.assertEqual(selected(uncovered_only=True), {"gap"})
self.assertEqual(selected(covered_only=True), {"new-case"})
self.assertEqual(state["pending"], rows)
def test_previous_pass_remains_deferred_after_failure_or_fingerprint_change(self):
records = {"old": {"status": "failed", "evidencePath": "old-pass.json", "fingerprint": "stale"},
"new": {"status": "failed", "lastFailureEvidencePath": "first-failure.json"}}
def read(path):
return {"deferPreviouslyPassedValidation": True} if path.name == "execution-policy.json" else {"records": records}
with patch.object(implementation_policy, "_read_json", side_effect=read):
self.assertEqual(implementation_policy.deferred_validation_ids(), {"old"})
with patch.object(implementation_policy, "_read_json", return_value={}):
self.assertEqual(implementation_policy.deferred_validation_ids(), set())
def test_only_selected_semantic_batch_requests_full_ifc_reverification(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
batch, pipeline, packet = (root / name for name in ("batch.json", "pipeline.json", "packet.json"))
with patch.object(scheduler, "continuous_acceptance_enabled", return_value=True), \
patch.object(scheduler, "MODULE_BATCH_PATH", batch), \
patch.object(scheduler, "MODULE_PIPELINE_PATH", pipeline), \
patch.object(scheduler, "IMPLEMENTATION_PACKET_PATH", packet), \
patch.object(evidence, "requirements", return_value=[]):
for selected_id, semantic in [("Part:desktop:Part_CheckGeometry", False),
("business-format:BIM:semantic-export:.ifc", True)]:
batch.write_text(json.dumps({"batchId": "current", "records": [[selected_id]]}))
prompt = scheduler._build_prompt_unbounded("task", failures=[])
self.assertEqual("Read the full semantic contract" in prompt, semantic)
self.assertEqual("The independent verifier runs separately" in prompt, semantic)
self.assertEqual("Reuse current IFC evidence" in prompt, not semantic)
self.assertIn("Do not modify the semantic contract", prompt)
self.assertIn("Keep all original requirements", prompt)
def test_two_refreshes_reserve_uncovered_work_without_preempting_runtime_repairs(self):
refresh = {"tier": "business-acceptance", "businessLane": "acceptance", "outcome": "progress", "firstBusinessPassIds": []}
history = [refresh, {"outcome": "provider_wait"}, refresh]
self.assertTrue(planner.continuous_coverage_due(history))
self.assertFalse(planner.continuous_coverage_due(history + [{"businessLane": "development"}]))
self.assertFalse(planner.continuous_coverage_due(history + [{"firstBusinessPassIds": ["new"]}]))
def load(path):
return {"continuousDevelopment": True, "completionFocus": "business-acceptance"} if path.name == "execution-policy.json" else {"history": history}
with patch.object(planner, "load", side_effect=load):
phases = [command for _, command in planner.phase_order({}, 0)]
self.assertEqual(phases[0][-1], "--priority-only")
self.assertEqual(phases[1][-1], "--runtime-only")
self.assertLess(phases.index(["tools/prepare_business_acceptance_batch.py", "--uncovered-only"]),
phases.index(["tools/prepare_business_acceptance_batch.py", "--covered-only"]))
def test_scope_cannot_shrink_or_drop_the_semantic_requirement(self):
def read(path):
if path.name == "feature-capability-registry.json":
return {"records": [{"id": "base", "module": "BIM"}]}
if path.name == "execution-policy.json":
return {"minimumOriginalRequirements": 2}
return {}
with patch.object(evidence, "read", side_effect=read):
with self.assertRaisesRegex(ValueError, "scope was reduced"):
evidence.requirements()
def missing_supplement(path):
if path.name == "execution-policy.json":
return {"requiredSupplementalIds": ["semantic"]}
return {}
with patch.object(evidence, "read", side_effect=missing_supplement):
with self.assertRaisesRegex(ValueError, "supplemental"):
evidence.requirements()
def test_partial_semantic_success_does_not_close_full_contract(self):
result = {"capabilityId": "semantic", "status": "passed", "cases": [
{"input": {"class": "IfcWall"}, "actual": {"class": "IfcWall"}, "covers": ["classes"],
"assertions": [{"name": "class preserved", "passed": True}]}]}
supplemental = {"records": [{"id": "semantic", "requiredProofAreas": ["classes", "materials"]}]}
with patch.object(evidence, "read", return_value=supplemental):
self.assertFalse(evidence.successful_result(result))
self.assertEqual(evidence.missing_proof_areas(result), ["materials"])
result["cases"].append({"input": {"material": "Concrete"}, "covers": ["materials"],
"assertions": [{"name": "material association", "passed": True}]})
self.assertFalse(evidence.successful_result(result))
result["cases"][-1]["actual"] = {"material": "Concrete", "relation": "IfcRelAssociatesMaterial"}
self.assertTrue(evidence.successful_result(result))
def test_semantic_requirement_keeps_original_scope_and_cannot_inherit_geometry_pass(self):
original = {"id": "business-format:BIM:export:.ifc", "module": "BIM", "command": "export IFC"}
semantic = {"id": "business-format:BIM:semantic-export:.ifc", "module": "BIM",
"command": "bim.formatWorkflow", "sourcePath": "native.cpp", "acceptanceRequired": True}
def read(path):
if path.name == "feature-capability-registry.json":
return {"records": [original]}
if path.name == "business-acceptance-supplement.json":
return {"records": [semantic]}
return {}
with patch.object(evidence, "read", side_effect=read):
rows = evidence.requirements()
self.assertEqual(rows, [original, semantic])
with patch.object(batches, "requirements", return_value=rows), \
patch.object(batches, "deferred_validation_ids", return_value=set()), \
patch.object(batches, "accepted_ids", return_value={original["id"]}), \
patch.object(batches, "load_registry", return_value={"records": {}}), \
patch.object(batches, "contract_ids", return_value=set()), \
patch.object(batches, "cooling_ids", return_value=set()):
state = batches.inventory()
self.assertEqual([row["id"] for row in state["pending"]], [semantic["id"]])
self.assertFalse(batches.plan(state)["acceptanceReady"])
def test_contract_changes_invalidate_semantic_fingerprint(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
doc = root / "contract.md"
doc.write_text("Preserve classes")
row = {"id": "semantic", "module": "BIM", "contractDocument": "contract.md", "requiredBehavior": ["classes"]}
with patch.object(evidence, "ROOT", root), patch.object(evidence, "module_inputs", return_value={}):
before = evidence.fingerprint(row)
doc.write_text("Preserve classes and material relationships")
evidence.digest.cache_clear()
self.assertNotEqual(before, evidence.fingerprint(row))
before = evidence.fingerprint(row)
self.assertNotEqual(before, evidence.fingerprint({**row, "requiredBehavior": ["classes", "materials"]}))
evidence.digest.cache_clear()
def test_priority_then_uncovered_work_does_not_stop_at_route_receipts(self):
with patch.object(planner, "load", return_value={"continuousDevelopment": True, "completionFocus": "business-acceptance"}):
phases = planner.phase_order({}, 0)
self.assertEqual(phases[0][1], ["tools/prepare_business_acceptance_batch.py", "--priority-only"])
self.assertIn(["tools/prepare_business_acceptance_batch.py", "--uncovered-only"], [item[1] for item in phases])
def test_authorized_profile_allows_focused_tests_but_not_legacy_full_gates(self):
with patch.object(scheduler, "continuous_acceptance_enabled", return_value=True):
for command in ("python3 tools/run_business_acceptance.py --module BIM --reuse",
"cmake --build freecad-source/build-web/configure --target FreeCADWebBimRuntimeProbe -j2",
"npm --prefix web run build"):
self.assertFalse(scheduler.forbidden_implementation_command(command, "build"))
self.assertTrue(scheduler.forbidden_implementation_command("python3 tools/validate_web_context.py", "validation"))
def test_prompt_rejects_a_foreign_batch_packet(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
batch, pipeline, packet = (root / name for name in ("batch.json", "pipeline.json", "packet.json"))
batch.write_text(json.dumps({"batchId": "current", "selected": {"module": "Part"}, "records": [["Part:one"]]}))
pipeline.write_text(json.dumps({"batchId": "foreign", "steps": [{"exitCode": 1, "stderr": "FOREIGN ERROR"}]}))
packet.write_text(json.dumps({"batchId": "foreign", "build": {"target": "FOREIGN TARGET"}, "editableExcerpts": ["FOREIGN SOURCE"]}))
with patch.object(scheduler, "continuous_acceptance_enabled", return_value=True), \
patch.object(scheduler, "MODULE_BATCH_PATH", batch), \
patch.object(scheduler, "MODULE_PIPELINE_PATH", pipeline), \
patch.object(scheduler, "IMPLEMENTATION_PACKET_PATH", packet), \
patch.object(evidence, "requirements", return_value=[]):
prompt = scheduler._build_prompt_unbounded("task", failures=[])
self.assertNotIn("FOREIGN", prompt)
self.assertIn("full IFC semantic export", prompt)
if __name__ == "__main__":
unittest.main()