-
Notifications
You must be signed in to change notification settings - Fork 404
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<C>_round<R>.txt", | ||
"outputFormat": "TEXT", | ||
"logEnd": true | ||
pathTemplate: '../tx_records_client<C>_round<R>.txt', | ||
outputFormat: 'TEXT', | ||
logEnd: true | ||
} | ||
}, | ||
workload: { | ||
|
@@ -63,42 +74,263 @@ 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', async () => { | ||
const testMessage = new TestMessage('test', [], msgContent); | ||
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0); | ||
sinon.stub(controller.recordedRateController, 'end').resolves(); | ||
|
||
controller.records = [100, 200, 300]; | ||
const fsWriteSyncStub = sandbox.stub(fs, 'writeFileSync'); | ||
const fsAppendSyncStub = sandbox.stub(fs, 'appendFileSync'); | ||
|
||
await controller.end(); | ||
|
||
sinon.assert.calledOnce(fsWriteSyncStub); | ||
sinon.assert.calledThrice(fsAppendSyncStub); | ||
|
||
// Verify the content written to the file | ||
sinon.assert.calledWith(fsWriteSyncStub, sinon.match.string, '', 'utf-8'); | ||
sinon.assert.calledWith(fsAppendSyncStub.getCall(0), sinon.match.string, '100\n'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to use the controller.records array content and length to drive these checks as it's the content of the records we want to make sure matches. |
||
sinon.assert.calledWith(fsAppendSyncStub.getCall(1), sinon.match.string, '200\n'); | ||
sinon.assert.calledWith(fsAppendSyncStub.getCall(2), sinon.match.string, '300\n'); | ||
|
||
|
||
fsWriteSyncStub.restore(); | ||
fsAppendSyncStub.restore(); | ||
}); | ||
|
||
it('should export records to binary big endian format', 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, // Ensure roundIndex is set | ||
txDuration: 250, | ||
totalWorkers: 2 | ||
}; | ||
const testMessage = new TestMessage('test', [], msgContent); | ||
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0); | ||
sinon.stub(controller.recordedRateController, 'end').resolves(); | ||
|
||
controller.records = [100, 200, 300]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment about the test data here as for the text test. |
||
const fsWriteSyncStub = sandbox.stub(fs, 'writeFileSync'); | ||
|
||
await controller.end(); | ||
|
||
sinon.assert.calledOnce(fsWriteSyncStub); | ||
const buffer = fsWriteSyncStub.getCall(0).args[1]; | ||
buffer.readUInt32BE(0).should.equal(3); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to use the controller.records array content and length to drive these checks as it's the content of the records we want to make sure matches. |
||
buffer.readUInt32BE(4).should.equal(100); | ||
buffer.readUInt32BE(8).should.equal(200); | ||
buffer.readUInt32BE(12).should.equal(300); | ||
|
||
fsWriteSyncStub.restore(); | ||
}); | ||
|
||
it('should export records to binary little endian format', 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(); | ||
|
||
controller.records = [100, 200, 300]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment here regarding the record data given in the text test |
||
const fsWriteSyncStub = sandbox.stub(fs, 'writeFileSync'); | ||
|
||
await controller.end(); | ||
|
||
sinon.assert.calledOnce(fsWriteSyncStub); | ||
const buffer = fsWriteSyncStub.getCall(0).args[1]; | ||
buffer.readUInt32LE(0).should.equal(3); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to use the controller.records array content and length to drive these checks as it's the content of the records we want to make sure matches. |
||
buffer.readUInt32LE(4).should.equal(100); | ||
buffer.readUInt32LE(8).should.equal(200); | ||
buffer.readUInt32LE(12).should.equal(300); | ||
|
||
fsWriteSyncStub.restore(); | ||
}); | ||
it('should export to text format when output format is TEXT', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already tested above. given that you track the writes to the file, we know it's only writing to text and not the others. |
||
const testMessage = new TestMessage('test', [], msgContent); | ||
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0); | ||
|
||
const mockController = { | ||
end: sinon.stub().resolves(), | ||
applyRateControl: sinon.stub().resolves(), | ||
}; | ||
controller.recordedRateController.controller = mockController; | ||
|
||
const exportToTextSpy = sinon.spy(controller, '_exportToText'); | ||
const exportToBinaryLittleEndianSpy = sinon.spy(controller, '_exportToBinaryLittleEndian'); | ||
const exportToBinaryBigEndianSpy = sinon.spy(controller, '_exportToBinaryBigEndian'); | ||
|
||
await controller.end(); | ||
|
||
sinon.assert.calledOnce(exportToTextSpy); | ||
sinon.assert.notCalled(exportToBinaryLittleEndianSpy); | ||
sinon.assert.notCalled(exportToBinaryBigEndianSpy); | ||
sinon.assert.notCalled(logger.error); | ||
|
||
exportToTextSpy.restore(); | ||
exportToBinaryLittleEndianSpy.restore(); | ||
exportToBinaryBigEndianSpy.restore(); | ||
}); | ||
|
||
it('should export to binary little endian format when output format is BIN_LE', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need this test as it's already tested. |
||
msgContent.rateControl.opts.outputFormat = 'BIN_LE'; | ||
const testMessage = new TestMessage('test', [], msgContent); | ||
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0); | ||
|
||
const mockController = { | ||
end: sinon.stub().resolves(), | ||
applyRateControl: sinon.stub().resolves(), | ||
}; | ||
controller.recordedRateController.controller = mockController; | ||
|
||
const exportToTextSpy = sinon.spy(controller, '_exportToText'); | ||
const exportToBinaryLittleEndianSpy = sinon.spy(controller, '_exportToBinaryLittleEndian'); | ||
const exportToBinaryBigEndianSpy = sinon.spy(controller, '_exportToBinaryBigEndian'); | ||
|
||
await controller.end(); | ||
|
||
sinon.assert.notCalled(exportToTextSpy); | ||
sinon.assert.calledOnce(exportToBinaryLittleEndianSpy); | ||
sinon.assert.notCalled(exportToBinaryBigEndianSpy); | ||
sinon.assert.notCalled(logger.error); | ||
|
||
exportToTextSpy.restore(); | ||
exportToBinaryLittleEndianSpy.restore(); | ||
exportToBinaryBigEndianSpy.restore(); | ||
}); | ||
|
||
|
||
it('should export to binary big endian format when output format is BIN_BE', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need this test as it's already tested |
||
msgContent.rateControl.opts.outputFormat = 'BIN_BE'; | ||
const testMessage = new TestMessage('test', [], msgContent); | ||
const controller = RecordRate.createRateController(testMessage, stubStatsCollector, 0); | ||
|
||
const mockController = { | ||
end: sinon.stub().resolves(), | ||
applyRateControl: sinon.stub().resolves(), | ||
}; | ||
controller.recordedRateController.controller = mockController; | ||
|
||
const exportToTextSpy = sinon.spy(controller, '_exportToText'); | ||
const exportToBinaryLittleEndianSpy = sinon.spy(controller, '_exportToBinaryLittleEndian'); | ||
const exportToBinaryBigEndianSpy = sinon.spy(controller, '_exportToBinaryBigEndian'); | ||
|
||
await controller.end(); | ||
|
||
sinon.assert.notCalled(exportToTextSpy); | ||
sinon.assert.notCalled(exportToBinaryLittleEndianSpy); | ||
sinon.assert.calledOnce(exportToBinaryBigEndianSpy); | ||
sinon.assert.notCalled(logger.error); | ||
|
||
exportToTextSpy.restore(); | ||
exportToBinaryLittleEndianSpy.restore(); | ||
exportToBinaryBigEndianSpy.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/); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A better set of data here, might be to do the followint
controller.records[1]=100;
controller.records[3]=200;
controller.records[7]=300;
We would then expect the output to be
0
100
0
200
0
0
0
300
But in fact it won't do this. So there is a bug.
why 0 instead of blank lines ? well I think 0 makes more sense given that the data should be consistent and it's also what would happen if there were a known number of transactions before hand
In fact there are at least 3 bugs in the rate controller (2 in the code and 1 bug/clarification in the documentation) which me looking at this test highlights. We need to make sure that tests being written try to mimic the real world scenarios , so here the assumption that records are going to be sequential in position 0,1,2 are not accurate.
This test does test the format of the output, so we could have another test that tests the real world behaviour but rather than having 2 tests doing almost identical testing we can have one test to do it all. I think both approaches would be ok, but it highlights that this test is not adequate on it's own and again is an example where 100% coverage doesn't mean it's properly tested.