Files
hyperframes/packages/core/src/projects.test.ts
T
Miguel Ángel 6cb95d844d test(core): findProjects stop tests fit Windows' time budget and pin a stopped walk ending (#4653)
* test(core): findProjects stop tests build two projects, not four hundred files

* test(core): stop tests keep a folder queued at the stop, with room for Windows setup
2026-09-28 09:29:44 +00:00

239 lines
7.4 KiB
TypeScript

import {
chmodSync,
mkdirSync,
mkdtempSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, relative, sep } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { findProjects, type FoundProject } from "./projects";
const roots: string[] = [];
afterEach(() => {
vi.unstubAllEnvs();
for (const root of roots.splice(0)) {
chmodSync(root, 0o755);
rmSync(root, { recursive: true, force: true });
}
});
function tree(files: string[]): string {
const root = realpathSync.native(mkdtempSync(join(tmpdir(), "hf-find-projects-")));
roots.push(root);
for (const file of files) {
mkdirSync(join(root, file, ".."), { recursive: true });
writeFileSync(join(root, file), file.endsWith(".git") ? "gitdir: ../.git/modules/x" : "");
}
return root;
}
// One more project than the walk visits at once, so a folder is still queued when the search stops.
function queuedAtStop(): string {
return tree(Array.from({ length: 65 }, (_, i) => [`f${i}/index.html`, `f${i}/meta.json`]).flat());
}
async function find(root: string, spotlight: string[] = []): Promise<FoundProject[]> {
const found: FoundProject[] = [];
await findProjects({
root,
onProject: (project) => found.push(project),
spotlight: async () => spotlight.map((path) => join(root, path)),
});
return found;
}
const paths = (root: string, found: FoundProject[]) =>
found.map((project) => relative(root, project.path).split(sep).join("/")).sort();
describe("findProjects", () => {
it("finds a folder with index.html and a project marker, and nothing else", async () => {
const root = tree([
"film/index.html",
"film/meta.json",
"site/index.html",
"notes/project.json",
]);
const found = await find(root);
expect(paths(root, found)).toEqual(["film"]);
expect(found[0]).toMatchObject({ name: "film", source: "walk" });
expect(Date.parse(found[0]!.mtime)).toBeGreaterThan(0);
});
it("skips node_modules and hidden folders", async () => {
const root = tree([
"app/node_modules/pkg/index.html",
"app/node_modules/pkg/project.json",
".cache/film/index.html",
".cache/film/meta.json",
]);
expect(await find(root)).toEqual([]);
});
it("skips a git worktree copy but walks a submodule", async () => {
const root = tree([
"repo-copy/film/index.html",
"repo-copy/film/meta.json",
"vendor/sub/.git",
"vendor/sub/film/index.html",
"vendor/sub/film/hyperframes.json",
]);
writeFileSync(join(root, "repo-copy", ".git"), "gitdir: /src/repo/.git/worktrees/repo-copy\n");
expect(paths(root, await find(root))).toEqual(["vendor/sub/film"]);
});
it("skips Library in the home folder but not a folder named Library elsewhere", async () => {
const root = tree([
"Library/film/index.html",
"Library/film/meta.json",
"Movies/Library/film/index.html",
"Movies/Library/film/meta.json",
]);
vi.stubEnv("HOME", root + sep);
vi.stubEnv("USERPROFILE", root + sep);
const found = await find(root, ["Library/film/meta.json", "Movies/Library/film/meta.json"]);
expect(paths(root, found)).toEqual(["Movies/Library/film"]);
});
it("searches the home folder when no root is given", async () => {
const root = tree(["film/index.html", "film/meta.json"]);
vi.stubEnv("HOME", root);
vi.stubEnv("USERPROFILE", root);
const found: FoundProject[] = [];
await findProjects({ onProject: (project) => found.push(project), spotlight: async () => [] });
expect(paths(root, found)).toEqual(["film"]);
});
it("stops reporting and rejects once its signal is aborted", async () => {
const root = queuedAtStop();
const controller = new AbortController();
const found: FoundProject[] = [];
const search = findProjects({
root,
signal: controller.signal,
spotlight: async () => [],
onProject: (project) => {
found.push(project);
controller.abort();
},
});
await expect(search).rejects.toMatchObject({ name: "AbortError" });
expect(found).toHaveLength(1);
}, 20_000);
it("stops the search when onProject throws", async () => {
const root = queuedAtStop();
const onProject = vi.fn(() => {
throw new Error("list is closed");
});
await expect(findProjects({ root, onProject, spotlight: async () => [] })).rejects.toThrow(
"list is closed",
);
expect(onProject).toHaveBeenCalledTimes(1);
}, 20_000);
it("does not start when its signal is already aborted", async () => {
const onProject = vi.fn();
const search = findProjects({
root: join(tree([]), "missing"),
onProject,
signal: AbortSignal.abort(),
});
await expect(search).rejects.toMatchObject({ name: "AbortError" });
expect(onProject).not.toHaveBeenCalled();
});
it("does not look for projects inside a project", async () => {
const root = tree([
"film/index.html",
"film/hyperframes.json",
"film/compositions/intro/index.html",
"film/compositions/intro/meta.json",
]);
expect(paths(root, await find(root))).toEqual(["film"]);
});
it("searches a root that is itself a git worktree copy", async () => {
const root = tree(["film/index.html", "film/meta.json"]);
writeFileSync(join(root, ".git"), "gitdir: /src/repo/.git/worktrees/copy\n");
expect(paths(root, await find(root))).toEqual(["film"]);
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"finishes past a symlink loop and an unreadable folder",
async () => {
const root = tree([
"film/index.html",
"film/meta.json",
"locked/film/index.html",
"locked/film/meta.json",
]);
symlinkSync(root, join(root, "loop"), "dir");
chmodSync(join(root, "locked"), 0o000);
try {
expect(paths(root, await find(root))).toEqual(["film"]);
} finally {
chmodSync(join(root, "locked"), 0o755);
}
},
);
it("fails on a root that is missing or not a folder instead of finding nothing", async () => {
const root = tree(["file.txt"]);
await expect(find(join(root, "missing"))).rejects.toMatchObject({ code: "ENOENT" });
await expect(find(join(root, "file.txt"))).rejects.toMatchObject({ code: "ENOTDIR" });
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"fails on a root it may not read instead of finding nothing",
async () => {
const root = tree([]);
chmodSync(root, 0o000);
await expect(find(root)).rejects.toMatchObject({ code: "EACCES" });
},
);
it("reports a Spotlight hit once, and only where the walk would also count it", async () => {
const root = tree([
"film/index.html",
"film/meta.json",
"film/compositions/intro/index.html",
"film/compositions/intro/meta.json",
"app/node_modules/pkg/index.html",
"app/node_modules/pkg/project.json",
"repo-copy/film/index.html",
"repo-copy/film/meta.json",
"readme/meta.json",
]);
writeFileSync(join(root, "repo-copy", ".git"), "gitdir: /src/repo/.git/worktrees/repo-copy\n");
const found = await find(root, [
"film/meta.json",
"film/compositions/intro/meta.json",
"app/node_modules/pkg/project.json",
"repo-copy/film/meta.json",
"readme/meta.json",
"../elsewhere/meta.json",
]);
expect(paths(root, found)).toEqual(["film"]);
});
});