125 lines
4.3 KiB
JavaScript
125 lines
4.3 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const atomic = require('../atomic-file');
|
|
const dirs = [];
|
|
|
|
function fresh(tag) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `peoplelib-atomic-${tag}-`));
|
|
dirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
test.after(() => {
|
|
for (const dir of dirs) {
|
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (e) { /* ignore */ }
|
|
}
|
|
});
|
|
|
|
test('元数据写入在改名前 fsync 数据', () => {
|
|
const dir = fresh('fsync');
|
|
const dest = path.join(dir, 'meta.json');
|
|
const order = [];
|
|
const realFsync = fs.fsyncSync;
|
|
const realRename = fs.renameSync;
|
|
fs.fsyncSync = function tracked(fd) {
|
|
order.push('fsync');
|
|
return realFsync.call(this, fd);
|
|
};
|
|
fs.renameSync = function tracked(from, to) {
|
|
order.push(`rename:${path.basename(String(from))}->${path.basename(String(to))}`);
|
|
return realRename.call(this, from, to);
|
|
};
|
|
try {
|
|
atomic.writeJson(dest, { hello: '世界' });
|
|
} finally {
|
|
fs.fsyncSync = realFsync;
|
|
fs.renameSync = realRename;
|
|
}
|
|
// rename 只保证目录项替换原子,不保证数据已落盘;
|
|
// 因此临时文件的 fsync 必须发生在改名之前
|
|
const firstRename = order.findIndex((step) => step.startsWith('rename:'));
|
|
assert.ok(firstRename > 0, `改名前应先 fsync,实际顺序 ${order.join(',')}`);
|
|
assert.strictEqual(order[0], 'fsync');
|
|
assert.deepStrictEqual(JSON.parse(fs.readFileSync(dest, 'utf8')), { hello: '世界' });
|
|
});
|
|
|
|
test('写入失败时清理临时文件并把备份换回原位', () => {
|
|
const dir = fresh('rollback');
|
|
const dest = path.join(dir, 'meta.json');
|
|
atomic.writeJson(dest, { round: 1 });
|
|
const before = fs.readFileSync(dest, 'utf8');
|
|
|
|
const realRename = fs.renameSync;
|
|
let failed = false;
|
|
fs.renameSync = function failing(from, to) {
|
|
if (!failed && String(from) === `${dest}.tmp` && String(to) === dest) {
|
|
failed = true;
|
|
throw new Error('模拟替换失败');
|
|
}
|
|
return realRename.apply(this, arguments);
|
|
};
|
|
try {
|
|
assert.throws(() => atomic.writeJson(dest, { round: 2 }), /模拟替换失败/);
|
|
} finally {
|
|
fs.renameSync = realRename;
|
|
}
|
|
assert.ok(failed);
|
|
assert.strictEqual(fs.readFileSync(dest, 'utf8'), before);
|
|
assert.strictEqual(fs.existsSync(`${dest}.tmp`), false);
|
|
assert.strictEqual(fs.existsSync(`${dest}.bak`), false);
|
|
});
|
|
|
|
test('目录 fsync 失败不影响写入结果', () => {
|
|
const dir = fresh('dirfail');
|
|
const dest = path.join(dir, 'meta.json');
|
|
const realOpen = fs.openSync;
|
|
// Windows 无法对目录取句柄,这条路径必须容错
|
|
fs.openSync = function guarded(target, flags, ...rest) {
|
|
if (String(target) === dir) throw new Error('EISDIR');
|
|
return realOpen.call(this, target, flags, ...rest);
|
|
};
|
|
try {
|
|
atomic.writeJson(dest, { ok: true });
|
|
} finally {
|
|
fs.openSync = realOpen;
|
|
}
|
|
assert.deepStrictEqual(JSON.parse(fs.readFileSync(dest, 'utf8')), { ok: true });
|
|
});
|
|
|
|
test('writeBytes 原子覆盖二进制文件且不残留临时文件', () => {
|
|
const dir = fresh('bytes');
|
|
const dest = path.join(dir, 'book.epub');
|
|
fs.writeFileSync(dest, Buffer.from([1, 2, 3]));
|
|
atomic.writeBytes(dest, Buffer.from([4, 5, 0, 255]));
|
|
assert.deepStrictEqual([...fs.readFileSync(dest)], [4, 5, 0, 255]);
|
|
assert.strictEqual(fs.existsSync(`${dest}.tmp`), false);
|
|
assert.strictEqual(fs.existsSync(`${dest}.bak`), false);
|
|
});
|
|
|
|
test('writeBytes 替换失败时恢复旧二进制内容', () => {
|
|
const dir = fresh('bytes-rollback');
|
|
const dest = path.join(dir, 'book.epub');
|
|
fs.writeFileSync(dest, Buffer.from([1, 2, 3]));
|
|
const realRename = fs.renameSync;
|
|
let failed = false;
|
|
fs.renameSync = function failing(from, to) {
|
|
if (!failed && String(from) === `${dest}.tmp` && String(to) === dest) {
|
|
failed = true;
|
|
throw new Error('模拟二进制替换失败');
|
|
}
|
|
return realRename.apply(this, arguments);
|
|
};
|
|
try {
|
|
assert.throws(() => atomic.writeBytes(dest, Buffer.from([9, 9])), /模拟二进制替换失败/);
|
|
} finally {
|
|
fs.renameSync = realRename;
|
|
}
|
|
assert.deepStrictEqual([...fs.readFileSync(dest)], [1, 2, 3]);
|
|
assert.strictEqual(fs.existsSync(`${dest}.tmp`), false);
|
|
assert.strictEqual(fs.existsSync(`${dest}.bak`), false);
|
|
});
|