mirror of
https://github.com/gittrahan/support-fins.git
synced 2026-10-21 19:32:48 -05:00
137 lines
7.7 KiB
JavaScript
137 lines
7.7 KiB
JavaScript
import * as fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { openProjectCache } from '../project-cache.js';
|
|
import { assert } from '../../../tests/_util.js';
|
|
|
|
const data = { triangles: new Float32Array([0, 1 / 3, -2, 3, 4, 5, 6, 7, 8]),
|
|
offset: { x: -12.25, y: 7, z: 0 }, stats: { fins: 1, nested: { count: 2 } } };
|
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
|
|
async function temporary(fn) {
|
|
const dir = await Deno.makeTempDir();
|
|
try { await fn(dir); } finally { await Deno.remove(dir, { recursive: true }); }
|
|
}
|
|
|
|
function request(dir, changes = {}, warnings = []) {
|
|
return { input: path.join(dir, 'part.3mf'), bytes: new Uint8Array([1, 2, 3]),
|
|
selection: { objects: [1, 2], explicit: false }, settings: { dialog: { gap: 0.1 } },
|
|
warn: (s) => warnings.push(s), ...changes };
|
|
}
|
|
const open = (r, build = 'build-a') => openProjectCache(r, async () => build);
|
|
const folders = (root) => fs.readdir(root);
|
|
const records = async (root) => (await fs.readdir(root, { recursive: true })).filter((n) => n.endsWith('.cache'));
|
|
|
|
Deno.test('cache: persist exact geometry, offsets, stats and zero-fin objects; remove only completed run', () => temporary(async (dir) => {
|
|
const r = request(dir), root = `${r.input}.fins-cache`, c = await open(r);
|
|
await c.save(1, data);
|
|
await c.save(2, { ...data, triangles: new Float32Array() });
|
|
const resumed = await open(r);
|
|
const first = await resumed.load(1), second = await resumed.load(2);
|
|
assert(same(first, data), 'checkpoint changed geometry or metadata');
|
|
assert(second && second.triangles.length === 0, 'zero-fin object treated as a miss');
|
|
assert(await resumed.load(3) === null, 'absent object reused');
|
|
await resumed.finish();
|
|
let gone = false;
|
|
try { await fs.stat(root); } catch (e) { gone = e.code === 'ENOENT'; }
|
|
assert(gone, 'completed cache left behind');
|
|
}));
|
|
|
|
Deno.test('cache: invalidate changed source, selection, settings or engine; retain other runs', () => temporary(async (dir) => {
|
|
const r = request(dir), root = `${r.input}.fins-cache`, c = await open(r);
|
|
await c.save(1, data);
|
|
for (const [changes, build] of [
|
|
[{ bytes: new Uint8Array([1, 2, 4]) }, 'build-a'],
|
|
[{ selection: { objects: [1], explicit: true } }, 'build-a'],
|
|
[{ selection: { objects: [1, 2], explicit: true } }, 'build-a'],
|
|
[{ settings: { dialog: { gap: 0.2 } } }, 'build-a'],
|
|
[{}, 'build-b'],
|
|
]) {
|
|
const warnings = [], other = await open(request(dir, changes, warnings), build);
|
|
assert(await other.load(1) === null, 'stale result accepted');
|
|
assert(warnings.some((s) => s.includes('no compatible')), 'invalidation not explained');
|
|
await other.finish();
|
|
assert((await records(root)).length === 1, 'other run deleted the original checkpoint');
|
|
}
|
|
assert(same(await (await open(r)).load(1), data), 'original cache no longer reusable');
|
|
}));
|
|
|
|
Deno.test('cache: settings key order and input filename do not change content identity', () => temporary(async (dir) => {
|
|
const cacheDir = path.join(dir, 'shared'), r = request(dir, { cacheDir, settings: { dialog: { gap: 0.1, layerHeight: 0.2 } } });
|
|
await (await open(r)).save(1, data);
|
|
const c = await open({ ...r, input: path.join(dir, 'renamed.3mf'), settings: { dialog: { layerHeight: 0.2, gap: 0.1 } } });
|
|
assert(same(await c.load(1), data), 'equivalent settings/content missed the cache');
|
|
await c.finish();
|
|
assert((await fs.stat(cacheDir)).isDirectory() && (await folders(cacheDir)).length === 0,
|
|
'custom cache parent was removed');
|
|
}));
|
|
|
|
Deno.test('cache: corrupt records warn and miss; incomplete writes and unrelated files survive cleanup', () => temporary(async (dir) => {
|
|
const warnings = [], r = request(dir, {}, warnings), root = `${r.input}.fins-cache`;
|
|
await (await open(r)).save(1, data);
|
|
const [record] = await records(root), file = path.join(root, record), bytes = await fs.readFile(file);
|
|
bytes[bytes.length - 1] ^= 1;
|
|
await fs.writeFile(file, bytes);
|
|
const folder = path.dirname(file);
|
|
await fs.writeFile(path.join(folder, 'object-2-interrupted.cache.tmp'), 'incomplete');
|
|
await fs.writeFile(path.join(folder, 'notes.txt'), 'unrelated');
|
|
await fs.writeFile(path.join(root, 'keep.txt'), 'keep');
|
|
const c = await open(r);
|
|
assert(await c.load(1) === null && await c.load(2) === null, 'corrupt or partial record reused');
|
|
assert(warnings.some((s) => s.includes('checksum mismatch')), 'corruption not reported');
|
|
await c.save(1, data);
|
|
await c.finish();
|
|
assert((await records(root)).length === 0, 'completed checkpoints retained');
|
|
assert(await fs.readFile(path.join(root, 'keep.txt'), 'utf8') === 'keep', 'unrelated root file removed');
|
|
assert(await fs.readFile(path.join(folder, 'notes.txt'), 'utf8') === 'unrelated', 'unrelated run file removed');
|
|
assert(await fs.readFile(path.join(folder, 'object-2-interrupted.cache.tmp'), 'utf8') === 'incomplete', 'unknown temporary write removed');
|
|
}));
|
|
|
|
Deno.test('cache: symlinked checkpoint is not read and cleanup cannot delete its target', () => temporary(async (dir) => {
|
|
const r = request(dir), root = `${r.input}.fins-cache`, warnings = [];
|
|
const c = await open(r); await c.save(1, data);
|
|
const [record] = await records(root), file = path.join(root, record), outside = path.join(dir, 'keep.bin');
|
|
await fs.rename(file, outside); await fs.symlink(outside, file);
|
|
const next = await open({ ...r, warn: (s) => warnings.push(s) });
|
|
assert(await next.load(1) === null, 'symlink followed');
|
|
await next.finish();
|
|
assert((await fs.stat(outside)).size > 0, 'symlink target deleted');
|
|
assert(warnings.some((s) => s.includes('not a regular file')), 'symlink not reported');
|
|
}));
|
|
|
|
Deno.test('cache: failed write is fatal and leaves earlier checkpoints intact', () => temporary(async (dir) => {
|
|
const r = request(dir), root = `${r.input}.fins-cache`, c = await open(r);
|
|
await c.save(1, data);
|
|
const [name] = await folders(root), folder = path.join(root, name), moved = path.join(root, 'moved');
|
|
await fs.rename(folder, moved); await fs.writeFile(folder, 'not a directory');
|
|
let error;
|
|
try { await c.save(2, data); } catch (e) { error = e; }
|
|
assert(error?.message.includes('cannot save checkpoint for object 2'), 'write failure was not fatal/actionable');
|
|
await fs.unlink(folder); await fs.rename(moved, folder);
|
|
assert(same(await (await open(r)).load(1), data), 'previous checkpoint lost');
|
|
}));
|
|
|
|
Deno.test('cache: concurrent writers use independent records and cleanup does not remove unseen writes', () => temporary(async (dir) => {
|
|
const r = request(dir), a = await open(r), b = await open(r);
|
|
await Promise.all([a.save(1, data), b.save(2, data)]);
|
|
await a.finish();
|
|
assert(same(await (await open(r)).load(2), data), 'one writer removed another writer\'s checkpoint');
|
|
await b.finish();
|
|
}));
|
|
|
|
Deno.test('cache: source build identity detects uncommitted engine/schema changes without Git', () => temporary(async (dir) => {
|
|
await fs.mkdir(path.join(dir, 'plugins/cli'), { recursive: true });
|
|
await fs.mkdir(path.join(dir, 'plugins/shared/engine'), { recursive: true });
|
|
await fs.mkdir(path.join(dir, 'web'), { recursive: true });
|
|
const module = path.join(dir, 'plugins/cli/project-cache.mjs');
|
|
await fs.copyFile(new URL('../project-cache.js', import.meta.url), module);
|
|
const { runtimeIdentity } = await import(pathToFileURL(module).href);
|
|
const before = await runtimeIdentity();
|
|
await fs.writeFile(path.join(dir, 'web/fins.js'), 'changed engine');
|
|
const engine = await runtimeIdentity();
|
|
await fs.writeFile(path.join(dir, 'plugins/shared/engine/options.json'), '{"changed":true}');
|
|
const schema = await runtimeIdentity();
|
|
assert(before !== engine && engine !== schema, 'unstamped source edits reused a build identity');
|
|
assert(schema === await runtimeIdentity(), 'identity unstable for unchanged files');
|
|
}));
|