UNPKG

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
// @ts-check 'use strict' 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, }