mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-25 10:19:30 -05:00
39a7f2a743
* wip: graceful exit * add types for signal-exit * use graceful exit; properly forward signals to forked process from gulp * rm extraneous * wait for child processes to close before exiting parent processes * do not teardown twice; allow for multiple will-quit messages * wait for async child to exit, clean up * bring back env vars rather than inlined undefines * updated tests * fix exit codes * pingpong code * system test * rm extraneous debug entry * fix various * fix operator precedence w/ promise * improper treekill usage, coerce null exitCode * fix concurrent duplicate .close() on watchers * changelog * clear timeout * refinements * refine * infer exit code from signal * if graceful exit fails in before-quit handler, exit with code 1 * v8 build errors * revert lockfile teardown to previous pattern, keep on graceful-exit * exit code 1 no matter what the signal is * system test snapshots * snapshot * integration test timing fixes, more robust teardown * no eventmap type huh * universal reset of graceful exit * rm all listeners from studio lifecycle * euid on windows * fix posix code 112 exit criterion * mock process.exit in interactive spec * fix graceful exit step * better handling of return types for exit codes * fixes an issue where system tests were not tearing down correctly * rm dead code * 112 * Update record_spec.js * better messaging, debugging * additional debugging for child process; fix CT teardown * changelog * fix run child fixture failing to exit * rm console * add proper error listener so test does not fail prematurely * system test snapshots; no longer emit err from async child * add back snapshots that got removed? * only resolve on signals, not reject * shut down gql ws server correctly * properly await project shutdown in unit test * prevent repeated graphql-ws .dispose() calls * fix spawn unit test * debounce signals in graceful exit signal handlers to account for duplicate kills from signal-exit * only emit sigint/term message once, in case signal-exit goes overboard * changelog * Apply suggestion from @cacieprins * do not double-resolve * rm unused types * rm all listeners on child process even if already killed * sigkill = exit with 137 * prevent zombie plugin child processes * prevent morgan output during shutdown * surround sigint messaging with newlines to improve visual experience * correct changelog * fix ci-only issues * enable mockery for morgan in before each instead of outside of test closure * changelog * fix out of order * ensure stdin is no longer set to raw mode when signals are received * fix spawn unit tests for setRawMode(false) * check tty in gulp script * lock, cache * wsp * prevent orphaned steps in graceful shutdown * dont stack sigint/term handlers in pkgs/electron open * changelog
165 lines
4.2 KiB
TypeScript
165 lines
4.2 KiB
TypeScript
import type { SpawnerResult, Spawner } from './system-tests'
|
|
import Docker from 'dockerode'
|
|
import stream from 'stream'
|
|
import EventEmitter from 'events'
|
|
import path from 'path'
|
|
import { promises as fs } from 'fs'
|
|
import execa from 'execa'
|
|
import Fixtures from './fixtures'
|
|
import { nock } from './spec_helper'
|
|
|
|
let docker: Docker | null = null
|
|
|
|
const getDocker = () => {
|
|
return docker || (docker = new Docker())
|
|
}
|
|
|
|
const log = (...args) => {
|
|
console.error('🐋', ...args)
|
|
}
|
|
|
|
class DockerProcess extends EventEmitter implements SpawnerResult {
|
|
stdout = new stream.PassThrough()
|
|
stderr = new stream.PassThrough()
|
|
|
|
/** Placeholder; Cypress runs in Docker — harness interrupt does not tree-kill this pid. */
|
|
pid = 0
|
|
|
|
constructor (private dockerImage: string) {
|
|
super()
|
|
}
|
|
|
|
pull () {
|
|
return new Promise<void>((resolve, reject) => {
|
|
log('Pulling image', this.dockerImage)
|
|
getDocker().pull(this.dockerImage, null, (err, stream) => {
|
|
if (err) return reject(err)
|
|
|
|
const onFinished = (err) => {
|
|
log('Pull complete', { err })
|
|
if (err) return reject(err)
|
|
|
|
resolve()
|
|
}
|
|
|
|
const onProgress = (event) => {
|
|
log('Pull progress', JSON.stringify(event))
|
|
}
|
|
|
|
docker.modem.followProgress(stream, onFinished, onProgress)
|
|
}, null)
|
|
})
|
|
}
|
|
|
|
run (opts: {
|
|
cmd: string
|
|
args: string[]
|
|
env: Record<string, string>
|
|
}) {
|
|
const containerCreateEnv = []
|
|
|
|
for (const k in opts.env) {
|
|
// skip problematic env vars that we don't wanna preserve from `process.env`
|
|
if (
|
|
['DISPLAY', 'USER', 'HOME', 'USERNAME', 'PATH'].includes(k)
|
|
|| k.startsWith('npm_')
|
|
) {
|
|
continue
|
|
}
|
|
|
|
containerCreateEnv.push([k, opts.env[k]].join('='))
|
|
}
|
|
|
|
log('Running image', this.dockerImage)
|
|
|
|
const cmd = [opts.cmd, ...opts.args]
|
|
|
|
log('Running cmd', cmd.join(' '))
|
|
|
|
getDocker().run(
|
|
this.dockerImage,
|
|
cmd,
|
|
[this.stdout, this.stderr],
|
|
// option docs: https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
|
|
{
|
|
AutoRemove: true,
|
|
Entrypoint: 'bash',
|
|
Tty: false, // so we can use stdout and stderr
|
|
Env: containerCreateEnv,
|
|
Privileged: true,
|
|
Binds: [
|
|
[path.join(__dirname, '..', '..'), '/cypress'],
|
|
// map tmpDir to the same absolute path on the container to make it easier to reason about paths in tests
|
|
[Fixtures.cyTmpDir, Fixtures.cyTmpDir],
|
|
].map((a) => a.join(':')),
|
|
},
|
|
// option docs: https://docs.docker.com/engine/api/v1.37/#operation/ContainerStart
|
|
{},
|
|
(err, data) => {
|
|
if (err) {
|
|
log('Docker run errored:', { err, data })
|
|
|
|
return this.emit('error', err)
|
|
}
|
|
|
|
log('Docker run exited:', { err, data })
|
|
this.emit('exit', data.StatusCode, null)
|
|
},
|
|
)
|
|
}
|
|
|
|
kill (): boolean {
|
|
throw new Error('.kill not implemented for DockerProcess.')
|
|
}
|
|
}
|
|
|
|
const checkBuiltBinary = async () => {
|
|
try {
|
|
await fs.stat(path.join(__dirname, '..', '..', 'cypress.zip'))
|
|
} catch (err) {
|
|
throw new Error('Expected built cypress.zip at project root. Run `yarn binary-build`, `yarn binary-package`, and `yarn binary-zip`.')
|
|
}
|
|
|
|
try {
|
|
await fs.stat(path.join(__dirname, '..', '..', 'cli/build/package.json'))
|
|
} catch (err) {
|
|
throw new Error('Expected built CLI in /cli/build. Run `yarn build` in `cli`.')
|
|
}
|
|
}
|
|
|
|
export const dockerSpawner: Spawner = async (cmd, args, env, options) => {
|
|
await checkBuiltBinary()
|
|
|
|
const projectPath = Fixtures.projectPath(options.project)
|
|
|
|
log('Running chmod 0777 on', projectPath, 'to avoid Docker permissions issues.')
|
|
await execa('chmod', `-R 0777 ${projectPath}`.split(' '))
|
|
|
|
const proc = new DockerProcess(options.dockerImage)
|
|
|
|
nock.enableNetConnect('localhost')
|
|
|
|
await proc.pull()
|
|
|
|
if (options.withBinary) {
|
|
args = [cmd, ...args]
|
|
cmd = `/cypress/system-tests/scripts/bootstrap-docker-container.sh`
|
|
} else {
|
|
throw new Error('Docker testing is only supported with built binaries (withBinary: true)')
|
|
}
|
|
|
|
env = {
|
|
...env,
|
|
TEST_PROJECT_DIR: projectPath,
|
|
REPO_DIR: '/cypress',
|
|
}
|
|
|
|
proc.run({
|
|
cmd,
|
|
args,
|
|
env,
|
|
})
|
|
|
|
return proc
|
|
}
|