195 строки
5.1 KiB
JavaScript
195 строки
5.1 KiB
JavaScript
const cli = require('heroku-cli-util')
|
|
const io = require('socket.io-client')
|
|
const ansiEscapes = require('ansi-escapes')
|
|
const api = require('./heroku-api')
|
|
const TestRunStates = require('./test-run-states')
|
|
const wait = require('co-wait')
|
|
const TestRunStatesUtil = require('./test-run-states-util')
|
|
|
|
const SIMI = 'https://simi-production.herokuapp.com'
|
|
|
|
const { PENDING, CREATING, BUILDING, RUNNING, DEBUGGING, ERRORED, FAILED, SUCCEEDED, CANCELLED } = TestRunStates
|
|
|
|
// used to pad the status column so that the progress bars align
|
|
const maxStateLength = Math.max.apply(null, Object.keys(TestRunStates).map((k) => TestRunStates[k]))
|
|
|
|
const STATUS_ICONS = {
|
|
[PENDING]: '⋯',
|
|
[CREATING]: '⋯',
|
|
[BUILDING]: '⋯',
|
|
[RUNNING]: '⋯',
|
|
[DEBUGGING]: '⋯',
|
|
[ERRORED]: '!',
|
|
[FAILED]: '✗',
|
|
[SUCCEEDED]: '✓',
|
|
[CANCELLED]: '!'
|
|
}
|
|
|
|
const STATUS_COLORS = {
|
|
[PENDING]: 'yellow',
|
|
[CREATING]: 'yellow',
|
|
[BUILDING]: 'yellow',
|
|
[RUNNING]: 'yellow',
|
|
[DEBUGGING]: 'yellow',
|
|
[ERRORED]: 'red',
|
|
[FAILED]: 'red',
|
|
[SUCCEEDED]: 'green',
|
|
[CANCELLED]: 'yellow'
|
|
}
|
|
|
|
function statusIcon ({ status }) {
|
|
return cli.color[STATUS_COLORS[status] || 'yellow'](STATUS_ICONS[status] || '-')
|
|
}
|
|
|
|
function printLine (testRun) {
|
|
return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`
|
|
}
|
|
|
|
function limit (testRuns, count) {
|
|
return testRuns.slice(0, count)
|
|
}
|
|
|
|
function sort (testRuns) {
|
|
return testRuns.sort((a, b) => a.number < b.number ? 1 : -1)
|
|
}
|
|
|
|
function redraw (testRuns, watch, count = 15) {
|
|
const arranged = limit(sort(testRuns), count)
|
|
|
|
if (watch) {
|
|
process.stdout.write(ansiEscapes.eraseDown)
|
|
}
|
|
|
|
const rows = arranged.map((testRun) => columns(testRun, testRuns))
|
|
|
|
// this is a massive hack but I basically create a table that does not print so I can calculate its width if it were printed
|
|
let width = 0
|
|
function printLine (line) {
|
|
width = line.length
|
|
}
|
|
|
|
cli.table(rows, {
|
|
printLine: printLine,
|
|
printHeader: false
|
|
})
|
|
|
|
const printRows = arranged.map((testRun) => columns(testRun, testRuns).concat([progressBar(testRun, testRuns, width)]))
|
|
|
|
cli.table(printRows, {
|
|
printLine: console.log,
|
|
printHeader: false
|
|
})
|
|
|
|
if (watch) {
|
|
process.stdout.write(ansiEscapes.cursorUp(arranged.length))
|
|
}
|
|
}
|
|
|
|
function handleTestRunEvent (newTestRun, testRuns) {
|
|
const previousTestRun = testRuns.find(({ id }) => id === newTestRun.id)
|
|
if (previousTestRun) {
|
|
const previousTestRunIndex = testRuns.indexOf(previousTestRun)
|
|
testRuns.splice(previousTestRunIndex, 1)
|
|
}
|
|
|
|
testRuns.push(newTestRun)
|
|
|
|
return testRuns
|
|
}
|
|
|
|
function * render (pipeline, { heroku, watch, json }) {
|
|
let testRuns = yield api.testRuns(heroku, pipeline.id)
|
|
|
|
if (json) {
|
|
cli.styledJSON(testRuns)
|
|
return
|
|
}
|
|
|
|
cli.styledHeader(
|
|
`${watch ? 'Watching' : 'Showing'} latest test runs for the ${pipeline.name} pipeline`
|
|
)
|
|
|
|
if (watch) {
|
|
process.stdout.write(ansiEscapes.cursorHide)
|
|
}
|
|
|
|
redraw(testRuns, watch)
|
|
|
|
if (!watch) {
|
|
return
|
|
}
|
|
|
|
const socket = io(SIMI, { transports: ['websocket'], upgrade: false })
|
|
|
|
socket.on('connect', () => {
|
|
socket.emit('joinRoom', {
|
|
room: `pipelines/${pipeline.id}/test-runs`,
|
|
token: heroku.options.token
|
|
})
|
|
})
|
|
|
|
socket.on('create', ({ resource, data }) => {
|
|
if (resource === 'test-run') {
|
|
testRuns = handleTestRunEvent(data, testRuns)
|
|
redraw(testRuns, watch)
|
|
}
|
|
})
|
|
|
|
socket.on('update', ({ resource, data }) => {
|
|
if (resource === 'test-run') {
|
|
testRuns = handleTestRunEvent(data, testRuns)
|
|
redraw(testRuns, watch)
|
|
}
|
|
})
|
|
|
|
// refresh the table every second for progress bar updates
|
|
while (true) {
|
|
yield wait(1000)
|
|
redraw(testRuns, watch)
|
|
}
|
|
}
|
|
|
|
function timeDiff (updatedAt, createdAt) {
|
|
return (updatedAt.getTime() - createdAt.getTime()) / 1000
|
|
}
|
|
|
|
function averageTime (testRuns) {
|
|
return testRuns.map((testRun) => timeDiff(new Date(testRun.updated_at), new Date(testRun.created_at))).reduce((a, b) => a + b, 0) / testRuns.length
|
|
}
|
|
|
|
function progressBar (testRun, allTestRuns, tableWidth) {
|
|
let numBarDefault = 100
|
|
let numBars
|
|
if (process.stderr.isTTY) {
|
|
numBars = Math.min(process.stderr.getWindowSize()[0] - tableWidth, numBarDefault)
|
|
} else {
|
|
numBars = numBarDefault
|
|
}
|
|
|
|
// only include the last X runs which have finished
|
|
const numRuns = 10
|
|
const terminalRuns = allTestRuns.filter(TestRunStatesUtil.isTerminal).slice(0, numRuns)
|
|
|
|
if (TestRunStatesUtil.isTerminal(testRun) || terminalRuns.length === 0) {
|
|
return ''
|
|
}
|
|
|
|
const avg = averageTime(terminalRuns)
|
|
const testRunElapsed = timeDiff(new Date(), new Date(testRun.created_at))
|
|
const percentageComplete = Math.min(Math.floor((testRunElapsed / avg) * numBars), numBars)
|
|
return `[${'='.repeat(percentageComplete)}${' '.repeat(numBars - percentageComplete)}]`
|
|
}
|
|
|
|
function padStatus (testStatus) {
|
|
return testStatus + ' '.repeat(Math.max(0, maxStateLength - testStatus.length))
|
|
}
|
|
|
|
function columns (testRun, allTestRuns) {
|
|
return [statusIcon(testRun), testRun.number, testRun.commit_branch, testRun.commit_sha.slice(0, 7), padStatus(testRun.status)]
|
|
}
|
|
|
|
module.exports = {
|
|
render,
|
|
printLine
|
|
}
|