Skip to content

Test double buffer accessor ns#551

Open
nshehz wants to merge 3 commits into
masterfrom
test-DoubleBufferAccessor-ns
Open

Test double buffer accessor ns#551
nshehz wants to merge 3 commits into
masterfrom
test-DoubleBufferAccessor-ns

Conversation

@nshehz
Copy link
Copy Markdown
Contributor

@nshehz nshehz commented Mar 10, 2026

No description provided.

@nshehz nshehz requested a review from killenb March 10, 2026 16:15
@nshehz nshehz assigned nshehz and unassigned killenb Mar 10, 2026
Copy link
Copy Markdown
Member

@killenb killenb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You put a lot of effort in identifying the test cases that need to be covered. However, basically all tests have issues:

  • They are not testing along the public API and will break when refactoring
  • They are often testing implementation details instead of wanted behaviour. Some do not have any logical test coverage at all.
  • Many tests are fully covered by the UnifiedBackendTest, and most can be implemented to be checked there. There are only two test cases which do not work out of the box
    • The test with two accessors locking each other in different threads
    • The test that the double buffering is turned off while reading (is missing a callback in the dummy. We should extend the dummy here. I was just missing this in a test this week, but could work around it because my accessors are writeable. There is a write callback). But it's also not cleanly testable here without the callback.

We should move all tests that can be covered by the UnifiedBackendTest to a testDoubleBufferAccessorUnified, and only have the ones that cannot be handles there in this dedicated test.

  • One test case is missing: Two different registers which are using the same double buffering handshare (e.g. FD and the according macro pulse number). This will also be a dedicated test at the moment, because the unified backend test cannot handle multiple registers.

Comment on lines +76 to +78
accessor.doPreRead(TransferType::read);
accessor.doReadTransferSynchronously();
accessor.doPostRead(TransferType::read, true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you call these individually? It should just be accessor.read().

Comment on lines +58 to +60
accessor.doPreRead(TransferType::read);
accessor.doReadTransferSynchronously();
accessor.doPostRead(TransferType::read, true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you call these individually? It should just be accessor.read().

Comment on lines +12 to +18
/* expose protected members */
template<typename T>
class TestableDoubleBufferAccessor : public DoubleBufferAccessor<T> {
public:
using DoubleBufferAccessor<T>::DoubleBufferAccessor;
using DoubleBufferAccessor<T>::buffer_2D;
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't. You should test observable behaviour, not implementation details. Internal interfaces might be subject to refactoring, which breaks the test.


auto dbInfo = registerInfo.doubleBuffer.value();

TestableDoubleBufferAccessor<int> accessor(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just get the accessor through the regular device interface. Use it as normal code would.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW. This call is wrong. It needs the mutex that is shared by the backend. You can't just give "any" mutex.


BOOST_AUTO_TEST_CASE(test_current_buffer_selection) {
Device device;
device.open("(dummy?map=simpleJsonFile.jmap)");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should get our own jmap file. The "simple" now contains everything, which is very crowded and difficult to understand. This is an additional hurdle when trying to debug stuff. A stripped down map file makes this easier.

Comment on lines +143 to +176
BOOST_AUTO_TEST_CASE(test_exception_does_not_leave_lock) {
Device device;
device.open("(dummy?map=simpleJsonFile.jmap)");

auto backend = boost::dynamic_pointer_cast<NumericAddressedBackend>(device.getBackend());
BOOST_REQUIRE(backend);

auto mutex = std::make_shared<detail::CountedRecursiveMutex>();

auto registerInfo = backend->getRegisterInfo("DAQ.FD");
auto dbInfo = registerInfo.doubleBuffer.value();

TestableDoubleBufferAccessor<int> 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
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not test anything. The accessor does not know about the exception at all.
You are just calling doPreaRead() and doPostRead() and do "something" in between.

The correct test would be: Use the exception backend to raise an exception during transfer. Then check that the next transfer in another thread can be executed (no deadlock). This is observable behaviour from the outside, that's all that counts.

But don't implement it here. It is all covered by the UnifiedBackendTest.

Comment on lines +210 to +256
// 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<NumericAddressedBackend>(device.getBackend());
auto mutex = std::make_shared<detail::CountedRecursiveMutex>();
auto registerInfo = backend->getRegisterInfo("DAQ.FD");
auto dbInfo = registerInfo.doubleBuffer.value();

TestableDoubleBufferAccessor<int> accessor(dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 4, 0, AccessModeFlags{});

auto buf0 = device.getTwoDRegisterAccessor<int>("/DAQ/FD.BUF0");
auto inactive = device.getOneDRegisterAccessor<int>("/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
);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, you are testing deep down of the stuff where the framework is responsible. Calling doPostRead() with newData flag true and false is done by the TransferElement. This is all covered by the UnifiedBackendTest. No need to write dedicated tests here.

Comment on lines +258 to +286
// Test multiple channels
BOOST_AUTO_TEST_CASE(test_multiple_channels) {
Device device;
device.open("(dummy?map=simpleJsonFile.jmap)");
auto backend = boost::dynamic_pointer_cast<NumericAddressedBackend>(device.getBackend());
auto mutex = std::make_shared<detail::CountedRecursiveMutex>();
auto dbInfo = backend->getRegisterInfo("DAQ.FD").doubleBuffer.value();

TestableDoubleBufferAccessor<int> accessor(dbInfo, backend, mutex, "/DAQ/FD", 4, 0, AccessModeFlags{});

auto buf0 = device.getTwoDRegisterAccessor<int>("/DAQ/FD.BUF0");
auto inactive = device.getOneDRegisterAccessor<int>("/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());
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this test that has anything to do with double buffering that is not tested above?

BOOST_CHECK(!accessor1->mayReplaceOther(accessor1));

BOOST_CHECK(accessor1->mayReplaceOther(accessor2));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is covered by the UnifiedBackendTest. No need to write a dedicated test.

Comment on lines +326 to +338
// 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<NumericAddressedBackend>(device.getBackend());
auto mutex = std::make_shared<detail::CountedRecursiveMutex>();
auto registerInfo = backend->getRegisterInfo("DAQ.FD");
auto dbInfo = registerInfo.doubleBuffer.value();

TestableDoubleBufferAccessor<int> accessor(dbInfo, backend, mutex, RegisterPath("/DAQ/FD"), 0, 0, AccessModeFlags{});

BOOST_CHECK_EQUAL(accessor.getNumberOfSamples(), 0);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an edge cage but a wrong test. An Accessor with 0 elements should not exist, it should even have an assertion that it never is instantiated.

Correct behaviour: If call device::getTwoDRegisterAccessor() (or OneD), with nWords =0, the backend's getRegisterAccessor_impl is returning an accessor of the full length specified in the map file.

I don't know if this is covered in the UnifiedBackendTest. If not, it should be added there (and in the specification if it is not there). Should be checked.

Copy link
Copy Markdown
Member

@killenb killenb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please squash all commits into one. It just adds one file. The history how this was done can be squashed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants