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:
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user