github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/task-group-detail-test.js (about) 1 import { currentURL, settled } from '@ember/test-helpers'; 2 import { module, test } from 'qunit'; 3 import { setupApplicationTest } from 'ember-qunit'; 4 import { setupMirage } from 'ember-cli-mirage/test-support'; 5 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 6 import { formatBytes } from 'nomad-ui/helpers/format-bytes'; 7 import TaskGroup from 'nomad-ui/tests/pages/jobs/job/task-group'; 8 import Layout from 'nomad-ui/tests/pages/layout'; 9 import pageSizeSelect from './behaviors/page-size-select'; 10 import moment from 'moment'; 11 12 let job; 13 let taskGroup; 14 let tasks; 15 let allocations; 16 let managementToken; 17 18 const sum = (total, n) => total + n; 19 20 module('Acceptance | task group detail', function(hooks) { 21 setupApplicationTest(hooks); 22 setupMirage(hooks); 23 24 hooks.beforeEach(async function() { 25 server.create('agent'); 26 server.create('node', 'forceIPv4'); 27 28 job = server.create('job', { 29 groupsCount: 2, 30 createAllocations: false, 31 }); 32 33 const taskGroups = server.db.taskGroups.where({ jobId: job.id }); 34 taskGroup = taskGroups[0]; 35 36 tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); 37 38 server.create('node', 'forceIPv4'); 39 40 allocations = server.createList('allocation', 2, { 41 jobId: job.id, 42 taskGroup: taskGroup.name, 43 clientStatus: 'running', 44 }); 45 46 // Allocations associated to a different task group on the job to 47 // assert that they aren't showing up in on this page in error. 48 server.createList('allocation', 3, { 49 jobId: job.id, 50 taskGroup: taskGroups[1].name, 51 clientStatus: 'running', 52 }); 53 54 // Set a static name to make the search test deterministic 55 server.db.allocations.forEach(alloc => { 56 alloc.name = 'aaaaa'; 57 }); 58 59 // Mark the first alloc as rescheduled 60 allocations[0].update({ 61 nextAllocation: allocations[1].id, 62 }); 63 allocations[1].update({ 64 previousAllocation: allocations[0].id, 65 }); 66 67 managementToken = server.create('token'); 68 69 window.localStorage.clear(); 70 }); 71 72 test('it passes an accessibility audit', async function(assert) { 73 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 74 await a11yAudit(assert); 75 }); 76 77 test('/jobs/:id/:task-group should list high-level metrics for the allocation', async function(assert) { 78 const totalCPU = tasks.mapBy('resources.CPU').reduce(sum, 0); 79 const totalMemory = tasks.mapBy('resources.MemoryMB').reduce(sum, 0); 80 const totalDisk = taskGroup.ephemeralDisk.SizeMB; 81 82 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 83 84 assert.equal(TaskGroup.tasksCount, `# Tasks ${tasks.length}`, '# Tasks'); 85 assert.equal( 86 TaskGroup.cpu, 87 `Reserved CPU ${totalCPU} MHz`, 88 'Aggregated CPU reservation for all tasks' 89 ); 90 assert.equal( 91 TaskGroup.mem, 92 `Reserved Memory ${totalMemory} MiB`, 93 'Aggregated Memory reservation for all tasks' 94 ); 95 assert.equal( 96 TaskGroup.disk, 97 `Reserved Disk ${totalDisk} MiB`, 98 'Aggregated Disk reservation for all tasks' 99 ); 100 101 assert.equal(document.title, `Task group ${taskGroup.name} - Job ${job.name} - Nomad`); 102 }); 103 104 test('/jobs/:id/:task-group should have breadcrumbs for job and jobs', async function(assert) { 105 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 106 107 assert.equal(Layout.breadcrumbFor('jobs.index').text, 'Jobs', 'First breadcrumb says jobs'); 108 assert.equal( 109 Layout.breadcrumbFor('jobs.job.index').text, 110 job.name, 111 'Second breadcrumb says the job name' 112 ); 113 assert.equal( 114 Layout.breadcrumbFor('jobs.job.task-group').text, 115 taskGroup.name, 116 'Third breadcrumb says the job name' 117 ); 118 }); 119 120 test('/jobs/:id/:task-group first breadcrumb should link to jobs', async function(assert) { 121 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 122 123 await Layout.breadcrumbFor('jobs.index').visit(); 124 assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs'); 125 }); 126 127 test('/jobs/:id/:task-group second breadcrumb should link to the job for the task group', async function(assert) { 128 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 129 130 await Layout.breadcrumbFor('jobs.job.index').visit(); 131 assert.equal( 132 currentURL(), 133 `/jobs/${job.id}`, 134 'Second breadcrumb links back to the job for the task group' 135 ); 136 }); 137 138 test('/jobs/:id/:task-group should list one page of allocations for the task group', async function(assert) { 139 server.createList('allocation', TaskGroup.pageSize, { 140 jobId: job.id, 141 taskGroup: taskGroup.name, 142 clientStatus: 'running', 143 }); 144 145 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 146 147 assert.ok( 148 server.db.allocations.where({ jobId: job.id }).length > TaskGroup.pageSize, 149 'There are enough allocations to invoke pagination' 150 ); 151 152 assert.equal( 153 TaskGroup.allocations.length, 154 TaskGroup.pageSize, 155 'All allocations for the task group' 156 ); 157 }); 158 159 test('each allocation should show basic information about the allocation', async function(assert) { 160 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 161 162 const allocation = allocations.sortBy('modifyIndex').reverse()[0]; 163 const allocationRow = TaskGroup.allocations.objectAt(0); 164 165 assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short id'); 166 assert.equal( 167 allocationRow.createTime, 168 moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), 169 'Allocation create time' 170 ); 171 assert.equal( 172 allocationRow.modifyTime, 173 moment(allocation.modifyTime / 1000000).fromNow(), 174 'Allocation modify time' 175 ); 176 assert.equal(allocationRow.status, allocation.clientStatus, 'Client status'); 177 assert.equal(allocationRow.jobVersion, allocation.jobVersion, 'Job Version'); 178 assert.equal( 179 allocationRow.client, 180 server.db.nodes.find(allocation.nodeId).id.split('-')[0], 181 'Node ID' 182 ); 183 assert.equal( 184 allocationRow.volume, 185 Object.keys(taskGroup.volumes).length ? 'Yes' : '', 186 'Volumes' 187 ); 188 189 await allocationRow.visitClient(); 190 191 assert.equal(currentURL(), `/clients/${allocation.nodeId}`, 'Node links to node page'); 192 }); 193 194 test('each allocation should show stats about the allocation', async function(assert) { 195 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 196 197 const allocation = allocations.sortBy('name')[0]; 198 const allocationRow = TaskGroup.allocations.objectAt(0); 199 200 const allocStats = server.db.clientAllocationStats.find(allocation.id); 201 const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); 202 203 const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); 204 const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0); 205 206 assert.equal( 207 allocationRow.cpu, 208 Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed, 209 'CPU %' 210 ); 211 212 assert.equal( 213 allocationRow.cpuTooltip, 214 `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`, 215 'Detailed CPU information is in a tooltip' 216 ); 217 218 assert.equal( 219 allocationRow.mem, 220 allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed, 221 'Memory used' 222 ); 223 224 assert.equal( 225 allocationRow.memTooltip, 226 `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`, 227 'Detailed memory information is in a tooltip' 228 ); 229 }); 230 231 test('when the allocation search has no matches, there is an empty message', async function(assert) { 232 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 233 234 await TaskGroup.search('zzzzzz'); 235 236 assert.ok(TaskGroup.isEmpty, 'Empty state is shown'); 237 assert.equal( 238 TaskGroup.emptyState.headline, 239 'No Matches', 240 'Empty state has an appropriate message' 241 ); 242 }); 243 244 test('when the allocation has reschedule events, the allocation row is denoted with an icon', async function(assert) { 245 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 246 247 const rescheduleRow = TaskGroup.allocationFor(allocations[0].id); 248 const normalRow = TaskGroup.allocationFor(allocations[1].id); 249 250 assert.ok(rescheduleRow.rescheduled, 'Reschedule row has a reschedule icon'); 251 assert.notOk(normalRow.rescheduled, 'Normal row has no reschedule icon'); 252 }); 253 254 test('/jobs/:id/:task-group should present task lifecycles', async function(assert) { 255 job = server.create('job', { 256 groupsCount: 2, 257 groupTaskCount: 3, 258 }); 259 260 const taskGroups = server.db.taskGroups.where({ jobId: job.id }); 261 taskGroup = taskGroups[0]; 262 263 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 264 265 assert.ok(TaskGroup.lifecycleChart.isPresent); 266 assert.equal(TaskGroup.lifecycleChart.title, 'Task Lifecycle Configuration'); 267 268 tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); 269 const taskNames = tasks.mapBy('name'); 270 271 // This is thoroughly tested in allocation detail tests, so this mostly checks what’s different 272 273 assert.equal(TaskGroup.lifecycleChart.tasks.length, 3); 274 275 TaskGroup.lifecycleChart.tasks.forEach(Task => { 276 assert.ok(taskNames.includes(Task.name)); 277 assert.notOk(Task.isActive); 278 assert.notOk(Task.isFinished); 279 }); 280 }); 281 282 test('when the task group depends on volumes, the volumes table is shown', async function(assert) { 283 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 284 285 assert.ok(TaskGroup.hasVolumes); 286 assert.equal(TaskGroup.volumes.length, Object.keys(taskGroup.volumes).length); 287 }); 288 289 test('when the task group does not depend on volumes, the volumes table is not shown', async function(assert) { 290 job = server.create('job', { noHostVolumes: true, shallow: true }); 291 taskGroup = server.db.taskGroups.where({ jobId: job.id })[0]; 292 293 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 294 295 assert.notOk(TaskGroup.hasVolumes); 296 }); 297 298 test('each row in the volumes table lists information about the volume', async function(assert) { 299 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 300 301 TaskGroup.volumes[0].as(volumeRow => { 302 const volume = taskGroup.volumes[volumeRow.name]; 303 assert.equal(volumeRow.name, volume.Name); 304 assert.equal(volumeRow.type, volume.Type); 305 assert.equal(volumeRow.source, volume.Source); 306 assert.equal(volumeRow.permissions, volume.ReadOnly ? 'Read' : 'Read/Write'); 307 }); 308 }); 309 310 test('the count stepper sends the appropriate POST request', async function(assert) { 311 window.localStorage.nomadTokenSecret = managementToken.secretId; 312 313 job = server.create('job', { 314 groupCount: 0, 315 createAllocations: false, 316 shallow: true, 317 noActiveDeployment: true, 318 }); 319 const scalingGroup = server.create('task-group', { 320 job, 321 name: 'scaling', 322 count: 1, 323 shallow: true, 324 withScaling: true, 325 }); 326 job.update({ taskGroupIds: [scalingGroup.id] }); 327 328 await TaskGroup.visit({ id: job.id, name: scalingGroup.name }); 329 await TaskGroup.countStepper.increment.click(); 330 await settled(); 331 332 const scaleRequest = server.pretender.handledRequests.find( 333 req => req.method === 'POST' && req.url.endsWith('/scale') 334 ); 335 const requestBody = JSON.parse(scaleRequest.requestBody); 336 assert.equal(requestBody.Target.Group, scalingGroup.name); 337 assert.equal(requestBody.Count, scalingGroup.count + 1); 338 }); 339 340 test('the count stepper is disabled when a deployment is running', async function(assert) { 341 window.localStorage.nomadTokenSecret = managementToken.secretId; 342 343 job = server.create('job', { 344 groupCount: 0, 345 createAllocations: false, 346 shallow: true, 347 activeDeployment: true, 348 }); 349 const scalingGroup = server.create('task-group', { 350 job, 351 name: 'scaling', 352 count: 1, 353 shallow: true, 354 withScaling: true, 355 }); 356 job.update({ taskGroupIds: [scalingGroup.id] }); 357 358 await TaskGroup.visit({ id: job.id, name: scalingGroup.name }); 359 360 assert.ok(TaskGroup.countStepper.input.isDisabled); 361 assert.ok(TaskGroup.countStepper.increment.isDisabled); 362 assert.ok(TaskGroup.countStepper.decrement.isDisabled); 363 }); 364 365 test('when the job for the task group is not found, an error message is shown, but the URL persists', async function(assert) { 366 await TaskGroup.visit({ id: 'not-a-real-job', name: 'not-a-real-task-group' }); 367 368 assert.equal( 369 server.pretender.handledRequests 370 .filter(request => !request.url.includes('policy')) 371 .findBy('status', 404).url, 372 '/v1/job/not-a-real-job', 373 'A request to the nonexistent job is made' 374 ); 375 assert.equal(currentURL(), '/jobs/not-a-real-job/not-a-real-task-group', 'The URL persists'); 376 assert.ok(TaskGroup.error.isPresent, 'Error message is shown'); 377 assert.equal(TaskGroup.error.title, 'Not Found', 'Error message is for 404'); 378 }); 379 380 test('when the task group is not found on the job, an error message is shown, but the URL persists', async function(assert) { 381 await TaskGroup.visit({ id: job.id, name: 'not-a-real-task-group' }); 382 383 assert.ok( 384 server.pretender.handledRequests 385 .filterBy('status', 200) 386 .mapBy('url') 387 .includes(`/v1/job/${job.id}`), 388 'A request to the job is made and succeeds' 389 ); 390 assert.equal(currentURL(), `/jobs/${job.id}/not-a-real-task-group`, 'The URL persists'); 391 assert.ok(TaskGroup.error.isPresent, 'Error message is shown'); 392 assert.equal(TaskGroup.error.title, 'Not Found', 'Error message is for 404'); 393 }); 394 395 pageSizeSelect({ 396 resourceName: 'allocation', 397 pageObject: TaskGroup, 398 pageObjectList: TaskGroup.allocations, 399 async setup() { 400 server.createList('allocation', TaskGroup.pageSize, { 401 jobId: job.id, 402 taskGroup: taskGroup.name, 403 clientStatus: 'running', 404 }); 405 406 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 407 }, 408 }); 409 410 test('when a task group has no scaling events, there is no recent scaling events section', async function(assert) { 411 const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name); 412 taskGroupScale.update({ events: [] }); 413 414 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 415 416 assert.notOk(TaskGroup.hasScaleEvents); 417 }); 418 419 test('the recent scaling events section shows all recent scaling events in reverse chronological order', async function(assert) { 420 const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name); 421 taskGroupScale.update({ 422 events: [ 423 server.create('scale-event', { error: true }), 424 server.create('scale-event', { error: true }), 425 server.create('scale-event', { error: true }), 426 server.create('scale-event', { error: true }), 427 server.create('scale-event', { count: 3, error: false }), 428 server.create('scale-event', { count: 1, error: false }), 429 ], 430 }); 431 const scaleEvents = taskGroupScale.events.models.sortBy('time').reverse(); 432 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 433 434 assert.ok(TaskGroup.hasScaleEvents); 435 assert.notOk(TaskGroup.hasScalingTimeline); 436 437 scaleEvents.forEach((scaleEvent, idx) => { 438 const ScaleEvent = TaskGroup.scaleEvents[idx]; 439 assert.equal(ScaleEvent.time, moment(scaleEvent.time / 1000000).format('MMM DD HH:mm:ss ZZ')); 440 assert.equal(ScaleEvent.message, scaleEvent.message); 441 442 if (scaleEvent.count != null) { 443 assert.equal(ScaleEvent.count, scaleEvent.count); 444 } 445 446 if (scaleEvent.error) { 447 assert.ok(ScaleEvent.error); 448 } 449 450 if (Object.keys(scaleEvent.meta).length) { 451 assert.ok(ScaleEvent.isToggleable); 452 } else { 453 assert.notOk(ScaleEvent.isToggleable); 454 } 455 }); 456 }); 457 458 test('when a task group has at least two count scaling events and the count scaling events outnumber the non-count scaling events, a timeline is shown in addition to the accordion', async function(assert) { 459 const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name); 460 taskGroupScale.update({ 461 events: [ 462 server.create('scale-event', { error: true }), 463 server.create('scale-event', { error: true }), 464 server.create('scale-event', { count: 7, error: false }), 465 server.create('scale-event', { count: 10, error: false }), 466 server.create('scale-event', { count: 2, error: false }), 467 server.create('scale-event', { count: 3, error: false }), 468 server.create('scale-event', { count: 2, error: false }), 469 server.create('scale-event', { count: 9, error: false }), 470 server.create('scale-event', { count: 1, error: false }), 471 ], 472 }); 473 const scaleEvents = taskGroupScale.events.models.sortBy('time').reverse(); 474 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 475 476 assert.ok(TaskGroup.hasScaleEvents); 477 assert.ok(TaskGroup.hasScalingTimeline); 478 479 assert.equal( 480 TaskGroup.scalingAnnotations.length, 481 scaleEvents.filter(ev => ev.count == null).length 482 ); 483 }); 484 });