feat(web): automate native shader reader batches
This commit is contained in:
@@ -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}")
|
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:
|
def native_geometry_batch(inventory: dict) -> dict | None:
|
||||||
candidates = [
|
candidates = [
|
||||||
unit for unit in inventory.get("units", [])
|
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:
|
def prepare_native_fixture(batch: dict, task: str, contract: dict) -> dict:
|
||||||
"""Generate a native fixture, shrinking only when Blender rejects a mix."""
|
"""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
|
return contract
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if batch.get("nativeCompositorFixture"):
|
if batch.get("nativeCompositorFixture"):
|
||||||
ensure_native_compositor_fixture(batch, contract)
|
ensure_native_compositor_fixture(batch, contract)
|
||||||
else:
|
elif batch.get("nativeGeometryFixture"):
|
||||||
ensure_native_geometry_fixture(batch, contract)
|
ensure_native_geometry_fixture(batch, contract)
|
||||||
|
else:
|
||||||
|
ensure_native_shader_fixture(batch, contract)
|
||||||
return contract
|
return contract
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
units = list(batch.get("units", []))
|
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
|
module = record.get("module") if isinstance(record, dict) else None
|
||||||
receipt_path = ROOT / "tests/golden" / current / "module-receipt.json"
|
receipt_path = ROOT / "tests/golden" / current / "module-receipt.json"
|
||||||
receipt = read_json(receipt_path) if receipt_path.is_file() else {}
|
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
|
return False
|
||||||
if receipt.get("status") != "BLOCKED" or len(receipt.get("selectedRecordIds", [])) <= 5:
|
if receipt.get("status") != "BLOCKED":
|
||||||
return False
|
return False
|
||||||
by_id = {unit.get("id"): unit for unit in inventory.get("units", []) if isinstance(unit, dict)}
|
by_id = {unit.get("id"): unit for unit in inventory.get("units", []) if isinstance(unit, dict)}
|
||||||
selected_ids = list(receipt.get("selectedRecordIds", []))[:5]
|
selected_ids = list(receipt.get("selectedRecordIds", []))[:5]
|
||||||
units = [by_id[unit_id] for unit_id in selected_ids if unit_id in by_id]
|
units = [by_id[unit_id] for unit_id in selected_ids if unit_id in by_id]
|
||||||
if len(units) != len(selected_ids):
|
if len(units) != len(selected_ids):
|
||||||
return False
|
return False
|
||||||
|
is_shader = str(module["moduleId"]).startswith("AUTO_SHADER")
|
||||||
|
if not is_shader and len(receipt.get("selectedRecordIds", [])) <= 5:
|
||||||
|
return False
|
||||||
batch = {
|
batch = {
|
||||||
"moduleId": str(module["moduleId"]),
|
"moduleId": str(module["moduleId"]),
|
||||||
"title": "recover native compositor node reader batch",
|
"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],
|
"types": [unit["identifier"] for unit in units],
|
||||||
"units": units,
|
"units": units,
|
||||||
"nativeCompositorFixture": True,
|
"nativeCompositorFixture": True,
|
||||||
"test": "web/tests/e2e/compositor-main-reader-production.spec.ts",
|
"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}/compositor-main-reader-production.json",
|
"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)
|
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"]
|
fixture = batch["fixture"]
|
||||||
source_paths = [unit["locations"][0]["path"] for unit in units]
|
source_paths = [unit["locations"][0]["path"] for unit in units]
|
||||||
source_paths += [
|
source_paths += [
|
||||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
"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,
|
fixture,
|
||||||
"web/app/src/vendor/blender/web_engine.wasm",
|
"web/app/src/vendor/blender/web_engine.wasm",
|
||||||
]
|
]
|
||||||
types = json.dumps(batch["types"], separators=(",", ":"))
|
types = json.dumps(batch["types"], separators=(",", ":"))
|
||||||
ids = json.dumps(selected_ids, 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 = (
|
command = (
|
||||||
f"WBV2_BATCH_TASK={current} WBV2_BATCH_PRODUCTION={production} "
|
f"WBV2_BATCH_TASK={current} WBV2_BATCH_PRODUCTION={production} "
|
||||||
f"WBV2_BATCH_FIXTURE={fixture} WBV2_BATCH_TYPES='{types}' WBV2_BATCH_UNITS='{ids}' "
|
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({
|
module.update({
|
||||||
"batchId": contract["batchId"],
|
"batchId": contract["batchId"],
|
||||||
@@ -843,6 +877,9 @@ def select_batch(inventory: dict) -> dict:
|
|||||||
geometry = native_geometry_batch(inventory)
|
geometry = native_geometry_batch(inventory)
|
||||||
if geometry:
|
if geometry:
|
||||||
return geometry
|
return geometry
|
||||||
|
shader = native_shader_batch(inventory)
|
||||||
|
if shader:
|
||||||
|
return shader
|
||||||
summary = pending_summary(inventory)
|
summary = pending_summary(inventory)
|
||||||
write_planner_status({
|
write_planner_status({
|
||||||
"status": "waiting_for_exact_evidence",
|
"status": "waiting_for_exact_evidence",
|
||||||
@@ -885,6 +922,11 @@ def main() -> int:
|
|||||||
batch["fixture"] = f"tests/files/web/generated/wbv2-geometry-{digest}.blend"
|
batch["fixture"] = f"tests/files/web/generated/wbv2-geometry-{digest}.blend"
|
||||||
if args.write:
|
if args.write:
|
||||||
contract = prepare_native_fixture(batch, module_task, contract)
|
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"]
|
batch["batchId"] = contract["batchId"]
|
||||||
write_planner_status({
|
write_planner_status({
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
|
|||||||
57
my-tools/generate_wbv2_shader_fixture.py
Normal file
57
my-tools/generate_wbv2_shader_fixture.py
Normal file
@@ -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())
|
||||||
Reference in New Issue
Block a user