Skip to content

Commit

Permalink
Test coverage for NodeStatsTracker
Browse files Browse the repository at this point in the history
  • Loading branch information
DingoEatingFuzz committed Sep 11, 2018
1 parent 3c09777 commit f8c8c3c
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 7 deletions.
5 changes: 3 additions & 2 deletions ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Ember from 'ember';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
Expand Down Expand Up @@ -25,10 +26,10 @@ export default Controller.extend(Sortable, {
}),

pollStats: task(function*() {
while (true) {
do {
yield this.get('stats').poll();
yield timeout(1000);
}
} while (!Ember.testing);
}),

actions: {
Expand Down
5 changes: 3 additions & 2 deletions ui/app/controllers/clients/client.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Ember from 'ember';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
Expand Down Expand Up @@ -41,10 +42,10 @@ export default Controller.extend(Sortable, Searchable, {
}),

pollStats: task(function*() {
while (true) {
do {
yield this.get('stats').poll();
yield timeout(1000);
}
} while (!Ember.testing);
}),

actions: {
Expand Down
6 changes: 4 additions & 2 deletions ui/app/routes/clients/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ export default Route.extend(WithWatchers, {

watchers: collect('watch', 'watchAllocations'),

setupController(controller) {
setupController(controller, model) {
this._super(...arguments);
controller.get('pollStats').perform();
if (model) {
controller.get('pollStats').perform();
}
},

resetController(controller) {
Expand Down
2 changes: 1 addition & 1 deletion ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export default function() {
this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
this.get('/client/fs/logs/:allocation_id', clientAllocationLog);

this.get('/client/v1/client/stats', function({ clientStats }, { queryParams }) {
this.get('/client/stats', function({ clientStats }, { queryParams }) {
return this.serialize(clientStats.find(queryParams.node_id));
});

Expand Down
4 changes: 4 additions & 0 deletions ui/mirage/factories/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ export default Factory.extend({
node.update({
eventIds: events.mapBy('id'),
});

server.create('client-stats', {
id: node.id,
});
},
});

Expand Down
222 changes: 222 additions & 0 deletions ui/tests/unit/utils/node-stats-tracker-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import EmberObject from '@ember/object';
import { assign } from '@ember/polyfills';
import wait from 'ember-test-helpers/wait';
import { module, test } from 'ember-qunit';
import sinon from 'sinon';
import Pretender from 'pretender';
import NodeStatsTracker, { stats } from 'nomad-ui/utils/classes/node-stats-tracker';
import fetch from 'nomad-ui/utils/fetch';

module('Unit | Util | NodeStatsTracker');

const refDate = Date.now();

const MockNode = overrides =>
assign(
{
id: 'some-identifier',
resources: {
cpu: 2000,
memory: 4096,
},
},
overrides
);

const mockFrame = step => ({
CPUTicksConsumed: step + 1000,
Memory: {
Used: (step + 2048) * 1024 * 1024,
},
Timestamp: refDate + step,
});

test('the NodeStatsTracker constructor expects a fetch definition and a node', function(assert) {
const tracker = NodeStatsTracker.create();
assert.throws(
() => {
tracker.poll();
},
/StatsTrackers need a fetch method/,
'Polling does not work without a fetch method provided'
);
});

test('the url property is computed based off the node id', function(assert) {
const node = MockNode();
const tracker = NodeStatsTracker.create({ fetch, node });

assert.equal(
tracker.get('url'),
`/v1/client/stats?node_id=${node.id}`,
'Url is derived from the node id'
);
});

test('reservedCPU and reservedMemory properties come from the node', function(assert) {
const node = MockNode();
const tracker = NodeStatsTracker.create({ fetch, node });

assert.equal(tracker.get('reservedCPU'), node.resources.cpu, 'reservedCPU comes from the node');
assert.equal(
tracker.get('reservedMemory'),
node.resources.memory,
'reservedMemory comes from the node'
);
});

test('poll results in requesting the url and calling append with the resulting JSON', function(assert) {
const node = MockNode();
const tracker = NodeStatsTracker.create({ fetch, node, append: sinon.spy() });
const mockFrame = {
Some: {
data: ['goes', 'here'],
twelve: 12,
},
};

const server = new Pretender(function() {
this.get('/v1/client/stats', () => [200, {}, JSON.stringify(mockFrame)]);
});

tracker.poll();

assert.equal(server.handledRequests.length, 1, 'Only one request was made');
assert.equal(
server.handledRequests[0].url,
`/v1/client/stats?node_id=${node.id}`,
'The correct URL was requested'
);

return wait().then(() => {
assert.ok(
tracker.append.calledWith(mockFrame),
'The JSON response was passed into append as a POJO'
);

server.shutdown();
});
});

test('append appropriately maps a data frame to the tracked stats for cpu and memory for the node', function(assert) {
const node = MockNode();
const tracker = NodeStatsTracker.create({ fetch, node });

assert.deepEqual(tracker.get('cpu'), [], 'No tracked cpu yet');
assert.deepEqual(tracker.get('memory'), [], 'No tracked memory yet');

tracker.append(mockFrame(1));

assert.deepEqual(
tracker.get('cpu'),
[{ timestamp: refDate + 1, used: 1001, percent: 1001 / 2000 }],
'One frame of cpu'
);

assert.deepEqual(
tracker.get('memory'),
[{ timestamp: refDate + 1, used: 2049 * 1024 * 1024, percent: 2049 / 4096 }],
'One frame of memory'
);

tracker.append(mockFrame(2));

assert.deepEqual(
tracker.get('cpu'),
[
{ timestamp: refDate + 1, used: 1001, percent: 1001 / 2000 },
{ timestamp: refDate + 2, used: 1002, percent: 1002 / 2000 },
],
'Two frames of cpu'
);

assert.deepEqual(
tracker.get('memory'),
[
{ timestamp: refDate + 1, used: 2049 * 1024 * 1024, percent: 2049 / 4096 },
{ timestamp: refDate + 2, used: 2050 * 1024 * 1024, percent: 2050 / 4096 },
],
'Two frames of memory'
);
});

test('each stat list has maxLength equal to bufferSize', function(assert) {
const node = MockNode();
const bufferSize = 10;
const tracker = NodeStatsTracker.create({ fetch, node, bufferSize });

for (let i = 1; i <= 20; i++) {
tracker.append(mockFrame(i));
}

assert.equal(
tracker.get('cpu.length'),
bufferSize,
`20 calls to append, only ${bufferSize} frames in the stats array`
);
assert.equal(
tracker.get('memory.length'),
bufferSize,
`20 calls to append, only ${bufferSize} frames in the stats array`
);

assert.equal(
tracker.get('cpu')[0].timestamp,
refDate + 11,
'Old frames are removed in favor of newer ones'
);
assert.equal(
tracker.get('memory')[0].timestamp,
refDate + 11,
'Old frames are removed in favor of newer ones'
);
});

test('the stats computed property macro constructs a NodeStatsTracker based on a nodeProp and a fetch definition', function(assert) {
const node = MockNode();
const fetchSpy = sinon.spy();

const SomeClass = EmberObject.extend({
stats: stats('theNode', function() {
return () => fetchSpy(this);
}),
});
const someObject = SomeClass.create({
theNode: node,
});

assert.equal(
someObject.get('stats.url'),
`/v1/client/stats?node_id=${node.id}`,
'stats computed property macro creates a NodeStatsTracker'
);

someObject.get('stats').fetch();

assert.ok(
fetchSpy.calledWith(someObject),
'the fetch factory passed into the macro gets called to assign a bound version of fetch to the NodeStatsTracker instance'
);
});

test('changing the value of the nodeProp constructs a new NodeStatsTracker', function(assert) {
const node1 = MockNode();
const node2 = MockNode();
const SomeClass = EmberObject.extend({
stats: stats('theNode', () => fetch),
});

const someObject = SomeClass.create({
theNode: node1,
});

const stats1 = someObject.get('stats');

someObject.set('theNode', node2);
const stats2 = someObject.get('stats');

assert.notOk(
stats1 === stats2,
'Changing the value of the node results in creating a new NodeStatsTracker instance'
);
});

0 comments on commit f8c8c3c

Please sign in to comment.