mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 17:01:25 -07:00
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import test from 'node:test';
|
|
import { isDeepStrictEqual } from 'node:util';
|
|
import { getSnapshotPath, readSnapshot, writeSnapshot } from './cache';
|
|
import { CHARACTER_DICTIONARY_FORMAT_VERSION } from './constants';
|
|
import type { CharacterDictionarySnapshot } from './types';
|
|
|
|
function makeTempDir(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-character-dictionary-cache-'));
|
|
}
|
|
|
|
function createSnapshot(): CharacterDictionarySnapshot {
|
|
return {
|
|
formatVersion: CHARACTER_DICTIONARY_FORMAT_VERSION,
|
|
mediaId: 130298,
|
|
mediaTitle: 'The Eminence in Shadow',
|
|
entryCount: 1,
|
|
updatedAt: 1_700_000_000_000,
|
|
termEntries: [['アルファ', 'あるふぁ', '', '', 0, ['Alpha'], 0, 'name']],
|
|
images: [
|
|
{
|
|
path: 'img/m130298-c1.png',
|
|
dataBase64:
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+nmX8AAAAASUVORK5CYII=',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
test('writeSnapshot persists and readSnapshot restores current-format snapshots', async () => {
|
|
const outputDir = makeTempDir();
|
|
const snapshotPath = getSnapshotPath(outputDir, 130298);
|
|
const snapshot = createSnapshot();
|
|
|
|
await writeSnapshot(snapshotPath, snapshot);
|
|
|
|
assert.deepEqual(await readSnapshot(snapshotPath), { ...snapshot, nameSplitSource: 'heuristic' });
|
|
});
|
|
|
|
// A manual generate and an auto-sync can both land on the same media, so two writes for one
|
|
// snapshot can overlap. They must not stream into a shared temp file and interleave into a
|
|
// half-and-half snapshot.
|
|
test('concurrent writeSnapshot calls for the same media leave one complete snapshot', async () => {
|
|
const outputDir = makeTempDir();
|
|
const snapshotPath = getSnapshotPath(outputDir, 130298);
|
|
const base = createSnapshot();
|
|
// Distinct titles, lengths, and term text so the surviving file can be pinned to exactly one
|
|
// writer rather than merely "a snapshot that parses". A shared temp file is caught by the
|
|
// losing writers failing to rename; interleaved content is only caught when the timing happens
|
|
// to leave a mix, which is why the assertion checks identity rather than shape.
|
|
const variants: CharacterDictionarySnapshot[] = ['alpha', 'beta', 'gamma'].map((label, index) => {
|
|
const entryCount = 400 + index * 100;
|
|
return {
|
|
...base,
|
|
mediaTitle: `${base.mediaTitle} ${label}`,
|
|
entryCount,
|
|
termEntries: Array.from({ length: entryCount }, (_entry, entryIndex) => [
|
|
`${label}${entryIndex}`,
|
|
'なまえ',
|
|
'name primary',
|
|
'',
|
|
75,
|
|
[`${label} character ${entryIndex} `.repeat(600)],
|
|
0,
|
|
'',
|
|
]) as CharacterDictionarySnapshot['termEntries'],
|
|
};
|
|
});
|
|
|
|
await Promise.all(variants.map((variant) => writeSnapshot(snapshotPath, variant)));
|
|
|
|
const restored = await readSnapshot(snapshotPath);
|
|
const expected = variants.map((variant) => ({
|
|
...variant,
|
|
nameSplitSource: 'heuristic' as const,
|
|
}));
|
|
const matches = expected.filter((candidate) => isDeepStrictEqual(restored, candidate));
|
|
assert.equal(
|
|
matches.length,
|
|
1,
|
|
`expected exactly one writer's complete snapshot to survive, got ${
|
|
restored === null
|
|
? 'an unreadable file'
|
|
: `entryCount=${restored.entryCount}, terms=${restored.termEntries.length}, title=${restored.mediaTitle}`
|
|
}`,
|
|
);
|
|
|
|
// Every writer cleaned up after itself, so no temp files are left behind.
|
|
const leftovers = fs
|
|
.readdirSync(path.dirname(snapshotPath))
|
|
.filter((name) => name.includes('.tmp-'));
|
|
assert.deepEqual(leftovers, []);
|
|
});
|
|
|
|
test('readSnapshot preserves the mecab name-split source and defaults missing values to heuristic', async () => {
|
|
const outputDir = makeTempDir();
|
|
const snapshotPath = getSnapshotPath(outputDir, 130298);
|
|
const snapshot: CharacterDictionarySnapshot = {
|
|
...createSnapshot(),
|
|
nameSplitSource: 'mecab',
|
|
};
|
|
|
|
await writeSnapshot(snapshotPath, snapshot);
|
|
|
|
assert.equal((await readSnapshot(snapshotPath))?.nameSplitSource, 'mecab');
|
|
});
|
|
|
|
test('readSnapshot ignores snapshots written with an older format version', async () => {
|
|
const outputDir = makeTempDir();
|
|
const snapshotPath = getSnapshotPath(outputDir, 130298);
|
|
const staleSnapshot = {
|
|
...createSnapshot(),
|
|
formatVersion: CHARACTER_DICTIONARY_FORMAT_VERSION - 1,
|
|
};
|
|
|
|
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
|
fs.writeFileSync(snapshotPath, JSON.stringify(staleSnapshot), 'utf8');
|
|
|
|
assert.equal(await readSnapshot(snapshotPath), null);
|
|
});
|