|
"use strict"; |
|
|
|
const _ = require('lodash'); |
|
const chalk = require('chalk'); |
|
const { |
|
Listr |
|
} = require('listr2'); |
|
const debug = require('debug')('cypress:cli'); |
|
const { |
|
stripIndent |
|
} = require('common-tags'); |
|
const Promise = require('bluebird'); |
|
const logSymbols = require('log-symbols'); |
|
const path = require('path'); |
|
const os = require('os'); |
|
const verbose = require('../VerboseRenderer'); |
|
const { |
|
throwFormErrorText, |
|
errors |
|
} = require('../errors'); |
|
const util = require('../util'); |
|
const logger = require('../logger'); |
|
const xvfb = require('../exec/xvfb'); |
|
const state = require('./state'); |
|
const VERIFY_TEST_RUNNER_TIMEOUT_MS = +util.getEnv('CYPRESS_VERIFY_TIMEOUT') || 30000; |
|
const checkExecutable = binaryDir => { |
|
const executable = state.getPathToExecutable(binaryDir); |
|
debug('checking if executable exists', executable); |
|
return util.isExecutableAsync(executable).then(isExecutable => { |
|
debug('Binary is executable? :', isExecutable); |
|
if (!isExecutable) { |
|
return throwFormErrorText(errors.binaryNotExecutable(executable))(); |
|
} |
|
}).catch({ |
|
code: 'ENOENT' |
|
}, () => { |
|
if (util.isCi()) { |
|
return throwFormErrorText(errors.notInstalledCI(executable))(); |
|
} |
|
return throwFormErrorText(errors.missingApp(binaryDir))(stripIndent` |
|
Cypress executable not found at: ${chalk.cyan(executable)} |
|
`); |
|
}); |
|
}; |
|
const runSmokeTest = (binaryDir, options) => { |
|
let executable = state.getPathToExecutable(binaryDir); |
|
const onSmokeTestError = (smokeTestCommand, linuxWithDisplayEnv) => { |
|
return err => { |
|
debug('Smoke test failed:', err); |
|
let errMessage = err.stderr || err.message; |
|
debug('error message:', errMessage); |
|
if (err.timedOut) { |
|
debug('error timedOut is true'); |
|
return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, true))(errMessage); |
|
} |
|
if (linuxWithDisplayEnv && util.isBrokenGtkDisplay(errMessage)) { |
|
util.logBrokenGtkDisplayWarning(); |
|
return throwFormErrorText(errors.invalidSmokeTestDisplayError)(errMessage); |
|
} |
|
return throwFormErrorText(errors.missingDependency)(errMessage); |
|
}; |
|
}; |
|
const needsXvfb = xvfb.isNeeded(); |
|
debug('needs Xvfb?', needsXvfb); |
|
|
|
|
|
|
|
|
|
|
|
const spawn = linuxWithDisplayEnv => { |
|
const random = _.random(0, 1000); |
|
const args = ['--smoke-test', `--ping=${random}`]; |
|
if (needsSandbox()) { |
|
|
|
debug('disabling Electron sandbox'); |
|
args.unshift('--no-sandbox'); |
|
} |
|
if (options.dev) { |
|
executable = 'node'; |
|
args.unshift(path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js')); |
|
} |
|
const smokeTestCommand = `${executable} ${args.join(' ')}`; |
|
debug('running smoke test'); |
|
debug('using Cypress executable %s', executable); |
|
debug('smoke test command:', smokeTestCommand); |
|
debug('smoke test timeout %d ms', options.smokeTestTimeout); |
|
const stdioOptions = _.extend({}, { |
|
env: { |
|
...process.env, |
|
FORCE_COLOR: 0 |
|
}, |
|
timeout: options.smokeTestTimeout |
|
}); |
|
return Promise.resolve(util.exec(executable, args, stdioOptions)).catch(onSmokeTestError(smokeTestCommand, linuxWithDisplayEnv)).then(result => { |
|
|
|
|
|
|
|
const smokeTestStdout = _.get(result, 'stdout', ''); |
|
debug('smoke test stdout "%s"', smokeTestStdout); |
|
if (!util.stdoutLineMatches(String(random), smokeTestStdout)) { |
|
debug('Smoke test failed because could not find %d in:', random, result); |
|
const smokeTestStderr = _.get(result, 'stderr', ''); |
|
const errorText = smokeTestStderr || smokeTestStdout; |
|
return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, false))(errorText); |
|
} |
|
}); |
|
}; |
|
const spawnInXvfb = linuxWithDisplayEnv => { |
|
return xvfb.start().then(() => { |
|
return spawn(linuxWithDisplayEnv); |
|
}).finally(xvfb.stop); |
|
}; |
|
const userFriendlySpawn = linuxWithDisplayEnv => { |
|
debug('spawning, should retry on display problem?', Boolean(linuxWithDisplayEnv)); |
|
return spawn(linuxWithDisplayEnv).catch({ |
|
code: 'INVALID_SMOKE_TEST_DISPLAY_ERROR' |
|
}, () => { |
|
return spawnInXvfb(linuxWithDisplayEnv); |
|
}); |
|
}; |
|
if (needsXvfb) { |
|
return spawnInXvfb(); |
|
} |
|
|
|
|
|
|
|
|
|
const linuxWithDisplayEnv = util.isPossibleLinuxWithIncorrectDisplay(); |
|
return userFriendlySpawn(linuxWithDisplayEnv); |
|
}; |
|
function testBinary(version, binaryDir, options) { |
|
debug('running binary verification check', version); |
|
|
|
|
|
if (!options.force) { |
|
logger.log(stripIndent` |
|
It looks like this is your first time using Cypress: ${chalk.cyan(version)} |
|
`); |
|
} |
|
logger.log(); |
|
|
|
|
|
|
|
|
|
let renderer = util.isCi() ? verbose : 'default'; |
|
if (logger.logLevel() === 'silent') renderer = 'silent'; |
|
const rendererOptions = { |
|
renderer |
|
}; |
|
const tasks = new Listr([{ |
|
options: { |
|
title: util.titleize('Verifying Cypress can run', chalk.gray(binaryDir)) |
|
}, |
|
task: (ctx, task) => { |
|
debug('clearing out the verified version'); |
|
return state.clearBinaryStateAsync(binaryDir).then(() => { |
|
return Promise.all([runSmokeTest(binaryDir, options), Promise.resolve().delay(1500) |
|
]); |
|
}).then(() => { |
|
debug('write verified: true'); |
|
return state.writeBinaryVerifiedAsync(true, binaryDir); |
|
}).then(() => { |
|
util.setTaskTitle(task, util.titleize(chalk.green('Verified Cypress!'), chalk.gray(binaryDir)), rendererOptions.renderer); |
|
}); |
|
} |
|
}], { |
|
rendererOptions |
|
}); |
|
return tasks.run(); |
|
} |
|
const maybeVerify = (installedVersion, binaryDir, options) => { |
|
return state.getBinaryVerifiedAsync(binaryDir).then(isVerified => { |
|
debug('is Verified ?', isVerified); |
|
let shouldVerify = !isVerified; |
|
|
|
|
|
if (options.force) { |
|
debug('force verify'); |
|
shouldVerify = true; |
|
} |
|
if (shouldVerify) { |
|
return testBinary(installedVersion, binaryDir, options).then(() => { |
|
if (options.welcomeMessage) { |
|
logger.log(); |
|
logger.log('Opening Cypress...'); |
|
} |
|
}); |
|
} |
|
}); |
|
}; |
|
const start = (options = {}) => { |
|
debug('verifying Cypress app'); |
|
const packageVersion = util.pkgVersion(); |
|
let binaryDir = state.getBinaryDir(packageVersion); |
|
_.defaults(options, { |
|
dev: false, |
|
force: false, |
|
welcomeMessage: true, |
|
smokeTestTimeout: VERIFY_TEST_RUNNER_TIMEOUT_MS |
|
}); |
|
if (options.dev) { |
|
return runSmokeTest('', options); |
|
} |
|
const parseBinaryEnvVar = () => { |
|
const envBinaryPath = util.getEnv('CYPRESS_RUN_BINARY'); |
|
debug('CYPRESS_RUN_BINARY exists, =', envBinaryPath); |
|
logger.log(stripIndent` |
|
${chalk.yellow('Note:')} You have set the environment variable: |
|
|
|
${chalk.white('CYPRESS_RUN_BINARY=')}${chalk.cyan(envBinaryPath)} |
|
|
|
This overrides the default Cypress binary path used. |
|
`); |
|
logger.log(); |
|
return util.isExecutableAsync(envBinaryPath).then(isExecutable => { |
|
debug('CYPRESS_RUN_BINARY is executable? :', isExecutable); |
|
if (!isExecutable) { |
|
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(stripIndent` |
|
The supplied binary path is not executable |
|
`); |
|
} |
|
}).then(() => { |
|
return state.parseRealPlatformBinaryFolderAsync(envBinaryPath); |
|
}).then(envBinaryDir => { |
|
if (!envBinaryDir) { |
|
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(); |
|
} |
|
debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir); |
|
binaryDir = envBinaryDir; |
|
}).catch({ |
|
code: 'ENOENT' |
|
}, err => { |
|
return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(err.message); |
|
}); |
|
}; |
|
return Promise.try(() => { |
|
debug('checking environment variables'); |
|
if (util.getEnv('CYPRESS_RUN_BINARY')) { |
|
return parseBinaryEnvVar(); |
|
} |
|
}).then(() => { |
|
return checkExecutable(binaryDir); |
|
}).tap(() => { |
|
return debug('binaryDir is ', binaryDir); |
|
}).then(() => { |
|
return state.getBinaryPkgAsync(binaryDir); |
|
}).then(pkg => { |
|
return state.getBinaryPkgVersion(pkg); |
|
}).then(binaryVersion => { |
|
if (!binaryVersion) { |
|
debug('no Cypress binary found for cli version ', packageVersion); |
|
return throwFormErrorText(errors.missingApp(binaryDir))(` |
|
Cannot read binary version from: ${chalk.cyan(state.getBinaryPkgPath(binaryDir))} |
|
`); |
|
} |
|
debug(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`); |
|
if (binaryVersion !== packageVersion) { |
|
|
|
|
|
logger.log(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`); |
|
logger.log(); |
|
logger.warn(stripIndent` |
|
|
|
|
|
${logSymbols.warning} Warning: Binary version ${chalk.green(binaryVersion)} does not match the expected package version ${chalk.green(packageVersion)} |
|
|
|
These versions may not work properly together. |
|
`); |
|
logger.log(); |
|
} |
|
return maybeVerify(binaryVersion, binaryDir, options); |
|
}).catch(err => { |
|
if (err.known) { |
|
throw err; |
|
} |
|
return throwFormErrorText(errors.unexpected)(err.stack); |
|
}); |
|
}; |
|
const isLinuxLike = () => os.platform() !== 'win32'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const needsSandbox = () => isLinuxLike(); |
|
module.exports = { |
|
start, |
|
VERIFY_TEST_RUNNER_TIMEOUT_MS, |
|
needsSandbox |
|
}; |