Files
workinf_Blender_Wasm/my-tools/diagnose_local_save_stability.mjs
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

90 lines
4.5 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import createEngine from '../build_web_blender6/bin/web_engine.js';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const engine = await createEngine({ wasmBinary: fs.readFileSync(path.join(root, 'build_web_blender6/bin/web_engine.wasm')) });
const handle = engine._web_engine_create();
const pointers = engine._malloc(8);
const started = performance.now();
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
const output = () => {
const pointer = engine.HEAPU32[pointers / 4];
const length = engine.HEAPU32[pointers / 4 + 1];
return { pointer, bytes: engine.HEAPU8.slice(pointer, pointer + length) };
};
const open = file => {
const bytes = fs.readFileSync(path.join(root, file));
const pointer = engine._malloc(bytes.length);
try {
engine.HEAPU8.set(bytes, pointer);
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.length), 0);
} finally { engine._free(pointer); }
};
const invoke = (cmd, params = {}, ok = true) => {
const bytes = Buffer.from(JSON.stringify({ cmd, params }));
const pointer = engine._malloc(bytes.length);
try {
engine.HEAPU8.set(bytes, pointer);
assert.equal(engine._web_engine_call_json(handle, pointer, bytes.length, pointers, pointers + 4), 0);
const result = JSON.parse(new TextDecoder().decode(output().bytes));
assert.equal(result.ok, ok, result.error);
} finally { engine._free(pointer); }
};
const command = (command, ok = true) => invoke('applyCommand', { command }, ok);
const snapshot = () => {
assert.equal(engine._web_engine_get_scene_snapshot(handle, pointers, pointers + 4), 0);
return JSON.parse(new TextDecoder().decode(output().bytes));
};
const save = () => {
assert.equal(engine._web_engine_save_blend(handle, pointers, pointers + 4), 0);
const result = output();
engine._web_engine_free_buffer(result.pointer);
return result.bytes;
};
try {
open('tests/files/web/nonmesh_scene.blend');
command({ type: 'createObjectData', objectType: 'EMPTY', name: 'Save Stability Probe' });
const beforeState = snapshot();
const font = beforeState.nonMeshData.find(data => data.type === 'FONT');
assert(font);
const first = save();
const savedMemory = engine._web_engine_get_allocated_bytes();
assert.equal(hash(save()), hash(first), 'unchanged repeated saves must have identical bytes');
assert.equal(engine._web_engine_get_allocated_bytes(), savedMemory, 'repeated saves must not accumulate retained buffers');
command({ type: 'setFontLinks', dataId: font.id, links: { regular: 'vfont:MissingLocalFont' } }, false);
const rejectedState = snapshot();
assert.equal(rejectedState.revision, beforeState.revision);
assert.deepEqual(rejectedState.nonMeshData, beforeState.nonMeshData);
assert.deepEqual(rejectedState.vfonts, beforeState.vfonts);
const afterFailure = save();
assert.equal(hash(afterFailure), hash(first), 'a rejected command must preserve saved bytes for the same revision');
command({ type: 'createObjectData', objectType: 'EMPTY', name: 'Changed Save Probe' });
const changed = save();
assert.notEqual(hash(changed), hash(first), 'successful edits must invalidate saved bytes');
invoke('undo');
assert(!snapshot().nodes.some(node => node.name === 'Changed Save Probe'));
assert.notEqual(hash(save()), hash(changed), 'undo must not export the newer cached state');
invoke('redo');
assert(snapshot().nodes.some(node => node.name === 'Changed Save Probe'));
const redone = save();
assert.equal(hash(save()), hash(redone));
open('tests/files/web/basic_scene.blend');
const reopened = save();
assert.equal(hash(reopened), hash(fs.readFileSync(path.join(root, 'tests/files/web/basic_scene.blend'))),
'opening a different project must invalidate the saved byte cache');
open('tests/files/web/nonmesh_scene.blend');
assert.equal(hash(save()), hash(fs.readFileSync(path.join(root, 'tests/files/web/nonmesh_scene.blend'))));
console.log(JSON.stringify({ status: 'PASS', seconds: (performance.now() - started) / 1000,
savedHash: hash(first), afterFailureHash: hash(afterFailure), changedHash: hash(changed) }));
} catch (error) {
console.log(JSON.stringify({ status: 'FAIL', error: error.message, seconds: (performance.now() - started) / 1000 }));
process.exitCode = 1;
} finally {
engine._free(pointers);
engine._web_engine_destroy(handle);
}