Skip to content

Commit

Permalink
feat(watch): reset and rerun all tests by entering rs (#493)
Browse files Browse the repository at this point in the history
  • Loading branch information
wellwelwel authored Jul 2, 2024
1 parent 5e4893e commit 8a27789
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 84 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "🐷 Poku makes testing easy for Node.js, Bun, Deno, and you at the same time.",
"main": "./lib/modules/index.js",
"license": "MIT",
"type": "commonjs",
"bin": {
"poku": "./lib/bin/index.js"
},
Expand Down
142 changes: 90 additions & 52 deletions src/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { platformIsValid } from '../parsers/get-runtime.js';
import { format } from '../services/format.js';
import { kill } from '../modules/helpers/kill.js';
import { mapTests, normalizePath } from '../services/map-tests.js';
import { watch } from '../services/watch.js';
import { watch, type Watcher } from '../services/watch.js';
import { onSigint, poku } from '../modules/essentials/poku.js';
import { Write } from '../services/write.js';

Expand Down Expand Up @@ -106,72 +106,110 @@ if (debug) {
console.dir(options, { depth: null, colors: true });
}

Promise.all(tasks).then(() => {
poku(dirs, options).then(() => {
if (watchMode) {
const executing = new Set<string>();
const interval = Number(getArg('watch-interval')) || 1500;
const watchers: Set<Watcher> = new Set();
const executing = new Set<string>();
const interval = Number(getArg('watch-interval')) || 1500;

const resultsClear = () => {
fileResults.success.clear();
fileResults.fail.clear();
};
let isRunning = false;

process.removeListener('SIGINT', onSigint);
resultsClear();
const listenStdin = (input: Buffer | string) => {
if (isRunning || executing.size > 0) {
return;
}

if (String(input).trim() === 'rs') {
watchers.forEach((watcher) => watcher.stop());
watchers.clear();
resultsClear();
startTests();
}
};

const resultsClear = () => {
fileResults.success.clear();
fileResults.fail.clear();
};

const startTests = () => {
if (isRunning || executing.size > 0) {
return;
}

isRunning = true;

Promise.all(tasks).then(() => {
poku(dirs, options)
.then(() => {
if (watchMode) {
process.stdin.removeListener('data', listenStdin);
process.removeListener('SIGINT', onSigint);
resultsClear();

mapTests('.', dirs, options.filter, options.exclude).then(
(mappedTests) => {
for (const mappedTest of Array.from(mappedTests.keys())) {
const currentWatcher = watch(mappedTest, (file, event) => {
if (event === 'change') {
const filePath = normalizePath(file);
if (executing.has(filePath)) {
return;
}

executing.add(filePath);
resultsClear();

const tests = mappedTests.get(filePath);
if (!tests) {
return;
}

poku(Array.from(tests), options).then(() => {
setTimeout(() => {
executing.delete(filePath);
}, interval);
});
}
});

currentWatcher.then((watcher) => watchers.add(watcher));
}
}
);

mapTests('.', dirs, options.filter, options.exclude).then(
(mappedTests) => {
for (const mappedTest of Array.from(mappedTests.keys())) {
watch(mappedTest, (file, event) => {
for (const dir of dirs) {
const currentWatcher = watch(dir, (file, event) => {
if (event === 'change') {
const filePath = normalizePath(file);
if (executing.has(filePath)) {
if (executing.has(file)) {
return;
}

executing.add(filePath);
executing.add(file);
resultsClear();

const tests = mappedTests.get(filePath);
if (!tests) {
return;
}

poku(Array.from(tests), options).then(() => {
poku(file, options).then(() => {
setTimeout(() => {
executing.delete(filePath);
executing.delete(file);
}, interval);
});
}
});
}
}
);

for (const dir of dirs) {
watch(dir, (file, event) => {
if (event === 'change') {
if (executing.has(file)) {
return;
}
currentWatcher.then((watcher) => watchers.add(watcher));
}

executing.add(file);
resultsClear();
Write.hr();
Write.log(
`${format('Watching:').bold()} ${format(dirs.join(', ')).underline()}`
);

poku(file, options).then(() => {
setTimeout(() => {
executing.delete(file);
}, interval);
});
}
});
}

Write.hr();
Write.log(
`${format('Watching:').bold()} ${format(dirs.join(', ')).underline()}`
);
}
process.stdin.setEncoding('utf-8');
process.stdin.on('data', listenStdin);
}
})
.finally(() => {
isRunning = false;
});
});
});
};

startTests();
2 changes: 0 additions & 2 deletions src/modules/helpers/list-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const isFile = async (fullPath: string) =>
export const escapeRegExp = (string: string) =>
string.replace(regex.safeRegExp, '\\$&');

/* c8 ignore next 3 */
const envFilter = env.FILTER?.trim()
? new RegExp(escapeRegExp(env.FILTER), 'i')
: undefined;
Expand All @@ -42,7 +41,6 @@ export const getAllFiles = async (
): Promise<Set<string>> => {
const currentFiles = await readdir(sanitizePath(dirPath));

/* c8 ignore next 3 */
const filter: RegExp = envFilter
? envFilter
: configs?.filter instanceof RegExp
Expand Down
2 changes: 1 addition & 1 deletion src/services/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { join } from 'node:path';
import { readdir, stat } from '../polyfills/fs.js';
import { listFiles } from '../modules/helpers/list-files.js';

class Watcher {
export class Watcher {
private rootDir: string;
private files: string[] = [];
private fileWatchers: Map<string, FSWatcher> = new Map();
Expand Down
63 changes: 45 additions & 18 deletions test/c8.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import { poku, test, describe, it, assert } from '../src/modules/index.js';
import { isWindows } from '../src/parsers/get-runner.js';
import { inspectCLI } from './helpers/capture-cli.test.js';

console.log('\n😴 It will be really slow (press "Ctrl + C" twice to cancel)\n');

test(async () => {
await describe('CLI', async () => {
await it('Sequential (Unit)', async () => {
await it('Sequential (Just Touch)', async () => {
const results = await inspectCLI(
'npx tsx src/bin/index.ts --platform=node --include=test/unit'
'npx tsx src/bin/index.ts --platform=node --include=test/integration/import.test.ts'
);

console.log(results.stdout);
Expand All @@ -17,9 +15,9 @@ test(async () => {
assert.strictEqual(results.exitCode, 0, 'Passed');
});

await it('Parallel (Unit)', async () => {
await it('Parallel (Just Touch)', async () => {
const results = await inspectCLI(
'npx tsx src/bin/index.ts --platform=node --parallel --include=test/unit'
'npx tsx src/bin/index.ts --platform=node --parallel --include=test/integration/import.test.ts'
);

console.log(results.stdout);
Expand All @@ -28,11 +26,38 @@ test(async () => {
assert.strictEqual(results.exitCode, 0, 'Passed');
});

await it('Parallel + Unit + Options', async () => {
await it('Parallel (FILTER Env)', async () => {
const results = await inspectCLI(
'npx tsx src/bin/index.ts --platform=node --parallel --include=test/integration',
{
env: { ...process.env, FILTER: 'import' },
}
);

console.log(results.stdout);
console.log(results.stderr);

assert.strictEqual(results.exitCode, 0, 'Passed');
});

await it('Sequential + Options (Just Touch)', async () => {
const results = await inspectCLI(
isWindows
? 'npx tsx src/bin/index.ts --parallel --concurrency=4 --platform=node --fast-fail --debug --exclude=".bak" --kill-port=4000 --kill-range="4000-4001" --include=test/unit --filter=".test.|.spec."'
: 'npx tsx src/bin/index.ts --parallel --concurrency=4 --platform=node --fast-fail --debug --exclude=.bak --kill-port=4000 --kill-range=4000-4001 --include=test/unit --filter=.test.|.spec.'
? 'npx tsx src/bin/index.ts --concurrency=4 --platform=node --fast-fail --debug --exclude=".bak" --kill-port=4000 --kill-range="4000-4001" --include=test/integration/import.test.ts --filter=".test.|.spec."'
: 'npx tsx src/bin/index.ts --concurrency=4 --platform=node --fast-fail --debug --exclude=.bak --kill-port=4000 --kill-range=4000-4001 --include=test/integration/import.test.ts --filter=.test.|.spec.'
);

console.log(results.stdout);
console.log(results.stderr);

assert.strictEqual(results.exitCode, 0, 'Passed');
});

await it('Parallel + Options (Just Touch)', async () => {
const results = await inspectCLI(
isWindows
? 'npx tsx src/bin/index.ts --parallel --concurrency=4 --platform=node --fast-fail --debug --exclude=".bak" --kill-port=4000 --kill-range="4000-4001" --include=test/integration/import.test.ts --filter=".test.|.spec."'
: 'npx tsx src/bin/index.ts --parallel --concurrency=4 --platform=node --fast-fail --debug --exclude=.bak --kill-port=4000 --kill-range=4000-4001 --include=test/integration/import.test.ts --filter=.test.|.spec.'
);

console.log(results.stdout);
Expand All @@ -43,53 +68,55 @@ test(async () => {
});

await describe('API', async () => {
await it('Sequential (Unit)', async () => {
const exitCode = await poku(['test/unit'], {
await it('Sequential (Single Input)', async () => {
const exitCode = await poku('test/integration/import.test.ts', {
platform: 'node',
noExit: true,
});

assert.strictEqual(exitCode, 0, 'Passed');
});

await it('Sequential (File)', async () => {
await it('Parallel (Single Input)', async () => {
const exitCode = await poku('test/integration/import.test.ts', {
platform: 'node',
parallel: true,
noExit: true,
});

assert.strictEqual(exitCode, 0, 'Passed');
});

await it('Parallel (File)', async () => {
const exitCode = await poku('test/integration/import.test.ts', {
await it('Unit (Exclude as Regex)', async () => {
const exitCode = await poku('test/unit', {
platform: 'node',
parallel: true,
exclude: /watch|map-tests/,
noExit: true,
});

assert.strictEqual(exitCode, 0, 'Passed');
});

await it('Parallel (Unit)', async () => {
const exitCode = await poku(['test/unit'], {
await it('Unit (Exclude as Array of Regex)', async () => {
const exitCode = await poku('test/unit', {
platform: 'node',
parallel: true,
exclude: [/watch/, /map-tests/],
noExit: true,
});

assert.strictEqual(exitCode, 0, 'Passed');
});

await it('Parallel + All + Options', async () => {
await it('Parallel + Unit + Integration + E2E + Options', async () => {
const exitCode = await poku(
['test/unit', 'test/integration', 'test/e2e'],
{
platform: 'node',
parallel: true,
debug: true,
concurrency: 4,
exclude: [/import.test/],
filter: /\.(test|spec)\./,
failFast: true,
noExit: true,
Expand Down
34 changes: 23 additions & 11 deletions website/docs/documentation/poku/options/watch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,33 @@ npx poku --watch --watch-interval=1500

<hr />

:::tip

Quickly reset and rerun all tests by typing <kbd>rs</kbd> on the console, followed by <kbd>Enter</kbd>.

- This will clean and repopulate all the directories and files to be watched.
- It's not possible to use it while the tests are running.

:::

<hr />

:::info

If it's not possible to determine which test file depends on the changed source file, no test will be run.

:::

<hr />

:::note

Currently, the deep dependency search doesn't consider line-breaking imports, for example:
❌ Dynamic paths are not supported:

```ts
import {
moduleA,
moduleB,
moduleC,
moduleD,
moduleE,
moduleF,
} from './some.js';
```
const dynamicPath = './some-file.js';

- See [**#499**](https://github.com/wellwelwel/poku/issues/449).
await import(dynamicPath);
```

:::

0 comments on commit 8a27789

Please sign in to comment.