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  });