UNPKG

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