Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add more test for recordRate rate-controller #1627

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
tunedev marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
tunedev marked this conversation as resolved.
Show resolved Hide resolved
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 () => {
tunedev marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
tunedev marked this conversation as resolved.
Show resolved Hide resolved
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');
});
});
});
Loading