Files
FreeCADWasmDev/tools/test_delivery_planning.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

235 lines
15 KiB
Python

import contextlib
import io
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import migration_workflows as workflows
import prepare_business_acceptance_batch as acceptance
import prepare_feature_capability_batch as features
import generate_feature_capability_registry as registry
import prepare_implementation_business_batch as planner
import prepare_module_implementation_packet as packets
import run_freecad_web_scheduler as scheduler
from report_migration_delivery import report
class DeliveryTests(unittest.TestCase):
def test_native_marker_recognizes_fully_qualified_desktop_api_id(self):
source = '''
// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Assembly_CreateJointAngle")
if (apiId == QStringLiteral("Assembly:desktop:Assembly_CreateJointAngle")) {
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
return true;
}
'''
self.assertIn("Assembly_CreateJointAngle", registry.implemented_desktop_commands(source))
def test_native_marker_recognizes_dispatcher_boolean_command_alias(self):
source = '''
// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Assembly_SelectComponentsWithDoFs")
const bool dofs = apiId == QStringLiteral("Assembly:desktop:Assembly_SelectComponentsWithDoFs");
if (dofs) { result.insert(QStringLiteral("status"), QStringLiteral("exposed")); }
'''
self.assertIn("Assembly_SelectComponentsWithDoFs", registry.implemented_desktop_commands(source))
def test_native_marker_recognizes_name_value_alias_with_json_status(self):
source = r'''
// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("BIM_Nudge_Down")
const bool nudge = familyValue == "desktop-command" &&
(nameValue == "BIM_Nudge_Down" || nameValue == "BIM_Nudge_Left");
if (nudge) { recordsJson += "\"status\":\"exposed\""; }
'''
self.assertIn("BIM_Nudge_Down", registry.implemented_desktop_commands(source))
self.assertNotIn("BIM_Nudge_Right", registry.implemented_desktop_commands(source))
def test_native_marker_recognizes_shared_draft_snap_branch(self):
source = r'''
// FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Draft_Snap_Grid")
const int snapIndex = name.startsWith(QStringLiteral("Draft_Snap_")) ? 1 : -1;
if (family == QStringLiteral("desktop-command") && snapIndex >= 0) {
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
}
'''
self.assertIn("Draft_Snap_Grid", registry.implemented_desktop_commands(source))
def test_same_module_cases_for_different_native_owners_are_not_combined(self):
rows = [{"id": "Part:create", "module": "Part", "command": "create"},
{"id": "Part:cylinder", "module": "Part", "command": "cylinder"},
{"id": "Part:sphere", "module": "Part", "command": "sphere"}]
document = {"tree": "build", "target": "FreeCADWebDocumentProbe"}
state = {"pending": rows, "rows": rows, "accepted": set(), "cooling": set(), "receipts": {},
"covered": {"Part": {row["id"] for row in rows}},
"buildOverrides": {"Part": {"Part:cylinder": document, "Part:sphere": document}}}
groups = acceptance.candidates(state)
self.assertEqual([len(group["rows"]) for group in groups], [1, 2])
self.assertEqual(len(state["pending"]), 3)
contract = {"build": {"target": "PartBoxProbe"}, "buildOverrides": state["buildOverrides"]["Part"]}
self.assertEqual(packets.acceptance_build_for_ids(contract, {"Part:cylinder", "Part:sphere"}), (document, True))
self.assertEqual(packets.acceptance_build_for_ids(contract, {"Part:create"}), (contract["build"], False))
self.assertEqual(packets.acceptance_build_for_ids(contract, set()), (contract["build"], False))
def state(self):
rows = [{"id": key, "module": "Web", "command": key} for key, family in workflows.PRODUCT_WORKFLOWS.items() if family == "product-workspace"]
rows += [{"id": "Part:save", "module": "Part", "command": "part.saveReopen", "category": "persistence"},
{"id": "TechDraw:desktop:View", "module": "TechDraw", "command": "View", "kind": "desktop-gui"},
{"id": "Import:inspect", "module": "Import", "command": "import.inspectSourceInventory", "category": "format"}]
return {"rows": rows, "pending": rows, "accepted": set(), "receipts": {}, "cooling": set(),
"covered": {row["module"]: set() for row in rows}}
def test_core_workspace_precedes_desktop_and_support_routes_without_dropping_scope(self):
state = self.state()
batch = acceptance.plan(state, delivery_only=True)
self.assertEqual(batch["selected"]["module"], "Web")
self.assertEqual(batch["selected"]["recordCount"], 4)
self.assertEqual(batch["businessWorkflow"], "product-workspace")
self.assertEqual(batch["records"][0][0], "product:react-primary-shell")
state["cooling"] = {row[0] for row in batch["records"]}
batch = acceptance.plan(state, delivery_only=True)
self.assertEqual([row[0] for row in batch["records"]], ["Part:save"])
self.assertEqual(len(state["pending"]), 7)
self.assertFalse(workflows.delivery_route(state["rows"][-1]))
def test_save_and_format_families_are_distinct_and_aliases_share_one_family(self):
self.assertEqual(workflows.workflow_family({"id": "Part:save", "category": "persistence"}), "document-persistence")
self.assertEqual(workflows.workflow_family({"id": "business-format:Import:import:.step"}),
workflows.workflow_family({"id": "business-format:Import:export:.stp"}))
def test_business_and_desktop_feature_families_never_mix(self):
groups = [(('Part', 'module-business-document-persistence'), [{"id": 'save'}]),
(('Part', 'desktop-command-command'), [{"id": 'gui'}])]
_, family, rows, _ = features.select_group_batch(groups)
self.assertEqual(family, 'module-business-document-persistence')
self.assertEqual([row['id'] for row in rows], ['save'])
_, _, rows, _ = features.select_group_batch(list(reversed(groups)))
self.assertEqual([row['id'] for row in rows], ['gui'])
def test_delivery_and_runtime_filters_preserve_original_inventory(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
paths = {key: root / f'{key}.json' for key in ('REGISTRY', 'ISSUES', 'STATE', 'OUTPUT', 'PIPELINE', 'PROGRESS')}
rows = [{"id": "Sketcher:create", "module": "Sketcher", "command": "sketcher.createRecompute", "category": "modeling"},
{"id": "TechDraw:desktop:View", "module": "TechDraw", "command": "View", "kind": "desktop-gui"}]
paths['REGISTRY'].write_text(json.dumps({'records': rows}))
paths['ISSUES'].write_text(json.dumps({'issues': [{'id': 'bug', 'capabilityId': rows[1]['id'], 'status': 'open'}]}))
paths['STATE'].write_text('{"active_task":"task"}')
original = paths['REGISTRY'].read_bytes()
with patch.multiple(features, **paths), patch.object(features, 'capability_source_fingerprint', return_value='stable'), \
patch.object(features, 'implementation_ready_ids', return_value=set()), patch.object(features, 'cooling_ids', return_value=set()), \
contextlib.redirect_stdout(io.StringIO()):
features.main(['--delivery-only'])
batch = json.loads(paths['OUTPUT'].read_text())
self.assertEqual([row[0] for row in batch['records']], ['Sketcher:create'])
self.assertTrue(batch['selected']['family'].startswith('module-business-'))
features.main(['--runtime-only'])
self.assertEqual(json.loads(paths['OUTPUT'].read_text())['selected']['family'], 'runtime-issue')
self.assertEqual(paths['REGISTRY'].read_bytes(), original)
def test_delivery_handoff_is_not_interrupted_by_unrelated_historical_review(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / 'batch.json'
output.write_text('{"selected":{"module":"TechDraw","tier":"feature-workflow"}}')
calls = []
def run(command, **kwargs):
calls.append(command)
chosen = command[1] == 'tools/prepare_business_acceptance_batch.py' and '--delivery-only' in command
output.write_text(json.dumps({'status': 'ready', 'selectionScope': 'delivery', 'selected': {'module': 'Web'}} if chosen else {'status': 'no_ready_batch'}))
return type('Result', (), {'returncode': 0, 'stdout': '', 'stderr': ''})()
with patch.object(planner, 'BATCH', output), patch.object(planner, 'feature_phase_remaining', return_value=600), \
patch.object(planner.subprocess, 'run', side_effect=run), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(planner.main(), 0)
self.assertFalse(any('tools/prepare_failure_reconciliation_batch.py' in call for call in calls))
phases = planner.phase_order({}, 600)
self.assertTrue(any(command == ['tools/prepare_feature_capability_batch.py'] for _, command in phases))
self.assertTrue(any(command == ['tools/prepare_business_acceptance_batch.py'] for _, command in phases))
def test_remaining_module_handoff_preserves_business_batch(self):
for target in ("tools/prepare_feature_capability_batch.py", "tools/prepare_business_acceptance_batch.py"):
with self.subTest(target=target), tempfile.TemporaryDirectory() as directory:
output = Path(directory) / 'batch.json'
output.write_text('{"selected":{"module":"Assembly","tier":"feature-workflow"}}')
calls = []
selected = {"status": "ready", "batchId": "business-next", "selected": {"module": "Draft"}}
def run(command, **kwargs):
calls.append(command)
chosen = command[1:] == [target]
review = "tools/prepare_failure_reconciliation_batch.py" in command
output.write_text(json.dumps(selected if chosen else {"status": "ready", "batchId": "historical-review"}
if review else {"status": "no_ready_batch"}))
return type('Result', (), {'returncode': 0, 'stdout': '', 'stderr': ''})()
with patch.object(planner, 'BATCH', output), patch.object(planner, 'feature_phase_remaining', return_value=600), \
patch.object(planner.subprocess, 'run', side_effect=run), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(planner.main(), 0)
self.assertEqual(json.loads(output.read_text())['batchId'], 'business-next')
self.assertFalse(any('tools/prepare_failure_reconciliation_batch.py' in call for call in calls))
def test_historical_review_remains_available_after_business_phases(self):
phases = planner.phase_order({}, 600)
self.assertEqual(phases[-1][1], ['tools/prepare_failure_reconciliation_batch.py'])
self.assertIn(('P0-observed-runtime-repair', ['tools/prepare_feature_capability_batch.py', '--runtime-only']), phases)
def test_persistence_uses_its_existing_document_worker_source(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
probes = root / 'probes'
probes.mkdir()
(probes / 'DocumentProbe.cpp').write_text('document source')
(probes / 'CMakeLists.txt').write_text('add_executable(FreeCADWebDocumentProbe DocumentProbe.cpp)')
manifest = root / 'web/public/modules/manifest.json'
manifest.parent.mkdir(parents=True)
manifest.write_text(json.dumps({'modules': {'Part': {'workers': {'part-save.worker.ts': {'runtimeUrl': '/kernel/FreeCADWebDocumentProbe.js'}}}}}))
route = root / 'web/src/module-registry.generated.ts'
route.parent.mkdir(parents=True)
route.write_text('case "Part:part.saveReopen": return "part-save.worker.ts";')
row = {'id': 'Part:save', 'command': 'part.saveReopen', 'category': 'persistence', 'worker': 'part.worker.ts'}
with patch.multiple(packets, ROOT=root, PROBE_ROOT=probes):
self.assertEqual(packets.workflow_worker_sources('Part', ['Part:save'], {'Part:save': row}), [probes / 'DocumentProbe.cpp'])
row['kind'] = 'desktop-gui'
self.assertEqual(packets.workflow_worker_sources('Part', ['Part:save'], {'Part:save': row}), [])
def test_report_keeps_full_scope_and_does_not_count_receipts_as_acceptance(self):
state = self.state()
state['receipts']['Part:save'] = {'status': 'implementation_ready'}
result = report(state)
self.assertEqual(result['fullScope'], 7)
self.assertEqual(result['deliverySubset'], 5)
self.assertEqual(result['fullAccepted'], 0)
self.assertEqual(sum(row['reusableReceipts'] for row in result['groups']), 1)
self.assertEqual(len(state['pending']), 7)
def test_product_acceptance_packet_targets_real_page_not_web_server_probe(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
current = root / 'docs/current'
current.mkdir(parents=True)
source = root / 'web/src/main.ts'
source.parent.mkdir(parents=True)
source.write_text('mountWorkspace();\n')
(root / 'web/index.html').write_text('<script src="/src/main.ts"></script>\n')
(source.parent / 'web-facade.ts').write_text('unrelatedServerFixture();\n')
(current / 'feature-capability-registry.json').write_text('{"records": []}')
(current / 'runtime-issue-ledger.json').write_text('{"issues": []}')
batch_path = current / 'batch.json'
output_path = current / 'packet.json'
batch_path.write_text(json.dumps({'status': 'ready', 'batchId': 'batch', 'selected': {
'module': 'Web', 'tier': 'business-acceptance', 'family': 'business-acceptance'},
'records': [['product:react-primary-shell', 'React shell', 'acceptance_required', 'web/src/main.ts', 1]]}))
with patch.multiple(packets, ROOT=root, BATCH_PATH=batch_path, OUTPUT_PATH=output_path, PROBE_ROOT=root / 'probes'), \
patch.object(packets, 'probe_sources', return_value=[]), patch.object(packets, 'reconcile_failures', return_value={}), \
patch.object(packets, 'module_context', return_value={}), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(packets.main(), 0)
packet = json.loads(output_path.read_text())
self.assertEqual(packet['build']['kind'], 'frontend')
paths = {row['path'] for row in packet['editableExcerpts']}
self.assertIn('web/src/main.ts', paths)
self.assertIn('web/index.html', paths)
self.assertNotIn('web/src/web-facade.ts', paths)
if __name__ == '__main__':
unittest.main()