From 434e1cfb59ae4ed8081f403805320cde9944a807 Mon Sep 17 00:00:00 2001 From: Nadeem Date: Mon, 9 Mar 2026 14:53:04 +0100 Subject: [PATCH 1/3] test: initial commit for testdoublebufferaccessor. --- src/DoubleBufferAccessor.cc | 2 +- src/NumericAddressedBackend.cc | 2 + .../testDoubleBufferAccessor.cc | 211 ++++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 tests/executables_src/testDoubleBufferAccessor.cc diff --git a/src/DoubleBufferAccessor.cc b/src/DoubleBufferAccessor.cc index d9158f300..e740b4bf0 100644 --- a/src/DoubleBufferAccessor.cc +++ b/src/DoubleBufferAccessor.cc @@ -16,7 +16,7 @@ namespace ChimeraTK { backend->getRegisterAccessor(_doubleBufferInfo.enableRegisterPath, 1, _doubleBufferInfo.index, {}); _currentBufferNumberReg = backend->getRegisterAccessor( _doubleBufferInfo.inactiveBufferRegisterPath, 1, _doubleBufferInfo.index, {}); - + std::cout << "numberOfWords:" << numberOfWords << std::endl; auto buf0Name = registerPathName + ".BUF0"; auto buf1Name = registerPathName + ".BUF1"; diff --git a/src/NumericAddressedBackend.cc b/src/NumericAddressedBackend.cc index 4082dde23..c4ef0c339 100644 --- a/src/NumericAddressedBackend.cc +++ b/src/NumericAddressedBackend.cc @@ -144,6 +144,8 @@ namespace ChimeraTK { boost::shared_ptr> accessor; // obtain register info auto registerInfo = getRegisterInfo(registerPathName); + std::cout << "registerInfo.getNumberOfDimensions():" << registerInfo.getNumberOfDimensions() << std::endl; + std::cout << "registerPathName:" << registerPathName << std::endl; if(registerInfo.doubleBuffer == std::nullopt) { // 1D or scalar register if(registerInfo.getNumberOfDimensions() <= 1) { diff --git a/tests/executables_src/testDoubleBufferAccessor.cc b/tests/executables_src/testDoubleBufferAccessor.cc new file mode 100644 index 000000000..c480a0486 --- /dev/null +++ b/tests/executables_src/testDoubleBufferAccessor.cc @@ -0,0 +1,211 @@ +#define BOOST_TEST_MODULE testDoubleBufferAccessor +#include "Device.h" +#include "DoubleBufferAccessor.h" +#include "DummyBackend.h" + +#include + +using namespace ChimeraTK; + +namespace { + + /* expose protected members */ + template + class TestableDoubleBufferAccessor : public DoubleBufferAccessor { + public: + using DoubleBufferAccessor::DoubleBufferAccessor; + using DoubleBufferAccessor::buffer_2D; + }; + +} // namespace + +BOOST_AUTO_TEST_CASE(test_read_buffer0) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + BOOST_REQUIRE(backend); + + auto mutex = std::make_shared(); + + auto registerInfo = backend->getRegisterInfo("DAQ.FD"); + BOOST_REQUIRE(registerInfo.doubleBuffer != std::nullopt); + + auto dbInfo = registerInfo.doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor( + dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 16384, 0, AccessModeFlags{}); + + /* firmware-visible buffers */ + auto buf0 = device.getTwoDRegisterAccessor("/DAQ/FD/BUF0"); + auto buf1 = device.getTwoDRegisterAccessor("/DAQ/FD/BUF1"); + + auto inactive = device.getOneDRegisterAccessor("/DAQ/DOUBLE_BUF/INACTIVE_BUF_ID"); + + /* simulate firmware writing to buffer0 */ + buf0[0][0] = 4; + buf0[0][1] = 8; + buf0[0][2] = 12; + buf0[0][3] = 16; + buf0.write(); + + /* firmware state: + inactive = 1 → firmware writes BUF0 + */ + + inactive[0] = 1; + inactive.write(); + + // enable[1] = 1; + // enable.write(); + + accessor.doPreRead(TransferType::read); + accessor.doReadTransferSynchronously(); + accessor.doPostRead(TransferType::read, true); + + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][0], 4); + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][1], 8); + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][2], 12); + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][3], 16); + + inactive[0] = 0; + inactive.write(); + + buf1[0][0] = 140; + buf1[0][1] = 144; + buf1[0][2] = 148; + buf1[0][3] = 152; + buf1.write(); + + accessor.doPreRead(TransferType::read); + accessor.doReadTransferSynchronously(); + accessor.doPostRead(TransferType::read, true); + + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][0], 140); + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][1], 144); + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][2], 148); + BOOST_CHECK_EQUAL(accessor.buffer_2D[0][3], 152); +} + +BOOST_AUTO_TEST_CASE(test_transfer_lock_blocks_other_accessor) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + BOOST_REQUIRE(backend); + + auto mutex = std::make_shared(); + + auto registerInfo = backend->getRegisterInfo("DAQ.FD"); + auto dbInfo = registerInfo.doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor1( + dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 16384, 0, AccessModeFlags{}); + + TestableDoubleBufferAccessor accessor2( + dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 16384, 0, AccessModeFlags{}); + + std::atomic secondEntered{false}; + + /* Thread 1 acquires transfer lock */ + accessor1.doPreRead(TransferType::read); + + /* start accessor2 asynchronously */ + auto future = std::async(std::launch::async, [&] { + accessor2.doPreRead(TransferType::read); + accessor2.doPostRead(TransferType::read, false); + }); + + /* accessor2 must still be blocked */ + auto status = future.wait_for(std::chrono::milliseconds(50)); + BOOST_CHECK(status == std::future_status::timeout); + + /* release lock */ + accessor1.doPostRead(TransferType::read, false); + + /* now accessor2 must complete */ + status = future.wait_for(std::chrono::milliseconds(200)); + BOOST_CHECK(status == std::future_status::ready); +} + +BOOST_AUTO_TEST_CASE(test_mutex_usecount) { + auto mutex = std::make_shared(); + + mutex->lock(); + BOOST_CHECK_EQUAL(mutex->useCount(), 1); + + mutex->lock(); + BOOST_CHECK_EQUAL(mutex->useCount(), 2); + + mutex->unlock(); + BOOST_CHECK_EQUAL(mutex->useCount(), 1); + + mutex->unlock(); + BOOST_CHECK_EQUAL(mutex->useCount(), 0); +} + +BOOST_AUTO_TEST_CASE(test_exception_does_not_leave_lock) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + BOOST_REQUIRE(backend); + + auto mutex = std::make_shared(); + + auto registerInfo = backend->getRegisterInfo("DAQ.FD"); + auto dbInfo = registerInfo.doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor( + dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 16384, 0, AccessModeFlags{}); + + // Acquire lock via pre-read + accessor.doPreRead(TransferType::read); + BOOST_CHECK_EQUAL(mutex->useCount(), 1); // lock acquired + + // Simulate exception during transfer + bool exceptionThrown = false; + try { + throw std::runtime_error("simulated transfer exception"); + } + catch(const std::runtime_error&) { + exceptionThrown = true; + // mandate: still call postRead to release lock + accessor.doPostRead(TransferType::read, false); + } + + BOOST_CHECK(exceptionThrown); + BOOST_CHECK_EQUAL(mutex->useCount(), 0); // lock released +} + +// ------------------------------------------------------------ +// Test that _enableDoubleBufferReg toggles during pre/post read +BOOST_AUTO_TEST_CASE(test_firmware_handshake_toggle) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + auto mutex = std::make_shared(); + auto registerInfo = backend->getRegisterInfo("DAQ.FD"); + auto dbInfo = registerInfo.doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor( + dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 16384, 0, AccessModeFlags{}); + + // Get the actual register for handshake + auto enableReg = device.getOneDRegisterAccessor("/DAQ/DOUBLE_BUF/ENA"); + enableReg.read(); + BOOST_CHECK_EQUAL(enableReg[0], 1); // must be enabled*/ + + // Pre-read should disable the double buffer + accessor.doPreRead(TransferType::read); + enableReg.read(); + BOOST_CHECK_EQUAL(enableReg[0], 0); // must be disabled + + // Read transfer + accessor.doReadTransferSynchronously(); + + // Post-read should re-enable the double buffer + accessor.doPostRead(TransferType::read, true); + enableReg.read(); + BOOST_CHECK_EQUAL(enableReg[0], 1); // must be re-enabled*/ +} From 4d98647b924b3fe647239a57edd48d9d4ba36a1a Mon Sep 17 00:00:00 2001 From: Nadeem Date: Tue, 10 Mar 2026 11:19:17 +0100 Subject: [PATCH 2/3] tests: extended testDoubleBufferAccessor --- .../testDoubleBufferAccessor.cc | 137 +++++++++++++++++- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/tests/executables_src/testDoubleBufferAccessor.cc b/tests/executables_src/testDoubleBufferAccessor.cc index c480a0486..cf5b399cd 100644 --- a/tests/executables_src/testDoubleBufferAccessor.cc +++ b/tests/executables_src/testDoubleBufferAccessor.cc @@ -16,10 +16,9 @@ namespace { using DoubleBufferAccessor::DoubleBufferAccessor; using DoubleBufferAccessor::buffer_2D; }; - } // namespace -BOOST_AUTO_TEST_CASE(test_read_buffer0) { +BOOST_AUTO_TEST_CASE(test_current_buffer_selection) { Device device; device.open("(dummy?map=simpleJsonFile.jmap)"); @@ -56,9 +55,6 @@ BOOST_AUTO_TEST_CASE(test_read_buffer0) { inactive[0] = 1; inactive.write(); - // enable[1] = 1; - // enable.write(); - accessor.doPreRead(TransferType::read); accessor.doReadTransferSynchronously(); accessor.doPostRead(TransferType::read, true); @@ -209,3 +205,134 @@ BOOST_AUTO_TEST_CASE(test_firmware_handshake_toggle) { enableReg.read(); BOOST_CHECK_EQUAL(enableReg[0], 1); // must be re-enabled*/ } + +// ------------------------------------------------------------ +// Test that hasNewData=false does not swap buffer_2D +BOOST_AUTO_TEST_CASE(test_has_new_data_false) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + auto mutex = std::make_shared(); + auto registerInfo = backend->getRegisterInfo("DAQ.FD"); + auto dbInfo = registerInfo.doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor(dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 4, 0, AccessModeFlags{}); + + auto buf0 = device.getTwoDRegisterAccessor("/DAQ/FD.BUF0"); + auto inactive = device.getOneDRegisterAccessor("/DAQ/DOUBLE_BUF/INACTIVE_BUF_ID"); + inactive[0] = 1; + inactive.write(); + + buf0[0] = {100, 200, 300, 400}; + buf0.write(); + + // Initial read + accessor.doPreRead(TransferType::read); + accessor.doReadTransferSynchronously(); + accessor.doPostRead(TransferType::read, true); + + auto oldBuffer0 = accessor.buffer_2D[0]; + + // update reg + buf0[0] = {20, 40, 60, 80}; + buf0.write(); + + // hasNewData=false → buffer_2D should not update + accessor.doPreRead(TransferType::read); + accessor.doReadTransferSynchronously(); + accessor.doPostRead(TransferType::read, false); + + BOOST_CHECK_EQUAL_COLLECTIONS( + oldBuffer0.begin(), oldBuffer0.end(), accessor.buffer_2D[0].begin(), accessor.buffer_2D[0].end() // still old + ); + // hasNewData=true → buffer_2D should update + accessor.doPreRead(TransferType::read); + accessor.doReadTransferSynchronously(); + accessor.doPostRead(TransferType::read, true); + + BOOST_CHECK_EQUAL_COLLECTIONS( + buf0[0].begin(), buf0[0].end(), accessor.buffer_2D[0].begin(), accessor.buffer_2D[0].end() // now updated + ); +} +// ------------------------------------------------------------ +// Test multiple channels +BOOST_AUTO_TEST_CASE(test_multiple_channels) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + auto mutex = std::make_shared(); + auto dbInfo = backend->getRegisterInfo("DAQ.FD").doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor(dbInfo, backend, mutex, "/DAQ/FD", 4, 0, AccessModeFlags{}); + + auto buf0 = device.getTwoDRegisterAccessor("/DAQ/FD.BUF0"); + auto inactive = device.getOneDRegisterAccessor("/DAQ/DOUBLE_BUF/INACTIVE_BUF_ID"); + inactive[0] = 1; + inactive.write(); + + // simulate 2 channels + buf0[0] = {16, 20, 24, 32}; + buf0[1] = {100, 200, 300, 400}; + buf0.write(); + + accessor.doPreRead(TransferType::read); + accessor.doReadTransferSynchronously(); + accessor.doPostRead(TransferType::read, true); + + BOOST_CHECK_EQUAL_COLLECTIONS( + buf0[0].begin(), buf0[0].end(), accessor.buffer_2D[0].begin(), accessor.buffer_2D[0].end()); + BOOST_CHECK_EQUAL_COLLECTIONS( + buf0[1].begin(), buf0[1].end(), accessor.buffer_2D[1].begin(), accessor.buffer_2D[1].end()); +} + +// ------------------------------------------------------------ +// Test mayReplaceOther logic +BOOST_AUTO_TEST_CASE(test_may_replace_other) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + auto mutex = std::make_shared(); + auto registerInfo = backend->getRegisterInfo("DAQ.FD"); + auto dbInfo = registerInfo.doubleBuffer.value(); + + auto accessor1 = boost::make_shared>( + dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 16384, 0, AccessModeFlags{}); + + auto accessor2 = boost::make_shared>( + dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 16384, 0, AccessModeFlags{}); + + // must not replace itself + BOOST_CHECK(!accessor1->mayReplaceOther(accessor1)); + + BOOST_CHECK(accessor1->mayReplaceOther(accessor2)); +} + +BOOST_AUTO_TEST_CASE(test_write_not_allowed) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + auto mutex = std::make_shared(); + auto dbInfo = backend->getRegisterInfo("DAQ.FD").doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor(dbInfo, backend, mutex, "/DAQ/FD", 4, 0, AccessModeFlags{}); + + BOOST_CHECK_THROW(accessor.doPreWrite(TransferType::write, {}), ChimeraTK::logic_error); + + // doPostWrite does nothing but should not throw + BOOST_CHECK_NO_THROW(accessor.doPostWrite(TransferType::write, {})); +} + +// ------------------------------------------------------------ +// Edge case: numberOfWords = 0 +BOOST_AUTO_TEST_CASE(test_zero_words) { + Device device; + device.open("(dummy?map=simpleJsonFile.jmap)"); + auto backend = boost::dynamic_pointer_cast(device.getBackend()); + auto mutex = std::make_shared(); + auto registerInfo = backend->getRegisterInfo("DAQ.FD"); + auto dbInfo = registerInfo.doubleBuffer.value(); + + TestableDoubleBufferAccessor accessor(dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 0, 0, AccessModeFlags{}); + + BOOST_CHECK_EQUAL(accessor.getNumberOfSamples(), 0); +} From e3e54aa378f607522490a022945f223e79b1eac8 Mon Sep 17 00:00:00 2001 From: Nadeem Date: Tue, 10 Mar 2026 16:30:28 +0100 Subject: [PATCH 3/3] fix: removed forgotten debug cout. --- src/DoubleBufferAccessor.cc | 2 +- src/NumericAddressedBackend.cc | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/DoubleBufferAccessor.cc b/src/DoubleBufferAccessor.cc index e740b4bf0..d9158f300 100644 --- a/src/DoubleBufferAccessor.cc +++ b/src/DoubleBufferAccessor.cc @@ -16,7 +16,7 @@ namespace ChimeraTK { backend->getRegisterAccessor(_doubleBufferInfo.enableRegisterPath, 1, _doubleBufferInfo.index, {}); _currentBufferNumberReg = backend->getRegisterAccessor( _doubleBufferInfo.inactiveBufferRegisterPath, 1, _doubleBufferInfo.index, {}); - std::cout << "numberOfWords:" << numberOfWords << std::endl; + auto buf0Name = registerPathName + ".BUF0"; auto buf1Name = registerPathName + ".BUF1"; diff --git a/src/NumericAddressedBackend.cc b/src/NumericAddressedBackend.cc index c4ef0c339..4082dde23 100644 --- a/src/NumericAddressedBackend.cc +++ b/src/NumericAddressedBackend.cc @@ -144,8 +144,6 @@ namespace ChimeraTK { boost::shared_ptr> accessor; // obtain register info auto registerInfo = getRegisterInfo(registerPathName); - std::cout << "registerInfo.getNumberOfDimensions():" << registerInfo.getNumberOfDimensions() << std::endl; - std::cout << "registerPathName:" << registerPathName << std::endl; if(registerInfo.doubleBuffer == std::nullopt) { // 1D or scalar register if(registerInfo.getNumberOfDimensions() <= 1) {