背景(PLAN-v0.7.5.md 根因 5):项目声称的崩溃语义一直没有被真实验证。
e2e 里的 `crashPage()` 实际是:
win.__ms = null; await page.close();
—— 那是**优雅关闭**:没有未完成 I/O、不经过任何崩溃窗口。于是
`checkpointInterval: 999999999` + 不 close 的"崩溃恢复"用例,测的其实是
"正常关闭后重开"。CI 注释却写着"e2e 覆盖真实崩溃注入(CDP Page.crash)"。
改动:
1. `crashPage()` 改用 CDP `Page.crash` 终止渲染进程(进行中的 OPFS 写入、
未 flush 的缓冲、同步句柄全部立即消失)。实测要点:
- `Page.crash` 之后 `page.isClosed()` 仍为 false,不能用它判成败;
- OPFS 数据**在同一个 context 内**跨崩溃保留(新建 context 是另一份存储),
因此崩溃后新开页面即可继续验证。
2. harness 增加两类真崩溃注入:
- `insertNoAwait`:发起写入但不等待(`page.evaluate` 会等 Promise,
因此"写到一半就崩"无法用普通 await 表达);
- `armCrashOnOpfsWrite({ phase })`:包装 `FileSystemWritableFileStream`
的 `write`/`close`,在第 n 次调用处进入死循环,随后被 Page.crash 杀掉 ——
对应 copy-on-write 的两个真实窗口。**注**:OPFS 用的是 createWritable,
不是 `FileSystemSyncAccessHandle`(后者主线程不可用,实测)。
3. 新增两个用例覆盖上述窗口,并断言钩子**确实装上**(armed === true),
避免"注入了但没走到"的假绿:
- write 阶段崩溃 → 已确认数据完好;
- commit 阶段崩溃 → 3 条旧数据**完全不变**(copy-on-write 的核心不变量:
未 commit 就崩溃不能出现半个文件)。
4. 原"写入后强制终止"用例加逐条抽查(0/1/24/25/49)—— 只断言计数会被
"重复主键覆盖后计数恰好相等"蒙对。
5. CI 注释改为准确描述实际覆盖的窗口,并点明 e2e 依赖 dist/ 产物。
验证:14 项 e2e 全绿(8.9s,真实 Chromium + 真实 OPFS);
jest 全量 85 套件 / 1665 测试通过。
165 lines
6.3 KiB
HTML
165 lines
6.3 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>MetonaSqlark E2E Harness</title>
|
||
</head>
|
||
<body>
|
||
<h1>MetonaSqlark E2E Harness</h1>
|
||
<div id="status">loading</div>
|
||
<script src="/dist/metona-sqlark.js"></script>
|
||
<script>
|
||
// E2E 驱动器:暴露全局 API,测试通过 page.evaluate 调用
|
||
// 使用高层 API(MetonaSqlark.create),保证 e2e 覆盖真实入口路径
|
||
window.__ms = {
|
||
version: () => (window.MetonaSqlark ? window.MetonaSqlark.VERSION : null),
|
||
|
||
/**
|
||
* 创建并打开 Aria 库。
|
||
* opts: {
|
||
* name, diskEngine ('opfs'|'memory'),
|
||
* walSyncMode, checkpointInterval,
|
||
* encryptionPassword, memtableSizeThreshold
|
||
* }
|
||
*/
|
||
open: async (opts) => {
|
||
// opts.mode: 'aria'(默认)| 'disk'(KVStoreEngine)
|
||
const config = {
|
||
name: opts.name,
|
||
mode: opts.mode || 'aria',
|
||
diskEngine: opts.diskEngine || 'opfs',
|
||
};
|
||
if (opts.mode !== 'disk') {
|
||
config.aria = {
|
||
walSyncMode: opts.walSyncMode || 'full',
|
||
checkpointInterval: opts.checkpointInterval || 100000,
|
||
memtableSizeThreshold: opts.memtableSizeThreshold || 64 * 1024 * 1024,
|
||
...(opts.encryptionPassword ? { encryption: { password: opts.encryptionPassword } } : {}),
|
||
};
|
||
}
|
||
const db = await window.MetonaSqlark.create(config);
|
||
window.__ms.__db = db;
|
||
return { ok: true, engineName: db.getEngine().name };
|
||
},
|
||
|
||
createTable: async (opts) => {
|
||
const db = window.__ms.__db;
|
||
const columns = {};
|
||
for (const col of opts.columns) {
|
||
columns[col.name] = { type: col.type, primaryKey: !!col.primaryKey, index: !!col.index };
|
||
}
|
||
await db.defineTable(opts.table, columns);
|
||
return { ok: true };
|
||
},
|
||
|
||
insert: async (opts) => {
|
||
const db = window.__ms.__db;
|
||
const pks = await db.table(opts.table).insertMany(opts.rows);
|
||
return { pks };
|
||
},
|
||
|
||
count: async (opts) => {
|
||
const db = window.__ms.__db;
|
||
return { count: await db.table(opts.table).count() };
|
||
},
|
||
|
||
find: async (opts) => {
|
||
const db = window.__ms.__db;
|
||
const rows = await db.table(opts.table).select().where(opts.where || {}).execute();
|
||
return { rows };
|
||
},
|
||
|
||
query: async (opts) => {
|
||
const db = window.__ms.__db;
|
||
return { rows: await db.query(opts.sql) };
|
||
},
|
||
|
||
repair: async () => {
|
||
const db = window.__ms.__db;
|
||
await db.repair();
|
||
return { ok: true };
|
||
},
|
||
|
||
clearAll: async () => {
|
||
const db = window.__ms.__db;
|
||
await db.clearAll();
|
||
return { ok: true };
|
||
},
|
||
|
||
close: async () => {
|
||
const db = window.__ms.__db;
|
||
await db.close();
|
||
window.__ms.__db = null;
|
||
return { ok: true };
|
||
},
|
||
|
||
getEngine: () => (window.__ms.__db ? window.__ms.__db.getEngine() : null),
|
||
|
||
/**
|
||
* v0.8.0(PC-2):**不等写入完成**就返回 —— 供测试在写入进行中触发真崩溃。
|
||
*
|
||
* `page.evaluate` 会等待返回的 Promise,因此"发起写入后立刻崩溃"无法用
|
||
* 普通 await 表达。这里把 Promise 挂到 `__ms.__pending` 后立即返回,
|
||
* 页面在写入尚未落盘时被杀 → 真正覆盖"WAL 半写 / OPFS 写入中途"窗口。
|
||
*/
|
||
insertNoAwait: (opts) => {
|
||
const db = window.__ms.__db;
|
||
// 故意不 await:这就是"崩溃发生在写入进行中"的语义
|
||
window.__ms.__pending = db.table(opts.table).insertMany(opts.rows);
|
||
window.__ms.__pending.catch(() => {}); // 避免未处理拒绝噪音(进程即将被杀)
|
||
return { started: true };
|
||
},
|
||
|
||
/** 未完成写入的数量(诊断用) */
|
||
hasPendingWrite: () => ({ pending: !!window.__ms.__pending }),
|
||
|
||
/**
|
||
* v0.8.0(PC-2):在"下一次 OPFS 写入"处安装崩溃钩子。
|
||
*
|
||
* Aria 的 OPFS 后端用 `FileSystemFileHandle.createWritable()`(copy-on-write):
|
||
* 数据先写入交换副本,`close()` 时才原子替换原文件。因此"写到一半崩溃"
|
||
* 有两种真正不同的窗口,都要能注入:
|
||
* - `phase: 'write'` —— 数据写入交换副本的**中途**(`write()` 被调用时卡死);
|
||
* - `phase: 'commit'` —— 数据已全部写入交换副本、但**尚未**原子替换
|
||
* (`close()` 被调用时卡死)。这个窗口最关键:此时崩溃必须保证
|
||
* **原文件保持旧内容**(不能出现半个文件)。
|
||
*
|
||
* 卡死方式:进入永不返回的同步死循环,随后由 CDP `Page.crash` 终止进程 ——
|
||
* 等价于"浏览器在写的瞬间被强杀"。
|
||
*
|
||
* 返回 `{ armed, target, phase }`,测试据此断言钩子确实安装成功
|
||
*(避免"注入了但没走到"的假绿)。
|
||
*/
|
||
armCrashOnOpfsWrite: (opts) => {
|
||
const phase = (opts && opts.phase) || 'write';
|
||
// n 缺省/0 → 第 1 次就崩
|
||
const target = Math.max(1, Number(opts && opts.n) || 1);
|
||
const proto = window.FileSystemWritableFileStream && window.FileSystemWritableFileStream.prototype;
|
||
if (!proto) return { armed: false, reason: 'FileSystemWritableFileStream unavailable' };
|
||
if (window.__ms.__crashArmed) return { armed: true, alreadyArmed: true, phase };
|
||
window.__ms.__crashArmed = true;
|
||
window.__ms.__crashCount = 0;
|
||
|
||
const spin = () => { while (true) { /* crash window: Page.crash 在此终止进程 */ } };
|
||
const wrap = (name) => {
|
||
const original = proto[name];
|
||
if (typeof original !== 'function') return false;
|
||
proto[name] = function patched(...args) {
|
||
window.__ms.__crashCount++;
|
||
if (window.__ms.__crashCount >= target) spin();
|
||
return original.apply(this, args);
|
||
};
|
||
return true;
|
||
};
|
||
const hooked = phase === 'commit' ? wrap('close') : wrap('write');
|
||
return { armed: hooked, target, phase };
|
||
},
|
||
|
||
/** 已发生的目标 OPFS 调用次数(验证崩溃窗口确实被触发) */
|
||
opfsWriteCount: () => ({ count: window.__ms.__crashCount || 0 }),
|
||
};
|
||
document.getElementById('status').textContent = 'ready';
|
||
</script>
|
||
</body>
|
||
</html>
|