Robin Malfait 516ba530f0
Setup integration tests (#5466)
* setup integration tests

* fix rgb color syntax

* ensure integration tests always exit

If for any reason the integration tests fail, then it will run forever
on CI (~2hours or something). The `--forceExit` is not ideal but it will
prevent long running processes.

* fix incorrect test

We were never properly waiting for the command to finish.

* handle AbortError properly

In CI, when an AbortController gets aborted an error is thrown
(AbortError). If we don't catch it properly then it will "leak" and the
test will fail.

* improve IO functions

* quit integration tests after 10seconds

* only test a few integrations

* test all integrations using matrix

This will cancel other builds when one fails, it will also separate the
output per integration which can be useful especially now that we are
still figuring things out.

* rename `build` to `test`

* add --verbose flag to receive output in the console

* when reading stdout or stderr, wait a certain about to ensure stability

Debouncing for 200ms means that if another message comes in within those
200ms we delay the execution of the callback.

* simplify workflow

* use terminal output instead of disk events

* cache node_modules for integrations

* empty commit, to test cache hits
2021-09-14 16:18:14 +02:00

123 lines
2.8 KiB
JavaScript

let path = require('path')
let { spawn } = require('child_process')
let resolveToolRoot = require('./resolve-tool-root')
let runningProcessess = []
afterEach(() => {
runningProcessess.splice(0).forEach((runningProcess) => runningProcess.stop())
})
function debounce(fn, ms) {
let state = { timer: undefined }
return (...args) => {
if (state.timer) clearTimeout(state.timer)
state.timer = setTimeout(() => fn(...args), ms)
}
}
module.exports = function $(command, options = {}) {
let abortController = new AbortController()
let cwd = resolveToolRoot()
let args = command.split(' ')
command = args.shift()
command = command === 'node' ? command : path.resolve(cwd, 'node_modules', '.bin', command)
let stdoutMessages = []
let stderrMessages = []
let stdoutActors = []
let stderrActors = []
function notifyNext(actors, messages) {
if (actors.length <= 0) return
let [next] = actors
for (let [idx, message] of messages.entries()) {
if (next.predicate(message)) {
messages.splice(0, idx + 1)
let actorIdx = actors.indexOf(next)
actors.splice(actorIdx, 1)
next.resolve()
break
}
}
}
let notifyNextStdoutActor = debounce(() => {
return notifyNext(stdoutActors, stdoutMessages)
}, 200)
let notifyNextStderrActor = debounce(() => {
return notifyNext(stderrActors, stderrMessages)
}, 200)
let runningProcess = new Promise((resolve, reject) => {
let child = spawn(command, args, {
...options,
env: {
...process.env,
...options.env,
},
signal: abortController.signal,
cwd,
})
let stdout = ''
let stderr = ''
let combined = ''
child.stdout.on('data', (data) => {
stdoutMessages.push(data.toString())
notifyNextStdoutActor()
stdout += data
combined += data
})
child.stderr.on('data', (data) => {
stderrMessages.push(data.toString())
notifyNextStderrActor()
stderr += data
combined += data
})
child.on('error', (err) => {
if (err.name !== 'AbortError') {
throw err
}
})
child.on('close', (code, signal) => {
;(signal === 'SIGTERM' ? resolve : code === 0 ? resolve : reject)({
code,
stdout,
stderr,
combined,
})
})
})
runningProcessess.push(runningProcess)
return Object.assign(runningProcess, {
stop() {
abortController.abort()
return runningProcess
},
onStdout(predicate) {
return new Promise((resolve) => {
stdoutActors.push({ predicate, resolve })
notifyNextStdoutActor()
})
},
onStderr(predicate) {
return new Promise((resolve) => {
stderrActors.push({ predicate, resolve })
notifyNextStderrActor()
})
},
})
}