ava 3.15.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/entrypoints/cli.mjs +4 -0
  2. package/entrypoints/eslint-plugin-helper.cjs +109 -0
  3. package/entrypoints/main.cjs +2 -0
  4. package/entrypoints/main.mjs +1 -0
  5. package/entrypoints/plugin.cjs +2 -0
  6. package/entrypoints/plugin.mjs +4 -0
  7. package/index.d.ts +6 -816
  8. package/lib/api.js +108 -49
  9. package/lib/assert.js +255 -270
  10. package/lib/chalk.js +9 -14
  11. package/lib/cli.js +118 -112
  12. package/lib/code-excerpt.js +12 -17
  13. package/lib/concordance-options.js +29 -65
  14. package/lib/context-ref.js +3 -6
  15. package/lib/create-chain.js +32 -20
  16. package/lib/environment-variables.js +1 -4
  17. package/lib/eslint-plugin-helper-worker.js +73 -0
  18. package/lib/extensions.js +2 -2
  19. package/lib/fork.js +81 -84
  20. package/lib/glob-helpers.cjs +140 -0
  21. package/lib/globs.js +136 -163
  22. package/lib/{ipc-flow-control.js → ipc-flow-control.cjs} +1 -0
  23. package/lib/is-ci.js +4 -2
  24. package/lib/like-selector.js +7 -13
  25. package/lib/line-numbers.js +11 -18
  26. package/lib/load-config.js +56 -180
  27. package/lib/module-types.js +3 -7
  28. package/lib/node-arguments.js +4 -5
  29. package/lib/{now-and-timers.js → now-and-timers.cjs} +0 -0
  30. package/lib/parse-test-args.js +22 -11
  31. package/lib/pkg.cjs +2 -0
  32. package/lib/plugin-support/shared-worker-loader.js +45 -48
  33. package/lib/plugin-support/shared-workers.js +24 -46
  34. package/lib/provider-manager.js +20 -14
  35. package/lib/reporters/beautify-stack.js +6 -12
  36. package/lib/reporters/colors.js +40 -15
  37. package/lib/reporters/default.js +114 -364
  38. package/lib/reporters/format-serialized-error.js +7 -18
  39. package/lib/reporters/improper-usage-messages.js +8 -9
  40. package/lib/reporters/prefix-title.js +17 -15
  41. package/lib/reporters/tap.js +18 -25
  42. package/lib/run-status.js +29 -23
  43. package/lib/runner.js +157 -172
  44. package/lib/scheduler.js +53 -0
  45. package/lib/serialize-error.js +61 -64
  46. package/lib/snapshot-manager.js +271 -289
  47. package/lib/test.js +135 -291
  48. package/lib/watcher.js +69 -44
  49. package/lib/worker/base.js +208 -0
  50. package/lib/worker/channel.cjs +290 -0
  51. package/lib/worker/dependency-tracker.js +24 -23
  52. package/lib/worker/{ensure-forked.js → guard-environment.cjs} +5 -4
  53. package/lib/worker/line-numbers.js +58 -20
  54. package/lib/worker/main.cjs +12 -0
  55. package/lib/worker/{options.js → options.cjs} +0 -0
  56. package/lib/worker/{plugin.js → plugin.cjs} +30 -21
  57. package/lib/worker/state.cjs +5 -0
  58. package/lib/worker/utils.cjs +6 -0
  59. package/package.json +71 -68
  60. package/plugin.d.ts +51 -53
  61. package/readme.md +5 -13
  62. package/types/assertions.d.ts +327 -0
  63. package/types/subscribable.ts +6 -0
  64. package/types/test-fn.d.ts +231 -0
  65. package/types/try-fn.d.ts +58 -0
  66. package/cli.js +0 -11
  67. package/eslint-plugin-helper.js +0 -201
  68. package/index.js +0 -8
  69. package/lib/worker/ipc.js +0 -201
  70. package/lib/worker/main.js +0 -21
  71. package/lib/worker/subprocess.js +0 -266
  72. package/plugin.js +0 -9
