github.com/hernad/nomad@v1.6.112/ui/tests/unit/utils/node-stats-tracker-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import EmberObject from '@ember/object'; 7 import { assign } from '@ember/polyfills'; 8 import { module, test } from 'qunit'; 9 import sinon from 'sinon'; 10 import Pretender from 'pretender'; 11 import NodeStatsTracker, { 12 stats, 13 } from 'nomad-ui/utils/classes/node-stats-tracker'; 14 import fetch from 'nomad-ui/utils/fetch'; 15 import statsTrackerFrameMissingBehavior from './behaviors/stats-tracker-frame-missing'; 16 17 import { settled } from '@ember/test-helpers'; 18 19 module('Unit | Util | NodeStatsTracker', function () { 20 const refDate = Date.now() * 1000000; 21 const makeDate = (ts) => new Date(ts / 1000000); 22 23 const MockNode = (overrides) => 24 assign( 25 { 26 id: 'some-identifier', 27 resources: { 28 cpu: 2000, 29 memory: 4096, 30 }, 31 }, 32 overrides 33 ); 34 35 const mockFrame = (step) => ({ 36 CPUTicksConsumed: step + 1000, 37 Memory: { 38 Used: (step + 2048) * 1024 * 1024, 39 }, 40 Timestamp: refDate + step, 41 }); 42 43 test('the NodeStatsTracker constructor expects a fetch definition and a node', async function (assert) { 44 const tracker = NodeStatsTracker.create(); 45 assert.throws( 46 () => { 47 tracker.fetch(); 48 }, 49 /StatsTrackers need a fetch method/, 50 'Polling does not work without a fetch method provided' 51 ); 52 }); 53 54 test('the url property is computed based off the node id', async function (assert) { 55 const node = MockNode(); 56 const tracker = NodeStatsTracker.create({ fetch, node }); 57 58 assert.equal( 59 tracker.get('url'), 60 `/v1/client/stats?node_id=${node.id}`, 61 'Url is derived from the node id' 62 ); 63 }); 64 65 test('reservedCPU and reservedMemory properties come from the node', async function (assert) { 66 const node = MockNode(); 67 const tracker = NodeStatsTracker.create({ fetch, node }); 68 69 assert.equal( 70 tracker.get('reservedCPU'), 71 node.resources.cpu, 72 'reservedCPU comes from the node' 73 ); 74 assert.equal( 75 tracker.get('reservedMemory'), 76 node.resources.memory, 77 'reservedMemory comes from the node' 78 ); 79 }); 80 81 test('poll results in requesting the url and calling append with the resulting JSON', async function (assert) { 82 const node = MockNode(); 83 const tracker = NodeStatsTracker.create({ 84 fetch, 85 node, 86 append: sinon.spy(), 87 }); 88 const mockFrame = { 89 Some: { 90 data: ['goes', 'here'], 91 twelve: 12, 92 }, 93 }; 94 95 const server = new Pretender(function () { 96 this.get('/v1/client/stats', () => [200, {}, JSON.stringify(mockFrame)]); 97 }); 98 99 tracker.get('poll').perform(); 100 101 assert.equal(server.handledRequests.length, 1, 'Only one request was made'); 102 assert.equal( 103 server.handledRequests[0].url, 104 `/v1/client/stats?node_id=${node.id}`, 105 'The correct URL was requested' 106 ); 107 108 await settled(); 109 assert.ok( 110 tracker.append.calledWith(mockFrame), 111 'The JSON response was passed into append as a POJO' 112 ); 113 114 server.shutdown(); 115 }); 116 117 test('append appropriately maps a data frame to the tracked stats for cpu and memory for the node', async function (assert) { 118 const node = MockNode(); 119 const tracker = NodeStatsTracker.create({ fetch, node }); 120 121 assert.deepEqual(tracker.get('cpu'), [], 'No tracked cpu yet'); 122 assert.deepEqual(tracker.get('memory'), [], 'No tracked memory yet'); 123 124 tracker.append(mockFrame(1)); 125 126 assert.deepEqual( 127 tracker.get('cpu'), 128 [{ timestamp: makeDate(refDate + 1), used: 1001, percent: 1001 / 2000 }], 129 'One frame of cpu' 130 ); 131 132 assert.deepEqual( 133 tracker.get('memory'), 134 [ 135 { 136 timestamp: makeDate(refDate + 1), 137 used: 2049 * 1024 * 1024, 138 percent: 2049 / 4096, 139 }, 140 ], 141 'One frame of memory' 142 ); 143 144 tracker.append(mockFrame(2)); 145 146 assert.deepEqual( 147 tracker.get('cpu'), 148 [ 149 { timestamp: makeDate(refDate + 1), used: 1001, percent: 1001 / 2000 }, 150 { timestamp: makeDate(refDate + 2), used: 1002, percent: 1002 / 2000 }, 151 ], 152 'Two frames of cpu' 153 ); 154 155 assert.deepEqual( 156 tracker.get('memory'), 157 [ 158 { 159 timestamp: makeDate(refDate + 1), 160 used: 2049 * 1024 * 1024, 161 percent: 2049 / 4096, 162 }, 163 { 164 timestamp: makeDate(refDate + 2), 165 used: 2050 * 1024 * 1024, 166 percent: 2050 / 4096, 167 }, 168 ], 169 'Two frames of memory' 170 ); 171 }); 172 173 test('each stat list has maxLength equal to bufferSize', async function (assert) { 174 const node = MockNode(); 175 const bufferSize = 10; 176 const tracker = NodeStatsTracker.create({ fetch, node, bufferSize }); 177 178 for (let i = 1; i <= 20; i++) { 179 tracker.append(mockFrame(i)); 180 } 181 182 assert.equal( 183 tracker.get('cpu.length'), 184 bufferSize, 185 `20 calls to append, only ${bufferSize} frames in the stats array` 186 ); 187 assert.equal( 188 tracker.get('memory.length'), 189 bufferSize, 190 `20 calls to append, only ${bufferSize} frames in the stats array` 191 ); 192 193 assert.equal( 194 +tracker.get('cpu')[0].timestamp, 195 +makeDate(refDate + 11), 196 'Old frames are removed in favor of newer ones' 197 ); 198 assert.equal( 199 +tracker.get('memory')[0].timestamp, 200 +makeDate(refDate + 11), 201 'Old frames are removed in favor of newer ones' 202 ); 203 }); 204 205 test('the stats computed property macro constructs a NodeStatsTracker based on a nodeProp and a fetch definition', async function (assert) { 206 const node = MockNode(); 207 const fetchSpy = sinon.spy(); 208 209 const SomeClass = EmberObject.extend({ 210 stats: stats('theNode', function () { 211 return () => fetchSpy(this); 212 }), 213 }); 214 const someObject = SomeClass.create({ 215 theNode: node, 216 }); 217 218 assert.equal( 219 someObject.get('stats.url'), 220 `/v1/client/stats?node_id=${node.id}`, 221 'stats computed property macro creates a NodeStatsTracker' 222 ); 223 224 someObject.get('stats').fetch(); 225 226 assert.ok( 227 fetchSpy.calledWith(someObject), 228 'the fetch factory passed into the macro gets called to assign a bound version of fetch to the NodeStatsTracker instance' 229 ); 230 }); 231 232 test('changing the value of the nodeProp constructs a new NodeStatsTracker', async function (assert) { 233 const node1 = MockNode(); 234 const node2 = MockNode(); 235 const SomeClass = EmberObject.extend({ 236 stats: stats('theNode', () => fetch), 237 }); 238 239 const someObject = SomeClass.create({ 240 theNode: node1, 241 }); 242 243 const stats1 = someObject.get('stats'); 244 245 someObject.set('theNode', node2); 246 const stats2 = someObject.get('stats'); 247 248 assert.notStrictEqual( 249 stats1, 250 stats2, 251 'Changing the value of the node results in creating a new NodeStatsTracker instance' 252 ); 253 }); 254 255 statsTrackerFrameMissingBehavior({ 256 resourceName: 'node', 257 ResourceConstructor: MockNode, 258 TrackerConstructor: NodeStatsTracker, 259 mockFrame, 260 compileResources(frame) { 261 const timestamp = makeDate(frame.Timestamp); 262 return [ 263 { 264 timestamp, 265 used: frame.CPUTicksConsumed, 266 percent: frame.CPUTicksConsumed / 2000, 267 }, 268 { 269 timestamp, 270 used: frame.Memory.Used, 271 percent: frame.Memory.Used / 1024 / 1024 / 4096, 272 }, 273 ]; 274 }, 275 }); 276 });