|
"use strict"; |
|
|
|
const _ = require('lodash'); |
|
const arch = require('arch'); |
|
const os = require('os'); |
|
const ospath = require('ospath'); |
|
const crypto = require('crypto'); |
|
const la = require('lazy-ass'); |
|
const is = require('check-more-types'); |
|
const tty = require('tty'); |
|
const path = require('path'); |
|
const isCi = require('is-ci'); |
|
const execa = require('execa'); |
|
const getos = require('getos'); |
|
const chalk = require('chalk'); |
|
const Promise = require('bluebird'); |
|
const cachedir = require('cachedir'); |
|
const logSymbols = require('log-symbols'); |
|
const executable = require('executable'); |
|
const { |
|
stripIndent |
|
} = require('common-tags'); |
|
const supportsColor = require('supports-color'); |
|
const isInstalledGlobally = require('is-installed-globally'); |
|
const logger = require('./logger'); |
|
const debug = require('debug')('cypress:cli'); |
|
const fs = require('./fs'); |
|
const pkg = require(path.join(__dirname, '..', 'package.json')); |
|
const issuesUrl = 'https://github.com/cypress-io/cypress/issues'; |
|
const getosAsync = Promise.promisify(getos); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getFileChecksum = filename => { |
|
la(is.unemptyString(filename), 'expected filename', filename); |
|
const hashStream = () => { |
|
const s = crypto.createHash('sha512'); |
|
s.setEncoding('hex'); |
|
return s; |
|
}; |
|
return new Promise((resolve, reject) => { |
|
const stream = fs.createReadStream(filename); |
|
stream.on('error', reject).pipe(hashStream()).on('error', reject).on('finish', function () { |
|
resolve(this.read()); |
|
}); |
|
}); |
|
}; |
|
const getFileSize = filename => { |
|
la(is.unemptyString(filename), 'expected filename', filename); |
|
return fs.statAsync(filename).get('size'); |
|
}; |
|
const isBrokenGtkDisplayRe = /Gtk: cannot open display/; |
|
const stringify = val => { |
|
return _.isObject(val) ? JSON.stringify(val) : val; |
|
}; |
|
function normalizeModuleOptions(options = {}) { |
|
return _.mapValues(options, stringify); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const isLinux = () => { |
|
return os.platform() === 'linux'; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isBrokenGtkDisplay = str => { |
|
return isBrokenGtkDisplayRe.test(str); |
|
}; |
|
const isPossibleLinuxWithIncorrectDisplay = () => { |
|
return isLinux() && process.env.DISPLAY; |
|
}; |
|
const logBrokenGtkDisplayWarning = () => { |
|
debug('Cypress exited due to a broken gtk display because of a potential invalid DISPLAY env... retrying after starting Xvfb'); |
|
|
|
|
|
logger.warn(stripIndent` |
|
|
|
${logSymbols.warning} Warning: Cypress failed to start. |
|
|
|
This is likely due to a misconfigured DISPLAY environment variable. |
|
|
|
DISPLAY was set to: "${process.env.DISPLAY}" |
|
|
|
Cypress will attempt to fix the problem and rerun. |
|
`); |
|
logger.warn(); |
|
}; |
|
function stdoutLineMatches(expectedLine, stdout) { |
|
const lines = stdout.split('\n').map(val => val.trim()); |
|
return lines.some(line => line === expectedLine); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isValidCypressInternalEnvValue(value) { |
|
if (_.isUndefined(value)) { |
|
|
|
return true; |
|
} |
|
|
|
|
|
const names = ['development', 'test', 'staging', 'production']; |
|
return _.includes(names, value); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isNonProductionCypressInternalEnvValue(value) { |
|
return !_.isUndefined(value) && value !== 'production'; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function printNodeOptions(log = debug) { |
|
if (!log.enabled) { |
|
return; |
|
} |
|
if (process.env.NODE_OPTIONS) { |
|
log('NODE_OPTIONS=%s', process.env.NODE_OPTIONS); |
|
} else { |
|
log('NODE_OPTIONS is not set'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dequote = str => { |
|
la(is.string(str), 'expected a string to remove double quotes', str); |
|
if (str.length > 1 && str[0] === '"' && str[str.length - 1] === '"') { |
|
return str.substr(1, str.length - 2); |
|
} |
|
return str; |
|
}; |
|
const parseOpts = opts => { |
|
opts = _.pick(opts, 'autoCancelAfterFailures', 'browser', 'cachePath', 'cacheList', 'cacheClear', 'cachePrune', 'ciBuildId', 'ct', 'component', 'config', 'configFile', 'cypressVersion', 'destination', 'detached', 'dev', 'e2e', 'exit', 'env', 'force', 'global', 'group', 'headed', 'headless', 'inspect', 'inspectBrk', 'key', 'path', 'parallel', 'port', 'project', 'quiet', 'reporter', 'reporterOptions', 'record', 'runnerUi', 'runProject', 'spec', 'tag'); |
|
if (opts.exit) { |
|
opts = _.omit(opts, 'exit'); |
|
} |
|
|
|
|
|
|
|
const cleanOpts = { |
|
...opts |
|
}; |
|
const toDequote = ['group', 'ciBuildId']; |
|
for (const prop of toDequote) { |
|
if (_.has(opts, prop)) { |
|
cleanOpts[prop] = dequote(opts[prop]); |
|
} |
|
} |
|
debug('parsed cli options %o', cleanOpts); |
|
return cleanOpts; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const getApplicationDataFolder = (...paths) => { |
|
const { |
|
env |
|
} = process; |
|
|
|
|
|
let folder = env.CYPRESS_CONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development'; |
|
const PRODUCT_NAME = pkg.productName || pkg.name; |
|
const OS_DATA_PATH = ospath.data(); |
|
const ELECTRON_APP_DATA_PATH = path.join(OS_DATA_PATH, PRODUCT_NAME); |
|
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) { |
|
folder = `${folder}-e2e-test`; |
|
} |
|
const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths); |
|
return p; |
|
}; |
|
const util = { |
|
normalizeModuleOptions, |
|
parseOpts, |
|
isValidCypressInternalEnvValue, |
|
isNonProductionCypressInternalEnvValue, |
|
printNodeOptions, |
|
isCi() { |
|
return isCi; |
|
}, |
|
getEnvOverrides(options = {}) { |
|
return _.chain({}).extend(util.getEnvColors()).extend(util.getForceTty()).omitBy(_.isUndefined) |
|
.mapValues(value => { |
|
|
|
return value ? '1' : '0'; |
|
}).extend(util.getOriginalNodeOptions()).value(); |
|
}, |
|
getOriginalNodeOptions() { |
|
const opts = {}; |
|
if (process.env.NODE_OPTIONS) { |
|
opts.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS; |
|
} |
|
return opts; |
|
}, |
|
getForceTty() { |
|
return { |
|
FORCE_STDIN_TTY: util.isTty(process.stdin.fd), |
|
FORCE_STDOUT_TTY: util.isTty(process.stdout.fd), |
|
FORCE_STDERR_TTY: util.isTty(process.stderr.fd) |
|
}; |
|
}, |
|
getEnvColors() { |
|
const sc = util.supportsColor(); |
|
return { |
|
FORCE_COLOR: sc, |
|
DEBUG_COLORS: sc, |
|
MOCHA_COLORS: sc ? true : undefined |
|
}; |
|
}, |
|
isTty(fd) { |
|
return tty.isatty(fd); |
|
}, |
|
supportsColor() { |
|
|
|
|
|
if (process.env.NO_COLOR) { |
|
return false; |
|
} |
|
|
|
|
|
|
|
if (process.env.CI) { |
|
return true; |
|
} |
|
|
|
|
|
return Boolean(supportsColor.stdout) && Boolean(supportsColor.stderr); |
|
}, |
|
cwd() { |
|
return process.cwd(); |
|
}, |
|
pkgBuildInfo() { |
|
return pkg.buildInfo; |
|
}, |
|
pkgVersion() { |
|
return pkg.version; |
|
}, |
|
exit(code) { |
|
process.exit(code); |
|
}, |
|
logErrorExit1(err) { |
|
logger.error(err.message); |
|
process.exit(1); |
|
}, |
|
dequote, |
|
titleize(...args) { |
|
|
|
|
|
args[0] = _.padEnd(` ${args[0]}`, 24); |
|
|
|
|
|
args = _.compact(args); |
|
return chalk.blue(...args); |
|
}, |
|
calculateEta(percent, elapsed) { |
|
|
|
|
|
|
|
if (percent === 100) { |
|
return 0; |
|
} |
|
|
|
|
|
|
|
|
|
return elapsed * (1 / (percent / 100)) - elapsed; |
|
}, |
|
convertPercentToPercentage(num) { |
|
|
|
|
|
|
|
return Math.round(_.isFinite(num) ? num * 100 : 0); |
|
}, |
|
secsRemaining(eta) { |
|
|
|
return (_.isFinite(eta) ? eta / 1000 : 0).toFixed(0); |
|
}, |
|
setTaskTitle(task, title, renderer) { |
|
|
|
if (renderer === 'default' && task.title !== title) { |
|
task.title = title; |
|
} |
|
}, |
|
isInstalledGlobally() { |
|
return isInstalledGlobally; |
|
}, |
|
isSemver(str) { |
|
return /^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(str); |
|
}, |
|
isExecutableAsync(filePath) { |
|
return Promise.resolve(executable(filePath)); |
|
}, |
|
isLinux, |
|
getOsVersionAsync() { |
|
return Promise.try(() => { |
|
if (isLinux()) { |
|
return getosAsync().then(osInfo => { |
|
return [osInfo.dist, osInfo.release].join(' - '); |
|
}).catch(() => { |
|
return os.release(); |
|
}); |
|
} |
|
return os.release(); |
|
}); |
|
}, |
|
async getPlatformInfo() { |
|
const [version, osArch] = await Promise.all([util.getOsVersionAsync(), this.getRealArch()]); |
|
return stripIndent` |
|
Platform: ${os.platform()}-${osArch} (${version}) |
|
Cypress Version: ${util.pkgVersion()} |
|
`; |
|
}, |
|
_cachedArch: undefined, |
|
|
|
|
|
|
|
async getRealArch() { |
|
if (this._cachedArch) return this._cachedArch; |
|
async function _getRealArch() { |
|
const osPlatform = os.platform(); |
|
|
|
const osArch = os.arch(); |
|
debug('detecting arch %o', { |
|
osPlatform, |
|
osArch |
|
}); |
|
if (osArch === 'arm64') return 'arm64'; |
|
if (osPlatform === 'darwin') { |
|
|
|
|
|
const { |
|
stdout |
|
} = await execa('sysctl', ['-n', 'sysctl.proc_translated']).catch(() => ''); |
|
debug('rosetta check result: %o', { |
|
stdout |
|
}); |
|
if (stdout === '1') return 'arm64'; |
|
} |
|
if (osPlatform === 'linux') { |
|
|
|
|
|
const { |
|
stdout |
|
} = await execa('uname', ['-m']).catch(() => ''); |
|
debug('arm uname -m result: %o ', { |
|
stdout |
|
}); |
|
if (['aarch64_be', 'aarch64', 'armv8b', 'armv8l'].includes(stdout)) return 'arm64'; |
|
} |
|
|
|
|
|
const pkgArch = arch(); |
|
if (pkgArch === 'x86') return 'ia32'; |
|
return pkgArch; |
|
} |
|
return this._cachedArch = await _getRealArch(); |
|
}, |
|
|
|
|
|
|
|
|
|
formAbsolutePath(filename) { |
|
if (path.isAbsolute(filename)) { |
|
return filename; |
|
} |
|
return path.join(process.cwd(), '..', '..', filename); |
|
}, |
|
getEnv(varName, trim) { |
|
la(is.unemptyString(varName), 'expected environment variable name, not', varName); |
|
const configVarName = `npm_config_${varName}`; |
|
const configVarNameLower = configVarName.toLowerCase(); |
|
const packageConfigVarName = `npm_package_config_${varName}`; |
|
let result; |
|
if (process.env.hasOwnProperty(varName)) { |
|
debug(`Using ${varName} from environment variable`); |
|
result = process.env[varName]; |
|
} else if (process.env.hasOwnProperty(configVarName)) { |
|
debug(`Using ${varName} from npm config`); |
|
result = process.env[configVarName]; |
|
} else if (process.env.hasOwnProperty(configVarNameLower)) { |
|
debug(`Using ${varName.toLowerCase()} from npm config`); |
|
result = process.env[configVarNameLower]; |
|
} else if (process.env.hasOwnProperty(packageConfigVarName)) { |
|
debug(`Using ${varName} from package.json config`); |
|
result = process.env[packageConfigVarName]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return trim ? dequote(_.trim(result)) : result; |
|
}, |
|
getCacheDir() { |
|
return cachedir('Cypress'); |
|
}, |
|
isPostInstall() { |
|
return process.env.npm_lifecycle_event === 'postinstall'; |
|
}, |
|
exec: execa, |
|
stdoutLineMatches, |
|
issuesUrl, |
|
isBrokenGtkDisplay, |
|
logBrokenGtkDisplayWarning, |
|
isPossibleLinuxWithIncorrectDisplay, |
|
getGitHubIssueUrl(number) { |
|
la(is.positive(number), 'github issue should be a positive number', number); |
|
la(_.isInteger(number), 'github issue should be an integer', number); |
|
return `${issuesUrl}/${number}`; |
|
}, |
|
getFileChecksum, |
|
getFileSize, |
|
getApplicationDataFolder |
|
}; |
|
module.exports = util; |