package/lib/watcher.js CHANGED
@@ -1,18 +1,26 @@
1
- 'use strict';
2
- const nodePath = require('path');
3
- const debug = require('debug')('ava:watcher');
4
- const chokidar = require('chokidar');
5
- const diff = require('lodash/difference');
6
- const flatten = require('lodash/flatten');
7
- const chalk = require('./chalk').get();
8
- const {applyTestFileFilter, classify, getChokidarIgnorePatterns} = require('./globs');
9
- const {levels: providerLevels} = require('./provider-manager');
10
-
11
- function rethrowAsync(err) {
1
+ import nodePath from 'node:path';
2
+
3
+ import chokidar_ from 'chokidar';
4
+ import createDebug from 'debug';
5
+
6
+ import {chalk} from './chalk.js';
7
+ import {applyTestFileFilter, classify, getChokidarIgnorePatterns} from './globs.js';
8
+
9
+ let chokidar = chokidar_;
10
+ export function _testOnlyReplaceChokidar(replacement) {
11
+ chokidar = replacement;
12
+ }
13
+
14
+ let debug = createDebug('ava:watcher');
15
+ export function _testOnlyReplaceDebug(replacement) {
16
+ debug = replacement('ava:watcher');
17
+ }
18
+
19
+ function rethrowAsync(error) {
12
20
  // Don't swallow exceptions. Note that any
13
21
  // expected error should already have been logged
14
22
  setImmediate(() => {
15
- throw err;
23
+ throw error;
16
24
  });
17
25
  }
18
26
 
@@ -77,7 +85,7 @@ class TestDependency {
77
85
  }
78
86
  }
79
87
 
