mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(test): reject excluded symlink aliases in exact selection
A matching realpath does not make an excluded symlink spelling a discovered test. Compare differing path components, rejecting symlink aliases while preserving ordinary case and Unicode filesystem identity. Validation: all 14 runner boundary tests pass, including excluded file/directory aliases, valid selections and no target execution. Based on #2849. Co-authored-by: ShuaixinHuang <1836656514@qq.com>
This commit is contained in:
1 parent
ef153774f7
commit
b782064d54
2 files changed
+75
-6
No files matched your search
@@ -77,7 +77,7 @@ try {
|
||||
throw cause;
|
||||
}
|
||||
if (
|
||||
!(await hasCaseMismatchedSymlinkComponent(absolute, candidate)) &&
|
||||
!(await hasMismatchedSymlinkComponent(absolute, candidate)) &&
|
||||
(await realpath(candidate)) === canonical
|
||||
)
|
||||
matches = [...matches, ...specifications];
|
||||
@@ -99,13 +99,14 @@ try {
|
||||
if (shutdownSignal !== undefined)
|
||||
process.exitCode = signalExitCodes[shutdownSignal];
|
||||
|
||||
async function hasCaseMismatchedSymlinkComponent(selected, candidate) {
|
||||
async function hasMismatchedSymlinkComponent(selected, candidate) {
|
||||
const selectedAbsolute = resolve(selected);
|
||||
const candidateAbsolute = resolve(candidate);
|
||||
const root = parse(selectedAbsolute).root;
|
||||
if (root !== parse(candidateAbsolute).root) return true;
|
||||
const selectedParts = relative(root, selectedAbsolute).split(sep);
|
||||
const candidateParts = relative(root, candidateAbsolute).split(sep);
|
||||
if (selectedParts.length !== candidateParts.length) return true;
|
||||
let selectedPath = root;
|
||||
let candidatePath = root;
|
||||
for (let index = 0; index < selectedParts.length; index += 1) {
|
||||
@@ -118,14 +119,12 @@ async function hasCaseMismatchedSymlinkComponent(selected, candidate) {
|
||||
candidatePath = candidatePath
|
||||
? resolve(candidatePath, candidatePart)
|
||||
: candidatePart;
|
||||
if (
|
||||
selectedPart !== candidatePart &&
|
||||
selectedPart.toLowerCase() === candidatePart.toLowerCase()
|
||||
) {
|
||||
if (selectedPart !== candidatePart) {
|
||||
const [selectedEntry, candidateEntry] = await Promise.all([
|
||||
lstat(selectedPath),
|
||||
lstat(candidatePath),
|
||||
]);
|
||||
// Matching inode/realpath does not make an excluded symlink a discovered path.
|
||||
if (selectedEntry.isSymbolicLink() || candidateEntry.isSymbolicLink())
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,76 @@ const testCommand = (root: string, ...arguments_: string[]) =>
|
||||
{ ...process.env, NO_COLOR: "1" },
|
||||
);
|
||||
|
||||
describe("development test runner symlink aliases", () => {
|
||||
it("rejects excluded file and directory aliases without running their targets", async ({
|
||||
skip,
|
||||
}) => {
|
||||
for (const { kind, mode } of [
|
||||
{ kind: "file", mode: "focused" },
|
||||
{ kind: "directory", mode: "local" },
|
||||
{ kind: "directory", mode: "focused" },
|
||||
] as const) {
|
||||
const root = await fixture();
|
||||
const included =
|
||||
kind === "file" ? join(root, "src") : join(root, "src", "domain");
|
||||
const includedTest = join(included, "value.test.ts");
|
||||
const alias =
|
||||
kind === "file"
|
||||
? join(root, "src", "excluded.test.ts")
|
||||
: join(root, "src", "contracts");
|
||||
if (kind === "directory") {
|
||||
await mkdir(included);
|
||||
for (const name of ["value.ts", "value.test.ts"])
|
||||
await copyFile(join(root, "src", name), join(included, name));
|
||||
}
|
||||
try {
|
||||
await symlink(
|
||||
kind === "file" ? includedTest : included,
|
||||
alias,
|
||||
kind === "directory"
|
||||
? process.platform === "win32"
|
||||
? "junction"
|
||||
: "dir"
|
||||
: undefined,
|
||||
);
|
||||
} catch (cause) {
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
cause instanceof Error &&
|
||||
"code" in cause &&
|
||||
(cause.code === "EPERM" || cause.code === "EACCES")
|
||||
)
|
||||
skip("Windows symlink creation requires unavailable privileges");
|
||||
throw cause;
|
||||
}
|
||||
const selectedAlias =
|
||||
kind === "file"
|
||||
? "src/excluded.test.ts"
|
||||
: "src/contracts/value.test.ts";
|
||||
const includedSelection =
|
||||
kind === "file" ? "src/value.test.ts" : "src/domain/value.test.ts";
|
||||
await writeFile(
|
||||
join(root, "vitest.config.mjs"),
|
||||
`export default { test: { projects: [{ test: { name: "domain", include: ["${includedSelection}"] } }] } };\n`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
testCommand(root, mode, selectedAlias),
|
||||
).rejects.toMatchObject({
|
||||
code: 1,
|
||||
stderr: expect.stringContaining(
|
||||
`Selected test files were not discovered: ${selectedAlias}`,
|
||||
),
|
||||
});
|
||||
await expect(
|
||||
readFile(join(root, "selected.txt"), "utf8"),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await testCommand(root, mode, includedSelection);
|
||||
expect(await readFile(join(root, "selected.txt"), "utf8")).toBe("1");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("development test runner exact selection", () => {
|
||||
it("matches selected path casing to the host filesystem", async () => {
|
||||
const root = await fixture();
|
||||
|
||||
Reference in new issue
Block a user