import json import os from pathlib import Path import subprocess import sys import tempfile import time import unittest from unittest.mock import patch import run_module_batch_pipeline as pipeline from prepare_module_implementation_packet import acceptance_build_for_ids, frontend_acceptance_build class BusinessTimeoutTests(unittest.TestCase): def test_mixed_product_and_desktop_web_batch_retains_its_declared_frontend_owner(self): contract = {"build": {"kind": "frontend", "tree": "web", "target": "FreeCADWebReactProduct"}} declared, explicit = acceptance_build_for_ids(contract, {"product:file-workflows", "Web:desktop:Std_Open"}) self.assertTrue(explicit) self.assertEqual(frontend_acceptance_build(declared)["runtime"], "web/dist/index.html") with self.assertRaises(ValueError): frontend_acceptance_build({**declared, "tree": "../outside"}) def test_outer_timeout_includes_declared_case_and_selected_verifiers(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) contracts = root / "docs/business-acceptance" contracts.mkdir(parents=True) (contracts / "Web.json").write_text(json.dumps({"timeoutSeconds": 840})) rows = [{"id": "web"}, {"id": "ifc", "independentVerification": ["node", "verify.mjs"]}] with patch.object(pipeline, "ROOT", root), patch("business_acceptance.requirements", return_value=rows): self.assertEqual(pipeline.business_timeout({"selected": {"module": "Web"}, "records": [["web"]]}), 870) self.assertEqual(pipeline.business_timeout({"selected": {"module": "BIM"}, "records": [["ifc"]]}), 390) (contracts / "Web.json").write_text(json.dumps({"timeoutSeconds": True})) self.assertEqual(pipeline.business_timeout({"selected": {"module": "Web"}, "records": [["web"]]}), 210) def test_outer_timeout_cleans_real_runner_case_and_grandchild(self): tools = Path(__file__).resolve().parent with tempfile.TemporaryDirectory() as directory: root = Path(directory) current = root / "docs/current" current.mkdir(parents=True) contracts = root / "docs/business-acceptance" contracts.mkdir() (current / "feature-capability-registry.json").write_text(json.dumps({"records": [ {"id": "Web:timeout", "module": "Web", "command": "timeout"}]})) (contracts / "Web.json").write_text(json.dumps({"command": ["python3", "case.py"], "timeoutSeconds": 30})) child = "import time; from pathlib import Path; time.sleep(3); Path('orphan').write_text('bad')" (root / "case.py").write_text("import subprocess,sys,time\nsubprocess.Popen([sys.executable,'-c'," + repr(child) + "])\nfrom pathlib import Path\nPath('started').touch()\nprint('case started',flush=True)\ntime.sleep(30)\n") wrapper = (f"import sys,runpy; from pathlib import Path; sys.path.insert(0,{str(tools)!r}); " f"import business_acceptance as b; b.ROOT=Path({str(root)!r}); b.LEDGER=b.ROOT/'docs/current/business-acceptance.json'; " f"sys.argv=['runner','--module','Web','--ids','Web:timeout']; runpy.run_path({str(tools / 'run_business_acceptance.py')!r},run_name='__main__')") with self.assertRaises(subprocess.TimeoutExpired): pipeline.run_business_process([sys.executable, "-c", wrapper], root, dict(os.environ), 1) self.assertTrue((root / "started").exists()) time.sleep(2.5) self.assertFalse((root / "orphan").exists()) def test_timeout_retains_wrapper_diagnostics(self): with self.assertRaises(subprocess.TimeoutExpired) as caught: pipeline.run_business_process([sys.executable, "-c", "import sys,time; print('outer-start',flush=True); print('error-detail',file=sys.stderr,flush=True); time.sleep(30)"], Path.cwd(), dict(os.environ), 0.2) self.assertIn("outer-start", caught.exception.output) self.assertIn("error-detail", caught.exception.stderr) if __name__ == "__main__": unittest.main()