From e746d54a986c91000b059a5f460d400130d9b9e1 Mon Sep 17 00:00:00 2001 From: Arnaud Buchholz Date: Mon, 20 Jan 2025 09:58:45 -0500 Subject: [PATCH] test(start): covers most scenarios --- src/start.js | 22 +++++---- src/start.spec.js | 111 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 115 insertions(+), 18 deletions(-) diff --git a/src/start.js b/src/start.js index ca80f95..0665326 100644 --- a/src/start.js +++ b/src/start.js @@ -17,14 +17,18 @@ module.exports = async function start (job) { // check if existing NPM script const packagePath = join(job.cwd, 'package.json') - const packageStat = await stat(packagePath) - if (packageStat.isFile()) { - output.debug('start', 'Found package.json in cwd') - const packageFile = JSON.parse(await readFile(packagePath, 'utf-8')) - if (packageFile.scripts[command]) { - output.debug('start', 'Found matching start script in package.json') - start = `npm run ${start}` + try { + const packageStat = await stat(packagePath) + if (packageStat.isFile()) { + output.debug('start', 'Found package.json in cwd') + const packageFile = JSON.parse(await readFile(packagePath, 'utf-8')) + if (packageFile.scripts[command]) { + output.debug('start', 'Found matching start script in package.json') + start = `npm run ${start}` + } } + } catch (e) { + output.debug('start', 'Missing or invalid package.json in cwd', e) } let childProcessExited = false @@ -33,7 +37,7 @@ module.exports = async function start (job) { cwd: job.cwd, windowsHide: true }) - childProcess.on('exit', () => { + childProcess.on('close', () => { output.debug('start', 'start command process exited') childProcessExited = true }) @@ -46,7 +50,7 @@ module.exports = async function start (job) { const begin = Date.now() // eslint-disable-next-line no-unmodified-loop-condition - while (!childProcessExited && Date.now() - begin < job.startTimeout) { + while (!childProcessExited && Date.now() - begin <= job.startTimeout) { try { const response = await fetch(url) output.debug('start', url, response.status) diff --git a/src/start.spec.js b/src/start.spec.js index 3f20489..c32ca86 100644 --- a/src/start.spec.js +++ b/src/start.spec.js @@ -1,26 +1,119 @@ const { mock: mockChildProcess } = require('child_process') -const { start } = require('./start') +const { join } = require('path') +const start = require('./start') + +jest.mock('ps-tree', () => (_, cb) => cb(null, [{ + PID: 1 +}, { + PID: 2 +}])) +jest.spyOn(process, 'kill') describe('src/start', () => { let job + let returnUrlAnswerAfter // ms + const VALID_URL = 'http://localhost/valid' + const INVALID_URL = 'http://localhost/invalid' + let startExecuted = false beforeEach(() => { - job = {} + process.kill.mockClear() + returnUrlAnswerAfter = 500 // ms + job = { + cwd: __dirname, + startTimeout: 5000, + start: 'start', + url: [VALID_URL] + } + mockChildProcess({ + api: 'exec', + scriptPath: 'start', + exec: () => { startExecuted = true }, + close: false + }) + let firstFetch + global.fetch = async (url) => { + if (firstFetch === undefined) { + firstFetch = Date.now() + throw new Error('E_CONNECT failed') + } else if (Date.now() - firstFetch < returnUrlAnswerAfter) { + throw new Error('E_CONNECT failed') + } + if (url === VALID_URL) { + return { status: 200 } + } + if (url === INVALID_URL) { + return { status: 404 } + } + } }) - it('detects equivalent script cwd\'s package.json', () => {}) + it('runs command', async () => { + await start(job) + expect(startExecuted).toStrictEqual(true) + }) - it('runs command despite cwd\'s package.json', () => {}) + it('detects equivalent script cwd\'s package.json', async () => { + job.cwd = join(__dirname, '..') + job.start = 'test' + let executed = false + mockChildProcess({ + api: 'exec', + scriptPath: 'npm', + args: ['run', 'test'], + exec: () => { executed = true }, + close: false + }) + await start(job) + expect(executed).toStrictEqual(true) + }) - it('runs command', () => {}) + it('runs command despite cwd\'s package.json', async () => { + job.cwd = join(__dirname, '..') + job.start = 'test2' + let executed = false + mockChildProcess({ + api: 'exec', + scriptPath: 'test2', + exec: () => { executed = true }, + close: false + }) + await start(job) + expect(executed).toStrictEqual(true) + }) describe('waiting for URL to be available', () => { - it('detects command termination and fails', () => {}) + it('detects command termination and fails', async () => { + job.start = 'close' + mockChildProcess({ + api: 'exec', + scriptPath: 'close', + exec: () => {}, + close: true + }) + returnUrlAnswerAfter = 5000 + job.startTimeout = 5000 + await expect(start(job)).rejects.toThrowError(/Start command failed with exit code/) + }) - it('times out after expected limit and fails', () => {}) + it('times out after expected limit and fails', async () => { + returnUrlAnswerAfter = 5000 + job.startTimeout = 500 + await expect(start(job)).rejects.toThrowError(/Timeout while waiting for/) + }) - it('succeeds when URL is available', () => {}) + it('times out after expected limit and fails (wrong URL)', async () => { + job.startTimeout = 1000 + job.url = [INVALID_URL] + await expect(start(job)).rejects.toThrowError(/Timeout while waiting for/) + }) }) - it('stops the command by killing all children processes', () => {}) + it('stops the command by killing all children processes', async () => { + const started = await start(job) + await started.stop() + expect(process.kill).toHaveBeenCalledTimes(2) + expect(process.kill).toHaveBeenCalledWith(1, 'SIGKILL') + expect(process.kill).toHaveBeenCalledWith(2, 'SIGKILL') + }) })