239 lines
9.0 KiB
JavaScript
239 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import { promises as fs } from 'node:fs';
|
|
import fsSync from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { run as runRipgrep } from '../tools/ripgrep.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(__dirname, '..');
|
|
const chrootRoot = '/home/seb/src/aiTools/tmp';
|
|
const sandboxRoot = path.resolve(chrootRoot, 'rg-tests');
|
|
|
|
async function rimraf(dir) {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
async function ensureDir(dir) {
|
|
await fs.mkdir(dir, { recursive: true });
|
|
}
|
|
|
|
async function writeFiles(baseDir, filesMap) {
|
|
for (const [rel, content] of Object.entries(filesMap || {})) {
|
|
const filePath = path.resolve(baseDir, rel);
|
|
await ensureDir(path.dirname(filePath));
|
|
await fs.writeFile(filePath, content, 'utf8');
|
|
}
|
|
}
|
|
|
|
function slugify(name) {
|
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
}
|
|
|
|
function expectEqual(actual, expected, label) {
|
|
if (actual !== expected) {
|
|
const ellipsize = (s) => (s && s.length > 400 ? s.slice(0, 400) + '…' : (s ?? '<<undefined>>'));
|
|
throw new Error(`${label} mismatch.\nExpected:\n${ellipsize(expected)}\nActual:\n${ellipsize(actual)}`);
|
|
}
|
|
}
|
|
|
|
function expectRegex(actual, re, label) {
|
|
if (!re.test(actual)) {
|
|
throw new Error(`${label} mismatch. Expected to match ${re}, Actual: ${actual}`);
|
|
}
|
|
}
|
|
|
|
async function runCase(index, testCase) {
|
|
const idx = String(index + 1).padStart(2, '0');
|
|
const caseDir = path.resolve(sandboxRoot, `${idx}-${slugify(testCase.name)}`);
|
|
await ensureDir(chrootRoot);
|
|
await rimraf(caseDir);
|
|
await ensureDir(caseDir);
|
|
|
|
// Setup initial files
|
|
await writeFiles(caseDir, typeof testCase.before === 'function' ? await testCase.before({ dir: caseDir }) : (testCase.before || {}));
|
|
|
|
const args = await testCase.args({ dir: caseDir });
|
|
|
|
let threw = false;
|
|
let output = '';
|
|
try {
|
|
output = await runRipgrep(args);
|
|
} catch (err) {
|
|
threw = true;
|
|
output = err?.message || String(err);
|
|
}
|
|
|
|
if (testCase.expect?.error) {
|
|
if (!threw && typeof output === 'string') {
|
|
// We expect error formatting to be returned as string starting with 'ripgrep error:'
|
|
expectRegex(output, testCase.expect.error, 'Error string');
|
|
} else if (threw) {
|
|
expectRegex(output, testCase.expect.error, 'Thrown error');
|
|
}
|
|
} else {
|
|
if (typeof testCase.expect?.equals === 'string') {
|
|
expectEqual(output, testCase.expect.equals, 'Tool result');
|
|
}
|
|
if (typeof testCase.expect?.lineCount === 'number') {
|
|
const lines = output ? output.split('\n').filter(Boolean) : [];
|
|
expectEqual(lines.length, testCase.expect.lineCount, 'Line count');
|
|
}
|
|
}
|
|
}
|
|
|
|
function cases() {
|
|
const list = [];
|
|
|
|
// 1. Simple case-sensitive match
|
|
list.push({
|
|
name: 'simple case-sensitive match',
|
|
before: { 'a.txt': 'Hello\nWorld\nhello again' },
|
|
args: async ({ dir }) => ({ pattern: 'Hello', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }),
|
|
expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '01-simple-case-sensitive-match/a.txt'))}:1:Hello` }
|
|
});
|
|
|
|
// 2. Case-insensitive matches
|
|
list.push({
|
|
name: 'case-insensitive matches',
|
|
before: { 'a.txt': 'Hello\nWorld\nhello again' },
|
|
args: async ({ dir }) => ({ pattern: 'hello', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: true }),
|
|
expect: { equals: [
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '02-case-insensitive-matches/a.txt'))}:1:Hello`,
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '02-case-insensitive-matches/a.txt'))}:3:hello again`
|
|
].join('\n') }
|
|
});
|
|
|
|
// 3. filePattern filter to subdir and extension
|
|
list.push({
|
|
name: 'filePattern filter',
|
|
before: { 'sub/b.md': 'Alpha\nbeta\nGamma' },
|
|
args: async ({ dir }) => ({ pattern: 'Alpha', filePattern: path.relative(chrootRoot, path.join(dir, 'sub/*.md')), n_flag: true, i_flag: false }),
|
|
expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '03-filepattern-filter/sub/b.md'))}:1:Alpha` }
|
|
});
|
|
|
|
// 4. No matches -> empty string
|
|
list.push({
|
|
name: 'no matches returns empty',
|
|
before: { 'a.txt': 'x\ny' },
|
|
args: async ({ dir }) => ({ pattern: 'nomatch', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }),
|
|
expect: { equals: '' }
|
|
});
|
|
|
|
// 5. Output limited to 200 lines
|
|
list.push({
|
|
name: 'limit to 200 lines',
|
|
before: async () => {
|
|
const many = Array.from({ length: 250 }, (_, i) => `line${i + 1}`).join('\n');
|
|
return { 'long.txt': many };
|
|
},
|
|
args: async ({ dir }) => ({ pattern: 'line', filePattern: path.relative(chrootRoot, path.join(dir, 'long.txt')), n_flag: true, i_flag: false }),
|
|
expect: { lineCount: 200 }
|
|
});
|
|
|
|
// 6. Invalid regex pattern -> error line
|
|
list.push({
|
|
name: 'invalid regex pattern',
|
|
before: { 'a.txt': 'text' },
|
|
args: async ({ dir }) => ({ pattern: '[', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }),
|
|
expect: { error: /ripgrep error:/ }
|
|
});
|
|
|
|
// 7. No line numbers (n_flag false)
|
|
list.push({
|
|
name: 'no line numbers',
|
|
before: { 'a.txt': 'foo\nbar\nfoo' },
|
|
args: async ({ dir }) => ({ pattern: 'foo', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: false, i_flag: false }),
|
|
expect: { equals: [
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '07-no-line-numbers/a.txt'))}:foo`,
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '07-no-line-numbers/a.txt'))}:foo`
|
|
].join('\n') }
|
|
});
|
|
|
|
// 8. filePattern include-only to exclude .md (tool supports single -g, so include *.txt)
|
|
list.push({
|
|
name: 'filePattern include-only excludes md',
|
|
before: { 'a.txt': 'hit', 'b.md': 'hit' },
|
|
args: async ({ dir }) => ({ pattern: 'hit', filePattern: path.relative(chrootRoot, path.join(dir, '**/*.txt')), n_flag: true, i_flag: false }),
|
|
expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '08-filepattern-negation-excludes-md/a.txt'))}:1:hit` }
|
|
});
|
|
|
|
// 9. Empty filePattern searches all (we'll scope to the case dir by pattern and path shape)
|
|
list.push({
|
|
name: 'empty filePattern searches all',
|
|
before: { 'x.js': 'Hello', 'y.txt': 'Hello' },
|
|
args: async ({ dir }) => ({ pattern: 'Hello', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }),
|
|
expect: { equals: [
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '09-empty-filepattern-searches-all/x.js'))}:1:Hello`,
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '09-empty-filepattern-searches-all/y.txt'))}:1:Hello`
|
|
].join('\n') }
|
|
});
|
|
|
|
// 10. Anchored regex
|
|
list.push({
|
|
name: 'anchored regex',
|
|
before: { 'a.txt': 'Hello\nHello world\nHello' },
|
|
args: async ({ dir }) => ({ pattern: '^Hello$', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }),
|
|
expect: { equals: [
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '10-anchored-regex/a.txt'))}:1:Hello`,
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '10-anchored-regex/a.txt'))}:3:Hello`
|
|
].join('\n') }
|
|
});
|
|
|
|
// 11. Special regex characters
|
|
list.push({
|
|
name: 'special regex characters',
|
|
before: { 'a.txt': 'a+b?c\\d and a+b?c\\d' },
|
|
args: async ({ dir }) => ({ pattern: 'a\\+b\\?c\\\\d', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }),
|
|
expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '11-special-regex-characters/a.txt'))}:1:a+b?c\\d and a+b?c\\d` }
|
|
});
|
|
|
|
// 12. Multiple files across dirs deterministic order
|
|
list.push({
|
|
name: 'multi dirs deterministic',
|
|
before: { 'b/b.txt': 'X', 'a/a.txt': 'X' },
|
|
args: async ({ dir }) => ({ pattern: '^X$', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }),
|
|
expect: { equals: [
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '12-multi-dirs-deterministic/a/a.txt'))}:1:X`,
|
|
`${path.relative(chrootRoot, path.join(sandboxRoot, '12-multi-dirs-deterministic/b/b.txt'))}:1:X`
|
|
].join('\n') }
|
|
});
|
|
|
|
return list;
|
|
}
|
|
|
|
async function main() {
|
|
const all = cases();
|
|
await ensureDir(sandboxRoot);
|
|
const results = [];
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
for (let i = 0; i < all.length; i++) {
|
|
const tc = all[i];
|
|
const label = `${String(i + 1).padStart(2, '0')} ${tc.name}`;
|
|
try {
|
|
await runCase(i, tc);
|
|
console.log(`✓ ${label}`);
|
|
results.push({ name: tc.name, ok: true });
|
|
passed++;
|
|
} catch (err) {
|
|
console.error(`✗ ${label}`);
|
|
console.error(String(err?.stack || err));
|
|
results.push({ name: tc.name, ok: false, error: String(err?.message || err) });
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
console.log(`Passed: ${passed}, Failed: ${failed}, Total: ${all.length}`);
|
|
if (failed > 0) process.exit(1);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Fatal error in ripgrep test runner:', err);
|
|
process.exit(1);
|
|
});
|
|
|
|
|