github.com/anuvu/nomad@v0.8.7-atom1/ui/tests/acceptance/client-detail-test.js (about)

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