80
- class Watcher {
88
+ export default class Watcher {
81
89
  constructor({api, filter = [], globs, projectDir, providers, reporter}) {
82
90
  this.debouncer = new Debouncer(this);
83
91
 
@@ -88,7 +96,7 @@ class Watcher {
88
96
 
89
97
  const patternFilters = filter.map(({pattern}) => pattern);
90
98
 
91
- this.providers = providers.filter(({level}) => level >= providerLevels.pathRewrites);
99
+ this.providers = providers;
92
100
  this.run = (specificFiles = [], updateSnapshots = false) => {
93
101
  const clearLogOnNextRun = this.clearLogOnNextRun && this.runVector > 0;
94
102
  if (this.runVector > 0) {
@@ -104,12 +112,18 @@ class Watcher {
104
112
  if (runOnlyExclusive) {
105
113
  // The test files that previously contained exclusive tests are always
106
114
  // run, together with the remaining specific files.
107
- const remainingFiles = diff(specificFiles, exclusiveFiles);
108
- specificFiles = this.filesWithExclusiveTests.concat(remainingFiles);
115
+ const remainingFiles = specificFiles.filter(file => !exclusiveFiles.includes(file));
116
+ specificFiles = [...this.filesWithExclusiveTests, ...remainingFiles];
109
117
  }
110
118
 
111
119
  if (filter.length > 0) {
112
- specificFiles = applyTestFileFilter({cwd: projectDir, filter: patternFilters, testFiles: specificFiles});
120
+ specificFiles = applyTestFileFilter({
121
+ cwd: projectDir,
122
+ expandDirectories: false,
123
+ filter: patternFilters,
124
+ testFiles: specificFiles,
125
+ treatFilterPatternsAsFiles: false,
126
+ });
113
127
  }
114
128
 
115
129
  this.pruneFailures(specificFiles);
@@ -125,21 +139,21 @@ class Watcher {
125
139
  previousFailures: this.sumPreviousFailures(this.runVector),
126
140
  runOnlyExclusive,
127
141
  runVector: this.runVector,
128
- updateSnapshots: updateSnapshots === true
129
- }
142
+ updateSnapshots: updateSnapshots === true,
143
+ },
130
144
  })
131
- .then(runStatus => { // eslint-disable-line promise/prefer-await-to-then
145
+ .then(runStatus => {
132
146
  reporter.endRun();
133
147
  reporter.lineWriter.writeLine(END_MESSAGE);
134
148
 
135
149
  if (this.clearLogOnNextRun && (
136
- runStatus.stats.failedHooks > 0 ||
137
- runStatus.stats.failedTests > 0 ||
138
- runStatus.stats.failedWorkers > 0 ||
139
- runStatus.stats.internalErrors > 0 ||
140
- runStatus.stats.timeouts > 0 ||
141
- runStatus.stats.uncaughtExceptions > 0 ||
142
- runStatus.stats.unhandledRejections > 0
150
+ runStatus.stats.failedHooks > 0
151
+ || runStatus.stats.failedTests > 0
152
+ || runStatus.stats.failedWorkers > 0
153
+ || runStatus.stats.internalErrors > 0
154
+ || runStatus.stats.timeouts > 0
155
+ || runStatus.stats.uncaughtExceptions > 0
156
+ || runStatus.stats.unhandledRejections > 0
143
157
  )) {
144
158
  this.clearLogOnNextRun = false;
145
159
  }
@@ -150,6 +164,7 @@ class Watcher {
150
164
  this.testDependencies = [];
151
165
  this.trackTestDependencies(api);
152
166
 
167
+ this.temporaryFiles = new Set();
153
168
  this.touchedFiles = new Set();
154
169
  this.trackTouchedFiles(api);
155
170
 
@@ -168,7 +183,7 @@ class Watcher {
168
183
  chokidar.watch(['**/*'], {
169
184
  cwd: this.globs.cwd,
170
185
  ignored: getChokidarIgnorePatterns(this.globs),
171
- ignoreInitial: true
186
+ ignoreInitial: true,
172
187
  }).on('all', (event, path) => {
173
188
  if (event === 'add' || event === 'change' || event === 'unlink') {
174
189
  debug('Detected %s of %s', event, path);
@@ -231,9 +246,13 @@ class Watcher {
231
246
  return;
232
247
  }
233
248
 
234
- for (const file of evt.files) {
249
+ for (const file of evt.files.changedFiles) {
235
250
  this.touchedFiles.add(file);
236
251
  }
252
+
253
+ for (const file of evt.files.temporaryFiles) {
254
+ this.temporaryFiles.add(file);
255
+ }
237
256
  });
238
257
  });
239
258
  }
@@ -307,7 +326,7 @@ class Watcher {
307
326
  this.filesWithFailures.push({
308
327
  file,
309
328
  vector,
310
- count: 1
329
+ count: 1,
311
330
  });
312
331
  }
313
332
  }
@@ -379,6 +398,14 @@ class Watcher {
379
398
  return false;
380
399
  }
381
400
 
401
+ // Unlike touched files, temporary files are never cleared. We may see
402
+ // adds and unlinks detected separately, so we track the temporary files
403
+ // as long as AVA is running.
404
+ if (this.temporaryFiles.has(path)) {
405
+ debug('Ignoring known temporary file %s', path);
406
+ return false;
407
+ }
408
+
382
409
  return true;
383
410
  });
384
411
 
@@ -394,21 +421,23 @@ class Watcher {
394
421
  }
395
422
 
396
423
  const dirtyHelpersAndSources = [];
397
- const dirtyTests = [];
424
+ const addedOrChangedTests = [];
425
+ const unlinkedTests = [];
398
426
  for (const filePath of dirtyPaths) {
399
427
  const {isIgnoredByWatcher, isTest} = classify(filePath, this.globs);
400
428
  if (!isIgnoredByWatcher) {
401
429
  if (isTest) {
402
- dirtyTests.push(filePath);
430
+ if (dirtyStates[filePath] === 'unlink') {
431
+ unlinkedTests.push(filePath);
432
+ } else {
433
+ addedOrChangedTests.push(filePath);
434
+ }
403
435
  } else {
404
436
  dirtyHelpersAndSources.push(filePath);
405
437
  }
406
438
  }
407
439
  }
408
440
 
409
- const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink');
410
- const unlinkedTests = diff(dirtyTests, addedOrChangedTests);
411
-
412
441
  this.cleanUnlinkedTests(unlinkedTests);
413
442
 
414
443
  // No need to rerun tests if the only change is that tests were deleted
@@ -423,12 +452,10 @@ class Watcher {
423
452
  }
424
453
 
425
454
  // Try to find tests that depend on the changed source files
426
- const testsByHelpersOrSource = dirtyHelpersAndSources.map(path => {
427
- return this.testDependencies.filter(dep => dep.contains(path)).map(dep => {
428
- debug('%s is a dependency of %s', path, dep.file);
429
- return dep.file;
430
- });
431
- }, this).filter(tests => tests.length > 0);
455
+ const testsByHelpersOrSource = dirtyHelpersAndSources.map(path => this.testDependencies.filter(dep => dep.contains(path)).map(dep => {
456
+ debug('%s is a dependency of %s', path, dep.file);
457
+ return dep.file;
458
+ })).filter(tests => tests.length > 0);
432
459
 
433
460
  // Rerun all tests if source files were changed that could not be traced to
434
461
  // specific tests
@@ -440,8 +467,6 @@ class Watcher {
440
467
  }
441
468
 
442
469
  // Run all affected tests
443
- this.run([...new Set(addedOrChangedTests.concat(flatten(testsByHelpersOrSource)))]);
470
+ this.run([...new Set([addedOrChangedTests, testsByHelpersOrSource].flat(2))]);
444
471
  }
445
472
  }
446
-
447
- module.exports = Watcher;
@@ -0,0 +1,208 @@
1
+ import {createRequire} from 'node:module';
2
+ import process from 'node:process';
3
+ import {pathToFileURL} from 'node:url';
4
+ import {workerData} from 'node:worker_threads';
5
+
6
+ import setUpCurrentlyUnhandled from 'currently-unhandled';
7
+
8
+ import {set as setChalk} from '../chalk.js';
9
+ import nowAndTimers from '../now-and-timers.cjs';
10
+ import providerManager from '../provider-manager.js';
11
+ import Runner from '../runner.js';
12
+ import serializeError from '../serialize-error.js';
13
+
14
+ import channel from './channel.cjs';
15
+ import dependencyTracking from './dependency-tracker.js';
16
+ import lineNumberSelection from './line-numbers.js';
17
+ import {set as setOptions} from './options.cjs';
18
+ import {flags, refs, sharedWorkerTeardowns} from './state.cjs';
19
+ import {isRunningInThread, isRunningInChildProcess} from './utils.cjs';
20
+
21
+ const currentlyUnhandled = setUpCurrentlyUnhandled();
22
+
23
+ const run = async options => {
24
+ setOptions(options);
25
+ setChalk(options.chalkOptions);
26
+
27
+ if (options.chalkOptions.level > 0) {
28
+ const {stdout, stderr} = process;
29
+ global.console = Object.assign(global.console, new console.Console({stdout, stderr, colorMode: true}));
30
+ }
31
+
32
+ async function exit(code) {
33
+ if (!process.exitCode) {
34
+ process.exitCode = code;
35
+ }
36
+
37
+ dependencyTracking.flush();
38
+ await channel.flush();
39
+ process.exit(); // eslint-disable-line unicorn/no-process-exit
40
+ }
41
+
42
+ let checkSelectedByLineNumbers;
43
+ try {
44
+ checkSelectedByLineNumbers = lineNumberSelection({
45
+ file: options.file,
46
+ lineNumbers: options.lineNumbers,
47
+ });
48
+ } catch (error) {
49
+ channel.send({type: 'line-number-selection-error', err: serializeError('Line number selection error', false, error, options.file)});
50
+ checkSelectedByLineNumbers = () => false;
51
+ }
52
+
53
+ const runner = new Runner({
54
+ checkSelectedByLineNumbers,
55
+ experiments: options.experiments,
56
+ failFast: options.failFast,
57
+ failWithoutAssertions: options.failWithoutAssertions,
58
+ file: options.file,
59
+ match: options.match,
60
+ projectDir: options.projectDir,
61
+ recordNewSnapshots: options.recordNewSnapshots,
62
+ runOnlyExclusive: options.runOnlyExclusive,
63
+ serial: options.serial,
64
+ snapshotDir: options.snapshotDir,
65
+ updateSnapshots: options.updateSnapshots,
66
+ });
67
+
68
+ refs.runnerChain = runner.chain;
69
+
70
+ channel.peerFailed.then(() => {
71
+ runner.interrupt();
72
+ });
73
+
74
+ runner.on('dependency', dependencyTracking.track);
75
+ runner.on('stateChange', state => channel.send(state));
76
+
77
+ runner.on('error', error => {
78
+ channel.send({type: 'internal-error', err: serializeError('Internal runner error', false, error, runner.file)});
79
+ exit(1);
80
+ });
81
+
82
+ runner.on('finish', async () => {
83
+ try {
84
+ const {touchedFiles} = runner.saveSnapshotState();
85
+ if (touchedFiles) {
86
+ channel.send({type: 'touched-files', files: touchedFiles});
87
+ }
88
+ } catch (error) {
89
+ channel.send({type: 'internal-error', err: serializeError('Internal runner error', false, error, runner.file)});
90
+ exit(1);
91
+ return;
92
+ }
93
+
94
+ try {
95
+ await Promise.all(sharedWorkerTeardowns.map(fn => fn()));
96
+ } catch (error) {
97
+ channel.send({type: 'uncaught-exception', err: serializeError('Shared worker teardown error', false, error, runner.file)});
98
+ exit(1);
99
+ return;
100
+ }
101
+
102
+ nowAndTimers.setImmediate(() => {
103
+ for (const rejection of currentlyUnhandled()) {
104
+ channel.send({type: 'unhandled-rejection', err: serializeError('Unhandled rejection', true, rejection.reason, runner.file)});
105
+ }
106
+
107
+ exit(0);
108
+ });
109
+ });
110
+
111
+ process.on('uncaughtException', error => {
112
+ channel.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error, runner.file)});
113
+ exit(1);
114
+ });
115
+
116
+ // Store value to prevent required modules from modifying it.
117
+ const testPath = options.file;
118
+
119
+ const extensionsToLoadAsModules = Object.entries(options.moduleTypes)
120
+ .filter(([, type]) => type === 'module')
121
+ .map(([extension]) => extension);
122
+
123
+ // Install before processing options.require, so if helpers are added to the
124
+ // require configuration the *compiled* helper will be loaded.
125
+ const {projectDir, providerStates = []} = options;
126
+ const providers = [];
127
+ await Promise.all(providerStates.map(async ({type, state}) => {
128
+ if (type === 'typescript') {
129
+ const provider = await providerManager.typescript(projectDir);
130
+ providers.push(provider.worker({extensionsToLoadAsModules, state}));
131
+ }
132
+ }));
133
+
134
+ const require = createRequire(import.meta.url);
135
+ const load = async ref => {
136
+ for (const provider of providers) {
137
+ if (provider.canLoad(ref)) {
138
+ return provider.load(ref, {requireFn: require});
139
+ }
140
+ }
141
+
142
+ for (const extension of extensionsToLoadAsModules) {
143
+ if (ref.endsWith(`.${extension}`)) {
144
+ return import(pathToFileURL(ref)); // eslint-disable-line node/no-unsupported-features/es-syntax
145
+ }
146
+ }
147
+
148
+ // We still support require() since it's more easily monkey-patched.
149
+ return require(ref);
150
+ };
151
+
152
+ try {
153
+ for await (const ref of (options.require || [])) {
154
+ await load(ref);
155
+ }
156
+
157
+ // Install dependency tracker after the require configuration has been evaluated
158
+ // to make sure we also track dependencies with custom require hooks
159
+ dependencyTracking.install(require.extensions, testPath);
160
+
161
+ if (options.debug && options.debug.port !== undefined && options.debug.host !== undefined) {
162
+ // If an inspector was active when the main process started, and is
163
+ // already active for the worker process, do not open a new one.
164
+ const {default: inspector} = await import('node:inspector'); // eslint-disable-line node/no-unsupported-features/es-syntax
165
+ if (!options.debug.active || inspector.url() === undefined) {
166
+ inspector.open(options.debug.port, options.debug.host, true);
167
+ }
168
+
169
+ if (options.debug.break) {
170
+ debugger; // eslint-disable-line no-debugger
171
+ }
172
+ }
173
+
174
+ await load(testPath);
175
+
176
+ if (flags.loadedMain) {
177
+ // Unreference the channel if the test file required AVA. This stops it
178
+ // from keeping the event loop busy, which means the `beforeExit` event can be
179
+ // used to detect when tests stall.
180
+ channel.unref();
181
+ } else {
182
+ channel.send({type: 'missing-ava-import'});
183
+ exit(1);
184
+ }
185
+ } catch (error) {
186
+ channel.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error, runner.file)});
187
+ exit(1);
188
+ }
189
+ };
190
+
191
+ const onError = error => {
192
+ // There shouldn't be any errors, but if there are we may not have managed
193
+ // to bootstrap enough code to serialize them. Re-throw and let the process
194
+ // crash.
195
+ setImmediate(() => {
196
+ throw error;
197
+ });
198
+ };
199
+
200
+ if (isRunningInThread) {
201
+ channel.send({type: 'starting'}); // AVA won't terminate the worker thread until it's seen this message.
202
+ const {options} = workerData;
203
+ delete workerData.options; // Don't allow user code access.
204
+ run(options).catch(onError);
205
+ } else if (isRunningInChildProcess) {
206
+ channel.send({type: 'ready-for-options'});
207
+ channel.options.then(run).catch(onError);
208
+ }