diff --git a/README.md b/README.md index 5d0d3b2f..65a440af 100644 --- a/README.md +++ b/README.md @@ -343,19 +343,20 @@ rr x y myfile -o > /dev/null 0,21s user 0,04s system 86% cpu 0,294 total ### Regression -The tests come in three groups: +The tests come in four groups: -- **CLI tests** in [test/cli/run.sh](https://github.com/mathiasrw/rexreplace/blob/main/test/cli/run.sh) run the built command end to end. [test/cli/global.run.node.sh](https://github.com/mathiasrw/rexreplace/blob/main/test/cli/global.run.node.sh) installs RexReplace globally and makes a small replacement with each command. The scripts in [test/runtime](https://github.com/mathiasrw/rexreplace/tree/main/test/runtime) run them with Node and with Bun. You must have both. -- **JS tests** in [`test/js/*.test.js`](https://github.com/mathiasrw/rexreplace/tree/main/test/js) 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. +- **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. - **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 JS 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 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 + CLI tests + JS tests + fuzzy tests -> yarn test-js # only the JS tests +> yarn test # build + demo + CLI and engine tests + fuzzy tests +> yarn test-js # only the CLI and engine tests > yarn test-fuzzy # only the fuzzy tests -> yarn test-cli # only the CLI tests (build first) +> yarn test-cli # only the demo (build first) ``` The fuzzy tests get new input on every run, so they are not part of what the CI pipeline runs (`yarn test-ci`). The seed is printed, so a failing run can be repeated: diff --git a/bunfig.toml b/bunfig.toml index dab8e7f1..5c75643e 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,2 +1,2 @@ [test] -preload = ["./test/js/helper.js"] +preload = ["./test/helper.js"] diff --git a/devguide.md b/devguide.md index 33fae48c..ce4dd7c7 100644 --- a/devguide.md +++ b/devguide.md @@ -36,8 +36,10 @@ All output goes through `src/output.ts`. Pick the function by who the message is - **Tests are strict, lean and focused too.** One thing per test, and the name of the test says what it 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. -- A test for an issue goes in `test/js/fix/.test.js`. -- Use Bun native file IO (`Bun.write`, `Bun.file`) and `Bun.spawnSync` in new tests (#734). +- 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). - 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/package.json b/package.json index d9af7bc0..98d5548f 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,10 @@ "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/js", + "test-js": "AGENT=1 bun test ./test/engine ./test/cli", "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-global && yarn test-cli-node && yarn test-cli-bun", - "test-cli-global": "AGENT=1 bash test/cli/global.run.node.sh", + "test-cli": "yarn test-cli-node && yarn test-cli-bun", "test-cli-global-bun": "AGENT=1 bash test/cli/global.run.bun.sh", "test-cli-node": "AGENT=1 bash test/runtime/node.sh", "test-cli-bun": "AGENT=1 bash test/runtime/bun.sh", diff --git a/test/js/debug.test.js b/test/cli/debug.test.js similarity index 63% rename from test/js/debug.test.js rename to test/cli/debug.test.js index cb3f2b0c..36950668 100644 --- a/test/js/debug.test.js +++ b/test/cli/debug.test.js @@ -1,23 +1,13 @@ import {describe, test, expect} from 'bun:test'; -import fs from 'fs'; -import path from 'path'; - -// --debug is set by the CLI, so these cases run the CLI itself -const cli = path.join(import.meta.dir, '../../src/cli.ts'); - -function runCli(...args) { - const {stdout, stderr, exitCode} = Bun.spawnSync([process.execPath, cli, ...args], { - stdin: 'ignore', - }); - return {out: stdout.toString(), err: stderr.toString(), exit: exitCode}; -} +import {$} from 'bun'; +import {cli, sh} from '../helper.js'; describe('--debug', () => { test.each(['--debug', '-d'])( '%s prints only the status report, with what went into the engine and what it did', - flag => { - fs.writeFileSync('my.file', 'foobar'); - const result = runCli('o', 'x', 'my.file', flag); + async flag => { + await Bun.write('my.file', 'foobar'); + const result = await sh($`bun ${cli} o x my.file ${flag}`); expect(result.err).toBe(''); expect(result.exit).toBe(0); expect(result.out.startsWith('\n{')).toBe(true); @@ -36,27 +26,27 @@ describe('--debug', () => { 'Work on content from: my.file', 'Write new content to: my.file', ]); - expect(fs.readFileSync('my.file', 'utf8')).toBe('fxxbar'); + expect(await Bun.file('my.file').text()).toBe('fxxbar'); } ); - test('still prints the result of --output before the status report', () => { - fs.writeFileSync('my.file', 'foobar'); - const result = runCli('o', 'x', 'my.file', '--debug', '--output'); + test('still prints the result of --output before the status report', async () => { + await Bun.write('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']); }); - test('gives the errors in the status report, and the exit code 1', () => { - const result = runCli('o', 'x', 'nope.file', '--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`); expect(result.err).toBe(''); expect(result.exit).toBe(1); expect(JSON.parse(result.out).errors).toEqual(['0 files found']); }); - test('gives the error of an invalid pattern in the status report', () => { - fs.writeFileSync('my.file', 'foobar'); - const result = runCli('(', 'x', 'my.file', '--debug'); + test('gives the error of an invalid pattern in the status report', async () => { + await Bun.write('my.file', 'foobar'); + const result = await sh($`bun ${cli} '(' x my.file --debug`); expect(result.err).toBe(''); expect(result.exit).toBe(1); 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 new file mode 100644 index 00000000..4eddc724 --- /dev/null +++ b/test/cli/delimiter.test.js @@ -0,0 +1,176 @@ +// The delimiters after the names and after the content, as given on the command line +import {describe, test, expect} from 'bun:test'; +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'); + 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'); + 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'); + 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` + ); + expect(result.out).toBe('my.file§t€your.file'); + }); + + 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'); + 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'); + const tailFalse = await sh( + $`bun ${cli} b x my.file your.file -A --name-delimiter ', ' --name-delimiter-tail=false` + ); + expect(tailFalse.out).toBe('my.file, your.file'); + }); + + test('with no tail and one name gives just the name', async () => { + await Bun.write('my.file', 'foobar\n'); + const result = await sh( + $`bun ${cli} b x my.file --name-delimiter ', ' --no-name-delimiter-tail` + ); + expect(result.out).toBe('my.file'); + }); +}); + +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'); + 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'); + 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'); + }); + + test('is not put after piped data, unless asked for', async () => { + expect((await sh($`echo -n foo | bun ${cli} o x`)).out).toBe('fxx'); + expect((await sh($`echo -n foo | bun ${cli} o x --content-delimiter '|'`)).out).toBe('fxx'); + const tail = await sh( + $`echo -n foo | bun ${cli} o x --content-delimiter '|' --content-delimiter-tail` + ); + expect(tail.out).toBe('fxx|'); + }); + + test('is used as it is when given', async () => { + await Bun.write('my.file', 'foobar\n'); + await Bun.write('your.file', 'abc123\n'); + expect((await sh($`bun ${cli} b x my.file your.file -o --delimiter ''`)).out).toBe( + 'fooxar\naxc123\n' + ); + const delimiter = '§t-\\n'; + expect( + (await sh($`bun ${cli} b x my.file your.file -o --content-delimiter ${delimiter}`)).out + ).toBe('fooxar\n\t-\naxc123\n\t-\n'); + const noTail = await sh( + $`bun ${cli} b x my.file your.file -o --content-delimiter '|' --no-content-delimiter-tail` + ); + expect(noTail.out).toBe('fooxar\n|axc123\n'); + }); + + 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'); + expect((await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ','`)).out).toBe( + 'b,b,c,' + ); + expect( + (await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --no-tail`)).out + ).toBe('b,b,c'); + }); +}); + +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'); + expect(await sh($`bun ${cli} b x my.file your.file -o --delimiter '|'`)).toEqual({ + out: 'fooxar\n|axc123\n|', + err: 'my.file|your.file|', + exit: 0, + }); + }); + + 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'); + 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'); + 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'); + expect((await sh($`bun ${cli} b x my.file your.file -o --null-out --delimiter ', '`)).err).toBe( + 'my.file, your.file, ' + ); + expect( + (await sh($`bun ${cli} b x my.file your.file -o --delimiter ', ' --content-delimiter '|'`)) + .out + ).toBe('fooxar\n|axc123\n|'); + expect( + (await sh($`bun ${cli} b x my.file your.file -A --name-delimiter ',' --null-out`)).out + ).toBe('my.file,your.file,'); + }); + + 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'); + expect((await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --tail`)).out).toBe( + 'b,b,c,' + ); + expect((await sh($`bun ${cli} b x my.file your.file -o --delimiter '|' --no-tail`)).err).toBe( + 'my.file|your.file' + ); + expect((await sh($`bun ${cli} b x my.file your.file -o --delimiter '|' --tail`)).out).toBe( + 'fooxar\n|axc123\n|' + ); + expect( + ( + await sh( + $`bun ${cli} b x my.file your.file -o --delimiter '|' --no-tail --name-delimiter-tail` + ) + ).err + ).toBe('my.file|your.file|'); + }); +}); + +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'); + 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'); + } + ); +}); diff --git a/test/cli/encoding.test.js b/test/cli/encoding.test.js new file mode 100644 index 00000000..6455915b --- /dev/null +++ b/test/cli/encoding.test.js @@ -0,0 +1,30 @@ +// The CLI checks the encoding before stdin or any file is read (#690) +import {describe, test, expect} from 'bun:test'; +import {$} from 'bun'; +import {cli, sh} from '../helper.js'; + +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'); + 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'); + }); + + 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(lastLine(result.err)).toBe('Unknown encoding: nope'); + }); + + test('the name of an encoding is known in any case', async () => { + expect(await sh($`echo -n foo | bun ${cli} o x --encoding LATIN1`)).toEqual({ + out: 'fxx', + err: '', + exit: 0, + }); + }); +}); diff --git a/test/cli/fix/691.test.js b/test/cli/fix/691.test.js new file mode 100644 index 00000000..d554e5db --- /dev/null +++ b/test/cli/fix/691.test.js @@ -0,0 +1,75 @@ +// https://github.com/mathiasrw/rexreplace/issues/691 +// 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 {$} from 'bun'; +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. +async function replaceO(replacement, ...flags) { + await Bun.write('my.file', 'foobar'); + return sh($`bun ${cli} o ${replacement} my.file --output --no-content-delimiter-tail ${flags}`); +} + +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'); + }); + + test("`\\'` gives a single quote", async () => { + expect(await replaceO("\\'")).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'); + }); + + 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'); + }); + + test('the replacement can not run as code', async () => { + const result = await replaceO("\\'+process.exit(3)//"); + expect(result).toEqual({ + out: "f'+process.exit(3)//'+process.exit(3)//bar", + err: expectedOutput, + exit: 0, + }); + }); + + test('an invalid escape sequence gives an error message and no stack trace', async () => { + const result = await replaceO('\\x4'); + 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}); + }); + + test.each([ + ['\\n', '\n'], + ['\\t', '\t'], + ['\\x41', 'A'], + ['\\u00e9', 'é'], + ['\\\\', '\\'], + ['\\\\n', '\\n'], + ["'", "'"], + ["\\\\'", "\\'"], + ['"', '"'], + ['`${1}`', '`${1}`'], + ])('%j still gives %j', async (replacement, expected) => { + expect(await replaceO(replacement)).toEqual({ + out: `f${expected}${expected}bar`, + err: expectedOutput, + exit: 0, + }); + }); +}); diff --git a/test/cli/fix/700.test.js b/test/cli/fix/700.test.js new file mode 100644 index 00000000..ac06c152 --- /dev/null +++ b/test/cli/fix/700.test.js @@ -0,0 +1,58 @@ +// https://github.com/mathiasrw/rexreplace/issues/700 (also reported as #348) +// `§` is an alias for `\` but in the replacement it was converted after the escape sequences were interpreted, so `§n` gave a backslash and an `n` instead of a line break. +// 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'; + +// 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)}`); + +// Replace the `b` in `foobar` with the replacement +const replaceB = (replacement, ...flags) => run('foobar', 'b', replacement, ...flags); + +describe('#700 § is a full substitute for \\ in the replacement', () => { + test('the example from #348', async () => { + expect(await run('abc\n', '§n', 'X§n')).toEqual({out: 'abcX\n', err: '', exit: 0}); + expect(await run('abc\n', '§n', 'X\\n')).toEqual({out: 'abcX\n', err: '', exit: 0}); + }); + + test.each([ + ['§n', '\n'], + ['§t', '\t'], + ['§r§n', '\r\n'], + ['§x41', 'A'], + ['§u00e9', 'é'], + ['§u{1F600}', '😀'], + ['§§', '\\'], + ['§§n', '\\n'], + ['§', '\\'], + ["§'", "'"], + ['a§\nb', 'ab'], + ['[€&§n]', '[b\n]'], + ])('%j gives %j like it does with a backslash', async (replacement, expected) => { + const result = await replaceB(replacement); + expect(result).toEqual({out: `foo${expected}ar`, err: '', exit: 0}); + expect(await replaceB(replacement.replaceAll('§', '\\'))).toEqual(result); + }); + + 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', + exit: 1, + }); + }); + + test('--void-section keeps § as a plain char', async () => { + expect((await replaceB('§n', '--void-section')).out).toBe('foo§nar'); + expect((await replaceB('§', '--void-section')).out).toBe('foo§ar'); + expect((await replaceB('§x4', '--void-section')).out).toBe('foo§x4ar'); + expect((await replaceB('§\\n', '-§')).out).toBe('foo§\nar'); + }); + + test('--replacement-js still gets § as a backslash in the source', async () => { + expect((await replaceB('"a§nb"', '--replacement-js')).out).toBe('fooa\nbar'); + expect((await replaceB('"a§nb"', '--replacement-js', '--void-section')).out).toBe('fooa§nbar'); + }); +}); diff --git a/test/cli/fix/733.test.js b/test/cli/fix/733.test.js new file mode 100644 index 00000000..0115666e --- /dev/null +++ b/test/cli/fix/733.test.js @@ -0,0 +1,13 @@ +// 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 {$} 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'); + expect(await sh($`bun ${cli} o x ${name}`)).toEqual({out: name + '\n', err: '', exit: 0}); + expect(await Bun.file(name).text()).toBe('fxxbar'); + }); +}); diff --git a/test/js/fix/736.test.js b/test/cli/fix/736.test.js similarity index 83% rename from test/js/fix/736.test.js rename to test/cli/fix/736.test.js index c8d003bc..6c0c1bd7 100644 --- a/test/js/fix/736.test.js +++ b/test/cli/fix/736.test.js @@ -1,10 +1,10 @@ // https://github.com/mathiasrw/rexreplace/issues/736 // 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/run.sh. +// The flags that ask for the piped data are tested in test/cli/pipe.test.js. import {describe, test, expect} from 'bun:test'; -import path from 'path'; +import {cli} from '../../helper.js'; -const cli = path.resolve(import.meta.dir, '../../../src/cli.ts'); +// 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 () => { diff --git a/test/cli/fix/742.test.js b/test/cli/fix/742.test.js new file mode 100644 index 00000000..3e3732ae --- /dev/null +++ b/test/cli/fix/742.test.js @@ -0,0 +1,47 @@ +// 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 {$} 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'); + 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 => { + await Bun.write('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 () => { + const result = await sh($`bun ${cli} o x nope.file --status-compact --quiet-total`); + expect(JSON.parse(result.out).errors).toEqual(['0 files found']); + expect(result.exit).toBe(1); + }); + + test('ends the process on a panic, and still prints the status report', async () => { + await Bun.write('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 new file mode 100644 index 00000000..c5ea1724 --- /dev/null +++ b/test/cli/glob-pipe.test.js @@ -0,0 +1,34 @@ +// --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 {$} 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'); + expect(await sh($`echo -n '*.file' | bun ${cli} b x -g -A`)).toEqual({ + out: 'my.file\nyour.file\n', + err: '', + exit: 0, + }); + }); + + 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'); + 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'); + } + ); + + test('--null-in can only be used with --glob-pipe', async () => { + await Bun.write('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'); + }); +}); diff --git a/test/cli/global.run.bun.sh b/test/cli/global.run.bun.sh index 990683a5..e6782536 100644 --- a/test/cli/global.run.bun.sh +++ b/test/cli/global.run.bun.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Run this after a release. It installs the newly published rexreplace-bun globally with Bun, and that installs the published rexreplace it depends on. -# It removes the global rexreplace. Run global.run.node.sh to get that back. +# It removes the global rexreplace. Run `npm install -g rexreplace` to get that back. DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" diff --git a/test/cli/global.run.node.sh b/test/cli/global.run.node.sh deleted file mode 100644 index bc37a5ae..00000000 --- a/test/cli/global.run.node.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -source $DIR/aserta.sh - - -cd "$(dirname "${BASH_SOURCE[0]}")/../.." - -bun uninstall -g rexreplace rexreplace-bun -npm uninstall --location=global rexreplace rexreplace-bun - -npm install --location=global . - -assert_matches "rr --version" "(dev|[0-9])$" -echo 'foobar' > my.file -rr o x my.file -assert "cat my.file" "fxxbar" - - -assert_matches "rexreplace --version" "(dev|[0-9])$" -echo 'foobar' > my.file -rexreplace o x my.file -assert "cat my.file" "fxxbar" - - -assert_matches "rexreplace-bun --version" "(dev|[0-9])\+bun$" -echo 'foobar' > my.file -rexreplace-bun o x my.file -assert "cat my.file" "fxxbar" - - -rm my.file - - -assert_end "rexreplace global cli" diff --git a/test/cli/names.test.js b/test/cli/names.test.js new file mode 100644 index 00000000..3cd07d56 --- /dev/null +++ b/test/cli/names.test.js @@ -0,0 +1,76 @@ +// 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 {$} 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'); + expect(await sh($`bun ${cli} b x my.file your.file -A`)).toEqual({ + out: 'my.file\nyour.file\n', + err: '', + exit: 0, + }); + }); + + test('stay alone on stdout with --verbose', async () => { + await Bun.write('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'); + 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'); + }); + + describe('go to stderr when stdout has the content', () => { + test('with --output', async () => { + await Bun.write('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'); + }); + + test('with --output-match', async () => { + await Bun.write('my.file', 'foobar\n'); + expect(await sh($`bun ${cli} b _ my.file -m`)).toEqual({ + out: 'b\n', + err: 'my.file\n', + exit: 0, + }); + }); + + test('and are not printed with --quiet', async () => { + await Bun.write('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'); + 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'); + 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'); + 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 new file mode 100644 index 00000000..0435b86f --- /dev/null +++ b/test/cli/nl.test.js @@ -0,0 +1,53 @@ +// --nl (-l) ends stdout and stderr with a line break, unless they already end with one +import {describe, test, expect} from 'bun:test'; +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'); + expect( + (await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --no-tail --nl`)).out + ).toBe('b,b,c\n'); + expect( + (await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --no-tail -l`)).out + ).toBe('b,b,c\n'); + }); + + test('comes after the tail', async () => { + await Bun.write('my.file', 'foobar\n'); + await Bun.write('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'); + expect((await sh($`bun ${cli} b x my.file -o --nl`)).out).toBe('fooxar\n'); + }); + + test('adds a line break after piped data', async () => { + expect((await sh($`echo -n foo | bun ${cli} o x --nl`)).out).toBe('fxx\n'); + }); + + test('adds nothing when nothing is printed', async () => { + expect((await sh($`echo -n foo | bun ${cli} nope x -m --nl`)).out).toBe(''); + }); + + 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'); + 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'); + const stderr = await sh( + $`bun ${cli} b x my.file your.file -o --name-delimiter ',' --no-name-delimiter-tail --nl` + ); + expect(stderr.err).toBe('my.file,your.file\n'); + }); +}); diff --git a/test/cli/null-bytes.test.js b/test/cli/null-bytes.test.js new file mode 100644 index 00000000..4baf6d9c --- /dev/null +++ b/test/cli/null-bytes.test.js @@ -0,0 +1,41 @@ +// Null bytes come out where they went in, piped in and in a file +import {describe, test, expect} from 'bun:test'; +import {$} from 'bun'; +import {cli, sh} from '../helper.js'; + +const data = 'foo\0boo\0'; + +describe('null bytes are kept', () => { + describe('in piped data', () => { + test('when replacing, also with --output', async () => { + expect((await sh($`bun ${cli} o x < ${new Response(data)}`)).out).toBe('fxx\0bxx\0'); + expect((await sh($`bun ${cli} o x --output < ${new Response(data)}`)).out).toBe('fxx\0bxx\0'); + }); + + test('with --output-match and --null-out', async () => { + expect( + (await sh($`bun ${cli} o _ --output-match --null-out < ${new Response(data)}`)).out + ).toBe('o\0o\0o\0o\0'); + }); + }); + + describe('in a file', () => { + test('when it is changed in place', async () => { + await Bun.write('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'); + }); + + test('with --output, followed by the line break after the content', async () => { + await Bun.write('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); + 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 new file mode 100644 index 00000000..a6760c52 --- /dev/null +++ b/test/cli/pipe.test.js @@ -0,0 +1,71 @@ +import {describe, test, expect} from 'bun:test'; +import {$} from 'bun'; +import {cli, sh} from '../helper.js'; + +const warning = ' 🟡 Data is piped in, but no flag tells what to do with it. It is not used.\n\n'; + +describe('--pipe and --js-pipe', () => { + test('without --pipe the code does not get the piped data', async () => { + await Bun.write('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', + exit: 0, + }); + }); + + test('the code gets the piped data with --replacement-js --pipe', async () => { + await Bun.write('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'); + 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'); + expect((await sh($`echo -n ' x ' | bun ${cli} foo pipe my.file -o --js-pipe -T`)).out).toBe( + 'xbar\n' + ); + }); +}); + +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'); + 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'); + }); + + test('is not given without --pipe, as the data is not read', async () => { + await Bun.write('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'); + 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'); + 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'); + 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/cli/run.sh b/test/cli/run.sh index bba288cf..341a7172 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -1,29 +1,17 @@ #!/usr/bin/env bash +# A demo of how RexReplace is used, with a few examples of each feature. +# The checks of many combinations are in test/cli/*.test.js. + DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source $DIR/aserta.sh -# The tests call `rexreplace`. Set RR to test another command (like `rr` or `bun run rexreplace-bun`). +# The tests call `rexreplace`. Set RR to test another command (like `rexreplace-bun`). if [ -n "$RR" ]; then rexreplace() { command $RR "$@"; } fi -# # Command exit codes -# assert_success "true" -# assert_failure "false" -# assert_raises "unknown_cmd" 127 -# -# Expected output -# assert "echo test" "test" -# assert "seq 3" "1\n2\n3" -# assert_contains "echo foobar" "oba" -# assert_startswith "echo foobar" "foo" -# assert_endswith "echo foobar" "bar" -# assert_matches "echo foobar" "^f.*r$" -# -# assert_end "example" - reset() { echo echo "$(date +'%Y-%m-%d %H:%M:%S') - Reset test data " @@ -34,601 +22,238 @@ reset() { -# Plain usage +# Plain usage: the file is changed, and its name is printed reset -rexreplace x x my.file -assert "cat my.file" "foobar" - -reset -rexreplace o x my.file +assert "rexreplace o x my.file" "my.file" assert "cat my.file" "fxxbar" reset rexreplace "b" "\n" my.file assert "cat my.file" "foo\nar" - -# Replacement with a real line break, an escaped single quote or a lone backslash at the end (#691) -reset -rexreplace "b" $'\n' my.file -assert "cat my.file" "foo\nar" - -reset -rexreplace "b" "\\'" my.file -assert "cat my.file" "foo'ar" - -reset -rexreplace "r" '\' my.file -assert "cat my.file" 'fooba\\' - - -# rr can handle a pattern and replcaement starting with '-' +# The pattern and the replacement can start with '-' reset rexreplace '^(.+)$' '- $1' my.file rexreplace '- f' '_' my.file assert "cat my.file" "_oobar" -# Piped data +# Piped data is the text to search, and the result is printed reset assert "cat my.file | rexreplace foo xxx" "xxxbar" - -# -v +# --version and --help, also as -v and -h reset -assert_success "rexreplace -version" +assert_matches "rexreplace --version" "(dev|[0-9])" assert "rexreplace -v" "$(rexreplace --version)" -assert "rexreplace --version" "$(rexreplace -version)" - -# a single argument only containing -v is not a request for the version (-V is verbose) -assert_failure "rexreplace my-vile" -assert_failure "rexreplace -V" - - -# -h -reset -assert_success "rexreplace -help" -assert_success "rexreplace -h" assert_success "rexreplace --help" - -# a single argument only containing -h is not a request for help (-H is halt) +assert_success "rexreplace -h" +# A single argument that only has -v or -h in it is not a request for the version or help +assert_failure "rexreplace my-vile" assert_failure "rexreplace o-ho" -assert_failure "rexreplace -H" - -# -H, --halt, --bail -reset -assert_success "rexreplace --help | grep -F -- '--bail' >/dev/null" -assert_raises "rexreplace o x nope.file --bail" 1 -assert_raises "rexreplace o x nope.file --halt" 1 -assert_raises "rexreplace o x nope.file -H" 1 -# --bail is accepted (not an unknown argument) and leaves files untouched when nothing fails -assert_success "rexreplace o x my.file --bail" -assert "cat my.file" "fxxbar" -# -o +# --halt (or --bail) stops at the first error reset -assert "rexreplace x x my.file --output" "foobar" +assert_raises "rexreplace o x nope.file --halt" 1 +assert_raises "rexreplace o x nope.file --bail" 1 + +# --output prints the result and leaves the file as it is reset assert "rexreplace o x my.file --output" "fxxbar" +assert "cat my.file" "foobar" -# -E -# `default` is the engine of the runtime: V8 with Node and JSC with Bun. The engine of the other runtime can not be picked. +# --engine: `default` is the engine of the runtime: V8 with Node and JSC with Bun reset -# assert "rexreplace o x my.file --output --engine RE2" "fxxbar" # RE2 depricated assert "rexreplace o x my.file --output --engine default" "fxxbar" - if [[ "$(rexreplace --version)" == *+bun ]]; then - assert "rexreplace o x my.file --output --engine JSC" "fxxbar" assert_failure "rexreplace o x my.file --output --engine V8" else - assert "rexreplace o x my.file --output --engine V8" "fxxbar" assert_failure "rexreplace o x my.file --output --engine JSC" fi -# -e -# An unknown encoding stops the run with an error, both for files and for piped data (#690) +# --encoding reset -assert_failure "rexreplace o x my.file -e nope" -assert "cat my.file" 'foobar' -assert_failure "printf foo | rexreplace o x -e nope" -assert "printf foo | rexreplace o x -e nope 2>&1 | grep -v 'exited with code' | tail -1" 'Unknown encoding: nope' -assert "printf foo | rexreplace o x -e LATIN1" 'fxx' +assert "printf foo | rexreplace o x --encoding latin1" 'fxx' +assert_failure "rexreplace o x my.file --encoding nope" -# -b +# --keep-backup reset rexreplace o x my.file --keep-backup assert "cat my.file" "fxxbar" assert "cat my.file.*" "foobar" rm my.file.* -# -I -reset -assert "rexreplace Foo xxx my.file -o" "xxxbar" +# The search is case-insensitive, unless --void-ignore-case reset +assert "rexreplace Foo xxx my.file -o" "xxxbar" assert "rexreplace Foo xxx my.file -o --void-ignore-case" "foobar" -# -G +# --void-global replaces only the first match reset assert "rexreplace o x my.file -o --void-global" "fxobar" -# -O -reset -assert "rexreplace [fb]. _ my.file --output-match" "fo\\nba" - -reset -assert "rexreplace '([fb](.))' _ my.file --output-match" "foo\\nbaa" - -# -GO +# --output-match prints the matches, or the groups if the pattern has any reset -assert "rexreplace [fb]. _ my.file --output-match --voidGlobal" "fo" - +assert "rexreplace '[fb].' _ my.file --output-match" "fo\nba" +assert "rexreplace '([fb](.))' _ my.file --output-match" "foo\nbaa" -# -s +# --dot-all lets . match a line break reset echo foobar >> my.file -assert "rexreplace ar.foo _ my.file -o --dot-all " "foob_bar" +assert "rexreplace ar.foo _ my.file -o --dot-all" "foob_bar" - - -# -M +# ^ is the start of each line, unless --void-multiline reset echo foobar >> my.file assert "rexreplace '^.' 'x' my.file -o" "xoobar\nxoobar" - -reset -echo foobar >> my.file assert "rexreplace '^.' 'x' my.file -o --void-multiline" "xoobar\nfoobar" -# back reference +# Groups in the replacement, with $ or the alias € reset assert "rexreplace '(f?(o))o(.*)' '\$3\$1\$2' my.file -o" "barfoo" - -reset assert "rexreplace '(f?(o))o(.*)' '€3€1€2' my.file -o" "barfoo" - - -# globs -reset -echo barfoo > my_file -assert "rexreplace o x my*le -o" "fxxbar\nbarfxx" -rm my_file - - -# A path that is a file is used as it is, also with glob characters in its name (#718) -reset -echo foo > 'a[1].file' -echo foo > a1.file -assert "rexreplace o x 'a[1].file'" 'a[1].file' -assert "cat a1.file" 'foo' -assert "rexreplace o y '*.file' -X 'a\\[1\\].file' -X 'my.file' -X 'your.file'" 'a1.file' -assert "cat 'a[1].file'" 'fxx' - - -# -€ -reset -assert "rexreplace '.$' '$' my.file -o" 'fooba$' - -reset -assert "rexreplace '.€' '€' my.file -o" 'fooba$' - -reset assert "rexreplace '.€' '€' my.file -o --void-euro" 'foobar' -# -§ -reset -echo foo[bar] > my.file -assert "rexreplace '[\]]' '[' my.file -o" 'foo[bar[' - - +# § is an alias for \, also in the replacement, unless --void-section reset echo foo[bar] > my.file assert "rexreplace '[§]]' '[' my.file -o" 'foo[bar[' - -reset -echo foo[bar] > my.file assert "rexreplace '[§]]' '[' my.file -o --void-section" 'foo[bar]' - -# § works like \ in the replacement too (#700) reset rexreplace 'b' '§n' my.file assert "cat my.file" "foo\nar" -reset -rexreplace 'r' '§§' my.file -assert "cat my.file" 'fooba\\' - -reset -rexreplace 'b' '§n' my.file --void-section -assert "cat my.file" 'foo§nar' - - -# -j -reset -assert "rexreplace 'foo' '2+2' my.file -o --replacement-js" '4bar' - -reset -assert "rexreplace 'foo' '2+2' my.file -o -j" '4bar' +# Globs reset -assert "rexreplace 'foo' '2+2' my.file -o --js" '4bar' - -reset -assert "rexreplace 'foo' 'var i = 2; i + 2' my.file -o --replacement-js" '4bar' +echo barfoo > my_file +assert "rexreplace o x 'my*le' -o" "fxxbar\nbarfxx" +rm my_file +# A path to a file is used as it is, also with glob characters in its name reset -#assert "rexreplace '[fb](.)' '€1.toUpperCase();' my.file -o --replacement-js" 'OoAr' - +echo foo > 'a[1].file' +echo foo > a1.file +assert "rexreplace o x 'a[1].file'" 'a[1].file' +assert "cat a1.file" 'foo' -# Access to js variables +# Globs are case-insensitive like the search reset -assert "rexreplace 'fo(o)bar' '[!!fs,!!globs,find,text.trim()].join(\":\")' my.file -o --replacement-js" 'true:true:fo(o)bar:foobar' +assert "rexreplace 'oo' 'x' '*.FILE' -X 'YOUR*'" 'my.file' +# --exclude-glob and --exclude-re reset -assert "printf foobar | rexreplace 'foobar' \"['file:'+file,'dirpath:'+dirpath,'filename:'+filename,'name:'+name,'ext:'+ext,'text:'+text].join(':')\" -o --replacement-js" 'file:❌:dirpath:❌:filename:❌:name:❌:ext:❌:text:foobar' - - +rexreplace 'b' 'x' '*.file' --exclude-glob 'y*' +assert "cat your.file" 'abc123' reset -assert "rexreplace 'foobar' \"['filename:'+filename,'name:'+name,'ext:'+ext,'text:'+text].join(':')\" my.file -o --replacement-js" 'filename:my.file:name:my:ext:.file:text:foobar' +rexreplace 'b' 'x' '*.file' --exclude-re '^y' +assert "cat my.file" 'fooxar' +assert "cat your.file" 'abc123' -reset -assert "rexreplace 'foo((b)ar)' '€1+€2' my.file -o --replacement-js" 'barb' -## Test replacement-js on multiple files +# --literal searches for the pattern as plain text reset -assert "rexreplace 'foo((b)ar)' '€1+€2' *.file -o --replacement-js" 'barb\nabc123' +assert "rexreplace '*+*' 'b' my.file -o --literal" 'foobar' -reset -assert "rexreplace 'a(.+)' '\"_replace_\"+€1' *.file -o --replacement-js" 'foob_replace_r\n_replace_bc123' -# Content manually testes -# todo: automate test of content +# --replacement-js: the code gives the replacement reset -assert "rexreplace 'foobar' '[require, fs, globs, path, pipe, pipe_, find, find_, text, text_, file, file_, file_rel, file_rel_, dirpath, dirpath_, dirpath_rel, dirpath_rel_, dirname, dirname_, filename, filename_, name, name_, ext, ext_, cwd, cwd_, now, now_, time_obj, time, time_, mtime_obj, mtime, mtime_, ctime_obj, ctime, ctime_, bytes, bytes_, size, size_, _].length' my.file -o --replacement-js" '44' +assert "rexreplace 'foo' '2+2' my.file -o --replacement-js" '4bar' +assert "rexreplace 'foo((b)ar)' '€1+€2' my.file -o --js" 'barb' +assert "rexreplace 'foobar' \"[filename, name, ext].join(':')\" my.file -o -j" 'my.file:my:.file' -# -R +# --replacement-pipe: the replacement is piped in reset assert "printf x | rexreplace 'b' _ my.file -o --replacement-pipe" 'fooxar' - -# --pipe +# --js-pipe: the code gets the piped data as `pipe` reset -assert "printf x | rexreplace 'foo' 'pipe' my.file -o --replacement-js" 'nullbar' -assert "printf x | rexreplace 'foo' 'pipe' my.file -o --replacement-js --pipe" 'xbar' -assert "printf x | rexreplace 'foo' 'pipe' my.file -o -jp" 'xbar' assert "printf x | rexreplace 'foo' 'pipe' my.file -o --js-pipe" 'xbar' -assert "printf ' x ' | rexreplace 'foo' 'pipe' my.file -o --js-pipe -T" 'xbar' - -# Warning when piped data is not used +# Data that is piped in but not used gives a warning reset -assert "printf x | rexreplace 'o' 'x' my.file --pipe 2>&1 | grep 'Data is piped in'" ' 🟡 Data is piped in, but no flag tells what to do with it. It is not used.' -assert "cat my.file" 'fxxbar' -assert_failure "printf x | rexreplace 'o' 'x' my.file 2>&1 | grep 'Data is piped in'" -assert_failure "printf x | rexreplace 'o' 'x' my.file --pipe -q 2>&1 | grep 'Data is piped in'" -assert_failure "printf x | rexreplace 'b' _ my.file -R 2>&1 | grep 'Data is piped in'" -assert_failure "printf x | rexreplace 'b' 'pipe' my.file --js-pipe 2>&1 | grep 'Data is piped in'" -assert_failure "printf '' | rexreplace 'o' 'x' my.file --pipe 2>&1 | grep 'Data is piped in'" -assert_failure "printf '\n' | rexreplace 'o' 'x' my.file --pipe 2>&1 | grep 'Data is piped in'" -assert_failure "printf '\r\n' | rexreplace 'o' 'x' my.file --pipe 2>&1 | grep 'Data is piped in'" +assert "printf x | rexreplace 'o' 'x' my.file --pipe 2>&1 >/dev/null | grep -c 'Data is piped in'" '1' -# Names of affected files go to stdout (#703) +# The names of the changed files go to stdout, so they can be piped on reset assert "rexreplace 'b' 'x' my.file your.file -A" 'my.file\nyour.file' -assert "cat my.file" 'fooxar' - -reset -assert "rexreplace 'b' 'x' my.file 2>&1 >/dev/null" '' - -reset -assert "rexreplace 'b' 'x' my.file -V 2>/dev/null" 'my.file' - -reset -assert "rexreplace 'b' 'x' my.file -V 2>&1 >/dev/null | grep -cx 'my.file'" '0' - -reset -assert "rexreplace 'b' 'x' my.file -q" '' -assert "cat my.file" 'fooxar' - -# With --output and --output-match stdout has the result, so the names go to stderr -reset -assert "rexreplace 'b' 'x' my.file -o 2>/dev/null" 'fooxar' -assert "rexreplace 'b' 'x' my.file -o 2>&1 >/dev/null" 'my.file' -assert "rexreplace 'b' 'x' my.file -o -q 2>&1 >/dev/null" '' -assert "rexreplace 'z' 'x' my.file -o 2>&1 >/dev/null" '' -assert "rexreplace 'b' _ my.file -m 2>/dev/null" 'b' -assert "rexreplace 'b' _ my.file -m 2>&1 >/dev/null" 'my.file' -assert "rexreplace 'b' _ my.file -m -q 2>&1 >/dev/null" '' -assert "rexreplace 'b' 'x' my.file your.file -o --name-delimiter ', ' 2>&1 >/dev/null" 'my.file, your.file, ' -assert "cat my.file" 'foobar' - reset assert "rexreplace 'b' 'x' '*.file' -A | xargs cat" 'fooxar\naxc123' - -# --null -reset -assert "rexreplace 'b' 'x' my.file your.file -A --null | tr '\0' '|'" 'my.file|your.file|' - reset -assert "rexreplace 'b' 'x' my.file your.file -A --null | xargs -0 cat" 'fooxar\naxc123' +assert "rexreplace 'b' 'x' my.file your.file -A --null-out | xargs -0 cat" 'fooxar\naxc123' -# --name-delimiter +# With --output stdout has the result, so the names go to stderr reset -assert "rexreplace 'b' 'x' my.file your.file -A --name-delimiter ', '" 'my.file, your.file, ' - -reset -assert "rexreplace 'b' 'x' my.file your.file -A --name-delimiter '\t§t€'" 'my.file\t\t$your.file\t\t$' +assert "rexreplace 'b' 'x' my.file -o 2>&1 >/dev/null" 'my.file' +# --name-all also names the files where nothing was changed reset -assert "rexreplace 'b' 'x' my.file your.file -A --name-delimiter ',' --null" 'my.file,your.file,' +assert "rexreplace 'z' 'x' my.file your.file -A --name-all" 'my.file\nyour.file' -# --no-name-delimiter-tail +# --name-delimiter, with no tail after the last name reset assert "rexreplace 'b' 'x' my.file your.file -A --name-delimiter ', ' --no-name-delimiter-tail" 'my.file, your.file' +# --content-delimiter goes after the content of each file. A value that starts with - is given with = reset -assert "rexreplace 'b' 'x' my.file your.file -A --name-delimiter ', ' --name-delimiter-tail=false" 'my.file, your.file' +assert "rexreplace 'b' 'x' my.file your.file -o --content-delimiter='---\n'" 'fooxar\n---\naxc123\n---' +# --nl ends what is printed with a line break (tr shows it) reset -assert "rexreplace 'b' 'x' my.file --name-delimiter ', ' --no-name-delimiter-tail" 'my.file' +assert "printf foo | rexreplace 'o' 'x' --nl | tr '\n' '+'" 'fxx+' -reset -assert "rexreplace 'b' 'x' my.file your.file -A --name-delimiter '§t€' --no-name-delimiter-tail -§ -€" 'my.file§t€your.file' -# --name-all +# --simulate names the files that would be changed, but writes nothing reset -assert "rexreplace 'z' 'x' my.file your.file -A --name-all" 'my.file\nyour.file' +assert "rexreplace 'oo' 'x' my.file your.file --simulate" 'my.file' assert "cat my.file" 'foobar' -reset -assert "rexreplace 'b' 'x' my.file your.file -o --name-all 2>&1 >/dev/null" 'my.file\nyour.file' -# By default there is a line break after the content of each file, unless it already ends with one (tr shows the line breaks) +# --full-path prints the full path of each name reset -assert "rexreplace 'b' 'x' my.file your.file -o 2>/dev/null | tr '\n' '+'" 'fooxar+axc123+' -printf foo > a.file -printf bar > b.file -assert "rexreplace 'o' 'x' a.file b.file -o 2>/dev/null | tr '\n' '+'" 'fxx+bar+' -assert "rexreplace '[ab]' _ a.file b.file -m 2>/dev/null | tr '\n' '+'" 'b+a+' -assert "printf foo | rexreplace 'o' 'x' | tr '\n' '+'" 'fxx' +assert "rexreplace 'b' 'x' my.file --full-path" "$(pwd -P)/my.file" -# A given --content-delimiter is used as it is -reset -assert "rexreplace 'b' 'x' my.file your.file -o --delimiter '' 2>/dev/null" 'fooxar\naxc123' -assert "rexreplace '[bc]' _ my.file your.file -m --delimiter ',' 2>/dev/null" 'b,b,c,' -assert "rexreplace '[bc]' _ my.file your.file -m --delimiter ',' --no-tail 2>/dev/null" 'b,b,c' -assert "rexreplace 'b' 'x' my.file your.file -o --content-delimiter '§t-\n' 2>/dev/null" 'fooxar\n\t-\naxc123\n\t-' -assert "rexreplace 'b' 'x' my.file your.file -o --content-delimiter '|' --no-content-delimiter-tail 2>/dev/null" 'fooxar\n|axc123' -assert "printf foo | rexreplace 'o' 'x' --content-delimiter '|'" 'fxx' -assert "printf foo | rexreplace 'o' 'x' --content-delimiter '|' --content-delimiter-tail" 'fxx|' -assert "cat my.file" 'foobar' - -# --delimiter sets both, and the most specific flag wins -reset -assert "rexreplace 'b' 'x' my.file your.file -o --delimiter '|' 2>/dev/null" 'fooxar\n|axc123\n|' -assert "rexreplace 'b' 'x' my.file your.file -o --delimiter '|' 2>&1 >/dev/null" 'my.file|your.file|' -assert "rexreplace 'b' 'x' my.file your.file -o --null 2>/dev/null | tr '\0' '|'" 'fooxar\n|axc123\n|' -assert "rexreplace 'b' 'x' my.file your.file -o --null --delimiter ', ' 2>&1 >/dev/null" 'my.file, your.file, ' -assert "rexreplace 'b' 'x' my.file your.file -o --delimiter ', ' --content-delimiter '|' 2>/dev/null" 'fooxar\n|axc123\n|' -# --tail sets both tails, and the specific tail flag wins -reset -assert "rexreplace '[bc]' _ my.file your.file -m --delimiter ',' --tail 2>/dev/null" 'b,b,c,' -assert "rexreplace 'b' 'x' my.file your.file -o --delimiter '|' --no-tail 2>&1 >/dev/null" 'my.file|your.file' -assert "rexreplace 'b' 'x' my.file your.file -o --delimiter '|' --tail 2>/dev/null" 'fooxar\n|axc123\n|' -assert "rexreplace 'b' 'x' my.file your.file -o --delimiter '|' --no-tail --name-delimiter-tail 2>&1 >/dev/null" 'my.file|your.file|' - -# --nl ends stdout and stderr with a line break, unless they already do (tr shows the line breaks) -reset -assert "rexreplace '[bc]' _ my.file your.file -m --delimiter ',' --no-tail --nl 2>/dev/null | tr '\n' '+'" 'b,b,c+' -assert "rexreplace '[bc]' _ my.file your.file -m --delimiter ',' --no-tail -l 2>/dev/null | tr '\n' '+'" 'b,b,c+' -assert "rexreplace '[bc]' _ my.file your.file -m --delimiter ',' --tail --nl 2>/dev/null | tr '\n' '+'" 'b,b,c,+' -assert "rexreplace 'b' 'x' my.file -o --nl 2>/dev/null | tr '\n' '+'" 'fooxar+' -assert "printf foo | rexreplace 'o' 'x' --nl | tr '\n' '+'" 'fxx+' -assert "printf foo | rexreplace 'nope' 'x' -m --nl | tr '\n' '+'" '' -assert "rexreplace 'b' 'x' my.file your.file -o --name-delimiter ',' --no-name-delimiter-tail --nl 2>&1 >/dev/null | tr '\n' '+'" 'my.file,your.file+' -assert "rexreplace 'b' 'x' my.file your.file -A --name-delimiter ',' --no-name-delimiter-tail --nl | tr '\n' '+'" 'my.file,your.file+' - -reset -assert_failure "rexreplace 'b' 'x' my.file --name-delimiter '\x4'" -assert "rexreplace 'b' 'x' my.file --name-delimiter '\x4' 2>&1 | grep -c 'Invalid escape sequence in --name-delimiter'" '1' -assert "rexreplace 'b' 'x' my.file --content-delimiter '\x4' 2>&1 | grep -c 'Invalid escape sequence in --content-delimiter'" '1' -assert "rexreplace 'b' 'x' my.file --delimiter '\x4' 2>&1 | grep -c 'Invalid escape sequence in --delimiter'" '1' -assert "cat my.file" 'foobar' - -# UTF-8 goes in and out as it is (#735) -reset -assert "printf 'a🦖b' | rexreplace '🦖' '👩‍💻'" 'a👩‍💻b' -# Without --unicode . is half an emoji, so 'x.y' does not match an emoji between x and y -assert "printf 'x🦖y' | rexreplace 'x.y' '_'" 'x🦖y' -assert "printf 'x🦖y' | rexreplace 'x.y' '_' --unicode" '_' -echo foo > '🦖 中.file' -assert "rexreplace 'o' 'x' '🦖 中.file'" '🦖 中.file' -assert "cat '🦖 中.file'" 'fxx' - - -# --glob-pipe: each line of the piped data is a path or a glob to work on (#711) +# --glob-pipe: the files to work on are piped in reset assert "printf 'my.file\nyour.file\n' | rexreplace 'b' 'x' --glob-pipe -A" 'my.file\nyour.file' -reset -assert "printf '*.file' | rexreplace 'b' 'x' -g -A" 'my.file\nyour.file' -# The names of the changed files go on to the next command, also when a name has a line break in it +# The changed files of one run go on to the next run reset assert "rexreplace 'oo' 'x' '*.file' --null-out | rexreplace 'x' 'y' --glob-pipe --null-in" 'my.file' assert "cat my.file" 'fybar' -# The piped data can only be one thing -reset -assert_raises "printf 'my.file' | rexreplace 'b' 'x' -g -R" 1 -assert_raises "printf 'my.file' | rexreplace 'b' 'x' -g --js-pipe" 1 -assert_raises "printf 'my.file' | rexreplace 'b' 'x' --null-in" 1 -assert "cat my.file" 'foobar' -# Null bytes are kept: they come out where they went in. (Bytes that are not valid UTF-8 are only kept with -e latin1.) +# Null bytes are kept reset assert "printf 'foo\0boo\0' | rexreplace 'o' 'x' | tr '\0' '|'" 'fxx|bxx|' -assert "printf 'foo\0boo\0' | rexreplace 'o' 'x' --output | tr '\0' '|'" 'fxx|bxx|' assert "printf 'foo\0boo\0' | rexreplace 'o' '_' --output-match --null-out | tr '\0' '|'" 'o|o|o|o|' -printf 'foo\0boo\0' > null.file -assert "rexreplace 'o' 'x' null.file" 'null.file' -assert "tr '\0' '|' < null.file" 'fxx|bxx|' -printf 'foo\0boo\0' > null.file -assert "rexreplace 'o' 'x' null.file --output 2>/dev/null | tr '\0' '|'" 'fxx|bxx|' -assert "rexreplace 'o' '_' null.file --output-match --null-out 2>/dev/null | tr '\0' '|'" 'o|o|o|o|' - - -# Globs are case-insensitive like the search, unless --void-ignore-case (#716) -reset -assert "rexreplace 'oo' 'x' '*.FILE' -X 'YOUR*'" 'my.file' -assert "cat my.file" 'fxbar' -reset -assert_raises "rexreplace 'oo' 'x' '*.FILE' --void-ignore-case" 1 - -# --simulate: the files that would be changed are named, but nothing is written (#710) -reset -assert "rexreplace 'oo' 'x' my.file your.file --simulate" 'my.file' -assert "cat my.file" 'foobar' -assert "rexreplace 'b' 'x' my.file -S" 'my.file' -assert "cat my.file" 'foobar' - -# --full-path -reset -assert "rexreplace 'b' 'x' my.file -f" "$(pwd -P)/my.file" - -reset -assert "rexreplace 'b' 'x' $(pwd -P)/my.file" "$(pwd -P)/my.file" - -reset -assert "rexreplace 'b' 'x' $(pwd -P)/my.file --full-path" "$(pwd -P)/my.file" -assert "cat my.file" 'fooxar' - - -# -L -reset -assert "rexreplace 'b' '*+*' my.file -o" 'foo*+*ar' -assert "rexreplace '*+*' 'b' my.file -o --literal" 'foobar' - - - -# -x -reset - rexreplace 'b' '*+*' my.file -assert "cat my.file" 'foo*+*ar' -assert "cat your.file" 'abc123' -reset - rexreplace 'b' '*+*' '*.file' -assert "cat my.file" 'foo*+*ar' -assert "cat your.file" 'a*+*c123' +# UTF-8 goes in and out as it is reset - rexreplace 'b' '*+*' '*.file' -x y -assert "cat my.file" 'foobar' -assert "cat your.file" 'abc123' -reset - rexreplace 'b' '*+*' '*.file' -x ^y -assert "cat my.file" 'foo*+*ar' -assert "cat your.file" 'abc123' -reset - rexreplace 'b' '*+*' '*.file' -x ^Y -assert "cat my.file" 'foo*+*ar' -assert "cat your.file" 'abc123' -reset - rexreplace 'b' '*+*' '*.file' -x ^Y --void-ignore-case -assert "cat my.file" 'foo*+*ar' -assert "cat your.file" 'a*+*c123' - - -# -X -reset - rexreplace 'b' '*+*' '*.file' -X '*.file' -assert "cat my.file" 'foobar' -assert "cat your.file" 'abc123' -reset - rexreplace 'b' '*+*' '*.file' -X 'y*' -assert "cat my.file" 'foo*+*ar' -assert "cat your.file" 'abc123' - - - - - - - - - - -# # -P -# reset -# echo '.€' > pattern.txt -# echo '€' > replacement.txt -# assert "rexreplace 'pattern.txt' 'replacement.txt' my.file -o --pattern-file --replacement-file" 'fooba$' -# rm pattern.txt -# rm replacement.txt - -# # Multiply lines in files! -# reset -# echo " . \n € " > pattern.txt -# echo " €\n " > replacement.txt -# assert "rexreplace 'pattern.txt' 'replacement.txt' my.file -o --pattern-file --replacement-file" 'fooba$' -# rm pattern.txt -# rm replacement.txt - - -# # Singe line file (with space) -# reset -# echo 'fooba r' > my.file -# echo ' .€' > pattern.txt -# echo ' €' > replacement.txt -# assert "rexreplace 'pattern.txt' 'replacement.txt' my.file -o --pattern-file --replacement-file" 'fooba $' -# rm pattern.txt -# rm replacement.txt - - - - -# Todo: test -e -# assert "rexreplace ??? ??? my.file -e" "foobar" -# reset - -# Todo: test -q -# assert "rexreplace ??? ??? my.file -q" "foobar" -# reset +assert "printf 'a🦖b' | rexreplace '🦖' '👩‍💻'" 'a👩‍💻b' +assert "printf 'x🦖y' | rexreplace 'x.y' '_' --unicode" '_' +echo foo > '🦖 中.file' +assert "rexreplace 'o' 'x' '🦖 中.file'" '🦖 中.file' -# Todo: test -Q -# assert "rexreplace ??? ??? my.file -Q" "foobar" -# reset -# Todo: test -d -# assert "rexreplace ??? ??? my.file -d" "foobar" -# reset -rm my.file -rm your.file +rm -f *.file assert_end "rexreplace cli" - - - - - - - - - - diff --git a/test/js/basics.test.js b/test/engine/basics.test.js similarity index 92% rename from test/js/basics.test.js rename to test/engine/basics.test.js index e650ea6d..06956eda 100644 --- a/test/js/basics.test.js +++ b/test/engine/basics.test.js @@ -1,8 +1,7 @@ // What rexreplace does when no flags are given 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'; describe('pattern', () => { test('is a regular expression', async () => { @@ -181,24 +180,6 @@ describe('files', () => { expect(fs.readFileSync('.hidden.file', 'utf8')).toBe('foobar'); }); - // The file names are read by src/cli.ts, so this runs the CLI itself - test.each(['2024', '1e3', '0x10'])( - 'can have a name that reads as a number, like %s (#733)', - async name => { - await Bun.write(name, 'foobar'); - const cli = path.join(import.meta.dir, '../../src/cli.ts'); - const {stdout, stderr, exitCode} = Bun.spawnSync([process.execPath, cli, 'o', 'x', name], { - stdin: 'ignore', - }); - expect({out: stdout.toString(), err: stderr.toString(), exit: exitCode}).toEqual({ - out: name + '\n', - err: '', - exit: 0, - }); - expect(await Bun.file(name).text()).toBe('fxxbar'); - } - ); - 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'); diff --git a/test/js/crossover.test.js b/test/engine/crossover.test.js similarity index 99% rename from test/js/crossover.test.js rename to test/engine/crossover.test.js index 86ee983d..ffd705e8 100644 --- a/test/js/crossover.test.js +++ b/test/engine/crossover.test.js @@ -1,7 +1,7 @@ // Flags combined with each other import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('regex flags', () => { const flags = ['voidIgnoreCase', 'voidGlobal', 'voidMultiline', 'dotAll', 'unicode']; diff --git a/test/js/dot-all.test.js b/test/engine/dot-all.test.js similarity index 98% rename from test/js/dot-all.test.js rename to test/engine/dot-all.test.js index 33d1045a..e6b72d39 100644 --- a/test/js/dot-all.test.js +++ b/test/engine/dot-all.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--dot-all', () => { test('lets . match a newline', async () => { diff --git a/test/js/encoding.test.js b/test/engine/encoding.test.js similarity index 99% rename from test/js/encoding.test.js rename to test/engine/encoding.test.js index 288f2a47..9ef47893 100644 --- a/test/js/encoding.test.js +++ b/test/engine/encoding.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; // "fæo" as latin1 (not valid utf8) const latin1 = () => Buffer.from([0x66, 0xe6, 0x6f]); diff --git a/test/js/engine.test.js b/test/engine/engine.test.js similarity index 98% rename from test/js/engine.test.js rename to test/engine/engine.test.js index 8c63f709..84d2fd5e 100644 --- a/test/js/engine.test.js +++ b/test/engine/engine.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--engine', () => { test('default is the engine of the runtime', async () => { diff --git a/test/js/exclude-glob.test.js b/test/engine/exclude-glob.test.js similarity index 99% rename from test/js/exclude-glob.test.js rename to test/engine/exclude-glob.test.js index 699ba215..7a63d4e3 100644 --- a/test/js/exclude-glob.test.js +++ b/test/engine/exclude-glob.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { diff --git a/test/js/exclude-re.test.js b/test/engine/exclude-re.test.js similarity index 99% rename from test/js/exclude-re.test.js rename to test/engine/exclude-re.test.js index f6f4f148..d62477c1 100644 --- a/test/js/exclude-re.test.js +++ b/test/engine/exclude-re.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { diff --git a/test/js/fix/681.test.js b/test/engine/fix/681.test.js similarity index 96% rename from test/js/fix/681.test.js rename to test/engine/fix/681.test.js index 72c19bdd..357c981a 100644 --- a/test/js/fix/681.test.js +++ b/test/engine/fix/681.test.js @@ -1,7 +1,7 @@ // 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 {rr} from '../helper.js'; +import {rr} from '../../helper.js'; describe('#681 --halt exits with a numeric non-zero code', () => { test('no files found', async () => { diff --git a/test/js/fix/682.test.js b/test/engine/fix/682.test.js similarity index 97% rename from test/js/fix/682.test.js rename to test/engine/fix/682.test.js index 5176e780..ac6fc7db 100644 --- a/test/js/fix/682.test.js +++ b/test/engine/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/js/fix/683.test.js b/test/engine/fix/683.test.js similarity index 98% rename from test/js/fix/683.test.js rename to test/engine/fix/683.test.js index 66cc0cd1..aa9b6d63 100644 --- a/test/js/fix/683.test.js +++ b/test/engine/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'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { diff --git a/test/js/fix/699.test.js b/test/engine/fix/699.test.js similarity index 99% rename from test/js/fix/699.test.js rename to test/engine/fix/699.test.js index e22d8946..0295034d 100644 --- a/test/js/fix/699.test.js +++ b/test/engine/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'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { diff --git a/test/js/fix/703.test.js b/test/engine/fix/703.test.js similarity index 99% rename from test/js/fix/703.test.js rename to test/engine/fix/703.test.js index ebbdfc5a..7fe6cd2d 100644 --- a/test/js/fix/703.test.js +++ b/test/engine/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/js/fix/705.test.js b/test/engine/fix/705.test.js similarity index 99% rename from test/js/fix/705.test.js rename to test/engine/fix/705.test.js index 5526b0ab..b40376c5 100644 --- a/test/js/fix/705.test.js +++ b/test/engine/fix/705.test.js @@ -3,7 +3,7 @@ import {describe, test, expect, spyOn} from 'bun:test'; import fs from 'fs'; import {engine} from '../../../src/engine.ts'; -import {rr} from '../helper.js'; +import {rr} from '../../helper.js'; const content = names => names.map(name => fs.readFileSync(name, 'utf8')); diff --git a/test/js/fix/708.test.js b/test/engine/fix/708.test.js similarity index 84% rename from test/js/fix/708.test.js rename to test/engine/fix/708.test.js index 3fc6e88f..723a1db0 100644 --- a/test/js/fix/708.test.js +++ b/test/engine/fix/708.test.js @@ -3,20 +3,14 @@ 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); -// Make `count` files with the content `foo` and give their names -async function makeFiles(count) { - const names = Array.from({length: count}, (_, i) => `file-${i}.txt`); - await Promise.all(names.map(name => Bun.write(name, 'foo'))); - return names; -} - 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 = await makeFiles(workers * 3); + const names = Array.from({length: workers * 3}, (_, i) => `file-${i}.txt`); + await Promise.all(names.map(name => Bun.write(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()); @@ -26,7 +20,8 @@ describe('#708 the number of files in work at the same time is limited', () => { }); test(`at most ${workers} files are read at the same time`, async () => { - await makeFiles(workers * 3); + const names = Array.from({length: workers * 3}, (_, i) => `file-${i}.txt`); + await Promise.all(names.map(name => Bun.write(name, 'foo'))); const readFile = fs.promises.readFile; let reading = 0; let mostReading = 0; @@ -50,7 +45,8 @@ 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 () => { - await makeFiles(16); + const names = Array.from({length: 16}, (_, i) => `file-${i}.txt`); + await Promise.all(names.map(name => Bun.write(name, 'foo'))); const spy = spyOn(os, 'availableParallelism'); try { await rr('o', 'x', {includeGlob: ['*.txt'], voidAsync: false}); diff --git a/test/js/fix/710.test.js b/test/engine/fix/710.test.js similarity index 66% rename from test/js/fix/710.test.js rename to test/engine/fix/710.test.js index 1e6a4fa0..7e807d3f 100644 --- a/test/js/fix/710.test.js +++ b/test/engine/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 = [ @@ -10,21 +10,16 @@ const flows = [ ['--void-async', {voidAsync: true}], ]; -// Make a.file and c.file with `foo`, and b.file with `bar` (nothing to replace) -function makeFiles() { - fs.writeFileSync('a.file', 'foo'); - fs.writeFileSync('b.file', 'bar'); - fs.writeFileSync('c.file', 'foo'); - return {includeGlob: ['*.file'], simulate: true}; -} - const contents = () => ['a.file', 'b.file', 'c.file'].map(name => fs.readFileSync(name, 'utf8')); describe('#710 --simulate', () => { test.each(flows)( 'names the files that would be changed, and changes nothing, with %s', async (name, flags) => { - const result = await rr('o', 'x', {...makeFiles(), ...flags}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const result = await rr('o', 'x', {includeGlob: ['*.file'], simulate: true, ...flags}); expect(result.out.split('\n').sort()).toEqual(['', 'a.file', 'c.file']); expect(result.err).toBe(''); expect(result.exit).toBe(null); @@ -34,12 +29,18 @@ describe('#710 --simulate', () => { ); test('makes no backup, also with --keep-backup', async () => { - await rr('o', 'x', {...makeFiles(), keepBackup: true}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + await rr('o', 'x', {includeGlob: ['*.file'], simulate: true, keepBackup: true}); expect(fs.readdirSync('.').sort()).toEqual(['a.file', 'b.file', 'c.file']); }); test('has the files that would be changed in the status report', async () => { - const status = await rrStatus('o', 'x', makeFiles()); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const status = await rrStatus('o', 'x', {includeGlob: ['*.file'], simulate: true}); expect(status.files).toEqual({ searched: ['a.file', 'b.file', 'c.file'], updated: ['a.file', 'c.file'], @@ -47,7 +48,10 @@ describe('#710 --simulate', () => { }); test('with --name-all names all files, but only the changed ones are updated', async () => { - const config = {...makeFiles(), nameAll: true}; + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const config = {includeGlob: ['*.file'], simulate: true, nameAll: true}; expect((await rr('o', 'x', config)).out).toBe('a.file\nb.file\nc.file\n'); expect((await rrStatus('o', 'x', config)).files.updated).toEqual(['a.file', 'c.file']); expect(contents()).toEqual(['foo', 'bar', 'foo']); diff --git a/test/js/fix/711.test.js b/test/engine/fix/711.test.js similarity index 79% rename from test/js/fix/711.test.js rename to test/engine/fix/711.test.js index 75e2edd5..477206b2 100644 --- a/test/js/fix/711.test.js +++ b/test/engine/fix/711.test.js @@ -2,56 +2,61 @@ // 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'); -// Make each file with the content `foo` -function makeFiles(...names) { - names.forEach(name => fs.writeFileSync(name, 'foo')); -} - describe('#711 --glob-pipe', () => { test('each line of the piped data is a file to work on', async () => { - makeFiles('a.file', 'b.file', 'c.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.file', 'foo'); const config = {pipedData: 'a.file\nb.file\n', globPipe: true}; expect(await rr('o', 'x', config)).toEqual({out: 'a.file\nb.file\n', err: '', exit: null}); expect(content('c.file')).toBe('foo'); }); test('a line can be a glob', async () => { - makeFiles('a.file', 'b.file', 'c.txt'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.txt', 'foo'); const result = await rr('o', 'x', {pipedData: '*.file', globPipe: true}); expect(result.out).toBe('a.file\nb.file\n'); }); test('a line is used together with the paths and globs given as arguments', async () => { - makeFiles('a.file', 'b.file', 'c.txt'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.writeFileSync('c.txt', 'foo'); const config = {pipedData: 'a.file\n', includeGlob: ['*.txt'], globPipe: true}; expect((await rr('o', 'x', config)).out).toBe('a.file\nc.txt\n'); }); test('a line that is a file is used as it is, like an argument (#718)', async () => { - makeFiles('a[1].file', 'a1.file'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); const result = await rr('o', 'x', {pipedData: 'a[1].file\n', globPipe: true}); expect(result.out).toBe('a[1].file\n'); expect(content('a1.file')).toBe('foo'); }); test('a line that starts with ! takes files away from the other globs', async () => { - makeFiles('a.file', 'b.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const result = await rr('o', 'x', {pipedData: '*.file\n!b.file\n', globPipe: true}); expect(result.out).toBe('a.file\n'); }); test('takes Windows line breaks, leaves out empty lines and keeps spaces in names', async () => { - makeFiles('a.file', 'my file.txt'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('my file.txt', 'foo'); const config = {pipedData: '\r\na.file\r\n\r\nmy file.txt\r\n', globPipe: true}; expect((await rr('o', 'x', config)).out).toBe('a.file\nmy file.txt\n'); }); test('with --null-in the lines are separated by null bytes', async () => { - makeFiles('a.file', 'line\nbreak.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('line\nbreak.file', 'foo'); const config = {pipedData: 'a.file\0line\nbreak.file\0', globPipe: true, nullIn: true}; expect((await rr('o', 'x', config)).out).toBe('a.file\nline\nbreak.file\n'); expect(content('line\nbreak.file')).toBe('fxx'); @@ -66,7 +71,7 @@ describe('#711 --glob-pipe', () => { }); test('is told about in the status report', async () => { - makeFiles('a.file'); + fs.writeFileSync('a.file', 'foo'); const status = await rrStatus('o', 'x', {pipedData: 'a.file\n', globPipe: true}); expect(status.pipe).toEqual({bytes: 7, type: 'GLOBS', updated: false}); expect(status.files).toEqual({searched: ['a.file'], updated: ['a.file']}); @@ -76,7 +81,7 @@ describe('#711 --glob-pipe', () => { ['--replacement-pipe', {replacementPipe: true}], ['--pipe', {pipe: true}], ])('can not be used with %s, as the piped data can only be one thing', async (name, flags) => { - makeFiles('a.file'); + fs.writeFileSync('a.file', 'foo'); const result = await rr('o', 'x', {pipedData: 'a.file', globPipe: true, ...flags}); expect(result.exit).toBe(1); expect(result.err).toContain('--glob-pipe can not be used with'); diff --git a/test/js/fix/716.test.js b/test/engine/fix/716.test.js similarity index 84% rename from test/js/fix/716.test.js rename to test/engine/fix/716.test.js index ba77a265..f57dfe77 100644 --- a/test/js/fix/716.test.js +++ b/test/engine/fix/716.test.js @@ -2,17 +2,13 @@ // 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 -function makeFiles() { - fs.writeFileSync('a.file', 'foo'); - fs.writeFileSync('B.FILE', 'foo'); -} - describe('#716 the globs are case-insensitive like the search', () => { test('a glob finds a file whatever the case of its name', async () => { - makeFiles(); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('B.FILE', 'foo'); expect(await rr('o', 'x', {includeGlob: ['*.file']})).toEqual({ out: 'B.FILE\na.file\n', err: '', @@ -21,7 +17,8 @@ describe('#716 the globs are case-insensitive like the search', () => { }); test('--exclude-glob leaves out a file whatever the case of its name', async () => { - makeFiles(); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('B.FILE', 'foo'); // `*.*` finds both files also when globs are case-sensitive, so only --exclude-glob is tested const config = {includeGlob: ['*.*'], excludeGlob: ['b.*']}; expect((await rr('o', 'x', config)).out).toBe('a.file\n'); @@ -29,7 +26,8 @@ describe('#716 the globs are case-insensitive like the search', () => { }); test('--void-ignore-case makes all globs case-sensitive', async () => { - makeFiles(); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('B.FILE', 'foo'); const config = {includeGlob: ['*.file'], voidIgnoreCase: true}; expect((await rr('o', 'x', config)).out).toBe('a.file\n'); fs.writeFileSync('a.file', 'foo'); diff --git a/test/js/fix/718.test.js b/test/engine/fix/718.test.js similarity index 80% rename from test/js/fix/718.test.js rename to test/engine/fix/718.test.js index 01b2fd72..4ec33705 100644 --- a/test/js/fix/718.test.js +++ b/test/engine/fix/718.test.js @@ -3,23 +3,15 @@ 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'); -// Make each file with the content `foo` -function makeFiles(...names) { - names.forEach(name => { - fs.mkdirSync(path.dirname(name), {recursive: true}); - fs.writeFileSync(name, 'foo'); - }); -} - describe('#718 a path that is a file is used as it is', () => { test.each(['my (1).file', '(draft).file', '{a,b}.file', '!important.file', 'a+(b).file'])( '%s is found', async name => { - makeFiles(name); + fs.writeFileSync(name, 'foo'); expect(await rr('o', 'x', {includeGlob: [name]})).toEqual({ out: name + '\n', err: '', @@ -35,7 +27,8 @@ describe('#718 a path that is a file is used as it is', () => { ['what?.file', 'whatX.file'], ['star*.file', 'star123.file'], ])('%s does not also change %s', async (name, other) => { - makeFiles(name, other); + fs.writeFileSync(name, 'foo'); + fs.writeFileSync(other, 'foo'); expect(await rr('o', 'x', {includeGlob: [name]})).toEqual({ out: name + '\n', err: '', @@ -46,7 +39,9 @@ describe('#718 a path that is a file is used as it is', () => { }); test('in a folder', async () => { - makeFiles('pages/[id].file', 'pages/i.file'); + fs.mkdirSync('pages', {recursive: true}); + fs.writeFileSync('pages/[id].file', 'foo'); + fs.writeFileSync('pages/i.file', 'foo'); expect((await rr('o', 'x', {includeGlob: ['pages/[id].file']})).out).toBe('pages/[id].file\n'); expect(content('pages/i.file')).toBe('foo'); }); @@ -54,44 +49,51 @@ describe('#718 a path that is a file is used as it is', () => { describe('#718 together with globs', () => { test('a path that is not a file is still a glob', async () => { - makeFiles('a[1].file', 'a1.file'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); expect((await rr('o', 'x', {includeGlob: ['*.file']})).out).toBe('a1.file\na[1].file\n'); }); test('a path to a folder is not a file, so it is still a glob', async () => { - makeFiles('docs/a.file'); + 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/*.file']})).out).toBe('docs/a.file\n'); }); test('the files are sorted, both those given by their name and those found by a glob', async () => { - makeFiles('a.file', 'b[1].file', 'c.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b[1].file', 'foo'); + fs.writeFileSync('c.file', 'foo'); const result = await rr('o', 'x', {includeGlob: ['b[1].file', 'c.file', 'a*.file']}); expect(result.out).toBe('a.file\nb[1].file\nc.file\n'); }); test('a file given by its name and also found by a glob is only handled once', async () => { - makeFiles('a.file', 'b.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const result = await rr('o', 'x', {includeGlob: ['./a.file', '*.file']}); expect(result).toEqual({out: './a.file\nb.file\n', err: '', exit: null}); expect(fs.readdirSync('.').sort()).toEqual(['a.file', 'b.file']); }); test('a file given with a relative and an absolute path is only handled once', async () => { - makeFiles('a.file'); + fs.writeFileSync('a.file', 'foo'); const result = await rr('o', 'x', {includeGlob: ['a.file', path.resolve('a.file')]}); expect(result.out).toBe('a.file\n'); expect(content('a.file')).toBe('fxx'); }); test('a glob that starts with ! takes files away from the other globs', async () => { - makeFiles('a1.file', 'a2.file'); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('a2.file', 'foo'); expect((await rr('o', 'x', {includeGlob: ['*.file', '!a1.file']})).out).toBe('a2.file\n'); expect(content('a1.file')).toBe('foo'); }); test('a glob that starts with ! does not take away a file given by its name', async () => { - makeFiles('a1.file', 'a2.file'); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('a2.file', 'foo'); const config = {includeGlob: ['a1.file', '*.file', '!a1.file']}; expect((await rr('o', 'x', config)).out).toBe('a1.file\na2.file\n'); expect(content('a1.file')).toBe('fxx'); @@ -101,7 +103,9 @@ describe('#718 together with globs', () => { describe('#718 with --exclude-glob', () => { test('it is always a glob, also when it is the name of a file', async () => { // As a glob, `a[1].file` finds both `a[1].file` and `a1.file` - makeFiles('a[1].file', 'a1.file', 'b.file'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const config = { includeGlob: ['a[1].file', 'a1.file', 'b.file'], excludeGlob: ['a[1].file'], @@ -112,28 +116,32 @@ describe('#718 with --exclude-glob', () => { }); test('a name with glob characters is left out when they are escaped', async () => { - makeFiles('a[1].file', 'a1.file'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); const config = {includeGlob: ['*.file'], excludeGlob: ['a\\[1\\].file']}; expect((await rr('o', 'x', config)).out).toBe('a1.file\n'); expect(content('a[1].file')).toBe('foo'); }); test('all of them are globbed together, so a glob that starts with ! keeps a file', async () => { - makeFiles('a.file', 'b.file'); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); const config = {includeGlob: ['*.file'], excludeGlob: ['*.file', '!a.file']}; expect((await rr('o', 'x', config)).out).toBe('a.file\n'); expect(content('b.file')).toBe('foo'); }); test('a glob leaves out a file given by its name', async () => { - makeFiles('a[1].file'); + 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(content('a[1].file')).toBe('foo'); }); test('a glob leaves out a file given with another path to it', async () => { - makeFiles('docs/a.file', 'docs/b.file'); + fs.mkdirSync('docs', {recursive: true}); + fs.writeFileSync('docs/a.file', 'foo'); + fs.writeFileSync('docs/b.file', 'foo'); const config = {includeGlob: ['./docs/a.file', 'docs/b.file'], excludeGlob: ['docs/a*']}; expect((await rr('o', 'x', config)).out).toBe('docs/b.file\n'); expect(content('docs/a.file')).toBe('foo'); @@ -142,14 +150,15 @@ describe('#718 with --exclude-glob', () => { describe('#718 with --exclude-re', () => { test('it also leaves out a file given by its name', async () => { - makeFiles('a[1].file', 'a1.file'); + fs.writeFileSync('a[1].file', 'foo'); + fs.writeFileSync('a1.file', 'foo'); const config = {includeGlob: ['a[1].file', 'a1.file'], excludeRe: ['\\[']}; expect((await rr('o', 'x', config)).out).toBe('a1.file\n'); expect(content('a[1].file')).toBe('foo'); }); test('it sees the full path with --full-path', async () => { - makeFiles('a.file'); + fs.writeFileSync('a.file', 'foo'); 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}; diff --git a/test/js/fix/719.test.js b/test/engine/fix/719.test.js similarity index 98% rename from test/js/fix/719.test.js rename to test/engine/fix/719.test.js index 17fc6f7e..58220a93 100644 --- a/test/js/fix/719.test.js +++ b/test/engine/fix/719.test.js @@ -1,7 +1,7 @@ // 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 {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/js/fix/726.test.js b/test/engine/fix/726.test.js similarity index 99% rename from test/js/fix/726.test.js rename to test/engine/fix/726.test.js index 843e7147..4c1db156 100644 --- a/test/js/fix/726.test.js +++ b/test/engine/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/js/fix/728.test.js b/test/engine/fix/728.test.js similarity index 98% rename from test/js/fix/728.test.js rename to test/engine/fix/728.test.js index ac4359e2..8c1422bc 100644 --- a/test/js/fix/728.test.js +++ b/test/engine/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/js/fix/742.test.js b/test/engine/fix/742.test.js similarity index 73% rename from test/js/fix/742.test.js rename to test/engine/fix/742.test.js index 82bff536..454db424 100644 --- a/test/js/fix/742.test.js +++ b/test/engine/fix/742.test.js @@ -3,29 +3,14 @@ // when the run can not go on, and only the CLI acts on the process. import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import path from 'path'; -import {rr, rrStatus} from '../helper.js'; - -const cli = path.join(import.meta.dir, '../../../src/cli.ts'); - -// Make a.file and c.file with `foo`, and b.file with `bar` (nothing to replace) -function makeFiles() { - fs.writeFileSync('a.file', 'foo'); - fs.writeFileSync('b.file', 'bar'); - fs.writeFileSync('c.file', 'foo'); - return {includeGlob: ['*.file']}; -} - -function runCli(...args) { - const {stdout, stderr, exitCode} = Bun.spawnSync([process.execPath, cli, ...args], { - stdin: 'ignore', - }); - return {out: stdout.toString(), err: stderr.toString(), exit: exitCode}; -} +import {rr, rrStatus} from '../../helper.js'; describe('#742 the engine resolves with a status', () => { test('with the files searched and updated', async () => { - expect(await rrStatus('o', 'x', makeFiles())).toEqual({ + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + expect(await rrStatus('o', 'x', {includeGlob: ['*.file']})).toEqual({ files: {searched: ['a.file', 'b.file', 'c.file'], updated: ['a.file', 'c.file']}, pipe: null, warnings: [], @@ -34,13 +19,19 @@ describe('#742 the engine resolves with a status', () => { }); test('with the files that would be updated with --output', async () => { - const status = await rrStatus('o', 'x', {...makeFiles(), output: true}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const status = await rrStatus('o', 'x', {includeGlob: ['*.file'], output: true}); expect(status.files.updated).toEqual(['a.file', 'c.file']); expect(fs.readFileSync('a.file', 'utf8')).toBe('foo'); }); test('with --name-all only updates the files that are changed', async () => { - const status = await rrStatus('o', 'x', {...makeFiles(), nameAll: true}); + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'bar'); + fs.writeFileSync('c.file', 'foo'); + const status = await rrStatus('o', 'x', {includeGlob: ['*.file'], nameAll: true}); expect(status.files.updated).toEqual(['a.file', 'c.file']); }); @@ -192,45 +183,3 @@ 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', () => { - fs.writeFileSync('a.file', 'foo'); - const result = runCli('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', - flag => { - fs.writeFileSync('a.file', 'foo'); - const result = runCli('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', () => { - const result = runCli('o', 'x', 'nope.file', '--status-compact', '--quiet-total'); - expect(JSON.parse(result.out).errors).toEqual(['0 files found']); - expect(result.exit).toBe(1); - }); - - test('ends the process on a panic, and still prints the status report', () => { - fs.writeFileSync('a.file', 'foo'); - const result = runCli('(', 'x', 'a.file', '--status-compact', '--quiet-total'); - expect(JSON.parse(result.out).errors).toHaveLength(1); - expect(result.exit).toBe(1); - }); -}); diff --git a/test/js/halt.test.js b/test/engine/halt.test.js similarity index 67% rename from test/js/halt.test.js rename to test/engine/halt.test.js index 42bd66c8..80d65d17 100644 --- a/test/js/halt.test.js +++ b/test/engine/halt.test.js @@ -1,19 +1,6 @@ import {describe, test, expect} from 'bun:test'; -import {spawnSync} from 'child_process'; import fs from 'fs'; -import path from 'path'; -import {rr} from './helper.js'; - -// --bail is a CLI alias for --halt, so these cases run the CLI itself (like test/js/fix/691.test.js). -const cli = path.join(import.meta.dir, '../../src/cli.ts'); - -function runCli(...args) { - const {stdout, stderr, status} = spawnSync(process.execPath, [cli, ...args], { - input: '', - encoding: 'utf8', - }); - return {out: stdout, err: stderr, exit: status}; -} +import {rr} from '../helper.js'; describe('--halt', () => { // a.file can not be read, and the files are handled in the order of the list @@ -50,28 +37,6 @@ describe('--halt', () => { expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).toBe(1); }); - describe('--bail alias', () => { - test('is listed in --help with --halt', () => { - const result = runCli('--help'); - expect(result.exit).toBe(0); - expect(result.out).toContain('--bail'); - expect(result.out).toMatch(/-H,\s*--halt,\s*--bail/); - }); - - test.each(['--bail', '--halt', '-H'])('%s stops on the first error like --halt', flag => { - const result = runCli('o', 'x', 'nope.file', flag); - expect(result.err).toContain('0 files found'); - expect(result.exit).toBe(1); - }); - - test('--bail is accepted and changes nothing when all goes well', () => { - fs.writeFileSync('my.file', 'foobar'); - const result = runCli('o', 'x', 'my.file', '--bail'); - expect(result.exit).toBe(0); - expect(fs.readFileSync('my.file', 'utf8')).toBe('fxxbar'); - }); - }); - describe('combined', () => { test.each(['quiet', 'quietTotal'])('with --%s it still stops', async flag => { const result = await rr('o', 'x', { diff --git a/test/js/keep-backup.test.js b/test/engine/keep-backup.test.js similarity index 99% rename from test/js/keep-backup.test.js rename to test/engine/keep-backup.test.js index ca071e0f..3f7974c4 100644 --- a/test/js/keep-backup.test.js +++ b/test/engine/keep-backup.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; const backups = (dir = '.') => fs.readdirSync(dir).filter(name => name.endsWith('.backup')); diff --git a/test/js/literal.test.js b/test/engine/literal.test.js similarity index 99% rename from test/js/literal.test.js rename to test/engine/literal.test.js index 99d0fb4c..50b96328 100644 --- a/test/js/literal.test.js +++ b/test/engine/literal.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--literal', () => { test('searches for the pattern as plain text', async () => { diff --git a/test/js/output-match.test.js b/test/engine/output-match.test.js similarity index 99% rename from test/js/output-match.test.js rename to test/engine/output-match.test.js index f605a8cf..868907d1 100644 --- a/test/js/output-match.test.js +++ b/test/engine/output-match.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--output-match', () => { test('prints each match on its own line', async () => { diff --git a/test/js/output.test.js b/test/engine/output.test.js similarity index 99% rename from test/js/output.test.js rename to test/engine/output.test.js index d0cda2f1..07a8f315 100644 --- a/test/js/output.test.js +++ b/test/engine/output.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--output', () => { test('sends the result to stdout and leaves the file alone', async () => { diff --git a/test/js/pattern-file.test.js b/test/engine/pattern-file.test.js similarity index 100% rename from test/js/pattern-file.test.js rename to test/engine/pattern-file.test.js diff --git a/test/js/quiet-total.test.js b/test/engine/quiet-total.test.js similarity index 98% rename from test/js/quiet-total.test.js rename to test/engine/quiet-total.test.js index 6a507356..2edf9f14 100644 --- a/test/js/quiet-total.test.js +++ b/test/engine/quiet-total.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--quiet-total', () => { test('does not name the files that were changed', async () => { diff --git a/test/js/quiet.test.js b/test/engine/quiet.test.js similarity index 98% rename from test/js/quiet.test.js rename to test/engine/quiet.test.js index 8b54ab53..becfb8ea 100644 --- a/test/js/quiet.test.js +++ b/test/engine/quiet.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--quiet', () => { test('does not name the files that were changed', async () => { diff --git a/test/js/replacement-file.test.js b/test/engine/replacement-file.test.js similarity index 100% rename from test/js/replacement-file.test.js rename to test/engine/replacement-file.test.js diff --git a/test/js/replacement-js.test.js b/test/engine/replacement-js.test.js similarity index 99% rename from test/js/replacement-js.test.js rename to test/engine/replacement-js.test.js index ee819f0a..56d70a22 100644 --- a/test/js/replacement-js.test.js +++ b/test/engine/replacement-js.test.js @@ -1,7 +1,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 js = async (pattern, code, pipedData, config) => (await rr(pattern, code, {pipedData, replacementJs: true, ...config})).out; diff --git a/test/js/replacement-pipe.test.js b/test/engine/replacement-pipe.test.js similarity index 99% rename from test/js/replacement-pipe.test.js rename to test/engine/replacement-pipe.test.js index b0b8ad7d..0aa21aac 100644 --- a/test/js/replacement-pipe.test.js +++ b/test/engine/replacement-pipe.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--replacement-pipe', () => { test('uses the piped data as replacement', async () => { diff --git a/test/js/unicode.test.js b/test/engine/unicode.test.js similarity index 98% rename from test/js/unicode.test.js rename to test/engine/unicode.test.js index df24f638..5510a18c 100644 --- a/test/js/unicode.test.js +++ b/test/engine/unicode.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--unicode', () => { test('lets . match a full emoji', async () => { diff --git a/test/js/utf8.test.js b/test/engine/utf8.test.js similarity index 90% rename from test/js/utf8.test.js rename to test/engine/utf8.test.js index 1675052e..ec9aed00 100644 --- a/test/js/utf8.test.js +++ b/test/engine/utf8.test.js @@ -2,7 +2,7 @@ // Emoji and other UTF-8 in every way data comes in and goes out. import {describe, test, expect} from 'bun:test'; import path from 'path'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; // rr() gives stdout as a JavaScript string. This is what real stdout (or a file) gets, so half an emoji shows up as `�`. const asUtf8 = text => Buffer.from(text).toString(); @@ -103,27 +103,28 @@ describe('#735 UTF-8 in file names', () => { }); describe('#735 UTF-8 in the delimiters', () => { - async function makeFiles() { + test('--name-delimiter, also with no tail', async () => { await Bun.write('a.file', 'foo'); await Bun.write('b.file', 'foo'); - return {includeGlob: ['*.file']}; - } - - test('--name-delimiter, also with no tail', async () => { - const config = {...(await makeFiles()), nameDelimiter: '🦖'}; + const config = {includeGlob: ['*.file'], nameDelimiter: '🦖'}; expect(asUtf8((await rr('o', 'x', config)).out)).toBe('a.file🦖b.file🦖'); - await makeFiles(); + await Bun.write('a.file', 'foo'); + await Bun.write('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 () => { - const config = {...(await makeFiles()), output: true, contentDelimiter: '👩‍💻'}; + await Bun.write('a.file', 'foo'); + await Bun.write('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 () => { - const config = {...(await makeFiles()), output: true, delimiter: '🦕'}; + await Bun.write('a.file', 'foo'); + await Bun.write('b.file', 'foo'); + const config = {includeGlob: ['*.file'], output: true, delimiter: '🦕'}; const result = await rr('o', 'x', config); expect(asUtf8(result.out)).toBe('fxx🦕fxx🦕'); expect(asUtf8(result.err)).toBe('a.file🦕b.file🦕'); diff --git a/test/js/verbose.test.js b/test/engine/verbose.test.js similarity index 98% rename from test/js/verbose.test.js rename to test/engine/verbose.test.js index dcaf7ac3..bcb53ce0 100644 --- a/test/js/verbose.test.js +++ b/test/engine/verbose.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--verbose', () => { test('tells what is going on', async () => { diff --git a/test/js/version.test.js b/test/engine/version.test.js similarity index 85% rename from test/js/version.test.js rename to test/engine/version.test.js index 63c37ac3..0bcba3d4 100644 --- a/test/js/version.test.js +++ b/test/engine/version.test.js @@ -12,6 +12,4 @@ describe('--version', () => { expect(pkgBun.version).toBe(pkg.version + '+bun'); expect(pkgBun.dependencies.rexreplace).toBe(pkg.version); }); - - test.todo('printing the version is done by src/cli.ts and is tested in test/cli/run.sh'); }); diff --git a/test/js/void-async.test.js b/test/engine/void-async.test.js similarity index 99% rename from test/js/void-async.test.js rename to test/engine/void-async.test.js index 0542b42d..592c3eb8 100644 --- a/test/js/void-async.test.js +++ b/test/engine/void-async.test.js @@ -1,6 +1,6 @@ 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` diff --git a/test/js/void-backup.test.js b/test/engine/void-backup.test.js similarity index 98% rename from test/js/void-backup.test.js rename to test/engine/void-backup.test.js index 288677c2..3ee1ed1e 100644 --- a/test/js/void-backup.test.js +++ b/test/engine/void-backup.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--void-backup', () => { test('writes straight to the file', async () => { diff --git a/test/js/void-euro.test.js b/test/engine/void-euro.test.js similarity index 99% rename from test/js/void-euro.test.js rename to test/engine/void-euro.test.js index 7a74ecd3..c0c64234 100644 --- a/test/js/void-euro.test.js +++ b/test/engine/void-euro.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--void-euro', () => { test('per default € is an alias for $ in the pattern', async () => { diff --git a/test/js/void-global.test.js b/test/engine/void-global.test.js similarity index 98% rename from test/js/void-global.test.js rename to test/engine/void-global.test.js index 96c59ca8..5893b81b 100644 --- a/test/js/void-global.test.js +++ b/test/engine/void-global.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--void-global', () => { test('stops after the first match', async () => { diff --git a/test/js/void-ignore-case.test.js b/test/engine/void-ignore-case.test.js similarity index 98% rename from test/js/void-ignore-case.test.js rename to test/engine/void-ignore-case.test.js index d64a10ea..b8f1444f 100644 --- a/test/js/void-ignore-case.test.js +++ b/test/engine/void-ignore-case.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--void-ignore-case', () => { test('makes the search case sensitive', async () => { diff --git a/test/js/void-multiline.test.js b/test/engine/void-multiline.test.js similarity index 98% rename from test/js/void-multiline.test.js rename to test/engine/void-multiline.test.js index 2778d7ba..7df40933 100644 --- a/test/js/void-multiline.test.js +++ b/test/engine/void-multiline.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--void-multiline', () => { test('makes ^ match the start of the content only', async () => { diff --git a/test/js/void-section.test.js b/test/engine/void-section.test.js similarity index 99% rename from test/js/void-section.test.js rename to test/engine/void-section.test.js index 02c10999..3ba95863 100644 --- a/test/js/void-section.test.js +++ b/test/engine/void-section.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr} from './helper.js'; +import {rr} from '../helper.js'; describe('--void-section', () => { test('per default § is an alias for \\ in the pattern', async () => { diff --git a/test/fuzzy/basics.test.js b/test/fuzzy/basics.test.js index 1187c927..008bdfcc 100644 --- a/test/fuzzy/basics.test.js +++ b/test/fuzzy/basics.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random, ended} from '../js/helper.js'; +import {rr, random, ended} from '../helper.js'; describe('basics', () => { test('gives the same as String.replace with the flags gim', async () => { diff --git a/test/fuzzy/crossover.test.js b/test/fuzzy/crossover.test.js index e87eca20..88b48258 100644 --- a/test/fuzzy/crossover.test.js +++ b/test/fuzzy/crossover.test.js @@ -1,7 +1,7 @@ // Random combinations of flags and input compared with what plain javascript gives import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random, ended} from '../js/helper.js'; +import {rr, random, ended} from '../helper.js'; const flags = [ 'verbose', diff --git a/test/fuzzy/dot-all.test.js b/test/fuzzy/dot-all.test.js index d8c15bb0..fc86ba50 100644 --- a/test/fuzzy/dot-all.test.js +++ b/test/fuzzy/dot-all.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--dot-all', () => { test('gives the same as String.replace with and without the s flag', async () => { diff --git a/test/fuzzy/encoding.test.js b/test/fuzzy/encoding.test.js index ad67345e..ed7262a8 100644 --- a/test/fuzzy/encoding.test.js +++ b/test/fuzzy/encoding.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--encoding', () => { test('latin1 leaves all bytes around the match alone', async () => { diff --git a/test/fuzzy/exclude-glob.test.js b/test/fuzzy/exclude-glob.test.js index 1d637729..892605a2 100644 --- a/test/fuzzy/exclude-glob.test.js +++ b/test/fuzzy/exclude-glob.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { diff --git a/test/fuzzy/exclude-re.test.js b/test/fuzzy/exclude-re.test.js index 9b672320..b3ec0bea 100644 --- a/test/fuzzy/exclude-re.test.js +++ b/test/fuzzy/exclude-re.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; // Make `names` as files containing "foo" and return the ones changed by replacing "foo" const changed = async (names, config) => { diff --git a/test/fuzzy/keep-backup.test.js b/test/fuzzy/keep-backup.test.js index 193190b2..2bd4936e 100644 --- a/test/fuzzy/keep-backup.test.js +++ b/test/fuzzy/keep-backup.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; const backups = (dir = '.') => fs.readdirSync(dir).filter(name => name.endsWith('.backup')); diff --git a/test/fuzzy/literal.test.js b/test/fuzzy/literal.test.js index 7741d527..4113e6f6 100644 --- a/test/fuzzy/literal.test.js +++ b/test/fuzzy/literal.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--literal', () => { const alphabet = 'abAB01 \t\n-[]{}()*+?.,/\\^$|#\'"`%&;<>!~=:@€§æ中'; diff --git a/test/fuzzy/output-match.test.js b/test/fuzzy/output-match.test.js index 100cc12a..e9831bc7 100644 --- a/test/fuzzy/output-match.test.js +++ b/test/fuzzy/output-match.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--output-match', () => { test('prints what String.match finds', async () => { diff --git a/test/fuzzy/output.test.js b/test/fuzzy/output.test.js index 0bc81e8d..d0a76666 100644 --- a/test/fuzzy/output.test.js +++ b/test/fuzzy/output.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random, ended} from '../js/helper.js'; +import {rr, random, ended} from '../helper.js'; const read = names => Promise.all(names.map(name => Bun.file(name).text())); diff --git a/test/fuzzy/replacement-js.test.js b/test/fuzzy/replacement-js.test.js index 3336891b..d3b212aa 100644 --- a/test/fuzzy/replacement-js.test.js +++ b/test/fuzzy/replacement-js.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; const js = async (pattern, code, pipedData, config) => (await rr(pattern, code, {pipedData, replacementJs: true, ...config})).out; diff --git a/test/fuzzy/replacement-pipe.test.js b/test/fuzzy/replacement-pipe.test.js index ac2df810..aa2ca815 100644 --- a/test/fuzzy/replacement-pipe.test.js +++ b/test/fuzzy/replacement-pipe.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random, ended} from '../js/helper.js'; +import {rr, random, ended} from '../helper.js'; describe('--replacement-pipe', () => { test('inserts any piped text without a $ exactly as it is', async () => { diff --git a/test/fuzzy/unicode.test.js b/test/fuzzy/unicode.test.js index 321e757e..2e6c5c88 100644 --- a/test/fuzzy/unicode.test.js +++ b/test/fuzzy/unicode.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--unicode', () => { const alphabet = ['a', 'b', ' ', '\n', 'æ', '中', '🦖', '🦕', '👍🏽', 'é']; diff --git a/test/fuzzy/void-async.test.js b/test/fuzzy/void-async.test.js index dc3061dc..7737f2e6 100644 --- a/test/fuzzy/void-async.test.js +++ b/test/fuzzy/void-async.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--void-async', () => { test('the sync and the async flow end up with the same files', async () => { diff --git a/test/fuzzy/void-backup.test.js b/test/fuzzy/void-backup.test.js index 64a84c6c..1dae589c 100644 --- a/test/fuzzy/void-backup.test.js +++ b/test/fuzzy/void-backup.test.js @@ -1,6 +1,6 @@ import {describe, test, expect} from 'bun:test'; import fs from 'fs'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--void-backup', () => { test('ends up with the same file as with a backup', async () => { diff --git a/test/fuzzy/void-euro.test.js b/test/fuzzy/void-euro.test.js index 4b468f20..693afb53 100644 --- a/test/fuzzy/void-euro.test.js +++ b/test/fuzzy/void-euro.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--void-euro', () => { test('€ and $ can be swapped freely unless voided', async () => { diff --git a/test/fuzzy/void-global.test.js b/test/fuzzy/void-global.test.js index 62a159b0..acdd9ad5 100644 --- a/test/fuzzy/void-global.test.js +++ b/test/fuzzy/void-global.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--void-global', () => { test('gives the same as String.replace without the g flag', async () => { diff --git a/test/fuzzy/void-ignore-case.test.js b/test/fuzzy/void-ignore-case.test.js index 02429715..89284000 100644 --- a/test/fuzzy/void-ignore-case.test.js +++ b/test/fuzzy/void-ignore-case.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--void-ignore-case', () => { test('gives the same as String.replace with and without the i flag', async () => { diff --git a/test/fuzzy/void-multiline.test.js b/test/fuzzy/void-multiline.test.js index e5c7dace..1d0559f9 100644 --- a/test/fuzzy/void-multiline.test.js +++ b/test/fuzzy/void-multiline.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--void-multiline', () => { test('gives the same as String.replace with and without the m flag', async () => { diff --git a/test/fuzzy/void-section.test.js b/test/fuzzy/void-section.test.js index 89c8d853..9e6a0dd5 100644 --- a/test/fuzzy/void-section.test.js +++ b/test/fuzzy/void-section.test.js @@ -1,5 +1,5 @@ import {describe, test, expect} from 'bun:test'; -import {rr, random} from '../js/helper.js'; +import {rr, random} from '../helper.js'; describe('--void-section', () => { test('§ and \\ can be swapped freely unless voided', async () => { diff --git a/test/js/helper.js b/test/helper.js similarity index 84% rename from test/js/helper.js rename to test/helper.js index 4dc9779a..ed2ccfd9 100644 --- a/test/js/helper.js +++ b/test/helper.js @@ -3,8 +3,8 @@ import {spyOn, beforeEach, afterEach} from 'bun:test'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import {engine} from '../../src/engine.ts'; -import {alias} from '../../src/alias.ts'; +import {engine} from '../src/engine.ts'; +import {alias} from '../src/alias.ts'; // Every test runs in its own empty directory const root = process.cwd(); @@ -87,6 +87,16 @@ async function run(pattern, replacement, config) { return {result, status}; } +// The CLI, to run with Bun in a shell command: $`bun ${cli} o x my.file` +export const cli = path.join(import.meta.dir, '../src/cli.ts'); + +// Run a shell command made with Bun's $, and give what was sent to stdout (`out`) and stderr (`err`), and the exit code (`exit`). +// Bun's shell turns non-ASCII text in the command into \u escapes, so give a value with `§`, `€` or emoji as ${'…'}. +export async function sh(command) { + const result = await command.nothrow().quiet(); + return {out: result.stdout.toString(), err: result.stderr.toString(), exit: result.exitCode}; +} + // What the content of the files gives by default: a line break after each item, unless it already ends with one export function ended(...items) { return items.map(item => (item.endsWith('\n') ? item : item + '\n')).join(''); diff --git a/test/js/fix/691.test.js b/test/js/fix/691.test.js deleted file mode 100644 index fb3cc7c0..00000000 --- a/test/js/fix/691.test.js +++ /dev/null @@ -1,83 +0,0 @@ -// https://github.com/mathiasrw/rexreplace/issues/691 -// 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 {spawnSync} from 'child_process'; -import fs from 'fs'; -import path from 'path'; - -const cli = path.join(import.meta.dir, '../../../src/cli.ts'); - -// 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'); - const {stdout, stderr, status} = spawnSync( - process.execPath, - [cli, 'o', replacement, 'my.file', '--output', '--no-content-delimiter-tail', ...flags], - {input: '', encoding: 'utf8'} - ); - return {out: stdout, err: stderr, exit: status}; -} - -describe('#691 replacement that used to crash the CLI', () => { - test('a real line break is inserted as a line break', () => { - expect(replaceO('a\nb')).toEqual({out: 'fa\nba\nbbar', err: expectedOutput, exit: 0}); - expect(replaceO('a\r\nb').out).toBe('fa\r\nba\r\nbbar'); - expect(replaceO('\u2028\u2029').out).toBe('f\u2028\u2029\u2028\u2029bar'); - }); - - test("`\\'` gives a single quote", () => { - expect(replaceO("\\'")).toEqual({out: "f''bar", err: expectedOutput, exit: 0}); - }); - - test('a lone backslash at the end gives a backslash', () => { - expect(replaceO('\\')).toEqual({out: 'f\\\\bar', err: expectedOutput, exit: 0}); - expect(replaceO('x\\').out).toBe('fx\\x\\bar'); - }); - - test('a backslash in front of a real line break still joins the lines (as before)', () => { - expect(replaceO('a\\\nb')).toEqual({out: 'fababbar', err: expectedOutput, exit: 0}); - expect(replaceO('a\\\r\nb').out).toBe('fababbar'); - expect(replaceO('a \\\n b \\\n c').out).toBe('fa b ca b cbar'); - }); - - test('the replacement can not run as code', () => { - const result = replaceO("\\'+process.exit(3)//"); - expect(result).toEqual({ - out: "f'+process.exit(3)//'+process.exit(3)//bar", - err: expectedOutput, - exit: 0, - }); - }); - - test('an invalid escape sequence gives an error message and no stack trace', () => { - const result = replaceO('\\x4'); - expect(result.out).toBe(''); - expect(result.err).toBe(' ❌ Invalid escape sequence in replacement: \\x4\n\n'); - expect(result.exit).toBe(1); - expect(replaceO('\\x4', '--quiet-total')).toEqual({out: '', err: '', exit: 1}); - }); - - test.each([ - ['\\n', '\n'], - ['\\t', '\t'], - ['\\x41', 'A'], - ['\\u00e9', 'é'], - ['\\\\', '\\'], - ['\\\\n', '\\n'], - ["'", "'"], - ["\\\\'", "\\'"], - ['"', '"'], - ['`${1}`', '`${1}`'], - ])('%j still gives %j', (replacement, expected) => { - expect(replaceO(replacement)).toEqual({ - out: `f${expected}${expected}bar`, - err: expectedOutput, - exit: 0, - }); - }); -}); diff --git a/test/js/fix/692.test.js b/test/js/fix/692.test.js deleted file mode 100644 index 5471e10c..00000000 --- a/test/js/fix/692.test.js +++ /dev/null @@ -1,43 +0,0 @@ -// 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 layer, so the CLI is started as a separate process. -import {describe, test, expect} from 'bun:test'; -import {spawnSync} from 'child_process'; -import path from 'path'; - -const cli = path.resolve(import.meta.dir, '../../../src/cli.ts'); - -function run(...args) { - const {stdout, stderr, status} = spawnSync(process.execPath, [cli, ...args], { - encoding: 'utf8', - }); - return {out: stdout, err: stderr, exit: status}; -} - -describe('#692 single argument', () => { - const version = run('--version').out; - - test.each(['-v', '-version', '--version'])('%s prints only the version', arg => { - const result = run(arg); - expect(result.out).toBe(version); - expect(result.out.trim()).toMatch(/^\S+$/); - expect(result.exit).toBe(0); - }); - - test.each(['-h', '-help', '--help'])('%s prints the help with exit code 0', arg => { - const result = run(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 version or help - test.each(['my-vile', '-V', 'o-ho', '-H', 'a-v', 'a-h', 'xversion', 'xhelp'])( - '%s is a usage error: help on stderr, nothing on stdout, exit code 1', - arg => { - const result = run(arg); - expect(result.out).toBe(''); - expect(result.err).toContain('Options:'); - expect(result.exit).toBe(1); - } - ); -}); diff --git a/test/js/fix/700.test.js b/test/js/fix/700.test.js deleted file mode 100644 index 57568dc7..00000000 --- a/test/js/fix/700.test.js +++ /dev/null @@ -1,66 +0,0 @@ -// https://github.com/mathiasrw/rexreplace/issues/700 (also reported as #348) -// `§` is an alias for `\` but in the replacement it was converted after the escape sequences were interpreted, so `§n` gave a backslash and an `n` instead of a line break. -// 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 {spawnSync} from 'child_process'; -import path from 'path'; - -const cli = path.join(import.meta.dir, '../../../src/cli.ts'); - -// Give `input` to the CLI as piped data and return what it gives back -function run(input, ...args) { - const {stdout, stderr, status} = spawnSync(process.execPath, [cli, ...args], { - input, - encoding: 'utf8', - }); - return {out: stdout, err: stderr, exit: status}; -} - -// Replace the `b` in `foobar` with the replacement -const replaceB = (replacement, ...flags) => run('foobar', 'b', replacement, ...flags); - -describe('#700 § is a full substitute for \\ in the replacement', () => { - test('the example from #348', () => { - expect(run('abc\n', '§n', 'X§n')).toEqual({out: 'abcX\n', err: '', exit: 0}); - expect(run('abc\n', '§n', 'X\\n')).toEqual({out: 'abcX\n', err: '', exit: 0}); - }); - - test.each([ - ['§n', '\n'], - ['§t', '\t'], - ['§r§n', '\r\n'], - ['§x41', 'A'], - ['§u00e9', 'é'], - ['§u{1F600}', '😀'], - ['§§', '\\'], - ['§§n', '\\n'], - ['§', '\\'], - ["§'", "'"], - ['a§\nb', 'ab'], - ['[€&§n]', '[b\n]'], - ])('%j gives %j like it does with a backslash', (replacement, expected) => { - const result = replaceB(replacement); - expect(result).toEqual({out: `foo${expected}ar`, err: '', exit: 0}); - expect(replaceB(replacement.replaceAll('§', '\\'))).toEqual(result); - }); - - test('an invalid escape sequence gives the same error as with a backslash', () => { - expect(replaceB('§x4')).toEqual({ - out: '', - err: ' ❌ Invalid escape sequence in replacement: §x4\n\n', - exit: 1, - }); - }); - - test('--void-section keeps § as a plain char', () => { - expect(replaceB('§n', '--void-section').out).toBe('foo§nar'); - expect(replaceB('§', '--void-section').out).toBe('foo§ar'); - expect(replaceB('§x4', '--void-section').out).toBe('foo§x4ar'); - expect(replaceB('§\\n', '-§').out).toBe('foo§\nar'); - }); - - test('--replacement-js still gets § as a backslash in the source', () => { - expect(replaceB('"a§nb"', '--replacement-js').out).toBe('fooa\nbar'); - expect(replaceB('"a§nb"', '--replacement-js', '--void-section').out).toBe('fooa§nbar'); - }); -}); diff --git a/test/js/help.test.js b/test/js/help.test.js deleted file mode 100644 index e8c4554b..00000000 --- a/test/js/help.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import {describe, test} from 'bun:test'; - -describe('--help', () => { - test.todo('is done by src/cli.ts (not the engine) and is tested in test/cli/run.sh'); -}); diff --git a/test/js/trim-pipe.test.js b/test/js/trim-pipe.test.js deleted file mode 100644 index 67b541a0..00000000 --- a/test/js/trim-pipe.test.js +++ /dev/null @@ -1,5 +0,0 @@ -import {describe, test} from 'bun:test'; - -describe('--trim-pipe', () => { - test.todo('is done by src/cli.ts (not the engine) and is tested in test/cli/run.sh'); -}); diff --git a/test/runtime/bun.sh b/test/runtime/bun.sh index 7699958d..a51d53d1 100644 --- a/test/runtime/bun.sh +++ b/test/runtime/bun.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Runs the CLI tests with `bun run` on rexreplace and on rexreplace-bun +# Runs the CLI tests with the rexreplace-bun package, where all commands use Bun set -e @@ -19,34 +19,15 @@ mkdir -p ./npm/rexreplace-bun/node_modules ln -sfn ../../.. ./npm/rexreplace-bun/node_modules/rexreplace # Install in ./tmp/runtime, so the global install on the machine is not changed -npm install --location=global --prefix ./tmp/runtime/rexreplace . npm install --location=global --prefix ./tmp/runtime/rexreplace-bun ./npm/rexreplace-bun -# The commands of rexreplace -export PATH="$PWD/tmp/runtime/rexreplace/bin:$PATH" - -echo "CLI tests of rr from rexreplace (it asks for Node, so Bun starts it with Node)" -bun run rr --version -RR="bun run rr" bash test/cli/run.sh -RR="bun run rr" bash test/cli/run.ai.sh - -echo "CLI tests of rexreplace-bun from rexreplace" -bun run rexreplace-bun --version -RR="bun run rexreplace-bun" bash test/cli/run.sh -RR="bun run rexreplace-bun" bash test/cli/run.ai.sh - -# The commands of rexreplace-bun export PATH="$PWD/tmp/runtime/rexreplace-bun/bin:$PATH" -echo "CLI tests of rr from rexreplace-bun" -bun run rr --version -RR="bun run rr" bash test/cli/run.sh -RR="bun run rr" bash test/cli/run.ai.sh - -echo "CLI tests of rexreplace-bun from rexreplace-bun" -bun run rexreplace-bun --version -RR="bun run rexreplace-bun" bash test/cli/run.sh -RR="bun run rexreplace-bun" bash test/cli/run.ai.sh +# All three commands of rexreplace-bun use Bun, so the version ends with +bun +rr --version | grep -q '+bun$' +rexreplace --version | grep -q '+bun$' +rexreplace-bun --version | grep -q '+bun$' -# rexreplace-bun must use Bun. The version then ends with +bun. -bun run rr --version | grep -q '+bun$' +echo "CLI tests of rexreplace-bun (Bun)" +RR="rexreplace-bun" bash test/cli/run.sh +RR="rexreplace-bun" bash test/cli/run.ai.sh diff --git a/test/runtime/node.sh b/test/runtime/node.sh index 3b849b30..f2c8ec13 100644 --- a/test/runtime/node.sh +++ b/test/runtime/node.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +# Runs the CLI tests with the `rexreplace` command, which uses Node + set -e cd "$(dirname "${BASH_SOURCE[0]}")/../.." @@ -16,7 +18,11 @@ npm install --location=global --prefix ./tmp/runtime/rexreplace . export PATH="$PWD/tmp/runtime/rexreplace/bin:$PATH" -echo "CLI tests of rr from rexreplace" -rr --version -RR="rr" bash test/cli/run.sh -RR="rr" bash test/cli/run.ai.sh +# The package has three commands: rr and rexreplace use Node, and rexreplace-bun uses Bun +rr --version | grep -qv '+bun$' +rexreplace --version | grep -qv '+bun$' +rexreplace-bun --version | grep -q '+bun$' + +echo "CLI tests of rexreplace (Node)" +RR="rexreplace" bash test/cli/run.sh +RR="rexreplace" bash test/cli/run.ai.sh