diff --git a/my-tools/authorize_next_wbv2_task.py b/my-tools/authorize_next_wbv2_task.py index 8450e044b..71200a08d 100644 --- a/my-tools/authorize_next_wbv2_task.py +++ b/my-tools/authorize_next_wbv2_task.py @@ -427,6 +427,22 @@ def ensure_native_geometry_fixture(batch: dict, contract: dict) -> None: raise SystemExit(f"native geometry fixture preparation failed for {batch['moduleId']}:\n{result.stdout}{result.stderr}") +def ensure_native_shader_fixture(batch: dict, contract: dict) -> None: + if not batch.get("nativeShaderFixture"): + return + digest = contract["batchId"].rsplit(":", 1)[-1] + fixture = Path(f"tests/files/web/generated/wbv2-shader-{digest}.blend") + batch["fixture"] = fixture.as_posix() + if (ROOT / fixture).is_file(): + return + command = ["python3", "my-tools/generate_wbv2_shader_fixture.py", "--output", fixture.as_posix()] + for unit in batch["units"]: + command.extend(["--node", unit["identifier"]]) + result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False) + if result.returncode: + raise SystemExit(f"native shader fixture preparation failed for {batch['moduleId']}:\n{result.stdout}{result.stderr}") + + def native_geometry_batch(inventory: dict) -> dict | None: candidates = [ unit for unit in inventory.get("units", []) @@ -461,16 +477,26 @@ def native_geometry_batch(inventory: dict) -> dict | None: } +def native_shader_batch(inventory: dict) -> dict | None: + candidates = [unit for unit in inventory.get("units", []) if isinstance(unit, dict) and unit.get("coverageStatus") != "VERIFIED" and unit.get("kind") == "node" and isinstance(unit.get("identifier"), str) and unit["identifier"].startswith("ShaderNode")] + if not candidates: + return None + units = candidates[:5] + return {"moduleId": "AUTO_SHADER_NATIVE_NODE_READER", "title": "verify auto-generated native shader node reader batch", "owner": "NODES", "identifiers": [u["identifier"] for u in units], "rna": [], "types": [u["identifier"] for u in units], "units": units, "candidateUnits": candidates, "nativeShaderFixture": True, "test": "web/tests/e2e/shader-main-reader-production.spec.ts", "production": "tests/golden/{task}/shader-main-reader-production.json", "adaptiveKey": "AUTO_SHADER_NATIVE_NODE_READER:LOCAL_EXACT:NODES", "adaptiveHistory": [], "adaptiveCurrentSize": len(units), "adaptiveNextSize": len(units), "adaptiveMinSize": 1, "adaptiveMaxSize": 5} + + def prepare_native_fixture(batch: dict, task: str, contract: dict) -> dict: """Generate a native fixture, shrinking only when Blender rejects a mix.""" - if not (batch.get("nativeCompositorFixture") or batch.get("nativeGeometryFixture")): + if not (batch.get("nativeCompositorFixture") or batch.get("nativeGeometryFixture") or batch.get("nativeShaderFixture")): return contract while True: try: if batch.get("nativeCompositorFixture"): ensure_native_compositor_fixture(batch, contract) - else: + elif batch.get("nativeGeometryFixture"): ensure_native_geometry_fixture(batch, contract) + else: + ensure_native_shader_fixture(batch, contract) return contract except SystemExit: units = list(batch.get("units", [])) @@ -495,15 +521,18 @@ def recover_blocked_native_batch(current: str, index: dict, plan: dict, inventor module = record.get("module") if isinstance(record, dict) else None receipt_path = ROOT / "tests/golden" / current / "module-receipt.json" receipt = read_json(receipt_path) if receipt_path.is_file() else {} - if not isinstance(module, dict) or not str(module.get("moduleId", "")).startswith("AUTO_COMPOSITOR_NATIVE_NODE_READER"): + if not isinstance(module, dict) or not str(module.get("moduleId", "")).startswith(("AUTO_COMPOSITOR_NATIVE_NODE_READER", "AUTO_SHADER_NATIVE_NODE_READER")): return False - if receipt.get("status") != "BLOCKED" or len(receipt.get("selectedRecordIds", [])) <= 5: + if receipt.get("status") != "BLOCKED": return False by_id = {unit.get("id"): unit for unit in inventory.get("units", []) if isinstance(unit, dict)} selected_ids = list(receipt.get("selectedRecordIds", []))[:5] units = [by_id[unit_id] for unit_id in selected_ids if unit_id in by_id] if len(units) != len(selected_ids): return False + is_shader = str(module["moduleId"]).startswith("AUTO_SHADER") + if not is_shader and len(receipt.get("selectedRecordIds", [])) <= 5: + return False batch = { "moduleId": str(module["moduleId"]), "title": "recover native compositor node reader batch", @@ -513,26 +542,31 @@ def recover_blocked_native_batch(current: str, index: dict, plan: dict, inventor "types": [unit["identifier"] for unit in units], "units": units, "nativeCompositorFixture": True, - "test": "web/tests/e2e/compositor-main-reader-production.spec.ts", - "production": "tests/golden/{task}/compositor-main-reader-production.json", + "test": "web/tests/e2e/shader-main-reader-production.spec.ts" if is_shader else "web/tests/e2e/compositor-main-reader-production.spec.ts", + "production": "tests/golden/{task}/shader-main-reader-production.json" if is_shader else "tests/golden/{task}/compositor-main-reader-production.json", + "nativeShaderFixture": is_shader, + "nativeCompositorFixture": not is_shader, } contract = batch_contract(current, batch) - ensure_native_compositor_fixture(batch, contract) + if is_shader: + ensure_native_shader_fixture(batch, contract) + else: + ensure_native_compositor_fixture(batch, contract) fixture = batch["fixture"] source_paths = [unit["locations"][0]["path"] for unit in units] source_paths += [ "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp", - "web/tests/e2e/compositor-main-reader-production.spec.ts", + batch["test"], fixture, "web/app/src/vendor/blender/web_engine.wasm", ] types = json.dumps(batch["types"], separators=(",", ":")) ids = json.dumps(selected_ids, separators=(",", ":")) - production = f"tests/golden/{current}/compositor-main-reader-production.json" + production = f"tests/golden/{current}/shader-main-reader-production.json" if is_shader else f"tests/golden/{current}/compositor-main-reader-production.json" command = ( f"WBV2_BATCH_TASK={current} WBV2_BATCH_PRODUCTION={production} " f"WBV2_BATCH_FIXTURE={fixture} WBV2_BATCH_TYPES='{types}' WBV2_BATCH_UNITS='{ids}' " - "WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/compositor-main-reader-production.spec.ts" + f"WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 {batch['test']}" ) module.update({ "batchId": contract["batchId"], @@ -843,6 +877,9 @@ def select_batch(inventory: dict) -> dict: geometry = native_geometry_batch(inventory) if geometry: return geometry + shader = native_shader_batch(inventory) + if shader: + return shader summary = pending_summary(inventory) write_planner_status({ "status": "waiting_for_exact_evidence", @@ -885,6 +922,11 @@ def main() -> int: batch["fixture"] = f"tests/files/web/generated/wbv2-geometry-{digest}.blend" if args.write: contract = prepare_native_fixture(batch, module_task, contract) + if batch.get("nativeShaderFixture"): + digest = contract["batchId"].rsplit(":", 1)[-1] + batch["fixture"] = f"tests/files/web/generated/wbv2-shader-{digest}.blend" + if args.write: + ensure_native_shader_fixture(batch, contract) batch["batchId"] = contract["batchId"] write_planner_status({ "status": "ready", diff --git a/my-tools/generate_wbv2_shader_fixture.py b/my-tools/generate_wbv2_shader_fixture.py new file mode 100644 index 000000000..08aee7702 --- /dev/null +++ b/my-tools/generate_wbv2_shader_fixture.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Generate a native Blender material node fixture for one WBV2 reader batch.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +NATIVE_BLENDER = ROOT / "build_blender_native/bin/blender" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True) + parser.add_argument("--node", action="append", required=True) + args = parser.parse_args() + output = Path(args.output) + if output.is_absolute() or ".." in output.parts or output.suffix != ".blend" or not str(output).startswith("tests/files/web/"): + parser.error("--output must be a repository-relative .blend path under tests/files/web/") + nodes = list(dict.fromkeys(args.node)) + if not nodes or any(not node.startswith("ShaderNode") or not node.replace("_", "").isalnum() for node in nodes): + parser.error("every --node must be a Blender ShaderNode identifier") + if not NATIVE_BLENDER.is_file(): + raise SystemExit(f"native Blender fixture generator is unavailable: {NATIVE_BLENDER.relative_to(ROOT)}") + destination = ROOT / output + destination.parent.mkdir(parents=True, exist_ok=True) + script = ( + "import bpy\n" + f"destination = {str(destination)!r}\n" + f"node_types = {nodes!r}\n" + "material = bpy.data.materials.new('WBV2 Shader Batch')\n" + "material.use_nodes = True\n" + "mesh = bpy.data.meshes.new('WBV2 Shader Mesh')\n" + "mesh.from_pydata([(-1,-1,0),(1,-1,0),(0,1,0)], [], [(0,1,2)])\n" + "obj = bpy.data.objects.new('WBV2 Shader Object', mesh)\n" + "bpy.context.collection.objects.link(obj)\n" + "obj.data.materials.append(material)\n" + "tree = material.node_tree\n" + "tree.nodes.clear()\n" + "for node_type in node_types:\n" + " node = tree.nodes.new(node_type)\n" + " node.name = 'WBV2 ' + node_type\n" + "bpy.ops.wm.save_as_mainfile(filepath=destination)\n" + ) + result = subprocess.run([str(NATIVE_BLENDER), "--background", "--factory-startup", "--python-expr", script], cwd=ROOT, text=True, capture_output=True, check=False) + if result.returncode or not destination.is_file(): + raise SystemExit(f"native shader fixture generation failed ({result.returncode}):\n{result.stdout}{result.stderr}") + print(json.dumps({"status": "PASS", "fixture": output.as_posix(), "nodes": nodes, "bytes": destination.stat().st_size})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())