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 manifestPath = path.join(root, "tests/golden/M12-01A/asset-catalog-field-inventory.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); const sourceByRole = new Map(); const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); assert.equal(manifest.schemaVersion, 1); assert.equal(manifest.task, "M12-01A"); assert.equal(manifest.enablingTask, true); assert.equal(manifest.parityStateChange, false); assert.equal(manifest.blenderVersion, "5.2.0"); assert.equal(manifest.nextTask, "M12-01B"); const expectedRoles = [ "DNA_ASSET_STORAGE", "RNA_ASSET_API", "ASSET_BLEND_IO", "CATALOG_MODEL", "CATALOG_PATH", "CATALOG_FILE_API", "CATALOG_FILE_FORMAT", "CURRENT_WEB_V1_SNAPSHOT", ]; assert.deepEqual(manifest.sources.map((source) => source.role), expectedRoles); for (const source of manifest.sources) { const absolutePath = path.join(root, source.path); const bytes = fs.readFileSync(absolutePath); assert.equal(sha256(bytes), source.sha256, `${source.role} source hash drifted`); sourceByRole.set(source.role, bytes.toString("utf8")); } const expectedTagFields = ["next", "prev", "name"]; const expectedDNAFields = [ "local_type_info", "properties", "catalog_id", "catalog_simple_name", "author", "description", "copyright", "license", "tags", "active_tag", "tot_tags", "flag", "preferred_import_method", "_pad", ]; assert.deepEqual(manifest.assetTag.fields.map((field) => field.name), expectedTagFields); assert.deepEqual(manifest.assetMetaData.dnaFields.map((field) => field.name), expectedDNAFields); assert.equal(new Set(expectedDNAFields).size, manifest.assetMetaData.dnaFields.length); const dna = sourceByRole.get("DNA_ASSET_STORAGE"); for (const field of [...manifest.assetTag.fields, ...manifest.assetMetaData.dnaFields]) { assert.ok(dna.includes(field.sourceToken), `DNA token for ${field.name} is missing`); } assert.equal(manifest.assetTag.fields.find((field) => field.name === "name").maximumStorageBytesIncludingNull, 64); assert.equal(manifest.assetMetaData.dnaFields.find((field) => field.name === "properties").semantic, "CUSTOM_METADATA_NO_ID_POINTERS"); assert.equal(manifest.assetMetaData.dnaFields.find((field) => field.name === "catalog_simple_name").semantic, "RECOVERY_ONLY_NOT_AUTHORITY"); const rna = sourceByRole.get("RNA_ASSET_API"); const assetDataDefinition = rna.slice(rna.indexOf("static void rna_def_asset_data"), rna.indexOf("static void rna_def_asset_representation")); const sourceRNAProperties = [...assetDataDefinition.matchAll(/RNA_def_property\(srna, "([^"]+)"/g)].map((match) => match[1]); assert.deepEqual(sourceRNAProperties, manifest.assetMetaData.rnaProperties.map((property) => property.identifier)); assert.match(rna, /RNA_def_property_string_maxlength\(prop, MAX_NAME\)/); assert.match(rna, /RNA_def_function\(srna, "new", "rna_AssetMetaData_tag_new"\)/); assert.match(rna, /RNA_def_function\(srna, "remove", "rna_AssetMetaData_tag_remove"\)/); 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 python = [ "import bpy,json", "props=[{'identifier':p.identifier,'type':p.type,'isReadonly':p.is_readonly,'isRuntime':p.is_runtime} for p in bpy.types.AssetMetaData.bl_rna.properties if p.identifier != 'rna_type']", "print('M12_RNA='+json.dumps(props,separators=(',',':')))", ].join(";"); const runtimeOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", python], { encoding: "utf8" }); const runtimeLine = runtimeOutput.split(/\r?\n/).find((line) => line.startsWith("M12_RNA=")); assert.ok(runtimeLine, "Blender AssetMetaData RNA report is missing"); assert.deepEqual(JSON.parse(runtimeLine.slice("M12_RNA=".length)), manifest.assetMetaData.rnaProperties); const blendIO = sourceByRole.get("ASSET_BLEND_IO"); for (const value of ["properties", "author", "description", "copyright", "license", "tags"]) { assert.match(blendIO, new RegExp(`asset_data->${value}`), `${value} is not bound to Blend IO`); } assert.match(blendIO, /asset_data->tags\.count\(\) == asset_data->tot_tags/); const catalog = sourceByRole.get("CATALOG_MODEL"); for (const field of [...manifest.catalog.semanticFields, ...manifest.catalog.runtimeFlags]) { assert.ok(catalog.includes(field.sourceToken), `catalog token for ${field.name} is missing`); } assert.deepEqual(manifest.catalog.definitionFile.recordFields, ["catalog_id", "path", "simple_name"]); assert.deepEqual(manifest.catalog.definitionFile.writeOrder, ["path", "is_first_loaded", "catalog_id"]); assert.deepEqual(manifest.catalog.identityRules.duplicatePathSelectionOrder, ["is_first_loaded", "catalog_id"]); const catalogFile = sourceByRole.get("CATALOG_FILE_FORMAT"); assert.match(catalogFile, /SUPPORTED_VERSION = 1/); assert.match(catalogFile, /VERSION_MARKER = "VERSION "/); assert.match(catalogFile, /catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name/); const catalogPath = sourceByRole.get("CATALOG_PATH"); for (const statement of [ "Only slashes are used as path component separators", "Paths are stored as byte sequences, and assumed to be UTF8", "Empty components (caused by double slashes or leading/trailing slashes) are removed", ]) assert.ok(catalogPath.includes(statement)); const webPath = path.join(root, manifest.sources.find((source) => source.role === "CURRENT_WEB_V1_SNAPSHOT").path); const webSource = ts.createSourceFile(webPath, fs.readFileSync(webPath, "utf8"), ts.ScriptTarget.Latest, true); function interfaceFields(name) { const declaration = webSource.statements.find((statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === name); assert.ok(declaration, `missing Web interface ${name}`); return declaration.members.filter(ts.isPropertySignature).map((member) => member.name.getText(webSource)); } assert.deepEqual(interfaceFields("AssetCatalogIR"), manifest.currentWebSnapshot.assetCatalogIRFields); assert.deepEqual(interfaceFields("AssetEntryIR"), manifest.currentWebSnapshot.assetEntryIRFields); assert.deepEqual(manifest.currentWebSnapshot.missingMetadata, [ "properties", "description", "copyright", "catalog_simple_name", "active_tag", "use_preferred_import_method", "preferred_import_method", ]); assert.equal(manifest.currentWebSnapshot.semanticDrift.length, 5); process.stdout.write( `asset-catalog-field-inventory-ok dna=${expectedDNAFields.length} rna=${sourceRNAProperties.length} tag=${expectedTagFields.length} catalog=${manifest.catalog.semanticFields.length} gaps=${manifest.currentWebSnapshot.missingMetadata.length} next=${manifest.nextTask}\n`, );