test.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. 'use strict';
  2. const debug = require('debug')('egg-bin');
  3. const fs = require('fs');
  4. const path = require('path');
  5. const globby = require('globby');
  6. const Command = require('../command');
  7. const changed = require('jest-changed-files');
  8. class TestCommand extends Command {
  9. constructor(rawArgv) {
  10. super(rawArgv);
  11. this.usage = 'Usage: egg-bin test [files] [options]';
  12. this.options = {
  13. require: {
  14. description: 'require the given module',
  15. alias: 'r',
  16. type: 'array',
  17. },
  18. grep: {
  19. description: 'only run tests matching <pattern>',
  20. alias: 'g',
  21. type: 'array',
  22. },
  23. timeout: {
  24. description: 'set test-case timeout in milliseconds',
  25. alias: 't',
  26. type: 'number',
  27. },
  28. 'full-trace': {
  29. description: 'display the full stack trace',
  30. },
  31. changed: {
  32. description: 'only test with changed files and match ${cwd}/test/**/*.test.(js|ts)',
  33. alias: 'c',
  34. },
  35. 'dry-run': {
  36. type: 'boolean',
  37. description: 'whether show test command, no test will be executed',
  38. alias: 'd',
  39. },
  40. espower: {
  41. type: 'boolean',
  42. description: 'whether require intelli-espower-loader(js) or espower-typescript(ts) for power-assert',
  43. default: true,
  44. alias: 'e',
  45. },
  46. };
  47. }
  48. get description() {
  49. return 'Run test with mocha';
  50. }
  51. * run(context) {
  52. const opt = {
  53. env: Object.assign({
  54. NODE_ENV: 'test',
  55. }, context.env),
  56. execArgv: context.execArgv,
  57. };
  58. const mochaFile = require.resolve('mocha/bin/_mocha');
  59. const testArgs = yield this.formatTestArgs(context);
  60. if (!testArgs) return;
  61. if (context.argv['dry-run']) {
  62. debug('test with dry-run');
  63. console.log(mochaFile);
  64. console.log(testArgs.join('\n'));
  65. return;
  66. }
  67. debug('run test: %s %s', mochaFile, testArgs.join(' '));
  68. yield this.helper.forkNode(mochaFile, testArgs, opt);
  69. }
  70. /**
  71. * format test args then change it to array style
  72. * @param {Object} context - { cwd, argv, ...}
  73. * @return {Array} [ '--require=xxx', 'xx.test.js' ]
  74. * @protected
  75. */
  76. * formatTestArgs({ argv, debugOptions }) {
  77. const testArgv = Object.assign({}, argv);
  78. /* istanbul ignore next */
  79. testArgv.timeout = testArgv.timeout || process.env.TEST_TIMEOUT || 60000;
  80. testArgv.reporter = testArgv.reporter || process.env.TEST_REPORTER;
  81. // force exit
  82. testArgv.exit = true;
  83. // whether is debug mode, if pass --inspect then `debugOptions` is valid
  84. // others like WebStorm 2019 will pass NODE_OPTIONS, and egg-bin itself will be debug, so could detect `process.env.JB_DEBUG_FILE`.
  85. if (debugOptions || process.env.JB_DEBUG_FILE) {
  86. // --no-timeout
  87. testArgv.timeout = false;
  88. }
  89. // collect require
  90. let requireArr = testArgv.require || testArgv.r || [];
  91. /* istanbul ignore next */
  92. if (!Array.isArray(requireArr)) requireArr = [ requireArr ];
  93. // clean mocha stack, inspired by https://github.com/rstacruz/mocha-clean
  94. // [mocha built-in](https://github.com/mochajs/mocha/blob/master/lib/utils.js#L738) don't work with `[npminstall](https://github.com/cnpm/npminstall)`, so we will override it.
  95. if (!testArgv.fullTrace) requireArr.unshift(require.resolve('../mocha-clean'));
  96. requireArr.push(require.resolve('co-mocha'));
  97. if (requireArr.includes('intelli-espower-loader')) {
  98. console.warn('[egg-bin] don\'t need to manually require `intelli-espower-loader` anymore');
  99. } else if (testArgv.espower) {
  100. if (testArgv.typescript) {
  101. // espower-typescript must append after ts-node
  102. requireArr.push(testArgv.tscompiler, require.resolve('../espower-typescript'));
  103. } else {
  104. requireArr.push(require.resolve('intelli-espower-loader'));
  105. }
  106. }
  107. testArgv.require = requireArr;
  108. let pattern;
  109. // changed
  110. if (testArgv.changed) {
  111. pattern = yield this._getChangedTestFiles();
  112. if (!pattern.length) {
  113. console.log('No changed test files');
  114. return;
  115. }
  116. }
  117. if (!pattern) {
  118. // specific test files
  119. pattern = testArgv._.slice();
  120. }
  121. if (!pattern.length && process.env.TESTS) {
  122. pattern = process.env.TESTS.split(',');
  123. }
  124. // collect test files
  125. if (!pattern.length) {
  126. pattern = [ `test/**/*.test.${testArgv.typescript ? 'ts' : 'js'}` ];
  127. }
  128. pattern = pattern.concat([ '!test/fixtures', '!test/node_modules' ]);
  129. // expand glob and skip node_modules and fixtures
  130. const files = globby.sync(pattern);
  131. files.sort();
  132. if (files.length === 0) {
  133. console.log(`No test files found with ${pattern}`);
  134. return;
  135. }
  136. // auto add setup file as the first test file
  137. const setupFile = path.join(process.cwd(), `test/.setup.${testArgv.typescript ? 'ts' : 'js'}`);
  138. if (fs.existsSync(setupFile)) {
  139. files.unshift(setupFile);
  140. }
  141. testArgv._ = files;
  142. // remove alias
  143. testArgv.$0 = undefined;
  144. testArgv.r = undefined;
  145. testArgv.t = undefined;
  146. testArgv.g = undefined;
  147. testArgv.e = undefined;
  148. testArgv.typescript = undefined;
  149. testArgv['dry-run'] = undefined;
  150. testArgv.dryRun = undefined;
  151. return this.helper.unparseArgv(testArgv);
  152. }
  153. * _getChangedTestFiles() {
  154. const cwd = process.cwd();
  155. const res = yield changed.getChangedFilesForRoots([ cwd ]);
  156. const changedFiles = res.changedFiles;
  157. const files = [];
  158. for (const file of changedFiles) {
  159. // only find ${cwd}/test/**/*.test.(js|ts)
  160. if (file.startsWith(path.join(cwd, 'test')) && file.match(/\.test\.(js|ts)$/)) {
  161. files.push(file);
  162. }
  163. }
  164. return files;
  165. }
  166. }
  167. module.exports = TestCommand;