api.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. 'use strict';
  2. const path = require('path');
  3. const fs = require('fs');
  4. const os = require('os');
  5. const commonPathPrefix = require('common-path-prefix');
  6. const escapeStringRegexp = require('escape-string-regexp');
  7. const uniqueTempDir = require('unique-temp-dir');
  8. const isCi = require('is-ci');
  9. const resolveCwd = require('resolve-cwd');
  10. const debounce = require('lodash.debounce');
  11. const Bluebird = require('bluebird');
  12. const getPort = require('get-port');
  13. const arrify = require('arrify');
  14. const makeDir = require('make-dir');
  15. const ms = require('ms');
  16. const babelPipeline = require('./lib/babel-pipeline');
  17. const Emittery = require('./lib/emittery');
  18. const RunStatus = require('./lib/run-status');
  19. const AvaFiles = require('./lib/ava-files');
  20. const fork = require('./lib/fork');
  21. const serializeError = require('./lib/serialize-error');
  22. function resolveModules(modules) {
  23. return arrify(modules).map(name => {
  24. const modulePath = resolveCwd.silent(name);
  25. if (modulePath === null) {
  26. throw new Error(`Could not resolve required module '${name}'`);
  27. }
  28. return modulePath;
  29. });
  30. }
  31. class Api extends Emittery {
  32. constructor(options) {
  33. super();
  34. this.options = Object.assign({match: []}, options);
  35. this.options.require = resolveModules(this.options.require);
  36. this._allExtensions = this.options.extensions.all;
  37. this._regexpFullExtensions = new RegExp(`\\.(${this.options.extensions.full.map(ext => escapeStringRegexp(ext)).join('|')})$`);
  38. this._precompiler = null;
  39. }
  40. run(files, runtimeOptions) {
  41. const apiOptions = this.options;
  42. runtimeOptions = runtimeOptions || {};
  43. // Each run will have its own status. It can only be created when test files
  44. // have been found.
  45. let runStatus;
  46. // Irrespectively, perform some setup now, before finding test files.
  47. // Track active forks and manage timeouts.
  48. const failFast = apiOptions.failFast === true;
  49. let bailed = false;
  50. const pendingWorkers = new Set();
  51. const timedOutWorkerFiles = new Set();
  52. let restartTimer;
  53. if (apiOptions.timeout) {
  54. const timeout = ms(apiOptions.timeout);
  55. restartTimer = debounce(() => {
  56. // If failFast is active, prevent new test files from running after
  57. // the current ones are exited.
  58. if (failFast) {
  59. bailed = true;
  60. }
  61. for (const worker of pendingWorkers) {
  62. timedOutWorkerFiles.add(worker.file);
  63. worker.exit();
  64. }
  65. runStatus.emitStateChange({type: 'timeout', period: timeout});
  66. }, timeout);
  67. } else {
  68. restartTimer = Object.assign(() => {}, {cancel() {}});
  69. }
  70. // Find all test files.
  71. return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files, extensions: this._allExtensions}).findTestFiles()
  72. .then(files => {
  73. runStatus = new RunStatus(files.length);
  74. const emittedRun = this.emit('run', {
  75. clearLogOnNextRun: runtimeOptions.clearLogOnNextRun === true,
  76. failFastEnabled: failFast,
  77. filePathPrefix: commonPathPrefix(files),
  78. files,
  79. matching: apiOptions.match.length > 0,
  80. previousFailures: runtimeOptions.previousFailures || 0,
  81. runOnlyExclusive: runtimeOptions.runOnlyExclusive === true,
  82. runVector: runtimeOptions.runVector || 0,
  83. status: runStatus
  84. });
  85. // Bail out early if no files were found.
  86. if (files.length === 0) {
  87. return emittedRun.then(() => {
  88. return runStatus;
  89. });
  90. }
  91. runStatus.on('stateChange', record => {
  92. if (record.testFile && !timedOutWorkerFiles.has(record.testFile)) {
  93. // Restart the timer whenever there is activity from workers that
  94. // haven't already timed out.
  95. restartTimer();
  96. }
  97. if (failFast && (record.type === 'hook-failed' || record.type === 'test-failed' || record.type === 'worker-failed')) {
  98. // Prevent new test files from running once a test has failed.
  99. bailed = true;
  100. // Try to stop currently scheduled tests.
  101. for (const worker of pendingWorkers) {
  102. worker.notifyOfPeerFailure();
  103. }
  104. }
  105. });
  106. return emittedRun
  107. .then(() => this._setupPrecompiler())
  108. .then(precompilation => {
  109. if (!precompilation.enabled) {
  110. return null;
  111. }
  112. // Compile all test and helper files. Assumes the tests only load
  113. // helpers from within the `resolveTestsFrom` directory. Without
  114. // arguments this is the `projectDir`, else it's `process.cwd()`
  115. // which may be nested too deeply.
  116. return new AvaFiles({cwd: this.options.resolveTestsFrom, extensions: this._allExtensions})
  117. .findTestHelpers().then(helpers => {
  118. return {
  119. cacheDir: precompilation.cacheDir,
  120. map: [...files, ...helpers].reduce((acc, file) => {
  121. try {
  122. const realpath = fs.realpathSync(file);
  123. const filename = path.basename(realpath);
  124. const cachePath = this._regexpFullExtensions.test(filename) ?
  125. precompilation.precompileFull(realpath) :
  126. precompilation.precompileEnhancementsOnly(realpath);
  127. if (cachePath) {
  128. acc[realpath] = cachePath;
  129. }
  130. } catch (err) {
  131. throw Object.assign(err, {file});
  132. }
  133. return acc;
  134. }, {})
  135. };
  136. });
  137. })
  138. .then(precompilation => {
  139. // Resolve the correct concurrency value.
  140. let concurrency = Math.min(os.cpus().length, isCi ? 2 : Infinity);
  141. if (apiOptions.concurrency > 0) {
  142. concurrency = apiOptions.concurrency;
  143. }
  144. if (apiOptions.serial) {
  145. concurrency = 1;
  146. }
  147. // Try and run each file, limited by `concurrency`.
  148. return Bluebird.map(files, file => {
  149. // No new files should be run once a test has timed out or failed,
  150. // and failFast is enabled.
  151. if (bailed) {
  152. return;
  153. }
  154. return this._computeForkExecArgv().then(execArgv => {
  155. const options = Object.assign({}, apiOptions, {
  156. // If we're looking for matches, run every single test process in exclusive-only mode
  157. runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true
  158. });
  159. if (precompilation) {
  160. options.cacheDir = precompilation.cacheDir;
  161. options.precompiled = precompilation.map;
  162. } else {
  163. options.precompiled = {};
  164. }
  165. if (runtimeOptions.updateSnapshots) {
  166. // Don't use in Object.assign() since it'll override options.updateSnapshots even when false.
  167. options.updateSnapshots = true;
  168. }
  169. const worker = fork(file, options, execArgv);
  170. runStatus.observeWorker(worker, file);
  171. pendingWorkers.add(worker);
  172. worker.promise.then(() => { // eslint-disable-line max-nested-callbacks
  173. pendingWorkers.delete(worker);
  174. });
  175. restartTimer();
  176. return worker.promise;
  177. });
  178. }, {concurrency});
  179. })
  180. .catch(err => {
  181. runStatus.emitStateChange({type: 'internal-error', err: serializeError('Internal error', false, err)});
  182. })
  183. .then(() => {
  184. restartTimer.cancel();
  185. return runStatus;
  186. });
  187. });
  188. }
  189. _setupPrecompiler() {
  190. if (this._precompiler) {
  191. return this._precompiler;
  192. }
  193. const cacheDir = this.options.cacheEnabled === false ?
  194. uniqueTempDir() :
  195. path.join(this.options.projectDir, 'node_modules', '.cache', 'ava');
  196. // Ensure cacheDir exists
  197. makeDir.sync(cacheDir);
  198. const {projectDir, babelConfig} = this.options;
  199. const compileEnhancements = this.options.compileEnhancements !== false;
  200. const precompileFull = babelConfig ?
  201. babelPipeline.build(projectDir, cacheDir, babelConfig, compileEnhancements) :
  202. filename => {
  203. throw new Error(`Cannot apply full precompilation, possible bad usage: ${filename}`);
  204. };
  205. const precompileEnhancementsOnly = compileEnhancements && this.options.extensions.enhancementsOnly.length > 0 ?
  206. babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) :
  207. filename => {
  208. throw new Error(`Cannot apply enhancement-only precompilation, possible bad usage: ${filename}`);
  209. };
  210. this._precompiler = {
  211. cacheDir,
  212. enabled: babelConfig || compileEnhancements,
  213. precompileEnhancementsOnly,
  214. precompileFull
  215. };
  216. return this._precompiler;
  217. }
  218. _computeForkExecArgv() {
  219. const execArgv = this.options.testOnlyExecArgv || process.execArgv;
  220. if (execArgv.length === 0) {
  221. return Promise.resolve(execArgv);
  222. }
  223. let debugArgIndex = -1;
  224. // --inspect-brk is used in addition to --inspect to break on first line and wait
  225. execArgv.some((arg, index) => {
  226. const isDebugArg = /^--inspect(-brk)?($|=)/.test(arg);
  227. if (isDebugArg) {
  228. debugArgIndex = index;
  229. }
  230. return isDebugArg;
  231. });
  232. const isInspect = debugArgIndex >= 0;
  233. if (!isInspect) {
  234. execArgv.some((arg, index) => {
  235. const isDebugArg = /^--debug(-brk)?($|=)/.test(arg);
  236. if (isDebugArg) {
  237. debugArgIndex = index;
  238. }
  239. return isDebugArg;
  240. });
  241. }
  242. if (debugArgIndex === -1) {
  243. return Promise.resolve(execArgv);
  244. }
  245. return getPort().then(port => {
  246. const forkExecArgv = execArgv.slice();
  247. let flagName = isInspect ? '--inspect' : '--debug';
  248. const oldValue = forkExecArgv[debugArgIndex];
  249. if (oldValue.indexOf('brk') > 0) {
  250. flagName += '-brk';
  251. }
  252. forkExecArgv[debugArgIndex] = `${flagName}=${port}`;
  253. return forkExecArgv;
  254. });
  255. }
  256. }
  257. module.exports = Api;