aboutsummaryrefslogtreecommitdiff
path: root/scripts/cmd/test.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/cmd/test.js')
-rw-r--r--scripts/cmd/test.js89
1 files changed, 89 insertions, 0 deletions
diff --git a/scripts/cmd/test.js b/scripts/cmd/test.js
new file mode 100644
index 000000000..3b266ff1c
--- /dev/null
+++ b/scripts/cmd/test.js
@@ -0,0 +1,89 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import { run } from 'node:test';
+import { spec } from 'node:test/reporters';
+import { pathToFileURL } from 'node:url';
+import { parseArgs } from 'node:util';
+import glob from 'fast-glob';
+
+const isCI = !!process.env.CI;
+const defaultTimeout = isCI ? 1400000 : 600000;
+
+export default async function test() {
+ const args = parseArgs({
+ allowPositionals: true,
+ options: {
+ // aka --test-name-pattern: https://nodejs.org/api/test.html#filtering-tests-by-name
+ match: { type: 'string', alias: 'm' },
+ // aka --test-only: https://nodejs.org/api/test.html#only-tests
+ only: { type: 'boolean', alias: 'o' },
+ // aka --test-concurrency: https://nodejs.org/api/test.html#test-runner-execution-model
+ parallel: { type: 'boolean', alias: 'p' },
+ // experimental: https://nodejs.org/api/test.html#watch-mode
+ watch: { type: 'boolean', alias: 'w' },
+ // Test timeout in milliseconds (default: 30000ms)
+ timeout: { type: 'string', alias: 't' },
+ // Test setup file
+ setup: { type: 'string', alias: 's' },
+ // Test teardown file
+ teardown: { type: 'string' },
+ },
+ });
+
+ const pattern = args.positionals[1];
+ if (!pattern) throw new Error('Missing test glob pattern');
+
+ const files = await glob(pattern, {
+ filesOnly: true,
+ absolute: true,
+ ignore: ['**/node_modules/**'],
+ });
+
+ // For some reason, the `only` option does not work and we need to explicitly set the CLI flag instead.
+ // Node.js requires opt-in to run .only tests :(
+ // https://nodejs.org/api/test.html#only-tests
+ if (args.values.only) {
+ process.env.NODE_OPTIONS ??= '';
+ process.env.NODE_OPTIONS += ' --test-only';
+ }
+
+ if (!args.values.parallel) {
+ // If not parallel, we create a temporary file that imports all the test files
+ // so that it all runs in a single process.
+ const tempTestFile = path.resolve('./node_modules/.astro/test.mjs');
+ await fs.mkdir(path.dirname(tempTestFile), { recursive: true });
+ await fs.writeFile(
+ tempTestFile,
+ files.map((f) => `import ${JSON.stringify(pathToFileURL(f).toString())};`).join('\n'),
+ );
+
+ files.length = 0;
+ files.push(tempTestFile);
+ }
+
+ const teardownModule = args.values.teardown
+ ? await import(pathToFileURL(path.resolve(args.values.teardown)).toString())
+ : undefined;
+
+ // https://nodejs.org/api/test.html#runoptions
+ run({
+ files,
+ testNamePatterns: args.values.match,
+ concurrency: args.values.parallel,
+ only: args.values.only,
+ setup: args.values.setup,
+ watch: args.values.watch,
+ timeout: args.values.timeout ? Number(args.values.timeout) : defaultTimeout, // Node.js defaults to Infinity, so set better fallback
+ })
+ .on('test:fail', () => {
+ // For some reason, a test fail using the JS API does not set an exit code of 1,
+ // so we set it here manually
+ process.exitCode = 1;
+ })
+ .on('end', () => {
+ const testPassed = process.exitCode === 0 || process.exitCode === undefined;
+ teardownModule?.default(testPassed);
+ })
+ .pipe(new spec())
+ .pipe(process.stdout);
+}