diff --git a/packages/caliper-core/lib/worker/tx-observers/logging-tx-observer.js b/packages/caliper-core/lib/worker/tx-observers/logging-tx-observer.js index 687d71eca..043229d2e 100644 --- a/packages/caliper-core/lib/worker/tx-observers/logging-tx-observer.js +++ b/packages/caliper-core/lib/worker/tx-observers/logging-tx-observer.js @@ -46,21 +46,12 @@ class LoggingTxObserver extends TxObserverInterface{ * @param {TxStatus | TxStatus[]} results The result information of the finished TXs. Can be a collection of results for a batch of TXs. */ txFinished(results) { - // TODO: appending metadata should be done by the dispatch if (Array.isArray(results)) { for (let result of results) { - // add extra metadata - result.workerIndex = this.workerIndex; - result.roundIndex = this.roundIndex; - // TODO: use fast-json-stringify this.logFunction(JSON.stringify(result)); } } else { - // add extra metadata - results.workerIndex = this.workerIndex; - results.roundIndex = this.roundIndex; - // TODO: use fast-json-stringify this.logFunction(JSON.stringify(results)); } diff --git a/packages/caliper-core/lib/worker/tx-observers/tx-observer-dispatch.js b/packages/caliper-core/lib/worker/tx-observers/tx-observer-dispatch.js index f424e15f5..d0c7264e9 100644 --- a/packages/caliper-core/lib/worker/tx-observers/tx-observer-dispatch.js +++ b/packages/caliper-core/lib/worker/tx-observers/tx-observer-dispatch.js @@ -102,6 +102,16 @@ class TxObserverDispatch extends TxObserverInterface { return; } + if (Array.isArray(results)) { + for (let result of results) { + result.workerIndex = this.workerIndex; + result.roundIndex = this.currentRound; + } + }else { + results.workerIndex = this.workerIndex; + results.roundIndex = this.currentRound; + } + for (let observer of this.txObservers) { observer.txFinished(results); } @@ -109,26 +119,3 @@ class TxObserverDispatch extends TxObserverInterface { } module.exports = TxObserverDispatch; - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/caliper-core/test/worker/tx-observers/logging-tx-observer.js b/packages/caliper-core/test/worker/tx-observers/logging-tx-observer.js new file mode 100644 index 000000000..015973c19 --- /dev/null +++ b/packages/caliper-core/test/worker/tx-observers/logging-tx-observer.js @@ -0,0 +1,116 @@ +/* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); +const expect = chai.expect; +const mockery = require('mockery'); + +chai.use(require('sinon-chai')); + +describe('When monitoring transaction activity', () => { + let createLoggingTxObserver, CaliperUtils, observer, logStubs; + + before(() => { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false + }); + + logStubs = { + info: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }; + + CaliperUtils = { + getLogger: sinon.stub().returns(logStubs) + }; + + mockery.registerMock('../../common/utils/caliper-utils', CaliperUtils); + createLoggingTxObserver = require('../../../lib/worker/tx-observers/logging-tx-observer').createTxObserver; + }); + + beforeEach(() => { + logStubs.info.resetHistory(); + logStubs.warn.resetHistory(); + logStubs.error.resetHistory(); + observer = createLoggingTxObserver({ messageLevel: 'info' }, null, 0); + }); + + afterEach(() => { + sinon.restore(); + }); + + after(() => { + mockery.deregisterAll(); + mockery.disable(); + }); + + describe('On initialization', () => { + const logLevels = ['info', 'warn', 'error']; + + logLevels.forEach(level => { + it(`should use the '${level}' log level if provided in options`, () => { + observer = createLoggingTxObserver({ messageLevel: level }, null, 0); + + // Simulate a finished transaction + const result = { status: 'success' }; + observer.txFinished(result); + + // Ensure the correct logger was called + expect(logStubs[level]).to.have.been.calledOnce; + expect(logStubs[level]).to.have.been.calledWith(JSON.stringify({ + status: 'success', + })); + + // Ensure other loggers were not called + Object.keys(logStubs).forEach(otherLevel => { + if (otherLevel !== level) { + expect(logStubs[otherLevel]).to.not.have.been.called; + } + }); + }); + }); + + }); + + describe('When processing submitted transactions', () => { + it('should ignore submissions and not log any data', () => { + observer.txSubmitted(5); + expect(logStubs.info).to.not.have.been.called; + }); + }); + + describe('When processing finished transactions', () => { + it('should log multiple transaction results', () => { + const results = [{ status: 'success' }, { status: 'failed' }]; + observer.txFinished(results); + expect(logStubs.info).to.have.been.calledTwice; + expect(logStubs.info.firstCall).to.have.been.calledWithMatch(JSON.stringify({ + status: 'success', + })); + expect(logStubs.info.secondCall).to.have.been.calledWithMatch(JSON.stringify({ + status: 'failed', + })); + }); + + it('should handle empty results without logging', () => { + observer.txFinished([]); + expect(logStubs.info).to.not.have.been.called; + }); + }); +}); diff --git a/packages/caliper-core/test/worker/tx-observers/tx-observer-dispatch.js b/packages/caliper-core/test/worker/tx-observers/tx-observer-dispatch.js new file mode 100644 index 000000000..b03e5db4f --- /dev/null +++ b/packages/caliper-core/test/worker/tx-observers/tx-observer-dispatch.js @@ -0,0 +1,209 @@ +/* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +'use strict'; + +const sinon = require('sinon'); +const chai = require('chai'); +const expect = chai.expect; + +const TxObserverDispatch = require('../../../lib/worker/tx-observers/tx-observer-dispatch'); +const TxObserverInterface = require('../../../lib/worker/tx-observers/tx-observer-interface'); +const CaliperUtils = require('../../../lib/common/utils/caliper-utils'); +const { TxStatus } = require('../../../'); + +describe('Transaction Observer Dispatch behavior', function() { + let mockMessenger; + let internalObserver; + let dispatcher; + let mockFactory; + + beforeEach(function() { + // Mocks and stubs + mockMessenger = sinon.stub(); + internalObserver = sinon.createStubInstance(TxObserverInterface); + + // Mock a factory function for creating TX observers + mockFactory = sinon.stub().returns({ + activate: sinon.stub(), + deactivate: sinon.stub(), + txSubmitted: sinon.stub(), + txFinished: sinon.spy(), + }); + + // Stub the utils to return the mock factory function + sinon.stub(CaliperUtils, 'loadModuleFunction').returns(mockFactory); + + // Instantiate the dispatcher + dispatcher = new TxObserverDispatch(mockMessenger, internalObserver, 'managerUuid', 1); + }); + + afterEach(function() { + // Restore any stubs or mocks + sinon.restore(); + }); + + describe('When Activated', function() { + it('should activate all registered observers', async function() { + await dispatcher.activate(0, 'test-round'); + + // Ensure txObservers is not empty + expect(dispatcher.txObservers).to.not.be.empty; + expect(internalObserver.activate.calledOnce).to.be.true; + dispatcher.txObservers.forEach(observer => { + expect(observer.activate.calledOnce).to.be.true; + }); + }); + }); + + describe('When Deactivated', function() { + it('should deactivate all registered observers', async function() { + await dispatcher.activate(0, 'test-round'); + // Deactivate the dispatcher + await dispatcher.deactivate(); + + // Ensure txObservers is not empty + expect(dispatcher.txObservers).to.not.be.empty; + expect(internalObserver.deactivate.calledOnce).to.be.true; + dispatcher.txObservers.forEach(observer => { + expect(observer).to.have.property('deactivate'); + expect(observer.deactivate.calledOnce).to.be.true; + }); + }); + + }); + + describe('When Transaction is Submitted', function() { + it('should forward the transaction submission event to all observers after the dispatcher is activated', async function() { + // Activate the dispatcher first + await dispatcher.activate(0, 'test-round'); + + // Call txSubmitted + dispatcher.txSubmitted(5); + + + // Ensure txObservers is not empty + expect(dispatcher.txObservers).to.not.be.empty; + // Ensure each observer's txSubmitted method was called with the correct count + dispatcher.txObservers.forEach(observer => { + expect(observer.txSubmitted.calledWith(5)).to.be.true; + }); + }); + + + it('should not forward the transaction submission event to observers if the dispatcher is not active', function() { + dispatcher.active = false; + dispatcher.txSubmitted(5); + + // Ensure txObservers is not empty + expect(dispatcher.txObservers).to.not.be.empty; + dispatcher.txObservers.forEach(observer => { + expect(observer.txSubmitted.called).to.be.false; + }); + }); + + }); + + describe('When Transaction is Completed', function() { + it('should forward the transaction completion event to all observers after the dispatcher is activated', async function() { + const mockResult = { status: 'success' }; + + // Activate the dispatcher first + await dispatcher.activate(0, 'test-round'); + + // Call txFinished + dispatcher.txFinished(mockResult); + + + // Ensure txObservers is not empty + expect(dispatcher.txObservers).to.not.be.empty; + // Ensure each observer's txFinished method was called with the correct result + dispatcher.txObservers.forEach(observer => { + expect(observer.txFinished.calledWith(sinon.match({ + status: 'success', + workerIndex: dispatcher.workerIndex, + roundIndex: dispatcher.currentRound, + }))).to.be.true; + }); + }); + + it('should not forward the transaction completion event to observers if the dispatcher is not active', function() { + dispatcher.active = false; + const mockResult = { status: 'success' }; + dispatcher.txFinished(mockResult); + + + // Ensure txObservers is not empty + expect(dispatcher.txObservers).to.not.be.empty; + dispatcher.txObservers.forEach(observer => { + expect(observer.txFinished.called).to.be.false; + }); + }); + + it('should correctly process a single TxStatus object', async function() { + // Create a TxStatus object with a string result field + const txStatus = new TxStatus('tx1'); + txStatus.SetStatusSuccess(); + txStatus.result = 'Some string result'; + + // Activate the dispatcher first + await dispatcher.activate(0, 'test-round'); + + // Call txFinished with the TxStatus object + dispatcher.txFinished(txStatus); + + // Ensure txObservers is not empty + expect(dispatcher.txObservers).to.not.be.empty; + + // Assert that txStatus now has workerIndex and roundIndex set + expect(txStatus.workerIndex).to.equal(dispatcher.workerIndex); + expect(txStatus.roundIndex).to.equal(dispatcher.currentRound); + + dispatcher.txObservers.forEach(observer => { + expect(observer.txFinished.calledOnce).to.be.true; + const calledArg = observer.txFinished.getCall(0).args[0]; + expect(calledArg).to.equal(txStatus); + }); + }); + + it('should correctly process an array of TxStatus objects', async function() { + const txStatus1 = new TxStatus('tx1'); + txStatus1.SetStatusSuccess(); + txStatus1.result = 'Result 1'; + + const txStatus2 = new TxStatus('tx2'); + txStatus2.SetStatusFail(); + txStatus2.result = 'Result 2'; + + const resultsArray = [txStatus1, txStatus2]; + await dispatcher.activate(0, 'test-round'); + + dispatcher.txFinished(resultsArray); + expect(dispatcher.txObservers).to.not.be.empty; + + resultsArray.forEach(txStatus => { + expect(txStatus.workerIndex).to.equal(dispatcher.workerIndex); + expect(txStatus.roundIndex).to.equal(dispatcher.currentRound); + }); + + dispatcher.txObservers.forEach(observer => { + expect(observer.txFinished.calledOnce).to.be.true; + const calledArg = observer.txFinished.getCall(0).args[0]; + expect(calledArg).to.deep.equal(resultsArray); + }); + }); + + + }); +});