85 行
3.6 KiB
JavaScript
85 行
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const TEST_FILE_PATTERN = /\.(?:spec|test)\.[cm]?[jt]sx?$/u;
|
|
|
|
export function fullLocalSpecFiles(root) {
|
|
const files = [];
|
|
function visit(directory) {
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
if (entry.isDirectory()) {
|
|
if (entry.name !== "manual") visit(path.join(directory, entry.name));
|
|
} else if (entry.isFile() && TEST_FILE_PATTERN.test(entry.name)) {
|
|
files.push(path.relative(root, path.join(directory, entry.name)).split(path.sep).join("/"));
|
|
} else if (entry.isSymbolicLink()) {
|
|
throw new Error("full_local_symlink_requires_explicit_ownership");
|
|
}
|
|
}
|
|
}
|
|
visit(path.join(root, "client/tests/e2e"));
|
|
return files.sort();
|
|
}
|
|
|
|
function sourceFile(root, file) {
|
|
if (typeof file !== "string" || !file) return "invalid_source";
|
|
let relative = path.isAbsolute(file) ? path.relative(root, file) : file;
|
|
relative = relative.replaceAll("\\", "/");
|
|
if (relative.startsWith("tests/e2e/")) relative = `client/${relative}`;
|
|
if (!relative.startsWith("client/tests/e2e/")) relative = `client/tests/e2e/${relative}`;
|
|
relative = path.posix.normalize(relative);
|
|
return relative.startsWith("client/tests/e2e/") ? relative : "invalid_source";
|
|
}
|
|
|
|
export function verifyFullLocalDiscovery(root, report) {
|
|
const expectedSpecs = fullLocalSpecFiles(root);
|
|
const cases = [];
|
|
const observed = new Set();
|
|
const issues = [];
|
|
function visit(suites, inheritedFile = "") {
|
|
for (const suite of Array.isArray(suites) ? suites : []) {
|
|
const suiteFile = suite.file ?? inheritedFile;
|
|
for (const spec of Array.isArray(suite.specs) ? suite.specs : []) {
|
|
const file = sourceFile(root, spec.file ?? suiteFile);
|
|
observed.add(file);
|
|
if (!Array.isArray(spec.tests) || !spec.tests.length) issues.push("spec_without_cases");
|
|
for (const test of Array.isArray(spec.tests) ? spec.tests : []) {
|
|
cases.push({ file, id: spec.id, title: spec.title, line: spec.line, projectName: test.projectName ?? "", repeatEachIndex: test.repeatEachIndex ?? 0 });
|
|
}
|
|
}
|
|
visit(suite.suites, suiteFile);
|
|
}
|
|
}
|
|
visit(report?.suites);
|
|
if (!Array.isArray(report?.suites) || !Array.isArray(report?.errors)) issues.push("invalid_discovery_report");
|
|
if (report?.errors?.length) issues.push("discovery_errors");
|
|
if (!expectedSpecs.length || !cases.length) issues.push("empty_discovery");
|
|
const missingSpecs = expectedSpecs.filter((file) => !observed.has(file));
|
|
const unexpectedSpecs = [...observed].filter((file) => !expectedSpecs.includes(file)).sort();
|
|
if (missingSpecs.length) issues.push("missing_specs");
|
|
if (unexpectedSpecs.length) issues.push("unexpected_specs");
|
|
return {
|
|
status: issues.length ? "FAIL" : "PASS",
|
|
kind: "FULL_LOCAL_DISCOVERY_ONLY",
|
|
expectedSpecs,
|
|
observedSpecs: [...observed].sort(),
|
|
missingSpecs,
|
|
unexpectedSpecs,
|
|
cases,
|
|
issues: [...new Set(issues)],
|
|
};
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
try {
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const summary = verifyFullLocalDiscovery(root, JSON.parse(readFileSync(process.argv[2], "utf8")));
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
process.exitCode = summary.status === "PASS" ? 0 : 1;
|
|
} catch {
|
|
console.error("[full-local-discovery] report_or_source_invalid");
|
|
process.exitCode = 1;
|
|
}
|
|
}
|