import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const inventory = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03A/library-operation-inventory.json"), "utf8")); const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); const sources = new Map(); assert.equal(inventory.schemaVersion, 1); assert.equal(inventory.task, "M12-03A"); assert.equal(inventory.enablingTask, true); assert.equal(inventory.parityStateChange, false); assert.equal(inventory.blenderVersion, "5.2.0"); assert.equal(inventory.nextTask, "M12-03B"); assert.deepEqual(inventory.sources.map((source) => source.role), [ "LINK_APPEND_CONTEXT_API", "LINK_APPEND_CLOSURE", "LINK_APPEND_OPERATORS", "PYTHON_LIBRARY_LOAD", "BLEND_LIBRARY_READER", "OVERRIDE_STORAGE", "OVERRIDE_API", "OVERRIDE_CLOSURE", "CURRENT_WEB_LIBRARY_GATE", ]); for (const source of inventory.sources) { const bytes = fs.readFileSync(path.join(root, source.path)); assert.equal(sha256(bytes), source.sha256, `${source.role} source hash drifted`); sources.set(source.role, bytes.toString("utf8")); } assert.equal(sha256(fs.readFileSync(path.join(root, inventory.runtimeFixture.path))), inventory.runtimeFixture.sha256); assert.equal(inventory.dataBlockSurface.discoverableCollections.length, 36); assert.equal(new Set(inventory.dataBlockSurface.discoverableCollections).size, 36); assert.deepEqual(inventory.dataBlockSurface.onlyAppendableCollections, ["screens", "workspaces"]); assert.deepEqual(inventory.operations.map((operation) => operation.operation), ["APPEND", "LINK", "LIBRARY_OVERRIDE"]); for (const operation of inventory.operations) { assert.ok(operation.dataBlockRoles.length >= 6, `${operation.operation} has an incomplete role inventory`); assert.ok(operation.closure && Object.keys(operation.closure).length >= 5, `${operation.operation} has an incomplete closure inventory`); } const context = sources.get("LINK_APPEND_CONTEXT_API"); for (const token of [ "LINK_APPEND_ACT_KEEP_LINKED", "LINK_APPEND_ACT_REUSE_LOCAL", "LINK_APPEND_ACT_MAKE_LOCAL", "LINK_APPEND_ACT_COPY_LOCAL", "LINK_APPEND_TAG_INDIRECT", "LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY", "LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY_ONLY", "ID *new_id", "Library *source_library", "ID *liboverride_id", "ID *reusable_local_id", "enum class ProcessStage", ]) assert.ok(context.includes(token), `link/append context token is missing: ${token}`); const closure = sources.get("LINK_APPEND_CLOSURE"); for (const token of [ "BKE_library_foreach_ID_link", "IDWALK_CB_EMBEDDED", "IDWALK_CB_EMBEDDED_NOT_OWNING", "IDWALK_CB_INTERNAL", "IDWALK_CB_LOOPBACK", "LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY_ONLY", "new_id->lib->runtime->parent", ]) assert.ok(closure.includes(token), `dependency closure token is missing: ${token}`); assert.match(closure, /BKE_blendfile_append\(/); assert.match(closure, /BKE_blendfile_override\(/); const operators = sources.get("LINK_APPEND_OPERATORS"); for (const token of [ "BKE_idtype_idcode_is_linkable(idcode)", "BKE_idtype_idcode_is_only_appendable(idcode)", "BLO_LIBLINK_APPEND_RECURSIVE", "BLO_LIBLINK_APPEND_LOCAL_ID_REUSE", "void WM_OT_link", "void WM_OT_append", ]) assert.ok(operators.includes(token), `operator inventory token is missing: ${token}`); const pythonLoad = sources.get("PYTHON_LIBRARY_LOAD"); for (const argument of inventory.operatorSurface.pythonLoadArguments) { assert.ok(pythonLoad.includes(`\"${argument}\"`), `Python load argument is missing: ${argument}`); } for (const token of [ "if (!is_library && !BKE_idtype_idcode_is_linkable(code))", "if (!BKE_idtype_idcode_is_linkable(idcode) || (idcode == ID_WS && !do_append))", "create_liboverrides", "BKE_blendfile_link(lapp_context", "BKE_blendfile_append(lapp_context", "BKE_blendfile_override(lapp_context", ]) assert.ok(pythonLoad.includes(token), `Python library closure token is missing: ${token}`); const reader = sources.get("BLEND_LIBRARY_READER"); for (const token of ["BLO_library_link_begin", "BLO_library_link_named_part", "BLO_library_link_end"]) { assert.ok(reader.includes(token), `reader stage is missing: ${token}`); } const overrideStorage = sources.get("OVERRIDE_STORAGE"); for (const token of [ "struct IDOverrideLibraryPropertyOperation", "struct IDOverrideLibraryProperty", "struct IDOverrideLibrary", "ID *reference", "ID *hierarchy_root", "ListBaseT properties", ]) assert.ok(overrideStorage.includes(token), `override storage token is missing: ${token}`); const overrideApi = sources.get("OVERRIDE_API"); for (const token of [ "BKE_lib_override_library_create(", "BKE_lib_override_library_resync(", "BKE_lib_override_library_operations_create(", ]) assert.ok(overrideApi.includes(token), `override API token is missing: ${token}`); assert.match(sources.get("OVERRIDE_CLOSURE"), /BKE_library_foreach_ID_link/); const web = sources.get("CURRENT_WEB_LIBRARY_GATE"); assert.match(web, /gateLibraryMutation\(operation:/); assert.match(web, /LIBRARY_MUTATION_UNAVAILABLE/); for (const gap of inventory.currentWebGap.missing) assert.ok(!inventory.currentWebGap.represented.includes(gap)); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m); const script = [ "import bpy,json", `fixture=${JSON.stringify(path.join(root, inventory.runtimeFixture.path))}`, "result={}", "result['appendProperties']=[p.identifier for p in bpy.ops.wm.append.get_rna_type().properties if p.identifier != 'rna_type']", "result['linkProperties']=[p.identifier for p in bpy.ops.wm.link.get_rna_type().properties if p.identifier != 'rna_type']", "ctx=bpy.data.libraries.load(fixture,link=True)", "source,target=ctx.__enter__()", "result['collections']=sorted([name for name in dir(source) if not name.startswith('_') and name not in {'libraries','version'}])", "result['libraries']=[{'filepath':item.filepath,'isArchive':item.is_archive} for item in source.libraries]", "ctx.__exit__(None,None,None)", "result['overrideRNA']={name:[{'identifier':p.identifier,'type':p.type,'isReadonly':p.is_readonly} for p in getattr(bpy.types,name).bl_rna.properties if p.identifier != 'rna_type'] for name in ['IDOverrideLibrary','IDOverrideLibraryProperty','IDOverrideLibraryPropertyOperation']}", "print('M12_03A='+json.dumps(result,separators=(',',':'),sort_keys=True))", ].join(";"); const runtimeOutput = execFileSync(blender, ["--background", "--factory-startup", "--python-expr", script], { cwd: root, encoding: "utf8" }); const runtimeLine = runtimeOutput.split(/\r?\n/).find((line) => line.startsWith("M12_03A=")); assert.ok(runtimeLine, "Blender link/append/override runtime inventory is missing"); const runtime = JSON.parse(runtimeLine.slice("M12_03A=".length)); assert.deepEqual(runtime.appendProperties, inventory.operatorSurface.appendProperties); assert.deepEqual(runtime.linkProperties, inventory.operatorSurface.linkProperties); assert.deepEqual(runtime.collections, inventory.dataBlockSurface.discoverableCollections); assert.deepEqual(runtime.libraries, inventory.runtimeFixture.nestedLibraries); assert.deepEqual(runtime.overrideRNA, inventory.overrideRNA); process.stdout.write( `library-operation-inventory-ok roots=${runtime.collections.length} appendOnly=${inventory.dataBlockSurface.onlyAppendableCollections.length} operations=${inventory.operations.length} overrideFields=${Object.values(runtime.overrideRNA).reduce((sum, values) => sum + values.length, 0)} gaps=${inventory.currentWebGap.missing.length} next=${inventory.nextTask}\n`, );