github.com/hernad/nomad@v1.6.112/ui/tests/integration/components/task-log-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import { run } from '@ember/runloop'; 7 import { module, test } from 'qunit'; 8 import { setupRenderingTest } from 'ember-qunit'; 9 import { find, click, render, settled } from '@ember/test-helpers'; 10 import hbs from 'htmlbars-inline-precompile'; 11 import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; 12 import Pretender from 'pretender'; 13 import { logEncode } from '../../../mirage/data/logs'; 14 15 const HOST = '1.1.1.1:1111'; 16 const allowedConnectionTime = 100; 17 const commonProps = { 18 interval: 200, 19 allocation: { 20 id: 'alloc-1', 21 node: { 22 httpAddr: HOST, 23 }, 24 }, 25 taskState: 'task-name', 26 clientTimeout: allowedConnectionTime, 27 serverTimeout: allowedConnectionTime, 28 }; 29 30 const logHead = [logEncode(['HEAD'], 0)]; 31 const logTail = [logEncode(['TAIL'], 0)]; 32 const streamFrames = ['one\n', 'two\n', 'three\n', 'four\n', 'five\n']; 33 let streamPointer = 0; 34 let logMode = null; 35 36 module('Integration | Component | task log', function (hooks) { 37 setupRenderingTest(hooks); 38 39 hooks.beforeEach(function () { 40 const handler = ({ queryParams }) => { 41 let frames; 42 let data; 43 44 if (logMode === 'head') { 45 frames = logHead; 46 } else if (logMode === 'tail') { 47 frames = logTail; 48 } else { 49 frames = streamFrames; 50 } 51 52 if (frames === streamFrames) { 53 data = queryParams.plain 54 ? frames[streamPointer] 55 : logEncode(frames, streamPointer); 56 streamPointer++; 57 } else { 58 data = queryParams.plain 59 ? frames.join('') 60 : logEncode(frames, frames.length - 1); 61 } 62 63 return [200, {}, data]; 64 }; 65 66 this.server = new Pretender(function () { 67 this.get(`http://${HOST}/v1/client/fs/logs/:allocation_id`, handler); 68 this.get('/v1/client/fs/logs/:allocation_id', handler); 69 this.get('/v1/regions', () => [200, {}, '[]']); 70 }); 71 }); 72 73 hooks.afterEach(function () { 74 window.localStorage.clear(); 75 this.server.shutdown(); 76 streamPointer = 0; 77 logMode = null; 78 }); 79 80 test('Basic appearance', async function (assert) { 81 assert.expect(8); 82 83 run.later(run, run.cancelTimers, commonProps.interval); 84 85 this.setProperties(commonProps); 86 await render( 87 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} />` 88 ); 89 90 assert.ok(find('[data-test-log-action="stdout"]'), 'Stdout button'); 91 assert.ok(find('[data-test-log-action="stderr"]'), 'Stderr button'); 92 assert.ok(find('[data-test-log-action="head"]'), 'Head button'); 93 assert.ok(find('[data-test-log-action="tail"]'), 'Tail button'); 94 assert.ok( 95 find('[data-test-log-action="toggle-stream"]'), 96 'Stream toggle button' 97 ); 98 99 assert.ok( 100 find('[data-test-log-box].is-full-bleed.is-dark'), 101 'Body is full-bleed and dark' 102 ); 103 104 assert.ok( 105 find('pre.cli-window'), 106 'Cli is preformatted and using the cli-window component class' 107 ); 108 109 await componentA11yAudit(this.element, assert); 110 }); 111 112 test('Streaming starts on creation', async function (assert) { 113 assert.expect(3); 114 115 run.later(run, run.cancelTimers, commonProps.interval); 116 117 this.setProperties(commonProps); 118 await render( 119 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} />` 120 ); 121 122 const logUrlRegex = new RegExp( 123 `${HOST}/v1/client/fs/logs/${commonProps.allocation.id}` 124 ); 125 assert.ok( 126 this.server.handledRequests.filter((req) => logUrlRegex.test(req.url)) 127 .length, 128 'Log requests were made' 129 ); 130 131 await settled(); 132 assert.equal( 133 find('[data-test-log-cli]').textContent, 134 streamFrames[0], 135 'First chunk of streaming log is shown' 136 ); 137 138 await componentA11yAudit(this.element, assert); 139 }); 140 141 test('Clicking Head loads the log head', async function (assert) { 142 logMode = 'head'; 143 run.later(run, run.cancelTimers, commonProps.interval); 144 145 this.setProperties(commonProps); 146 await render( 147 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} />` 148 ); 149 150 click('[data-test-log-action="head"]'); 151 152 await settled(); 153 assert.ok( 154 this.server.handledRequests.find( 155 ({ queryParams: qp }) => qp.origin === 'start' && qp.offset === '0' 156 ), 157 'Log head request was made' 158 ); 159 assert.equal( 160 find('[data-test-log-cli]').textContent, 161 logHead[0], 162 'Head of the log is shown' 163 ); 164 }); 165 166 test('Clicking Tail loads the log tail', async function (assert) { 167 logMode = 'tail'; 168 run.later(run, run.cancelTimers, commonProps.interval); 169 170 this.setProperties(commonProps); 171 await render( 172 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} />` 173 ); 174 175 click('[data-test-log-action="tail"]'); 176 177 await settled(); 178 assert.ok( 179 this.server.handledRequests.find( 180 ({ queryParams: qp }) => qp.origin === 'end' 181 ), 182 'Log tail request was made' 183 ); 184 assert.equal( 185 find('[data-test-log-cli]').textContent, 186 logTail[0], 187 'Tail of the log is shown' 188 ); 189 }); 190 191 test('Clicking toggleStream starts and stops the log stream', async function (assert) { 192 assert.expect(3); 193 194 run.later(run, run.cancelTimers, commonProps.interval); 195 196 const { interval } = commonProps; 197 this.setProperties(commonProps); 198 await render( 199 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} @interval={{interval}} />` 200 ); 201 202 run.later(() => { 203 click('[data-test-log-action="toggle-stream"]'); 204 }, interval); 205 206 await settled(); 207 assert.equal( 208 find('[data-test-log-cli]').textContent, 209 streamFrames[0], 210 'First frame loaded' 211 ); 212 213 run.later(() => { 214 assert.equal( 215 find('[data-test-log-cli]').textContent, 216 streamFrames[0], 217 'Still only first frame' 218 ); 219 click('[data-test-log-action="toggle-stream"]'); 220 run.later(run, run.cancelTimers, interval * 2); 221 }, interval * 2); 222 223 await settled(); 224 assert.equal( 225 find('[data-test-log-cli]').textContent, 226 streamFrames[0] + streamFrames[0] + streamFrames[1], 227 'Now includes second frame' 228 ); 229 }); 230 231 test('Clicking stderr switches the log to standard error', async function (assert) { 232 run.later(run, run.cancelTimers, commonProps.interval); 233 234 this.setProperties(commonProps); 235 await render( 236 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} />` 237 ); 238 239 click('[data-test-log-action="stderr"]'); 240 run.later(run, run.cancelTimers, commonProps.interval); 241 242 await settled(); 243 assert.ok( 244 this.server.handledRequests.filter( 245 (req) => req.queryParams.type === 'stderr' 246 ).length, 247 'stderr log requests were made' 248 ); 249 }); 250 251 test('Clicking stderr/stdout mode buttons does nothing when the mode remains the same', async function (assert) { 252 const { interval } = commonProps; 253 254 run.later(() => { 255 click('[data-test-log-action="stdout"]'); 256 run.later(run, run.cancelTimers, interval * 6); 257 }, interval * 2); 258 259 this.setProperties(commonProps); 260 await render( 261 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} />` 262 ); 263 264 assert.equal( 265 find('[data-test-log-cli]').textContent, 266 streamFrames[0] + streamFrames[0] + streamFrames[1], 267 'Now includes second frame' 268 ); 269 }); 270 271 test('When the client is inaccessible, task-log falls back to requesting logs through the server', async function (assert) { 272 run.later(run, run.cancelTimers, allowedConnectionTime * 2); 273 274 // override client response to timeout 275 this.server.get( 276 `http://${HOST}/v1/client/fs/logs/:allocation_id`, 277 () => [400, {}, ''], 278 allowedConnectionTime * 2 279 ); 280 281 this.setProperties(commonProps); 282 await render(hbs`<TaskLog 283 @allocation={{allocation}} 284 @task={{taskState}} 285 @clientTimeout={{clientTimeout}} 286 @serverTimeout={{serverTimeout}} />`); 287 288 const clientUrlRegex = new RegExp( 289 `${HOST}/v1/client/fs/logs/${commonProps.allocation.id}` 290 ); 291 assert.ok( 292 this.server.handledRequests.filter((req) => clientUrlRegex.test(req.url)) 293 .length, 294 'Log request was initially made directly to the client' 295 ); 296 297 await settled(); 298 const serverUrl = `/v1/client/fs/logs/${commonProps.allocation.id}`; 299 assert.ok( 300 this.server.handledRequests.filter((req) => req.url.startsWith(serverUrl)) 301 .length, 302 'Log request was later made to the server' 303 ); 304 305 assert.ok( 306 this.server.handledRequests.filter((req) => 307 clientUrlRegex.test(req.url) 308 )[0].aborted, 309 'Client log request was aborted' 310 ); 311 }); 312 313 test('When both the client and the server are inaccessible, an error message is shown', async function (assert) { 314 assert.expect(5); 315 316 run.later(run, run.cancelTimers, allowedConnectionTime * 5); 317 318 // override client and server responses to timeout 319 this.server.get( 320 `http://${HOST}/v1/client/fs/logs/:allocation_id`, 321 () => [400, {}, ''], 322 allowedConnectionTime * 2 323 ); 324 this.server.get( 325 '/v1/client/fs/logs/:allocation_id', 326 () => [400, {}, ''], 327 allowedConnectionTime * 2 328 ); 329 330 this.setProperties(commonProps); 331 await render(hbs`<TaskLog 332 @allocation={{allocation}} 333 @task={{taskState}} 334 @clientTimeout={{clientTimeout}} 335 @serverTimeout={{serverTimeout}} />`); 336 337 const clientUrlRegex = new RegExp( 338 `${HOST}/v1/client/fs/logs/${commonProps.allocation.id}` 339 ); 340 assert.ok( 341 this.server.handledRequests.filter((req) => clientUrlRegex.test(req.url)) 342 .length, 343 'Log request was initially made directly to the client' 344 ); 345 const serverUrl = `/v1/client/fs/logs/${commonProps.allocation.id}`; 346 assert.ok( 347 this.server.handledRequests.filter((req) => req.url.startsWith(serverUrl)) 348 .length, 349 'Log request was later made to the server' 350 ); 351 assert.ok( 352 find('[data-test-connection-error]'), 353 'An error message is shown' 354 ); 355 356 await click('[data-test-connection-error-dismiss]'); 357 assert.notOk( 358 find('[data-test-connection-error]'), 359 'The error message is dismissable' 360 ); 361 362 await componentA11yAudit(this.element, assert); 363 }); 364 365 test('When the client is inaccessible, the server is accessible, and stderr is pressed before the client timeout occurs, the no connection error is not shown', async function (assert) { 366 // override client response to timeout 367 this.server.get( 368 `http://${HOST}/v1/client/fs/logs/:allocation_id`, 369 () => [400, {}, ''], 370 allowedConnectionTime * 2 371 ); 372 373 // Click stderr before the client request responds 374 run.later(() => { 375 click('[data-test-log-action="stderr"]'); 376 run.later(run, run.cancelTimers, commonProps.interval * 5); 377 }, allowedConnectionTime / 2); 378 379 this.setProperties(commonProps); 380 await render(hbs`<TaskLog 381 @allocation={{allocation}} 382 @task={{taskState}} 383 @clientTimeout={{clientTimeout}} 384 @serverTimeout={{serverTimeout}} />`); 385 386 const clientUrlRegex = new RegExp( 387 `${HOST}/v1/client/fs/logs/${commonProps.allocation.id}` 388 ); 389 const clientRequests = this.server.handledRequests.filter((req) => 390 clientUrlRegex.test(req.url) 391 ); 392 assert.ok( 393 clientRequests.find((req) => req.queryParams.type === 'stdout'), 394 'Client request for stdout' 395 ); 396 assert.ok( 397 clientRequests.find((req) => req.queryParams.type === 'stderr'), 398 'Client request for stderr' 399 ); 400 401 const serverUrl = `/v1/client/fs/logs/${commonProps.allocation.id}`; 402 assert.ok( 403 this.server.handledRequests 404 .filter((req) => req.url.startsWith(serverUrl)) 405 .find((req) => req.queryParams.type === 'stderr'), 406 'Server request for stderr' 407 ); 408 409 assert.notOk( 410 find('[data-test-connection-error]'), 411 'An error message is not shown' 412 ); 413 }); 414 415 test('The log streaming mode is persisted in localStorage', async function (assert) { 416 window.localStorage.nomadLogMode = JSON.stringify('stderr'); 417 418 run.later(run, run.cancelTimers, commonProps.interval); 419 420 this.setProperties(commonProps); 421 await render( 422 hbs`<TaskLog @allocation={{allocation}} @task={{taskState}} />` 423 ); 424 425 assert.ok( 426 this.server.handledRequests.filter( 427 (req) => req.queryParams.type === 'stderr' 428 ).length 429 ); 430 assert.notOk( 431 this.server.handledRequests.filter( 432 (req) => req.queryParams.type === 'stdout' 433 ).length 434 ); 435 436 click('[data-test-log-action="stdout"]'); 437 run.later(run, run.cancelTimers, commonProps.interval); 438 439 await settled(); 440 assert.ok( 441 this.server.handledRequests.filter( 442 (req) => req.queryParams.type === 'stdout' 443 ).length 444 ); 445 assert.equal(window.localStorage.nomadLogMode, JSON.stringify('stdout')); 446 }); 447 });