Files
FreeCADWasmDev/tools/test_business_migration.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

133 lines
8.8 KiB
Python

"""Regressions for omitted business commands and false completion claims."""
import json
import contextlib
import io
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import business_acceptance as acceptance
import run_business_acceptance as runner
import generate_web_business_completion_audit as audit
from tools.generate_web_workbench_inventory import python_gui_commands, source_gui_inventory
class InventoryTests(unittest.TestCase):
def test_module_root_registration_and_named_menu_lists(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
module = root / "src/Mod/Assembly"
module.mkdir(parents=True)
(module / "CommandJoint.py").write_text('Gui.addCommand("Assembly_CreateJointRevolute", Command())\n')
init = module / "InitGui.py"
init.write_text('commands = ["Assembly_CreateAssembly"]\nextra = ["Assembly_SolveAssembly"]\nself.appendMenu("Assembly", commands + extra)\n')
inventory = source_gui_inventory(root, module)
self.assertEqual(set(inventory["commands"]), {"Assembly_CreateJointRevolute", "Assembly_CreateAssembly", "Assembly_SolveAssembly"})
def test_cyclic_alias_does_not_hang_or_invent_command(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "InitGui.py"
path.write_text('first = second\nsecond = first\nself.appendToolbar("Label", first)\n')
self.assertEqual(python_gui_commands(path), [])
class AcceptanceTests(unittest.TestCase):
def tearDown(self):
acceptance.digest.cache_clear()
acceptance.module_inputs.cache_clear()
def test_empty_or_failed_assertions_cannot_complete_business(self):
self.assertFalse(acceptance.successful_result({"status": "passed", "cases": []}))
self.assertFalse(acceptance.successful_result({"status": "passed", "cases": [{"input": {}, "assertions": [{"name": "volume", "passed": False}]}]}))
self.assertFalse(acceptance.successful_result({"status": "passed", "cases": [{"assertions": [{"name": "volume", "passed": True}]}]}))
def test_runtime_issue_closure_requires_declared_success_and_unchanged_observation(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
path = root / "docs/current/runtime-issue-ledger.json"
path.parent.mkdir(parents=True)
evidence = root / "evidence.json"
evidence.write_text("verified results")
issue = {"id": "runtime:one", "capabilityId": "TechDraw:create", "status": "fix_deployed_pending_use", "occurrences": 1, "args": {"length": 10}}
result = {"capabilityId": issue["capabilityId"], "status": "passed", "cases": [{"input": issue["args"], "assertions": [{"name": "native length", "passed": True}]}]}
report = {"executedAt": "2026-09-08T00:00:00Z", "results": [result]}
before = {issue["id"]: runner.observation(issue)}
passed = {issue["capabilityId"]}
with patch.object(runner, "ROOT", root):
path.write_text(json.dumps({"issues": [issue]}))
self.assertEqual(runner.close_tested_issues(report, evidence, before, passed), 0)
result["resolvedIssueIds"] = [issue["id"]]
self.assertEqual(runner.close_tested_issues(report, evidence, before, set()), 0)
newer = {**issue, "occurrences": 2, "lastSeenAt": "later"}
path.write_text(json.dumps({"issues": [newer]}))
self.assertEqual(runner.close_tested_issues(report, evidence, before, passed), 0)
path.write_text(json.dumps({"issues": [issue]}))
self.assertEqual(runner.close_tested_issues(report, evidence, before, passed), 1)
closed = json.loads(path.read_text())["issues"][0]
self.assertEqual(closed["status"], "resolved")
self.assertEqual(closed["businessEvidenceSha256"], acceptance.digest(evidence))
self.assertEqual(closed["resolvedObservation"], before[issue["id"]])
def test_current_evidence_is_reused_but_changed_module_is_reopened(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "web/src/part-facade.ts"
source.parent.mkdir(parents=True)
source.write_text("version1")
evidence = root / "evidence.json"
result = {"capabilityId": "Part:part.create", "status": "passed", "cases": [{"input": {"length": 2}, "assertions": [{"name": "volume", "passed": True}]}]}
evidence.write_text(json.dumps({"schema": "freecad-web-business-cases/v1", "results": [result]}))
ledger = root / "ledger.json"
row = {"id": result["capabilityId"], "module": "Part"}
with patch.object(acceptance, "ROOT", root), patch.object(acceptance, "LEDGER", ledger):
ledger.write_text(json.dumps({"records": {row["id"]: {"status": "passed", "fingerprint": acceptance.fingerprint(row), "evidencePath": "evidence.json", "evidenceSha256": acceptance.digest(evidence)}}}))
self.assertEqual(acceptance.accepted_ids([row]), {row["id"]})
source.write_text("version2")
acceptance.digest.cache_clear()
acceptance.module_inputs.cache_clear()
self.assertEqual(acceptance.accepted_ids([row]), set())
def test_runner_requires_successful_execution_and_preserves_prior_evidence(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "docs/current").mkdir(parents=True)
(root / "docs/business-acceptance").mkdir()
script = root / "case.py"
row = {"id": "Part:part.create", "module": "Part", "implementationStatus": "routed"}
result = {"schema": "freecad-web-business-cases/v1", "results": [{"capabilityId": row["id"], "status": "passed", "cases": [{"input": {"length": 2}, "assertions": [{"name": "volume", "passed": True}]}]}]}
script.write_text(f"print({json.dumps(result)!r})\n")
(root / "docs/business-acceptance/Part.json").write_text(json.dumps({"command": ["python3", "case.py"]}))
(root / "docs/current/module-batch-manifest.json").write_text(json.dumps({"batchId": "batch", "selected": {"module": "Part"}, "records": [[row["id"]]]}))
ledger = root / "docs/current/business-acceptance.json"
with patch.object(acceptance, "ROOT", root), patch.object(acceptance, "LEDGER", ledger), patch.object(runner, "ROOT", root), patch.object(runner, "LEDGER", ledger), patch.object(runner, "requirements", return_value=[row]), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(runner.main(), 0)
self.assertEqual(acceptance.accepted_ids([row]), {row["id"]})
previous = json.loads(ledger.read_text())["records"][row["id"]]["evidencePath"]
script.write_text("raise SystemExit(1)\n")
self.assertEqual(runner.main(), 1)
self.assertTrue((root / previous).is_file())
self.assertEqual(json.loads(ledger.read_text())["records"][row["id"]]["status"], "failed")
self.assertEqual(acceptance.accepted_ids([row]), set())
def test_unrouted_business_remains_in_completion_denominator(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
sources = {key: root / f"{key}.json" for key in audit.SOURCES}
rows = [{"id": "Part:one", "module": "Part", "implementationStatus": "routed"},
{"id": "Part:two", "module": "Part", "implementationStatus": "implementation_required"}]
sources["features"].write_text(json.dumps({"records": rows}))
sources["receipts"].write_text(json.dumps({"records": {row["id"]: {"status": "implementation_ready"} for row in rows}}))
sources["modules"].write_text(json.dumps({"modules": {str(i): {} for i in range(32)}}))
with patch.object(audit, "ROOT", root), patch.object(audit, "SOURCES", sources), patch.object(audit, "OUTPUT", root / "out.json"), patch.object(audit, "MARKDOWN", root / "out.md"), patch.object(audit, "requirements", return_value=rows), patch.object(audit, "accepted_ids", return_value=set()), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(audit.main(), 0)
result = json.loads((root / "out.json").read_text())
self.assertEqual(result["implementationProgress"], {"completed": 1, "total": 2, "remaining": 1})
self.assertFalse(result["businessFunctionalComplete"])
if __name__ == "__main__":
unittest.main()