runner.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. 'use strict';
  2. const path = require('path');
  3. const matcher = require('matcher');
  4. const ContextRef = require('./context-ref');
  5. const createChain = require('./create-chain');
  6. const Emittery = require('./emittery');
  7. const snapshotManager = require('./snapshot-manager');
  8. const serializeError = require('./serialize-error');
  9. const Runnable = require('./test');
  10. class Runner extends Emittery {
  11. constructor(options) {
  12. super();
  13. options = options || {};
  14. this.failFast = options.failFast === true;
  15. this.failWithoutAssertions = options.failWithoutAssertions !== false;
  16. this.file = options.file;
  17. this.match = options.match || [];
  18. this.projectDir = options.projectDir;
  19. this.runOnlyExclusive = options.runOnlyExclusive === true;
  20. this.serial = options.serial === true;
  21. this.snapshotDir = options.snapshotDir;
  22. this.updateSnapshots = options.updateSnapshots;
  23. this.activeRunnables = new Set();
  24. this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
  25. this.interrupted = false;
  26. this.snapshots = null;
  27. this.tasks = {
  28. after: [],
  29. afterAlways: [],
  30. afterEach: [],
  31. afterEachAlways: [],
  32. before: [],
  33. beforeEach: [],
  34. concurrent: [],
  35. serial: [],
  36. todo: []
  37. };
  38. const uniqueTestTitles = new Set();
  39. let hasStarted = false;
  40. let scheduledStart = false;
  41. this.chain = createChain((metadata, args) => { // eslint-disable-line complexity
  42. if (hasStarted) {
  43. throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.');
  44. }
  45. if (!scheduledStart) {
  46. scheduledStart = true;
  47. process.nextTick(() => {
  48. hasStarted = true;
  49. this.start();
  50. });
  51. }
  52. const specifiedTitle = typeof args[0] === 'string' ?
  53. args.shift() :
  54. '';
  55. const implementations = Array.isArray(args[0]) ?
  56. args.shift() :
  57. args.splice(0, 1);
  58. if (metadata.todo) {
  59. if (implementations.length > 0) {
  60. throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
  61. }
  62. if (specifiedTitle === '') {
  63. throw new TypeError('`todo` tests require a title');
  64. }
  65. if (uniqueTestTitles.has(specifiedTitle)) {
  66. throw new Error(`Duplicate test title: ${specifiedTitle}`);
  67. } else {
  68. uniqueTestTitles.add(specifiedTitle);
  69. }
  70. if (this.match.length > 0) {
  71. // --match selects TODO tests.
  72. if (matcher([specifiedTitle], this.match).length === 1) {
  73. metadata.exclusive = true;
  74. this.runOnlyExclusive = true;
  75. }
  76. }
  77. this.tasks.todo.push({title: specifiedTitle, metadata});
  78. this.emit('stateChange', {
  79. type: 'declared-test',
  80. title: specifiedTitle,
  81. knownFailing: false,
  82. todo: true
  83. });
  84. } else {
  85. if (implementations.length === 0) {
  86. throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.');
  87. }
  88. for (const implementation of implementations) {
  89. let title = implementation.title ?
  90. implementation.title.apply(implementation, [specifiedTitle].concat(args)) :
  91. specifiedTitle;
  92. if (typeof title !== 'string') {
  93. throw new TypeError('Test & hook titles must be strings');
  94. }
  95. if (title === '') {
  96. if (metadata.type === 'test') {
  97. throw new TypeError('Tests must have a title');
  98. } else if (metadata.always) {
  99. title = `${metadata.type}.always hook`;
  100. } else {
  101. title = `${metadata.type} hook`;
  102. }
  103. }
  104. if (metadata.type === 'test') {
  105. if (uniqueTestTitles.has(title)) {
  106. throw new Error(`Duplicate test title: ${title}`);
  107. } else {
  108. uniqueTestTitles.add(title);
  109. }
  110. }
  111. const task = {
  112. title,
  113. implementation,
  114. args,
  115. metadata: Object.assign({}, metadata)
  116. };
  117. if (metadata.type === 'test') {
  118. if (this.match.length > 0) {
  119. // --match overrides .only()
  120. task.metadata.exclusive = matcher([title], this.match).length === 1;
  121. }
  122. if (task.metadata.exclusive) {
  123. this.runOnlyExclusive = true;
  124. }
  125. this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
  126. this.emit('stateChange', {
  127. type: 'declared-test',
  128. title,
  129. knownFailing: metadata.failing,
  130. todo: false
  131. });
  132. } else if (!metadata.skipped) {
  133. this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task);
  134. }
  135. }
  136. }
  137. }, {
  138. serial: false,
  139. exclusive: false,
  140. skipped: false,
  141. todo: false,
  142. failing: false,
  143. callback: false,
  144. always: false
  145. });
  146. }
  147. compareTestSnapshot(options) {
  148. if (!this.snapshots) {
  149. this.snapshots = snapshotManager.load({
  150. file: this.file,
  151. fixedLocation: this.snapshotDir,
  152. name: path.basename(this.file),
  153. projectDir: this.projectDir,
  154. relFile: path.relative(this.projectDir, this.file),
  155. testDir: path.dirname(this.file),
  156. updating: this.updateSnapshots
  157. });
  158. this.emit('dependency', this.snapshots.snapPath);
  159. }
  160. return this.snapshots.compare(options);
  161. }
  162. saveSnapshotState() {
  163. if (this.snapshots) {
  164. return this.snapshots.save();
  165. }
  166. if (this.updateSnapshots) {
  167. // TODO: There may be unused snapshot files if no test caused the
  168. // snapshots to be loaded. Prune them. But not if tests (including hooks!)
  169. // were skipped. Perhaps emit a warning if this occurs?
  170. }
  171. return null;
  172. }
  173. onRun(runnable) {
  174. this.activeRunnables.add(runnable);
  175. }
  176. onRunComplete(runnable) {
  177. this.activeRunnables.delete(runnable);
  178. }
  179. attributeLeakedError(err) {
  180. for (const runnable of this.activeRunnables) {
  181. if (runnable.attributeLeakedError(err)) {
  182. return true;
  183. }
  184. }
  185. return false;
  186. }
  187. beforeExitHandler() {
  188. for (const runnable of this.activeRunnables) {
  189. runnable.finishDueToInactivity();
  190. }
  191. }
  192. runMultiple(runnables) {
  193. let allPassed = true;
  194. const storedResults = [];
  195. const runAndStoreResult = runnable => {
  196. return this.runSingle(runnable).then(result => {
  197. if (!result.passed) {
  198. allPassed = false;
  199. }
  200. storedResults.push(result);
  201. });
  202. };
  203. let waitForSerial = Promise.resolve();
  204. return runnables.reduce((prev, runnable) => {
  205. if (runnable.metadata.serial || this.serial) {
  206. waitForSerial = prev.then(() => {
  207. // Serial runnables run as long as there was no previous failure, unless
  208. // the runnable should always be run.
  209. return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable);
  210. });
  211. return waitForSerial;
  212. }
  213. return Promise.all([
  214. prev,
  215. waitForSerial.then(() => {
  216. // Concurrent runnables are kicked off after the previous serial
  217. // runnables have completed, as long as there was no previous failure
  218. // (or if the runnable should always be run). One concurrent runnable's
  219. // failure does not prevent the next runnable from running.
  220. return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable);
  221. })
  222. ]);
  223. }, waitForSerial).then(() => ({allPassed, storedResults}));
  224. }
  225. runSingle(runnable) {
  226. this.onRun(runnable);
  227. return runnable.run().then(result => {
  228. // If run() throws or rejects then the entire test run crashes, so
  229. // onRunComplete() doesn't *have* to be inside a finally().
  230. this.onRunComplete(runnable);
  231. return result;
  232. });
  233. }
  234. runHooks(tasks, contextRef, titleSuffix) {
  235. const hooks = tasks.map(task => new Runnable({
  236. contextRef,
  237. failWithoutAssertions: false,
  238. fn: task.args.length === 0 ?
  239. task.implementation :
  240. t => task.implementation.apply(null, [t].concat(task.args)),
  241. compareTestSnapshot: this.boundCompareTestSnapshot,
  242. updateSnapshots: this.updateSnapshots,
  243. metadata: task.metadata,
  244. title: `${task.title}${titleSuffix || ''}`
  245. }));
  246. return this.runMultiple(hooks, this.serial).then(outcome => {
  247. if (outcome.allPassed) {
  248. return true;
  249. }
  250. // Only emit results for failed hooks.
  251. for (const result of outcome.storedResults) {
  252. if (!result.passed) {
  253. this.emit('stateChange', {
  254. type: 'hook-failed',
  255. title: result.title,
  256. err: serializeError('Hook failure', true, result.error),
  257. duration: result.duration,
  258. logs: result.logs
  259. });
  260. }
  261. }
  262. return false;
  263. });
  264. }
  265. runTest(task, contextRef) {
  266. const hookSuffix = ` for ${task.title}`;
  267. return this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix).then(hooksOk => {
  268. // Don't run the test if a `beforeEach` hook failed.
  269. if (!hooksOk) {
  270. return false;
  271. }
  272. const test = new Runnable({
  273. contextRef,
  274. failWithoutAssertions: this.failWithoutAssertions,
  275. fn: task.args.length === 0 ?
  276. task.implementation :
  277. t => task.implementation.apply(null, [t].concat(task.args)),
  278. compareTestSnapshot: this.boundCompareTestSnapshot,
  279. updateSnapshots: this.updateSnapshots,
  280. metadata: task.metadata,
  281. title: task.title
  282. });
  283. return this.runSingle(test).then(result => {
  284. if (result.passed) {
  285. this.emit('stateChange', {
  286. type: 'test-passed',
  287. title: result.title,
  288. duration: result.duration,
  289. knownFailing: result.metadata.failing,
  290. logs: result.logs
  291. });
  292. return this.runHooks(this.tasks.afterEach, contextRef, hookSuffix);
  293. }
  294. this.emit('stateChange', {
  295. type: 'test-failed',
  296. title: result.title,
  297. err: serializeError('Test failure', true, result.error),
  298. duration: result.duration,
  299. knownFailing: result.metadata.failing,
  300. logs: result.logs
  301. });
  302. // Don't run `afterEach` hooks if the test failed.
  303. return false;
  304. });
  305. }).then(hooksAndTestOk => {
  306. return this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix).then(alwaysOk => {
  307. return hooksAndTestOk && alwaysOk;
  308. });
  309. });
  310. }
  311. start() {
  312. const concurrentTests = [];
  313. const serialTests = [];
  314. for (const task of this.tasks.serial) {
  315. if (this.runOnlyExclusive && !task.metadata.exclusive) {
  316. continue;
  317. }
  318. this.emit('stateChange', {
  319. type: 'selected-test',
  320. title: task.title,
  321. knownFailing: task.metadata.failing,
  322. skip: task.metadata.skipped,
  323. todo: false
  324. });
  325. if (!task.metadata.skipped) {
  326. serialTests.push(task);
  327. }
  328. }
  329. for (const task of this.tasks.concurrent) {
  330. if (this.runOnlyExclusive && !task.metadata.exclusive) {
  331. continue;
  332. }
  333. this.emit('stateChange', {
  334. type: 'selected-test',
  335. title: task.title,
  336. knownFailing: task.metadata.failing,
  337. skip: task.metadata.skipped,
  338. todo: false
  339. });
  340. if (!task.metadata.skipped) {
  341. if (this.serial) {
  342. serialTests.push(task);
  343. } else {
  344. concurrentTests.push(task);
  345. }
  346. }
  347. }
  348. for (const task of this.tasks.todo) {
  349. if (this.runOnlyExclusive && !task.metadata.exclusive) {
  350. continue;
  351. }
  352. this.emit('stateChange', {
  353. type: 'selected-test',
  354. title: task.title,
  355. knownFailing: false,
  356. skip: false,
  357. todo: true
  358. });
  359. }
  360. if (concurrentTests.length === 0 && serialTests.length === 0) {
  361. this.emit('finish');
  362. // Don't run any hooks if there are no tests to run.
  363. return;
  364. }
  365. const contextRef = new ContextRef();
  366. // Note that the hooks and tests always begin running asynchronously.
  367. const beforePromise = this.runHooks(this.tasks.before, contextRef);
  368. const serialPromise = beforePromise.then(beforeHooksOk => {
  369. // Don't run tests if a `before` hook failed.
  370. if (!beforeHooksOk) {
  371. return false;
  372. }
  373. return serialTests.reduce((prev, task) => {
  374. return prev.then(prevOk => {
  375. // Don't start tests after an interrupt.
  376. if (this.interrupted) {
  377. return prevOk;
  378. }
  379. // Prevent subsequent tests from running if `failFast` is enabled and
  380. // the previous test failed.
  381. if (!prevOk && this.failFast) {
  382. return false;
  383. }
  384. return this.runTest(task, contextRef.copy());
  385. });
  386. }, Promise.resolve(true));
  387. });
  388. const concurrentPromise = Promise.all([beforePromise, serialPromise]).then(prevOkays => {
  389. const beforeHooksOk = prevOkays[0];
  390. const serialOk = prevOkays[1];
  391. // Don't run tests if a `before` hook failed, or if `failFast` is enabled
  392. // and a previous serial test failed.
  393. if (!beforeHooksOk || (!serialOk && this.failFast)) {
  394. return false;
  395. }
  396. // Don't start tests after an interrupt.
  397. if (this.interrupted) {
  398. return true;
  399. }
  400. // If a concurrent test fails, even if `failFast` is enabled it won't
  401. // stop other concurrent tests from running.
  402. return Promise.all(concurrentTests.map(task => {
  403. return this.runTest(task, contextRef.copy());
  404. })).then(allOkays => allOkays.every(ok => ok));
  405. });
  406. const beforeExitHandler = this.beforeExitHandler.bind(this);
  407. process.on('beforeExit', beforeExitHandler);
  408. concurrentPromise
  409. // Only run `after` hooks if all hooks and tests passed.
  410. .then(ok => ok && this.runHooks(this.tasks.after, contextRef))
  411. // Always run `after.always` hooks.
  412. .then(() => this.runHooks(this.tasks.afterAlways, contextRef))
  413. .then(() => {
  414. process.removeListener('beforeExit', beforeExitHandler);
  415. })
  416. .then(() => this.emit('finish'), err => this.emit('error', err));
  417. }
  418. interrupt() {
  419. this.interrupted = true;
  420. }
  421. }
  422. module.exports = Runner;