util.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. 'use strict';
  2. var _templateObject = _taggedTemplateLiteral(['\n\n ', ' Warning: Cypress failed to start.\n\n This is likely due to a misconfigured DISPLAY environment variable.\n\n DISPLAY was set to: "', '"\n\n Cypress will attempt to fix the problem and rerun.\n '], ['\n\n ', ' Warning: Cypress failed to start.\n\n This is likely due to a misconfigured DISPLAY environment variable.\n\n DISPLAY was set to: "', '"\n\n Cypress will attempt to fix the problem and rerun.\n ']);
  3. function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
  4. function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
  5. var _ = require('lodash');
  6. var R = require('ramda');
  7. var os = require('os');
  8. var crypto = require('crypto');
  9. var la = require('lazy-ass');
  10. var is = require('check-more-types');
  11. var tty = require('tty');
  12. var path = require('path');
  13. var _isCi = require('is-ci');
  14. var execa = require('execa');
  15. var getos = require('getos');
  16. var chalk = require('chalk');
  17. var Promise = require('bluebird');
  18. var cachedir = require('cachedir');
  19. var logSymbols = require('log-symbols');
  20. var executable = require('executable');
  21. var _require = require('common-tags'),
  22. stripIndent = _require.stripIndent;
  23. var _supportsColor = require('supports-color');
  24. var _isInstalledGlobally = require('is-installed-globally');
  25. var pkg = require(path.join(__dirname, '..', 'package.json'));
  26. var logger = require('./logger');
  27. var debug = require('debug')('cypress:cli');
  28. var fs = require('./fs');
  29. var issuesUrl = 'https://github.com/cypress-io/cypress/issues';
  30. var getosAsync = Promise.promisify(getos);
  31. /**
  32. * Returns SHA512 of a file
  33. *
  34. * Implementation lifted from https://github.com/sindresorhus/hasha
  35. * but without bringing that dependency (since hasha is Node v8+)
  36. */
  37. var getFileChecksum = function getFileChecksum(filename) {
  38. la(is.unemptyString(filename), 'expected filename', filename);
  39. var hashStream = function hashStream() {
  40. var s = crypto.createHash('sha512');
  41. s.setEncoding('hex');
  42. return s;
  43. };
  44. return new Promise(function (resolve, reject) {
  45. var stream = fs.createReadStream(filename);
  46. stream.on('error', reject).pipe(hashStream()).on('error', reject).on('finish', function () {
  47. resolve(this.read());
  48. });
  49. });
  50. };
  51. var getFileSize = function getFileSize(filename) {
  52. la(is.unemptyString(filename), 'expected filename', filename);
  53. return fs.statAsync(filename).get('size');
  54. };
  55. var isBrokenGtkDisplayRe = /Gtk: cannot open display/;
  56. var stringify = function stringify(val) {
  57. return _.isObject(val) ? JSON.stringify(val) : val;
  58. };
  59. function normalizeModuleOptions() {
  60. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  61. return _.mapValues(options, stringify);
  62. }
  63. /**
  64. * Returns true if the platform is Linux. We do a lot of different
  65. * stuff on Linux (like Xvfb) and it helps to has readable code
  66. */
  67. var isLinux = function isLinux() {
  68. return os.platform() === 'linux';
  69. };
  70. /**
  71. * If the DISPLAY variable is set incorrectly, when trying to spawn
  72. * Cypress executable we get an error like this:
  73. ```
  74. [1005:0509/184205.663837:WARNING:browser_main_loop.cc(258)] Gtk: cannot open display: 99
  75. ```
  76. */
  77. var isBrokenGtkDisplay = function isBrokenGtkDisplay(str) {
  78. return isBrokenGtkDisplayRe.test(str);
  79. };
  80. var isPossibleLinuxWithIncorrectDisplay = function isPossibleLinuxWithIncorrectDisplay() {
  81. return isLinux() && process.env.DISPLAY;
  82. };
  83. var logBrokenGtkDisplayWarning = function logBrokenGtkDisplayWarning() {
  84. debug('Cypress exited due to a broken gtk display because of a potential invalid DISPLAY env... retrying after starting Xvfb');
  85. // if we get this error, we are on Linux and DISPLAY is set
  86. logger.warn(stripIndent(_templateObject, logSymbols.warning, process.env.DISPLAY));
  87. logger.warn();
  88. };
  89. function stdoutLineMatches(expectedLine, stdout) {
  90. var lines = stdout.split('\n').map(R.trim);
  91. var lineMatches = R.equals(expectedLine);
  92. return lines.some(lineMatches);
  93. }
  94. /**
  95. * Confirms if given value is a valid CYPRESS_ENV value. Undefined values
  96. * are valid, because the system can set the default one.
  97. *
  98. * @param {string} value
  99. * @example util.isValidCypressEnvValue(process.env.CYPRESS_ENV)
  100. */
  101. function isValidCypressEnvValue(value) {
  102. if (_.isUndefined(value)) {
  103. // will get default value
  104. return true;
  105. }
  106. // names of config environments, see "packages/server/config/app.yml"
  107. var names = ['development', 'test', 'staging', 'production'];
  108. return _.includes(names, value);
  109. }
  110. /**
  111. * Prints NODE_OPTIONS using debug() module, but only
  112. * if DEBUG=cypress... is set
  113. */
  114. function printNodeOptions() {
  115. var log = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : debug;
  116. if (!log.enabled) {
  117. return;
  118. }
  119. if (process.env.NODE_OPTIONS) {
  120. log('NODE_OPTIONS=%s', process.env.NODE_OPTIONS);
  121. } else {
  122. log('NODE_OPTIONS is not set');
  123. }
  124. }
  125. /**
  126. * Removes double quote characters
  127. * from the start and end of the given string IF they are both present
  128. *
  129. * @param {string} str Input string
  130. * @returns {string} Trimmed string or the original string if there are no double quotes around it.
  131. * @example
  132. ```
  133. dequote('"foo"')
  134. // returns string 'foo'
  135. dequote('foo')
  136. // returns string 'foo'
  137. ```
  138. */
  139. var dequote = function dequote(str) {
  140. la(is.string(str), 'expected a string to remove double quotes', str);
  141. if (str.length > 1 && str[0] === '"' && str[str.length - 1] === '"') {
  142. return str.substr(1, str.length - 2);
  143. }
  144. return str;
  145. };
  146. var parseOpts = function parseOpts(opts) {
  147. opts = _.pick(opts, 'browser', 'cachePath', 'cacheList', 'cacheClear', 'ciBuildId', 'config', 'configFile', 'cypressVersion', 'destination', 'detached', 'dev', 'exit', 'env', 'force', 'global', 'group', 'headed', 'headless', 'key', 'path', 'parallel', 'port', 'project', 'reporter', 'reporterOptions', 'record', 'spec', 'tag');
  148. if (opts.exit) {
  149. opts = _.omit(opts, 'exit');
  150. }
  151. // some options might be quoted - which leads to unexpected results
  152. // remove double quotes from certain options
  153. var removeQuotes = {
  154. group: dequote,
  155. ciBuildId: dequote
  156. };
  157. var cleanOpts = R.evolve(removeQuotes, opts);
  158. debug('parsed cli options %o', cleanOpts);
  159. return cleanOpts;
  160. };
  161. var util = {
  162. normalizeModuleOptions: normalizeModuleOptions,
  163. parseOpts: parseOpts,
  164. isValidCypressEnvValue: isValidCypressEnvValue,
  165. printNodeOptions: printNodeOptions,
  166. isCi: function isCi() {
  167. return _isCi;
  168. },
  169. getEnvOverrides: function getEnvOverrides() {
  170. return _.chain({}).extend(util.getEnvColors()).extend(util.getForceTty()).omitBy(_.isUndefined) // remove undefined values
  171. .mapValues(function (value) {
  172. // stringify to 1 or 0
  173. return value ? '1' : '0';
  174. }).value();
  175. },
  176. getForceTty: function getForceTty() {
  177. return {
  178. FORCE_STDIN_TTY: util.isTty(process.stdin.fd),
  179. FORCE_STDOUT_TTY: util.isTty(process.stdout.fd),
  180. FORCE_STDERR_TTY: util.isTty(process.stderr.fd)
  181. };
  182. },
  183. getEnvColors: function getEnvColors() {
  184. var sc = util.supportsColor();
  185. return {
  186. FORCE_COLOR: sc,
  187. DEBUG_COLORS: sc,
  188. MOCHA_COLORS: sc ? true : undefined
  189. };
  190. },
  191. isTty: function isTty(fd) {
  192. return tty.isatty(fd);
  193. },
  194. supportsColor: function supportsColor() {
  195. // if we've been explictly told not to support
  196. // color then turn this off
  197. if (process.env.NO_COLOR) {
  198. return false;
  199. }
  200. // https://github.com/cypress-io/cypress/issues/1747
  201. // always return true in CI providers
  202. if (process.env.CI) {
  203. return true;
  204. }
  205. // ensure that both stdout and stderr support color
  206. return Boolean(_supportsColor.stdout) && Boolean(_supportsColor.stderr);
  207. },
  208. cwd: function cwd() {
  209. return process.cwd();
  210. },
  211. pkgVersion: function pkgVersion() {
  212. return pkg.version;
  213. },
  214. exit: function exit(code) {
  215. process.exit(code);
  216. },
  217. logErrorExit1: function logErrorExit1(err) {
  218. logger.error(err.message);
  219. process.exit(1);
  220. },
  221. dequote: dequote,
  222. titleize: function titleize() {
  223. for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
  224. args[_key] = arguments[_key];
  225. }
  226. // prepend first arg with space
  227. // and pad so that all messages line up
  228. args[0] = _.padEnd(' ' + args[0], 24);
  229. // get rid of any falsy values
  230. args = _.compact(args);
  231. return chalk.blue.apply(chalk, _toConsumableArray(args));
  232. },
  233. calculateEta: function calculateEta(percent, elapsed) {
  234. // returns the number of seconds remaining
  235. // if we're at 100% already just return 0
  236. if (percent === 100) {
  237. return 0;
  238. }
  239. // take the percentage and divide by one
  240. // and multiple that against elapsed
  241. // subtracting what's already elapsed
  242. return elapsed * (1 / (percent / 100)) - elapsed;
  243. },
  244. convertPercentToPercentage: function convertPercentToPercentage(num) {
  245. // convert a percent with values between 0 and 1
  246. // with decimals, so that it is between 0 and 100
  247. // and has no decimal places
  248. return Math.round(_.isFinite(num) ? num * 100 : 0);
  249. },
  250. secsRemaining: function secsRemaining(eta) {
  251. // calculate the seconds reminaing with no decimal places
  252. return (_.isFinite(eta) ? eta / 1000 : 0).toFixed(0);
  253. },
  254. setTaskTitle: function setTaskTitle(task, title, renderer) {
  255. // only update the renderer title when not running in CI
  256. if (renderer === 'default' && task.title !== title) {
  257. task.title = title;
  258. }
  259. },
  260. isInstalledGlobally: function isInstalledGlobally() {
  261. return _isInstalledGlobally;
  262. },
  263. isSemver: function isSemver(str) {
  264. return (/^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(str)
  265. );
  266. },
  267. isExecutableAsync: function isExecutableAsync(filePath) {
  268. return Promise.resolve(executable(filePath));
  269. },
  270. isLinux: isLinux,
  271. getOsVersionAsync: function getOsVersionAsync() {
  272. return Promise.try(function () {
  273. if (isLinux()) {
  274. return getosAsync().then(function (osInfo) {
  275. return [osInfo.dist, osInfo.release].join(' - ');
  276. }).catch(function () {
  277. return os.release();
  278. });
  279. }
  280. return os.release();
  281. });
  282. },
  283. // attention:
  284. // when passing relative path to NPM post install hook, the current working
  285. // directory is set to the `node_modules/cypress` folder
  286. // the user is probably passing relative path with respect to root package folder
  287. formAbsolutePath: function formAbsolutePath(filename) {
  288. if (path.isAbsolute(filename)) {
  289. return filename;
  290. }
  291. return path.join(process.cwd(), '..', '..', filename);
  292. },
  293. getEnv: function getEnv(varName, trim) {
  294. la(is.unemptyString(varName), 'expected environment variable name, not', varName);
  295. var envVar = process.env[varName];
  296. var configVar = process.env['npm_config_' + varName];
  297. var packageConfigVar = process.env['npm_package_config_' + varName];
  298. var result = void 0;
  299. if (envVar) {
  300. debug('Using ' + varName + ' from environment variable');
  301. result = envVar;
  302. } else if (configVar) {
  303. debug('Using ' + varName + ' from npm config');
  304. result = configVar;
  305. } else if (packageConfigVar) {
  306. debug('Using ' + varName + ' from package.json config');
  307. result = packageConfigVar;
  308. }
  309. // environment variables are often set double quotes to escape characters
  310. // and on Windows it can lead to weird things: for example
  311. // set FOO="C:\foo.txt" && node -e "console.log('>>>%s<<<', process.env.FOO)"
  312. // will print
  313. // >>>"C:\foo.txt" <<<
  314. // see https://github.com/cypress-io/cypress/issues/4506#issuecomment-506029942
  315. // so for sanity sake we should first trim whitespace characters and remove
  316. // double quotes around environment strings if the caller is expected to
  317. // use this environment string as a file path
  318. return trim ? dequote(_.trim(result)) : result;
  319. },
  320. getCacheDir: function getCacheDir() {
  321. return cachedir('Cypress');
  322. },
  323. isPostInstall: function isPostInstall() {
  324. return process.env.npm_lifecycle_event === 'postinstall';
  325. },
  326. exec: execa,
  327. stdoutLineMatches: stdoutLineMatches,
  328. issuesUrl: issuesUrl,
  329. isBrokenGtkDisplay: isBrokenGtkDisplay,
  330. logBrokenGtkDisplayWarning: logBrokenGtkDisplayWarning,
  331. isPossibleLinuxWithIncorrectDisplay: isPossibleLinuxWithIncorrectDisplay,
  332. getGitHubIssueUrl: function getGitHubIssueUrl(number) {
  333. la(is.positive(number), 'github issue should be a positive number', number);
  334. la(_.isInteger(number), 'github issue should be an integer', number);
  335. return issuesUrl + '/' + number;
  336. },
  337. getFileChecksum: getFileChecksum,
  338. getFileSize: getFileSize
  339. };
  340. module.exports = util;