StimulusProducer: add duplicate-quale guard and attach-identity buffer lookup.

Provide ensureNoDuplicateQualeIface and getAttachedStimulusBufferByAttachIdentity
so session-scoped stimBuff plugins can reject duplicate quales and detach by
stable DAP line identity rather than full spec equality.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 11:02:18 -04:00
parent 7af684039d
commit 809861be2b
4 changed files with 353 additions and 0 deletions
@@ -82,6 +82,32 @@ std::shared_ptr<StimulusBuffer> StimulusProducer::getAttachedStimulusBuffer(
return nullptr;
}
std::shared_ptr<StimulusBuffer> StimulusProducer::getAttachedStimulusBufferByAttachIdentity(
const std::string& deviceIdentifier,
const std::string& qualeIfaceApi) const
{
for (const auto& buffer : attachedStimulusBuffers)
{
if (!buffer || !buffer->deviceAttachmentSpec)
{
throw std::runtime_error(
"StimulusProducer::getAttachedStimulusBufferByAttachIdentity: "
"encountered null buffer or null deviceAttachmentSpec in "
"attachedStimulusBuffers (should never happen)");
}
if (buffer->deviceAttachmentSpec->deviceIdentifier != deviceIdentifier)
{ continue; }
if (buffer->deviceAttachmentSpec->qualeIfaceApi != qualeIfaceApi)
{ continue; }
return buffer;
}
return nullptr;
}
bool StimulusProducer::hasBufferWithQualeIfaceApi(
const std::string& qualeIfaceApi) const
{
@@ -104,6 +130,18 @@ bool StimulusProducer::hasBufferWithQualeIfaceApi(
return false;
}
void StimulusProducer::ensureNoDuplicateQualeIface(
const std::string& qualeIfaceApi) const
{
if (!hasBufferWithQualeIfaceApi(qualeIfaceApi)) {
return;
}
throw std::runtime_error(
"duplicate qualeIface '" + qualeIfaceApi
+ "' for this producer session");
}
bool StimulusProducer::addAttachedStimulusBufferIfNotExists(
const std::shared_ptr<StimulusBuffer>& buffer)
{
@@ -41,3 +41,29 @@ add_dependencies(intrinThresholdParams_tests gtest_main)
add_test(
NAME intrinThresholdParams_tests
COMMAND intrinThresholdParams_tests)
add_executable(stimulusProducerQualeIface_tests
stimulusProducerQualeIface_tests.cpp
)
target_include_directories(stimulusProducerQualeIface_tests PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
${CMAKE_SOURCE_DIR}/smocore/include
)
target_link_libraries(stimulusProducerQualeIface_tests
gtest_main
attachmentSupport
spinscale
spinscale_test_support
${Boost_LIBRARIES}
${OPENCL_LIBRARIES}
)
add_dependencies(stimulusProducerQualeIface_tests gtest_main)
add_test(
NAME stimulusProducerQualeIface_tests
COMMAND stimulusProducerQualeIface_tests)
@@ -0,0 +1,281 @@
#include <boost/asio/io_context.hpp>
#include <gtest/gtest.h>
#include <opts.h>
#include <spinscale/componentThread.h>
#include <spinscale/co/invokers.h>
#include <spinscale/syncCancelerForAsyncWork.h>
#include <support/exceptionAssertions.h>
#include <unistd.h>
#include <user/comparatorApiDesc.h>
#include <user/compute.h>
#include <user/stimulusBuffer.h>
#include <user/stimulusProducer.h>
#define CL_TARGET_OPENCL_VERSION 120
#include <CL/cl.h>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace smo {
namespace stim_buff {
namespace {
class TestStimulusProducer
: public StimulusProducer
{
public:
using StimulusProducer::StimulusProducer;
std::shared_ptr<StimulusBuffer> getOrCreateAttachedStimulusBuffer(
const std::shared_ptr<device::DeviceAttachmentSpec>&
deviceAttachmentSpec) override
{
(void)deviceAttachmentSpec;
throw std::logic_error("not used in these unit tests");
}
bool exportsQualeIfaceApi(const std::string& qualeIfaceApi) const override
{
(void)qualeIfaceApi;
return true;
}
protected:
sscl::co::ViralNonPostingInvoker<void> stimFrameProductionTimesliceCInd(
sscl::SyncCancelerForAsyncWork& canceler) override
{
(void)canceler;
co_return;
}
};
std::optional<std::string> searchForLibInSmoSearchPathsStub(
const std::string& libraryPath)
{
(void)libraryPath;
return std::nullopt;
}
std::shared_ptr<sscl::ComponentThread> componentThreadGetSelfStub()
{
return sscl::ComponentThread::getSelf();
}
OptionParser& optionParserGetOptionsStub()
{
return OptionParser::getOptions();
}
void releaseUseHostPtrBufferStub(
std::shared_ptr<smo::compute::ClBuffer> buffer)
{
(void)buffer;
}
std::shared_ptr<smo::compute::ComputeDevice> computeManagerGetDeviceStub()
{
return nullptr;
}
void computeManagerReleaseDeviceStub(
std::shared_ptr<smo::compute::ComputeDevice> device)
{
(void)device;
}
std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>
comparatorManagerGetComparatorTypeStub(smo::cologex::ComparatorTypeId typeId)
{
(void)typeId;
return nullptr;
}
std::unique_ptr<smo::cologex::Comparator> comparatorGetNewInstanceStub(
const std::shared_ptr<smo::cologex::ExportedComparatorTypeDesc>& comparatorType)
{
(void)comparatorType;
return nullptr;
}
std::shared_ptr<smo::compute::ClBuffer> createUseHostPtrBufferStub(
void* hostPtr,
size_t size,
cl_mem_flags flags)
{
static const std::vector<std::shared_ptr<smo::compute::ComputeDevice>>
emptyDevices;
return std::make_shared<smo::compute::ClBuffer>(
hostPtr, size, flags, emptyDevices);
}
const SmoCallbacks kTestSmoCallbacks = {
.searchForLibInSmoSearchPaths = searchForLibInSmoSearchPathsStub,
.ComponentThread_getSelf = componentThreadGetSelfStub,
.OptionParser_getOptions = optionParserGetOptionsStub,
.ComputeManager_createUseHostPtrBuffer = createUseHostPtrBufferStub,
.ComputeManager_releaseUseHostPtrBuffer = releaseUseHostPtrBufferStub,
.ComputeManager_getDevice = computeManagerGetDeviceStub,
.ComputeManager_releaseDevice = computeManagerReleaseDeviceStub,
.ComparatorManager_getComparatorType =
comparatorManagerGetComparatorTypeStub,
.Comparator_getNewInstance = comparatorGetNewInstanceStub,
};
StagingBuffer::IOEngineConstraints testBufferConstraints()
{
const size_t pageSize = static_cast<size_t>(sysconf(_SC_PAGE_SIZE));
const size_t channelByteSize = pageSize;
return StagingBuffer::IOEngineConstraints(
1,
channelByteSize,
pageSize,
pageSize);
}
std::shared_ptr<device::DeviceAttachmentSpec> makeAttachSpec(
const std::string& deviceSelector,
const std::string& qualeIfaceApi,
const std::string& deviceIdentifier = "cam0")
{
auto spec = std::make_shared<device::DeviceAttachmentSpec>();
spec->deviceIdentifier = deviceIdentifier;
spec->sensorType = 'e';
spec->qualeIfaceApi = qualeIfaceApi;
spec->stimBuffApi = "testBuff";
spec->provider = "testProvider";
spec->deviceSelector = deviceSelector;
return spec;
}
std::shared_ptr<StimulusBuffer> makeAttachedTestBuffer(
TestStimulusProducer& producer,
const std::shared_ptr<device::DeviceAttachmentSpec>& attachSpec)
{
const StagingBuffer::IOEngineConstraints constraints =
testBufferConstraints();
return std::make_shared<StimulusBuffer>(
producer,
attachSpec,
CONFIG_STIMBUFF_FRAME_PERIOD_MS,
constraints,
constraints,
kTestSmoCallbacks,
CL_MEM_READ_WRITE);
}
class StimulusProducerQualeIfaceTest
: public ::testing::Test
{
protected:
boost::asio::io_context ioContext;
std::shared_ptr<device::DeviceAttachmentSpec> producerSpec =
makeAttachSpec("model-substr:USB;location:external", "colour-yuv-y");
TestStimulusProducer producer{producerSpec, ioContext};
};
TEST_F(StimulusProducerQualeIfaceTest, HasBufferWithQualeIfaceApiDetectsChannel)
{
EXPECT_FALSE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
const auto attachSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y");
const auto buffer = makeAttachedTestBuffer(producer, attachSpec);
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(buffer));
EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
EXPECT_FALSE(producer.hasBufferWithQualeIfaceApi("colour-yuv-u"));
}
TEST_F(StimulusProducerQualeIfaceTest,
AddAttachedStimulusBufferIfNotExistsAllowsSameQualeDifferentSelector)
{
const auto firstSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y-0");
const auto firstBuffer = makeAttachedTestBuffer(producer, firstSpec);
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(firstBuffer));
const auto secondSpec = makeAttachSpec(
"model-substr:USB Video Device;location:external",
"colour-yuv-y",
"cam-y-1");
const auto secondBuffer = makeAttachedTestBuffer(producer, secondSpec);
EXPECT_TRUE(producer.addAttachedStimulusBufferIfNotExists(secondBuffer));
EXPECT_EQ(producer.attachedStimulusBuffers.size(), 2u);
EXPECT_TRUE(producer.hasBufferWithQualeIfaceApi("colour-yuv-y"));
EXPECT_NE(firstSpec->deviceSelector, secondSpec->deviceSelector);
}
TEST_F(StimulusProducerQualeIfaceTest,
EnsureNoDuplicateQualeIfaceRejectsAlternateSelector)
{
const auto firstSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y-0");
const auto firstBuffer = makeAttachedTestBuffer(producer, firstSpec);
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(firstBuffer));
const auto duplicateQualeSpec = makeAttachSpec(
"model-substr:USB Video Device;location:external",
"colour-yuv-y",
"cam-y-1");
try {
producer.ensureNoDuplicateQualeIface(
duplicateQualeSpec->qualeIfaceApi);
FAIL() << "Expected std::runtime_error";
}
catch (const std::runtime_error& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"duplicate qualeIface 'colour-yuv-y'");
}
}
TEST_F(StimulusProducerQualeIfaceTest,
EnsureNoDuplicateQualeIfaceAllowsDistinctChannels)
{
const auto ySpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-y", "cam-y");
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(
makeAttachedTestBuffer(producer, ySpec)));
EXPECT_NO_THROW(
producer.ensureNoDuplicateQualeIface("colour-yuv-u"));
EXPECT_NO_THROW(
producer.ensureNoDuplicateQualeIface("colour-yuv-v"));
}
TEST_F(StimulusProducerQualeIfaceTest,
GetAttachedStimulusBufferByAttachIdentityIgnoresSelectorString)
{
const auto attachedSpec = makeAttachSpec(
"model-substr:USB;location:external", "colour-yuv-u", "cam-u");
ASSERT_TRUE(producer.addAttachedStimulusBufferIfNotExists(
makeAttachedTestBuffer(producer, attachedSpec)));
const auto foundBuffer =
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-u", "colour-yuv-u");
ASSERT_NE(foundBuffer, nullptr);
EXPECT_EQ(foundBuffer, producer.attachedStimulusBuffers.front());
EXPECT_NE(
foundBuffer->deviceAttachmentSpec->deviceSelector,
"model-substr:USB Video Device;location:external");
EXPECT_EQ(
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-u", "colour-yuv-y"),
nullptr);
EXPECT_EQ(
producer.getAttachedStimulusBufferByAttachIdentity(
"cam-y", "colour-yuv-u"),
nullptr);
}
} // namespace
} // namespace stim_buff
} // namespace smo
+8
View File
@@ -56,6 +56,10 @@ public:
virtual std::shared_ptr<StimulusBuffer> getAttachedStimulusBuffer(
const std::shared_ptr<device::DeviceAttachmentSpec>& spec) const;
std::shared_ptr<StimulusBuffer> getAttachedStimulusBufferByAttachIdentity(
const std::string& deviceIdentifier,
const std::string& qualeIfaceApi) const;
virtual std::shared_ptr<StimulusBuffer> getOrCreateAttachedStimulusBuffer(
const std::shared_ptr<device::DeviceAttachmentSpec>
&deviceAttachmentSpec) = 0;
@@ -72,6 +76,10 @@ public:
// Check if any attached buffer has the specified qualeIfaceApi
bool hasBufferWithQualeIfaceApi(const std::string& qualeIfaceApi) const;
/** Reject a second buffer for the same qualeIface on this producer session. */
void ensureNoDuplicateQualeIface(
const std::string& qualeIfaceApi) const;
protected:
// Virtual functions for derived classes to override
virtual int getStopDelayMs() const