93 lines
2.8 KiB
JavaScript
93 lines
2.8 KiB
JavaScript
/**
|
||||
|
|
* front matter 解析器回归测试:拿仓库里全部真实文章跑无损往返。
|
|||
|
|
*
|
|||
|
|
* node test/frontmatter-roundtrip.mjs [contentDir]
|
|||
|
|
*
|
|||
|
|
* 断言三件事:
|
|||
|
|
* 1. split → join 能**逐字节**还原原文件(说明切分没吃掉任何字符)
|
|||
|
|
* 2. parse → stringify → parse 后对象与首次解析**深相等**(说明丢了不信息)
|
|||
|
|
* 3. 记录所有解析失败的样本,便于判断是否需要补语法
|
|||
|
|
*/
|
|||
|
|
import fs from 'node:fs';
|
|||
|
|
import path from 'node:path';
|
|||
|
|
import { splitFrontMatter, joinFrontMatter, parse, stringify, deepEqual } from '../src/frontmatter.mjs';
|
|||
|
|
|
|||
|
|
const root = process.argv[2] || path.resolve(import.meta.dirname, '../../content/posts');
|
|||
|
|
|
|||
|
|
function walk(dir, out = []) {
|
|||
|
|
for (const name of fs.readdirSync(dir)) {
|
|||
|
|
const p = path.join(dir, name);
|
|||
|
|
const st = fs.statSync(p);
|
|||
|
|
if (st.isDirectory()) walk(p, out);
|
|||
|
|
else if (name.endsWith('.md')) out.push(p);
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const files = walk(root);
|
|||
|
|
let ok = 0;
|
|||
|
|
const byteFail = [];
|
|||
|
|
const parseFail = [];
|
|||
|
|
const roundFail = [];
|
|||
|
|
|
|||
|
|
for (const f of files) {
|
|||
|
|
const text = fs.readFileSync(f, 'utf8');
|
|||
|
|
const rel = path.relative(root, f);
|
|||
|
|
|
|||
|
|
const { raw, body, eol } = splitFrontMatter(text);
|
|||
|
|
if (raw == null) {
|
|||
|
|
parseFail.push([rel, '没有 front matter']);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. 逐字节还原(含原始换行风格)
|
|||
|
|
if (joinFrontMatter(raw, body, eol) !== text) byteFail.push(rel);
|
|||
|
|
|
|||
|
|
// 2. 解析 + 往返
|
|||
|
|
let first;
|
|||
|
|
try {
|
|||
|
|
first = parse(raw);
|
|||
|
|
} catch (e) {
|
|||
|
|
parseFail.push([rel, e.message]);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let second;
|
|||
|
|
try {
|
|||
|
|
second = parse(stringify(first));
|
|||
|
|
} catch (e) {
|
|||
|
|
roundFail.push([rel, 'stringify/parse 失败: ' + e.message]);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!deepEqual(first, second)) {
|
|||
|
|
const diff = Object.keys(first).concat(Object.keys(second)).filter(
|
|||
|
|
(k, i, a) => a.indexOf(k) === i && !deepEqual(first[k], second[k]),
|
|||
|
|
);
|
|||
|
|
roundFail.push([rel, '字段不一致: ' + diff.join(', ')]);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ok++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const total = files.length;
|
|||
|
|
console.log('样本总数 :', total);
|
|||
|
|
console.log('解析 + 往返通过 :', ok);
|
|||
|
|
console.log('切分不还原 :', byteFail.length);
|
|||
|
|
console.log('解析失败 :', parseFail.length);
|
|||
|
|
console.log('往返不一致 :', roundFail.length);
|
|||
|
|
|
|||
|
|
const show = (title, arr) => {
|
|||
|
|
if (!arr.length) return;
|
|||
|
|
console.log('\n--- ' + title + ' ---');
|
|||
|
|
for (const [f, why] of arr.slice(0, 12)) console.log(' ' + f + ' || ' + why);
|
|||
|
|
if (arr.length > 12) console.log(' … 另有 ' + (arr.length - 12) + ' 条');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
show('切分不还原(必须为 0)', byteFail.map((f) => [f, 'join != 原文']));
|
|||
|
|
show('解析失败', parseFail);
|
|||
|
|
show('往返不一致', roundFail);
|
|||
|
|
|
|||
|
|
const bad = byteFail.length + parseFail.length + roundFail.length;
|
|||
|
|
process.exit(bad === 0 ? 0 : 1);
|