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 21, 2024
1 parent 3c6b739 commit 31d70ee
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 49 deletions.
16 changes: 11 additions & 5 deletions packages/caliper-core/lib/worker/rate-control/recordRate.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ class RecordRateController extends RateInterface {

this.records = [];
// if we know the number of transactions beforehand, pre-allocate the array
if (testMessage.getNumberOfTxs()) {
this.records = new Array(testMessage.getNumberOfTxs());
const numTx = testMessage.getNumberOfTxs();
if (numTx) {
this.records = new Array(numTx + 1);
this.records.fill(0);
}

Expand Down Expand Up @@ -89,7 +90,10 @@ class RecordRateController extends RateInterface {
*/
_exportToText() {
fs.writeFileSync(this.pathTemplate, '', 'utf-8');
this.records.forEach(submitTime => fs.appendFileSync(this.pathTemplate, `${submitTime}\n`));
for (let i = 0; i < this.records.length; i++) {
const time = this.records[i] !== undefined ? this.records[i] : 0;
fs.appendFileSync(this.pathTemplate, `${time}\n`);
}
}

/**
Expand All @@ -103,7 +107,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 All @@ -120,7 +125,8 @@ class RecordRateController extends RateInterface {
offset = buffer.writeUInt32BE(this.records.length, offset);

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

fs.writeFileSync(this.pathTemplate, buffer, 'binary');
Expand Down
225 changes: 181 additions & 44 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,20 @@
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 chai = require('chai');
chai.should();
const { expect } = require('chai');
const sinon = require('sinon');

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

before(() => {
mockery.enable({
warnOnReplace: false,
Expand All @@ -34,25 +39,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 +72,170 @@ 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'
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');
});

const formats = ['TEXT', 'BIN_BE', 'BIN_LE'];
const recordScenarios = [
{
description: 'with gaps (sparse records)',
records: { 1: 100, 3: 200, 7: 300 },
expectedLength: 8
},
testRound: 0,
txDuration: 250,
totalWorkers: 2
};
const testMessage = new TestMessage('test', [], msgContent);
{
description: 'fully populated (sequential records)',
records: { 0: 50, 1: 100, 2: 150, 3: 200, 4: 250 },
expectedLength: 5
}
];

formats.forEach(format => {
recordScenarios.forEach(scenario => {
it(`should export records to ${format} format ${scenario.description}`, async () => {
// Prepare message content with the specific output format
const msgContentCopy = JSON.parse(JSON.stringify(msgContent));
msgContentCopy.rateControl.opts.outputFormat = format;
const testMessage = new TestMessage('test', [], msgContentCopy);
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0);

sinon.stub(controller.recordedRateController, 'end').resolves();
Object.keys(scenario.records).forEach(index => {
controller.records[index] = scenario.records[index];
});

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

await controller.end();

if (format === 'TEXT') {
sinon.assert.calledOnce(fsWriteSyncStub);
sinon.assert.callCount(fsAppendSyncStub, scenario.expectedLength);
expect(controller.records.length).to.equal(scenario.expectedLength);

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);
}
} else {
sinon.assert.calledOnce(fsWriteSyncStub);
const buffer = fsWriteSyncStub.getCall(0).args[1];

const stubStatsCollector = sinon.createStubInstance(TransactionStatisticsCollector);
(() => {
RecordRate.createRateController(testMessage, stubStatsCollector, 0)
}).should.throw(/Module "nonexistent-rate" could not be loaded/);
// Determine the read method based on format
const readUInt32 = format === 'BIN_BE' ? Buffer.prototype.readUInt32BE : Buffer.prototype.readUInt32LE;

// Verify that the buffer starts with the length of the records array
const length = readUInt32.call(buffer, 0);
length.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 = readUInt32.call(buffer, 4 + i * 4);
actualValue.should.equal(expectedValue);
}
}

// Restore stubs
fsWriteSyncStub.restore();
fsAppendSyncStub.restore();
});
});
});
});

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

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(6);
controller.records.every(record => {
expect(record).to.equal(0);
});
});

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

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

0 comments on commit 31d70ee

Please sign in to comment.