From e0d4cf99dfa3ac2e6259f1a03fd6f75ed27f24b2 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 14:00:09 +1100 Subject: [PATCH 01/11] Handle files in tests with the sync functions of fs --- devguide.md | 2 +- test/cli/debug.test.js | 9 +++--- test/cli/delimiter.test.js | 63 +++++++++++++++++++------------------ test/cli/encoding.test.js | 5 +-- test/cli/fix/691.test.js | 5 +-- test/cli/fix/733.test.js | 5 +-- test/cli/fix/736.test.js | 5 +-- test/cli/fix/742.test.js | 7 +++-- test/cli/glob-pipe.test.js | 13 ++++---- test/cli/names.test.js | 29 ++++++++--------- test/cli/nl.test.js | 19 +++++------ test/cli/null-bytes.test.js | 9 +++--- test/cli/pipe.test.js | 21 +++++++------ test/engine/fix/703.test.js | 52 ++++++++++++++++-------------- test/engine/fix/708.test.js | 18 +++++------ test/engine/fix/719.test.js | 11 ++++--- test/engine/fix/726.test.js | 29 ++++++++--------- test/engine/utf8.test.js | 41 ++++++++++++------------ test/fuzzy/output.test.js | 13 +++++--- 19 files changed, 188 insertions(+), 168 deletions(-) diff --git a/devguide.md b/devguide.md index ce4dd7c7..73b3a8e5 100644 --- a/devguide.md +++ b/devguide.md @@ -39,7 +39,7 @@ All output goes through `src/output.ts`. Pick the function by who the message is - The folder tells what is tested, so a file is named after the feature: `test/engine/.test.js` tests the engine with `rr()`, and `test/cli/.test.js` tests the CLI by running it with Bun's `$` shell, like ``sh($`bun ${cli} o x my.file`)``. - A test for an issue goes in `test/engine/fix/.test.js` or `test/cli/fix/.test.js`. - `test/cli/run.sh` is a demo of how RexReplace is used: a few clear examples for each feature. Checks of many combinations go in `test/cli/.test.js`. -- Use Bun native file IO (`Bun.write`, `Bun.file`) in new tests (#734). +- Handle files in tests with the sync functions of `fs`, like `fs.writeFileSync` and `fs.readFileSync`, so there is no `await` for files. Each test writes the files it uses itself, so you can see them next to the command. - Names in tests say what they hold, like `expectedOutput`, not `named`. - Async work can finish in any order. Do not expect an order the code does not promise. diff --git a/test/cli/debug.test.js b/test/cli/debug.test.js index 36950668..b2e48d55 100644 --- a/test/cli/debug.test.js +++ b/test/cli/debug.test.js @@ -1,4 +1,5 @@ import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; @@ -6,7 +7,7 @@ describe('--debug', () => { test.each(['--debug', '-d'])( '%s prints only the status report, with what went into the engine and what it did', async flag => { - await Bun.write('my.file', 'foobar'); + fs.writeFileSync('my.file', 'foobar'); const result = await sh($`bun ${cli} o x my.file ${flag}`); expect(result.err).toBe(''); expect(result.exit).toBe(0); @@ -26,12 +27,12 @@ describe('--debug', () => { 'Work on content from: my.file', 'Write new content to: my.file', ]); - expect(await Bun.file('my.file').text()).toBe('fxxbar'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fxxbar'); } ); test('still prints the result of --output before the status report', async () => { - await Bun.write('my.file', 'foobar'); + fs.writeFileSync('my.file', 'foobar'); const result = await sh($`bun ${cli} o x my.file --debug --output`); expect(result.out.startsWith('fxxbar\n\n{')).toBe(true); expect(JSON.parse(result.out.slice('fxxbar\n'.length)).files.updated).toEqual(['my.file']); @@ -45,7 +46,7 @@ describe('--debug', () => { }); test('gives the error of an invalid pattern in the status report', async () => { - await Bun.write('my.file', 'foobar'); + fs.writeFileSync('my.file', 'foobar'); const result = await sh($`bun ${cli} '(' x my.file --debug`); expect(result.err).toBe(''); expect(result.exit).toBe(1); diff --git a/test/cli/delimiter.test.js b/test/cli/delimiter.test.js index 4eddc724..5c00b819 100644 --- a/test/cli/delimiter.test.js +++ b/test/cli/delimiter.test.js @@ -1,27 +1,28 @@ // The delimiters after the names and after the content, as given on the command line import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; describe('--name-delimiter', () => { test('goes after each name', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const result = await sh($`bun ${cli} b x my.file your.file -A --name-delimiter ', '`); expect(result.out).toBe('my.file, your.file, '); }); test('understands escape sequences and the € and § aliases', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const delimiter = '\\t§t€'; const result = await sh($`bun ${cli} b x my.file your.file -A --name-delimiter ${delimiter}`); expect(result.out).toBe('my.file\t\t$your.file\t\t$'); }); test('keeps € and § with --void-euro and --void-section', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const delimiter = '§t€'; const result = await sh( $`bun ${cli} b x my.file your.file -A --name-delimiter ${delimiter} --no-name-delimiter-tail --void-section --void-euro` @@ -30,14 +31,14 @@ describe('--name-delimiter', () => { }); test('is only put between the names with --no-name-delimiter-tail or --name-delimiter-tail=false', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const noTail = await sh( $`bun ${cli} b x my.file your.file -A --name-delimiter ', ' --no-name-delimiter-tail` ); expect(noTail.out).toBe('my.file, your.file'); - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const tailFalse = await sh( $`bun ${cli} b x my.file your.file -A --name-delimiter ', ' --name-delimiter-tail=false` ); @@ -45,7 +46,7 @@ describe('--name-delimiter', () => { }); test('with no tail and one name gives just the name', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); const result = await sh( $`bun ${cli} b x my.file --name-delimiter ', ' --no-name-delimiter-tail` ); @@ -55,11 +56,11 @@ describe('--name-delimiter', () => { describe('the content delimiter', () => { test('is a line break by default, unless the content already ends with one', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} b x my.file your.file -o`)).out).toBe('fooxar\naxc123\n'); - await Bun.write('a.file', 'foo'); - await Bun.write('b.file', 'bar'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); expect((await sh($`bun ${cli} o x a.file b.file -o`)).out).toBe('fxx\nbar\n'); expect((await sh($`bun ${cli} '[ab]' _ a.file b.file -m`)).out).toBe('b\na\n'); }); @@ -74,8 +75,8 @@ describe('the content delimiter', () => { }); test('is used as it is when given', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} b x my.file your.file -o --delimiter ''`)).out).toBe( 'fooxar\naxc123\n' ); @@ -90,8 +91,8 @@ describe('the content delimiter', () => { }); test('goes after each match with --output-match, also with no tail', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ','`)).out).toBe( 'b,b,c,' ); @@ -103,8 +104,8 @@ describe('the content delimiter', () => { describe('--delimiter, --null-out and --tail', () => { test('--delimiter sets both delimiters', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect(await sh($`bun ${cli} b x my.file your.file -o --delimiter '|'`)).toEqual({ out: 'fooxar\n|axc123\n|', err: 'my.file|your.file|', @@ -113,21 +114,21 @@ describe('--delimiter, --null-out and --tail', () => { }); test('--null-out is a null byte after each name and each content', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} b x my.file your.file -A --null-out`)).out).toBe( 'my.file\0your.file\0' ); - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} b x my.file your.file -o --null-out`)).out).toBe( 'fooxar\n\0axc123\n\0' ); }); test('the most specific flag wins', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} b x my.file your.file -o --null-out --delimiter ', '`)).err).toBe( 'my.file, your.file, ' ); @@ -141,8 +142,8 @@ describe('--delimiter, --null-out and --tail', () => { }); test('--tail sets both tails, and the specific tail flag wins', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --tail`)).out).toBe( 'b,b,c,' ); @@ -166,11 +167,11 @@ describe('an invalid escape sequence in a delimiter', () => { test.each([['--name-delimiter'], ['--content-delimiter'], ['--delimiter']])( 'in %s stops with an error and changes nothing', async flag => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); const result = await sh($`bun ${cli} b x my.file ${flag} '\x4'`); expect(result.err).toContain(`Invalid escape sequence in ${flag}`); expect(result.exit).toBe(1); - expect(await Bun.file('my.file').text()).toBe('foobar\n'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n'); } ); }); diff --git a/test/cli/encoding.test.js b/test/cli/encoding.test.js index 6455915b..8c8b5465 100644 --- a/test/cli/encoding.test.js +++ b/test/cli/encoding.test.js @@ -1,5 +1,6 @@ // The CLI checks the encoding before stdin or any file is read (#690) import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; @@ -7,11 +8,11 @@ const lastLine = text => text.trim().split('\n').at(-1); describe('--encoding', () => { test('an unknown encoding stops the run with an error, and the file is not changed', async () => { - await Bun.write('my.file', 'foobar'); + fs.writeFileSync('my.file', 'foobar'); const result = await sh($`bun ${cli} o x my.file --encoding nope`); expect(result.exit).toBe(1); expect(lastLine(result.err)).toBe('Unknown encoding: nope'); - expect(await Bun.file('my.file').text()).toBe('foobar'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); test('an unknown encoding also stops the run for piped data', async () => { diff --git a/test/cli/fix/691.test.js b/test/cli/fix/691.test.js index d554e5db..bf2bbf4e 100644 --- a/test/cli/fix/691.test.js +++ b/test/cli/fix/691.test.js @@ -2,6 +2,7 @@ // A replacement containing a real line break, a `\'` or a lone backslash at the end crashed the CLI with a SyntaxError and a stack trace. // The replacement is unescaped by the CLI (not the engine), so these tests run the CLI itself. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../../helper.js'; @@ -10,8 +11,8 @@ const expectedOutput = 'my.file\n'; // Replace each `o` in `foobar` with the replacement and return what the CLI gives back. // Without the tail only the result of the replacement is given. -async function replaceO(replacement, ...flags) { - await Bun.write('my.file', 'foobar'); +function replaceO(replacement, ...flags) { + fs.writeFileSync('my.file', 'foobar'); return sh($`bun ${cli} o ${replacement} my.file --output --no-content-delimiter-tail ${flags}`); } diff --git a/test/cli/fix/733.test.js b/test/cli/fix/733.test.js index 0115666e..a46a0b25 100644 --- a/test/cli/fix/733.test.js +++ b/test/cli/fix/733.test.js @@ -1,13 +1,14 @@ // https://github.com/mathiasrw/rexreplace/issues/733 // yargs turned a file glob like `2024` into a number, and fast-glob only takes strings, so RexReplace crashed. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../../helper.js'; describe('#733 a file glob that reads as a number', () => { test.each(['2024', '1e3', '0x10'])('is kept as a string, like %s', async name => { - await Bun.write(name, 'foobar'); + fs.writeFileSync(name, 'foobar'); expect(await sh($`bun ${cli} o x ${name}`)).toEqual({out: name + '\n', err: '', exit: 0}); - expect(await Bun.file(name).text()).toBe('fxxbar'); + expect(fs.readFileSync(name, 'utf8')).toBe('fxxbar'); }); }); diff --git a/test/cli/fix/736.test.js b/test/cli/fix/736.test.js index 6c0c1bd7..64d105d0 100644 --- a/test/cli/fix/736.test.js +++ b/test/cli/fix/736.test.js @@ -2,13 +2,14 @@ // stdin was read every time it was not a terminal, so a pipe that was open but never written to made the run wait forever. // The flags that ask for the piped data are tested in test/cli/pipe.test.js. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {cli} from '../../helper.js'; // Bun.spawn and not sh() from the helper: the test needs a pipe that stays open, and a pipe in a shell command ends describe('#736 stdin is only read when the data is asked for', () => { test('files are changed without waiting for a pipe that is never closed', async () => { - await Bun.write('my.file', 'foobar'); + fs.writeFileSync('my.file', 'foobar'); const proc = Bun.spawn([process.execPath, cli, 'o', 'x', 'my.file'], { cwd: process.cwd(), stdin: 'pipe', @@ -19,6 +20,6 @@ describe('#736 stdin is only read when the data is asked for', () => { proc.kill(); expect(exit).toBe(0); - expect(await Bun.file('my.file').text()).toBe('fxxbar'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fxxbar'); }); }); diff --git a/test/cli/fix/742.test.js b/test/cli/fix/742.test.js index 3e3732ae..ff2d6285 100644 --- a/test/cli/fix/742.test.js +++ b/test/cli/fix/742.test.js @@ -1,12 +1,13 @@ // https://github.com/mathiasrw/rexreplace/issues/742 // The CLI is the only part that acts on the process. It prints the status report with --status and --status-compact. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../../helper.js'; describe('#742 the CLI', () => { test('--status prints a line break and the status report with an indent of two spaces after the result', async () => { - await Bun.write('a.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); const result = await sh($`bun ${cli} o x a.file --status`); const status = { files: {searched: ['a.file'], updated: ['a.file']}, @@ -24,7 +25,7 @@ describe('#742 the CLI', () => { test.each(['--status-compact', '-c'])( '%s prints the status report on one line, with no line break in front', async flag => { - await Bun.write('a.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); const result = await sh($`bun ${cli} o x a.file ${flag} --quiet-total`); expect(result.out).toBe( '{"files":{"searched":["a.file"],"updated":["a.file"]},"pipe":null,"warnings":[],"errors":[]}\n' @@ -39,7 +40,7 @@ describe('#742 the CLI', () => { }); test('ends the process on a panic, and still prints the status report', async () => { - await Bun.write('a.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); const result = await sh($`bun ${cli} '(' x a.file --status-compact --quiet-total`); expect(JSON.parse(result.out).errors).toHaveLength(1); expect(result.exit).toBe(1); diff --git a/test/cli/glob-pipe.test.js b/test/cli/glob-pipe.test.js index c5ea1724..9b7315d3 100644 --- a/test/cli/glob-pipe.test.js +++ b/test/cli/glob-pipe.test.js @@ -1,12 +1,13 @@ // --glob-pipe (-g): each line of the piped data is a path or a glob of the files to work on (#711) import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; describe('--glob-pipe', () => { test('a piped line can be a glob', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect(await sh($`echo -n '*.file' | bun ${cli} b x -g -A`)).toEqual({ out: 'my.file\nyour.file\n', err: '', @@ -17,18 +18,18 @@ describe('--glob-pipe', () => { test.each([['--replacement-pipe'], ['--pipe'], ['--js-pipe']])( 'can not be used with %s, as the piped data can only be one thing', async flag => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); const result = await sh($`echo -n my.file | bun ${cli} b x --glob-pipe ${flag}`); expect(result.err).toContain('are mutually exclusive'); expect(result.exit).toBe(1); - expect(await Bun.file('my.file').text()).toBe('foobar\n'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n'); } ); test('--null-in can only be used with --glob-pipe', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); const result = await sh($`echo -n my.file | bun ${cli} b x --null-in`); expect(result.exit).toBe(1); - expect(await Bun.file('my.file').text()).toBe('foobar\n'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n'); }); }); diff --git a/test/cli/names.test.js b/test/cli/names.test.js index 3cd07d56..8713d605 100644 --- a/test/cli/names.test.js +++ b/test/cli/names.test.js @@ -1,12 +1,13 @@ // The names of the changed files are the result on stdout. With --output and --output-match stdout has the content, so the names go to stderr. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; describe('the names of the changed files', () => { test('go to stdout, and nothing goes to stderr', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect(await sh($`bun ${cli} b x my.file your.file -A`)).toEqual({ out: 'my.file\nyour.file\n', err: '', @@ -15,31 +16,31 @@ describe('the names of the changed files', () => { }); test('stay alone on stdout with --verbose', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); const result = await sh($`bun ${cli} b x my.file --verbose`); expect(result.out).toBe('my.file\n'); expect(result.err.split('\n')).not.toContain('my.file'); }); test('are not printed with --quiet, but the file is changed', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect(await sh($`bun ${cli} b x my.file -q`)).toEqual({out: '', err: '', exit: 0}); - expect(await Bun.file('my.file').text()).toBe('fooxar\n'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fooxar\n'); }); describe('go to stderr when stdout has the content', () => { test('with --output', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect(await sh($`bun ${cli} b x my.file -o`)).toEqual({ out: 'fooxar\n', err: 'my.file\n', exit: 0, }); - expect(await Bun.file('my.file').text()).toBe('foobar\n'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n'); }); test('with --output-match', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect(await sh($`bun ${cli} b _ my.file -m`)).toEqual({ out: 'b\n', err: 'my.file\n', @@ -48,26 +49,26 @@ describe('the names of the changed files', () => { }); test('and are not printed with --quiet', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`bun ${cli} b x my.file -o -q`)).err).toBe(''); expect((await sh($`bun ${cli} b _ my.file -m -q`)).err).toBe(''); }); test('only for a file where something would change', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`bun ${cli} z x my.file -o`)).err).toBe(''); }); test('with --name-delimiter', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const result = await sh($`bun ${cli} b x my.file your.file -o --name-delimiter ', '`); expect(result.err).toBe('my.file, your.file, '); }); test('with --name-all also for the files where nothing would change', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect((await sh($`bun ${cli} b x my.file your.file -o --name-all`)).err).toBe( 'my.file\nyour.file\n' ); diff --git a/test/cli/nl.test.js b/test/cli/nl.test.js index 0435b86f..602a026c 100644 --- a/test/cli/nl.test.js +++ b/test/cli/nl.test.js @@ -1,12 +1,13 @@ // --nl (-l) ends stdout and stderr with a line break, unless they already end with one import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; describe('--nl', () => { test('adds a line break after the last match, also as -l', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect( (await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --no-tail --nl`)).out ).toBe('b,b,c\n'); @@ -16,15 +17,15 @@ describe('--nl', () => { }); test('comes after the tail', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); expect( (await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --tail --nl`)).out ).toBe('b,b,c,\n'); }); test('adds nothing when the output already ends with a line break', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`bun ${cli} b x my.file -o --nl`)).out).toBe('fooxar\n'); }); @@ -37,14 +38,14 @@ describe('--nl', () => { }); test('ends the names with a line break, on stdout and on stderr', async () => { - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const stdout = await sh( $`bun ${cli} b x my.file your.file -A --name-delimiter ',' --no-name-delimiter-tail --nl` ); expect(stdout.out).toBe('my.file,your.file\n'); - await Bun.write('my.file', 'foobar\n'); - await Bun.write('your.file', 'abc123\n'); + fs.writeFileSync('my.file', 'foobar\n'); + fs.writeFileSync('your.file', 'abc123\n'); const stderr = await sh( $`bun ${cli} b x my.file your.file -o --name-delimiter ',' --no-name-delimiter-tail --nl` ); diff --git a/test/cli/null-bytes.test.js b/test/cli/null-bytes.test.js index 4baf6d9c..f0cc89e4 100644 --- a/test/cli/null-bytes.test.js +++ b/test/cli/null-bytes.test.js @@ -1,5 +1,6 @@ // Null bytes come out where they went in, piped in and in a file import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; @@ -21,18 +22,18 @@ describe('null bytes are kept', () => { describe('in a file', () => { test('when it is changed in place', async () => { - await Bun.write('null.file', data); + fs.writeFileSync('null.file', data); expect(await sh($`bun ${cli} o x null.file`)).toEqual({out: 'null.file\n', err: '', exit: 0}); - expect(await Bun.file('null.file').text()).toBe('fxx\0bxx\0'); + expect(fs.readFileSync('null.file', 'utf8')).toBe('fxx\0bxx\0'); }); test('with --output, followed by the line break after the content', async () => { - await Bun.write('null.file', data); + fs.writeFileSync('null.file', data); expect((await sh($`bun ${cli} o x null.file --output`)).out).toBe('fxx\0bxx\0\n'); }); test('with --output-match and --null-out', async () => { - await Bun.write('null.file', data); + fs.writeFileSync('null.file', data); expect((await sh($`bun ${cli} o _ null.file --output-match --null-out`)).out).toBe( 'o\0o\0o\0o\0' ); diff --git a/test/cli/pipe.test.js b/test/cli/pipe.test.js index a6760c52..c7221078 100644 --- a/test/cli/pipe.test.js +++ b/test/cli/pipe.test.js @@ -1,4 +1,5 @@ import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; @@ -6,7 +7,7 @@ const warning = ' 🟡 Data is piped in, but no flag tells what to do with it. I describe('--pipe and --js-pipe', () => { test('without --pipe the code does not get the piped data', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect(await sh($`echo -n x | bun ${cli} foo pipe my.file -o --replacement-js`)).toEqual({ out: 'nullbar\n', err: 'my.file\n', @@ -15,20 +16,20 @@ describe('--pipe and --js-pipe', () => { }); test('the code gets the piped data with --replacement-js --pipe', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect( (await sh($`echo -n x | bun ${cli} foo pipe my.file -o --replacement-js --pipe`)).out ).toBe('xbar\n'); }); test('the code gets the piped data with -jp and --js-pipe', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`echo -n x | bun ${cli} foo pipe my.file -o -jp`)).out).toBe('xbar\n'); expect((await sh($`echo -n x | bun ${cli} foo pipe my.file -o --js-pipe`)).out).toBe('xbar\n'); }); test('--trim-pipe trims the piped data first', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`echo -n ' x ' | bun ${cli} foo pipe my.file -o --js-pipe -T`)).out).toBe( 'xbar\n' ); @@ -37,33 +38,33 @@ describe('--pipe and --js-pipe', () => { describe('the warning that piped data is not used', () => { test('is given when data is read with --pipe, but not used', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect(await sh($`echo -n x | bun ${cli} o x my.file --pipe`)).toEqual({ out: 'my.file\n', err: warning, exit: 0, }); - expect(await Bun.file('my.file').text()).toBe('fxxbar\n'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fxxbar\n'); }); test('is not given without --pipe, as the data is not read', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`echo -n x | bun ${cli} o x my.file`)).err).toBe(''); }); test('is not given with --quiet', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`echo -n x | bun ${cli} o x my.file --pipe -q`)).err).toBe(''); }); test('is not given when the data is used', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`echo -n x | bun ${cli} b _ my.file -R`)).err).toBe(''); expect((await sh($`echo -n x | bun ${cli} b pipe my.file --js-pipe`)).err).toBe(''); }); test('is not given when nothing, or only a line break, is piped in', async () => { - await Bun.write('my.file', 'foobar\n'); + fs.writeFileSync('my.file', 'foobar\n'); expect((await sh($`echo -n '' | bun ${cli} o x my.file --pipe`)).err).toBe(''); expect((await sh($`echo '' | bun ${cli} o x my.file --pipe`)).err).toBe(''); expect((await sh($`bun ${cli} o x my.file --pipe < ${new Response('\r\n')}`)).err).toBe(''); diff --git a/test/engine/fix/703.test.js b/test/engine/fix/703.test.js index 7fe6cd2d..56e665ec 100644 --- a/test/engine/fix/703.test.js +++ b/test/engine/fix/703.test.js @@ -8,17 +8,17 @@ import {rr} from '../../helper.js'; // Note that rr() from the helper sets voidAsync per default, so the names come in the order of the file list // Replace in a.file, b.file and c.file where b.file has nothing to replace -async function run(flags = {}) { - await Bun.write('a.file', 'foo'); - await Bun.write('b.file', 'bar'); - await Bun.write('c.file', 'foo'); +function run(flags = {}) { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); return rr('o', 'x', {includeGlob: ['*.file'], ...flags}); } describe('#703 the names of affected files', () => { test('go to stdout with one name on each line', async () => { expect(await run()).toEqual({out: 'a.file\nc.file\n', err: '', exit: null}); - expect(await Bun.file('a.file').text()).toBe('fxx'); + expect(fs.readFileSync('a.file', 'utf8')).toBe('fxx'); }); // A file is named when the result differs from the content, so `rexreplace o o` names nothing @@ -41,7 +41,7 @@ describe('#703 the names of affected files', () => { test.each(['quiet', 'quietTotal'])('are not printed with --%s', async flag => { expect(await run({[flag]: true})).toEqual({out: '', err: '', exit: null}); - expect(await Bun.file('c.file').text()).toBe('fxx'); + expect(fs.readFileSync('c.file', 'utf8')).toBe('fxx'); }); test('are not printed for piped data', async () => { @@ -54,7 +54,8 @@ describe('#703 the names of affected files', () => { }); test('are printed the way the path was given', async () => { - await Bun.write('sub/my.file', 'foo'); + fs.mkdirSync('sub', {recursive: true}); + fs.writeFileSync('sub/my.file', 'foo'); expect((await rr('o', 'x', {includeGlob: ['sub/*.file']})).out).toBe('sub/my.file\n'); const file = path.resolve('sub/my.file'); @@ -67,7 +68,7 @@ describe('#703 the names of affected files', () => { }); test('keeps a name with a line break in one piece', async () => { - await Bun.write('my\n.file', 'foo'); + fs.writeFileSync('my\n.file', 'foo'); expect((await rr('o', 'x', {includeGlob: ['*.file'], nullOut: true})).out).toBe( 'my\n.file\0' ); @@ -103,7 +104,7 @@ describe('#703 the names of affected files', () => { }); test('prints no delimiter when only one file is changed', async () => { - await Bun.write('my.file', 'foo'); + fs.writeFileSync('my.file', 'foo'); const config = { includeGlob: ['my.file'], nameDelimiter: ', ', @@ -121,8 +122,9 @@ describe('#703 the names of affected files', () => { // The names must not be held back until all files are done test('prints the files changed before the run is stopped by --halt', async () => { - await Bun.write('a.file', 'foo'); - await Bun.write('locked/b.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); + fs.mkdirSync('locked', {recursive: true}); + fs.writeFileSync('locked/b.file', 'foo'); fs.chmodSync('locked', 0o555); try { const result = await rr('o', 'x', { @@ -140,8 +142,9 @@ describe('#703 the names of affected files', () => { }); test('with --nl still ends with a line break when the run is stopped by --halt', async () => { - await Bun.write('a.file', 'foo'); - await Bun.write('locked/b.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); + fs.mkdirSync('locked', {recursive: true}); + fs.writeFileSync('locked/b.file', 'foo'); fs.chmodSync('locked', 0o555); try { const result = await rr('o', 'x', { @@ -165,7 +168,7 @@ describe('#703 the names of affected files', () => { err: '', exit: null, }); - expect(await Bun.file('b.file').text()).toBe('bar'); + expect(fs.readFileSync('b.file', 'utf8')).toBe('bar'); }); test('names every file printed with --output', async () => { @@ -192,9 +195,9 @@ describe('#703 the names of affected files', () => { }); test('adds no line break by default to content that already ends with one', async () => { - await Bun.write('a.file', 'foo\n'); - await Bun.write('b.file', ''); - await Bun.write('c.file', 'bar'); + fs.writeFileSync('a.file', 'foo\n'); + fs.writeFileSync('b.file', ''); + fs.writeFileSync('c.file', 'bar'); const config = {includeGlob: ['*.file'], output: true}; expect((await rr('o', 'x', config)).out).toBe('fxx\n\nbar\n'); }); @@ -219,7 +222,7 @@ describe('#703 the names of affected files', () => { }); test('is put after the last match, from a file or a pipe', async () => { - await Bun.write('a.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); const fromFile = await rr('o', '_', {includeGlob: ['a.file'], outputMatch: true}); const fromPipe = await rr('o', '_', {pipedData: 'foo', outputMatch: true}); expect(fromFile.out).toBe('o\no\n'); @@ -279,15 +282,15 @@ describe('#703 the names of affected files', () => { test('adds nothing when the output already ends with a line break', async () => { expect((await rr('o', 'x', {pipedData: 'foo\n\n', nl: true})).out).toBe('fxx\n\n'); - await Bun.write('a.file', 'foo\n'); - await Bun.write('b.file', ''); + fs.writeFileSync('a.file', 'foo\n'); + fs.writeFileSync('b.file', ''); const config = {includeGlob: ['*.file'], output: true, nl: true}; expect((await rr('o', 'x', config)).out).toBe('fxx\n\n'); }); test('adds nothing when nothing is printed', async () => { expect((await rr('nope', 'x', {pipedData: 'foo', outputMatch: true, nl: true})).out).toBe(''); - await Bun.write('a.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); expect((await rr('nope', 'x', {includeGlob: ['a.file'], nl: true})).out).toBe(''); }); @@ -347,7 +350,7 @@ describe('#703 the names of affected files', () => { ])('with --%s', (flag, out) => { test('go to stderr', async () => { expect(await run({[flag]: true})).toEqual({out, err: 'a.file\nc.file\n', exit: null}); - expect(await Bun.file('a.file').text()).toBe('foo'); + expect(fs.readFileSync('a.file', 'utf8')).toBe('foo'); }); test('are the same names as without the flag', async () => { @@ -378,11 +381,12 @@ describe('#703 the names of affected files', () => { test('leaves a path that is already full as it is', async () => { fs.mkdirSync('work'); + fs.mkdirSync('other'); const file = path.resolve('other', 'my.file'); - await Bun.write(file, 'foo'); + fs.writeFileSync(file, 'foo'); process.chdir('work'); expect((await rr('o', 'x', {includeGlob: [file], fullPath: true})).out).toBe(file + '\n'); - expect(await Bun.file(file).text()).toBe('fxx'); + expect(fs.readFileSync(file, 'utf8')).toBe('fxx'); }); test.each([ diff --git a/test/engine/fix/708.test.js b/test/engine/fix/708.test.js index 723a1db0..9eee43dc 100644 --- a/test/engine/fix/708.test.js +++ b/test/engine/fix/708.test.js @@ -10,18 +10,18 @@ const workers = Math.max(16, os.availableParallelism() * 2); describe('#708 the number of files in work at the same time is limited', () => { test('all files are changed when there are more files than workers', async () => { const names = Array.from({length: workers * 3}, (_, i) => `file-${i}.txt`); - await Promise.all(names.map(name => Bun.write(name, 'foo'))); + names.forEach(name => fs.writeFileSync(name, 'foo')); const result = await rr('o', 'x', {includeGlob: ['*.txt'], voidAsync: false}); expect(result.err).toBe(''); expect(result.out.split('\n').sort()).toEqual(['', ...names].sort()); for (const name of names) { - expect(await Bun.file(name).text()).toBe('fxx'); + expect(fs.readFileSync(name, 'utf8')).toBe('fxx'); } }); test(`at most ${workers} files are read at the same time`, async () => { const names = Array.from({length: workers * 3}, (_, i) => `file-${i}.txt`); - await Promise.all(names.map(name => Bun.write(name, 'foo'))); + names.forEach(name => fs.writeFileSync(name, 'foo')); const readFile = fs.promises.readFile; let reading = 0; let mostReading = 0; @@ -46,7 +46,7 @@ describe('#708 the number of files in work at the same time is limited', () => { test('the number of CPU cores is not asked for with 16 files or fewer', async () => { const names = Array.from({length: 16}, (_, i) => `file-${i}.txt`); - await Promise.all(names.map(name => Bun.write(name, 'foo'))); + names.forEach(name => fs.writeFileSync(name, 'foo')); const spy = spyOn(os, 'availableParallelism'); try { await rr('o', 'x', {includeGlob: ['*.txt'], voidAsync: false}); @@ -57,19 +57,19 @@ describe('#708 the number of files in work at the same time is limited', () => { }); test('one file is handled in the synchronous flow', async () => { - await Bun.write('my.file', 'foo'); + fs.writeFileSync('my.file', 'foo'); const result = await rr('o', 'x', { includeGlob: ['my.file'], voidAsync: false, verbose: true, }); expect(result.err).toContain('Open sync: my.file'); - expect(await Bun.file('my.file').text()).toBe('fxx'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fxx'); }); test('a file that can not be read gives an error in the synchronous flow, and the run goes on', async () => { - await Bun.write('locked.file', 'foo'); - await Bun.write('my.file', 'foo'); + fs.writeFileSync('locked.file', 'foo'); + fs.writeFileSync('my.file', 'foo'); fs.chmodSync('locked.file', 0); const result = await rr('o', 'x', { @@ -80,6 +80,6 @@ describe('#708 the number of files in work at the same time is limited', () => { expect(result.out).toBe('my.file\n'); expect(result.err).toContain('EACCES'); expect(result.exit).toBe(1); - expect(await Bun.file('my.file').text()).toBe('fxx'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fxx'); }); }); diff --git a/test/engine/fix/719.test.js b/test/engine/fix/719.test.js index 58220a93..17c1571f 100644 --- a/test/engine/fix/719.test.js +++ b/test/engine/fix/719.test.js @@ -1,21 +1,22 @@ // https://github.com/mathiasrw/rexreplace/issues/719 // Data piped in together with files was not used, and nothing told about it. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {rr} from '../../helper.js'; const warning = ' 🟡 Data is piped in, but no flag tells what to do with it. It is not used.\n\n'; const expectedOutput = 'my.file\n'; // Run on my.file (with the content `foobar`) with `x` piped in -async function piped(flags = {}, replacement = '_') { - await Bun.write('my.file', 'foobar'); +function piped(flags = {}, replacement = '_') { + fs.writeFileSync('my.file', 'foobar'); return rr('b', replacement, {includeGlob: ['my.file'], pipedData: 'x', ...flags}); } describe('#719 a warning when piped data is not used', () => { test('is given when data is piped in and files are also given', async () => { expect(await piped()).toEqual({out: expectedOutput, err: warning, exit: null}); - expect(await Bun.file('my.file').text()).toBe('foo_ar'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foo_ar'); }); test.each(['quiet', 'quietTotal'])('is not shown with --%s', async flag => { @@ -28,7 +29,7 @@ describe('#719 a warning when piped data is not used', () => { err: warning, exit: null, }); - expect(await Bun.file('my.file').text()).toBe('foo_ar'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foo_ar'); }); test('is not given when the piped data is used', async () => { @@ -42,7 +43,7 @@ describe('#719 a warning when piped data is not used', () => { err: '', exit: null, }); - expect(await Bun.file('my.file').text()).toBe('fooxar'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('fooxar'); }); test.each([null, '', '\n', '\r\n'])('is not given when %j is piped in', async pipedData => { diff --git a/test/engine/fix/726.test.js b/test/engine/fix/726.test.js index 4c1db156..573ed668 100644 --- a/test/engine/fix/726.test.js +++ b/test/engine/fix/726.test.js @@ -12,60 +12,60 @@ const flows = [ ]; // Make `name` with the content `foobar` in a directory that is not the working directory and give the absolute path of it -async function outside(name) { +function outside(name) { fs.mkdirSync('work'); fs.mkdirSync('other'); const file = path.resolve('other', name); - await Bun.write(file, 'foobar'); + fs.writeFileSync(file, 'foobar'); process.chdir('work'); return file; } describe('#726 a file given with an absolute path is changed', () => { test.each(flows)('as a path with %s', async (name, flags) => { - const file = await outside('my.file'); + const file = outside('my.file'); const result = await rr('o', 'x', {includeGlob: [file], ...flags}); expect(result).toEqual({out: file + '\n', err: '', exit: null}); - expect(await Bun.file(file).text()).toBe('fxxbar'); + expect(fs.readFileSync(file, 'utf8')).toBe('fxxbar'); expect(fs.readdirSync(path.dirname(file))).toEqual(['my.file']); }); test.each(flows)('as a glob with %s', async (name, flags) => { - const file = await outside('my.file'); + const file = outside('my.file'); const glob = path.join(path.dirname(file), '*.file'); const result = await rr('o', 'x', {includeGlob: [glob], ...flags}); expect(result).toEqual({out: file + '\n', err: '', exit: null}); - expect(await Bun.file(file).text()).toBe('fxxbar'); + expect(fs.readFileSync(file, 'utf8')).toBe('fxxbar'); expect(fs.readdirSync(path.dirname(file))).toEqual(['my.file']); }); test('in the working directory', async () => { - await Bun.write('my.file', 'foobar'); + fs.writeFileSync('my.file', 'foobar'); const file = path.resolve('my.file'); const result = await rr('o', 'x', {includeGlob: [file]}); expect(result).toEqual({out: file + '\n', err: '', exit: null}); - expect(await Bun.file(file).text()).toBe('fxxbar'); + expect(fs.readFileSync(file, 'utf8')).toBe('fxxbar'); expect(fs.readdirSync('.')).toEqual(['my.file']); }); test.each(flows)( '--keep-backup puts the backup next to the file with %s', async (name, flags) => { - const file = await outside('my.file'); + const file = outside('my.file'); await rr('o', 'x', {includeGlob: [file], keepBackup: true, ...flags}); const [backup, ...rest] = fs .readdirSync(path.dirname(file)) .filter(name => name !== 'my.file'); expect(rest).toEqual([]); expect(backup).toMatch(/^my\.file\..+\.backup$/); - expect(await Bun.file(path.join(path.dirname(file), backup)).text()).toBe('foobar'); - expect(await Bun.file(file).text()).toBe('fxxbar'); + expect(fs.readFileSync(path.join(path.dirname(file), backup), 'utf8')).toBe('foobar'); + expect(fs.readFileSync(file, 'utf8')).toBe('fxxbar'); expect(fs.readdirSync('.')).toEqual([]); } ); test('--replacement-js gets the path of the file as it is', async () => { - const file = await outside('my.file'); + const file = outside('my.file'); const result = await rr( 'foobar', '[file, dirpath, file_rel, dirpath_rel, typeof mtime].join("|")', @@ -88,13 +88,14 @@ describe('#726 a file given with an absolute path is changed', () => { test('a relative path still works from the working directory', async () => { fs.mkdirSync('sub'); - await Bun.write('sub/my.file', 'foobar'); + fs.mkdirSync('sub', {recursive: true}); + fs.writeFileSync('sub/my.file', 'foobar'); const result = await rr('o', 'x', { includeGlob: ['./sub/../sub/my.file'], keepBackup: true, }); expect(result.exit).toBe(null); - expect(await Bun.file('sub/my.file').text()).toBe('fxxbar'); + expect(fs.readFileSync('sub/my.file', 'utf8')).toBe('fxxbar'); expect(fs.readdirSync('sub')).toHaveLength(2); }); }); diff --git a/test/engine/utf8.test.js b/test/engine/utf8.test.js index ec9aed00..7070595e 100644 --- a/test/engine/utf8.test.js +++ b/test/engine/utf8.test.js @@ -1,6 +1,7 @@ // https://github.com/mathiasrw/rexreplace/issues/735 // Emoji and other UTF-8 in every way data comes in and goes out. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import path from 'path'; import {rr} from '../helper.js'; @@ -48,21 +49,19 @@ describe('#735 UTF-8 in the replacement', () => { describe('#735 UTF-8 in the content of a file', () => { test('is written as UTF-8 when changed in place', async () => { - await Bun.write('my.file', text); + fs.writeFileSync('my.file', text); await rr('🦖', '🦕', {includeGlob: ['my.file']}); - expect(Buffer.from(await Bun.file('my.file').arrayBuffer())).toEqual( - Buffer.from('æ 中 🦕 👩‍💻\r\n') - ); + expect(fs.readFileSync('my.file')).toEqual(Buffer.from('æ 中 🦕 👩‍💻\r\n')); }); test('is printed with --output', async () => { - await Bun.write('my.file', text); + fs.writeFileSync('my.file', text); const out = (await rr('🦖', '🦕', {includeGlob: ['my.file'], output: true})).out; expect(asUtf8(out)).toBe('æ 中 🦕 👩‍💻\r\n'); }); test('is printed with --output-match', async () => { - await Bun.write('my.file', text); + fs.writeFileSync('my.file', text); const config = {includeGlob: ['my.file'], outputMatch: true}; expect(asUtf8((await rr('🦖|👩‍💻', '_', config)).out)).toBe('🦖\n👩‍💻\n'); }); @@ -74,9 +73,9 @@ describe('#735 UTF-8 in piped data', () => { }); test('can be the replacement with --replacement-pipe', async () => { - await Bun.write('my.file', 'aXb'); + fs.writeFileSync('my.file', 'aXb'); await rr('X', '_', {includeGlob: ['my.file'], replacementPipe: true, pipedData: '🦖'}); - expect(await Bun.file('my.file').text()).toBe('a🦖b'); + expect(fs.readFileSync('my.file', 'utf8')).toBe('a🦖b'); }); }); @@ -84,19 +83,19 @@ describe('#735 UTF-8 in file names', () => { const name = '🦖 中.file'; test('a name is printed as it was given', async () => { - await Bun.write(name, 'foo'); + fs.writeFileSync(name, 'foo'); expect(asUtf8((await rr('o', 'x', {includeGlob: [name]})).out)).toBe(name + '\n'); - expect(await Bun.file(name).text()).toBe('fxx'); + expect(fs.readFileSync(name, 'utf8')).toBe('fxx'); }); test('a glob finds a name with emoji', async () => { - await Bun.write(name, 'foo'); - await Bun.write('other.file', 'foo'); + fs.writeFileSync(name, 'foo'); + fs.writeFileSync('other.file', 'foo'); expect(asUtf8((await rr('o', 'x', {includeGlob: ['*🦖*']})).out)).toBe(name + '\n'); }); test('a name is printed with its full path with --full-path', async () => { - await Bun.write(name, 'foo'); + fs.writeFileSync(name, 'foo'); const out = (await rr('o', 'x', {includeGlob: [name], fullPath: true})).out; expect(asUtf8(out)).toBe(path.resolve(name) + '\n'); }); @@ -104,26 +103,26 @@ describe('#735 UTF-8 in file names', () => { describe('#735 UTF-8 in the delimiters', () => { test('--name-delimiter, also with no tail', async () => { - await Bun.write('a.file', 'foo'); - await Bun.write('b.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const config = {includeGlob: ['*.file'], nameDelimiter: '🦖'}; expect(asUtf8((await rr('o', 'x', config)).out)).toBe('a.file🦖b.file🦖'); - await Bun.write('a.file', 'foo'); - await Bun.write('b.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const noTail = {...config, nameDelimiterTail: false}; expect(asUtf8((await rr('o', 'x', noTail)).out)).toBe('a.file🦖b.file'); }); test('--content-delimiter with a combined emoji', async () => { - await Bun.write('a.file', 'foo'); - await Bun.write('b.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const config = {includeGlob: ['*.file'], output: true, contentDelimiter: '👩‍💻'}; expect(asUtf8((await rr('o', 'x', config)).out)).toBe('fxx👩‍💻fxx👩‍💻'); }); test('--delimiter sets both', async () => { - await Bun.write('a.file', 'foo'); - await Bun.write('b.file', 'foo'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const config = {includeGlob: ['*.file'], output: true, delimiter: '🦕'}; const result = await rr('o', 'x', config); expect(asUtf8(result.out)).toBe('fxx🦕fxx🦕'); diff --git a/test/fuzzy/output.test.js b/test/fuzzy/output.test.js index d0a76666..d16a6999 100644 --- a/test/fuzzy/output.test.js +++ b/test/fuzzy/output.test.js @@ -1,7 +1,8 @@ import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {rr, random, ended} from '../helper.js'; -const read = names => Promise.all(names.map(name => Bun.file(name).text())); +const read = names => names.map(name => fs.readFileSync(name, 'utf8')); describe('--output', () => { test('sends exactly what would have been written to the files with the content delimiter after each', async () => { @@ -15,19 +16,21 @@ describe('--output', () => { // null is the default: a line break, unless the content already ends with one const contentDelimiter = rnd.pick([null, '', '\n', '\0', ', ']); for (const [n, name] of names.entries()) { - await Bun.write(name, texts[n]); + fs.writeFileSync(name, texts[n]); } const config = {includeGlob: names, output: true, contentDelimiter}; // As UTF-8 like stdout and the files, so half an emoji (from `.` without --unicode) becomes `�` in both const out = Buffer.from((await rr(pattern, '_', config)).out).toString(); - expect(await read(names)).toEqual(texts); + expect(read(names)).toEqual(texts); await rr(pattern, '_', {includeGlob: names}); expect(out, JSON.stringify({pattern, contentDelimiter, texts})).toBe( null === contentDelimiter - ? ended(...(await read(names))) - : (await read(names)).map(text => text + contentDelimiter).join('') + ? ended(...read(names)) + : read(names) + .map(text => text + contentDelimiter) + .join('') ); } }); From b86edd945c9a3241314e3a55ca4655647c1e9451 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 14:11:54 +1100 Subject: [PATCH 02/11] Write the files in the test itself, also where a helper ran the command --- test/cli/fix/691.test.js | 71 ++++++--- test/engine/fix/703.test.js | 299 +++++++++++++++++++++++++++++------- test/engine/fix/719.test.js | 40 +++-- 3 files changed, 322 insertions(+), 88 deletions(-) diff --git a/test/cli/fix/691.test.js b/test/cli/fix/691.test.js index bf2bbf4e..93db4cce 100644 --- a/test/cli/fix/691.test.js +++ b/test/cli/fix/691.test.js @@ -9,37 +9,62 @@ import {cli, sh} from '../../helper.js'; // With --output the name of the file goes to stderr const expectedOutput = 'my.file\n'; -// Replace each `o` in `foobar` with the replacement and return what the CLI gives back. -// Without the tail only the result of the replacement is given. -function replaceO(replacement, ...flags) { - fs.writeFileSync('my.file', 'foobar'); - return sh($`bun ${cli} o ${replacement} my.file --output --no-content-delimiter-tail ${flags}`); -} - +// Each test replaces each `o` in `foobar` with the replacement. Without the tail only the result of the replacement is given. describe('#691 replacement that used to crash the CLI', () => { test('a real line break is inserted as a line break', async () => { - expect(await replaceO('a\nb')).toEqual({out: 'fa\nba\nbbar', err: expectedOutput, exit: 0}); - expect((await replaceO('a\r\nb')).out).toBe('fa\r\nba\r\nbbar'); - expect((await replaceO('\u2028\u2029')).out).toBe('f\u2028\u2029\u2028\u2029bar'); + fs.writeFileSync('my.file', 'foobar'); + expect( + await sh($`bun ${cli} o ${'a\nb'} my.file --output --no-content-delimiter-tail`) + ).toEqual({out: 'fa\nba\nbbar', err: expectedOutput, exit: 0}); + expect( + (await sh($`bun ${cli} o ${'a\r\nb'} my.file --output --no-content-delimiter-tail`)).out + ).toBe('fa\r\nba\r\nbbar'); + expect( + (await sh($`bun ${cli} o ${'\u2028\u2029'} my.file --output --no-content-delimiter-tail`)).out + ).toBe('f\u2028\u2029\u2028\u2029bar'); }); test("`\\'` gives a single quote", async () => { - expect(await replaceO("\\'")).toEqual({out: "f''bar", err: expectedOutput, exit: 0}); + fs.writeFileSync('my.file', 'foobar'); + expect(await sh($`bun ${cli} o ${"\\'"} my.file --output --no-content-delimiter-tail`)).toEqual( + {out: "f''bar", err: expectedOutput, exit: 0} + ); }); test('a lone backslash at the end gives a backslash', async () => { - expect(await replaceO('\\')).toEqual({out: 'f\\\\bar', err: expectedOutput, exit: 0}); - expect((await replaceO('x\\')).out).toBe('fx\\x\\bar'); + fs.writeFileSync('my.file', 'foobar'); + expect(await sh($`bun ${cli} o ${'\\'} my.file --output --no-content-delimiter-tail`)).toEqual({ + out: 'f\\\\bar', + err: expectedOutput, + exit: 0, + }); + expect( + (await sh($`bun ${cli} o ${'x\\'} my.file --output --no-content-delimiter-tail`)).out + ).toBe('fx\\x\\bar'); }); test('a backslash in front of a real line break still joins the lines (as before)', async () => { - expect(await replaceO('a\\\nb')).toEqual({out: 'fababbar', err: expectedOutput, exit: 0}); - expect((await replaceO('a\\\r\nb')).out).toBe('fababbar'); - expect((await replaceO('a \\\n b \\\n c')).out).toBe('fa b ca b cbar'); + fs.writeFileSync('my.file', 'foobar'); + expect( + await sh($`bun ${cli} o ${'a\\\nb'} my.file --output --no-content-delimiter-tail`) + ).toEqual({out: 'fababbar', err: expectedOutput, exit: 0}); + expect( + (await sh($`bun ${cli} o ${'a\\\r\nb'} my.file --output --no-content-delimiter-tail`)).out + ).toBe('fababbar'); + expect( + ( + await sh( + $`bun ${cli} o ${'a \\\n b \\\n c'} my.file --output --no-content-delimiter-tail` + ) + ).out + ).toBe('fa b ca b cbar'); }); test('the replacement can not run as code', async () => { - const result = await replaceO("\\'+process.exit(3)//"); + fs.writeFileSync('my.file', 'foobar'); + const result = await sh( + $`bun ${cli} o ${"\\'+process.exit(3)//"} my.file --output --no-content-delimiter-tail` + ); expect(result).toEqual({ out: "f'+process.exit(3)//'+process.exit(3)//bar", err: expectedOutput, @@ -48,11 +73,14 @@ describe('#691 replacement that used to crash the CLI', () => { }); test('an invalid escape sequence gives an error message and no stack trace', async () => { - const result = await replaceO('\\x4'); + fs.writeFileSync('my.file', 'foobar'); + const result = await sh($`bun ${cli} o ${'\\x4'} my.file --output --no-content-delimiter-tail`); expect(result.out).toBe(''); expect(result.err).toBe(' ❌ Invalid escape sequence in replacement: \\x4\n\n'); expect(result.exit).toBe(1); - expect(await replaceO('\\x4', '--quiet-total')).toEqual({out: '', err: '', exit: 1}); + expect( + await sh($`bun ${cli} o ${'\\x4'} my.file --output --no-content-delimiter-tail --quiet-total`) + ).toEqual({out: '', err: '', exit: 1}); }); test.each([ @@ -67,7 +95,10 @@ describe('#691 replacement that used to crash the CLI', () => { ['"', '"'], ['`${1}`', '`${1}`'], ])('%j still gives %j', async (replacement, expected) => { - expect(await replaceO(replacement)).toEqual({ + fs.writeFileSync('my.file', 'foobar'); + expect( + await sh($`bun ${cli} o ${replacement} my.file --output --no-content-delimiter-tail`) + ).toEqual({ out: `f${expected}${expected}bar`, err: expectedOutput, exit: 0, diff --git a/test/engine/fix/703.test.js b/test/engine/fix/703.test.js index 56e665ec..c078ce27 100644 --- a/test/engine/fix/703.test.js +++ b/test/engine/fix/703.test.js @@ -7,17 +7,17 @@ import {rr} from '../../helper.js'; // Note that rr() from the helper sets voidAsync per default, so the names come in the order of the file list -// Replace in a.file, b.file and c.file where b.file has nothing to replace -function run(flags = {}) { - fs.writeFileSync('a.file', 'foo'); - fs.writeFileSync('b.file', 'bar'); - fs.writeFileSync('c.file', 'foo'); - return rr('o', 'x', {includeGlob: ['*.file'], ...flags}); -} - +// Most tests replace in a.file, b.file and c.file, where b.file has nothing to replace describe('#703 the names of affected files', () => { test('go to stdout with one name on each line', async () => { - expect(await run()).toEqual({out: 'a.file\nc.file\n', err: '', exit: null}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file']})).toEqual({ + out: 'a.file\nc.file\n', + err: '', + exit: null, + }); expect(fs.readFileSync('a.file', 'utf8')).toBe('fxx'); }); @@ -27,20 +27,33 @@ describe('#703 the names of affected files', () => { ); test('all come with the async flow', async () => { - const result = await run({voidAsync: false}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const result = await rr('o', 'x', {includeGlob: ['*.file'], voidAsync: false}); expect(result.out.split('\n').sort()).toEqual(['', 'a.file', 'c.file']); expect(result.err).toBe(''); }); test('are alone on stdout with --verbose', async () => { - const result = await run({verbose: true}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const result = await rr('o', 'x', {includeGlob: ['*.file'], verbose: true}); expect(result.out).toBe('a.file\nc.file\n'); expect(result.err).toContain('Open sync: a.file'); expect(result.err).toContain('Nothing changed in: b.file'); }); test.each(['quiet', 'quietTotal'])('are not printed with --%s', async flag => { - expect(await run({[flag]: true})).toEqual({out: '', err: '', exit: null}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], [flag]: true})).toEqual({ + out: '', + err: '', + exit: null, + }); expect(fs.readFileSync('c.file', 'utf8')).toBe('fxx'); }); @@ -64,7 +77,14 @@ describe('#703 the names of affected files', () => { describe('--null-out', () => { test('ends each name with a null byte', async () => { - expect(await run({nullOut: true})).toEqual({out: 'a.file\0c.file\0', err: '', exit: null}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], nullOut: true})).toEqual({ + out: 'a.file\0c.file\0', + err: '', + exit: null, + }); }); test('keeps a name with a line break in one piece', async () => { @@ -77,7 +97,10 @@ describe('#703 the names of affected files', () => { describe('--name-delimiter', () => { test('is put after each name', async () => { - expect(await run({nameDelimiter: ', '})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], nameDelimiter: ', '})).toEqual({ out: 'a.file, c.file, ', err: '', exit: null, @@ -85,22 +108,56 @@ describe('#703 the names of affected files', () => { }); test('can be an empty string', async () => { - expect((await run({nameDelimiter: ''})).out).toBe('a.filec.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], nameDelimiter: ''})).out).toBe( + 'a.filec.file' + ); }); test('has the € and § aliases like the replacement', async () => { - expect((await run({nameDelimiter: '€§'})).out).toBe('a.file$\\c.file$\\'); - expect((await run({nameDelimiter: '€§', voidEuro: true})).out).toBe('a.file€\\c.file€\\'); - expect((await run({nameDelimiter: '€§', voidSection: true})).out).toBe('a.file$§c.file$§'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], nameDelimiter: '€§'})).out).toBe( + 'a.file$\\c.file$\\' + ); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + (await rr('o', 'x', {includeGlob: ['*.file'], nameDelimiter: '€§', voidEuro: true})).out + ).toBe('a.file€\\c.file€\\'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + (await rr('o', 'x', {includeGlob: ['*.file'], nameDelimiter: '€§', voidSection: true})).out + ).toBe('a.file$§c.file$§'); }); }); describe('--no-name-delimiter-tail', () => { test('puts the delimiter only between the names', async () => { - expect((await run({nameDelimiter: ', ', nameDelimiterTail: false})).out).toBe( - 'a.file, c.file' + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + ( + await rr('o', 'x', { + includeGlob: ['*.file'], + nameDelimiter: ', ', + nameDelimiterTail: false, + }) + ).out + ).toBe('a.file, c.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], nameDelimiterTail: false})).out).toBe( + 'a.file\nc.file' ); - expect((await run({nameDelimiterTail: false})).out).toBe('a.file\nc.file'); }); test('prints no delimiter when only one file is changed', async () => { @@ -114,10 +171,22 @@ describe('#703 the names of affected files', () => { }); test('does not start the next run with a delimiter', async () => { - await run({nameDelimiter: ', ', nameDelimiterTail: false}); - expect((await run({nameDelimiter: ', ', nameDelimiterTail: false})).out).toBe( - 'a.file, c.file' - ); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + await rr('o', 'x', {includeGlob: ['*.file'], nameDelimiter: ', ', nameDelimiterTail: false}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + ( + await rr('o', 'x', { + includeGlob: ['*.file'], + nameDelimiter: ', ', + nameDelimiterTail: false, + }) + ).out + ).toBe('a.file, c.file'); }); // The names must not be held back until all files are done @@ -163,7 +232,10 @@ describe('#703 the names of affected files', () => { describe('--name-all', () => { test('also names the files where nothing is changed', async () => { - expect(await run({nameAll: true})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], nameAll: true})).toEqual({ out: 'a.file\nb.file\nc.file\n', err: '', exit: null, @@ -172,7 +244,10 @@ describe('#703 the names of affected files', () => { }); test('names every file printed with --output', async () => { - expect(await run({nameAll: true, output: true})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], nameAll: true, output: true})).toEqual({ out: 'fxx\nbar\nfxx\n', err: 'a.file\nb.file\nc.file\n', exit: null, @@ -190,8 +265,15 @@ describe('#703 the names of affected files', () => { describe('--content-delimiter', () => { test('is a line break after each file and each match by default', async () => { - expect((await run({output: true})).out).toBe('fxx\nbar\nfxx\n'); - expect((await run({outputMatch: true})).out).toBe('o\no\no\no\n'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], output: true})).out).toBe( + 'fxx\nbar\nfxx\n' + ); + expect((await rr('o', 'x', {includeGlob: ['*.file'], outputMatch: true})).out).toBe( + 'o\no\no\no\n' + ); }); test('adds no line break by default to content that already ends with one', async () => { @@ -203,17 +285,32 @@ describe('#703 the names of affected files', () => { }); test('is put after the content of each file', async () => { - expect((await run({output: true, contentDelimiter: '\t'})).out).toBe('fxx\tbar\tfxx\t'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + (await rr('o', 'x', {includeGlob: ['*.file'], output: true, contentDelimiter: '\t'})).out + ).toBe('fxx\tbar\tfxx\t'); }); test('is put after each match with --output-match, and not after the names', async () => { - const result = await run({outputMatch: true, contentDelimiter: ', '}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const result = await rr('o', 'x', { + includeGlob: ['*.file'], + outputMatch: true, + contentDelimiter: ', ', + }); expect(result).toEqual({out: 'o, o, o, o, ', err: 'a.file\nc.file\n', exit: null}); }); test('is only put between the content with --no-content-delimiter-tail', async () => { const config = {output: true, contentDelimiter: ', ', contentDelimiterTail: false}; - expect((await run(config)).out).toBe('fxx, bar, fxx'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], ...config})).out).toBe('fxx, bar, fxx'); }); test('leaves piped data as it is by default', async () => { @@ -238,16 +335,28 @@ describe('#703 the names of affected files', () => { describe('--tail', () => { test('puts the delimiter after the last name and the last content', async () => { - expect(await run({output: true, delimiter: ', ', tail: true})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + await rr('o', 'x', {includeGlob: ['*.file'], output: true, delimiter: ', ', tail: true}) + ).toEqual({ out: 'fxx, bar, fxx, ', err: 'a.file, c.file, ', exit: null, }); - expect((await run({outputMatch: true, tail: true})).out).toBe('o\no\no\no\n'); + expect( + (await rr('o', 'x', {includeGlob: ['*.file'], outputMatch: true, tail: true})).out + ).toBe('o\no\no\no\n'); }); test('--no-tail puts the delimiter only between the names and the content', async () => { - expect(await run({output: true, delimiter: ', ', tail: false})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + await rr('o', 'x', {includeGlob: ['*.file'], output: true, delimiter: ', ', tail: false}) + ).toEqual({ out: 'fxx, bar, fxx', err: 'a.file, c.file', exit: null, @@ -256,7 +365,10 @@ describe('#703 the names of affected files', () => { test('the specific tail flags win', async () => { const config = {output: true, delimiter: ', ', tail: false, nameDelimiterTail: true}; - expect(await run(config)).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], ...config})).toEqual({ out: 'fxx, bar, fxx', err: 'a.file, c.file, ', exit: null, @@ -267,12 +379,20 @@ describe('#703 the names of affected files', () => { describe('--nl', () => { test('ends the names with a line break', async () => { const config = {nameDelimiter: ', ', nameDelimiterTail: false, nl: true}; - expect((await run(config)).out).toBe('a.file, c.file\n'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], ...config})).out).toBe( + 'a.file, c.file\n' + ); }); test('ends the content and the names on stderr with a line break', async () => { const config = {outputMatch: true, delimiter: ', ', tail: false, nl: true}; - expect(await run(config)).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], ...config})).toEqual({ out: 'o, o, o, o\n', err: 'a.file, c.file\n', exit: null, @@ -301,14 +421,22 @@ describe('#703 the names of affected files', () => { contentDelimiterTail: true, nl: true, }; - expect((await run(config)).out).toBe('o, o, o, o, \n'); - expect((await run({outputMatch: true, tail: true, nl: true})).out).toBe('o\no\no\no\n'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], ...config})).out).toBe('o, o, o, o, \n'); + expect( + (await rr('o', 'x', {includeGlob: ['*.file'], outputMatch: true, tail: true, nl: true})).out + ).toBe('o\no\no\no\n'); }); }); describe('--delimiter and --null-out', () => { test('--delimiter sets both delimiters', async () => { - expect(await run({output: true, delimiter: ', '})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], output: true, delimiter: ', '})).toEqual({ out: 'fxx, bar, fxx, ', err: 'a.file, c.file, ', exit: null, @@ -316,7 +444,10 @@ describe('#703 the names of affected files', () => { }); test('--null-out is the same as --delimiter with a null byte', async () => { - expect(await run({output: true, nullOut: true})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], output: true, nullOut: true})).toEqual({ out: 'fxx\0bar\0fxx\0', err: 'a.file\0c.file\0', exit: null, @@ -324,18 +455,36 @@ describe('#703 the names of affected files', () => { }); test('the most specific delimiter wins', async () => { - expect((await run({output: true, delimiter: ', ', nullOut: true})).out).toBe( - 'fxx, bar, fxx, ' - ); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + ( + await rr('o', 'x', { + includeGlob: ['*.file'], + output: true, + delimiter: ', ', + nullOut: true, + }) + ).out + ).toBe('fxx, bar, fxx, '); expect( - await run({ + await rr('o', 'x', { + includeGlob: ['*.file'], output: true, delimiter: ', ', contentDelimiter: '|', nameDelimiter: ';', }) ).toEqual({out: 'fxx|bar|fxx|', err: 'a.file;c.file;', exit: null}); - expect(await run({output: true, nullOut: true, nameDelimiter: ', '})).toEqual({ + expect( + await rr('o', 'x', { + includeGlob: ['*.file'], + output: true, + nullOut: true, + nameDelimiter: ', ', + }) + ).toEqual({ out: 'fxx\0bar\0fxx\0', err: 'a.file, c.file, ', exit: null, @@ -349,30 +498,65 @@ describe('#703 the names of affected files', () => { ['outputMatch', 'o\no\no\no\n'], ])('with --%s', (flag, out) => { test('go to stderr', async () => { - expect(await run({[flag]: true})).toEqual({out, err: 'a.file\nc.file\n', exit: null}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], [flag]: true})).toEqual({ + out, + err: 'a.file\nc.file\n', + exit: null, + }); expect(fs.readFileSync('a.file', 'utf8')).toBe('foo'); }); test('are the same names as without the flag', async () => { - expect((await run({[flag]: true})).err).toBe((await run()).out); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], [flag]: true})).err).toBe( + (await rr('o', 'x', {includeGlob: ['*.file']})).out + ); }); test.each(['quiet', 'quietTotal'])('are not printed with --%s', async quiet => { - expect(await run({[flag]: true, [quiet]: true})).toEqual({out, err: '', exit: null}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], [flag]: true, [quiet]: true})).toEqual({ + out, + err: '', + exit: null, + }); }); test('follow --null-out, --name-delimiter and --full-path', async () => { - expect((await run({[flag]: true, nullOut: true})).err).toBe('a.file\0c.file\0'); - expect((await run({[flag]: true, nameDelimiter: ', '})).err).toBe('a.file, c.file, '); - expect((await run({[flag]: true, fullPath: true})).err).toBe( - path.resolve('a.file') + '\n' + path.resolve('c.file') + '\n' + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], [flag]: true, nullOut: true})).err).toBe( + 'a.file\0c.file\0' ); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + (await rr('o', 'x', {includeGlob: ['*.file'], [flag]: true, nameDelimiter: ', '})).err + ).toBe('a.file, c.file, '); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect( + (await rr('o', 'x', {includeGlob: ['*.file'], [flag]: true, fullPath: true})).err + ).toBe(path.resolve('a.file') + '\n' + path.resolve('c.file') + '\n'); }); }); describe('--full-path', () => { test('prints the full path of each name', async () => { - expect(await run({fullPath: true})).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rr('o', 'x', {includeGlob: ['*.file'], fullPath: true})).toEqual({ out: path.resolve('a.file') + '\n' + path.resolve('c.file') + '\n', err: '', exit: null, @@ -393,7 +577,10 @@ describe('#703 the names of affected files', () => { ['null', {nullOut: true}, '\0', '\0'], ['name-delimiter', {nameDelimiter: ', ', nameDelimiterTail: false}, ', ', ''], ])('works with --%s', async (name, flags, between, last) => { - expect((await run({fullPath: true, ...flags})).out).toBe( + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('o', 'x', {includeGlob: ['*.file'], fullPath: true, ...flags})).out).toBe( path.resolve('a.file') + between + path.resolve('c.file') + last ); }); diff --git a/test/engine/fix/719.test.js b/test/engine/fix/719.test.js index 17c1571f..26d4774b 100644 --- a/test/engine/fix/719.test.js +++ b/test/engine/fix/719.test.js @@ -7,24 +7,29 @@ import {rr} from '../../helper.js'; const warning = ' 🟡 Data is piped in, but no flag tells what to do with it. It is not used.\n\n'; const expectedOutput = 'my.file\n'; -// Run on my.file (with the content `foobar`) with `x` piped in -function piped(flags = {}, replacement = '_') { - fs.writeFileSync('my.file', 'foobar'); - return rr('b', replacement, {includeGlob: ['my.file'], pipedData: 'x', ...flags}); -} - describe('#719 a warning when piped data is not used', () => { test('is given when data is piped in and files are also given', async () => { - expect(await piped()).toEqual({out: expectedOutput, err: warning, exit: null}); + fs.writeFileSync('my.file', 'foobar'); + expect(await rr('b', '_', {includeGlob: ['my.file'], pipedData: 'x'})).toEqual({ + out: expectedOutput, + err: warning, + exit: null, + }); expect(fs.readFileSync('my.file', 'utf8')).toBe('foo_ar'); }); test.each(['quiet', 'quietTotal'])('is not shown with --%s', async flag => { - expect(await piped({[flag]: true})).toEqual({out: '', err: '', exit: null}); + fs.writeFileSync('my.file', 'foobar'); + expect(await rr('b', '_', {includeGlob: ['my.file'], pipedData: 'x', [flag]: true})).toEqual({ + out: '', + err: '', + exit: null, + }); }); test('does not stop the run with --halt', async () => { - expect(await piped({halt: true})).toEqual({ + fs.writeFileSync('my.file', 'foobar'); + expect(await rr('b', '_', {includeGlob: ['my.file'], pipedData: 'x', halt: true})).toEqual({ out: expectedOutput, err: warning, exit: null, @@ -33,12 +38,18 @@ describe('#719 a warning when piped data is not used', () => { }); test('is not given when the piped data is used', async () => { - expect(await piped({replacementPipe: true})).toEqual({ + fs.writeFileSync('my.file', 'foobar'); + expect( + await rr('b', '_', {includeGlob: ['my.file'], pipedData: 'x', replacementPipe: true}) + ).toEqual({ out: expectedOutput, err: '', exit: null, }); - expect(await piped({replacementJs: true}, 'pipe')).toEqual({ + fs.writeFileSync('my.file', 'foobar'); + expect( + await rr('b', 'pipe', {includeGlob: ['my.file'], pipedData: 'x', replacementJs: true}) + ).toEqual({ out: expectedOutput, err: '', exit: null, @@ -47,7 +58,12 @@ describe('#719 a warning when piped data is not used', () => { }); test.each([null, '', '\n', '\r\n'])('is not given when %j is piped in', async pipedData => { - expect(await piped({pipedData})).toEqual({out: expectedOutput, err: '', exit: null}); + fs.writeFileSync('my.file', 'foobar'); + expect(await rr('b', '_', {includeGlob: ['my.file'], pipedData})).toEqual({ + out: expectedOutput, + err: '', + exit: null, + }); }); test('is not given when only data is piped in', async () => { From fb83f6e1730b7b56c026cc8a20f8ea6eebe69db3 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 14:12:32 +1100 Subject: [PATCH 03/11] Write the files in the test itself in the tests for #726 and #728 --- test/engine/fix/726.test.js | 35 +++++++++++++++++++++-------------- test/engine/fix/728.test.js | 18 ++++++++---------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/test/engine/fix/726.test.js b/test/engine/fix/726.test.js index 573ed668..4d667901 100644 --- a/test/engine/fix/726.test.js +++ b/test/engine/fix/726.test.js @@ -11,19 +11,14 @@ const flows = [ ['--void-async', {voidAsync: true}], ]; -// Make `name` with the content `foobar` in a directory that is not the working directory and give the absolute path of it -function outside(name) { - fs.mkdirSync('work'); - fs.mkdirSync('other'); - const file = path.resolve('other', name); - fs.writeFileSync(file, 'foobar'); - process.chdir('work'); - return file; -} - +// The file is in a folder that is not the working folder, so it can only be given with its absolute path describe('#726 a file given with an absolute path is changed', () => { test.each(flows)('as a path with %s', async (name, flags) => { - const file = outside('my.file'); + fs.mkdirSync('work'); + fs.mkdirSync('other'); + const file = path.resolve('other', 'my.file'); + fs.writeFileSync(file, 'foobar'); + process.chdir('work'); const result = await rr('o', 'x', {includeGlob: [file], ...flags}); expect(result).toEqual({out: file + '\n', err: '', exit: null}); expect(fs.readFileSync(file, 'utf8')).toBe('fxxbar'); @@ -31,7 +26,11 @@ describe('#726 a file given with an absolute path is changed', () => { }); test.each(flows)('as a glob with %s', async (name, flags) => { - const file = outside('my.file'); + fs.mkdirSync('work'); + fs.mkdirSync('other'); + const file = path.resolve('other', 'my.file'); + fs.writeFileSync(file, 'foobar'); + process.chdir('work'); const glob = path.join(path.dirname(file), '*.file'); const result = await rr('o', 'x', {includeGlob: [glob], ...flags}); expect(result).toEqual({out: file + '\n', err: '', exit: null}); @@ -51,7 +50,11 @@ describe('#726 a file given with an absolute path is changed', () => { test.each(flows)( '--keep-backup puts the backup next to the file with %s', async (name, flags) => { - const file = outside('my.file'); + fs.mkdirSync('work'); + fs.mkdirSync('other'); + const file = path.resolve('other', 'my.file'); + fs.writeFileSync(file, 'foobar'); + process.chdir('work'); await rr('o', 'x', {includeGlob: [file], keepBackup: true, ...flags}); const [backup, ...rest] = fs .readdirSync(path.dirname(file)) @@ -65,7 +68,11 @@ describe('#726 a file given with an absolute path is changed', () => { ); test('--replacement-js gets the path of the file as it is', async () => { - const file = outside('my.file'); + fs.mkdirSync('work'); + fs.mkdirSync('other'); + const file = path.resolve('other', 'my.file'); + fs.writeFileSync(file, 'foobar'); + process.chdir('work'); const result = await rr( 'foobar', '[file, dirpath, file_rel, dirpath_rel, typeof mtime].join("|")', diff --git a/test/engine/fix/728.test.js b/test/engine/fix/728.test.js index 8c1422bc..30c08e57 100644 --- a/test/engine/fix/728.test.js +++ b/test/engine/fix/728.test.js @@ -10,17 +10,12 @@ const flows = [ ['--void-async', {voidAsync: true}], ]; -// Make a.file and b.file with the content `foo`, where b.file can not be read -function oneUnreadable() { - fs.writeFileSync('a.file', 'foo'); - fs.writeFileSync('b.file', 'foo'); - fs.chmodSync('b.file', 0); - return {includeGlob: ['a.file', 'b.file']}; -} - describe('#728 the exit code is 1 when a file gives an error', () => { test.each(flows)('and the other files are still changed with %s', async (name, flags) => { - const result = await rr('o', 'x', {...oneUnreadable(), ...flags}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.chmodSync('b.file', 0); + const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], ...flags}); expect(result.out).toBe('a.file\n'); expect(result.err).toContain('EACCES'); expect(result.exit).toBe(1); @@ -45,6 +40,9 @@ describe('#728 the exit code is 1 when a file gives an error', () => { }); test.each(['quiet', 'quietTotal'])('also with --%s', async flag => { - expect((await rr('o', 'x', {...oneUnreadable(), [flag]: true})).exit).toBe(1); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.chmodSync('b.file', 0); + expect((await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], [flag]: true})).exit).toBe(1); }); }); From db32953f1273cef1584a6dccf9abd7a33853d814 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 14:17:13 +1100 Subject: [PATCH 04/11] =?UTF-8?q?Start=20every=20error=20with=20=E2=9D=8C,?= =?UTF-8?q?=20and=20make=200=20files=20found=20a=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ src/engine.ts | 3 +- src/output.ts | 3 +- test/cli/debug.test.js | 6 ++-- test/cli/fix/742.test.js | 6 ++-- test/cli/run.ai.sh | 10 +++--- test/cli/run.sh | 11 ++++-- test/engine/basics.test.js | 6 ++-- test/engine/exclude-glob.test.js | 2 +- test/engine/exclude-re.test.js | 2 +- test/engine/fix/681.test.js | 13 ++++--- test/engine/fix/682.test.js | 8 ++--- test/engine/fix/683.test.js | 2 +- test/engine/fix/711.test.js | 4 +-- test/engine/fix/718.test.js | 6 ++-- test/engine/fix/720.test.js | 54 ++++++++++++++++++++++++++++ test/engine/halt.test.js | 16 ++++++--- test/engine/quiet-total.test.js | 4 ++- test/engine/quiet.test.js | 6 ++-- test/engine/replacement-pipe.test.js | 2 +- 20 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 test/engine/fix/720.test.js diff --git a/README.md b/README.md index 65a440af..b86ef4e4 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,8 @@ Files identified for potential processing are identified by: - Piped data that is printed is left as it came in, so no delimiter is added after the end of it. Matches from `--output-match` are different: they are new output, so they get the delimiter whether they come from a file or from piped data. +- Finding no files is a warning (🟡) and not an error, so the exit code is 0. Nothing went wrong, there was just nothing to do. Errors start with ❌ and give the exit code 1. + ### Limitations diff --git a/src/engine.ts b/src/engine.ts index 0bd0fb82..37f7c830 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -136,8 +136,9 @@ async function run(config) { config.files = getFilePaths(config); + // Nothing to do is not an error, so a glob or a pipe that gives no files does not stop a script if (!config.files.length) { - return error(config.files.length + ' files found'); + return warn('0 files found'); } chat(config.files.length + ' files found'); diff --git a/src/output.ts b/src/output.ts index 985cd895..ed746629 100644 --- a/src/output.ts +++ b/src/output.ts @@ -110,7 +110,8 @@ export const error = function (msg, data = '') { const text = msg + (data ? ' ' + data : ''); config.statusInfo?.errors.push(text); if (!config.quietTotal) { - write(font.red(text) + '\n', process.stderr); + // The line break gives an empty line under the error + write(font.red(' ❌ ' + text) + '\n\n', process.stderr); } if (config.halt) { throw new Panic(text); diff --git a/test/cli/debug.test.js b/test/cli/debug.test.js index b2e48d55..3cba492f 100644 --- a/test/cli/debug.test.js +++ b/test/cli/debug.test.js @@ -39,10 +39,12 @@ describe('--debug', () => { }); test('gives the errors in the status report, and the exit code 1', async () => { - const result = await sh($`bun ${cli} o x nope.file --debug`); + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + const result = await sh($`bun ${cli} o x a.file --debug`); expect(result.err).toBe(''); expect(result.exit).toBe(1); - expect(JSON.parse(result.out).errors).toEqual(['0 files found']); + expect(JSON.parse(result.out).errors[0]).toContain('EACCES'); }); test('gives the error of an invalid pattern in the status report', async () => { diff --git a/test/cli/fix/742.test.js b/test/cli/fix/742.test.js index ff2d6285..0ec96d7d 100644 --- a/test/cli/fix/742.test.js +++ b/test/cli/fix/742.test.js @@ -34,8 +34,10 @@ describe('#742 the CLI', () => { ); test('sets the exit code to 1 when a file gives an error, and prints the status report', async () => { - const result = await sh($`bun ${cli} o x nope.file --status-compact --quiet-total`); - expect(JSON.parse(result.out).errors).toEqual(['0 files found']); + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + const result = await sh($`bun ${cli} o x a.file --status-compact --quiet-total`); + expect(JSON.parse(result.out).errors[0]).toContain('EACCES'); expect(result.exit).toBe(1); }); diff --git a/test/cli/run.ai.sh b/test/cli/run.ai.sh index 01b69976..55ea7d21 100644 --- a/test/cli/run.ai.sh +++ b/test/cli/run.ai.sh @@ -93,17 +93,15 @@ fi reset # Successful run should have NO stdout echo assert "rexreplace foo xxx my.file -q" "" -# Error run (file not found) should still print error to stderr (tested indirectly via assert_raises failure message) -# We can't easily assert stderr content with aserta, but we can check it doesn't suppress errors needed for failure -assert_raises "rexreplace foo xxx non_existent_file.txt -q" 1 # Exit code should still be non-zero +# No files found is a warning, so the exit code is 0 +assert_raises "rexreplace foo xxx non_existent_file.txt -q" 0 # -Q, --quiet-total reset # Successful run should have NO stdout echo assert "rexreplace foo xxx my.file -Q" "" -# Error run should have NO output at all (stdout or stderr) -# Check exit code is still non-zero, but no output should be printed by aserta on failure if rexreplace is truly quiet -assert_raises "rexreplace foo xxx non_existent_file.txt -Q" 1 +# No files found is a warning, so the exit code is 0 +assert_raises "rexreplace foo xxx non_existent_file.txt -Q" 0 # -H, --halt reset diff --git a/test/cli/run.sh b/test/cli/run.sh index 341a7172..06596d6e 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -54,10 +54,15 @@ assert_failure "rexreplace my-vile" assert_failure "rexreplace o-ho" -# --halt (or --bail) stops at the first error +# --halt (or --bail) stops at the first error, here that my.file can not be read reset -assert_raises "rexreplace o x nope.file --halt" 1 -assert_raises "rexreplace o x nope.file --bail" 1 +chmod 000 my.file +assert_raises "rexreplace b x my.file your.file --halt" 1 +assert_raises "rexreplace b x my.file your.file --bail" 1 +assert "cat your.file" "abc123" + +# Finding no files is a warning and not an error +assert_raises "rexreplace o x nope.file" 0 # --output prints the result and leaves the file as it is diff --git a/test/engine/basics.test.js b/test/engine/basics.test.js index 06956eda..6dd1a679 100644 --- a/test/engine/basics.test.js +++ b/test/engine/basics.test.js @@ -176,13 +176,13 @@ describe('files', () => { fs.mkdirSync('dir.file'); fs.writeFileSync('.hidden.file', 'foobar'); const result = await rr('o', 'x', {includeGlob: ['*']}); - expect(result.err).toBe('0 files found\n'); + expect(result.err).toBe(' 🟡 0 files found\n\n'); expect(fs.readFileSync('.hidden.file', 'utf8')).toBe('foobar'); }); test('are reported if none are found', async () => { const result = await rr('o', 'x', {includeGlob: ['nope.file']}); - expect(result.err).toBe('0 files found\n'); + expect(result.err).toBe(' 🟡 0 files found\n\n'); expect(result.out).toBe(''); }); }); @@ -217,6 +217,6 @@ describe('piped data', () => { }); test('leaves nothing to do if neither data nor files are given', async () => { - expect(await rr('o', 'x')).toEqual({out: '', err: '0 files found\n', exit: 1}); + expect(await rr('o', 'x')).toEqual({out: '', err: ' 🟡 0 files found\n\n', exit: null}); }); }); diff --git a/test/engine/exclude-glob.test.js b/test/engine/exclude-glob.test.js index 7a63d4e3..19181d00 100644 --- a/test/engine/exclude-glob.test.js +++ b/test/engine/exclude-glob.test.js @@ -50,7 +50,7 @@ describe('--exclude-glob', () => { test('reports when all files are left out', async () => { fs.writeFileSync('my.file', 'foo'); const result = await rr('foo', 'x', {includeGlob: ['*.file'], excludeGlob: ['*.file']}); - expect(result.err).toBe('0 files found\n'); + expect(result.err).toBe(' 🟡 0 files found\n\n'); expect(fs.readFileSync('my.file', 'utf8')).toBe('foo'); }); diff --git a/test/engine/exclude-re.test.js b/test/engine/exclude-re.test.js index d62477c1..6e072c36 100644 --- a/test/engine/exclude-re.test.js +++ b/test/engine/exclude-re.test.js @@ -48,7 +48,7 @@ describe('--exclude-re', () => { test('reports when all files are left out', async () => { fs.writeFileSync('my.file', 'foo'); const result = await rr('foo', 'x', {includeGlob: ['my.file'], excludeRe: ['.']}); - expect(result.err).toBe('0 files found\n'); + expect(result.err).toBe(' 🟡 0 files found\n\n'); expect(fs.readFileSync('my.file', 'utf8')).toBe('foo'); }); diff --git a/test/engine/fix/681.test.js b/test/engine/fix/681.test.js index 357c981a..beef3631 100644 --- a/test/engine/fix/681.test.js +++ b/test/engine/fix/681.test.js @@ -1,12 +1,15 @@ // https://github.com/mathiasrw/rexreplace/issues/681 // `--halt` used the error message as exit code: Node 18 exited with 0, newer Node versions crashed with ERR_INVALID_ARG_TYPE. import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; import {rr} from '../../helper.js'; describe('#681 --halt exits with a numeric non-zero code', () => { - test('no files found', async () => { - const result = await rr('o', 'x', {includeGlob: ['nope.file'], halt: true}); - expect(result.err).toContain('0 files found'); + test('a file that can not be read', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + const result = await rr('o', 'x', {includeGlob: ['a.file'], halt: true}); + expect(result.err).toContain('EACCES'); expect(result.exit).toBe(1); }); @@ -17,8 +20,10 @@ describe('#681 --halt exits with a numeric non-zero code', () => { }); test('--quiet-total still halts with the exit code but shows no message', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); const result = await rr('o', 'x', { - includeGlob: ['nope.file'], + includeGlob: ['a.file'], halt: true, quietTotal: true, }); diff --git a/test/engine/fix/682.test.js b/test/engine/fix/682.test.js index ac6fc7db..73d23ecd 100644 --- a/test/engine/fix/682.test.js +++ b/test/engine/fix/682.test.js @@ -13,10 +13,10 @@ describe('#682 --quiet still displays errors', () => { expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); - test('no files found', async () => { - expect((await rr('o', 'x', {includeGlob: ['nope.file'], quiet: true})).err).toContain( - '0 files found' - ); + test('a file that can not be read', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + expect((await rr('o', 'x', {includeGlob: ['a.file'], quiet: true})).err).toContain('EACCES'); }); test('piped data with invalid regular expression', async () => { diff --git a/test/engine/fix/683.test.js b/test/engine/fix/683.test.js index aa9b6d63..2840a13c 100644 --- a/test/engine/fix/683.test.js +++ b/test/engine/fix/683.test.js @@ -15,7 +15,7 @@ describe('#683 --exclude-re follows the regex flags of the main search', () => { test('the example from the issue', async () => { fs.writeFileSync('a.file', 'x\n'); const config = {includeGlob: ['a.file'], excludeRe: ['A'], output: true}; - expect(await rr('x', 'y', config)).toEqual({out: '', err: '0 files found\n', exit: 1}); + expect(await rr('x', 'y', config)).toEqual({out: '', err: ' 🟡 0 files found\n\n', exit: null}); expect((await rr('x', 'y', {...config, voidIgnoreCase: true})).out).toBe('y\n'); }); diff --git a/test/engine/fix/711.test.js b/test/engine/fix/711.test.js index 477206b2..37a6b15d 100644 --- a/test/engine/fix/711.test.js +++ b/test/engine/fix/711.test.js @@ -65,8 +65,8 @@ describe('#711 --glob-pipe', () => { test('with nothing piped in and no arguments there are no files', async () => { expect(await rr('o', 'x', {pipedData: '', globPipe: true})).toEqual({ out: '', - err: '0 files found\n', - exit: 1, + err: ' 🟡 0 files found\n\n', + exit: null, }); }); diff --git a/test/engine/fix/718.test.js b/test/engine/fix/718.test.js index 4ec33705..4ff45080 100644 --- a/test/engine/fix/718.test.js +++ b/test/engine/fix/718.test.js @@ -57,7 +57,7 @@ describe('#718 together with globs', () => { test('a path to a folder is not a file, so it is still a glob', async () => { fs.mkdirSync('docs', {recursive: true}); fs.writeFileSync('docs/a.file', 'foo'); - expect((await rr('o', 'x', {includeGlob: ['docs']})).exit).toBe(1); + expect((await rr('o', 'x', {includeGlob: ['docs']})).err).toBe(' 🟡 0 files found\n\n'); expect((await rr('o', 'x', {includeGlob: ['docs/*.file']})).out).toBe('docs/a.file\n'); }); @@ -134,7 +134,7 @@ describe('#718 with --exclude-glob', () => { test('a glob leaves out a file given by its name', async () => { fs.writeFileSync('a[1].file', 'foo'); const config = {includeGlob: ['a[1].file'], excludeGlob: ['*.file']}; - expect(await rr('o', 'x', config)).toEqual({out: '', err: '0 files found\n', exit: 1}); + expect(await rr('o', 'x', config)).toEqual({out: '', err: ' 🟡 0 files found\n\n', exit: null}); expect(content('a[1].file')).toBe('foo'); }); @@ -162,7 +162,7 @@ describe('#718 with --exclude-re', () => { expect((await rr('o', 'x', {includeGlob: ['a.file'], excludeRe: ['^/']})).out).toBe('a.file\n'); fs.writeFileSync('a.file', 'foo'); const config = {includeGlob: ['a.file'], excludeRe: ['^/'], fullPath: true}; - expect((await rr('o', 'x', config)).exit).toBe(1); + expect((await rr('o', 'x', config)).err).toBe(' 🟡 0 files found\n\n'); expect(content('a.file')).toBe('foo'); }); }); diff --git a/test/engine/fix/720.test.js b/test/engine/fix/720.test.js new file mode 100644 index 00000000..e7095db5 --- /dev/null +++ b/test/engine/fix/720.test.js @@ -0,0 +1,54 @@ +// https://github.com/mathiasrw/rexreplace/issues/720 +// Only the errors that stopped the run started with ❌, so the other errors looked like the info around them. +// Finding no files was an error with the exit code 1, also when there was just nothing to do. +import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; +import {rr, rrStatus} from '../../helper.js'; + +describe('#720 every error starts with ❌', () => { + test('an error from a file, and the other files are still changed', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.chmodSync('a.file', 0); + const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file']}); + expect(result.out).toBe('b.file\n'); + expect(result.err).toStartWith(' ❌ '); + expect(result.err).toContain('EACCES'); + expect(result.err).toEndWith('\n\n'); + expect(result.exit).toBe(1); + }); + + test('an error that stops the run', async () => { + expect((await rr('(', 'x', {pipedData: 'foo'})).err).toStartWith( + ' ❌ Invalid regular expression' + ); + }); +}); + +describe('#720 finding no files is a warning', () => { + test('with the exit code 0', async () => { + expect(await rr('o', 'x', {includeGlob: ['nope.file']})).toEqual({ + out: '', + err: ' 🟡 0 files found\n\n', + exit: null, + }); + }); + + test('in the status report', async () => { + const status = await rrStatus('o', 'x', {includeGlob: ['nope.file']}); + expect(status.warnings).toEqual(['0 files found']); + expect(status.errors).toEqual([]); + }); + + test.each(['quiet', 'quietTotal'])('not shown with --%s', async flag => { + expect(await rr('o', 'x', {includeGlob: ['nope.file'], [flag]: true})).toEqual({ + out: '', + err: '', + exit: null, + }); + }); + + test('does not stop the run with --halt', async () => { + expect((await rr('o', 'x', {includeGlob: ['nope.file'], halt: true})).exit).toBe(null); + }); +}); diff --git a/test/engine/halt.test.js b/test/engine/halt.test.js index 80d65d17..c3518130 100644 --- a/test/engine/halt.test.js +++ b/test/engine/halt.test.js @@ -33,14 +33,18 @@ describe('--halt', () => { }); test('stops with a number as exit code', async () => { - expect((await rr('o', 'x', {includeGlob: ['nope.file'], halt: true})).exit).toBe(1); + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + expect((await rr('o', 'x', {includeGlob: ['a.file'], halt: true})).exit).toBe(1); expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).toBe(1); }); describe('combined', () => { test.each(['quiet', 'quietTotal'])('with --%s it still stops', async flag => { + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); const result = await rr('o', 'x', { - includeGlob: ['nope.file'], + includeGlob: ['a.file'], halt: true, [flag]: true, }); @@ -62,10 +66,14 @@ describe('--halt', () => { expect(await rr('f', 'x', config)).toEqual({out: 'Foo xoo', err: '', exit: null}); }); - test('with --exclude-glob leaving no files', async () => { + test('with --exclude-glob leaving no files it does not stop, as that is a warning', async () => { fs.writeFileSync('my.file', 'foobar'); const config = {includeGlob: ['my.file'], excludeGlob: ['*.file'], halt: true}; - expect((await rr('o', 'x', config)).exit).not.toBe(null); + expect(await rr('o', 'x', config)).toEqual({ + out: '', + err: ' 🟡 0 files found\n\n', + exit: null, + }); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); }); diff --git a/test/engine/quiet-total.test.js b/test/engine/quiet-total.test.js index 2edf9f14..80c5cf56 100644 --- a/test/engine/quiet-total.test.js +++ b/test/engine/quiet-total.test.js @@ -54,8 +54,10 @@ describe('--quiet-total', () => { }); test('with --halt it stops in silence', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); const result = await rr('o', 'x', { - includeGlob: ['nope.file'], + includeGlob: ['a.file'], quietTotal: true, halt: true, }); diff --git a/test/engine/quiet.test.js b/test/engine/quiet.test.js index becfb8ea..d0ffdd88 100644 --- a/test/engine/quiet.test.js +++ b/test/engine/quiet.test.js @@ -25,9 +25,9 @@ describe('--quiet', () => { expect((await rr('(', 'x', {pipedData: 'foobar', quiet: true})).err).toContain( 'Invalid regular expression' ); - expect((await rr('o', 'x', {includeGlob: ['nope.file'], quiet: true})).err).toContain( - '0 files found' - ); + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + expect((await rr('o', 'x', {includeGlob: ['a.file'], quiet: true})).err).toContain('EACCES'); }); describe('combined', () => { diff --git a/test/engine/replacement-pipe.test.js b/test/engine/replacement-pipe.test.js index 0aa21aac..ca544000 100644 --- a/test/engine/replacement-pipe.test.js +++ b/test/engine/replacement-pipe.test.js @@ -64,7 +64,7 @@ describe('--replacement-pipe', () => { test('does not treat the piped data as content to search in', async () => { const result = await rr('o', '_', {replacementPipe: true, pipedData: 'foo'}); - expect(result).toEqual({out: '', err: '0 files found\n', exit: 1}); + expect(result).toEqual({out: '', err: ' 🟡 0 files found\n\n', exit: null}); }); describe('combined', () => { From f96fc000ac8c2221b88b5e73d1bb52285f39d75e Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 14:42:40 +1100 Subject: [PATCH 05/11] Write the files in the test itself in the last tests with a helper --- test/engine/exclude-glob.test.js | 90 ++++++++++++--------- test/engine/exclude-re.test.js | 98 ++++++++++++----------- test/engine/fix/683.test.js | 73 ++++++++++++----- test/engine/fix/699.test.js | 121 ++++++++++++++++++----------- test/engine/halt.test.js | 13 ++-- test/engine/replacement-js.test.js | 117 +++++++++++++++++----------- 6 files changed, 313 insertions(+), 199 deletions(-) diff --git a/test/engine/exclude-glob.test.js b/test/engine/exclude-glob.test.js index 19181d00..1b3cd0f2 100644 --- a/test/engine/exclude-glob.test.js +++ b/test/engine/exclude-glob.test.js @@ -2,49 +2,61 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {rr} from '../helper.js'; -// Make `names` as files containing "foo" and return the ones changed by replacing "foo" -const changed = async (names, config) => { - names.forEach(name => { - fs.mkdirSync(name.replace(/[^/]*$/, '') || '.', {recursive: true}); - fs.writeFileSync(name, 'foo'); - }); - await rr('foo', 'x', config); - return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); -}; - describe('--exclude-glob', () => { test('leaves out the files found by the glob', async () => { const config = {includeGlob: ['*.file'], excludeGlob: ['y*']}; - expect(await changed(['my.file', 'your.file'], config)).toEqual(['my.file']); + fs.writeFileSync('my.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('my.file\n'); }); test('can be the name of a file', async () => { const config = {includeGlob: ['*.file'], excludeGlob: ['your.file']}; - expect(await changed(['my.file', 'your.file'], config)).toEqual(['my.file']); + fs.writeFileSync('my.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('my.file\n'); }); test('can be given several times', async () => { - const names = ['a.file', 'b.file', 'c.file']; - expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a.*', 'b.*']})).toEqual([ - 'c.file', - ]); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeGlob: ['a.*', 'b.*']})).out).toBe( + 'c.file\n' + ); }); test('understands braces and **', async () => { - const names = ['a.file', 'b.file', 'c.file', 'sub/a.file', 'sub/deep/d.file']; - expect(await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['{a,b}.file']})).toEqual( - ['c.file', 'sub/a.file', 'sub/deep/d.file'] + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + fs.mkdirSync('sub', {recursive: true}); + fs.writeFileSync('sub/a.file', 'foo'); + fs.mkdirSync('sub/deep', {recursive: true}); + fs.writeFileSync('sub/deep/d.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['**/*.file'], excludeGlob: ['{a,b}.file']})).out + ).toBe('c.file\nsub/a.file\nsub/deep/d.file\n'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + fs.mkdirSync('sub', {recursive: true}); + fs.writeFileSync('sub/a.file', 'foo'); + fs.mkdirSync('sub/deep', {recursive: true}); + fs.writeFileSync('sub/deep/d.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['**/*.file'], excludeGlob: ['sub/**']})).out).toBe( + 'a.file\nb.file\nc.file\n' ); - expect(await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['sub/**']})).toEqual([ - 'a.file', - 'b.file', - 'c.file', - ]); - expect(await changed(names, {includeGlob: ['**/*.file'], excludeGlob: ['**/a.file']})).toEqual([ - 'b.file', - 'c.file', - 'sub/deep/d.file', - ]); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + fs.mkdirSync('sub', {recursive: true}); + fs.writeFileSync('sub/a.file', 'foo'); + fs.mkdirSync('sub/deep', {recursive: true}); + fs.writeFileSync('sub/deep/d.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['**/*.file'], excludeGlob: ['**/a.file']})).out + ).toBe('b.file\nc.file\nsub/deep/d.file\n'); }); test('reports when all files are left out', async () => { @@ -56,20 +68,25 @@ describe('--exclude-glob', () => { test('makes no difference if it finds nothing', async () => { const config = {includeGlob: ['*.file'], excludeGlob: ['nope*', 'sub/**']}; - expect(await changed(['my.file'], config)).toEqual(['my.file']); + fs.writeFileSync('my.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('my.file\n'); }); describe('combined', () => { test('with --exclude-re both are used', async () => { - const names = ['a.file', 'b.file', 'c.file']; const config = {includeGlob: ['*.file'], excludeGlob: ['a.*'], excludeRe: ['^b']}; - expect(await changed(names, config)).toEqual(['c.file']); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('c.file\n'); }); test('with a negated glob', async () => { - const names = ['a.file', 'b.file', 'c.file']; const config = {includeGlob: ['*.file', '!a.file'], excludeGlob: ['b.*']}; - expect(await changed(names, config)).toEqual(['c.file']); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('c.file\n'); }); test.each(['output', 'outputMatch'])('with --%s only the files left are sent', async flag => { @@ -86,9 +103,10 @@ describe('--exclude-glob', () => { }); test('with --literal the glob is still a glob', async () => { - const names = ['a.file', 'b.file']; const config = {includeGlob: ['*.file'], excludeGlob: ['a.*'], literal: true}; - expect(await changed(names, config)).toEqual(['b.file']); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('b.file\n'); }); test('with piped data it has nothing to do', async () => { diff --git a/test/engine/exclude-re.test.js b/test/engine/exclude-re.test.js index 6e072c36..bbc436b2 100644 --- a/test/engine/exclude-re.test.js +++ b/test/engine/exclude-re.test.js @@ -2,47 +2,47 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {rr} from '../helper.js'; -// Make `names` as files containing "foo" and return the ones changed by replacing "foo" -const changed = async (names, config) => { - names.forEach(name => { - fs.mkdirSync(name.replace(/[^/]*$/, '') || '.', {recursive: true}); - fs.writeFileSync(name, 'foo'); - }); - await rr('foo', 'x', config); - return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); -}; - describe('--exclude-re', () => { test('leaves out files with a path matching the regex', async () => { const config = {includeGlob: ['*.file'], excludeRe: ['^y']}; - expect(await changed(['my.file', 'your.file'], config)).toEqual(['my.file']); + fs.writeFileSync('my.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('my.file\n'); }); test('matches anywhere in the path unless anchored', async () => { - const names = ['my.file', 'your.file']; - expect(await changed(names, {includeGlob: ['*.file'], excludeRe: ['y']})).toEqual([]); + fs.writeFileSync('my.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['y']})).out).toBe(''); }); test('is a regex', async () => { - const names = ['a1.file', 'a2.file', 'ab.file']; - expect(await changed(names, {includeGlob: ['*.file'], excludeRe: ['a\\d\\.file$']})).toEqual([ - 'ab.file', - ]); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('a2.file', 'foo'); + fs.writeFileSync('ab.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['a\\d\\.file$']})).out).toBe( + 'ab.file\n' + ); }); test('can be given several times', async () => { - const names = ['a.file', 'b.file', 'c.file']; - expect(await changed(names, {includeGlob: ['*.file'], excludeRe: ['^a', '^b']})).toEqual([ - 'c.file', - ]); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^a', '^b']})).out).toBe( + 'c.file\n' + ); }); test('sees the path of files in sub directories', async () => { - const names = ['a.file', 'sub/a.file', 'sub/deep/a.file']; - expect(await changed(names, {includeGlob: ['**/*.file'], excludeRe: ['^sub/[^/]+$']})).toEqual([ - 'a.file', - 'sub/deep/a.file', - ]); + fs.writeFileSync('a.file', 'foo'); + fs.mkdirSync('sub', {recursive: true}); + fs.writeFileSync('sub/a.file', 'foo'); + fs.mkdirSync('sub/deep', {recursive: true}); + fs.writeFileSync('sub/deep/a.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['**/*.file'], excludeRe: ['^sub/[^/]+$']})).out + ).toBe('a.file\nsub/deep/a.file\n'); }); test('reports when all files are left out', async () => { @@ -53,49 +53,59 @@ describe('--exclude-re', () => { }); test('makes no difference if it matches nothing', async () => { - expect(await changed(['my.file'], {includeGlob: ['*.file'], excludeRe: ['nope']})).toEqual([ - 'my.file', - ]); + fs.writeFileSync('my.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['nope']})).out).toBe( + 'my.file\n' + ); }); test('follows the regex flags of the main search', async () => { - const names = ['MY.file', 'your.file']; - expect(await changed(names, {includeGlob: ['*.file'], excludeRe: ['^my']})).toEqual([ - 'your.file', - ]); + fs.writeFileSync('MY.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^my']})).out).toBe( + 'your.file\n' + ); }); describe('combined', () => { test('with --exclude-glob both are used', async () => { - const names = ['a.file', 'b.file', 'c.file']; const config = {includeGlob: ['*.file'], excludeRe: ['^a'], excludeGlob: ['b.*']}; - expect(await changed(names, config)).toEqual(['c.file']); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('c.file\n'); }); test('with a negated glob', async () => { - const names = ['a.file', 'b.file', 'c.file']; const config = {includeGlob: ['*.file', '!b.file'], excludeRe: ['^a']}; - expect(await changed(names, config)).toEqual(['c.file']); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('c.file\n'); }); test('with --literal the exclusion is plain text', async () => { - const names = ['a.file', 'axfile']; const config = {includeGlob: ['a*'], excludeRe: ['a.file'], literal: true}; - expect(await changed(names, config)).toEqual(['axfile']); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('axfile', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('axfile\n'); }); test('with € and § as aliases unless voided', async () => { - const names = ['a1.file', 'ab.files']; - expect(await changed(names, {includeGlob: ['a*'], excludeRe: ['§d.file€']})).toEqual([ - 'ab.files', - ]); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('ab.files', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['a*'], excludeRe: ['§d.file€']})).out).toBe( + 'ab.files\n' + ); const voided = { includeGlob: ['a*'], excludeRe: ['§d.file€'], voidEuro: true, voidSection: true, }; - expect(await changed(names, voided)).toEqual(names); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('ab.files', 'foo'); + expect((await rr('foo', 'x', voided)).out).toBe('a1.file\nab.files\n'); }); test('with --output only the files left are sent', async () => { diff --git a/test/engine/fix/683.test.js b/test/engine/fix/683.test.js index 2840a13c..343c0f88 100644 --- a/test/engine/fix/683.test.js +++ b/test/engine/fix/683.test.js @@ -4,13 +4,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {rr} from '../../helper.js'; -// Make `names` as files containing "foo" and return the ones changed by replacing "foo" -const changed = async (names, config) => { - names.forEach(name => fs.writeFileSync(name, 'foo')); - await rr('foo', 'x', {includeGlob: ['*.file'], ...config}); - return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); -}; - describe('#683 --exclude-re follows the regex flags of the main search', () => { test('the example from the issue', async () => { fs.writeFileSync('a.file', 'x\n'); @@ -20,32 +13,70 @@ describe('#683 --exclude-re follows the regex flags of the main search', () => { }); test('ignores case as default', async () => { - expect(await changed(['MY.file', 'your.file'], {excludeRe: ['^my']})).toEqual(['your.file']); - expect(await changed(['my.file', 'your.file'], {excludeRe: ['^MY']})).toEqual(['your.file']); + fs.writeFileSync('MY.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^my']})).out).toBe( + 'your.file\n' + ); + fs.writeFileSync('my.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^MY']})).out).toBe( + 'your.file\n' + ); }); test('--void-ignore-case makes the case matter', async () => { - const names = ['MY.file', 'your.file']; - expect(await changed(names, {excludeRe: ['^my'], voidIgnoreCase: true})).toEqual(names); - expect(await changed(names, {excludeRe: ['^MY'], voidIgnoreCase: true})).toEqual(['your.file']); + fs.writeFileSync('MY.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^my'], voidIgnoreCase: true})) + .out + ).toBe('MY.file\nyour.file\n'); + fs.writeFileSync('MY.file', 'foo'); + fs.writeFileSync('your.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^MY'], voidIgnoreCase: true})) + .out + ).toBe('your.file\n'); }); test('--dot-all lets a dot match a line break in the path', async () => { - const names = ['a\nb.file', 'ab.file']; - expect(await changed(names, {excludeRe: ['a.b']})).toEqual(names); - expect(await changed(names, {excludeRe: ['a.b'], dotAll: true})).toEqual(['ab.file']); + fs.writeFileSync('a\nb.file', 'foo'); + fs.writeFileSync('ab.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['a.b']})).out).toBe( + 'a\nb.file\nab.file\n' + ); + fs.writeFileSync('a\nb.file', 'foo'); + fs.writeFileSync('ab.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['a.b'], dotAll: true})).out + ).toBe('ab.file\n'); }); test('--void-multiline makes ^ match only at the start of the path', async () => { - const names = ['a\nb.file', 'c.file']; - expect(await changed(names, {excludeRe: ['^b']})).toEqual(['c.file']); - expect(await changed(names, {excludeRe: ['^b'], voidMultiline: true})).toEqual(names); + fs.writeFileSync('a\nb.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^b']})).out).toBe( + 'c.file\n' + ); + fs.writeFileSync('a\nb.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['^b'], voidMultiline: true})).out + ).toBe('a\nb.file\nc.file\n'); }); test('--unicode lets \\u{...} match in the path', async () => { - const names = ['a.file', 'b.file']; - expect(await changed(names, {excludeRe: ['\\u{61}']})).toEqual(names); - expect(await changed(names, {excludeRe: ['\\u{61}'], unicode: true})).toEqual(['b.file']); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['\\u{61}']})).out).toBe( + 'a.file\nb.file\n' + ); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeRe: ['\\u{61}'], unicode: true})).out + ).toBe('b.file\n'); }); test('an invalid regex stops with an error (used to throw a SyntaxError)', async () => { diff --git a/test/engine/fix/699.test.js b/test/engine/fix/699.test.js index 0295034d..0446bb9b 100644 --- a/test/engine/fix/699.test.js +++ b/test/engine/fix/699.test.js @@ -5,73 +5,97 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {rr} from '../../helper.js'; -// Make `names` as files containing "foo" and return the ones changed by replacing "foo" -const changed = async (names, config) => { - names.forEach(name => fs.writeFileSync(name, 'foo')); - await rr('foo', 'x', config); - return names.filter(name => 'x' === fs.readFileSync(name, 'utf8')); -}; - const voided = {voidEuro: true, voidSection: true}; describe('#699 € and § are only aliases in the pattern, the replacement and --exclude-re', () => { describe('file globs', () => { test('€ is a plain char', async () => { - const names = ['cost€.file', 'cost$.file']; - expect(await changed(names, {includeGlob: ['cost€.file']})).toEqual(['cost€.file']); - expect(await changed(names, {includeGlob: ['cost$.file']})).toEqual(['cost$.file']); - expect(await changed(names, {includeGlob: ['*€.file']})).toEqual(['cost€.file']); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['cost€.file']})).out).toBe('cost€.file\n'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['cost$.file']})).out).toBe('cost$.file\n'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*€.file']})).out).toBe('cost€.file\n'); }); test('§ is a plain char', async () => { - const names = ['par§.file', 'par.file']; - expect(await changed(names, {includeGlob: ['par§.file']})).toEqual(['par§.file']); - expect(await changed(names, {includeGlob: ['*§.file']})).toEqual(['par§.file']); + fs.writeFileSync('par§.file', 'foo'); + fs.writeFileSync('par.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['par§.file']})).out).toBe('par§.file\n'); + fs.writeFileSync('par§.file', 'foo'); + fs.writeFileSync('par.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['*§.file']})).out).toBe('par§.file\n'); }); test('§ does not escape like a backslash does', async () => { - const names = ['a[1].file', 'a1.file']; - expect(await changed(names, {includeGlob: ['a\\[1\\].file']})).toEqual(['a[1].file']); - expect(await changed(names, {includeGlob: ['a§[1§].file']})).toEqual([]); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['a\\[1\\].file']})).out).toBe('a[1].file\n'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['a§[1§].file']})).out).toBe(''); }); test('--void-euro and --void-section make no difference', async () => { - const names = ['cost€.file', 'cost$.file', 'par§.file']; - expect(await changed(names, {includeGlob: ['cost€.file', 'par§.file'], ...voided})).toEqual([ - 'cost€.file', - 'par§.file', - ]); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + fs.writeFileSync('par§.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['cost€.file', 'par§.file'], ...voided})).out + ).toBe('cost€.file\npar§.file\n'); }); }); describe('--exclude-glob', () => { - const names = ['a[1].file', 'cost$.file', 'cost€.file', 'par§.file']; - test('€ and § are plain chars', async () => { - expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['cost€.file']})).toEqual([ - 'a[1].file', - 'cost$.file', - 'par§.file', - ]); - expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['par§.file']})).toEqual([ - 'a[1].file', - 'cost$.file', - 'cost€.file', - ]); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('par§.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeGlob: ['cost€.file']})).out + ).toBe('a[1].file\ncost$.file\npar§.file\n'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('par§.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeGlob: ['par§.file']})).out + ).toBe('a[1].file\ncost$.file\ncost€.file\n'); }); test('§ does not escape like a backslash does', async () => { + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('par§.file', 'foo'); expect( - await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a\\[1\\].file']}) - ).toEqual(['cost$.file', 'cost€.file', 'par§.file']); - expect(await changed(names, {includeGlob: ['*.file'], excludeGlob: ['a§[1§].file']})).toEqual( - names - ); + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeGlob: ['a\\[1\\].file']})).out + ).toBe('cost$.file\ncost€.file\npar§.file\n'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('par§.file', 'foo'); + expect( + (await rr('foo', 'x', {includeGlob: ['*.file'], excludeGlob: ['a§[1§].file']})).out + ).toBe('a[1].file\ncost$.file\ncost€.file\npar§.file\n'); }); test('--void-euro and --void-section make no difference', async () => { const config = {includeGlob: ['*.file'], excludeGlob: ['cost€.file', 'par§.file']}; - expect(await changed(names, {...config, ...voided})).toEqual(await changed(names, config)); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('par§.file', 'foo'); + expect((await rr('foo', 'x', config)).out).toBe('a[1].file\ncost$.file\n'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('cost$.file', 'foo'); + fs.writeFileSync('cost€.file', 'foo'); + fs.writeFileSync('par§.file', 'foo'); + expect((await rr('foo', 'x', {...config, ...voided})).out).toBe('a[1].file\ncost$.file\n'); }); }); @@ -111,13 +135,18 @@ describe('#699 € and § are only aliases in the pattern, the replacement and - }); test('--exclude-re', async () => { - const names = ['a1.file', 'ab.file', 'ab.files']; - expect(await changed(names, {includeGlob: ['a*'], excludeRe: ['§d', 'file€']})).toEqual([ - 'ab.files', - ]); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('ab.file', 'foo'); + fs.writeFileSync('ab.files', 'foo'); + expect((await rr('foo', 'x', {includeGlob: ['a*'], excludeRe: ['§d', 'file€']})).out).toBe( + 'ab.files\n' + ); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('ab.file', 'foo'); + fs.writeFileSync('ab.files', 'foo'); expect( - await changed(names, {includeGlob: ['a*'], excludeRe: ['§d', 'file€'], ...voided}) - ).toEqual(names); + (await rr('foo', 'x', {includeGlob: ['a*'], excludeRe: ['§d', 'file€'], ...voided})).out + ).toBe('a1.file\nab.file\nab.files\n'); }); }); }); diff --git a/test/engine/halt.test.js b/test/engine/halt.test.js index c3518130..6b62e10a 100644 --- a/test/engine/halt.test.js +++ b/test/engine/halt.test.js @@ -4,22 +4,21 @@ import {rr} from '../helper.js'; describe('--halt', () => { // a.file can not be read, and the files are handled in the order of the list - function lockedFirst() { + test('stops the process on the first error', async () => { fs.writeFileSync('a.file', 'foo'); fs.writeFileSync('b.file', 'foo'); fs.chmodSync('a.file', 0); - return {includeGlob: ['a.file', 'b.file']}; - } - - test('stops the process on the first error', async () => { - const result = await rr('o', 'x', {...lockedFirst(), halt: true}); + const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], halt: true}); expect(result.err).toContain('EACCES'); expect(result.exit).toBe(1); expect(fs.readFileSync('b.file', 'utf8')).toBe('foo'); }); test('is not the default, so the other files are still handled', async () => { - const result = await rr('o', 'x', lockedFirst()); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.chmodSync('a.file', 0); + const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file']}); expect(result.err).toContain('EACCES'); expect(result.exit).toBe(1); expect(fs.readFileSync('b.file', 'utf8')).toBe('fxx'); diff --git a/test/engine/replacement-js.test.js b/test/engine/replacement-js.test.js index 56d70a22..a068d180 100644 --- a/test/engine/replacement-js.test.js +++ b/test/engine/replacement-js.test.js @@ -6,20 +6,13 @@ import {rr} from '../helper.js'; const js = async (pattern, code, pipedData, config) => (await rr(pattern, code, {pipedData, replacementJs: true, ...config})).out; -// Run `code` against my.file holding `text` and return what it gives -const inFile = async (code, text = 'foobar', file = 'my.file') => { - fs.mkdirSync(path.dirname(file), {recursive: true}); - fs.writeFileSync(file, text); - return ( - await rr('^[^]*$', code, { - includeGlob: [file], - replacementJs: true, - output: true, - // Without the tail only the value of the code is given - contentDelimiterTail: false, - voidMultiline: true, - }) - ).out; +// The code runs once on the whole content of my.file. With --output and no tail only the value of the code is printed. +const wholeFile = { + includeGlob: ['my.file'], + replacementJs: true, + output: true, + contentDelimiterTail: false, + voidMultiline: true, }; const timestamp = /^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3}$/; @@ -105,21 +98,31 @@ describe('--replacement-js', () => { describe('variables', () => { test('fs, path and globs are available', async () => { - expect(await inFile('typeof fs.readFileSync + typeof path.join + typeof globs.sync')).toBe( - 'functionfunctionfunction' - ); - expect(await inFile('fs.readFileSync(file, "utf8").toUpperCase()')).toBe('FOOBAR'); - expect(await inFile('globs.sync("*").join()')).toBe('my.file'); + fs.writeFileSync('my.file', 'foobar'); + expect( + ( + await rr( + '^[^]*$', + 'typeof fs.readFileSync + typeof path.join + typeof globs.sync', + wholeFile + ) + ).out + ).toBe('functionfunctionfunction'); + expect( + (await rr('^[^]*$', 'fs.readFileSync(file, "utf8").toUpperCase()', wholeFile)).out + ).toBe('FOOBAR'); + expect((await rr('^[^]*$', 'globs.sync("*").join()', wholeFile)).out).toBe('my.file'); }); test('require and r find files relative to the current dir', async () => { fs.writeFileSync('data.json', '{"version": "1.2.3"}'); - expect(await inFile('require("./data.json").version')).toBe('1.2.3'); - expect(await inFile('r("./data.json").version')).toBe('1.2.3'); - expect(await inFile('r("data.json").version')).toBe('1.2.3'); - expect(await inFile('r("data").version')).toBe('1.2.3'); - expect(await inFile('r(cwd + "/data.json").version')).toBe('1.2.3'); - expect(await inFile('require("path").sep')).toBe(path.sep); + fs.writeFileSync('my.file', 'foobar'); + expect((await rr('^[^]*$', 'require("./data.json").version', wholeFile)).out).toBe('1.2.3'); + expect((await rr('^[^]*$', 'r("./data.json").version', wholeFile)).out).toBe('1.2.3'); + expect((await rr('^[^]*$', 'r("data.json").version', wholeFile)).out).toBe('1.2.3'); + expect((await rr('^[^]*$', 'r("data").version', wholeFile)).out).toBe('1.2.3'); + expect((await rr('^[^]*$', 'r(cwd + "/data.json").version', wholeFile)).out).toBe('1.2.3'); + expect((await rr('^[^]*$', 'require("path").sep', wholeFile)).out).toBe(path.sep); }); test('text is the full content and find is the pattern', async () => { @@ -148,7 +151,8 @@ describe('--replacement-js', () => { '[file, file_rel, dirpath, dirpath_rel, dirname, filename, name, ext].join("\\n")'; const cwd = process.cwd(); - expect((await inFile(code)).split('\n')).toEqual([ + fs.writeFileSync('my.file', 'foobar'); + expect((await rr('^[^]*$', code, wholeFile)).out.split('\n')).toEqual([ path.join(cwd, 'my.file'), 'my.file', cwd, @@ -159,7 +163,13 @@ describe('--replacement-js', () => { '.file', ]); - expect((await inFile(code, 'foobar', 'sub/dir/my.file.txt')).split('\n')).toEqual([ + fs.mkdirSync('sub/dir', {recursive: true}); + fs.writeFileSync('sub/dir/my.file.txt', 'foobar'); + expect( + (await rr('^[^]*$', code, {...wholeFile, includeGlob: ['sub/dir/my.file.txt']})).out.split( + '\n' + ) + ).toEqual([ path.join(cwd, 'sub/dir/my.file.txt'), path.join('sub/dir/my.file.txt'), path.join(cwd, 'sub/dir'), @@ -178,31 +188,40 @@ describe('--replacement-js', () => { }); test('cwd is the current working dir', async () => { - expect(await inFile('cwd')).toBe(process.cwd()); + fs.writeFileSync('my.file', 'foobar'); + expect((await rr('^[^]*$', 'cwd', wholeFile)).out).toBe(process.cwd()); expect(await js('foo', 'cwd', 'foo')).toBe(process.cwd()); }); test('bytes and size tell the size of the content', async () => { - expect(await inFile('bytes + "|" + size')).toBe('6|6 Bytes'); - expect(await inFile('bytes + "|" + size', 'x')).toBe('1|1 Byte'); - expect(await inFile('bytes + "|" + size', 'æ🦖')).toBe('6|6 Bytes'); - expect(await inFile('bytes + "|" + size', 'x'.repeat(1024))).toBe('1024|1.0 KB'); - expect(await inFile('bytes + "|" + size', 'x'.repeat(1536))).toBe('1536|1.5 KB'); - expect(await inFile('bytes + "|" + size', 'x'.repeat(3 * 1024 * 1024))).toBe( - '3145728|3.0 MB' - ); + fs.writeFileSync('my.file', 'foobar'); + expect((await rr('^[^]*$', 'bytes + "|" + size', wholeFile)).out).toBe('6|6 Bytes'); + fs.writeFileSync('my.file', 'x'); + expect((await rr('^[^]*$', 'bytes + "|" + size', wholeFile)).out).toBe('1|1 Byte'); + fs.writeFileSync('my.file', 'æ🦖'); + expect((await rr('^[^]*$', 'bytes + "|" + size', wholeFile)).out).toBe('6|6 Bytes'); + fs.writeFileSync('my.file', 'x'.repeat(1024)); + expect((await rr('^[^]*$', 'bytes + "|" + size', wholeFile)).out).toBe('1024|1.0 KB'); + fs.writeFileSync('my.file', 'x'.repeat(1536)); + expect((await rr('^[^]*$', 'bytes + "|" + size', wholeFile)).out).toBe('1536|1.5 KB'); + fs.writeFileSync('my.file', 'x'.repeat(3 * 1024 * 1024)); + expect((await rr('^[^]*$', 'bytes + "|" + size', wholeFile)).out).toBe('3145728|3.0 MB'); expect(await js('foobar', 'bytes + "|" + size', 'foobar')).toBe('6|6 Bytes'); }); test('size of empty content is "0 Bytes" (#687)', async () => { - expect(await inFile('bytes + "|" + size', '')).toBe('0|0 Bytes'); + fs.writeFileSync('my.file', ''); + expect((await rr('^[^]*$', 'bytes + "|" + size', wholeFile)).out).toBe('0|0 Bytes'); }); test('time and now tell when rexreplace was started', async () => { - expect(await inFile('time')).toMatch(timestamp); - expect(await inFile('now === time')).toBe('true'); - expect(await inFile('time_obj instanceof Date')).toBe('true'); - expect(Math.abs(Date.now() - Number(await inFile('time_obj.getTime()')))).toBeLessThan(60000); + fs.writeFileSync('my.file', 'foobar'); + expect((await rr('^[^]*$', 'time', wholeFile)).out).toMatch(timestamp); + expect((await rr('^[^]*$', 'now === time', wholeFile)).out).toBe('true'); + expect((await rr('^[^]*$', 'time_obj instanceof Date', wholeFile)).out).toBe('true'); + expect( + Math.abs(Date.now() - Number((await rr('^[^]*$', 'time_obj.getTime()', wholeFile)).out)) + ).toBeLessThan(60000); }); test('mtime and ctime tell about the last change of the file', async () => { @@ -249,14 +268,22 @@ describe('--replacement-js', () => { 'size', ]; const code = `[${names.map(name => `${name}_ === ${name} + " "`).join()}].join()`; - expect(await inFile(code)).toBe(names.map(() => 'true').join()); + fs.writeFileSync('my.file', 'foobar'); + expect((await rr('^[^]*$', code, wholeFile)).out).toBe(names.map(() => 'true').join()); expect(await js('foobar', code, 'foobar')).toBe(names.map(() => 'true').join()); }); test('are the same when the code runs once per match', async () => { - expect(await inFile('$0; [filename, name, ext, bytes, text, find, _, nl].join("|")')).toBe( - 'my.file|my|.file|6|foobar|^[^]*$| |\n' - ); + fs.writeFileSync('my.file', 'foobar'); + expect( + ( + await rr( + '^[^]*$', + '$0; [filename, name, ext, bytes, text, find, _, nl].join("|")', + wholeFile + ) + ).out + ).toBe('my.file|my|.file|6|foobar|^[^]*$| |\n'); }); test('follow the file when several files are handled', async () => { From 71c2744ccad1d5deaa63151cf60754156c03a253 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 14:59:01 +1100 Subject: [PATCH 06/11] Rename die() to panic(), and end a panic with No further processing --- src/cli.ts | 8 ++++---- src/engine.ts | 20 ++++++++++---------- src/output.ts | 25 ++++++++++++++++--------- test/cli/fix/691.test.js | 4 +++- test/cli/fix/700.test.js | 2 +- test/engine/encoding.test.js | 6 +++++- test/engine/fix/720.test.js | 19 +++++++++++++++++++ 7 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 0238f445..0287c387 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ // CLI interface for rexreplace import * as rexreplace from './engine.ts'; -import {outputConfig, die} from './output.ts'; +import {outputConfig, panic} from './output.ts'; import {alias} from './alias.ts'; // Make it visible when Bun is the runtime (like with rexreplace-bun) as the regex engine is then not the one from Node. @@ -541,9 +541,9 @@ const unescape = (key, name) => { } catch (e) { // The engine is not running yet, so the panic is caught here try { - die('Invalid escape sequence in ' + name + ':', typed[key]); - } catch (panic) { - exitOnPanic(config, panic); + panic('Invalid escape sequence in ' + name + ':', typed[key]); + } catch (e) { + exitOnPanic(config, e); } } }; diff --git a/src/engine.ts b/src/engine.ts index 37f7c830..a41b7263 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -8,7 +8,7 @@ const globs = require('fast-glob'); const now = new Date(); -import {outputConfig, chat, info, print, printDone, warn, error, die} from './output.ts'; +import {outputConfig, chat, info, print, printDone, warn, error, panic} from './output.ts'; const re = { mctime: /[mc]time/, @@ -30,7 +30,7 @@ const runtimeEngine = process.versions.bun ? 'JSC' : 'V8'; function validateEncoding(encoding = 'utf8') { if (!Buffer.isEncoding(encoding)) { - die('Unknown encoding:', encoding); + panic('Unknown encoding:', encoding); } } @@ -80,7 +80,7 @@ async function run(config) { // With --glob-pipe each line of the piped data is a path or a glob, used together with the ones given as arguments if (config.globPipe) { if (config.replacementPipe || config.pipe) { - die( + panic( '--glob-pipe can not be used with --replacement-pipe or --pipe, as the piped data can only be one thing' ); } @@ -337,14 +337,14 @@ function getFinalReplacement(replacement, conf: any) { if (conf.replacementPipe) { conf.pipedDataUsed = true; if (null === conf.pipedData) { - return die('No data piped into replacement'); + return panic('No data piped into replacement'); } replacement = conf.pipedData; } if (conf.outputMatch) { if (parseInt(process.versions.node) < 6) { - return die('outputMatch is only supported in node 6+'); + return panic('outputMatch is only supported in node 6+'); } return function (match, ...rest) { @@ -367,7 +367,7 @@ function getFinalReplacement(replacement, conf: any) { re.capturedGroupRef.test(conf.replacement) && parseInt(process.versions.node) < 6 ) { - return die('Captured groups for javascript replacement is only supported in node 6+'); + return panic('Captured groups for javascript replacement is only supported in node 6+'); } return replacement; @@ -395,12 +395,12 @@ function getFinalRegex(pattern, config) { try { regex = new RegExp(pattern, flags); } catch (e) { - die(e.message); + panic(e.message); } break; case 'V8': case 'JSC': - die( + panic( `When running via the ${runtime} runtime you can only use default or ${runtimeEngine} as engine` ); break; @@ -410,11 +410,11 @@ function getFinalRegex(pattern, config) { //regex = new RE2(pattern, flags); regex = new RegExp(pattern, flags); } catch (e) { - die(e.message); + panic(e.message); } break; default: - die(`Engine ${config.engine} not supported`); + panic(`Engine ${config.engine} not supported`); } return regex; diff --git a/src/output.ts b/src/output.ts index ed746629..7f50bdaf 100644 --- a/src/output.ts +++ b/src/output.ts @@ -93,26 +93,33 @@ export const chat = function (msg) { } }; -export const die = function (msg = '', data = '', displayHelp = false) { +// An error is told about, and goes into `errors` in the status report. A panic also says the run stops here. +const printError = function (text, stop) { + if (!config.quietTotal) { + // The line break gives an empty line under the error + write( + font.red(' ❌ ' + text + (stop ? '\n No further processing.' : '')) + '\n\n', + process.stderr + ); + } +}; + +// The run can not go on, so it stops here +export const panic = function (msg = '', data = '', displayHelp = false) { if (displayHelp && !config.quietTotal) { config.showHelp(); } const text = msg + (data ? ' ' + data : ''); config.statusInfo?.errors.push(text); - if (!config.quietTotal) { - // The line break gives an empty line under the error - write(font.red(' ❌ ' + text) + '\n\n', process.stderr); - } + printError(text, true); throw new Panic(text); }; +// Something failed, and the run goes on. With --halt it is a panic. export const error = function (msg, data = '') { const text = msg + (data ? ' ' + data : ''); config.statusInfo?.errors.push(text); - if (!config.quietTotal) { - // The line break gives an empty line under the error - write(font.red(' ❌ ' + text) + '\n\n', process.stderr); - } + printError(text, config.halt); if (config.halt) { throw new Panic(text); } diff --git a/test/cli/fix/691.test.js b/test/cli/fix/691.test.js index 93db4cce..ebf07034 100644 --- a/test/cli/fix/691.test.js +++ b/test/cli/fix/691.test.js @@ -76,7 +76,9 @@ describe('#691 replacement that used to crash the CLI', () => { fs.writeFileSync('my.file', 'foobar'); const result = await sh($`bun ${cli} o ${'\\x4'} my.file --output --no-content-delimiter-tail`); expect(result.out).toBe(''); - expect(result.err).toBe(' ❌ Invalid escape sequence in replacement: \\x4\n\n'); + expect(result.err).toBe( + ' ❌ Invalid escape sequence in replacement: \\x4\n No further processing.\n\n' + ); expect(result.exit).toBe(1); expect( await sh($`bun ${cli} o ${'\\x4'} my.file --output --no-content-delimiter-tail --quiet-total`) diff --git a/test/cli/fix/700.test.js b/test/cli/fix/700.test.js index ac06c152..2bb3fd3d 100644 --- a/test/cli/fix/700.test.js +++ b/test/cli/fix/700.test.js @@ -39,7 +39,7 @@ describe('#700 § is a full substitute for \\ in the replacement', () => { test('an invalid escape sequence gives the same error as with a backslash', async () => { expect(await replaceB('§x4')).toEqual({ out: '', - err: ' ❌ Invalid escape sequence in replacement: §x4\n\n', + err: ' ❌ Invalid escape sequence in replacement: §x4\n No further processing.\n\n', exit: 1, }); }); diff --git a/test/engine/encoding.test.js b/test/engine/encoding.test.js index 9ef47893..69ec6b00 100644 --- a/test/engine/encoding.test.js +++ b/test/engine/encoding.test.js @@ -48,7 +48,11 @@ describe('--encoding', () => { }); // The error message, and not the stack trace from deep inside node, when an encoding is unknown (#690) - const unknownEncoding = {out: '', err: ' ❌ Unknown encoding: nope\n\n', exit: 1}; + const unknownEncoding = { + out: '', + err: ' ❌ Unknown encoding: nope\n No further processing.\n\n', + exit: 1, + }; test('stops the run with an error if it is unknown (#690)', async () => { fs.writeFileSync('my.file', 'foobar'); diff --git a/test/engine/fix/720.test.js b/test/engine/fix/720.test.js index e7095db5..a2513fe4 100644 --- a/test/engine/fix/720.test.js +++ b/test/engine/fix/720.test.js @@ -25,6 +25,25 @@ describe('#720 every error starts with ❌', () => { }); }); +describe('#720 an error that stops the run says so', () => { + test('a panic', async () => { + expect((await rr('(', 'x', {pipedData: 'foo'})).err).toEndWith( + '\n No further processing.\n\n' + ); + }); + + test('an error from a file with --halt, but not without', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + expect((await rr('o', 'x', {includeGlob: ['a.file'], halt: true})).err).toContain( + 'No further processing.' + ); + expect((await rr('o', 'x', {includeGlob: ['a.file']})).err).not.toContain( + 'No further processing.' + ); + }); +}); + describe('#720 finding no files is a warning', () => { test('with the exit code 0', async () => { expect(await rr('o', 'x', {includeGlob: ['nope.file']})).toEqual({ From 5bb35fc1cae8ee3c521e633e1a68ba6238b0b09e Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 15:10:50 +1100 Subject: [PATCH 07/11] Give an error in a file exit code 2 and a panic exit code 3 --- README.md | 12 +++++++- src/cli.ts | 16 ++++++++--- test/cli/debug.test.js | 6 ++-- test/cli/delimiter.test.js | 2 +- test/cli/encoding.test.js | 4 +-- test/cli/fix/691.test.js | 4 +-- test/cli/fix/700.test.js | 2 +- test/cli/fix/742.test.js | 6 ++-- test/cli/fix/783.test.js | 42 ++++++++++++++++++++++++++++ test/cli/glob-pipe.test.js | 4 +-- test/cli/run.sh | 7 +++-- test/engine/basics.test.js | 2 +- test/engine/encoding.test.js | 2 +- test/engine/engine.test.js | 8 +++--- test/engine/fix/681.test.js | 6 ++-- test/engine/fix/682.test.js | 4 +-- test/engine/fix/683.test.js | 2 +- test/engine/fix/703.test.js | 4 +-- test/engine/fix/705.test.js | 4 +-- test/engine/fix/708.test.js | 2 +- test/engine/fix/711.test.js | 2 +- test/engine/fix/720.test.js | 2 +- test/engine/fix/728.test.js | 8 +++--- test/engine/fix/742.test.js | 4 +-- test/engine/halt.test.js | 10 +++---- test/engine/quiet-total.test.js | 4 +-- test/engine/quiet.test.js | 2 +- test/engine/replacement-pipe.test.js | 2 +- test/engine/unicode.test.js | 2 +- test/engine/void-section.test.js | 2 +- test/helper.js | 8 +++--- 31 files changed, 124 insertions(+), 61 deletions(-) create mode 100644 test/cli/fix/783.test.js diff --git a/README.md b/README.md index b86ef4e4..cd9928dd 100644 --- a/README.md +++ b/README.md @@ -311,10 +311,20 @@ Files identified for potential processing are identified by: - Piped data that is printed is left as it came in, so no delimiter is added after the end of it. Matches from `--output-match` are different: they are new output, so they get the delimiter whether they come from a file or from piped data. -- Finding no files is a warning (🟡) and not an error, so the exit code is 0. Nothing went wrong, there was just nothing to do. Errors start with ❌ and give the exit code 1. +- Finding no files is a warning (🟡) and not an error, so the exit code is 0. Nothing went wrong, there was just nothing to do. +### Exit codes + +| Code | Meaning | +| --- | --- | +| 0 | Done. Warnings (🟡) are ok. | +| 2 | One or more files gave an error (❌), and the run went on. | +| 3 | The run stopped (❌ … `No further processing.`): a bad pattern, an unknown encoding, wrong flags, or any error with `--halt`. | + +Code 1 is kept for a check that says no, like `grep` does when nothing is found. + ### Limitations - RexReplace reads each file fully into memory, so working on your 8Gb log files will probably not be ideal. diff --git a/src/cli.ts b/src/cli.ts index 0287c387..adc11505 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -29,6 +29,12 @@ const yargs = require('yargs') // A file can be named with only digits, like `2024`, so each glob is kept as the string that was given .parserConfiguration({'parse-positional-numbers': false}) .strict() + // Wrong flags are a panic, so the exit code is 3. The help and the message go to stderr, as yargs does it. + .fail((msg, err, yargs) => { + yargs.showHelp(); + console.error(msg || err.message); + process.exit(3); + }) .usage( 'RexReplace ' + @@ -466,7 +472,7 @@ function exitOnPanic(config, e) { if (e.status) { printStatus(config, e.status); } - process.exit(1); + process.exit(3); } function backOut(exitcode = 1) { @@ -492,8 +498,9 @@ function unescapeString(str = '') { return new Function(`return '${source}'`)(); } +// Help that was asked for is not an error. Too few arguments are wrong flags, so a panic. if (0 < needHelp) { - backOut(needHelp - 1); + backOut(1 === needHelp ? 0 : 3); } // All options into one big config object for the rexreplace core @@ -562,13 +569,14 @@ if (config.contentDelimiter) { unescape('contentDelimiter', '--content-delimiter'); } -// The exit code is only set, so the process ends by itself when all output is written +// The exit code is only set, so the process ends by itself when all output is written. +// 0: done, 2: an error in a file and the run went on, 3: a panic (the run stopped) const run = () => rexreplace.engine(config).then( status => { printStatus(config, status); if (status.errors.length) { - process.exitCode = 1; + process.exitCode = 2; } }, e => exitOnPanic(config, e) diff --git a/test/cli/debug.test.js b/test/cli/debug.test.js index 3cba492f..f5754ab6 100644 --- a/test/cli/debug.test.js +++ b/test/cli/debug.test.js @@ -38,12 +38,12 @@ describe('--debug', () => { expect(JSON.parse(result.out.slice('fxxbar\n'.length)).files.updated).toEqual(['my.file']); }); - test('gives the errors in the status report, and the exit code 1', async () => { + test('gives the errors in the status report, and the exit code 2', async () => { fs.writeFileSync('a.file', 'foo'); fs.chmodSync('a.file', 0); const result = await sh($`bun ${cli} o x a.file --debug`); expect(result.err).toBe(''); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(JSON.parse(result.out).errors[0]).toContain('EACCES'); }); @@ -51,7 +51,7 @@ describe('--debug', () => { fs.writeFileSync('my.file', 'foobar'); const result = await sh($`bun ${cli} '(' x my.file --debug`); expect(result.err).toBe(''); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(JSON.parse(result.out).errors[0]).toContain('Invalid regular expression'); }); }); diff --git a/test/cli/delimiter.test.js b/test/cli/delimiter.test.js index 5c00b819..215df651 100644 --- a/test/cli/delimiter.test.js +++ b/test/cli/delimiter.test.js @@ -170,7 +170,7 @@ describe('an invalid escape sequence in a delimiter', () => { fs.writeFileSync('my.file', 'foobar\n'); const result = await sh($`bun ${cli} b x my.file ${flag} '\x4'`); expect(result.err).toContain(`Invalid escape sequence in ${flag}`); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n'); } ); diff --git a/test/cli/encoding.test.js b/test/cli/encoding.test.js index 8c8b5465..bcf31009 100644 --- a/test/cli/encoding.test.js +++ b/test/cli/encoding.test.js @@ -10,14 +10,14 @@ describe('--encoding', () => { test('an unknown encoding stops the run with an error, and the file is not changed', async () => { fs.writeFileSync('my.file', 'foobar'); const result = await sh($`bun ${cli} o x my.file --encoding nope`); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(lastLine(result.err)).toBe('Unknown encoding: nope'); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); test('an unknown encoding also stops the run for piped data', async () => { const result = await sh($`echo -n foo | bun ${cli} o x --encoding nope`); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(lastLine(result.err)).toBe('Unknown encoding: nope'); }); diff --git a/test/cli/fix/691.test.js b/test/cli/fix/691.test.js index ebf07034..ccb9b563 100644 --- a/test/cli/fix/691.test.js +++ b/test/cli/fix/691.test.js @@ -79,10 +79,10 @@ describe('#691 replacement that used to crash the CLI', () => { expect(result.err).toBe( ' ❌ Invalid escape sequence in replacement: \\x4\n No further processing.\n\n' ); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect( await sh($`bun ${cli} o ${'\\x4'} my.file --output --no-content-delimiter-tail --quiet-total`) - ).toEqual({out: '', err: '', exit: 1}); + ).toEqual({out: '', err: '', exit: 3}); }); test.each([ diff --git a/test/cli/fix/700.test.js b/test/cli/fix/700.test.js index 2bb3fd3d..2f657f3a 100644 --- a/test/cli/fix/700.test.js +++ b/test/cli/fix/700.test.js @@ -40,7 +40,7 @@ describe('#700 § is a full substitute for \\ in the replacement', () => { expect(await replaceB('§x4')).toEqual({ out: '', err: ' ❌ Invalid escape sequence in replacement: §x4\n No further processing.\n\n', - exit: 1, + exit: 3, }); }); diff --git a/test/cli/fix/742.test.js b/test/cli/fix/742.test.js index 0ec96d7d..1e6eaf29 100644 --- a/test/cli/fix/742.test.js +++ b/test/cli/fix/742.test.js @@ -33,18 +33,18 @@ describe('#742 the CLI', () => { } ); - test('sets the exit code to 1 when a file gives an error, and prints the status report', async () => { + test('sets the exit code to 2 when a file gives an error, and prints the status report', async () => { fs.writeFileSync('a.file', 'foo'); fs.chmodSync('a.file', 0); const result = await sh($`bun ${cli} o x a.file --status-compact --quiet-total`); expect(JSON.parse(result.out).errors[0]).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); }); test('ends the process on a panic, and still prints the status report', async () => { fs.writeFileSync('a.file', 'foo'); const result = await sh($`bun ${cli} '(' x a.file --status-compact --quiet-total`); expect(JSON.parse(result.out).errors).toHaveLength(1); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); }); }); diff --git a/test/cli/fix/783.test.js b/test/cli/fix/783.test.js new file mode 100644 index 00000000..b35c3993 --- /dev/null +++ b/test/cli/fix/783.test.js @@ -0,0 +1,42 @@ +// https://github.com/mathiasrw/rexreplace/issues/783 +// The exit code was 1 for an error in a file, for a run that stopped and for wrong flags, so a script could not tell them apart. +import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; +import {$} from 'bun'; +import {cli, sh} from '../../helper.js'; + +describe('#783 the exit code tells what happened', () => { + test('0 when it is done, also with a warning', async () => { + fs.writeFileSync('my.file', 'foobar'); + expect((await sh($`bun ${cli} o x my.file`)).exit).toBe(0); + expect((await sh($`bun ${cli} o x nope.file`)).exit).toBe(0); + }); + + test('0 for the help that was asked for', async () => { + expect((await sh($`bun ${cli} -h`)).exit).toBe(0); + }); + + test('2 when a file gives an error and the run goes on', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.chmodSync('a.file', 0); + expect((await sh($`bun ${cli} o x a.file b.file`)).exit).toBe(2); + expect(fs.readFileSync('b.file', 'utf8')).toBe('fxx'); + }); + + test('3 when the run stops', async () => { + fs.writeFileSync('my.file', 'foobar'); + expect((await sh($`bun ${cli} '(' x my.file`)).exit).toBe(3); + expect((await sh($`bun ${cli} o x my.file --encoding nope`)).exit).toBe(3); + }); + + test('3 for wrong flags and too few arguments, with no stray line on stdout', async () => { + fs.writeFileSync('my.file', 'foobar'); + const result = await sh($`bun ${cli} o x my.file --nope`); + expect(result.exit).toBe(3); + expect(result.out).toBe(''); + expect(result.err).toEndWith('Unknown argument: nope\n'); + expect((await sh($`bun ${cli} o`)).exit).toBe(3); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); + }); +}); diff --git a/test/cli/glob-pipe.test.js b/test/cli/glob-pipe.test.js index 9b7315d3..959e175a 100644 --- a/test/cli/glob-pipe.test.js +++ b/test/cli/glob-pipe.test.js @@ -21,7 +21,7 @@ describe('--glob-pipe', () => { fs.writeFileSync('my.file', 'foobar\n'); const result = await sh($`echo -n my.file | bun ${cli} b x --glob-pipe ${flag}`); expect(result.err).toContain('are mutually exclusive'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n'); } ); @@ -29,7 +29,7 @@ describe('--glob-pipe', () => { test('--null-in can only be used with --glob-pipe', async () => { fs.writeFileSync('my.file', 'foobar\n'); const result = await sh($`echo -n my.file | bun ${cli} b x --null-in`); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n'); }); }); diff --git a/test/cli/run.sh b/test/cli/run.sh index 06596d6e..b4ac8284 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -54,12 +54,15 @@ assert_failure "rexreplace my-vile" assert_failure "rexreplace o-ho" +# The exit code: 0 when done, 2 when a file gave an error and the run went on, 3 when the run stopped # --halt (or --bail) stops at the first error, here that my.file can not be read reset chmod 000 my.file -assert_raises "rexreplace b x my.file your.file --halt" 1 -assert_raises "rexreplace b x my.file your.file --bail" 1 +assert_raises "rexreplace b x my.file your.file --halt" 3 +assert_raises "rexreplace b x my.file your.file --bail" 3 assert "cat your.file" "abc123" +assert_raises "rexreplace b x my.file your.file" 2 +assert "cat your.file" "axc123" # Finding no files is a warning and not an error assert_raises "rexreplace o x nope.file" 0 diff --git a/test/engine/basics.test.js b/test/engine/basics.test.js index 6dd1a679..d4c68c6d 100644 --- a/test/engine/basics.test.js +++ b/test/engine/basics.test.js @@ -42,7 +42,7 @@ describe('pattern', () => { test('stops with an error and leaves the files alone if it is not a valid regex', async () => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('(', 'x', {includeGlob: ['my.file']}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain('Invalid regular expression'); expect(result.out).toBe(''); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); diff --git a/test/engine/encoding.test.js b/test/engine/encoding.test.js index 69ec6b00..e18fdd2e 100644 --- a/test/engine/encoding.test.js +++ b/test/engine/encoding.test.js @@ -51,7 +51,7 @@ describe('--encoding', () => { const unknownEncoding = { out: '', err: ' ❌ Unknown encoding: nope\n No further processing.\n\n', - exit: 1, + exit: 3, }; test('stops the run with an error if it is unknown (#690)', async () => { diff --git a/test/engine/engine.test.js b/test/engine/engine.test.js index 84d2fd5e..7e8d71d8 100644 --- a/test/engine/engine.test.js +++ b/test/engine/engine.test.js @@ -15,7 +15,7 @@ describe('--engine', () => { test('stops with an error if the engine is the one of another runtime', async () => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('o', 'x', {includeGlob: ['my.file'], engine: 'V8'}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain( 'When running via the Bun runtime you can only use default or JSC as engine' ); @@ -25,13 +25,13 @@ describe('--engine', () => { test('stops with an error if the engine is unknown', async () => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('o', 'x', {includeGlob: ['my.file'], engine: 'nope'}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain('Engine nope not supported'); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); test('is case sensitive', async () => { - expect((await rr('o', 'x', {pipedData: 'foobar', engine: 'jsc'})).exit).toBe(1); + expect((await rr('o', 'x', {pipedData: 'foobar', engine: 'jsc'})).exit).toBe(3); }); describe('combined', () => { @@ -54,7 +54,7 @@ describe('--engine', () => { engine: 'nope', quietTotal: true, }); - expect(result).toEqual({out: '', err: '', exit: 1}); + expect(result).toEqual({out: '', err: '', exit: 3}); }); }); }); diff --git a/test/engine/fix/681.test.js b/test/engine/fix/681.test.js index beef3631..3c3ecb22 100644 --- a/test/engine/fix/681.test.js +++ b/test/engine/fix/681.test.js @@ -10,13 +10,13 @@ describe('#681 --halt exits with a numeric non-zero code', () => { fs.chmodSync('a.file', 0); const result = await rr('o', 'x', {includeGlob: ['a.file'], halt: true}); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); }); test('invalid regular expression', async () => { const result = await rr('(', 'x', {pipedData: 'foobar', halt: true}); expect(result.err).toContain('Invalid regular expression'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); }); test('--quiet-total still halts with the exit code but shows no message', async () => { @@ -27,6 +27,6 @@ describe('#681 --halt exits with a numeric non-zero code', () => { halt: true, quietTotal: true, }); - expect(result).toEqual({out: '', err: '', exit: 1}); + expect(result).toEqual({out: '', err: '', exit: 3}); }); }); diff --git a/test/engine/fix/682.test.js b/test/engine/fix/682.test.js index 73d23ecd..368e3842 100644 --- a/test/engine/fix/682.test.js +++ b/test/engine/fix/682.test.js @@ -9,7 +9,7 @@ describe('#682 --quiet still displays errors', () => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('(', 'x', {includeGlob: ['my.file'], quiet: true}); expect(result.err).toContain('Invalid regular expression'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); @@ -35,7 +35,7 @@ describe('#682 --quiet still displays errors', () => { expect(await rr('(', 'x', {includeGlob: ['my.file'], quietTotal: true})).toEqual({ out: '', err: '', - exit: 1, + exit: 3, }); expect((await rr('o', 'x', {includeGlob: ['nope.file'], quietTotal: true})).err).toBe(''); }); diff --git a/test/engine/fix/683.test.js b/test/engine/fix/683.test.js index 343c0f88..7d9dd4a8 100644 --- a/test/engine/fix/683.test.js +++ b/test/engine/fix/683.test.js @@ -83,7 +83,7 @@ describe('#683 --exclude-re follows the regex flags of the main search', () => { fs.writeFileSync('my.file', 'foo'); const result = await rr('foo', 'x', {includeGlob: ['my.file'], excludeRe: ['(']}); expect(result.err).toContain('Invalid regular expression'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(fs.readFileSync('my.file', 'utf8')).toBe('foo'); }); }); diff --git a/test/engine/fix/703.test.js b/test/engine/fix/703.test.js index c078ce27..42543964 100644 --- a/test/engine/fix/703.test.js +++ b/test/engine/fix/703.test.js @@ -204,7 +204,7 @@ describe('#703 the names of affected files', () => { }); expect(result.out).toBe('a.file'); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); } finally { fs.chmodSync('locked', 0o755); } @@ -223,7 +223,7 @@ describe('#703 the names of affected files', () => { nl: true, }); expect(result.out).toBe('a.file\n'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); } finally { fs.chmodSync('locked', 0o755); } diff --git a/test/engine/fix/705.test.js b/test/engine/fix/705.test.js index b40376c5..b5cc4da0 100644 --- a/test/engine/fix/705.test.js +++ b/test/engine/fix/705.test.js @@ -81,7 +81,7 @@ describe('#705 the engine returns a promise that resolves when all files are don const result = await rr('o', 'x', {includeGlob: ['*.file'], ...flags}); expect(result.err).toContain('ENAMETOOLONG'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(content(['a.file', tooLong, 'c.file'])).toEqual(['fxx', 'foo', 'fxx']); expect(fs.readdirSync('.')).toHaveLength(3); }); @@ -90,7 +90,7 @@ describe('#705 the engine returns a promise that resolves when all files are don fs.writeFileSync(tooLong, 'foo'); const result = await rr('o', 'x', {includeGlob: ['*.file'], halt: true, ...flags}); expect(result.err).toContain('ENAMETOOLONG'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); }); }); diff --git a/test/engine/fix/708.test.js b/test/engine/fix/708.test.js index 9eee43dc..c2a77616 100644 --- a/test/engine/fix/708.test.js +++ b/test/engine/fix/708.test.js @@ -79,7 +79,7 @@ describe('#708 the number of files in work at the same time is limited', () => { expect(result.out).toBe('my.file\n'); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(fs.readFileSync('my.file', 'utf8')).toBe('fxx'); }); }); diff --git a/test/engine/fix/711.test.js b/test/engine/fix/711.test.js index 37a6b15d..6d3b255f 100644 --- a/test/engine/fix/711.test.js +++ b/test/engine/fix/711.test.js @@ -83,7 +83,7 @@ describe('#711 --glob-pipe', () => { ])('can not be used with %s, as the piped data can only be one thing', async (name, flags) => { fs.writeFileSync('a.file', 'foo'); const result = await rr('o', 'x', {pipedData: 'a.file', globPipe: true, ...flags}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain('--glob-pipe can not be used with'); expect(content('a.file')).toBe('foo'); }); diff --git a/test/engine/fix/720.test.js b/test/engine/fix/720.test.js index a2513fe4..49857ac2 100644 --- a/test/engine/fix/720.test.js +++ b/test/engine/fix/720.test.js @@ -15,7 +15,7 @@ describe('#720 every error starts with ❌', () => { expect(result.err).toStartWith(' ❌ '); expect(result.err).toContain('EACCES'); expect(result.err).toEndWith('\n\n'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); }); test('an error that stops the run', async () => { diff --git a/test/engine/fix/728.test.js b/test/engine/fix/728.test.js index 30c08e57..0cc7e3f4 100644 --- a/test/engine/fix/728.test.js +++ b/test/engine/fix/728.test.js @@ -10,7 +10,7 @@ const flows = [ ['--void-async', {voidAsync: true}], ]; -describe('#728 the exit code is 1 when a file gives an error', () => { +describe('#728 the exit code is not 0 when a file gives an error', () => { test.each(flows)('and the other files are still changed with %s', async (name, flags) => { fs.writeFileSync('a.file', 'foo'); fs.writeFileSync('b.file', 'foo'); @@ -18,7 +18,7 @@ describe('#728 the exit code is 1 when a file gives an error', () => { const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], ...flags}); expect(result.out).toBe('a.file\n'); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(fs.readFileSync('a.file', 'utf8')).toBe('fxx'); }); @@ -31,7 +31,7 @@ describe('#728 the exit code is 1 when a file gives an error', () => { const result = await rr('o', 'x', {includeGlob: ['locked/my.file']}); expect(result.out).toBe(''); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(fs.readFileSync('locked/my.file', 'utf8')).toBe('foo'); } finally { // The folder must be writable again so the test folder can be removed @@ -43,6 +43,6 @@ describe('#728 the exit code is 1 when a file gives an error', () => { fs.writeFileSync('a.file', 'foo'); fs.writeFileSync('b.file', 'foo'); fs.chmodSync('b.file', 0); - expect((await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], [flag]: true})).exit).toBe(1); + expect((await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], [flag]: true})).exit).toBe(2); }); }); diff --git a/test/engine/fix/742.test.js b/test/engine/fix/742.test.js index 454db424..064221de 100644 --- a/test/engine/fix/742.test.js +++ b/test/engine/fix/742.test.js @@ -95,7 +95,7 @@ describe('#742 a panic', () => { fs.writeFileSync('c.file', 'foo'); fs.chmodSync('b.file', 0); const config = {includeGlob: ['*.file'], halt: true}; - expect((await rr('o', 'x', config)).exit).toBe(1); + expect((await rr('o', 'x', config)).exit).toBe(3); fs.writeFileSync('a.file', 'foo'); const status = await rrStatus('o', 'x', config); expect(status.files).toEqual({searched: ['a.file', 'b.file'], updated: ['a.file']}); @@ -103,7 +103,7 @@ describe('#742 a panic', () => { }); test('is thrown when the run can not go on, with the error in the status', async () => { - expect(await rr('(', 'x', {pipedData: 'foo'})).toMatchObject({out: '', exit: 1}); + expect(await rr('(', 'x', {pipedData: 'foo'})).toMatchObject({out: '', exit: 3}); expect((await rrStatus('(', 'x', {pipedData: 'foo'})).errors).toHaveLength(1); }); }); diff --git a/test/engine/halt.test.js b/test/engine/halt.test.js index 6b62e10a..8df1823a 100644 --- a/test/engine/halt.test.js +++ b/test/engine/halt.test.js @@ -10,7 +10,7 @@ describe('--halt', () => { fs.chmodSync('a.file', 0); const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], halt: true}); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(fs.readFileSync('b.file', 'utf8')).toBe('foo'); }); @@ -20,7 +20,7 @@ describe('--halt', () => { fs.chmodSync('a.file', 0); const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file']}); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(fs.readFileSync('b.file', 'utf8')).toBe('fxx'); }); @@ -34,8 +34,8 @@ describe('--halt', () => { test('stops with a number as exit code', async () => { fs.writeFileSync('a.file', 'foo'); fs.chmodSync('a.file', 0); - expect((await rr('o', 'x', {includeGlob: ['a.file'], halt: true})).exit).toBe(1); - expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).toBe(1); + expect((await rr('o', 'x', {includeGlob: ['a.file'], halt: true})).exit).toBe(3); + expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).toBe(3); }); describe('combined', () => { @@ -52,7 +52,7 @@ describe('--halt', () => { test('with an invalid pattern it stops with or without it', async () => { expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).not.toBe(null); - expect((await rr('(', 'x', {pipedData: 'foobar'})).exit).toBe(1); + expect((await rr('(', 'x', {pipedData: 'foobar'})).exit).toBe(3); }); test('with piped data and several flags when all goes well', async () => { diff --git a/test/engine/quiet-total.test.js b/test/engine/quiet-total.test.js index 80c5cf56..80ac758d 100644 --- a/test/engine/quiet-total.test.js +++ b/test/engine/quiet-total.test.js @@ -25,7 +25,7 @@ describe('--quiet-total', () => { test('still stops on an error', async () => { fs.writeFileSync('my.file', 'foobar'); - expect((await rr('(', 'x', {includeGlob: ['my.file'], quietTotal: true})).exit).toBe(1); + expect((await rr('(', 'x', {includeGlob: ['my.file'], quietTotal: true})).exit).toBe(3); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); @@ -50,7 +50,7 @@ describe('--quiet-total', () => { test('with --replacement-pipe and nothing piped it stops in silence', async () => { fs.writeFileSync('my.file', 'foobar'); const config = {includeGlob: ['my.file'], quietTotal: true, replacementPipe: true}; - expect(await rr('o', '_', config)).toEqual({out: '', err: '', exit: 1}); + expect(await rr('o', '_', config)).toEqual({out: '', err: '', exit: 3}); }); test('with --halt it stops in silence', async () => { diff --git a/test/engine/quiet.test.js b/test/engine/quiet.test.js index d0ffdd88..bbc9fd8d 100644 --- a/test/engine/quiet.test.js +++ b/test/engine/quiet.test.js @@ -17,7 +17,7 @@ describe('--quiet', () => { test('still stops on an invalid pattern', async () => { fs.writeFileSync('my.file', 'foobar'); - expect((await rr('(', 'x', {includeGlob: ['my.file'], quiet: true})).exit).toBe(1); + expect((await rr('(', 'x', {includeGlob: ['my.file'], quiet: true})).exit).toBe(3); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); diff --git a/test/engine/replacement-pipe.test.js b/test/engine/replacement-pipe.test.js index ca544000..6ce19b0e 100644 --- a/test/engine/replacement-pipe.test.js +++ b/test/engine/replacement-pipe.test.js @@ -57,7 +57,7 @@ describe('--replacement-pipe', () => { test('stops with an error if nothing is piped in', async () => { fs.writeFileSync('my.file', 'foobar'); const result = await rr('o', '_', {includeGlob: ['my.file'], replacementPipe: true}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain('No data piped into replacement'); expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); }); diff --git a/test/engine/unicode.test.js b/test/engine/unicode.test.js index 5510a18c..0b412258 100644 --- a/test/engine/unicode.test.js +++ b/test/engine/unicode.test.js @@ -38,7 +38,7 @@ describe('--unicode', () => { test('is strict about escapes that have no meaning', async () => { expect((await rr('\\-', 'x', {pipedData: 'a-b'})).out).toBe('axb'); const result = await rr('\\-', 'x', {pipedData: 'a-b', unicode: true}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain('Invalid regular expression'); expect(result.out).toBe(''); }); diff --git a/test/engine/void-section.test.js b/test/engine/void-section.test.js index 3ba95863..5bab9a63 100644 --- a/test/engine/void-section.test.js +++ b/test/engine/void-section.test.js @@ -44,7 +44,7 @@ describe('--void-section', () => { test('a lone § at the end of the pattern is an unfinished escape', async () => { const result = await rr('a§', 'x', {pipedData: 'a§'}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain('Invalid regular expression'); expect((await rr('a§', 'x', {pipedData: 'a§', voidSection: true})).out).toBe('x'); }); diff --git a/test/helper.js b/test/helper.js index ed2ccfd9..2b932fb0 100644 --- a/test/helper.js +++ b/test/helper.js @@ -69,17 +69,17 @@ async function run(pattern, replacement, config) { }; alias(options); status = await engine(options); - // Like the CLI, an error gives the exit code 1 when the run is done + // Like the CLI, an error gives the exit code 2 when the run is done if (status.errors.length) { - result.exit = 1; + result.exit = 2; } } catch (e) { - // Like the CLI, a panic stops the run with the exit code 1 + // Like the CLI, a panic stops the run with the exit code 3 if (!e?.panic) { throw e; } status = e.status; - result.exit = 1; + result.exit = 3; } finally { spies.forEach(spy => spy.mockRestore()); } From 8442cf250108edf36628e43f995271aca239dac8 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 15:39:38 +1100 Subject: [PATCH 08/11] Move the tests for issues to test/fix, one file for each issue --- README.md | 9 +++--- devguide.md | 2 +- package.json | 2 +- test/cli/fix/742.test.js | 50 ------------------------------- test/{engine => }/fix/681.test.js | 2 +- test/{engine => }/fix/682.test.js | 2 +- test/{engine => }/fix/683.test.js | 2 +- test/{cli => }/fix/691.test.js | 2 +- test/{engine => }/fix/699.test.js | 2 +- test/{cli => }/fix/700.test.js | 2 +- test/{engine => }/fix/703.test.js | 2 +- test/{engine => }/fix/705.test.js | 4 +-- test/{engine => }/fix/708.test.js | 2 +- test/{engine => }/fix/710.test.js | 2 +- test/{engine => }/fix/711.test.js | 2 +- test/{engine => }/fix/716.test.js | 2 +- test/{engine => }/fix/718.test.js | 2 +- test/{engine => }/fix/719.test.js | 2 +- test/{engine => }/fix/720.test.js | 2 +- test/{engine => }/fix/726.test.js | 2 +- test/{engine => }/fix/728.test.js | 2 +- test/{cli => }/fix/733.test.js | 2 +- test/{cli => }/fix/736.test.js | 2 +- test/{engine => }/fix/742.test.js | 49 ++++++++++++++++++++++++++++-- 24 files changed, 74 insertions(+), 78 deletions(-) delete mode 100644 test/cli/fix/742.test.js rename test/{engine => }/fix/681.test.js (96%) rename test/{engine => }/fix/682.test.js (97%) rename test/{engine => }/fix/683.test.js (98%) rename test/{cli => }/fix/691.test.js (98%) rename test/{engine => }/fix/699.test.js (99%) rename test/{cli => }/fix/700.test.js (98%) rename test/{engine => }/fix/703.test.js (99%) rename test/{engine => }/fix/705.test.js (97%) rename test/{engine => }/fix/708.test.js (98%) rename test/{engine => }/fix/710.test.js (98%) rename test/{engine => }/fix/711.test.js (98%) rename test/{engine => }/fix/716.test.js (97%) rename test/{engine => }/fix/718.test.js (99%) rename test/{engine => }/fix/719.test.js (98%) rename test/{engine => }/fix/720.test.js (98%) rename test/{engine => }/fix/726.test.js (99%) rename test/{engine => }/fix/728.test.js (98%) rename test/{cli => }/fix/733.test.js (93%) rename test/{cli => }/fix/736.test.js (96%) rename test/{engine => }/fix/742.test.js (79%) diff --git a/README.md b/README.md index b86ef4e4..56ad926a 100644 --- a/README.md +++ b/README.md @@ -345,18 +345,19 @@ rr x y myfile -o > /dev/null 0,21s user 0,04s system 86% cpu 0,294 total ### Regression -The tests come in four groups: +The tests come in five groups: - **The demo** in [test/cli/run.sh](https://github.com/mathiasrw/rexreplace/blob/main/test/cli/run.sh) shows how RexReplace is used, with a few examples of each feature. It is run with the installed commands: `rexreplace` with Node and `rexreplace-bun` with Bun, by the scripts in [test/runtime](https://github.com/mathiasrw/rexreplace/tree/main/test/runtime). You must have both. - **CLI tests** in [`test/cli/*.test.js`](https://github.com/mathiasrw/rexreplace/tree/main/test/cli) run the CLI with Bun's shell, and check stdout, stderr and the exit code. This is where combinations of flags are checked. - **Engine tests** in [`test/engine/*.test.js`](https://github.com/mathiasrw/rexreplace/tree/main/test/engine) call the engine directly (no build and no CLI involved), so they are fast enough to run while developing. There is one file per flag, named after the long version of the flag. +- **Fix tests** in [`test/fix/.test.js`](https://github.com/mathiasrw/rexreplace/tree/main/test/fix) test that an issue stays fixed, with the engine, the CLI or both. - **Fuzzy tests** in [`test/fuzzy/*.test.js`](https://github.com/mathiasrw/rexreplace/tree/main/test/fuzzy) feed the engine random input and compare the result with what plain javascript gives. -The build and the CLI, engine and fuzzy tests are run by [bun](https://bun.sh). After `git clone`'ing the repo and `yarn install`'ing you can invoke the tests with: +The build and the CLI, engine, fix and fuzzy tests are run by [bun](https://bun.sh). After `git clone`'ing the repo and `yarn install`'ing you can invoke the tests with: ```bash -> yarn test # build + demo + CLI and engine tests + fuzzy tests -> yarn test-js # only the CLI and engine tests +> yarn test # build + demo + CLI, engine and fix tests + fuzzy tests +> yarn test-js # only the CLI, engine and fix tests > yarn test-fuzzy # only the fuzzy tests > yarn test-cli # only the demo (build first) ``` diff --git a/devguide.md b/devguide.md index 73b3a8e5..69bdb9a7 100644 --- a/devguide.md +++ b/devguide.md @@ -37,7 +37,7 @@ All output goes through `src/output.ts`. Pick the function by who the message is - Expect the full result where it matters (`{out, err, exit}` from `rr()`), so nothing extra slips through. Use `rrStatus()` for the status of the same run. - A known bug is a `test.todo('BUG #: ...')` until it is fixed. Then it becomes a real test. - The folder tells what is tested, so a file is named after the feature: `test/engine/.test.js` tests the engine with `rr()`, and `test/cli/.test.js` tests the CLI by running it with Bun's `$` shell, like ``sh($`bun ${cli} o x my.file`)``. -- A test for an issue goes in `test/engine/fix/.test.js` or `test/cli/fix/.test.js`. +- A test for an issue goes in `test/fix/.test.js`. It tests the issue with whatever it takes: the engine with `rr()`, the CLI with `sh()`, or both. - `test/cli/run.sh` is a demo of how RexReplace is used: a few clear examples for each feature. Checks of many combinations go in `test/cli/.test.js`. - Handle files in tests with the sync functions of `fs`, like `fs.writeFileSync` and `fs.readFileSync`, so there is no `await` for files. Each test writes the files it uses itself, so you can see them next to the command. - Names in tests say what they hold, like `expectedOutput`, not `named`. diff --git a/package.json b/package.json index 98d5548f..e7c25b65 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "bundle-bun": "bun build src/cli.bun.ts --target=bun --format=cjs --packages=external", "prebuild": "rm -fr bin && yarn format", "sync-version": "bun npm/sync-version.js", - "test-js": "AGENT=1 bun test ./test/engine ./test/cli", + "test-js": "AGENT=1 bun test ./test/engine ./test/cli ./test/fix", "test-fuzzy": "AGENT=1 bun test ./test/fuzzy", "test-minify": "AGENT=1 yarn build-minify && yarn test-cli && yarn test-js", "test-cli": "yarn test-cli-node && yarn test-cli-bun", diff --git a/test/cli/fix/742.test.js b/test/cli/fix/742.test.js deleted file mode 100644 index 0ec96d7d..00000000 --- a/test/cli/fix/742.test.js +++ /dev/null @@ -1,50 +0,0 @@ -// https://github.com/mathiasrw/rexreplace/issues/742 -// The CLI is the only part that acts on the process. It prints the status report with --status and --status-compact. -import {describe, test, expect} from 'bun:test'; -import fs from 'fs'; -import {$} from 'bun'; -import {cli, sh} from '../../helper.js'; - -describe('#742 the CLI', () => { - test('--status prints a line break and the status report with an indent of two spaces after the result', async () => { - fs.writeFileSync('a.file', 'foo'); - const result = await sh($`bun ${cli} o x a.file --status`); - const status = { - files: {searched: ['a.file'], updated: ['a.file']}, - pipe: null, - warnings: [], - errors: [], - }; - expect(result).toEqual({ - out: 'a.file\n\n' + JSON.stringify(status, null, 2) + '\n', - err: '', - exit: 0, - }); - }); - - test.each(['--status-compact', '-c'])( - '%s prints the status report on one line, with no line break in front', - async flag => { - fs.writeFileSync('a.file', 'foo'); - const result = await sh($`bun ${cli} o x a.file ${flag} --quiet-total`); - expect(result.out).toBe( - '{"files":{"searched":["a.file"],"updated":["a.file"]},"pipe":null,"warnings":[],"errors":[]}\n' - ); - } - ); - - test('sets the exit code to 1 when a file gives an error, and prints the status report', async () => { - fs.writeFileSync('a.file', 'foo'); - fs.chmodSync('a.file', 0); - const result = await sh($`bun ${cli} o x a.file --status-compact --quiet-total`); - expect(JSON.parse(result.out).errors[0]).toContain('EACCES'); - expect(result.exit).toBe(1); - }); - - test('ends the process on a panic, and still prints the status report', async () => { - fs.writeFileSync('a.file', 'foo'); - const result = await sh($`bun ${cli} '(' x a.file --status-compact --quiet-total`); - expect(JSON.parse(result.out).errors).toHaveLength(1); - expect(result.exit).toBe(1); - }); -}); diff --git a/test/engine/fix/681.test.js b/test/fix/681.test.js similarity index 96% rename from test/engine/fix/681.test.js rename to test/fix/681.test.js index beef3631..8a07a794 100644 --- a/test/engine/fix/681.test.js +++ b/test/fix/681.test.js @@ -2,7 +2,7 @@ // `--halt` used the error message as exit code: Node 18 exited with 0, newer Node versions crashed with ERR_INVALID_ARG_TYPE. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; describe('#681 --halt exits with a numeric non-zero code', () => { test('a file that can not be read', async () => { diff --git a/test/engine/fix/682.test.js b/test/fix/682.test.js similarity index 97% rename from test/engine/fix/682.test.js rename to test/fix/682.test.js index 73d23ecd..cd2f45b1 100644 --- a/test/engine/fix/682.test.js +++ b/test/fix/682.test.js @@ -2,7 +2,7 @@ // `--quiet` promises "Only display errors (no other info)" but used to hide errors too (making it behave like `--quiet-total`). import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; describe('#682 --quiet still displays errors', () => { test('invalid regular expression', async () => { diff --git a/test/engine/fix/683.test.js b/test/fix/683.test.js similarity index 98% rename from test/engine/fix/683.test.js rename to test/fix/683.test.js index 343c0f88..e73ab08c 100644 --- a/test/engine/fix/683.test.js +++ b/test/fix/683.test.js @@ -2,7 +2,7 @@ // `--exclude-re` promises to "follow same regex flags and setup as the main search" but was used as a regex with no flags (so it was always case sensitive). import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; describe('#683 --exclude-re follows the regex flags of the main search', () => { test('the example from the issue', async () => { diff --git a/test/cli/fix/691.test.js b/test/fix/691.test.js similarity index 98% rename from test/cli/fix/691.test.js rename to test/fix/691.test.js index ebf07034..75f818a1 100644 --- a/test/cli/fix/691.test.js +++ b/test/fix/691.test.js @@ -4,7 +4,7 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {$} from 'bun'; -import {cli, sh} from '../../helper.js'; +import {cli, sh} from '../helper.js'; // With --output the name of the file goes to stderr const expectedOutput = 'my.file\n'; diff --git a/test/engine/fix/699.test.js b/test/fix/699.test.js similarity index 99% rename from test/engine/fix/699.test.js rename to test/fix/699.test.js index 0446bb9b..7b3ac504 100644 --- a/test/engine/fix/699.test.js +++ b/test/fix/699.test.js @@ -3,7 +3,7 @@ // These tests pin that file globs, `--exclude-glob` and piped data are used exactly as they are. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; const voided = {voidEuro: true, voidSection: true}; diff --git a/test/cli/fix/700.test.js b/test/fix/700.test.js similarity index 98% rename from test/cli/fix/700.test.js rename to test/fix/700.test.js index 2bb3fd3d..173d9050 100644 --- a/test/cli/fix/700.test.js +++ b/test/fix/700.test.js @@ -3,7 +3,7 @@ // The replacement is unescaped by the CLI (not the engine), so these tests run the CLI itself. import {describe, test, expect} from 'bun:test'; import {$} from 'bun'; -import {cli, sh} from '../../helper.js'; +import {cli, sh} from '../helper.js'; // Give `input` to the CLI as piped data and return what it gives back const run = (input, ...args) => sh($`bun ${cli} ${args} < ${new Response(input)}`); diff --git a/test/engine/fix/703.test.js b/test/fix/703.test.js similarity index 99% rename from test/engine/fix/703.test.js rename to test/fix/703.test.js index c078ce27..3792ce60 100644 --- a/test/engine/fix/703.test.js +++ b/test/fix/703.test.js @@ -3,7 +3,7 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import path from 'path'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; // Note that rr() from the helper sets voidAsync per default, so the names come in the order of the file list diff --git a/test/engine/fix/705.test.js b/test/fix/705.test.js similarity index 97% rename from test/engine/fix/705.test.js rename to test/fix/705.test.js index b40376c5..cfe7bed3 100644 --- a/test/engine/fix/705.test.js +++ b/test/fix/705.test.js @@ -2,8 +2,8 @@ // The engine used to return before the files were done, and there was no way to wait for it. import {describe, test, expect, spyOn} from 'bun:test'; import fs from 'fs'; -import {engine} from '../../../src/engine.ts'; -import {rr} from '../../helper.js'; +import {engine} from '../../src/engine.ts'; +import {rr} from '../helper.js'; const content = names => names.map(name => fs.readFileSync(name, 'utf8')); diff --git a/test/engine/fix/708.test.js b/test/fix/708.test.js similarity index 98% rename from test/engine/fix/708.test.js rename to test/fix/708.test.js index 9eee43dc..f1ddf837 100644 --- a/test/engine/fix/708.test.js +++ b/test/fix/708.test.js @@ -3,7 +3,7 @@ import {describe, test, expect, spyOn} from 'bun:test'; import fs from 'fs'; import os from 'os'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; const workers = Math.max(16, os.availableParallelism() * 2); diff --git a/test/engine/fix/710.test.js b/test/fix/710.test.js similarity index 98% rename from test/engine/fix/710.test.js rename to test/fix/710.test.js index 7e807d3f..44703061 100644 --- a/test/engine/fix/710.test.js +++ b/test/fix/710.test.js @@ -2,7 +2,7 @@ // There was no way to see which files a command would change without changing them. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, rrStatus} from '../../helper.js'; +import {rr, rrStatus} from '../helper.js'; // Note that rr() from the helper sets voidAsync per default, so the async flow is `voidAsync: false` const flows = [ diff --git a/test/engine/fix/711.test.js b/test/fix/711.test.js similarity index 98% rename from test/engine/fix/711.test.js rename to test/fix/711.test.js index 37a6b15d..d847e0b3 100644 --- a/test/engine/fix/711.test.js +++ b/test/fix/711.test.js @@ -2,7 +2,7 @@ // A list of files from another command could only be used with `xargs` or `$(…)`. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, rrStatus} from '../../helper.js'; +import {rr, rrStatus} from '../helper.js'; const content = name => fs.readFileSync(name, 'utf8'); diff --git a/test/engine/fix/716.test.js b/test/fix/716.test.js similarity index 97% rename from test/engine/fix/716.test.js rename to test/fix/716.test.js index f57dfe77..1873dae4 100644 --- a/test/engine/fix/716.test.js +++ b/test/fix/716.test.js @@ -2,7 +2,7 @@ // The search was case-insensitive by default, but the globs were case-sensitive. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; // The names differ in more than case, so they can both be there on a file system that is case-insensitive describe('#716 the globs are case-insensitive like the search', () => { diff --git a/test/engine/fix/718.test.js b/test/fix/718.test.js similarity index 99% rename from test/engine/fix/718.test.js rename to test/fix/718.test.js index 4ff45080..c25d5147 100644 --- a/test/engine/fix/718.test.js +++ b/test/fix/718.test.js @@ -3,7 +3,7 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import path from 'path'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; const content = name => fs.readFileSync(name, 'utf8'); diff --git a/test/engine/fix/719.test.js b/test/fix/719.test.js similarity index 98% rename from test/engine/fix/719.test.js rename to test/fix/719.test.js index 26d4774b..be96a67d 100644 --- a/test/engine/fix/719.test.js +++ b/test/fix/719.test.js @@ -2,7 +2,7 @@ // Data piped in together with files was not used, and nothing told about it. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; const warning = ' 🟡 Data is piped in, but no flag tells what to do with it. It is not used.\n\n'; const expectedOutput = 'my.file\n'; diff --git a/test/engine/fix/720.test.js b/test/fix/720.test.js similarity index 98% rename from test/engine/fix/720.test.js rename to test/fix/720.test.js index a2513fe4..e00e78c5 100644 --- a/test/engine/fix/720.test.js +++ b/test/fix/720.test.js @@ -3,7 +3,7 @@ // Finding no files was an error with the exit code 1, also when there was just nothing to do. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, rrStatus} from '../../helper.js'; +import {rr, rrStatus} from '../helper.js'; describe('#720 every error starts with ❌', () => { test('an error from a file, and the other files are still changed', async () => { diff --git a/test/engine/fix/726.test.js b/test/fix/726.test.js similarity index 99% rename from test/engine/fix/726.test.js rename to test/fix/726.test.js index 4d667901..f02ca3f1 100644 --- a/test/engine/fix/726.test.js +++ b/test/fix/726.test.js @@ -3,7 +3,7 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import path from 'path'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; // Note that rr() from the helper sets voidAsync per default, so the async flow is `voidAsync: false` const flows = [ diff --git a/test/engine/fix/728.test.js b/test/fix/728.test.js similarity index 98% rename from test/engine/fix/728.test.js rename to test/fix/728.test.js index 30c08e57..4bfb7289 100644 --- a/test/engine/fix/728.test.js +++ b/test/fix/728.test.js @@ -2,7 +2,7 @@ // An error from a file was printed, but the exit code was 0, so a script could not see that something went wrong. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from '../../helper.js'; +import {rr} from '../helper.js'; // Note that rr() from the helper sets voidAsync per default, so the async flow is `voidAsync: false` const flows = [ diff --git a/test/cli/fix/733.test.js b/test/fix/733.test.js similarity index 93% rename from test/cli/fix/733.test.js rename to test/fix/733.test.js index a46a0b25..f1327363 100644 --- a/test/cli/fix/733.test.js +++ b/test/fix/733.test.js @@ -3,7 +3,7 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {$} from 'bun'; -import {cli, sh} from '../../helper.js'; +import {cli, sh} from '../helper.js'; describe('#733 a file glob that reads as a number', () => { test.each(['2024', '1e3', '0x10'])('is kept as a string, like %s', async name => { diff --git a/test/cli/fix/736.test.js b/test/fix/736.test.js similarity index 96% rename from test/cli/fix/736.test.js rename to test/fix/736.test.js index 64d105d0..c7fbac2b 100644 --- a/test/cli/fix/736.test.js +++ b/test/fix/736.test.js @@ -3,7 +3,7 @@ // The flags that ask for the piped data are tested in test/cli/pipe.test.js. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {cli} from '../../helper.js'; +import {cli} from '../helper.js'; // Bun.spawn and not sh() from the helper: the test needs a pipe that stays open, and a pipe in a shell command ends diff --git a/test/engine/fix/742.test.js b/test/fix/742.test.js similarity index 79% rename from test/engine/fix/742.test.js rename to test/fix/742.test.js index 454db424..7320568f 100644 --- a/test/engine/fix/742.test.js +++ b/test/fix/742.test.js @@ -1,9 +1,10 @@ // https://github.com/mathiasrw/rexreplace/issues/742 // The engine ended the process itself and resolved with nothing useful. Now it resolves with a status, a panic is thrown -// when the run can not go on, and only the CLI acts on the process. +// when the run can not go on, and only the CLI acts on the process. It prints the status report with --status and --status-compact. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, rrStatus} from '../../helper.js'; +import {$} from 'bun'; +import {rr, rrStatus, cli, sh} from '../helper.js'; describe('#742 the engine resolves with a status', () => { test('with the files searched and updated', async () => { @@ -183,3 +184,47 @@ describe('#742 the engine part of the status', () => { expect(status.engine.execution.replacement).toBe('y'); }); }); + +describe('#742 the CLI', () => { + test('--status prints a line break and the status report with an indent of two spaces after the result', async () => { + fs.writeFileSync('a.file', 'foo'); + const result = await sh($`bun ${cli} o x a.file --status`); + const status = { + files: {searched: ['a.file'], updated: ['a.file']}, + pipe: null, + warnings: [], + errors: [], + }; + expect(result).toEqual({ + out: 'a.file\n\n' + JSON.stringify(status, null, 2) + '\n', + err: '', + exit: 0, + }); + }); + + test.each(['--status-compact', '-c'])( + '%s prints the status report on one line, with no line break in front', + async flag => { + fs.writeFileSync('a.file', 'foo'); + const result = await sh($`bun ${cli} o x a.file ${flag} --quiet-total`); + expect(result.out).toBe( + '{"files":{"searched":["a.file"],"updated":["a.file"]},"pipe":null,"warnings":[],"errors":[]}\n' + ); + } + ); + + test('sets the exit code to 1 when a file gives an error, and prints the status report', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.chmodSync('a.file', 0); + const result = await sh($`bun ${cli} o x a.file --status-compact --quiet-total`); + expect(JSON.parse(result.out).errors[0]).toContain('EACCES'); + expect(result.exit).toBe(1); + }); + + test('ends the process on a panic, and still prints the status report', async () => { + fs.writeFileSync('a.file', 'foo'); + const result = await sh($`bun ${cli} '(' x a.file --status-compact --quiet-total`); + expect(JSON.parse(result.out).errors).toHaveLength(1); + expect(result.exit).toBe(1); + }); +}); From 4b69a2f02c86cd781647ab2258b53b9639f9d8e0 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 15:40:20 +1100 Subject: [PATCH 09/11] Bring back the tests for #692 as test/fix/692.test.js --- test/fix/692.test.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/fix/692.test.js diff --git a/test/fix/692.test.js b/test/fix/692.test.js new file mode 100644 index 00000000..decbc4fc --- /dev/null +++ b/test/fix/692.test.js @@ -0,0 +1,31 @@ +// https://github.com/mathiasrw/rexreplace/issues/692 +// A single argument containing -v or -h printed the version or help. The checks live in the CLI, so the CLI is run. +import {describe, test, expect} from 'bun:test'; +import {$} from 'bun'; +import {cli, sh} from '../helper.js'; + +describe('#692 a single argument', () => { + test.each(['-v', '-version', '--version'])('%s prints only the version', async arg => { + const result = await sh($`bun ${cli} ${arg}`); + expect(result).toEqual({out: (await sh($`bun ${cli} --version`)).out, err: '', exit: 0}); + expect(result.out).toMatch(/^\S+\n$/); + }); + + test.each(['-h', '-help', '--help'])('%s prints the help to stdout', async arg => { + const result = await sh($`bun ${cli} ${arg}`); + expect(result.out).toContain('Options:'); + expect(result.err).toBe(''); + expect(result.exit).toBe(0); + }); + + // -V is verbose and -H is halt: other flags, not a request for the version or the help + test.each(['my-vile', '-V', 'o-ho', '-H', 'a-v', 'a-h', 'xversion', 'xhelp'])( + '%s is wrong use: the help goes to stderr, nothing to stdout, and the exit code is 3', + async arg => { + const result = await sh($`bun ${cli} ${arg}`); + expect(result.out).toBe(''); + expect(result.err).toContain('Options:'); + expect(result.exit).toBe(3); + } + ); +}); From 0918434dccf85786332bb9227a8c3c5c06bff0e2 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 15:40:27 +1100 Subject: [PATCH 10/11] =?UTF-8?q?Name=20panic()=20in=20the=20devguide=20an?= =?UTF-8?q?d=20tell=20that=20errors=20start=20with=20=E2=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devguide.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devguide.md b/devguide.md index 69bdb9a7..c393494e 100644 --- a/devguide.md +++ b/devguide.md @@ -23,13 +23,13 @@ All output goes through `src/output.ts`. Pick the function by who the message is | `printDone()` | At the end of a run, also when it is stopped | Ends stdout and stderr with a line break when `--nl` is set | | | `info(name)` | Unless `--quiet` | The name of an affected file. stdout, or stderr when stdout has content. | `a.md` | | `warn(msg)` | Unless `--quiet` | Something is likely a mistake, but the run goes on. Also added to `status.warnings`. | `Data is piped in, but no flag tells what to do with it` | -| `error(msg, data)` | Unless `--quiet-total` | Something went wrong. The run goes on, unless `--halt` is set: then a panic is thrown. Also added to `status.errors`, so the CLI gives the exit code 1. | `File not found: a.md` | -| `die(msg, data, help)` | Unless `--quiet-total` | Something went wrong and the run can not go on. Throws a panic, and the CLI exits with 1. Also added to `status.errors`. | `No data piped into replacement` | +| `error(msg, data)` | Unless `--quiet-total` | Something went wrong. Starts with ❌. The run goes on, unless `--halt` is set: then a panic is thrown. Also added to `status.errors`, so the CLI gives the exit code 1. | `File not found: a.md` | +| `panic(msg, data, help)` | Unless `--quiet-total` | Something went wrong and the run can not go on. Starts with ❌ and ends with `No further processing.` Throws a panic, and the CLI exits with 1. Also added to `status.errors`. | `No data piped into replacement` | | `chat(msg)` | `--verbose`, also with `--quiet`, but not with `--quiet-total` | The user: what happens to each file. Only ever to stderr. Also kept in `engine.steps` of the status report. | `Open sync: a.md`, `Nothing changed in: a.md` | - What the run did is in the status report: the engine resolves with it, and `--status` prints it. So there is no extra debug output to print along the way. - `--debug` prints only the status report, with what went into the engine and what it did. -- `warn()`, `error()` and `die()` add to the status report as they go, also when the quiet flags hide what they print. +- `warn()`, `error()` and `panic()` add to the status report as they go, also when the quiet flags hide what they print. ## Tests From 184578c8258dc48922a5afa67f5ef30164ccc7d7 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 15:40:37 +1100 Subject: [PATCH 11/11] Give the exit codes 2 and 3 in the devguide --- devguide.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/devguide.md b/devguide.md index c393494e..e02049e6 100644 --- a/devguide.md +++ b/devguide.md @@ -23,13 +23,14 @@ All output goes through `src/output.ts`. Pick the function by who the message is | `printDone()` | At the end of a run, also when it is stopped | Ends stdout and stderr with a line break when `--nl` is set | | | `info(name)` | Unless `--quiet` | The name of an affected file. stdout, or stderr when stdout has content. | `a.md` | | `warn(msg)` | Unless `--quiet` | Something is likely a mistake, but the run goes on. Also added to `status.warnings`. | `Data is piped in, but no flag tells what to do with it` | -| `error(msg, data)` | Unless `--quiet-total` | Something went wrong. Starts with ❌. The run goes on, unless `--halt` is set: then a panic is thrown. Also added to `status.errors`, so the CLI gives the exit code 1. | `File not found: a.md` | -| `panic(msg, data, help)` | Unless `--quiet-total` | Something went wrong and the run can not go on. Starts with ❌ and ends with `No further processing.` Throws a panic, and the CLI exits with 1. Also added to `status.errors`. | `No data piped into replacement` | +| `error(msg, data)` | Unless `--quiet-total` | Something went wrong. Starts with ❌. The run goes on, unless `--halt` is set: then a panic is thrown. Also added to `status.errors`, so the CLI gives the exit code 2. | `File not found: a.md` | +| `panic(msg, data, help)` | Unless `--quiet-total` | Something went wrong and the run can not go on. Starts with ❌ and ends with `No further processing.` Throws a panic, and the CLI exits with 3. Also added to `status.errors`. | `No data piped into replacement` | | `chat(msg)` | `--verbose`, also with `--quiet`, but not with `--quiet-total` | The user: what happens to each file. Only ever to stderr. Also kept in `engine.steps` of the status report. | `Open sync: a.md`, `Nothing changed in: a.md` | - What the run did is in the status report: the engine resolves with it, and `--status` prints it. So there is no extra debug output to print along the way. - `--debug` prints only the status report, with what went into the engine and what it did. - `warn()`, `error()` and `panic()` add to the status report as they go, also when the quiet flags hide what they print. +- The exit code is 0 when the run is done, 2 when a file gave an error, and 3 for a panic or wrong flags. 1 is kept for a check that says no, like `grep`. See Exit codes in the README. ## Tests