Files
rootandClaude Opus 5.5 591cc78c7d test(porting): differential tests against MoviePilot Rust-accelerated parsers
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]>
2026-09-27 22:44:11 +08:00

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");
}