import { Eltro as t, assert} from 'eltro' import fs from 'fs/promises' import HttpServer from '../core/http.mjs' import Util from '../core/util.mjs' import { request } from '../core/client.mjs' import { setTimeout } from 'timers/promises' import { prettyPrintMessage } from './helpers.mjs' import { pipeline } from 'stream' import getLog from '../core/log.mjs' const util = new Util(import.meta.url) const port = 61412 const turnDebuggingOn = false const runners = [ ['runner.mjs', 'testapp'], ['runner_cluster.mjs', 'testappcluster'], ] runners.forEach(function([runnerName, appname]) { t.timeout(10000).describe(runnerName, function() { let wasSuccessful = false let http = null let server = null let prefix = `http://localhost:${port}/` let files = [util.getPathFromRoot('./testappcluster')] let logs = [] let allLogs = [] let versions = [] let processor let integrationLog = getLog('test.integration', []) let compressorPath = util.getPathFromRoot('./7za.exe') if (process.platform !== 'win32') { compressorPath = util.getPathFromRoot('./7zas') } t.before(function() { http = new HttpServer() server = http.createServer(function(req, res) { req.on('error', function(err) { integrationLog.error(err, 'error') }) res.on('error', function(err) { integrationLog.error(err, 'error') }) integrationLog.info('[SERVER] got request ' + req.url) if (req.url === '/releases') { res.statusCode = 200 let output = versions.map(x => { return { name: x[0], body: x[1], assets: [{ name: x[2], browser_download_url: prefix + 'files/' + x[2] }] } }) res.end(JSON.stringify(output)); return } else if (req.url.startsWith('/files')) { let filename = req.url.substring(req.url.lastIndexOf('/') + 1) return fs.open(util.getPathFromRoot('./' + filename)) .then(function(file) { pipeline(file.createReadStream(), res, function(err) { if (err) { console.log(err) res.statusCode = 404 res.end(JSON.stringify({ error: 'unknown url' })) } }) }).catch(function(err) { console.log(err) res.statusCode = 404 res.end(JSON.stringify({ error: 'unknown url' })) }) } res.statusCode = 404 res.end(JSON.stringify({ error: 'unknown url' })) }) return fs.rm(util.getPathFromRoot('./db.json'), { force: true }) .then(function() { return server.listenAsync(port) }) }) t.after(function() { if (!turnDebuggingOn && !wasSuccessful) { for (let i = 0; i < allLogs.length; i++) { prettyPrintMessage(allLogs[i]) } } return Promise.all(files.map(function(file) { return fs.rm(file, { force: true, recursive: true }) })) .then(function() { if (processor && !processor.exitCode) { processor.kill() return waitUntilClosed() } }).then(function() { return http.closeServer() }) }) const version_1_stable = ` export function start(http, port, ctx) { const server = http.createServer(function (req, res) { res.writeHead(200); res.end(JSON.stringify({ version: 'v1' })) }) return server.listenAsync(port, '0.0.0.0') .then(() => { ctx.log.info({ port: port, listening: true }, \`Server is listening on \${port} serving v1\`) }) } ` const version_2_nolisten = ` export function start(http, port, ctx) { } ` const version_3_crashing = ` export function start(http, port, ctx) { process.exit(1) } ` const version_4_stable = ` export function start(http, port, ctx) { const server = http.createServer(function (req, res) { res.writeHead(200); res.end(JSON.stringify({ version: 'v4' })) }) return server.listenAsync(port, '0.0.0.0') .then(() => { ctx.log.info({ port: port, listening: true }, \`Server is listening on \${port} serving v4\`) }) } ` function file(relative) { let file = util.getPathFromRoot(relative) files.push(file) return file } function log(message) { let lines = message.split('\n') for (let line of lines) { if (!line.trim()) continue logs.push(line) allLogs.push(line) } } function parseLine(line) { if (line[0] === '{') { try { return JSON.parse(line) } catch {} } return { msg: line } } let logIndex = 0 function catchupLog(ms = 0) { if (logs.length > logIndex) { for (; logIndex < logs.length; logIndex++) { if (turnDebuggingOn) { prettyPrintMessage(logs[logIndex]) } } } if (ms > 0) { return setTimeout(ms) } } async function safeTry(func) { let lastException = null for (let i = 0; i < 3; i++) { if (i > 0) { allLogs.push('[safeTry] Failed with error ' + lastException.message + ', trying agian') await setTimeout(500) } try { await func() return } catch (err) { lastException = err } } throw lastException } integrationLog.on('newlog', function(record) { allLogs.push(JSON.stringify(record)) if (turnDebuggingOn) { prettyPrintMessage(JSON.stringify(record)) } }) let logWaitIndex = 0 function hasLogLine(regMatch) { if (logs.length > logWaitIndex) { for (; logWaitIndex < logs.length; logWaitIndex++) { if (typeof(regMatch) === 'function') { let res = regMatch(parseLine(logs[logWaitIndex])) if (res) return true } else if (logs[logWaitIndex].match(regMatch)) { return true } } } return false } function findInLogs(regMatch) { for (let i = 0; i < logs.length; i++) { if (typeof(regMatch) === 'function') { let res = regMatch(parseLine(logs[i])) if (res) return true } else if (logs[i].match(regMatch)) { return true } } } async function waitUntilListening() { let listeningLine = null while (processor.exitCode == null && !hasLogLine((rec) => { listeningLine = rec; return rec.listening && rec.port })) { await catchupLog(10) } catchupLog() if (listeningLine.listening && listeningLine.port) { return listeningLine } else { return null } } async function sendRequestToApplication(listening) { let lastErr = null for (let i = 0; i < 4; i++) { try { let checkListening = await request({}, `http://localhost:${listening.port}/`) return checkListening } catch (err) { lastErr = err integrationLog.info(`Request http://localhost:${listening.port}/ failed with ${err.message} trying again in 250ms`) await setTimeout(250) } } log('-- core.test.integration.mjs crash here --') log(lastErr.toString()) throw lastErr } async function waitUntilClosed(listening) { while (true) { catchupLog() try { await request({}, `http://localhost:${listening.port}/`) } catch (err) { break } await setTimeout(25) } catchupLog() logs.splice(0, logs.length); logIndex = 0; logWaitIndex = 0; if (turnDebuggingOn) { console.log('\n-------\n') } } function startRunner() { return util.runCommandBackground('node', [runnerName], util.getPathFromRoot('./'), log) } t.test('should be fully operational', async function() { let db; console.log() if (!turnDebuggingOn) { console.log('Running empty test') } let index = file('./index.mjs') await fs.writeFile(index, version_1_stable) await util.runCommand(compressorPath, ['a', file('./v1-sc.7z'), index], util.getPathFromRoot('./testapp')) processor = startRunner() while (processor.exitCode == null) { await catchupLog(10) } catchupLog() let secondLast = parseLine(logs[logs.length - 2]) let last = parseLine(logs[logs.length - 1]) assert.match(secondLast.msg, /No/i) assert.match(secondLast.msg, /versions/i) assert.match(secondLast.msg, /found/i) assert.match(last.msg, /starting/i) assert.match(last.msg, /runner/i) assert.match(last.err.message, /stable/i) assert.match(last.err.message, /application/i) // Reset our log logs.splice(0, logs.length); logIndex = 0; logWaitIndex = 0; if (turnDebuggingOn) { console.log('\n-------\n') } const assertNameVersion1 = 'v1_ok' if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion1} test`) } file(`./testapp/${assertNameVersion1}`) versions.splice(0, 0, [assertNameVersion1, 'ok version', 'v1-sc.7z']) processor = startRunner() let listening = await waitUntilListening() let checkListening = await sendRequestToApplication(listening) assert.strictEqual(checkListening.body.version, 'v1') while (!hasLogLine(/is up and running/)) { await setTimeout(10) if (processor.exitCode !== null) { throw new Error('Process exited with ' + processor.exitCode) } } await setTimeout(50) catchupLog() await safeTry(async function() { db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) assert.strictEqual(db.core[appname].active, assertNameVersion1) assert.strictEqual(db.core[appname].versions.length, 1) assert.strictEqual(db.core[appname].versions[0].stable, 1) assert.strictEqual(db.core[appname].versions[0].installed, true) }) // Create our second version await fs.writeFile(index, version_2_nolisten) await util.runCommand(compressorPath, ['a', file('./v2-sc.7z'), index], util.getPathFromRoot('./testapp')) const assertNameVersion2 = 'v2_nolisten' if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion2} test`) } file(`./testapp/${assertNameVersion2}`) versions.splice(0, 0, [assertNameVersion2, 'no listen version', 'v2-sc.7z']) // wait a second for it to trigger an update await setTimeout(500) while (!hasLogLine(/Error starting v2_nolisten/)) { await catchupLog(10) } if (appname !== 'testappcluster') { while (!hasLogLine(/restart.*v2_nolisten.*dirty/)) { await catchupLog(10) } while (processor.exitCode == null) { await catchupLog(10) } catchupLog() await safeTry(async function() { db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) assert.strictEqual(db.core[appname].active, assertNameVersion2) assert.strictEqual(db.core[appname].versions.length, 2) assert.strictEqual(db.core[appname].versions[0].stable, -1) assert.strictEqual(db.core[appname].versions[0].installed, true) assert.strictEqual(db.core[appname].versions[1].stable, 1) assert.strictEqual(db.core[appname].versions[1].installed, true) }) // Since application was in dirty state, on next attempt should attempt to // run v2 again and then falling back to v1 await waitUntilClosed() if (!turnDebuggingOn) { console.log(`Running fresh ${assertNameVersion2} test`) } processor = startRunner() await catchupLog(10) while (!hasLogLine(/Attempting to run version v2_nolisten/)) { await catchupLog(10) } } while (!hasLogLine(/Attempting to run version v1_ok/)) { await catchupLog(10) } listening = await waitUntilListening() assert.ok(listening) checkListening = await sendRequestToApplication(listening) assert.strictEqual(checkListening.body.version, 'v1') while (!hasLogLine(/is up and running/)) { await setTimeout(10) } await safeTry(async function() { db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) assert.strictEqual(db.core[appname].active, assertNameVersion1) assert.strictEqual(db.core[appname].versions.length, 2) assert.strictEqual(db.core[appname].versions[0].stable, -2) assert.strictEqual(db.core[appname].versions[1].stable, 1) }) assert.ok(findInLogs(/Attempting to run version v2_nolisten/)) assert.ok(findInLogs(/Error starting v2_nolisten/)) processor.kill() if (!turnDebuggingOn) { console.log(`Running version stability check test`) } await waitUntilClosed() processor = startRunner() listening = await waitUntilListening() assert.ok(listening) checkListening = await sendRequestToApplication(listening) assert.strictEqual(checkListening.body.version, 'v1') while (!hasLogLine(/is up and running/)) { await setTimeout(10) } await safeTry(async function() { db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) assert.strictEqual(db.core[appname].active, assertNameVersion1) assert.strictEqual(db.core[appname].versions.length, 2) assert.strictEqual(db.core[appname].versions[0].stable, -2) assert.strictEqual(db.core[appname].versions[1].stable, 1) }) assert.notOk(findInLogs(/Attempting to run version v2_nolisten/)) assert.notOk(findInLogs(/Error starting v2_nolisten/)) // Create our third version await fs.writeFile(index, version_3_crashing) await util.runCommand(compressorPath, ['a', file('./v3-sc.7z'), index], util.getPathFromRoot('./testapp')) const assertNameVersion3 = 'v3_crash' if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion3} test`) } file(`./testapp/${assertNameVersion3}`) versions.splice(0, 0, [assertNameVersion3, 'crash version', 'v3-sc.7z']) // wait a second for it to trigger an update await setTimeout(500) while (!hasLogLine(/Attempting to run version v3_crash/)) { await catchupLog(10) } if (appname !== 'testappcluster') { while (processor.exitCode == null) { await catchupLog(10) } await safeTry(async function() { db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) assert.strictEqual(db.core[appname].active, assertNameVersion3) assert.strictEqual(db.core[appname].versions.length, 3) assert.strictEqual(db.core[appname].versions[0].stable, -2) assert.strictEqual(db.core[appname].versions[1].stable, -2) assert.strictEqual(db.core[appname].versions[2].stable, 1) }) catchupLog() // Should recover afterwards await waitUntilClosed() processor = startRunner() listening = await waitUntilListening() assert.ok(listening) checkListening = await sendRequestToApplication(listening) assert.strictEqual(checkListening.body.version, 'v1') while (!hasLogLine(/core is running/)) { await setTimeout(10) } } else { while (!hasLogLine(/Attempting to run version v1_ok/)) { await catchupLog(10) } while (!hasLogLine(/is up and running/)) { await setTimeout(10) } await safeTry(async function() { db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) assert.strictEqual(db.core[appname].active, assertNameVersion1) assert.strictEqual(db.core[appname].versions.length, 3) assert.strictEqual(db.core[appname].versions[0].stable, -2) assert.strictEqual(db.core[appname].versions[1].stable, -2) assert.strictEqual(db.core[appname].versions[2].stable, 1) }) } // Create our fourth version await fs.writeFile(index, version_4_stable) await util.runCommand(compressorPath, ['a', file('./v4-sc.7z'), index], util.getPathFromRoot('./testapp')) const assertNameVersion4 = 'v4_stable' if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion4} test`) } file(`./testapp/${assertNameVersion4}`) versions.splice(0, 0, [assertNameVersion4, 'no listen version', 'v4-sc.7z']) // wait a second for it to trigger an update await setTimeout(500) while (!hasLogLine(/Attempting to run version v4_stable/)) { await catchupLog(10) } while (!hasLogLine(/Server is listening on 31313 serving v4/)) { await catchupLog(10) } while (!hasLogLine(/is up and running/)) { await setTimeout(10) } catchupLog() checkListening = await sendRequestToApplication(listening) assert.strictEqual(checkListening.body.version, 'v4') await setTimeout(10) await safeTry(async function() { db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) assert.strictEqual(db.core[appname].active, assertNameVersion4) assert.strictEqual(db.core[appname].versions.length, 4) assert.strictEqual(db.core[appname].versions[0].stable, 1) assert.strictEqual(db.core[appname].versions[1].stable, -2) }) if (appname === 'testappcluster') { let foundCore = false let foundWorker = false for (let line of allLogs) { if (line.startsWith('[FROMWORKERCORE] test-runner-cluster')) { foundCore = true } else if (line.startsWith('[FROMWORKERAPP] testappcluster-1')) { foundWorker = true } if (foundCore && foundWorker) { break } } assert.ok(foundCore) assert.ok(foundWorker) } wasSuccessful = true }) }) })