Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[test]
preload = ["./test/js/helper.js"]
preload = ["./test/helper.js"]
6 changes: 4 additions & 2 deletions devguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 #<issue>: ...')` until it is fixed. Then it becomes a real test.
- A test for an issue goes in `test/js/fix/<issue>.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/<feature>.test.js` tests the engine with `rr()`, and `test/cli/<feature>.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/<issue>.test.js` or `test/cli/fix/<issue>.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/<feature>.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.

Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 14 additions & 24 deletions test/js/debug.test.js → test/cli/debug.test.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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');
Expand Down
176 changes: 176 additions & 0 deletions test/cli/delimiter.test.js
Original file line number Diff line number Diff line change
@@ -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');
}
);
});
30 changes: 30 additions & 0 deletions test/cli/encoding.test.js
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
Loading
Loading