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"; import ts from "../../web/node_modules/typescript/lib/typescript.js"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const inventory = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02A/asset-preview-inventory.json"), "utf8")); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); assert.equal(inventory.task, "M12-02A"); assert.equal(inventory.enablingTask, true); assert.equal(inventory.parityStateChange, false); assert.equal(inventory.blenderVersion, "5.2.0"); assert.equal(inventory.nextTask, "M12-02B"); for (const item of [...inventory.sources, ...inventory.fixtures]) assert.equal(sha256(item.path), item.sha256, item.path); const source = (role) => fs.readFileSync(path.join(root, inventory.sources.find((item) => item.role === role).path), "utf8"); const dna = source("PREVIEW_DNA"); for (const token of ["unsigned int w[2]", "unsigned int h[2]", "short flag[2]", "short changed_timestamp[2]", "unsigned int *rect[2]", "PreviewImageRuntime *runtime"]) { assert.ok(dna.includes(token), `PreviewImage DNA token is missing: ${token}`); } const slots = source("PREVIEW_SLOT_ENUM"); assert.match(slots, /ICON_SIZE_ICON = 0/); assert.match(slots, /ICON_SIZE_PREVIEW = 1/); assert.equal(inventory.storage.slotCount, 2); const implementation = source("PREVIEW_IMPLEMENTATION"); assert.match(implementation, /PreviewImage assumes pre-multiplied alpha/); assert.match(implementation, /writer->write_uint32_array\(prv_copy\.w\[0\] \* prv_copy\.h\[0\]/); assert.match(implementation, /writer->write_uint32_array\(prv_copy\.w\[1\] \* prv_copy\.h\[1\]/); assert.match(implementation, /prv->flag\[i\] &= ~PRV_RENDERING/); assert.equal(inventory.storage.colorSpace, null); assert.ok(!inventory.storage.fields.some((field) => /color/i.test(field.name))); const rnaSource = source("PREVIEW_RNA"); assert.match(rnaSource, /Image pixels, as bytes \(always 32-bit RGBA\)/); assert.match(rnaSource, /length\[0\] = prv_img->w\[size\] \* prv_img->h\[size\] \* 4/); assert.match(rnaSource, /values\[i\] = data\[i\] \* \(1\.0f \/ 255\.0f\)/); assert.doesNotMatch(rnaSource.slice(rnaSource.indexOf("static void rna_def_image_preview"), rnaSource.indexOf("static void rna_def_image_user")), /color.?space/i); const parseInterface = (file, name) => { const absolute = path.join(root, file); const parsed = ts.createSourceFile(absolute, fs.readFileSync(absolute, "utf8"), ts.ScriptTarget.Latest, true); const declaration = parsed.statements.find((statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === name); assert.ok(declaration, `${name} is missing`); return declaration.members.filter(ts.isPropertySignature).map((member) => member.name.getText(parsed)); }; assert.deepEqual(parseInterface("web/protocol/asset-library-io.ts", "AssetPreviewIR"), inventory.currentWeb.v1Fields); assert.deepEqual(parseInterface("web/protocol/asset-catalog-v2.ts", "AssetCatalogV2PreviewIR"), inventory.currentWeb.v2Fields); 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 rnaProbe = [ "import bpy,json", "props=[{'identifier':p.identifier,'type':p.type,'readOnly':p.is_readonly,'arrayLength':p.array_length} for p in bpy.types.ImagePreview.bl_rna.properties if p.identifier != 'rna_type']", "idp=bpy.types.ID.bl_rna.properties['preview']", "print('M12_PREVIEW_RNA='+json.dumps({'idProperty':{'identifier':idp.identifier,'type':idp.type,'readOnly':idp.is_readonly,'nullWhenAbsent':True},'imagePreviewProperties':props},separators=(',',':')))", ].join(";"); const rnaOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", rnaProbe], { encoding: "utf8" }); const rnaLine = rnaOutput.split(/\r?\n/).find((line) => line.startsWith("M12_PREVIEW_RNA=")); assert.ok(rnaLine); assert.deepEqual(JSON.parse(rnaLine.slice("M12_PREVIEW_RNA=".length)), { idProperty: inventory.rna.idProperty, imagePreviewProperties: inventory.rna.imagePreviewProperties, }); const noPreviewScript = [ "import bpy,json", "items=[]", "groups=(bpy.data.objects,bpy.data.materials,bpy.data.worlds)", "[items.append({'type':item.bl_rna.identifier,'preview':None if item.preview is None else list(item.preview.image_size)}) for group in groups for item in group if item.asset_data]", "print('M12_NO_PREVIEW='+json.dumps(items,separators=(',',':')))", ].join(";"); const noPreviewOutput = execFileSync(blender, ["--factory-startup", "--background", path.join(root, inventory.fixtures[1].path), "--python-expr", noPreviewScript], { encoding: "utf8" }); const noPreviewLine = noPreviewOutput.split(/\r?\n/).find((line) => line.startsWith("M12_NO_PREVIEW=")); assert.ok(noPreviewLine); const noPreview = JSON.parse(noPreviewLine.slice("M12_NO_PREVIEW=".length)); assert.deepEqual(noPreview.map((item) => item.type), inventory.runtimeStates.noPreview.assetTypes); assert.deepEqual(noPreview.map((item) => item.preview), inventory.runtimeStates.noPreview.previewValues); const png = path.join(root, inventory.fixtures[0].path); const loadedScript = [ "import bpy,json,bpy.utils.previews", "collection=bpy.utils.previews.new()", `preview=collection.load('m12-preview',${JSON.stringify(png)},'IMAGE',True)`, "result={'sourceSize':[8,8],'imageSize':list(preview.image_size),'iconSize':list(preview.icon_size),'imagePackedPixelCount':len(preview.image_pixels),'imageFloatComponentCount':len(preview.image_pixels_float),'iconPackedPixelCount':len(preview.icon_pixels),'imageCustom':preview.is_image_custom,'iconCustom':preview.is_icon_custom}", "print('M12_LOADED_PREVIEW='+json.dumps(result,separators=(',',':')))", "bpy.utils.previews.remove(collection)", ].join(";"); const loadedOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", loadedScript], { encoding: "utf8" }); const loadedLine = loadedOutput.split(/\r?\n/).find((line) => line.startsWith("M12_LOADED_PREVIEW=")); assert.ok(loadedLine); assert.deepEqual(JSON.parse(loadedLine.slice("M12_LOADED_PREVIEW=".length)), inventory.runtimeStates.loadedPreview); process.stdout.write(`asset-preview-inventory-ok slots=${inventory.storage.slotCount} rna=${inventory.rna.imagePreviewProperties.length} absent=${noPreview.length} loaded=${inventory.runtimeStates.loadedPreview.imageSize.join("x")} next=${inventory.nextTask}\n`);