Skip to content

Commit

Permalink
add more test for recordRate rate-controller
Browse files Browse the repository at this point in the history
Signed-off-by: Babatunde Sanusi <[email protected]>
  • Loading branch information
tunedev committed Nov 11, 2024
1 parent 3c6b739 commit 249bc61
Show file tree
Hide file tree
Showing 2 changed files with 228 additions and 46 deletions.
10 changes: 7 additions & 3 deletions packages/caliper-core/lib/worker/rate-control/recordRate.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ class RecordRateController extends RateInterface {
*/
_exportToText() {
fs.writeFileSync(this.pathTemplate, '', 'utf-8');
this.records.forEach(submitTime => fs.appendFileSync(this.pathTemplate, `${submitTime}\n`));
this.records.forEach(submitTime => {
const time = submitTime !== undefined ? submitTime : 0;
fs.appendFileSync(this.pathTemplate, `${time}\n`);
});
}

/**
Expand All @@ -103,7 +106,8 @@ class RecordRateController extends RateInterface {
offset = buffer.writeUInt32LE(this.records.length, offset);

for (let i = 0; i < this.records.length; i++) {
offset = buffer.writeUInt32LE(this.records[i], offset);
const time = this.records[i] !== undefined ? this.records[i] : 0;
offset = buffer.writeUInt32LE(time, offset);
}

fs.writeFileSync(this.pathTemplate, buffer, 'binary');
Expand Down Expand Up @@ -132,7 +136,7 @@ class RecordRateController extends RateInterface {
*/
async applyRateControl() {
await this.recordedRateController.applyRateControl();
this.records[this.stats.getTotalSubmittedTx()] = Date.now() - this.stats.getRoundStartTime();
this.records[this.stats.getTotalSubmittedTx() - 1] = Date.now() - this.stats.getRoundStartTime();
}

/**
Expand Down
264 changes: 221 additions & 43 deletions packages/caliper-core/test/worker/rate-control/recordRate.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@
const mockery = require('mockery');
const path = require('path');
const RecordRate = require('../../../lib/worker/rate-control/recordRate');
const fs = require('fs');
const TestMessage = require('../../../lib/common/messages/testMessage');
const MockRate = require('./mockRate');
const TransactionStatisticsCollector = require('../../../lib/common/core/transaction-statistics-collector');
const util = require('../../../lib/common/utils/caliper-utils');
const logger = util.getLogger('record-rate-controller');

const chai = require('chai');
chai.should();
const sinon = require('sinon');

describe('RecordRate controller', () => {
let msgContent;
let stubStatsCollector;
let sandbox;

before(() => {
mockery.enable({
warnOnReplace: false,
Expand All @@ -34,25 +41,29 @@ describe('RecordRate controller', () => {
});

mockery.registerMock(path.join(__dirname, '../../../lib/worker/rate-control/noRate.js'), MockRate);
sandbox = sinon.createSandbox();
});

after(() => {
mockery.deregisterAll();
mockery.disable();
if (fs.existsSync('../tx_records_client0_round0.txt')) {
fs.unlinkSync('../tx_records_client0_round0.txt');
}
});

it('should apply rate control to the recorded rate controller', async () => {
const msgContent = {
beforeEach(() => {
msgContent = {
label: 'test',
rateControl: {
"type": "record-rate",
"opts": {
"rateController": {
"type": "zero-rate"
type: 'record-rate',
opts: {
rateController: {
type: 'zero-rate'
},
"pathTemplate": "../tx_records_client<C>_round<R>.txt",
"outputFormat": "TEXT",
"logEnd": true
pathTemplate: '../tx_records_client<C>_round<R>.txt',
outputFormat: 'TEXT',
logEnd: true
}
},
workload: {
Expand All @@ -63,42 +74,209 @@ describe('RecordRate controller', () => {
totalWorkers: 2
};

const testMessage = new TestMessage('test', [], msgContent);
const stubStatsCollector = sinon.createStubInstance(TransactionStatisticsCollector);
const rateController = RecordRate.createRateController(testMessage, stubStatsCollector, 0);
const mockRate = MockRate.createRateController();
mockRate.reset();
mockRate.isApplyRateControlCalled().should.equal(false);
await rateController.applyRateControl();
mockRate.isApplyRateControlCalled().should.equal(true);
stubStatsCollector = new TransactionStatisticsCollector();
stubStatsCollector.getTotalSubmittedTx = sandbox.stub();
});

it('should throw an error if the rate controller to record is unknown', async () => {
const msgContent = {
label: 'test',
rateControl: {
"type": "record-rate",
"opts": {
"rateController": {
"type": "nonexistent-rate"
},
"pathTemplate": "../tx_records_client<C>_round<R>.txt",
"outputFormat": "TEXT",
"logEnd": true
}
},
workload: {
module: 'module.js'
},
testRound: 0,
txDuration: 250,
totalWorkers: 2
};
const testMessage = new TestMessage('test', [], msgContent);
afterEach(() => {
sandbox.restore();
});

describe('Export Formats', () => {
it('should default outputFormat to TEXT if undefined', () => {
msgContent.rateControl.opts.outputFormat = undefined;
const testMessage = new TestMessage('test', [], msgContent);
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0);
controller.outputFormat.should.equal('TEXT');
});


it('should set outputFormat to TEXT if invalid format is provided', () => {
msgContent.rateControl.opts.outputFormat = 'INVALID_FORMAT';
const testMessage = new TestMessage('test', [], msgContent);
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0);

controller.outputFormat.should.equal('TEXT');
});

it('should export records to text format with gaps', async () => {
const testMessage = new TestMessage('test', [], msgContent);
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0);
sinon.stub(controller.recordedRateController, 'end').resolves();

controller.records = new Array(8).fill(undefined); // Initialize with length 8
controller.records[1] = 100;
controller.records[3] = 200;
controller.records[7] = 300;

const fsWriteSyncStub = sandbox.stub(fs, 'writeFileSync');
const fsAppendSyncStub = sandbox.stub(fs, 'appendFileSync');

await controller.end();

sinon.assert.calledOnce(fsWriteSyncStub);
sinon.assert.callCount(fsAppendSyncStub, controller.records.length);

// Verify the content written to the file
for (let i = 0; i < controller.records.length; i++) {
const time = controller.records[i] !== undefined ? controller.records[i] : 0;
const expectedValue = `${time}\n`;
sinon.assert.calledWith(fsAppendSyncStub.getCall(i), sinon.match.string, expectedValue);
}
fsWriteSyncStub.restore();
fsAppendSyncStub.restore();
});

it('should export records to binary big endian format with gaps', async () => {
const msgContent = {
label: 'test',
rateControl: {
type: 'record-rate',
opts: {
rateController: {
type: 'zero-rate'
},
pathTemplate: '../tx_records_client<C>_round<R>.txt',
outputFormat: 'BIN_BE'
}
},
testRound: 0,
txDuration: 250,
totalWorkers: 2
};
const testMessage = new TestMessage('test', [], msgContent);
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0);
sinon.stub(controller.recordedRateController, 'end').resolves();

// Use non-sequential indices with gaps
controller.records = new Array(8).fill(undefined); // Initialize with length 8
controller.records[1] = 100;
controller.records[3] = 200;
controller.records[7] = 300;

const fsWriteSyncStub = sandbox.stub(fs, 'writeFileSync');

await controller.end();

sinon.assert.calledOnce(fsWriteSyncStub);
const buffer = fsWriteSyncStub.getCall(0).args[1];

// Verify that the buffer starts with the length of the records array
buffer.readUInt32BE(0).should.equal(controller.records.length);

// Verify each value in the buffer
for (let i = 0; i < controller.records.length; i++) {
const expectedValue = controller.records[i] !== undefined ? controller.records[i] : 0;
const actualValue = buffer.readUInt32BE(4 + i * 4);
actualValue.should.equal(expectedValue);
}

fsWriteSyncStub.restore();
});


it('should export records to binary little endian format with gaps', async () => {
msgContent.rateControl.opts.outputFormat = 'BIN_LE';
const testMessage = new TestMessage('test', [], msgContent);
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0);

sandbox.stub(controller.recordedRateController, 'end').resolves();

// Create an array with gaps to simulate non-sequential transaction submissions
controller.records = new Array(8).fill(undefined); // Initialize with length 8
controller.records[2] = 100;
controller.records[4] = 200;
controller.records[7] = 300;

const fsWriteSyncStub = sandbox.stub(fs, 'writeFileSync');

await controller.end();

sinon.assert.calledOnce(fsWriteSyncStub);
const buffer = fsWriteSyncStub.getCall(0).args[1];

// Verify that the buffer starts with the length of the records array
buffer.readUInt32LE(0).should.equal(controller.records.length);

// Verify each value in the buffer
for (let i = 0; i < controller.records.length; i++) {
const expectedValue = controller.records[i] !== undefined ? controller.records[i] : 0;
const actualValue = buffer.readUInt32LE(4 + i * 4);
actualValue.should.equal(expectedValue);
}

fsWriteSyncStub.restore();
});

it('should throw an error if pathTemplate is undefined', () => {
msgContent.rateControl.opts.pathTemplate = undefined;
const testMessage = new TestMessage('test', [], msgContent);

(() => {
RecordRate.createRateController(testMessage, stubStatsCollector, 0);
}).should.throw('The path to save the recording to is undefined');
});
});

describe('When Applying Rate Control', () => {
it('should apply rate control to the recorded rate controller', async () => {
const testMessage = new TestMessage('test', [], msgContent);
const rateController = RecordRate.createRateController(testMessage, stubStatsCollector, 0);
const mockRate = MockRate.createRateController();
mockRate.reset();
mockRate.isApplyRateControlCalled().should.equal(false);
await rateController.applyRateControl();
mockRate.isApplyRateControlCalled().should.equal(true);
});

it('should replace path template placeholders for various worker and round indices', () => {
const testCases = [
{ testRound: 0, workerIndex: 0, expectedPath: '../tx_records_client0_round0.txt' },
{ testRound: 1, workerIndex: 2, expectedPath: '../tx_records_client2_round1.txt' },
{ testRound: 5, workerIndex: 3, expectedPath: '../tx_records_client3_round5.txt' },
{ testRound: 10, workerIndex: 7, expectedPath: '../tx_records_client7_round10.txt' },
];

testCases.forEach(({ testRound, workerIndex, expectedPath }) => {
const content = JSON.parse(JSON.stringify(msgContent));
content.testRound = testRound;
const testMessage = new TestMessage('test', [], content);
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, workerIndex);
controller.pathTemplate.should.equal(util.resolvePath(expectedPath));
});
});

it('should throw an error if the rate controller to record is unknown', async () => {
msgContent.rateControl.opts.rateController.type = 'nonexistent-rate';
msgContent.rateControl.opts.logEnd = true;
const testMessage = new TestMessage('test', [], msgContent);

(() => {
RecordRate.createRateController(testMessage, stubStatsCollector, 0);
}).should.throw(/Module "nonexistent-rate" could not be loaded/);
});

it('should throw an error if rateController is undefined', () => {
msgContent.rateControl.opts.rateController = undefined;
const testMessage = new TestMessage('test', [], msgContent);

(() => {
RecordRate.createRateController(testMessage, stubStatsCollector, 0);
}).should.throw('The rate controller to record is undefined');
});
});

describe('When Creating a RecordRate Controller', () => {
it('should initialize records array if the number of transactions is provided', () => {
const testMessage = new TestMessage('test', [], msgContent);
sinon.stub(testMessage, 'getNumberOfTxs').returns(5);

const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0);

controller.records.should.be.an('array').that.has.lengthOf(5);
stubStatsCollector.getTotalSubmittedTx.returns(1);
controller.records.every(record => record.should.equal(0));
});

const stubStatsCollector = sinon.createStubInstance(TransactionStatisticsCollector);
(() => {
RecordRate.createRateController(testMessage, stubStatsCollector, 0)
}).should.throw(/Module "nonexistent-rate" could not be loaded/);
});
});

0 comments on commit 249bc61

Please sign in to comment.