1 | const cli = require('heroku-cli-util')
|
2 | const io = require('socket.io-client')
|
3 | const ansiEscapes = require('ansi-escapes')
|
4 | const api = require('./heroku-api')
|
5 | const TestRunStates = require('./test-run-states')
|
6 | const wait = require('co-wait')
|
7 | const TestRunStatesUtil = require('./test-run-states-util')
|
8 |
|
9 | const SIMI = 'https://simi-production.herokuapp.com'
|
10 |
|
11 | const { PENDING, CREATING, BUILDING, RUNNING, DEBUGGING, ERRORED, FAILED, SUCCEEDED, CANCELLED } = TestRunStates
|
12 |
|
13 |
|
14 | const maxStateLength = Math.max.apply(null, Object.keys(TestRunStates).map((k) => TestRunStates[k]))
|
15 |
|
16 | const STATUS_ICONS = {
|
17 | [PENDING]: '⋯',
|
18 | [CREATING]: '⋯',
|
19 | [BUILDING]: '⋯',
|
20 | [RUNNING]: '⋯',
|
21 | [DEBUGGING]: '⋯',
|
22 | [ERRORED]: '!',
|
23 | [FAILED]: '✗',
|
24 | [SUCCEEDED]: '✓',
|
25 | [CANCELLED]: '!'
|
26 | }
|
27 |
|
28 | const 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 |
|
40 | function statusIcon ({ status }) {
|
41 | return cli.color[STATUS_COLORS[status] || 'yellow'](STATUS_ICONS[status] || '-')
|
42 | }
|
43 |
|
44 | function printLine (testRun) {
|
45 | return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`
|
46 | }
|
47 |
|
48 | function limit (testRuns, count) {
|
49 | return testRuns.slice(0, count)
|
50 | }
|
51 |
|
52 | function sort (testRuns) {
|
53 | return testRuns.sort((a, b) => a.number < b.number ? 1 : -1)
|
54 | }
|
55 |
|
56 | function 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 |
|
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 |
|
88 | function 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 |
|
100 | function * 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 |
|
146 | while (true) {
|
147 | yield wait(1000)
|
148 | redraw(testRuns, watch)
|
149 | }
|
150 | }
|
151 |
|
152 | function timeDiff (updatedAt, createdAt) {
|
153 | return (updatedAt.getTime() - createdAt.getTime()) / 1000
|
154 | }
|
155 |
|
156 | function 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 |
|
160 | function 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 |
|
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 |
|
183 | function padStatus (testStatus) {
|
184 | return testStatus + ' '.repeat(Math.max(0, maxStateLength - testStatus.length))
|
185 | }
|
186 |
|
187 | function columns (testRun, allTestRuns) {
|
188 | return [statusIcon(testRun), testRun.number, testRun.commit_branch, testRun.commit_sha.slice(0, 7), padStatus(testRun.status)]
|
189 | }
|
190 |
|
191 | module.exports = {
|
192 | render,
|
193 | printLine
|
194 | }
|