/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ /* eslint-disable qunit/require-expect */ import { module, test } from 'qunit'; import { currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; import moment from 'moment'; import { formatBytes, formatHertz } from 'nomad-ui/utils/units'; import PluginDetail from 'nomad-ui/tests/pages/storage/plugins/detail'; import Layout from 'nomad-ui/tests/pages/layout'; module('Acceptance | plugin detail', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); let plugin; hooks.beforeEach(function () { server.create('node-pool'); server.create('node'); plugin = server.create('csi-plugin', { controllerRequired: true }); }); test('it passes an accessibility audit', async function (assert) { await PluginDetail.visit({ id: plugin.id }); await a11yAudit(assert); }); test('/csi/plugins/:id should have a breadcrumb trail linking back to Plugins and Storage', async function (assert) { await PluginDetail.visit({ id: plugin.id }); assert.equal(Layout.breadcrumbFor('csi.index').text, 'Storage'); assert.equal(Layout.breadcrumbFor('csi.plugins').text, 'Plugins'); assert.equal(Layout.breadcrumbFor('csi.plugins.plugin').text, plugin.id); }); test('/csi/plugins/:id should show the plugin name in the title', async function (assert) { await PluginDetail.visit({ id: plugin.id }); assert.equal(document.title, `CSI Plugin ${plugin.id} - Nomad`); assert.equal(PluginDetail.title, plugin.id); }); test('/csi/plugins/:id should list additional details for the plugin below the title', async function (assert) { await PluginDetail.visit({ id: plugin.id }); assert.ok( PluginDetail.controllerHealth.includes( `${Math.round( (plugin.controllersHealthy / plugin.controllersExpected) * 100 )}%` ) ); assert.ok( PluginDetail.controllerHealth.includes( `${plugin.controllersHealthy}/${plugin.controllersExpected}` ) ); assert.ok( PluginDetail.nodeHealth.includes( `${Math.round((plugin.nodesHealthy / plugin.nodesExpected) * 100)}%` ) ); assert.ok( PluginDetail.nodeHealth.includes( `${plugin.nodesHealthy}/${plugin.nodesExpected}` ) ); assert.ok(PluginDetail.provider.includes(plugin.provider)); }); test('/csi/plugins/:id should list all the controller plugin allocations for the plugin', async function (assert) { await PluginDetail.visit({ id: plugin.id }); assert.equal( PluginDetail.controllerAllocations.length, plugin.controllers.length ); plugin.controllers.models .sortBy('updateTime') .reverse() .forEach((allocation, idx) => { assert.equal( PluginDetail.controllerAllocations.objectAt(idx).id, allocation.allocID ); }); }); test('/csi/plugins/:id should list all the node plugin allocations for the plugin', async function (assert) { await PluginDetail.visit({ id: plugin.id }); assert.equal(PluginDetail.nodeAllocations.length, plugin.nodes.length); plugin.nodes.models .sortBy('updateTime') .reverse() .forEach((allocation, idx) => { assert.equal( PluginDetail.nodeAllocations.objectAt(idx).id, allocation.allocID ); }); }); test('each allocation should have high-level details for the allocation', async function (assert) { const controller = plugin.controllers.models .sortBy('updateTime') .reverse()[0]; const allocation = server.db.allocations.find(controller.allocID); const allocStats = server.db.clientAllocationStats.find(allocation.id); const taskGroup = server.db.taskGroups.findBy({ name: allocation.taskGroup, jobId: allocation.jobId, }); const tasks = taskGroup.taskIds.map((id) => server.db.tasks.find(id)); const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); const memoryUsed = tasks.reduce( (sum, task) => sum + task.resources.MemoryMB, 0 ); await PluginDetail.visit({ id: plugin.id }); PluginDetail.controllerAllocations.objectAt(0).as((allocationRow) => { assert.equal( allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID' ); assert.equal( allocationRow.createTime, moment(allocation.createTime / 1000000).format('MMM D') ); assert.equal( allocationRow.createTooltip, moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ') ); assert.equal( allocationRow.modifyTime, moment(allocation.modifyTime / 1000000).fromNow() ); assert.equal( allocationRow.health, controller.healthy ? 'Healthy' : 'Unhealthy' ); assert.equal( allocationRow.client, server.db.nodes.find(allocation.nodeId).id.split('-')[0], 'Node ID' ); assert.equal( allocationRow.clientTooltip.substr(0, 15), server.db.nodes.find(allocation.nodeId).name.substr(0, 15), 'Node Name' ); assert.equal( allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name' ); assert.ok(allocationRow.taskGroup, 'Task group name'); assert.ok(allocationRow.jobVersion, 'Job Version'); assert.equal( allocationRow.cpu, Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed, 'CPU %' ); const roundedTicks = Math.floor( allocStats.resourceUsage.CpuStats.TotalTicks ); assert.equal( allocationRow.cpuTooltip, `${formatHertz(roundedTicks, 'MHz')} / ${formatHertz(cpuUsed, 'MHz')}`, 'Detailed CPU information is in a tooltip' ); assert.equal( allocationRow.mem, allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed, 'Memory used' ); assert.equal( allocationRow.memTooltip, `${formatBytes( allocStats.resourceUsage.MemoryStats.RSS )} / ${formatBytes(memoryUsed, 'MiB')}`, 'Detailed memory information is in a tooltip' ); }); }); test('each allocation should link to the allocation detail page', async function (assert) { const controller = plugin.controllers.models .sortBy('updateTime') .reverse()[0]; await PluginDetail.visit({ id: plugin.id }); await PluginDetail.controllerAllocations.objectAt(0).visit(); assert.equal(currentURL(), `/allocations/${controller.allocID}`); }); test('when there are no plugin allocations, the tables present empty states', async function (assert) { const emptyPlugin = server.create('csi-plugin', { controllerRequired: true, controllersHealthy: 0, controllersExpected: 0, nodesHealthy: 0, nodesExpected: 0, }); await PluginDetail.visit({ id: emptyPlugin.id }); assert.ok(PluginDetail.controllerTableIsEmpty); assert.equal( PluginDetail.controllerEmptyState.headline, 'No Controller Plugin Allocations' ); assert.ok(PluginDetail.nodeTableIsEmpty); assert.equal( PluginDetail.nodeEmptyState.headline, 'No Node Plugin Allocations' ); }); test('when the plugin is node-only, the controller information is omitted', async function (assert) { const nodeOnlyPlugin = server.create('csi-plugin', { controllerRequired: false, }); await PluginDetail.visit({ id: nodeOnlyPlugin.id }); assert.notOk(PluginDetail.controllerAvailabilityIsPresent); assert.ok(PluginDetail.nodeAvailabilityIsPresent); assert.notOk(PluginDetail.controllerHealthIsPresent); assert.notOk(PluginDetail.controllerTableIsPresent); }); test('when there are more than 10 controller or node allocations, only 10 are shown', async function (assert) { const manyAllocationsPlugin = server.create('csi-plugin', { shallow: true, controllerRequired: false, nodesExpected: 15, }); await PluginDetail.visit({ id: manyAllocationsPlugin.id }); assert.equal(PluginDetail.nodeAllocations.length, 10); }); test('the View All links under each allocation table link to a filtered view of the plugins allocation list', async function (assert) { const serialize = (arr) => window.encodeURIComponent(JSON.stringify(arr)); await PluginDetail.visit({ id: plugin.id }); assert.ok( PluginDetail.goToControllerAllocationsText.includes( plugin.controllers.models.length ) ); await PluginDetail.goToControllerAllocations(); assert.equal( currentURL(), `/csi/plugins/${plugin.id}/allocations?type=${serialize(['controller'])}` ); await PluginDetail.visit({ id: plugin.id }); assert.ok( PluginDetail.goToNodeAllocationsText.includes(plugin.nodes.models.length) ); await PluginDetail.goToNodeAllocations(); assert.equal( currentURL(), `/csi/plugins/${plugin.id}/allocations?type=${serialize(['node'])}` ); }); });