github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/ui/tests/acceptance/client-detail-test.js (about) 1 import { currentURL } from '@ember/test-helpers'; 2 import { assign } from '@ember/polyfills'; 3 import { module, test } from 'qunit'; 4 import { setupApplicationTest } from 'ember-qunit'; 5 import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; 6 import { formatBytes } from 'nomad-ui/helpers/format-bytes'; 7 import formatDuration from 'nomad-ui/utils/format-duration'; 8 import moment from 'moment'; 9 import ClientDetail from 'nomad-ui/tests/pages/clients/detail'; 10 import Clients from 'nomad-ui/tests/pages/clients/list'; 11 import Jobs from 'nomad-ui/tests/pages/jobs/list'; 12 13 let node; 14 15 const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation; 16 17 module('Acceptance | client detail', function(hooks) { 18 setupApplicationTest(hooks); 19 setupMirage(hooks); 20 21 hooks.beforeEach(function() { 22 server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); 23 node = server.db.nodes[0]; 24 25 // Related models 26 server.create('agent'); 27 server.create('job', { createAllocations: false }); 28 server.createList('allocation', 3); 29 server.create('allocation', 'preempted'); 30 31 // Force all allocations into the running state so now allocation rows are missing 32 // CPU/Mem runtime metrics 33 server.schema.allocations.all().models.forEach(allocation => { 34 allocation.update({ clientStatus: 'running' }); 35 }); 36 }); 37 38 test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) { 39 await ClientDetail.visit({ id: node.id }); 40 41 assert.equal( 42 ClientDetail.breadcrumbFor('clients.index').text, 43 'Clients', 44 'First breadcrumb says clients' 45 ); 46 assert.equal( 47 ClientDetail.breadcrumbFor('clients.client').text, 48 node.id.split('-')[0], 49 'Second breadcrumb says the node short id' 50 ); 51 await ClientDetail.breadcrumbFor('clients.index').visit(); 52 assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients'); 53 }); 54 55 test('/clients/:id should list immediate details for the node in the title', async function(assert) { 56 await ClientDetail.visit({ id: node.id }); 57 58 assert.ok(ClientDetail.title.includes(node.name), 'Title includes name'); 59 assert.ok(ClientDetail.title.includes(node.id), 'Title includes id'); 60 assert.equal( 61 ClientDetail.statusLight.objectAt(0).id, 62 node.status, 63 'Title includes status light' 64 ); 65 }); 66 67 test('/clients/:id should list additional detail for the node below the title', async function(assert) { 68 await ClientDetail.visit({ id: node.id }); 69 70 assert.ok( 71 ClientDetail.statusDefinition.includes(node.status), 72 'Status is in additional details' 73 ); 74 assert.ok( 75 ClientDetail.statusDecorationClass.includes(`node-${node.status}`), 76 'Status is decorated with a status class' 77 ); 78 assert.ok( 79 ClientDetail.addressDefinition.includes(node.httpAddr), 80 'Address is in additional details' 81 ); 82 assert.ok( 83 ClientDetail.drainingDefinition.includes(node.drain + ''), 84 'Drain status is in additional details' 85 ); 86 assert.ok( 87 ClientDetail.eligibilityDefinition.includes(node.schedulingEligibility), 88 'Scheduling eligibility is in additional details' 89 ); 90 assert.ok( 91 ClientDetail.datacenterDefinition.includes(node.datacenter), 92 'Datacenter is in additional details' 93 ); 94 }); 95 96 test('/clients/:id should include resource utilization graphs', async function(assert) { 97 await ClientDetail.visit({ id: node.id }); 98 99 assert.equal(ClientDetail.resourceCharts.length, 2, 'Two resource utilization graphs'); 100 assert.equal(ClientDetail.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU'); 101 assert.equal(ClientDetail.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory'); 102 }); 103 104 test('/clients/:id should list all allocations on the node', async function(assert) { 105 const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length; 106 107 await ClientDetail.visit({ id: node.id }); 108 109 assert.equal( 110 ClientDetail.allocations.length, 111 allocationsCount, 112 `Allocations table lists all ${allocationsCount} associated allocations` 113 ); 114 }); 115 116 test('each allocation should have high-level details for the allocation', async function(assert) { 117 const allocation = server.db.allocations 118 .where({ nodeId: node.id }) 119 .sortBy('modifyIndex') 120 .reverse()[0]; 121 122 const allocStats = server.db.clientAllocationStats.find(allocation.id); 123 const taskGroup = server.db.taskGroups.findBy({ 124 name: allocation.taskGroup, 125 jobId: allocation.jobId, 126 }); 127 128 const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); 129 const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0); 130 const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0); 131 132 await ClientDetail.visit({ id: node.id }); 133 134 const allocationRow = ClientDetail.allocations.objectAt(0); 135 136 assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID'); 137 assert.equal( 138 allocationRow.createTime, 139 moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), 140 'Allocation create time' 141 ); 142 assert.equal( 143 allocationRow.modifyTime, 144 moment(allocation.modifyTime / 1000000).fromNow(), 145 'Allocation modify time' 146 ); 147 assert.equal(allocationRow.status, allocation.clientStatus, 'Client status'); 148 assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name'); 149 assert.ok(allocationRow.taskGroup, 'Task group name'); 150 assert.ok(allocationRow.jobVersion, 'Job Version'); 151 assert.equal( 152 allocationRow.cpu, 153 Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed, 154 'CPU %' 155 ); 156 assert.equal( 157 allocationRow.cpuTooltip, 158 `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`, 159 'Detailed CPU information is in a tooltip' 160 ); 161 assert.equal( 162 allocationRow.mem, 163 allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed, 164 'Memory used' 165 ); 166 assert.equal( 167 allocationRow.memTooltip, 168 `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`, 169 'Detailed memory information is in a tooltip' 170 ); 171 }); 172 173 test('each allocation should show job information even if the job is incomplete and already in the store', async function(assert) { 174 // First, visit clients to load the allocations for each visible node. 175 // Don't load the job belongsTo of the allocation! Leave it unfulfilled. 176 177 await Clients.visit(); 178 179 // Then, visit jobs to load all jobs, which should implicitly fulfill 180 // the job belongsTo of each allocation pointed at each job. 181 182 await Jobs.visit(); 183 184 // Finally, visit a node to assert that the job name and task group name are 185 // present. This will require reloading the job, since task groups aren't a 186 // part of the jobs list response. 187 188 await ClientDetail.visit({ id: node.id }); 189 190 const allocationRow = ClientDetail.allocations.objectAt(0); 191 const allocation = server.db.allocations 192 .where({ nodeId: node.id }) 193 .sortBy('modifyIndex') 194 .reverse()[0]; 195 196 assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name'); 197 assert.ok(allocationRow.taskGroup.includes(allocation.taskGroup), 'Task group name'); 198 }); 199 200 test('each allocation should link to the allocation detail page', async function(assert) { 201 const allocation = server.db.allocations 202 .where({ nodeId: node.id }) 203 .sortBy('modifyIndex') 204 .reverse()[0]; 205 206 await ClientDetail.visit({ id: node.id }); 207 await ClientDetail.allocations.objectAt(0).visit(); 208 209 assert.equal( 210 currentURL(), 211 `/allocations/${allocation.id}`, 212 'Allocation rows link to allocation detail pages' 213 ); 214 }); 215 216 test('each allocation should link to the job the allocation belongs to', async function(assert) { 217 await ClientDetail.visit({ id: node.id }); 218 219 const allocation = server.db.allocations.where({ nodeId: node.id })[0]; 220 const job = server.db.jobs.find(allocation.jobId); 221 222 await ClientDetail.allocations.objectAt(0).visitJob(); 223 224 assert.equal( 225 currentURL(), 226 `/jobs/${job.id}`, 227 'Allocation rows link to the job detail page for the allocation' 228 ); 229 }); 230 231 test('the allocation section should show the count of preempted allocations on the client', async function(assert) { 232 const allocations = server.db.allocations.where({ nodeId: node.id }); 233 234 await ClientDetail.visit({ id: node.id }); 235 236 assert.equal( 237 ClientDetail.allocationFilter.allCount, 238 allocations.length, 239 'All filter/badge shows all allocations count' 240 ); 241 assert.ok( 242 ClientDetail.allocationFilter.preemptionsCount.startsWith( 243 allocations.filter(wasPreemptedFilter).length 244 ), 245 'Preemptions filter/badge shows preempted allocations count' 246 ); 247 }); 248 249 test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) { 250 const allocations = server.db.allocations.where({ nodeId: node.id }); 251 252 await ClientDetail.visit({ id: node.id }); 253 await ClientDetail.allocationFilter.preemptions(); 254 255 assert.equal( 256 ClientDetail.allocations.length, 257 allocations.filter(wasPreemptedFilter).length, 258 'Only preempted allocations are shown' 259 ); 260 assert.equal( 261 currentURL(), 262 `/clients/${node.id}?preemptions=true`, 263 'Filter is persisted in the URL' 264 ); 265 }); 266 267 test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) { 268 const allocations = server.db.allocations.where({ nodeId: node.id }); 269 270 await ClientDetail.visit({ id: node.id }); 271 await ClientDetail.allocationFilter.preemptions(); 272 await ClientDetail.allocationFilter.all(); 273 274 assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown'); 275 assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL'); 276 }); 277 278 test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) { 279 const allocations = server.db.allocations.where({ nodeId: node.id }); 280 281 await ClientDetail.visit({ id: node.id, preemptions: true }); 282 283 assert.equal( 284 ClientDetail.allocations.length, 285 allocations.filter(wasPreemptedFilter).length, 286 'Only preempted allocations are shown' 287 ); 288 }); 289 290 test('/clients/:id should list all attributes for the node', async function(assert) { 291 await ClientDetail.visit({ id: node.id }); 292 293 assert.ok(ClientDetail.attributesTable, 'Attributes table is on the page'); 294 }); 295 296 test('/clients/:id lists all meta attributes', async function(assert) { 297 node = server.create('node', 'forceIPv4', 'withMeta'); 298 299 await ClientDetail.visit({ id: node.id }); 300 301 assert.ok(ClientDetail.metaTable, 'Meta attributes table is on the page'); 302 assert.notOk(ClientDetail.emptyMetaMessage, 'Meta attributes is not empty'); 303 304 const firstMetaKey = Object.keys(node.meta)[0]; 305 const firstMetaAttribute = ClientDetail.metaAttributes.objectAt(0); 306 assert.equal( 307 firstMetaAttribute.key, 308 firstMetaKey, 309 'Meta attributes for the node are bound to the attributes table' 310 ); 311 assert.equal( 312 firstMetaAttribute.value, 313 node.meta[firstMetaKey], 314 'Meta attributes for the node are bound to the attributes table' 315 ); 316 }); 317 318 test('/clients/:id shows an empty message when there is no meta data', async function(assert) { 319 await ClientDetail.visit({ id: node.id }); 320 321 assert.notOk(ClientDetail.metaTable, 'Meta attributes table is not on the page'); 322 assert.ok(ClientDetail.emptyMetaMessage, 'Meta attributes is empty'); 323 }); 324 325 test('when the node is not found, an error message is shown, but the URL persists', async function(assert) { 326 await ClientDetail.visit({ id: 'not-a-real-node' }); 327 328 assert.equal( 329 server.pretender.handledRequests.findBy('status', 404).url, 330 '/v1/node/not-a-real-node', 331 'A request to the nonexistent node is made' 332 ); 333 assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists'); 334 assert.ok(ClientDetail.error.isShown, 'Error message is shown'); 335 assert.equal(ClientDetail.error.title, 'Not Found', 'Error message is for 404'); 336 }); 337 338 test('/clients/:id shows the recent events list', async function(assert) { 339 await ClientDetail.visit({ id: node.id }); 340 341 assert.ok(ClientDetail.hasEvents, 'Client events section exists'); 342 }); 343 344 test('each node event shows basic node event information', async function(assert) { 345 const event = server.db.nodeEvents 346 .where({ nodeId: node.id }) 347 .sortBy('time') 348 .reverse()[0]; 349 350 await ClientDetail.visit({ id: node.id }); 351 352 const eventRow = ClientDetail.events.objectAt(0); 353 assert.equal( 354 eventRow.time, 355 moment(event.time).format("MMM DD, 'YY HH:mm:ss ZZ"), 356 'Event timestamp' 357 ); 358 assert.equal(eventRow.subsystem, event.subsystem, 'Event subsystem'); 359 assert.equal(eventRow.message, event.message, 'Event message'); 360 }); 361 362 test('/clients/:id shows the driver status of every driver for the node', async function(assert) { 363 // Set the drivers up so health and detection is well tested 364 const nodeDrivers = node.drivers; 365 const undetectedDriver = 'raw_exec'; 366 367 Object.values(nodeDrivers).forEach(driver => { 368 driver.Detected = true; 369 }); 370 371 nodeDrivers[undetectedDriver].Detected = false; 372 node.drivers = nodeDrivers; 373 374 const drivers = Object.keys(node.drivers) 375 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 376 .sortBy('Name'); 377 378 assert.ok(drivers.length > 0, 'Node has drivers'); 379 380 await ClientDetail.visit({ id: node.id }); 381 382 drivers.forEach((driver, index) => { 383 const driverHead = ClientDetail.driverHeads.objectAt(index); 384 385 assert.equal(driverHead.name, driver.Name, `${driver.Name}: Name is correct`); 386 assert.equal( 387 driverHead.detected, 388 driver.Detected ? 'Yes' : 'No', 389 `${driver.Name}: Detection is correct` 390 ); 391 assert.equal( 392 driverHead.lastUpdated, 393 moment(driver.UpdateTime).fromNow(), 394 `${driver.Name}: Last updated shows time since now` 395 ); 396 397 if (driver.Name === undetectedDriver) { 398 assert.notOk( 399 driverHead.healthIsShown, 400 `${driver.Name}: No health for the undetected driver` 401 ); 402 } else { 403 assert.equal( 404 driverHead.health, 405 driver.Healthy ? 'Healthy' : 'Unhealthy', 406 `${driver.Name}: Health is correct` 407 ); 408 assert.ok( 409 driverHead.healthClass.includes(driver.Healthy ? 'running' : 'failed'), 410 `${driver.Name}: Swatch with correct class is shown` 411 ); 412 } 413 }); 414 }); 415 416 test('each driver can be opened to see a message and attributes', async function(assert) { 417 // Only detected drivers can be expanded 418 const nodeDrivers = node.drivers; 419 Object.values(nodeDrivers).forEach(driver => { 420 driver.Detected = true; 421 }); 422 node.drivers = nodeDrivers; 423 424 const driver = Object.keys(node.drivers) 425 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 426 .sortBy('Name')[0]; 427 428 await ClientDetail.visit({ id: node.id }); 429 const driverHead = ClientDetail.driverHeads.objectAt(0); 430 const driverBody = ClientDetail.driverBodies.objectAt(0); 431 432 assert.notOk(driverBody.descriptionIsShown, 'Driver health description is not shown'); 433 assert.notOk(driverBody.attributesAreShown, 'Driver attributes section is not shown'); 434 435 await driverHead.toggle(); 436 assert.equal( 437 driverBody.description, 438 driver.HealthDescription, 439 'Driver health description is now shown' 440 ); 441 assert.ok(driverBody.attributesAreShown, 'Driver attributes section is now shown'); 442 }); 443 444 test('the status light indicates when the node is ineligible for scheduling', async function(assert) { 445 node = server.create('node', { 446 schedulingEligibility: 'ineligible', 447 }); 448 449 await ClientDetail.visit({ id: node.id }); 450 451 assert.equal( 452 ClientDetail.statusLight.objectAt(0).id, 453 'ineligible', 454 'Title status light is in the ineligible state' 455 ); 456 }); 457 458 test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', async function(assert) { 459 const deadline = 5400000000000; // 1.5 hours in nanoseconds 460 const forceDeadline = moment().add(1, 'd'); 461 462 node = server.create('node', { 463 drain: true, 464 schedulingEligibility: 'ineligible', 465 drainStrategy: { 466 Deadline: deadline, 467 ForceDeadline: forceDeadline.toISOString(), 468 IgnoreSystemJobs: false, 469 }, 470 }); 471 472 await ClientDetail.visit({ id: node.id }); 473 474 assert.ok( 475 ClientDetail.drain.deadline.includes(formatDuration(deadline)), 476 'Deadline is shown in a human formatted way' 477 ); 478 479 assert.ok( 480 ClientDetail.drain.forcedDeadline.includes(forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ")), 481 'Force deadline is shown as an absolute date' 482 ); 483 484 assert.ok( 485 ClientDetail.drain.forcedDeadline.includes(forceDeadline.fromNow()), 486 'Force deadline is shown as a relative date' 487 ); 488 489 assert.ok( 490 ClientDetail.drain.ignoreSystemJobs.endsWith('No'), 491 'Ignore System Jobs state is shown' 492 ); 493 }); 494 495 test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', async function(assert) { 496 const deadline = 0; 497 498 node = server.create('node', { 499 drain: true, 500 schedulingEligibility: 'ineligible', 501 drainStrategy: { 502 Deadline: deadline, 503 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 504 IgnoreSystemJobs: true, 505 }, 506 }); 507 508 await ClientDetail.visit({ id: node.id }); 509 510 assert.ok( 511 ClientDetail.drain.deadline.includes('No deadline'), 512 'The value for Deadline is "no deadline"' 513 ); 514 515 assert.notOk( 516 ClientDetail.drain.hasForcedDeadline, 517 'Forced deadline is not shown since there is no forced deadline' 518 ); 519 520 assert.ok( 521 ClientDetail.drain.ignoreSystemJobs.endsWith('Yes'), 522 'Ignore System Jobs state is shown' 523 ); 524 }); 525 526 test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', async function(assert) { 527 const deadline = -1; 528 529 node = server.create('node', { 530 drain: true, 531 schedulingEligibility: 'ineligible', 532 drainStrategy: { 533 Deadline: deadline, 534 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 535 IgnoreSystemJobs: false, 536 }, 537 }); 538 539 await ClientDetail.visit({ id: node.id }); 540 541 assert.equal(ClientDetail.drain.badgeLabel, 'Forced Drain', 'Forced Drain badge is described'); 542 assert.ok(ClientDetail.drain.badgeIsDangerous, 'Forced Drain is shown in a red badge'); 543 544 assert.notOk( 545 ClientDetail.drain.hasForcedDeadline, 546 'Forced deadline is not shown since there is no forced deadline' 547 ); 548 549 assert.ok( 550 ClientDetail.drain.ignoreSystemJobs.endsWith('No'), 551 'Ignore System Jobs state is shown' 552 ); 553 }); 554 }); 555 556 module('Acceptance | client detail (multi-namespace)', function(hooks) { 557 setupApplicationTest(hooks); 558 setupMirage(hooks); 559 560 hooks.beforeEach(function() { 561 server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); 562 node = server.db.nodes[0]; 563 564 // Related models 565 server.create('namespace'); 566 server.create('namespace', { id: 'other-namespace' }); 567 568 server.create('agent'); 569 570 // Make a job for each namespace, but have both scheduled on the same node 571 server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false }); 572 server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' }); 573 574 server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false }); 575 server.createList('allocation', 3, { 576 nodeId: node.id, 577 jobId: 'job-2', 578 clientStatus: 'running', 579 }); 580 }); 581 582 test('when the node has allocations on different namespaces, the associated jobs are fetched correctly', async function(assert) { 583 window.localStorage.nomadActiveNamespace = 'other-namespace'; 584 585 await ClientDetail.visit({ id: node.id }); 586 587 assert.equal( 588 ClientDetail.allocations.length, 589 server.db.allocations.length, 590 'All allocations are scheduled on this node' 591 ); 592 assert.ok( 593 server.pretender.handledRequests.findBy('url', '/v1/job/job-1'), 594 'Job One fetched correctly' 595 ); 596 assert.ok( 597 server.pretender.handledRequests.findBy('url', '/v1/job/job-2?namespace=other-namespace'), 598 'Job Two fetched correctly' 599 ); 600 }); 601 });