From 249bc6143c6e7b5bbdd4023f616e58e4399ef314 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 | 10 +- .../test/worker/rate-control/recordRate.js | 264 +++++++++++++++--- 2 files changed, 228 insertions(+), 46 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..4815dc30c 100644 --- a/packages/caliper-core/lib/worker/rate-control/recordRate.js +++ b/packages/caliper-core/lib/worker/rate-control/recordRate.js @@ -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`); + }); } /** @@ -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'); @@ -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(); } /** diff --git a/packages/caliper-core/test/worker/rate-control/recordRate.js b/packages/caliper-core/test/worker/rate-control/recordRate.js index 0509a48f6..c439d20bf 100644 --- a/packages/caliper-core/test/worker/rate-control/recordRate.js +++ b/packages/caliper-core/test/worker/rate-control/recordRate.js @@ -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, @@ -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_round.txt", - "outputFormat": "TEXT", - "logEnd": true + pathTemplate: '../tx_records_client_round.txt', + outputFormat: 'TEXT', + logEnd: true } }, workload: { @@ -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_round.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_round.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/); }); });