UNPKG

7.52 kBJavaScriptView Raw
1'use strict';
2const {pathToFileURL} = require('url');
3const currentlyUnhandled = require('currently-unhandled')();
4
5require('./ensure-forked'); // eslint-disable-line import/no-unassigned-import
6
7const ipc = require('./ipc');
8
9const supportsESM = async () => {
10 try {
11 await import('data:text/javascript,'); // eslint-disable-line node/no-unsupported-features/es-syntax
12 return true;
13 } catch {}
14
15 return false;
16};
17
18ipc.send({type: 'ready-for-options'});
19ipc.options.then(async options => {
20 require('./options').set(options);
21 require('../chalk').set(options.chalkOptions);
22
23 if (options.chalkOptions.level > 0) {
24 const {stdout, stderr} = process;
25 global.console = Object.assign(global.console, new console.Console({stdout, stderr, colorMode: true}));
26 }
27
28 const nowAndTimers = require('../now-and-timers');
29 const providerManager = require('../provider-manager');
30 const Runner = require('../runner');
31 const serializeError = require('../serialize-error');
32 const dependencyTracking = require('./dependency-tracker');
33 const lineNumberSelection = require('./line-numbers');
34
35 async function exit(code) {
36 if (!process.exitCode) {
37 process.exitCode = code;
38 }
39
40 dependencyTracking.flush();
41 await ipc.flush();
42 process.exit(); // eslint-disable-line unicorn/no-process-exit
43 }
44
45 // TODO: Initialize providers here, then pass to lineNumberSelection() so they
46 // can be used to parse the test file.
47 let checkSelectedByLineNumbers;
48 try {
49 checkSelectedByLineNumbers = lineNumberSelection({
50 file: options.file,
51 lineNumbers: options.lineNumbers
52 });
53 } catch (error) {
54 ipc.send({type: 'line-number-selection-error', err: serializeError('Line number selection error', false, error, options.file)});
55 checkSelectedByLineNumbers = () => false;
56 }
57
58 const runner = new Runner({
59 checkSelectedByLineNumbers,
60 experiments: options.experiments,
61 failFast: options.failFast,
62 failWithoutAssertions: options.failWithoutAssertions,
63 file: options.file,
64 match: options.match,
65 projectDir: options.projectDir,
66 recordNewSnapshots: options.recordNewSnapshots,
67 runOnlyExclusive: options.runOnlyExclusive,
68 serial: options.serial,
69 snapshotDir: options.snapshotDir,
70 updateSnapshots: options.updateSnapshots
71 });
72
73 ipc.peerFailed.then(() => { // eslint-disable-line promise/prefer-await-to-then
74 runner.interrupt();
75 });
76
77 const attributedRejections = new Set();
78 process.on('unhandledRejection', (reason, promise) => {
79 if (runner.attributeLeakedError(reason)) {
80 attributedRejections.add(promise);
81 }
82 });
83
84 runner.on('dependency', dependencyTracking.track);
85 runner.on('stateChange', state => ipc.send(state));
86
87 runner.on('error', error => {
88 ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, error, runner.file)});
89 exit(1);
90 });
91
92 runner.on('finish', () => {
93 try {
94 const {cannotSave, touchedFiles} = runner.saveSnapshotState();
95 if (cannotSave) {
96 ipc.send({type: 'snapshot-error'});
97 } else if (touchedFiles) {
98 ipc.send({type: 'touched-files', files: touchedFiles});
99 }
100 } catch (error) {
101 ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, error, runner.file)});
102 exit(1);
103 return;
104 }
105
106 nowAndTimers.setImmediate(() => {
107 currentlyUnhandled()
108 .filter(rejection => !attributedRejections.has(rejection.promise))
109 .forEach(rejection => {
110 ipc.send({type: 'unhandled-rejection', err: serializeError('Unhandled rejection', true, rejection.reason, runner.file)});
111 });
112
113 exit(0);
114 });
115 });
116
117 process.on('uncaughtException', error => {
118 if (runner.attributeLeakedError(error)) {
119 return;
120 }
121
122 ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error, runner.file)});
123 exit(1);
124 });
125
126 let accessedRunner = false;
127 exports.getRunner = () => {
128 accessedRunner = true;
129 return runner;
130 };
131
132 // Store value to prevent required modules from modifying it.
133 const testPath = options.file;
134
135 // Install basic source map support.
136 const sourceMapSupport = require('source-map-support');
137 sourceMapSupport.install({
138 environment: 'node',
139 handleUncaughtExceptions: false
140 });
141
142 const extensionsToLoadAsModules = Object.entries(options.moduleTypes)
143 .filter(([, type]) => type === 'module')
144 .map(([extension]) => extension);
145
146 // Install before processing options.require, so if helpers are added to the
147 // require configuration the *compiled* helper will be loaded.
148 const {projectDir, providerStates = []} = options;
149 const providers = providerStates.map(({type, state}) => {
150 if (type === 'babel') {
151 const provider = providerManager.babel(projectDir).worker({extensionsToLoadAsModules, state});
152 runner.powerAssert = provider.powerAssert;
153 return provider;
154 }
155
156 if (type === 'typescript') {
157 return providerManager.typescript(projectDir).worker({extensionsToLoadAsModules, state});
158 }
159
160 return null;
161 }).filter(provider => provider !== null);
162
163 let requireFn = require;
164 let isESMSupported;
165 const load = async ref => {
166 for (const extension of extensionsToLoadAsModules) {
167 if (ref.endsWith(`.${extension}`)) {
168 if (typeof isESMSupported !== 'boolean') {
169 // Lazily determine support since this prints an experimental warning.
170 // eslint-disable-next-line no-await-in-loop
171 isESMSupported = await supportsESM();
172 }
173
174 if (isESMSupported) {
175 return import(pathToFileURL(ref)); // eslint-disable-line node/no-unsupported-features/es-syntax
176 }
177
178 ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, new Error('ECMAScript Modules are not supported in this Node.js version.'))});
179 exit(1);
180 return;
181 }
182 }
183
184 for (const provider of providers) {
185 if (provider.canLoad(ref)) {
186 return provider.load(ref, {requireFn});
187 }
188 }
189
190 return requireFn(ref);
191 };
192
193 try {
194 for await (const ref of (options.require || [])) {
195 const mod = await load(ref);
196
197 try {
198 if (Reflect.has(mod, Symbol.for('esm:package'))) {
199 requireFn = mod(module);
200 }
201 } catch {}
202 }
203
204 // Install dependency tracker after the require configuration has been evaluated
205 // to make sure we also track dependencies with custom require hooks
206 dependencyTracking.install(testPath);
207
208 if (options.debug && options.debug.port !== undefined && options.debug.host !== undefined) {
209 // If an inspector was active when the main process started, and is
210 // already active for the worker process, do not open a new one.
211 const inspector = require('inspector'); // eslint-disable-line node/no-unsupported-features/node-builtins
212 if (!options.debug.active || inspector.url() === undefined) {
213 inspector.open(options.debug.port, options.debug.host, true);
214 }
215
216 if (options.debug.break) {
217 debugger; // eslint-disable-line no-debugger
218 }
219 }
220
221 await load(testPath);
222
223 if (accessedRunner) {
224 // Unreference the IPC channel if the test file required AVA. This stops it
225 // from keeping the event loop busy, which means the `beforeExit` event can be
226 // used to detect when tests stall.
227 ipc.unref();
228 } else {
229 ipc.send({type: 'missing-ava-import'});
230 exit(1);
231 }
232 } catch (error) {
233 ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error, runner.file)});
234 exit(1);
235 }
236}).catch(error => {
237 // There shouldn't be any errors, but if there are we may not have managed
238 // to bootstrap enough code to serialize them. Re-throw and let the process
239 // crash.
240 setImmediate(() => {
241 throw error;
242 });
243});