Snapshot differential tests (known deviations registered, current state passes): - title/path recognition: 216 samples vs MP MetaInfo/MetaInfoPath - filter rules: 42 rules x 28 torrents vs MP FilterModule - RSS/indexer parsing: 15 feeds, 72 indexer samples - zhconv: 69 samples Generator: scripts/mp-sync/gen-differential.py; report: docs/porting/mp-rust-logic-parity.md Co-Authored-By: Claude Opus 5.5 <[email protected]>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
/**
|
|
* MP 差分测试共用工具:读取 scripts/mp-sync/gen-differential.py 生成的期望 JSON,登记偏差集合与实际偏差集合比对。
|
|
* 偏差 key:`<sampleId>.status` / `<sampleId>.count` / `<sampleId>#<行号>.<字段>`。
|
|
*/
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
|
export type DeviationKind = "intentional" | "missing" | "our_bug" | "mp_bug";
|
|
export interface Deviation {
|
|
kind: DeviationKind;
|
|
note: string;
|
|
/** 仅在条件成立时才出现的偏差;local_tz_not_utc = 宿主本地时区偏移非 0 */
|
|
condition?: "local_tz_not_utc";
|
|
}
|
|
|
|
function conditionHolds(d: Deviation): boolean {
|
|
if (d.condition === "local_tz_not_utc") {
|
|
return new Date("2026-09-26T00:00:00").getTimezoneOffset() !== 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export interface DifferentialDoc<S> {
|
|
kind: string;
|
|
generatedFrom: Record<string, string>;
|
|
fields: string[];
|
|
samples: S[];
|
|
deviations: Record<string, Deviation>;
|
|
}
|
|
|
|
export function loadDoc<S>(name: string): DifferentialDoc<S> {
|
|
return JSON.parse(readFileSync(join(HERE, name), "utf8")) as DifferentialDoc<S>;
|
|
}
|
|
|
|
export function readInput(rel: string): string {
|
|
return readFileSync(join(HERE, rel), "utf8");
|
|
}
|
|
|
|
export interface Mismatch {
|
|
key: string;
|
|
mp: unknown;
|
|
ours: unknown;
|
|
}
|
|
|
|
/** null / undefined 视为同一"缺省";其余按 JSON 结构比较 */
|
|
export function sameValue(a: unknown, b: unknown): boolean {
|
|
if (a == null && b == null) return true;
|
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
}
|
|
|
|
export class FieldCollector {
|
|
readonly mismatches: Mismatch[] = [];
|
|
compared = 0;
|
|
equal = 0;
|
|
check(key: string, mp: unknown, ours: unknown): void {
|
|
this.compared++;
|
|
if (sameValue(mp, ours)) this.equal++;
|
|
else this.mismatches.push({ key, mp, ours });
|
|
}
|
|
}
|
|
|
|
/** 实际不一致集合 vs 登记集合:返回两个方向的差集(均为空才通过)。 */
|
|
export function diffAgainstRegistry(
|
|
actual: Mismatch[],
|
|
registry: Record<string, Deviation>,
|
|
): { unregistered: Mismatch[]; stale: string[] } {
|
|
const actualKeys = new Set(actual.map((m) => m.key));
|
|
return {
|
|
unregistered: actual.filter((m) => !(m.key in registry)),
|
|
stale: Object.entries(registry)
|
|
.filter(([k, d]) => !actualKeys.has(k) && conditionHolds(d))
|
|
.map(([k]) => k),
|
|
};
|
|
}
|
|
|
|
/** 设置环境变量 DIFF_DUMP_DIR 时把实际不一致写成 <name>.mismatches.json(整理 deviations 用) */
|
|
export function dumpMismatches(name: string, ms: Mismatch[], stats?: { compared: number; equal: number }): void {
|
|
const dir = process.env.DIFF_DUMP_DIR;
|
|
if (dir) {
|
|
writeFileSync(join(dir, `${name}.mismatches.json`), `${JSON.stringify({ stats, mismatches: ms }, null, 2)}\n`);
|
|
}
|
|
}
|
|
|
|
export function formatMismatches(ms: Mismatch[]): string {
|
|
return ms.map((m) => `${m.key}\n mp: ${JSON.stringify(m.mp)}\n ours: ${JSON.stringify(m.ours)}`).join("\n");
|
|
}
|