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