From 390d6d7c521cd2f3119c1621d1c15be6a185d053 Mon Sep 17 00:00:00 2001 From: Babatunde Sanusi Date: Wed, 11 Sep 2024 07:57:18 +0100 Subject: [PATCH] add more test for recordRate rate-controller Signed-off-by: Babatunde Sanusi --- .../lib/worker/rate-control/recordRate.js | 16 +- .../test/worker/rate-control/recordRate.js | 226 ++++++++++++++---- 2 files changed, 193 insertions(+), 49 deletions(-) diff --git a/packages/caliper-core/lib/worker/rate-control/recordRate.js b/packages/caliper-core/lib/worker/rate-control/recordRate.js index 3e7e04cac..147b3d56f 100644 --- a/packages/caliper-core/lib/worker/rate-control/recordRate.js +++ b/packages/caliper-core/lib/worker/rate-control/recordRate.js @@ -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); } @@ -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`); + } } /** @@ -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'); @@ -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'); diff --git a/packages/caliper-core/test/worker/rate-control/recordRate.js b/packages/caliper-core/test/worker/rate-control/recordRate.js index 0509a48f6..a38505f5f 100644 --- a/packages/caliper-core/test/worker/rate-control/recordRate.js +++ b/packages/caliper-core/test/worker/rate-control/recordRate.js @@ -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, @@ -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_round.txt", - "outputFormat": "TEXT", - "logEnd": true + pathTemplate: '../tx_records_client_round.txt', + outputFormat: 'TEXT', + logEnd: true } }, workload: { @@ -63,42 +72,171 @@ 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_round.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(); + controller.records = new Array(scenario.expectedLength); + + 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, controller.records.length); + + 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]; + + // 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(); + }); + }); + }); + + 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(6); + controller.records.every(record => { + expect(record).to.equal(0); + }); + }); - const stubStatsCollector = sinon.createStubInstance(TransactionStatisticsCollector); - (() => { - RecordRate.createRateController(testMessage, stubStatsCollector, 0) - }).should.throw(/Module "nonexistent-rate" could not be loaded/); }); });