chore: import upstream snapshot with attribution
Python CI / Python bindings (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 1s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 0s
Rust CI / cargo clippy (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 0s
Rust CI / cargo fmt (push) Failing after 0s
Stylua / Check lua files using Stylua (push) Failing after 1s
Lua CI / lua-language-server type check (push) Failing after 12m29s
e2e Tests / e2e (alpine-musl) (push) Successful in 57m31s
e2e Tests / e2e (macos-latest) (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Rust CI / Test (windows-latest) (push) Has been cancelled
e2e Tests / e2e (windows-latest) (push) Has been cancelled
Nix CI / check (push) Has been cancelled
Python CI / Python bindings (macos-latest) (push) Has been cancelled
Python CI / Python bindings (windows-latest) (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
Rust CI / Fuzz Tests (windows-latest) (push) Has been cancelled
Rust CI / Test (macos-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (macos-latest) (push) Has been cancelled
Python CI / Python bindings (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 1s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 0s
Rust CI / cargo clippy (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 0s
Rust CI / cargo fmt (push) Failing after 0s
Stylua / Check lua files using Stylua (push) Failing after 1s
Lua CI / lua-language-server type check (push) Failing after 12m29s
e2e Tests / e2e (alpine-musl) (push) Successful in 57m31s
e2e Tests / e2e (macos-latest) (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Rust CI / Test (windows-latest) (push) Has been cancelled
e2e Tests / e2e (windows-latest) (push) Has been cancelled
Nix CI / check (push) Has been cancelled
Python CI / Python bindings (macos-latest) (push) Has been cancelled
Python CI / Python bindings (windows-latest) (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
Rust CI / Fuzz Tests (windows-latest) (push) Has been cancelled
Rust CI / Test (macos-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (macos-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { FileFinder } from "@ff-labs/fff-node";
|
||||
|
||||
const finder = FileFinder.create({ basePath: "~/dev/linux-root" });
|
||||
if (!finder.ok) {
|
||||
throw new Error(`Failed to create FileFinder: ${finder.error}`);
|
||||
}
|
||||
|
||||
const scan = await finder.value.waitForScan(5_000);
|
||||
if (!scan.ok) {
|
||||
throw new Error(`Failed to wait for scan: ${scan.error}`);
|
||||
}
|
||||
|
||||
// 5ms on m1 mac at the linux kernel repo root
|
||||
const result = finder.value.grep("hardwre", { mode: "plain" });
|
||||
for (const match of result.value.items) {
|
||||
console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
||||
}
|
||||
|
||||
console.log(`\n${result.value.totalMatched} matches across ${result.value.totalFilesSearched} files`);
|
||||
|
||||
finder.value.destroy();
|
||||
@@ -0,0 +1,353 @@
|
||||
import { after, before, describe, it } from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FileFinder } from "../dist/src/index.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(__dirname, "..", "..", "..");
|
||||
const normalizePath = (p) => p.replace(/\\/g, "/");
|
||||
|
||||
/** @type {import("../dist/src/finder.js").FileFinder | null} */
|
||||
let finder = null;
|
||||
|
||||
describe("fff-node", { concurrency: 1 }, () => {
|
||||
before(async () => {
|
||||
const result = FileFinder.create({ basePath: REPO_ROOT });
|
||||
assert.ok(result.ok, `create failed: ${!result.ok ? result.error : ""}`);
|
||||
finder = result.value;
|
||||
|
||||
const wait = await finder.waitForScan(5_000);
|
||||
if (!wait.ok) {
|
||||
assert.ok(wait.ok, `waitForScan failed: ${wait.error}`);
|
||||
}
|
||||
|
||||
assert.equal(wait.value, true, "scan should finish within 5s");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (finder && !finder.isDestroyed) {
|
||||
finder.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it("isAvailable returns true when the native library is loadable", () => {
|
||||
assert.equal(FileFinder.isAvailable(), true);
|
||||
});
|
||||
|
||||
it("getScanProgress reports >100 indexed files", () => {
|
||||
const r = finder.getScanProgress();
|
||||
assert.ok(r.ok, "getScanProgress failed");
|
||||
assert.equal(typeof r.value.scannedFilesCount, "number");
|
||||
assert.ok(
|
||||
r.value.scannedFilesCount > 100,
|
||||
`expected >100, got ${r.value.scannedFilesCount}`,
|
||||
);
|
||||
|
||||
assert.equal(r.value.isScanning, false);
|
||||
});
|
||||
|
||||
describe("search", { concurrency: 1 }, () => {
|
||||
it("finds the root Cargo.toml", () => {
|
||||
const r = finder.fileSearch("Cargo.toml", { pageSize: 10 });
|
||||
assert.ok(r.ok, `search failed: ${!r.ok ? r.error : ""}`);
|
||||
const paths = r.value.items.map((i) => i.relativePath);
|
||||
assert.ok(
|
||||
paths.includes("Cargo.toml"),
|
||||
`expected Cargo.toml in results, got: ${paths}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("finds crates/fff-c/src/lib.rs", () => {
|
||||
const r = finder.fileSearch("lib.rs", { pageSize: 20 });
|
||||
assert.ok(r.ok);
|
||||
const paths = r.value.items.map((i) => i.relativePath);
|
||||
assert.ok(
|
||||
paths.some((p) => p.includes("fff-c") && p.endsWith("lib.rs")),
|
||||
`expected fff-c/src/lib.rs, got: ${paths}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("respects pageSize", () => {
|
||||
const r = finder.fileSearch("lua", { pageSize: 3 });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(
|
||||
r.value.items.length <= 3,
|
||||
`expected <=3, got ${r.value.items.length}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("items contain all required fields", () => {
|
||||
const r = finder.fileSearch("Cargo.toml", { pageSize: 1 });
|
||||
assert.ok(r.ok);
|
||||
const item = r.value.items[0];
|
||||
for (const f of ["relativePath", "fileName", "gitStatus"]) {
|
||||
assert.equal(typeof item[f], "string", `${f} should be a string`);
|
||||
}
|
||||
for (const f of ["size", "modified"]) {
|
||||
assert.equal(typeof item[f], "number", `${f} should be a number`);
|
||||
}
|
||||
});
|
||||
|
||||
it("scores contain all required fields", () => {
|
||||
const r = finder.fileSearch("Cargo.toml", { pageSize: 1 });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(r.value.scores.length > 0);
|
||||
const s = r.value.scores[0];
|
||||
for (const f of ["total", "baseScore"]) {
|
||||
assert.equal(typeof s[f], "number", `${f} should be a number`);
|
||||
}
|
||||
assert.equal(typeof s.matchType, "string");
|
||||
});
|
||||
|
||||
it("totalMatched <= totalFiles", () => {
|
||||
const r = finder.fileSearch("rs", { pageSize: 1 });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(r.value.totalMatched > 0);
|
||||
assert.ok(r.value.totalFiles > 100);
|
||||
assert.ok(r.value.totalFiles >= r.value.totalMatched);
|
||||
});
|
||||
|
||||
it("empty query returns files", () => {
|
||||
const r = finder.fileSearch("", { pageSize: 5 });
|
||||
assert.ok(r.ok, `empty query failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value.totalFiles, "number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("glob", { concurrency: 1 }, () => {
|
||||
it("filters by extension via raw glob pattern", () => {
|
||||
const r = finder.glob("**/*.rs", { pageSize: 50 });
|
||||
assert.ok(r.ok, `glob failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.ok(r.value.items.length > 0, "expected at least one .rs file");
|
||||
for (const item of r.value.items) {
|
||||
assert.ok(
|
||||
item.relativePath.endsWith(".rs"),
|
||||
`unexpected file: ${item.relativePath}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns empty result for non-matching pattern", () => {
|
||||
const r = finder.glob("**/this-extension-does-not-exist-anywhere.zzz");
|
||||
assert.ok(r.ok);
|
||||
assert.equal(r.value.items.length, 0);
|
||||
});
|
||||
|
||||
it("rejects empty pattern", () => {
|
||||
const r = finder.glob("");
|
||||
assert.equal(r.ok, false);
|
||||
});
|
||||
|
||||
it("respects pageSize", () => {
|
||||
const r = finder.glob("**/*.rs", { pageSize: 3 });
|
||||
assert.ok(r.ok);
|
||||
assert.ok(r.value.items.length <= 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("grep", { concurrency: 1 }, () => {
|
||||
it("finds FffResult in Rust sources", () => {
|
||||
// Constrain to .rs files so the assertion doesn't depend on result ordering
|
||||
// or content-indexing timing for other file types.
|
||||
const rustResults = finder.grep("*.rs FffResult", { mode: "plain" });
|
||||
assert.ok(
|
||||
rustResults.ok,
|
||||
`grep failed: ${!rustResults.ok ? rustResults.error : ""}`,
|
||||
);
|
||||
assert.ok(
|
||||
rustResults.value.items.length > 0,
|
||||
"expected at least one .rs match",
|
||||
);
|
||||
assert.ok(
|
||||
rustResults.value.items.some((m) => m.relativePath.endsWith(".rs")),
|
||||
);
|
||||
|
||||
const cResults = finder.grep("!**/*.{js,ts,rs} FffResult", {
|
||||
mode: "plain",
|
||||
});
|
||||
assert.ok(
|
||||
cResults.ok,
|
||||
`grep failed: ${!cResults.ok ? cResults.error : ""}`,
|
||||
);
|
||||
assert.ok(
|
||||
cResults.value.items.length > 0,
|
||||
"expected at least one non-js/ts/rs match",
|
||||
);
|
||||
assert.ok(
|
||||
cResults.value.items.some((m) => m.relativePath.endsWith(".h")),
|
||||
);
|
||||
});
|
||||
|
||||
it("match items contain all required fields", () => {
|
||||
const r = finder.grep("FffResult", { mode: "plain" });
|
||||
assert.ok(r.ok);
|
||||
const m = r.value.items[0];
|
||||
assert.equal(typeof m.relativePath, "string");
|
||||
assert.equal(typeof m.lineNumber, "number");
|
||||
assert.ok(m.lineNumber > 0, "lineNumber is 1-based");
|
||||
assert.equal(typeof m.lineContent, "string");
|
||||
assert.ok(m.lineContent.includes("FffResult"));
|
||||
assert.equal(typeof m.col, "number");
|
||||
assert.equal(typeof m.byteOffset, "number");
|
||||
assert.ok(Array.isArray(m.matchRanges));
|
||||
console.log(m.matchRanges);
|
||||
});
|
||||
|
||||
it("pagination returns a second page", () => {
|
||||
const r = finder.grep("fn", { mode: "plain" });
|
||||
assert.ok(r.ok);
|
||||
if (r.value.nextCursor) {
|
||||
const r2 = finder.grep("fn", { cursor: r.value.nextCursor });
|
||||
assert.ok(r2.ok, `page 2 failed: ${!r2.ok ? r2.error : ""}`);
|
||||
assert.equal(typeof r2.value.totalMatched, "number");
|
||||
}
|
||||
});
|
||||
|
||||
it("respects pageSize", () => {
|
||||
// Cap to one match per file so pageSize bounds the total deterministically.
|
||||
const unbounded = finder.grep("fn", {
|
||||
mode: "plain",
|
||||
maxMatchesPerFile: 1,
|
||||
});
|
||||
assert.ok(
|
||||
unbounded.ok,
|
||||
`grep failed: ${!unbounded.ok ? unbounded.error : ""}`,
|
||||
);
|
||||
assert.ok(unbounded.value.items.length > 2);
|
||||
|
||||
const limited = finder.grep("fn", {
|
||||
mode: "plain",
|
||||
maxMatchesPerFile: 1,
|
||||
pageSize: 2,
|
||||
});
|
||||
assert.ok(limited.ok, `grep failed: ${!limited.ok ? limited.error : ""}`);
|
||||
assert.ok(
|
||||
limited.value.items.length <= 2,
|
||||
`expected <=2 items, got ${limited.value.items.length}`,
|
||||
);
|
||||
assert.ok(
|
||||
limited.value.items.length < unbounded.value.items.length,
|
||||
"limited page should yield fewer matches than the default",
|
||||
);
|
||||
assert.ok(limited.value.nextCursor !== null, "nextCursor should be set");
|
||||
});
|
||||
|
||||
it("regex mode matches pub fn declarations", () => {
|
||||
const r = finder.grep("pub fn \\w+", { mode: "regex" });
|
||||
assert.ok(r.ok, `regex grep failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.ok(r.value.items.length > 0);
|
||||
});
|
||||
|
||||
it("decodes before/after context lines", () => {
|
||||
const r = finder.grep(
|
||||
"LOLLOWOIEJIWOIUOIWUIWUIOUWE", // the random text visible here
|
||||
{
|
||||
mode: "plain",
|
||||
beforeContext: 1,
|
||||
afterContext: 1,
|
||||
maxMatchesPerFile: 5,
|
||||
},
|
||||
);
|
||||
assert.ok(r.ok, `grep with context failed: ${!r.ok ? r.error : ""}`);
|
||||
|
||||
const match = r.value.items.find(
|
||||
(m) =>
|
||||
normalizePath(m.relativePath) === "packages/fff-node/test/e2e.mjs",
|
||||
);
|
||||
assert.ok(
|
||||
match,
|
||||
`expected a single match in the codebase, got: ${r.value.items
|
||||
.map((m) => normalizePath(m.relativePath))
|
||||
.join(", ")}`,
|
||||
);
|
||||
assert.deepEqual(match.contextBefore, [" const r = finder.grep("]);
|
||||
assert.deepEqual(match.contextAfter, [" {"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiGrep", { concurrency: 1 }, () => {
|
||||
it("finds lines matching any of the C FFI function names", () => {
|
||||
const r = finder.multiGrep({
|
||||
patterns: ["fff_create_instance", "fff_destroy", "fff_search"],
|
||||
});
|
||||
assert.ok(r.ok, `multiGrep failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.ok(r.value.items.length > 0);
|
||||
});
|
||||
|
||||
it("rejects empty patterns array", () => {
|
||||
const r = finder.multiGrep({ patterns: [] });
|
||||
assert.ok(!r.ok);
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshGitStatus returns a positive count", () => {
|
||||
const r = finder.refreshGitStatus();
|
||||
assert.ok(r.ok, `refreshGitStatus failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value, "number");
|
||||
assert.ok(r.value > 0);
|
||||
});
|
||||
|
||||
it("isScanning returns a boolean", () => {
|
||||
assert.equal(typeof finder.isScanning(), "boolean");
|
||||
});
|
||||
|
||||
describe("healthCheck", { concurrency: 1 }, () => {
|
||||
it("reports initialized state with instance", () => {
|
||||
const r = finder.healthCheck(REPO_ROOT);
|
||||
assert.ok(r.ok, `healthCheck failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value.version, "string");
|
||||
assert.ok(r.value.version.length > 0);
|
||||
assert.equal(r.value.git.available, true);
|
||||
assert.equal(r.value.git.repositoryFound, true);
|
||||
assert.equal(r.value.filePicker.initialized, true);
|
||||
assert.ok(r.value.filePicker.indexedFiles > 100);
|
||||
});
|
||||
|
||||
it("static check works without instance", () => {
|
||||
const r = FileFinder.healthCheckStatic(REPO_ROOT);
|
||||
assert.ok(r.ok, `static healthCheck failed: ${!r.ok ? r.error : ""}`);
|
||||
assert.equal(typeof r.value.version, "string");
|
||||
assert.equal(r.value.filePicker.initialized, false);
|
||||
assert.equal(r.value.git.repositoryFound, true);
|
||||
});
|
||||
});
|
||||
|
||||
it("create rejects a non-existent path", () => {
|
||||
const r = FileFinder.create({ basePath: "/nonexistent/fff-test-path" });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(
|
||||
r.error.toLowerCase().includes("invalid path") ||
|
||||
r.error.toLowerCase().includes("not exist"),
|
||||
`unexpected error: ${r.error}`,
|
||||
);
|
||||
});
|
||||
|
||||
// This must be the last describe block since it destroys the shared instance.
|
||||
describe("after destroy", { concurrency: 1 }, () => {
|
||||
before(() => {
|
||||
finder.destroy();
|
||||
});
|
||||
|
||||
it("isDestroyed is true", () => {
|
||||
assert.equal(finder.isDestroyed, true);
|
||||
});
|
||||
|
||||
it("search returns an error", () => {
|
||||
const r = finder.fileSearch("test");
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.error.includes("destroyed"));
|
||||
});
|
||||
|
||||
it("grep returns an error", () => {
|
||||
const r = finder.grep("test");
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.error.includes("destroyed"));
|
||||
});
|
||||
|
||||
it("double destroy is a safe no-op", () => {
|
||||
finder.destroy();
|
||||
assert.equal(finder.isDestroyed, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Stress reproducer for issue #515.
|
||||
*
|
||||
* Repeatedly creates a FileFinder, waits for scan, runs mixed file / dir /
|
||||
* grep operations with periodic scanFiles() and refreshGitStatus() calls,
|
||||
* destroys it, then repeats across two repos.
|
||||
*
|
||||
* Usage:
|
||||
* node test/stress-515.mjs [iterations] [repoA] [repoB]
|
||||
*/
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FileFinder } from "../dist/src/index.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(__dirname, "..", "..", "..");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const ITERS = Number(args[0] || process.env.FFF_STRESS_ITERS || 50);
|
||||
const REPO_A = resolve(args[1] || process.env.FFF_STRESS_REPO_A || REPO_ROOT);
|
||||
const REPO_B_CANDIDATE = args[2] || process.env.FFF_STRESS_REPO_B || resolve(REPO_ROOT, "big-repo");
|
||||
const REPO_B = existsSync(REPO_B_CANDIDATE) ? resolve(REPO_B_CANDIDATE) : REPO_A;
|
||||
|
||||
const REPOS = REPO_A === REPO_B ? [REPO_A] : [REPO_A, REPO_B];
|
||||
|
||||
const SEARCH_QUERIES = [
|
||||
"main",
|
||||
"lib",
|
||||
"fn",
|
||||
"TODO",
|
||||
"config",
|
||||
"README",
|
||||
"Cargo",
|
||||
"test",
|
||||
"src/",
|
||||
"init",
|
||||
"pub",
|
||||
"use",
|
||||
];
|
||||
|
||||
const GREP_QUERIES = [
|
||||
"fn ",
|
||||
"pub fn",
|
||||
"TODO",
|
||||
"FIXME",
|
||||
"use std",
|
||||
"impl ",
|
||||
"struct ",
|
||||
"let mut",
|
||||
"return",
|
||||
"match ",
|
||||
];
|
||||
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
async function runIteration(iter) {
|
||||
const base = REPOS[iter % REPOS.length];
|
||||
process.stdout.write(`[iter ${iter}] base=${base} `);
|
||||
|
||||
const created = FileFinder.create({
|
||||
basePath: base,
|
||||
logFilePath: `/tmp/fff-515-${iter}.log`,
|
||||
logLevel: "debug",
|
||||
});
|
||||
if (!created.ok) {
|
||||
console.error(`create failed: ${created.error}`);
|
||||
return false;
|
||||
}
|
||||
const finder = created.value;
|
||||
|
||||
const wait = await finder.waitForScan(60_000);
|
||||
if (!wait.ok || !wait.value) {
|
||||
console.error(`waitForScan failed: ${JSON.stringify(wait)}`);
|
||||
finder.destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
process.stdout.write("scan-done ");
|
||||
|
||||
const opCount = 30 + Math.floor(Math.random() * 30);
|
||||
for (let i = 0; i < opCount; i++) {
|
||||
const r = Math.random();
|
||||
if (r < 0.35) {
|
||||
finder.fileSearch(pick(SEARCH_QUERIES), { pageSize: 20 });
|
||||
} else if (r < 0.55) {
|
||||
finder.directorySearch(pick(SEARCH_QUERIES), { pageSize: 20 });
|
||||
} else if (r < 0.85) {
|
||||
finder.grep(pick(GREP_QUERIES), { mode: "plain", pageSize: 20 });
|
||||
} else if (r < 0.93) {
|
||||
finder.scanFiles();
|
||||
} else {
|
||||
finder.refreshGitStatus();
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write("ops-done ");
|
||||
|
||||
finder.destroy();
|
||||
process.stdout.write("destroyed\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log(`Running ${ITERS} iterations across:`);
|
||||
console.log(` A: ${REPO_A}`);
|
||||
console.log(` B: ${REPO_B}`);
|
||||
|
||||
for (let i = 0; i < ITERS; i++) {
|
||||
const ok = await runIteration(i);
|
||||
if (!ok) {
|
||||
console.error(`Aborting at iteration ${i}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Completed without crash.");
|
||||
})();
|
||||
Reference in New Issue
Block a user