start-server-and-test
Version:
Starts server, waits for URL, then runs test command; when the tests end, shuts down server
201 lines (175 loc) • 5.42 kB
JavaScript
// @ts-check
const la = require('lazy-ass')
const is = require('check-more-types')
const execa = require('execa')
const waitOn = require('wait-on')
const Promise = require('bluebird')
const kill = require('tree-kill')
const debug = require('debug')('start-server-and-test')
/**
* Used for timeout (ms)
*/
const fiveMinutes = 5 * 60 * 1000
const twoSeconds = 2000
const waitOnTimeout = process.env.WAIT_ON_TIMEOUT
? Number(process.env.WAIT_ON_TIMEOUT)
: fiveMinutes
const waitOnInterval = process.env.WAIT_ON_INTERVAL
? Number(process.env.WAIT_ON_INTERVAL)
: twoSeconds
const isDebug = () =>
process.env.DEBUG &&
process.env.DEBUG.indexOf('start-server-and-test') !== -1
const isInsecure = () => process.env.START_SERVER_AND_TEST_INSECURE
function waitAndRun({ start, url, runFn, namedArguments }) {
la(is.unemptyString(start), 'missing start script name', start)
la(is.fn(runFn), 'missing test script name', runFn)
la(
is.unemptyString(url) || is.unemptyArray(url),
'missing url to wait on',
url,
)
const isSuccessfulHttpCode = (status) =>
(status >= 200 && status < 300) || status === 304
const validateStatus = namedArguments.expect
? (status) => status === namedArguments.expect
: isSuccessfulHttpCode
debug(
'starting server with command "%s", verbose mode?',
start,
isDebug(),
)
const server = execa(start, {
shell: true,
stdio: ['ignore', 'inherit', 'inherit'],
})
let serverStopped
function stopServer() {
debug('stopping server and child processes')
if (!serverStopped) {
serverStopped = true
return Promise.fromNode((cb) =>
kill(server.pid, 'SIGINT', cb),
).catch((err) => {
const message = `${err?.message || ''}\n${err?.stdout || ''}\n${err?.stderr || ''}`
const alreadyExited =
// Unix system returns ESRCH when the process is already gone
err?.code === 'ESRCH' ||
// Windows: "ERROR: The process "<pid>" not found."
/ERROR:\s*The process\s+.+\s+not found\./i.test(message) ||
// Windows: "Reason: There is no running instance of the task."
/Reason:\s*There is no running instance of the task\./i.test(
message,
) ||
// Windows: "FEHLER: Der Prozess "<pid>" wurde nicht gefunden."
/FEHLER:\s*Der Prozess\s+.+\s+wurde nicht gefunden\./i.test(
message,
)
if (!alreadyExited) {
throw err
}
debug('process already exited')
})
}
}
const waited = new Promise((resolve, reject) => {
const onClose = () => {
reject(new Error('server closed unexpectedly'))
}
server.on('close', onClose)
debug('starting waitOn %s', url)
let proxy
if (namedArguments.proxyHost) {
if (!namedArguments.proxyPort) {
throw new Error('Proxy host provided but no port provided')
}
proxy = {
host: namedArguments.proxyHost,
port: namedArguments.proxyPort,
protocol: namedArguments.proxyProtocol,
}
if (namedArguments.proxyUser) {
if (typeof namedArguments.proxyPassword !== 'string') {
throw new Error(
'Proxy username provided but no password provided',
)
}
proxy.auth = {
username: namedArguments.proxyUser,
password: namedArguments.proxyPassword,
}
}
}
const options = {
resources: Array.isArray(url) ? url : [url],
interval: waitOnInterval,
window: 1000,
timeout: waitOnTimeout,
verbose: isDebug(),
strictSSL: !isInsecure(),
log: isDebug(),
headers: {
Accept: 'text/html, application/json, text/plain, */*',
},
validateStatus,
proxy,
}
debug('wait-on options %o', options)
waitOn(options, (err) => {
if (err) {
debug('error waiting for url', url)
debug(err.message)
return reject(err)
}
debug('waitOn finished successfully')
server.removeListener('close', onClose)
resolve()
})
})
return waited.tapCatch(stopServer).then(runFn).finally(stopServer)
}
const runTheTests = (testCommand) => () => {
debug('running test script command: %s', testCommand)
return execa(testCommand, { shell: true, stdio: 'inherit' })
}
/**
* Starts a single service and runs tests or recursively
* runs a service, then goes to the next list, until it reaches 1 service and runs test.
*/
function startAndTest({ services, test, namedArguments }) {
if (services.length === 0) {
throw new Error('Got zero services to start ...')
}
la(
is.number(namedArguments.expect),
'expected status should be a number',
namedArguments.expect,
)
if (services.length === 1) {
const runTests = runTheTests(test)
debug('single service "%s" to run and test', services[0].start)
return waitAndRun({
start: services[0].start,
url: services[0].url,
namedArguments,
runFn: runTests,
})
}
return waitAndRun({
start: services[0].start,
url: services[0].url,
namedArguments,
runFn: () => {
debug('previous service started, now going to the next one')
return startAndTest({
services: services.slice(1),
test,
namedArguments,
})
},
})
}
module.exports = {
startAndTest,
}