UNPKG

5.04 kBJavaScriptView Raw
1const cli = require('heroku-cli-util')
2const io = require('socket.io-client')
3const ansiEscapes = require('ansi-escapes')
4const api = require('./heroku-api')
5const TestRunStates = require('./test-run-states')
6const wait = require('co-wait')
7const TestRunStatesUtil = require('./test-run-states-util')
8
9const SIMI = 'https://simi-production.herokuapp.com'
10
11const { PENDING, CREATING, BUILDING, RUNNING, ERRORED, FAILED, SUCCEEDED } = TestRunStates
12
13// used to pad the status column so that the progress bars align
14const maxStateLength = Math.max.apply(null, Object.keys(TestRunStates).map((k) => TestRunStates[k]))
15
16const STATUS_ICONS = {
17 [PENDING]: '⋯',
18 [CREATING]: '⋯',
19 [BUILDING]: '⋯',
20 [RUNNING]: '⋯',
21 [ERRORED]: '!',
22 [FAILED]: '✗',
23 [SUCCEEDED]: '✓'
24}
25
26const STATUS_COLORS = {
27 [PENDING]: 'yellow',
28 [CREATING]: 'yellow',
29 [BUILDING]: 'yellow',
30 [RUNNING]: 'yellow',
31 [ERRORED]: 'red',
32 [FAILED]: 'red',
33 [SUCCEEDED]: 'green'
34}
35
36function statusIcon ({ status }) {
37 return cli.color[STATUS_COLORS[status]](STATUS_ICONS[status])
38}
39
40function printLine (testRun) {
41 return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`
42}
43
44function limit (testRuns, count) {
45 return testRuns.slice(0, count)
46}
47
48function sort (testRuns) {
49 return testRuns.sort((a, b) => a.number < b.number ? 1 : -1)
50}
51
52function redraw (testRuns, watch, count = 15) {
53 const arranged = limit(sort(testRuns), count)
54
55 if (watch) {
56 process.stdout.write(ansiEscapes.eraseDown)
57 }
58
59 const rows = arranged.map((testRun) => columns(testRun, testRuns))
60
61 // 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
62 let width = 0
63 function printLine (line) {
64 width = line.length
65 }
66
67 cli.table(rows, {
68 printLine: printLine,
69 printHeader: false
70 })
71
72 const printRows = arranged.map((testRun) => columns(testRun, testRuns).concat([progressBar(testRun, testRuns, width)]))
73
74 cli.table(printRows, {
75 printLine: console.log,
76 printHeader: false
77 })
78
79 if (watch) {
80 process.stdout.write(ansiEscapes.cursorUp(arranged.length))
81 }
82}
83
84function handleTestRunEvent (newTestRun, testRuns) {
85 const previousTestRun = testRuns.find(({ id }) => id === newTestRun.id)
86 if (previousTestRun) {
87 const previousTestRunIndex = testRuns.indexOf(previousTestRun)
88 testRuns.splice(previousTestRunIndex, 1)
89 }
90
91 testRuns.push(newTestRun)
92
93 return testRuns
94}
95
96function * render (pipeline, { heroku, watch }) {
97 cli.styledHeader(
98 `${watch ? 'Watching' : 'Showing'} latest test runs for the ${pipeline.name} pipeline`
99 )
100
101 let testRuns = yield api.testRuns(heroku, pipeline.id)
102
103 if (watch) {
104 process.stdout.write(ansiEscapes.cursorHide)
105 }
106
107 redraw(testRuns, watch)
108
109 if (!watch) {
110 return
111 }
112
113 const socket = io(SIMI, { transports: ['websocket'], upgrade: false })
114
115 socket.on('connect', () => {
116 socket.emit('joinRoom', {
117 room: `pipelines/${pipeline.id}/test-runs`,
118 token: heroku.options.token
119 })
120 })
121
122 socket.on('create', ({ resource, data }) => {
123 if (resource === 'test-run') {
124 testRuns = handleTestRunEvent(data, testRuns)
125 redraw(testRuns, watch)
126 }
127 })
128
129 socket.on('update', ({ resource, data }) => {
130 if (resource === 'test-run') {
131 testRuns = handleTestRunEvent(data, testRuns)
132 redraw(testRuns, watch)
133 }
134 })
135
136 // refresh the table every second for progress bar updates
137 while (true) {
138 yield wait(1000)
139 redraw(testRuns, watch)
140 }
141}
142
143function timeDiff (updatedAt, createdAt) {
144 return (updatedAt.getTime() - createdAt.getTime()) / 1000
145}
146
147function averageTime (testRuns) {
148 return testRuns.map((testRun) => timeDiff(new Date(testRun.updated_at), new Date(testRun.created_at))).reduce((a, b) => a + b, 0) / testRuns.length
149}
150
151function progressBar (testRun, allTestRuns, tableWidth) {
152 let numBarDefault = 100
153 let numBars
154 if (process.stderr.isTTY) {
155 numBars = Math.min(process.stderr.getWindowSize()[0] - tableWidth, numBarDefault)
156 } else {
157 numBars = numBarDefault
158 }
159
160 // only include the last X runs which have finished
161 const numRuns = 10
162 const terminalRuns = allTestRuns.filter(TestRunStatesUtil.isTerminal).slice(0, numRuns)
163
164 if (TestRunStatesUtil.isTerminal(testRun) || terminalRuns.length === 0) {
165 return ''
166 }
167
168 const avg = averageTime(terminalRuns)
169 const testRunElapsed = timeDiff(new Date(), new Date(testRun.created_at))
170 const percentageComplete = Math.min(Math.floor((testRunElapsed / avg) * numBars), numBars)
171 return `[${'='.repeat(percentageComplete)}${' '.repeat(numBars - percentageComplete)}]`
172}
173
174function padStatus (testStatus) {
175 return testStatus + ' '.repeat(Math.max(0, maxStateLength - testStatus.length))
176}
177
178function columns (testRun, allTestRuns) {
179 return [statusIcon(testRun), testRun.number, testRun.commit_branch, testRun.commit_sha.slice(0, 7), padStatus(testRun.status)]
180}
181
182module.exports = {
183 render,
184 printLine
185}