Updated core logic and how stable is calculated.
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed

Fixed some minor bugs.
Will now no longer travel through history but instead stop at last stable version.
This commit is contained in:
Jonatan Nilsson 2022-02-18 13:31:56 +00:00
parent 57be8a144a
commit 47344c5e7a
10 changed files with 185 additions and 95 deletions

View file

@ -19,13 +19,13 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
} }
let newRedirects = (redirects || 0) + 1 let newRedirects = (redirects || 0) + 1
if (!path || typeof(path) !== 'string' || !path.startsWith('http')) { if (!path || typeof(path) !== 'string' || !path.startsWith('http')) {
return Promise.reject(new Error('URL was empty or invalid')) return Promise.reject(new Error('URL was empty or invalid' + (typeof(path) === 'string' ? ': ' + path : '')))
} }
let parsed let parsed
try { try {
parsed = new URL(path) parsed = new URL(path)
} catch { } catch {
return Promise.reject(new Error('URL was empty or invalid')) return Promise.reject(new Error('URL was empty or invalid: ' + path))
} }
let h = http let h = http

View file

@ -22,7 +22,7 @@ export default class Core {
Core.providers.set(name, provider) Core.providers.set(name, provider)
} }
constructor(db, util, log) { constructor(db, util, log, restart = function() {}) {
// some sanity checks // some sanity checks
if (!log || typeof(log) !== 'object') throw new Error('log parameter was invalid') if (!log || typeof(log) !== 'object') throw new Error('log parameter was invalid')
if (typeof(log.event) !== 'object') throw new Error('log parameter was invalid') if (typeof(log.event) !== 'object') throw new Error('log parameter was invalid')
@ -34,11 +34,13 @@ export default class Core {
|| typeof(log.event.error) !== 'function') throw new Error('log parameter was invalid') || typeof(log.event.error) !== 'function') throw new Error('log parameter was invalid')
if (!util || !(util instanceof Util)) throw new Error('util not instance of Util') if (!util || !(util instanceof Util)) throw new Error('util not instance of Util')
if (!db || !(db instanceof Low)) throw new Error('db not instance of Low') if (!db || !(db instanceof Low)) throw new Error('db not instance of Low')
if (typeof(restart) !== 'function') throw new Error('restart was not a function')
this.running = false this.running = false
this.db = db this.db = db
this.util = util this.util = util
this.log = log this.log = log
this.restart = restart
this.applications = [] this.applications = []
this.applicationMap = new Map() this.applicationMap = new Map()
this._applicationFatalCrash = null this._applicationFatalCrash = null
@ -130,12 +132,17 @@ export default class Core {
for (let i = 0; i < this.db.data.core[name].versions.length; i++) { for (let i = 0; i < this.db.data.core[name].versions.length; i++) {
let version = this.db.data.core[name].versions[i] let version = this.db.data.core[name].versions[i]
if (!version.installed || version.stable < -1) continue if (!version.installed || version.stable < -1) continue
if (version.stable < 0 && !application.fresh) continue if (version.stable < 0 && !application.fresh) {
application.ctx.log.warn(`Restarting for ${version.version} due to last run failing while not being in fresh state`)
return this.restart(`Application ${name} has fresh false while attempting to run ${version.version} with stable -1`)
}
await application.closeServer() await application.closeServer()
this._applicationFatalCrash = this.criticalError.bind(this, application, version) this._applicationFatalCrash = this.criticalError.bind(this, application, version)
process.once('exit', this._applicationFatalCrash) process.once('exit', this._applicationFatalCrash)
let wasFresh = application.fresh
try { try {
application.ctx.log.info(`Attempting to run version ${version.version}`) application.ctx.log.info(`Attempting to run version ${version.version}`)
await application.runVersion(version.version) await application.runVersion(version.version)
@ -144,13 +151,23 @@ export default class Core {
await this.db.write() await this.db.write()
break break
} catch(err) { } catch(err) {
if (application.fresh) {
version.stable = -2
} else {
version.stable = Math.min(version.stable, 0) - 1
}
await this.db.write()
application.ctx.log.error(err, `Error starting ${version.version}: ${err.message}`) application.ctx.log.error(err, `Error starting ${version.version}: ${err.message}`)
process.off('exit', this._applicationFatalCrash)
if (version.stable < 1) {
if (wasFresh) {
version.stable = -2
} else {
version.stable = -1
}
await this.db.write()
if (version.stable === -1) {
return this.restart(`Application ${name} version ${version.version} failed to start but application was dirty, check if restarting fixes it`)
}
} else {
await this.db.write()
return this.restart(`Application ${name} version ${version.version} previously stable but now failing`)
}
} finally { } finally {
process.off('exit', this._applicationFatalCrash) process.off('exit', this._applicationFatalCrash)
} }

View file

@ -25,8 +25,7 @@ export default class GitProvider {
checked++ checked++
for (let asset of item.assets) { for (let asset of item.assets) {
if (!asset.name.endsWith('-sc.7z') if (!asset.name.endsWith('-sc.7z')) continue
&& !asset.name.endsWith('-sc.zip')) continue
return { return {
version: item.name, version: item.name,

View file

@ -26,7 +26,14 @@ export async function runner(root_import_meta_url, configname = 'config.json', d
runner.log = log runner.log = log
const db = await GetDB(config, log, util.getPathFromRoot('./' + dbname)) const db = await GetDB(config, log, util.getPathFromRoot('./' + dbname))
const core = new Core(db, util, log) const core = new Core(db, util, log, function(msg) {
if (msg) {
runner.log.warn('Got a restart request: ' + msg)
} else {
runner.log.warn('Got a restart request with no message or reason')
}
process.exit(1)
})
await core.init() await core.init()
await core.run() await core.run()

View file

@ -36,6 +36,7 @@ t.timeout(10000).test('should run update and install correctly', async function(
try { try {
await app.update() await app.update()
} catch (err) { } catch (err) {
console.log()
console.log(err) console.log(err)
if (ctx.db.data.core.testapp.versions.length) { if (ctx.db.data.core.testapp.versions.length) {
console.log(ctx.db.data.core.testapp.versions[0].log) console.log(ctx.db.data.core.testapp.versions[0].log)

View file

@ -51,17 +51,20 @@ t.describe('Basics', function() {
}) })
t.test('should fail if url is invalid', async function() { t.test('should fail if url is invalid', async function() {
function checkError(err) { function checkError(check, err) {
assert.match(err.message, /invalid/i) assert.match(err.message, /invalid/i)
assert.match(err.message, /url/i) assert.match(err.message, /url/i)
if (check) {
assert.match(err.message, new RegExp(check))
}
} }
await assert.isRejected(request({}, 123)).then(checkError) await assert.isRejected(request({}, 123)).then(checkError.bind(this, null))
await assert.isRejected(request({}, [])).then(checkError) await assert.isRejected(request({}, [])).then(checkError.bind(this, null))
await assert.isRejected(request({}, {})).then(checkError) await assert.isRejected(request({}, {})).then(checkError.bind(this, null))
await assert.isRejected(request({}, '')).then(checkError) await assert.isRejected(request({}, '')).then(checkError.bind(this, null))
await assert.isRejected(request({}, 'asdf')).then(checkError) await assert.isRejected(request({}, 'asdf')).then(checkError.bind(this, 'asdf'))
await assert.isRejected(request({}, 'httpppp')).then(checkError) await assert.isRejected(request({}, 'httpppp')).then(checkError.bind(this, 'httpppp'))
}) })
}) })

View file

@ -151,12 +151,15 @@ t.timeout(10000).describe('', function() {
let logIndex = 0 let logIndex = 0
function catchupLog() { function catchupLog(ms = 0) {
if (logs.length > logIndex) { if (logs.length > logIndex) {
for (; logIndex < logs.length; logIndex++) { for (; logIndex < logs.length; logIndex++) {
prettyPrintMessage(logs[logIndex]) prettyPrintMessage(logs[logIndex])
} }
} }
if (ms > 0) {
return setTimeout(ms)
}
} }
integrationLog.on('newlog', function(record) { integrationLog.on('newlog', function(record) {
@ -195,11 +198,14 @@ t.timeout(10000).describe('', function() {
let listeningLine = null let listeningLine = null
while (processor.exitCode == null while (processor.exitCode == null
&& !hasLogLine((rec) => { listeningLine = rec; return rec.listening && rec.port })) { && !hasLogLine((rec) => { listeningLine = rec; return rec.listening && rec.port })) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
catchupLog() catchupLog()
return listeningLine if (listeningLine.listening && listeningLine.port) {
return listeningLine
} else {
return null
}
} }
async function waitUntilClosed(listening) { async function waitUntilClosed(listening) {
@ -232,8 +238,7 @@ t.timeout(10000).describe('', function() {
while (processor.exitCode == null) { while (processor.exitCode == null) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
catchupLog() catchupLog()
@ -263,8 +268,7 @@ t.timeout(10000).describe('', function() {
assert.strictEqual(checkListening.body.version, 'v1') assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/core is running/)) { while (!hasLogLine(/core is running/)) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
catchupLog() catchupLog()
@ -288,45 +292,40 @@ t.timeout(10000).describe('', function() {
await setTimeout(500) await setTimeout(500)
while (!hasLogLine(/Error starting v2_nolisten/)) { while (!hasLogLine(/Error starting v2_nolisten/)) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
while (!hasLogLine(/restart request.*v2_nolisten.*dirty/)) {
while (!hasLogLine(/Attempting to run version v1_ok/)) { await catchupLog(10)
catchupLog()
await setTimeout(10)
} }
while (processor.exitCode == null) {
while (!hasLogLine(/Server is listening on 31313 serving v1/)) { await catchupLog(10)
catchupLog()
await setTimeout(10)
} }
catchupLog() catchupLog()
checkListening = await request({}, `http://localhost:${listening.port}/`)
assert.strictEqual(checkListening.body.version, 'v1')
await setTimeout(10)
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core.testapp.active, assertNameVersion1) assert.strictEqual(db.core.testapp.active, assertNameVersion2)
assert.strictEqual(db.core.testapp.versions.length, 2) assert.strictEqual(db.core.testapp.versions.length, 2)
assert.strictEqual(db.core.testapp.versions[0].stable, -1) assert.strictEqual(db.core.testapp.versions[0].stable, -1)
assert.strictEqual(db.core.testapp.versions[0].installed, true) assert.strictEqual(db.core.testapp.versions[0].installed, true)
assert.strictEqual(db.core.testapp.versions[1].stable, 1) assert.strictEqual(db.core.testapp.versions[1].stable, 1)
assert.strictEqual(db.core.testapp.versions[1].installed, true) assert.strictEqual(db.core.testapp.versions[1].installed, true)
processor.kill() // Since application was in dirty state, on next attempt should attempt to
// Wait for ports to be marked as closed // run v2 again and then falling back to v1
await waitUntilClosed()
await waitUntilClosed()
processor = startRunner() 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() listening = await waitUntilListening()
assert.ok(listening) assert.ok(listening)
checkListening = await request({}, `http://localhost:${listening.port}/`) checkListening = await request({}, `http://localhost:${listening.port}/`)
assert.strictEqual(checkListening.body.version, 'v1') assert.strictEqual(checkListening.body.version, 'v1')
@ -381,13 +380,11 @@ t.timeout(10000).describe('', function() {
await setTimeout(500) await setTimeout(500)
while (!hasLogLine(/Attempting to run version v3_crash/)) { while (!hasLogLine(/Attempting to run version v3_crash/)) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
while (processor.exitCode == null) { while (processor.exitCode == null) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json'))) db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
@ -427,13 +424,11 @@ t.timeout(10000).describe('', function() {
await setTimeout(500) await setTimeout(500)
while (!hasLogLine(/Attempting to run version v4_stable/)) { while (!hasLogLine(/Attempting to run version v4_stable/)) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
while (!hasLogLine(/Server is listening on 31313 serving v4/)) { while (!hasLogLine(/Server is listening on 31313 serving v4/)) {
catchupLog() await catchupLog(10)
await setTimeout(10)
} }
catchupLog() catchupLog()

View file

@ -207,13 +207,36 @@ t.describe('#constructor()', function() {
}) })
}) })
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() { t.test('should accept log, util and close function', function() {
const assertLog = log const assertLog = log
const assertRestarter = function() { }
let core = new Core(db, util, assertLog) let core = new Core(db, util, assertLog, assertRestarter)
assert.strictEqual(core.db, db) assert.strictEqual(core.db, db)
assert.strictEqual(core.util, util) assert.strictEqual(core.util, util)
assert.strictEqual(core.log, assertLog) assert.strictEqual(core.log, assertLog)
assert.strictEqual(core.restart, assertRestarter)
assert.deepStrictEqual(core.applications, []) assert.deepStrictEqual(core.applications, [])
assert.ok(core.applicationMap) assert.ok(core.applicationMap)
}) })
@ -626,6 +649,7 @@ t.describe('#runApplication()', function() {
} }
} }
core = new Core(db, util, log) core = new Core(db, util, log)
core.restart = stub()
db.write = stubWrite = stub().resolves() db.write = stubWrite = stub().resolves()
log.info.reset() log.info.reset()
log.warn.reset() log.warn.reset()
@ -667,6 +691,7 @@ t.describe('#runApplication()', function() {
const assertVersion = 'v50' const assertVersion = 'v50'
const assertError = new Error('jellyfish') const assertError = new Error('jellyfish')
testApp.runVersion.rejects(assertError) testApp.runVersion.rejects(assertError)
testApp.fresh = true
db.data.core[testAppName].versions.push({ db.data.core[testAppName].versions.push({
id: assertVersion, id: assertVersion,
version: assertVersion + 'asdf', version: assertVersion + 'asdf',
@ -690,6 +715,7 @@ t.describe('#runApplication()', function() {
t.test('should skip versions that have not been installed or high error', async function() { t.test('should skip versions that have not been installed or high error', async function() {
const assertError = new Error('Daikichi to Rin') const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError) testApp.runVersion.rejects(assertError)
testApp.fresh = true
db.data.core[testAppName].versions.push({ db.data.core[testAppName].versions.push({
id: '40', id: '40',
version: 'v40', version: 'v40',
@ -721,7 +747,10 @@ t.describe('#runApplication()', function() {
t.test('should attempt to run version with stable of -1 if fresh is true', async function() { t.test('should attempt to run version with stable of -1 if fresh is true', async function() {
const assertError = new Error('Daikichi to Rin') const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError) testApp.runVersion.returnWith(function() {
testApp.fresh = false
return Promise.reject(assertError)
})
testApp.fresh = true testApp.fresh = true
db.data.core[testAppName].versions.push({ db.data.core[testAppName].versions.push({
id: '30', id: '30',
@ -740,8 +769,7 @@ t.describe('#runApplication()', function() {
stable: 0, stable: 0,
}, ) }, )
let err = await assert.isRejected(core.runApplication(testApp)) await core.runApplication(testApp)
assert.notStrictEqual(err, assertError)
assert.notOk(log.error.called) assert.notOk(log.error.called)
assert.ok(testApp.ctx.log.error.called) assert.ok(testApp.ctx.log.error.called)
@ -751,11 +779,15 @@ t.describe('#runApplication()', function() {
assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message)) 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], '31')
assert.strict(testApp.runVersion.firstCall[0], '32') 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 skip version with stable of -1 if fresh is false', async function() { t.test('should call restart if program crashes and fresh is false', async function() {
const assertError = new Error('Daikichi to Rin') const assertErrorMessage = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError) const assertError = new Error('Country Lane')
testApp.runVersion.rejects(assertErrorMessage)
core.restart.rejects(assertError)
testApp.fresh = false testApp.fresh = false
db.data.core[testAppName].versions.push({ db.data.core[testAppName].versions.push({
id: '30', id: '30',
@ -775,22 +807,22 @@ t.describe('#runApplication()', function() {
}, ) }, )
let err = await assert.isRejected(core.runApplication(testApp)) let err = await assert.isRejected(core.runApplication(testApp))
assert.notStrictEqual(err, assertError) assert.strictEqual(err, assertError)
assert.notOk(log.error.called) assert.notOk(log.error.called)
assert.ok(testApp.ctx.log.error.called) assert.ok(testApp.ctx.log.error.called)
assert.strictEqual(testApp.ctx.log.error.callCount, 2) assert.strictEqual(testApp.ctx.log.error.callCount, 1)
assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError) 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('30'))
assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message)) assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertErrorMessage.message))
assert.strictEqual(testApp.ctx.log.error.secondCall[0], assertError) assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1)
assert.match(testApp.ctx.log.error.secondCall[1], new RegExp('32')) assert.ok(core.restart.called)
assert.match(testApp.ctx.log.error.secondCall[1], new RegExp(assertError.message)) assert.match(core.restart.firstCall[0], new RegExp(testAppName))
assert.strict(testApp.runVersion.firstCall[0], '30') assert.match(core.restart.firstCall[0], /v30/)
assert.strict(testApp.runVersion.firstCall[0], '32') assert.match(core.restart.firstCall[0], /dirty/)
}) })
t.test('should change status accordingly when application is fresh', async function() { t.test('should attempt next non-tested version if fresh is true', async function() {
const assertError = new Error('Daikichi to Rin') const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError) testApp.runVersion.rejects(assertError)
testApp.fresh = true testApp.fresh = true
@ -829,9 +861,49 @@ t.describe('#runApplication()', function() {
assert.ok(stubWrite.callCount, 2) assert.ok(stubWrite.callCount, 2)
}) })
t.test('should always go to -1 minimum on crash', async function() { t.test('should exit immediately if next version is -1 and fresh is false', async function() {
const assertError = new Error('Daikichi to Rin') const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError) 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 testApp.fresh = false
db.data.core[testAppName].versions.push({ db.data.core[testAppName].versions.push({
id: '28', id: '28',
@ -856,18 +928,16 @@ t.describe('#runApplication()', function() {
}) })
let err = await assert.isRejected(core.runApplication(testApp)) let err = await assert.isRejected(core.runApplication(testApp))
assert.notStrictEqual(err, assertError) assert.strictEqual(err, assertError)
assert.notOk(log.error.called) assert.notOk(log.error.called)
assert.ok(testApp.ctx.log.error.called) assert.ok(testApp.ctx.log.error.called)
assert.strictEqual(testApp.ctx.log.error.callCount, 1)
assert.strictEqual(testApp.ctx.log.error.callCount, 3) assert.strictEqual(db.data.core[testAppName].versions[0].stable, 5)
assert.strictEqual(stubWrite.callCount, 1)
assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1) assert.ok(core.restart.called)
assert.strictEqual(db.data.core[testAppName].versions[1].stable, -1) assert.match(core.restart.firstCall[0], new RegExp(testAppName))
assert.strictEqual(db.data.core[testAppName].versions[2].stable, -1) assert.match(core.restart.firstCall[0], /v28/)
assert.strictEqual(db.data.core[testAppName].versions[3].stable, -1) assert.match(core.restart.firstCall[0], /stable/)
assert.ok(stubWrite.callCount, 2)
}) })
t.test('should throw if no stable version is found', async function() { t.test('should throw if no stable version is found', async function() {

View file

@ -44,7 +44,7 @@ t.timeout(5000).describe('#checkConfig()', function() {
}) })
t.test('should otherwise succeed', function() { t.test('should otherwise succeed', function() {
return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-manager/releases' }) return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-helloworld/releases' })
.checkConfig() .checkConfig()
}) })

View file

@ -20,7 +20,7 @@ t.describe('#getLatestVersion()', function() {
assert.strictEqual(version.log, '') assert.strictEqual(version.log, '')
}) })
t.test('should support multiple extension', async function() { t.test('should skip zip files for now', async function() {
const assertName = 'Karen' const assertName = 'Karen'
const assertLink = 'My Wings' const assertLink = 'My Wings'
const assertFilename = 'test-sc.zip' const assertFilename = 'test-sc.zip'
@ -31,11 +31,9 @@ t.describe('#getLatestVersion()', function() {
{ name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] }, { name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] },
]}) ]})
let version = await provider.getLatestVersion() let err = await assert.isRejected(provider.getLatestVersion())
assert.strictEqual(version.version, assertName) assert.match(err.message, /release/)
assert.strictEqual(version.link, assertLink) assert.match(err.message, /found/)
assert.strictEqual(version.filename, assertFilename)
assert.strictEqual(version.log, '')
}) })
t.test('should skip versions with no assets', async function() { t.test('should skip versions with no assets', async function() {