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]>
This commit is contained in:
1 parent
b86f75c011
commit
591cc78c7d
44 files changed
+32206
No files matched your search
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
Reference in new issue
Block a user