import cluster from 'cluster' import { Eltro as t, assert, stub } from 'eltro' import fs from 'fs/promises' import Core from '../core/core.mjs' import Util from '../core/util.mjs' import { createFakeLog } from './helpers.mjs' import StaticProvider from '../core/providers/static.mjs' import lowdb from '../core/db.mjs' const util = new Util(import.meta.url) const log = createFakeLog() let db t.before(function() { return lowdb({}, log, null).then(function(res) { db = res }) }) t.describe('Core.addProvider()', function() { t.beforeEach(function() { Core.providers.clear() }) t.after(function() { Core.providers.clear() }) t.test('should fail if name is not a string', function() { let tests = [ [1, 'number'], [0, 'false number'], ['', 'false string'], [[], 'array'], [{}, 'object'], ] tests.forEach(function(check) { assert.throws(function() { Core.addProvider(check[0], StaticProvider) }, function(err) { assert.match(err.message, /name/i) assert.match(err.message, /string/i) return true }, `throw if name is ${check[1]}`) }) }) t.test('should fail if provider not a function', function() { let tests = [ [1, 'number'], [0, 'false number'], ['asdf', 'string'], ['', 'false string'], [[], 'array'], [{}, 'object'], ] tests.forEach(function(check) { assert.throws(function() { Core.addProvider('insertname', check[0]) }, function(err) { assert.match(err.message, /provider/i) assert.match(err.message, /class/i) assert.match(err.message, /insertname/i) return true }, `throw if provider is ${check[1]}`) }) }) t.test('should fail if provider instance is missing checkConfig', function() { let tests = [ [1, 'number'], [0, 'false number'], ['asdf', 'string'], ['', 'false string'], [[], 'array'], [{}, 'object'], ] tests.forEach(function(check) { assert.throws(function() { let provider = function() { this.getLatestVersion = function() {}; this.checkConfig = check[0] } Core.addProvider('somename', provider) }, function(err) { assert.match(err.message, /provider/i) assert.match(err.message, /class/i) assert.match(err.message, /missing/i) assert.match(err.message, /checkConfig/i) assert.match(err.message, /somename/i) return true }, `throw if provider checkConfig is ${check[1]}`) }) }) t.test('should fail if provider instance is missing getLatestVersion', function() { let tests = [ [1, 'number'], [0, 'false number'], ['asdf', 'string'], ['', 'false string'], [[], 'array'], [{}, 'object'], ] tests.forEach(function(check) { assert.throws(function() { let provider = function() { this.checkConfig = function() {}; this.getLatestVersion = check[0] } Core.addProvider('somename', provider) }, function(err) { assert.match(err.message, /provider/i) assert.match(err.message, /class/i) assert.match(err.message, /missing/i) assert.match(err.message, /getLatestVersion/i) assert.match(err.message, /somename/i) return true }, `throw if provider getLatestVersion is ${check[1]}`) }) }) t.test('should otherwise add provider to map', function() { assert.strictEqual(Core.providers.size, 0) Core.addProvider('testnamehere', StaticProvider) assert.strictEqual(Core.providers.size, 1) assert.strictEqual(Core.providers.get('testnamehere'), StaticProvider) }) }) t.describe('#constructor()', function() { t.test('should throw if util is not util', function() { let tests = [ [1, 'number'], [0, 'false number'], ['asdf', 'string'], ['', 'false string'], [[], 'array'], [{}, 'object'], [Util, 'not instance'], ] tests.forEach(function(check) { assert.throws(function() { new Core(db, check[0], log) }, function(err) { assert.match(err.message, /util/i) assert.match(err.message, /instance/i) return true }, `throw if util is ${check[1]}`) }) }) t.test('should throw if db is not lowdb', function() { let tests = [ [1, 'number'], [0, 'false number'], ['asdf', 'string'], ['', 'false string'], [[], 'array'], [{}, 'object'], [lowdb.Low, 'not instance'], ] tests.forEach(function(check) { assert.throws(function() { new Core(check[0], util, log) }, function(err) { assert.match(err.message, /db/i) assert.match(err.message, /instance/i) return true }, `throw if db is ${check[1]}`) }) }) t.test('should throw if log is not an object with event', function() { let func = function() {} let validEvent = { info: func, warn: func, error: func } let tests = [ [1, 'number'], [0, 'false number'], [null, 'null'], [undefined, 'undefined'], ['asdf', 'string'], ['', 'false string'], [[], 'array'], [{}, 'object'], [{warn: func, event: validEvent }, 'log only warn'], [{error: func, event: validEvent }, 'log only error'], [{info: func, event: validEvent }, 'log only info'], [{warn: func, error: func, event: validEvent }, 'log only warn and error'], [{warn: func, info: func, event: validEvent }, 'log only warn and info'], [{error: func, info: func, event: validEvent }, 'log only error and info'], [{ warn: func, error: func, info: func, event: { warn: func } }, 'event only warn'], [{ warn: func, error: func, info: func, event: { error: func } }, 'event only error'], [{ warn: func, error: func, info: func, event: { info: func } }, 'event only info'], [{ warn: func, error: func, info: func, event: { warn: func, error: func } }, 'event only warn and error'], [{ warn: func, error: func, info: func, event: { warn: func, info: func } }, 'event only warn and info'], [{ warn: func, error: func, info: func, event: { error: func, info: func } }, 'event only error and info'], ] tests.forEach(function(check) { assert.throws(function() { new Core(db, util, check[0], func) }, function(err) { assert.match(err.message, /log/i) assert.match(err.message, /valid/i) return true }, `throw if log is ${check[1]}`) }) }) t.test('should throw if restart is not a function', function() { let tests = [ [1, 'number'], [0, 'false number'], ['asdf', 'string'], ['', 'false string'], [[], 'array'], [{}, 'object'], ] tests.forEach(function(check) { assert.throws(function() { new Core(db, util, log, check[0]) }, function(err) { assert.match(err.message, /restart/i) assert.match(err.message, /function/i) return true }, `throw if restart is ${check[1]}`) }) }) t.test('should accept log, util and close function', function() { const assertLog = log const assertRestarter = function() { } let core = new Core(db, util, assertLog, assertRestarter) assert.strictEqual(core.db, db) assert.strictEqual(core.util, util) assert.strictEqual(core.log, assertLog) assert.strictEqual(core.restart, assertRestarter) assert.deepStrictEqual(core.applications, []) assert.ok(core.applicationMap) assert.strictEqual(core.isSlave, false) }) }) t.describe('#getApplication()', function() { t.test('should return application based on the name', function() { const assertName = 'Yami no Naka' const assertApplication = { a: 1 } let core = new Core(db, util, log) core.applicationMap.set(assertName, assertApplication) assert.strictEqual(core.getApplication(assertName), assertApplication) }) }) t.describe('#init()', function() { const assertProviderName = 'Kyousuu Gakku Gogyou Kikan' let core let fakeUtil let fakeProvider let fakeProviderConfig function FakeProvider(config) { fakeProvider(config) this.static = true this.checkConfig = fakeProviderConfig } t.beforeEach(function() { log.error.reset() core = new Core(db, util, log) core.util = fakeUtil = { verifyConfig: stub(), getAppNames: stub().returns([]), } fakeProvider = stub() fakeProviderConfig = stub() Core.providers.set(assertProviderName, FakeProvider) }) t.after(function() { return Promise.all([ fs.rm('./log_test_1.log', { recursive: true, force: true }), ]) }) t.test('it should call util.verifyConfig correctly', async function() { const assertError = new Error('Red faction IO drive mix') const assertConfig = { a: 1 } db.config = assertConfig fakeUtil.verifyConfig.throws(assertError) let err = await assert.isRejected(core.init()) assert.strictEqual(err, assertError) assert.strictEqual(fakeUtil.verifyConfig.firstCall[0], assertConfig) }) t.test('should call util.getNames correctly', async function() { const assertError = new Error('Hero within') const assertConfig = { a: 1 } db.config = assertConfig fakeUtil.getAppNames.throws(assertError) let err = await assert.isRejected(core.init()) assert.strictEqual(err, assertError) assert.strictEqual(fakeUtil.getAppNames.firstCall[0], assertConfig) }) t.test('should call provider constructor correctly', async function() { const assertError = new Error('Funny days') const assertAppName = 'Tsuugakuro' const assertConfig = { [assertAppName]: { provider: assertProviderName, } } db.config = assertConfig fakeProvider.throws(assertError) fakeUtil.getAppNames.returns([assertAppName]) let err = await assert.isRejected(core.init()) assert.notStrictEqual(err, assertError) assert.strictEqual(log.error.firstCall[0], assertError) assert.match(err.message, /successful/i) assert.match(err.message, /none/i) assert.strictEqual(fakeProvider.firstCall[0], assertConfig[assertAppName]) }) t.test('should call provider checkConfig correctly', async function() { const assertAppName = 'Zetsubou' const assertConfig = { [assertAppName]: { provider: assertProviderName, } } db.config = assertConfig const assertError = new Error('Shousou') fakeProviderConfig.rejects(assertError) fakeUtil.getAppNames.returns([assertAppName]) let err = await assert.isRejected(core.init()) assert.strictEqual(log.error.firstCall[0], assertError) assert.match(err.message, /successful/i) assert.match(err.message, /none/i) assert.strictEqual(fakeProviderConfig.firstCall[0], assertConfig[assertAppName]) }) t.test('should create an application with the provider and name and config', async function() { const assertAppName = 'Yasashii Ketsumatsu' const assertTestString = 'Serozore no Omoi' const assertConfig = { [assertAppName]: { provider: assertProviderName, teststring: assertTestString, } } db.config = assertConfig fakeUtil.getAppNames.returns([assertAppName]) assert.strictEqual(core.applications.length, 0) await core.init() let application = core.getApplication(assertAppName) assert.ok(application) assert.strictEqual(core.applications.length, 1) assert.strictEqual(core.applications[0], application) assert.strictEqual(application.name, assertAppName) assert.strictEqual(application.ctx.db, core.db) assert.strictEqual(application.ctx.util, core.util) assert.notStrictEqual(application.ctx.log, core.log) assert.strictEqual(application.ctx.core, core) assert.strictEqual(application.config.teststring, assertTestString) assert.ok(application.fresh) assert.ok(application.provider instanceof FakeProvider) }) t.test('should continue even if one application fails', async function() { log.error.reset() const assertProviderFailName = 'Knee Socks' let stubFailCheckConfig = stub() function FakeFailProvider(config) { fakeProvider(config) this.static = true this.checkConfig = stubFailCheckConfig } Core.providers.set(assertProviderFailName, FakeFailProvider) const assertError = new Error('Justice of light') const assertPathLog = 'log_test_1.log' const assertFirstAppName = 'Kaeshite' const assertSecondAppName = 'Motteke' const assertTestFirstString = 'Magia' const assertTestSecondString = 'Snow Falling' const assertConfig = { [assertFirstAppName]: { provider: assertProviderName, teststring: assertTestFirstString, log: [ { path: assertPathLog, level: 'info' }, { stream: 'process.stdout', level: 'warn' }, ] }, [assertSecondAppName]: { provider: assertProviderFailName, teststring: assertTestSecondString, }, } stubFailCheckConfig.throws(assertError) db.config = assertConfig fakeUtil.getAppNames.returns([assertSecondAppName, assertFirstAppName]) assert.strictEqual(core.applications.length, 0) await core.init() let application = core.getApplication(assertFirstAppName) assert.ok(application) assert.strictEqual(core.applications.length, 1) assert.strictEqual(core.applications[0], application) assert.strictEqual(application.name, assertFirstAppName) assert.strictEqual(application.ctx.db, core.db) assert.strictEqual(application.ctx.util, core.util) assert.notStrictEqual(application.ctx.log, core.log) // assert.strictEqual(application.ctx.core, core) assert.strictEqual(application.config.teststring, assertTestFirstString) assert.ok(application.fresh) assert.ok(application.provider instanceof FakeProvider) assert.ok(log.error.called) assert.strictEqual(log.error.firstCall[0], assertError) assert.match(log.error.firstCall[1], new RegExp(assertProviderFailName)) assert.match(log.error.firstCall[1], new RegExp(assertSecondAppName)) assert.match(log.error.firstCall[1], new RegExp(assertError.message)) assert.ok(application.ctx.log.streams.length >= 4) assert.strictEqual(application.ctx.log.streams[0].path, assertPathLog) assert.strictEqual(application.ctx.log.streams[0].level, 30) assert.strictEqual(application.ctx.log.streams[0].type, 'file') assert.strictEqual(application.ctx.log.streams[1].stream, process.stdout) assert.strictEqual(application.ctx.log.streams[1].level, 40) assert.strictEqual(application.ctx.log.streams[1].type, 'stream') }) t.test('should listen on cluster messages if one or more are cluster on', async function() { const assertAppName1 = 'Dai Sagara Yoshiharu' const assertAppName2 = 'Kuryo' const assertCoreName = 'Ichuu' const assertPayload1 = { a: 1 } const assertPayload2 = { b: 2 } const assertConfig = { name: assertCoreName, [assertAppName1]: { provider: assertProviderName, cluster: 2, }, [assertAppName2]: { provider: assertProviderName, cluster: 1, }, } db.config = assertConfig fakeUtil.getAppNames.returns([assertAppName1, assertAppName2]) assert.strictEqual(core.applications.length, 0) await core.init() core.log.emit = stub() assert.strictEqual(core.applications.length, 2) let app1 = core.getApplication(assertAppName1) let app2 = core.getApplication(assertAppName2) app1.ctx.log.emit = stub() app2.ctx.log.emit = stub() cluster.emit('message', null, { apptarget: app1.name, type: 'newlog', payload: assertPayload1 }) assert.notOk(core.log.emit.called) assert.ok(app1.ctx.log.emit.called) assert.ok(app1.ctx.log.emit.firstCall[0], 'newlog') assert.ok(app1.ctx.log.emit.firstCall[1], assertPayload1) assert.notOk(app2.ctx.log.emit.called) app1.ctx.log.emit.reset() cluster.emit('message', null, { apptarget: app2.name, type: 'newlog', payload: assertPayload2 }) assert.notOk(core.log.emit.called) assert.notOk(app1.ctx.log.emit.called) assert.ok(app2.ctx.log.emit.called) assert.ok(app2.ctx.log.emit.firstCall[0], 'newlog') assert.ok(app2.ctx.log.emit.firstCall[1], assertPayload2) app2.ctx.log.emit.reset() let tests = [ null, undefined, 12412, 'asdfag', {}, { apptarget: 12421, type: 'newlog', payload: {}}, { apptarget: {}, type: 'newlog', payload: {}}, { apptarget: null, type: 'newlog', payload: {}}, { type: 'newlog', payload: {}}, { apptarget: app1.name, type: 12421, payload: {}}, { apptarget: app1.name, type: {}, payload: {}}, { apptarget: app1.name, type: null, payload: {}}, { apptarget: app1.name, payload: {}}, { apptarget: app1.name, type: 'newlog', payload: 12421}, { apptarget: app1.name, type: 'newlog', payload: null}, { apptarget: app1.name, type: 'newlog', payload: 'test'}, ] tests.forEach(function(test) { cluster.emit('message', null, test) assert.notOk(core.log.emit.called) assert.notOk(app1.ctx.log.emit.called) assert.notOk(app2.ctx.log.emit.called) }) cluster.emit('message', null, { apptarget: assertCoreName, type: 'newlog', payload: assertPayload1 }) assert.notOk(app1.ctx.log.emit.called) assert.notOk(app2.ctx.log.emit.called) assert.ok(core.log.emit.called) assert.ok(core.log.emit.called) assert.ok(core.log.emit.firstCall[0], 'newlog') assert.ok(core.log.emit.firstCall[1], assertPayload1) }) }) t.describe('#run()', function() { let core let testAppOneName let testAppTwoName let stubRunApplication let stubOnOrOnce let stubWrite t.beforeEach(function() { testAppOneName = 'Tenshi' testAppTwoName = 'no CLOVER' db.data.core = { [testAppOneName]: { versions: [] }, [testAppTwoName]: { versions: [] }, } core = new Core(db, util, log) core.runApplication = stubRunApplication = stub().resolves() db.write = stubWrite = stub().resolves() log.info.reset() log.warn.reset() log.error.reset() for (let name of [testAppOneName, testAppTwoName]) { let onOrOnce = stub() let app = { name: name, fresh: false, on: onOrOnce, once: onOrOnce, ctx: { log: { info: stub(), warn: stub(), error: stub() }, }, update: stub().resolves(), startAutoupdater: stub(), } core.applicationMap.set(name, app) core.applications.push(app) } }) t.test('should call update on all applications', async function() { const assertFirstError = new Error('Manatsu') const assertSecondError = new Error('no Photograph') core.applicationMap.get(testAppOneName).update.rejects(assertFirstError) core.applicationMap.get(testAppTwoName).update.rejects(assertSecondError) await core.run() assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1) assert.strictEqual(core.applications[0].ctx.log.error.firstCall[0], assertFirstError) assert.match(core.applications[0].ctx.log.error.firstCall[1], /updat/) assert.match(core.applications[0].ctx.log.error.firstCall[1], new RegExp(assertFirstError.message)) assert.strictEqual(core.applications[1].ctx.log.error.callCount, 1) assert.strictEqual(core.applications[1].ctx.log.error.firstCall[0], assertSecondError) assert.match(core.applications[1].ctx.log.error.firstCall[1], /updat/) assert.match(core.applications[1].ctx.log.error.firstCall[1], new RegExp(assertSecondError.message)) }) t.test('app.on updated should be hooked eventually and should call runApplication', async function() { assert.notOk(core.applicationMap.get(testAppOneName).once.called) assert.notOk(core.applicationMap.get(testAppTwoName).once.called) assert.notOk(core.applicationMap.get(testAppOneName).on.called) assert.notOk(core.applicationMap.get(testAppTwoName).on.called) const assertFirstError = new Error('Manatsu') const assertSecondError = new Error('no Photograph') core.applicationMap.get(testAppOneName).update.returnWith(function() { assert.notOk(core.applicationMap.get(testAppOneName).once.called) assert.notOk(core.applicationMap.get(testAppOneName).on.called) return Promise.reject(assertFirstError) }) core.applicationMap.get(testAppTwoName).update.returnWith(function() { assert.notOk(core.applicationMap.get(testAppTwoName).once.called) assert.notOk(core.applicationMap.get(testAppOneName).on.called) return Promise.reject(assertSecondError) }) core.runApplication.returnWith(function(app) { assert.notOk(app.once.called) assert.notOk(app.on.called) return Promise.resolve() }) await core.run() assert.ok(core.applicationMap.get(testAppOneName).on.called) assert.ok(core.applicationMap.get(testAppTwoName).on.called) assert.strictEqual(core.applicationMap.get(testAppOneName).on.firstCall[0], 'updated') assert.strictEqual(core.applicationMap.get(testAppTwoName).on.firstCall[0], 'updated') assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1) assert.strictEqual(core.applications[0].ctx.log.error.firstCall[0], assertFirstError) assert.match(core.applications[0].ctx.log.error.firstCall[1], /updat/) assert.match(core.applications[0].ctx.log.error.firstCall[1], new RegExp(assertFirstError.message)) assert.strictEqual(core.applications[1].ctx.log.error.callCount, 1) assert.strictEqual(core.applications[1].ctx.log.error.firstCall[0], assertSecondError) assert.match(core.applications[1].ctx.log.error.firstCall[1], /updat/) assert.match(core.applications[1].ctx.log.error.firstCall[1], new RegExp(assertSecondError.message)) assert.strictEqual(stubRunApplication.callCount, 2) assert.strictEqual(stubRunApplication.getCallN(1)[0], core.applications[0]) assert.strictEqual(stubRunApplication.getCallN(2)[0], core.applications[1]) core.runApplication.resolves() core.applications[0].on.firstCall[1]() assert.strictEqual(stubRunApplication.callCount, 3) assert.strictEqual(stubRunApplication.getCallN(3)[0], core.applications[0]) core.applications[0].on.firstCall[1]() assert.strictEqual(stubRunApplication.callCount, 4) assert.strictEqual(stubRunApplication.getCallN(4)[0], core.applications[0]) core.applications[1].on.firstCall[1]() assert.strictEqual(stubRunApplication.callCount, 5) assert.strictEqual(stubRunApplication.getCallN(5)[0], core.applications[1]) }) t.test('should call startAutoupdater on all applications', async function() { stubRunApplication.rejects(new Error('not seen')) assert.notOk(core.applications[0].startAutoupdater.called) assert.notOk(core.applications[1].startAutoupdater.called) await assert.isRejected(core.run()) assert.ok(core.applications[0].startAutoupdater.called) assert.ok(core.applications[1].startAutoupdater.called) }) t.test('should be safe to call multiple times', async function() { await core.run() assert.strictEqual(stubRunApplication.callCount, 2) await core.run() await core.run() await core.run() await core.run() assert.strictEqual(stubRunApplication.callCount, 2) }) t.test('should call runApplication on all applications', async function() { const assertFirstError = new Error('Manatsu') const assertSecondError = new Error('no Photograph') const assertNoError = new Error('unseen') stubRunApplication.onCallN(1).rejects(assertFirstError) stubRunApplication.onCallN(2).rejects(assertSecondError) stubRunApplication.onCallN(3).rejects(assertNoError) await assert.isRejected(core.run()) assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1) assert.strictEqual(core.applications[0].ctx.log.error.firstCall[0], assertFirstError) assert.match(core.applications[0].ctx.log.error.firstCall[1], /run/) assert.match(core.applications[0].ctx.log.error.firstCall[1], new RegExp(assertFirstError.message)) assert.strictEqual(core.applications[1].ctx.log.error.callCount, 1) assert.strictEqual(core.applications[1].ctx.log.error.firstCall[0], assertSecondError) assert.match(core.applications[1].ctx.log.error.firstCall[1], /run/) assert.match(core.applications[1].ctx.log.error.firstCall[1], new RegExp(assertSecondError.message)) }) t.test('should throw if all applications fail', async function() { stubRunApplication.rejects(new Error('bla')) let err = await assert.isRejected(core.run()) assert.match(err.message, /no/i) assert.match(err.message, /application/i) }) t.test('should not throw if at least one application is run', async function() { stubRunApplication.onCallN(1).rejects(new Error('Scramble')) await core.run() assert.strictEqual(core.applications[0].ctx.log.error.callCount, 1) assert.strictEqual(core.applications[1].ctx.log.error.callCount, 0) }) t.test('should not throw if all applications are running', async function() { await core.run() assert.strictEqual(log.error.callCount, 0) }) }) t.describe('#runApplication()', function() { let core let testApp let testAppName let stubWrite t.beforeEach(function() { testAppName = 'Precure' db.data.core = { [testAppName]: { versions: [] } } core = new Core(db, util, log) core.restart = stub() db.write = stubWrite = stub().resolves() log.info.reset() log.warn.reset() log.error.reset() testApp = { name: testAppName, fresh: false, ctx: { log: { info: stub(), warn: stub(), error: stub(), }, }, closeServer: stub(), runVersion: stub(), } core.applicationMap.set(testAppName, testApp) }) t.test('Should always attempt to close application first', async function() { const assertAppName = 'Pyramid Collapse' const assertError = new Error('Pyramid Collapse') const stubClose = stub().rejects(assertError) db.data.core[assertAppName] = { versions: [{installed: true, stable: 0}] } core.applicationMap.set(assertAppName, { name: assertAppName, closeServer: stubClose, }) let err = await assert.isRejected(core.runApplication(core.applicationMap.get(assertAppName))) assert.strictEqual(err, assertError) }) t.test('should select first valid version', async function() { const assertVersion = 'v50' const assertError = new Error('jellyfish') testApp.runVersion.rejects(assertError) testApp.fresh = true db.data.core[testAppName].versions.push({ id: assertVersion, version: assertVersion + 'asdf', installed: true, stable: 0, }) let err = await assert.isRejected(core.runApplication(testApp)) assert.notStrictEqual(err, assertError) assert.notOk(log.error.called) assert.ok(testApp.ctx.log.error.called) assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertVersion)) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message)) assert.strict(testApp.runVersion.firstCall[0], assertVersion) assert.match(err.message, /no/i) assert.match(err.message, /found/i) }) t.test('should skip versions that have not been installed or high error', async function() { const assertError = new Error('Daikichi to Rin') testApp.runVersion.rejects(assertError) testApp.fresh = true db.data.core[testAppName].versions.push({ id: '40', version: 'v40', installed: false, stable: 0, }, { id: '41', version: 'v41', installed: true, stable: -2, }, { id: '42', version: 'v42', installed: true, stable: 0, }, ) let err = await assert.isRejected(core.runApplication(testApp)) assert.notStrictEqual(err, assertError) assert.notOk(log.error.called) assert.ok(testApp.ctx.log.error.called) assert.strictEqual(testApp.ctx.log.error.callCount, 1) assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('42')) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message)) assert.strict(testApp.runVersion.firstCall[0], '42') }) t.test('should attempt to run version with stable of -1 if fresh is true', async function() { const assertError = new Error('Daikichi to Rin') testApp.runVersion.returnWith(function() { testApp.fresh = false return Promise.reject(assertError) }) testApp.fresh = true db.data.core[testAppName].versions.push({ id: '30', version: 'v30', installed: false, stable: 0, }, { id: '31', version: 'v31', installed: true, stable: -1, }, { id: '32', version: 'v32', installed: true, stable: 0, }, ) await core.runApplication(testApp) assert.notOk(log.error.called) assert.ok(testApp.ctx.log.error.called) assert.strictEqual(testApp.ctx.log.error.callCount, 2) assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('31')) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message)) assert.strict(testApp.runVersion.firstCall[0], '31') assert.strict(testApp.runVersion.firstCall[0], '32') assert.strictEqual(db.data.core[testAppName].versions[1].stable, -2) assert.strictEqual(db.data.core[testAppName].versions[2].stable, -1) }) t.test('should call restart if program crashes and fresh is false', async function() { const assertErrorMessage = new Error('Daikichi to Rin') const assertError = new Error('Country Lane') testApp.runVersion.rejects(assertErrorMessage) core.restart.rejects(assertError) testApp.fresh = false db.data.core[testAppName].versions.push({ id: '30', version: 'v30', installed: true, stable: 0, }, { id: '31', version: 'v31', installed: true, stable: -1, }, { id: '32', version: 'v32', installed: true, stable: 0, }, ) let err = await assert.isRejected(core.runApplication(testApp)) assert.strictEqual(err, assertError) assert.notOk(log.error.called) assert.ok(testApp.ctx.log.error.called) assert.strictEqual(testApp.ctx.log.error.callCount, 1) assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertErrorMessage) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('30')) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertErrorMessage.message)) assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1) assert.ok(core.restart.called) assert.match(core.restart.firstCall[0], new RegExp(testAppName)) assert.match(core.restart.firstCall[0], /v30/) assert.match(core.restart.firstCall[0], /dirty/) }) t.test('should attempt next non-tested version if fresh is true', async function() { const assertError = new Error('Daikichi to Rin') testApp.runVersion.rejects(assertError) testApp.fresh = true db.data.core[testAppName].versions.push({ id: '30', version: 'v30', installed: true, stable: 0, }, { id: '31', version: 'v31', installed: true, stable: -1, }, { id: '32', version: 'v32', installed: true, stable: -2, }) let err = await assert.isRejected(core.runApplication(testApp)) assert.notStrictEqual(err, assertError) assert.notOk(log.error.called) assert.ok(testApp.ctx.log.error.called) assert.strictEqual(testApp.ctx.log.error.callCount, 2) assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('30')) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message)) assert.match(testApp.ctx.log.error.secondCall[1], new RegExp('31')) assert.match(testApp.ctx.log.error.secondCall[1], new RegExp(assertError.message)) assert.strictEqual(db.data.core[testAppName].versions[0].stable, -2) assert.strictEqual(db.data.core[testAppName].versions[1].stable, -2) assert.ok(stubWrite.callCount, 2) }) t.test('should exit immediately if next version is -1 and fresh is false', async function() { const assertError = new Error('Daikichi to Rin') core.restart.rejects(assertError) testApp.fresh = false db.data.core[testAppName].versions.push({ id: '30', version: 'v30', installed: true, stable: -1, }, { id: '31', version: 'v31', installed: true, stable: 0, }, { id: '32', version: 'v32', installed: true, stable: -2, }) let err = await assert.isRejected(core.runApplication(testApp)) assert.strictEqual(err, assertError) assert.notOk(log.error.called) assert.ok(testApp.ctx.log.warn.called) assert.strictEqual(testApp.ctx.log.warn.callCount, 1) assert.match(testApp.ctx.log.warn.firstCall[0], /restart/i) assert.match(testApp.ctx.log.warn.firstCall[0], /fresh/i) assert.match(testApp.ctx.log.warn.firstCall[0], /v30/i) assert.match(core.restart.firstCall[0], /v30/i) assert.match(core.restart.firstCall[0], /fresh/i) assert.match(core.restart.firstCall[0], /-1/i) assert.match(core.restart.firstCall[0], new RegExp(testAppName)) assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1) assert.strictEqual(db.data.core[testAppName].versions[1].stable, 0) }) t.test('should stop on first stable and call core.restart if crash occurs', async function() { const assertError = new Error('Daikichi to Rin') testApp.runVersion.rejects(new Error('empty message')) core.restart.rejects(assertError) testApp.fresh = false db.data.core[testAppName].versions.push({ id: '28', version: 'v28', installed: true, stable: 5, }, { id: '29', version: 'v29', installed: true, stable: 1, }, { id: '30', version: 'v30', installed: true, stable: 0, }, { id: '31', version: 'v31', installed: true, stable: -1, }) let err = await assert.isRejected(core.runApplication(testApp)) assert.strictEqual(err, assertError) assert.notOk(log.error.called) assert.ok(testApp.ctx.log.error.called) assert.strictEqual(testApp.ctx.log.error.callCount, 1) assert.strictEqual(db.data.core[testAppName].versions[0].stable, 5) assert.strictEqual(stubWrite.callCount, 1) assert.ok(core.restart.called) assert.match(core.restart.firstCall[0], new RegExp(testAppName)) assert.match(core.restart.firstCall[0], /v28/) assert.match(core.restart.firstCall[0], /stable/) }) t.test('should throw if no stable version is found', async function() { const assertError = new Error('Daikichi to Rin') testApp.runVersion.rejects(assertError) testApp.fresh = true db.data.core[testAppName].versions.push({ id: '20', version: 'v20', installed: true, stable: 0, }, { id: '21', version: 'v21', installed: true, stable: 0, }, { id: '22', version: 'v22', installed: true, stable: 0, }) let err = await assert.isRejected(core.runApplication(testApp)) assert.notStrictEqual(err, assertError) assert.match(err.message, /found/) }) t.test('should throw if no version are found', async function() { const assertError = new Error('Daikichi to Rin') testApp.runVersion.rejects(assertError) testApp.fresh = true db.data.core[testAppName].versions = [] let err = await assert.isRejected(core.runApplication(testApp)) assert.notStrictEqual(err, assertError) assert.match(err.message, /found/) assert.notMatch(err.message, /stable/i) }) t.test('should otherwise succeed if one is found', async function() { const assertError = new Error('Daikichi to Rin') let count = 0 testApp.runVersion.returnWith(function() { if (count > 0) return Promise.reject(assertError) count++ return Promise.resolve() }) testApp.fresh = true db.data.core[testAppName].versions.push({ id: '70', version: 'v70', installed: true, stable: 100, }, { id: '71', version: 'v71', installed: true, stable: 0, }, { id: '72', version: 'v72', installed: true, stable: 0, }) await core.runApplication(testApp) assert.notOk(log.error.called) assert.notOk(testApp.ctx.log.error.called) assert.strictEqual(testApp.runVersion.callCount, 1) assert.strictEqual(testApp.runVersion.firstCall[0], 'v70') assert.strictEqual(db.data.core[testAppName].versions[0].stable, 1) assert.strictEqual(db.data.core[testAppName].versions[1].stable, 0) assert.strictEqual(db.data.core[testAppName].versions[2].stable, 0) assert.ok(stubWrite.called) }) t.test('should succeed if running a minus one on fresh', async function() { const assertError = new Error('Daikichi to Rin') let count = 0 testApp.fresh = true testApp.runVersion.returnWith(function() { if (count > 0) return Promise.reject(assertError) count++ return Promise.resolve() }) testApp.fresh = true db.data.core[testAppName].versions.push({ id: '70', version: 'v70', installed: true, stable: -1, }, { id: '71', version: 'v71', installed: true, stable: 0, }, { id: '72', version: 'v72', installed: true, stable: 0, }) await core.runApplication(testApp) assert.notOk(log.error.called) assert.notOk(testApp.ctx.log.error.called) assert.strictEqual(testApp.runVersion.callCount, 1) assert.strictEqual(testApp.runVersion.firstCall[0], 'v70') assert.strictEqual(db.data.core[testAppName].versions[0].stable, 1) assert.strictEqual(db.data.core[testAppName].versions[1].stable, 0) assert.strictEqual(db.data.core[testAppName].versions[2].stable, 0) assert.ok(stubWrite.called) }) }) t.describe('#criticalError()', function() { let core let testApp let testAppName let stubWrite t.beforeEach(function() { testAppName = 'nano.RIPE' core = new Core(db, util, log) db.writeSync = stubWrite = stub() log.info.reset() log.warn.reset() log.error.reset() testApp = { name: testAppName, fresh: false, ctx: { log: { info: stub(), warn: stub(), error: stub(), fatal: stub(), }, }, closeServer: stub(), runVersion: stub(), } }) t.test('should log to fatal', function() { const assertVersion = { version: 'Dai kirai! Aishiteru', stable: 0, } assert.notOk(testApp.ctx.log.fatal.called) core.criticalError(testApp, assertVersion) assert.ok(testApp.ctx.log.fatal.called) assert.match(testApp.ctx.log.fatal.firstCall[0], /critical/i) assert.match(testApp.ctx.log.fatal.firstCall[0], new RegExp(assertVersion.version)) }) t.test('should always change to stable -2 regardless of fresh', function() { const assertVersion = { version: 'Dai kirai! Aishiteru', stable: 0, } testApp.fresh = false assertVersion.stable = 5 core.criticalError(testApp, assertVersion) assert.strictEqual(assertVersion.stable, -2) assertVersion.stable = -1 core.criticalError(testApp, assertVersion) assert.strictEqual(assertVersion.stable, -2) testApp.fresh = true assertVersion.stable = 5 core.criticalError(testApp, assertVersion) assert.strictEqual(assertVersion.stable, -2) assertVersion.stable = -1 core.criticalError(testApp, assertVersion) assert.strictEqual(assertVersion.stable, -2) }) t.test('should call db.writeSync afterwards', function() { let checkStable = 0 const assertVersion = { version: 'Dai kirai! Aishiteru', stable: 0, } stubWrite.returnWith(function() { checkStable = assertVersion.stable }) assert.notOk(stubWrite.called) core.criticalError(testApp, assertVersion) assert.ok(stubWrite.called) assert.strictEqual(checkStable, -2) }) })