LcamDev: Add baked in camera profiles; use new test supports

This commit is contained in:
2026-06-13 18:50:31 -04:00
parent f3ca20ac1d
commit 25d7b9c013
8 changed files with 488 additions and 90 deletions
+10
View File
@@ -56,6 +56,12 @@ if(ENABLE_LIB_lcameraDev)
option(ENABLE_LCAMERADEV_TOOLS "Build lcameraDev probe/list tools" ON)
if(ENABLE_LCAMERADEV_TOOLS)
if(NOT TARGET spinscale_test_support)
message(FATAL_ERROR
"lcameraDev probe tools require spinscale_test_support. "
"Configure with -DENABLE_TESTS=ON.")
endif()
add_executable(lcameraDev_list_cameras
tools/lcameraDevListCameras.cpp
tools/probeRunner.cpp
@@ -65,10 +71,12 @@ if(ENABLE_LIB_lcameraDev)
${CMAKE_CURRENT_SOURCE_DIR}/tools
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
)
target_link_libraries(lcameraDev_list_cameras PRIVATE
lcameraDev
spinscale
spinscale_test_support
Boost::system
Boost::log
)
@@ -82,10 +90,12 @@ if(ENABLE_LIB_lcameraDev)
${CMAKE_CURRENT_SOURCE_DIR}/tools
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
${CMAKE_SOURCE_DIR}/libspinscale/tests
)
target_link_libraries(lcameraDev_probe PRIVATE
lcameraDev
spinscale
spinscale_test_support
Boost::system
Boost::log
)
@@ -5,7 +5,10 @@ add_executable(lcameraDev_unit_tests
)
target_include_directories(lcameraDev_unit_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev/tests
${CMAKE_SOURCE_DIR}/tests/fixtures
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
)
@@ -20,3 +23,30 @@ target_link_libraries(lcameraDev_unit_tests
add_dependencies(lcameraDev_unit_tests gtest_main)
add_test(NAME lcameraDev_unit_tests COMMAND lcameraDev_unit_tests)
add_executable(lcameraDev_hil_tests
lcameraDev_hil_tests.cpp
)
target_include_directories(lcameraDev_hil_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev
${CMAKE_SOURCE_DIR}/commonLibs/lcameraDev/tests
${CMAKE_SOURCE_DIR}/tests/fixtures
${CMAKE_SOURCE_DIR}/libspinscale/tests
${CMAKE_SOURCE_DIR}/include
${CMAKE_BINARY_DIR}/include
)
target_link_libraries(lcameraDev_hil_tests
gtest_main
lcameraDev
spinscale
spinscale_test_support
${Boost_LIBRARIES}
)
add_dependencies(lcameraDev_hil_tests gtest_main spinscale_test_support)
add_test(NAME lcameraDev_hil_tests COMMAND lcameraDev_hil_tests)
set_tests_properties(lcameraDev_hil_tests PROPERTIES LABELS "HIL")
@@ -0,0 +1,45 @@
#ifndef LCAMERA_DEV_TESTS_CATALOG_HELPERS_H
#define LCAMERA_DEV_TESTS_CATALOG_HELPERS_H
#include <bakedCameraProfiles.h>
#include <cameraIdentity.h>
#include <string>
#include <vector>
namespace lcamera_dev {
namespace tests {
inline CameraIdentityRecord profileToIdentityRecord(
const test_fixtures::BakedCameraProfile& profile)
{
CameraIdentityRecord record;
record.id = profile.libcameraId;
record.model = profile.model;
record.locationLabel = profile.location;
return record;
}
inline std::vector<CameraIdentityRecord> bakedProfilesAsRecords(
const char *machineTag)
{
std::vector<CameraIdentityRecord> records;
for (std::size_t i = 0; i < test_fixtures::bakedCameraProfileCount; ++i)
{
const test_fixtures::BakedCameraProfile& profile =
test_fixtures::bakedCameraProfiles[i];
if (std::string(profile.machineTag) != machineTag) {
continue;
}
records.push_back(profileToIdentityRecord(profile));
}
return records;
}
} // namespace tests
} // namespace lcamera_dev
#endif // LCAMERA_DEV_TESTS_CATALOG_HELPERS_H
@@ -0,0 +1,193 @@
#include <boostAsioLinkageFix.h>
#include <catalogHelpers.h>
#include <lcameraDev.h>
#include <gtest/gtest.h>
#include <support/bakedDeviceCatalog.h>
#include <support/probeComponentThread.h>
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace lcamera_dev {
namespace {
constexpr const char *hilEnvVar = "LCAMERADEV_HIL";
constexpr const char *machineEnvVar = "LCAMERADEV_MACHINE";
constexpr const char *defaultMachineTag = "dell-laptop";
bool hilTestsEnabled()
{
const char *value = std::getenv(hilEnvVar);
return value != nullptr && std::string(value) == "1";
}
std::string machineTagFromEnvironment()
{
const char *value = std::getenv(machineEnvVar);
if (value != nullptr && std::string(value).size() > 0) {
return value;
}
return defaultMachineTag;
}
sscl::co::NonViralNonPostingInvoker enumerateCamerasCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
std::vector<lcamera_dev::LcameraDevCameraInfo>& enumeratedCameras)
{
(void)exceptionStorage;
(void)callerLambda;
enumeratedCameras = co_await lcameraDev_enumerateCamerasCReq();
co_return;
}
sscl::co::NonViralNonPostingInvoker getOrCreateCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const char *deviceSelector,
lcamera_dev::LcameraDevGetOrCreateResult& createResult)
{
(void)exceptionStorage;
(void)callerLambda;
createResult = co_await lcameraDev_getOrCreateDeviceCReq(deviceSelector);
co_return;
}
sscl::co::NonViralNonPostingInvoker releaseCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
(void)exceptionStorage;
(void)callerLambda;
co_await lcameraDev_releaseDeviceCReq(deviceSession);
co_return;
}
void runLcameraDevMainAndNurseryTask(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
sscl::tests::ProbeComponentThreadHarness harness("lcameraDev-hil");
harness.runSync(
[&work](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcameraDev_main(componentThread);
work(componentThread);
lcameraDev_exit();
});
}
class LcameraDevHilTest : public ::testing::Test
{
protected:
void SetUp() override
{
if (!hilTestsEnabled()) {
GTEST_SKIP() << "Set " << hilEnvVar << "=1 to run hardware tests";
}
machineTag = machineTagFromEnvironment();
requiredProfiles =
sscl::tests::requiredProfilesForMachine(machineTag.c_str());
if (requiredProfiles.empty()) {
GTEST_SKIP() << "No baked profiles for machine tag "
<< machineTag;
}
}
std::string machineTag;
std::vector<const test_fixtures::BakedCameraProfile *> requiredProfiles;
};
TEST_F(LcameraDevHilTest, EnumerateMatchesBakedCatalog)
{
std::vector<lcamera_dev::LcameraDevCameraInfo> enumeratedCameras;
runLcameraDevMainAndNurseryTask(
[&enumeratedCameras](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&enumeratedCameras](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return enumerateCamerasCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
enumeratedCameras);
});
});
EXPECT_GE(enumeratedCameras.size(), requiredProfiles.size());
for (const test_fixtures::BakedCameraProfile *profile : requiredProfiles)
{
bool found = false;
for (const lcamera_dev::LcameraDevCameraInfo& camera : enumeratedCameras)
{
if (camera.id == profile->libcameraId) {
found = true;
break;
}
}
EXPECT_TRUE(found)
<< "Missing baked profile camera id=" << profile->libcameraId;
}
}
TEST_F(LcameraDevHilTest, GetOrCreateByBakedSelector)
{
runLcameraDevMainAndNurseryTask(
[this](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
for (const test_fixtures::BakedCameraProfile *profile :
requiredProfiles)
{
lcamera_dev::LcameraDevGetOrCreateResult createResult;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[profile, &createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return getOrCreateCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
profile->exampleSelector,
createResult);
});
EXPECT_TRUE(createResult.deviceSession != nullptr)
<< profile->profileTag;
EXPECT_EQ(
createResult.resolvedIdentity.id,
profile->libcameraId)
<< profile->profileTag;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
}
});
}
} // namespace
} // namespace lcamera_dev
@@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <catalogHelpers.h>
#include <selectorParse.h>
#include <selectorResolve.h>
#include <stdexcept>
@@ -8,6 +9,8 @@
namespace lcamera_dev {
namespace {
constexpr const char *dellLaptopMachineTag = "dell-laptop";
static CameraIdentityRecord makeRecord(
const std::string& id,
const std::string& model = "",
@@ -20,7 +23,7 @@ static CameraIdentityRecord makeRecord(
return record;
}
static std::vector<CameraIdentityRecord> sampleRecords()
static std::vector<CameraIdentityRecord> syntheticAmbiguityRecords()
{
return {
makeRecord("/base/cam0", "imx219", "back"),
@@ -31,18 +34,68 @@ static std::vector<CameraIdentityRecord> sampleRecords()
TEST(FormatCameraListForDiagnosticsTest, ListsIndexedCameras)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::string text = formatCameraListForDiagnostics(records);
EXPECT_NE(text.find("Known cameras:"), std::string::npos);
EXPECT_NE(text.find("[0] id=/base/cam0"), std::string::npos);
EXPECT_NE(text.find("model=imx219"), std::string::npos);
EXPECT_NE(text.find("HDMI USB Camera"), std::string::npos);
EXPECT_NE(text.find("location=external"), std::string::npos);
EXPECT_NE(text.find("Integrated_Webcam_HD"), std::string::npos);
EXPECT_NE(text.find("location=front"), std::string::npos);
}
TEST(ResolveSelectorAgainstRecordsTest, BakedUsbHdmiCameraMatchesExampleSelector)
{
const std::vector<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model-substr:HDMI;location:EXTERNAL");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(
resolved.id,
"\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
EXPECT_EQ(resolved.locationLabel, "external");
}
TEST(ResolveSelectorAgainstRecordsTest, BakedIntegratedWebcamMatchesExampleSelector)
{
const std::vector<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model-substr:Integrated;location:front");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(
resolved.id,
"\\_SB_.PCI0.XHC_.RHUB.HS04-4:1.0-1bcf:2b8a");
EXPECT_EQ(resolved.locationLabel, "front");
}
TEST(ResolveSelectorAgainstRecordsTest, BakedUsbCameraMatchesLibcameraId)
{
const std::vector<CameraIdentityRecord> records =
tests::bakedProfilesAsRecords(dellLaptopMachineTag);
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector(
"lcamera-id:\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
const CameraIdentityRecord resolved =
resolveSelectorAgainstRecords(criteria, records);
EXPECT_EQ(
resolved.id,
"\\_SB_.PCI0.XHC_.RHUB.HS01-1:1.0-32e4:9415");
}
TEST(ResolveSelectorAgainstRecordsTest, MatchesLibcameraId)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("lcamera-id:/base/cam1");
@@ -54,7 +107,7 @@ TEST(ResolveSelectorAgainstRecordsTest, MatchesLibcameraId)
TEST(ResolveSelectorAgainstRecordsTest, MatchesModelSubstrAndLocation)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model-substr:Logitech;location:EXTERNAL");
@@ -66,7 +119,7 @@ TEST(ResolveSelectorAgainstRecordsTest, MatchesModelSubstrAndLocation)
TEST(ResolveSelectorAgainstRecordsTest, IndexSelectsNthCamera)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:2");
@@ -78,7 +131,7 @@ TEST(ResolveSelectorAgainstRecordsTest, IndexSelectsNthCamera)
TEST(ResolveSelectorAgainstRecordsTest, IndexCombinedWithModel)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:0;model:imx219");
@@ -90,7 +143,7 @@ TEST(ResolveSelectorAgainstRecordsTest, IndexCombinedWithModel)
TEST(ResolveSelectorAgainstRecordsTest, ZeroMatchesThrowsWithDiagnostics)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model:does-not-exist");
@@ -111,7 +164,7 @@ TEST(ResolveSelectorAgainstRecordsTest, ZeroMatchesThrowsWithDiagnostics)
TEST(ResolveSelectorAgainstRecordsTest, AmbiguousSelectorThrows)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("model:imx219");
@@ -122,7 +175,7 @@ TEST(ResolveSelectorAgainstRecordsTest, AmbiguousSelectorThrows)
TEST(ResolveSelectorAgainstRecordsTest, IndexOutOfRangeThrows)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:9");
@@ -133,7 +186,7 @@ TEST(ResolveSelectorAgainstRecordsTest, IndexOutOfRangeThrows)
TEST(ResolveSelectorAgainstRecordsTest, IndexConflictsWithOtherClauses)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:0;location:front");
@@ -144,7 +197,7 @@ TEST(ResolveSelectorAgainstRecordsTest, IndexConflictsWithOtherClauses)
TEST(ResolveSelectorAgainstRecordsTest, InvalidIndexValueThrows)
{
const std::vector<CameraIdentityRecord> records = sampleRecords();
const std::vector<CameraIdentityRecord> records = syntheticAmbiguityRecords();
const std::vector<SelectorCriterion> criteria =
parseDeviceSelector("index:not-a-number");
+4 -77
View File
@@ -1,88 +1,15 @@
#include <boostAsioLinkageFix.h>
#include <probeRunner.h>
#include <spinscale/component.h>
#include <future>
#include <iostream>
#include <support/probeComponentThread.h>
namespace lcamera_dev_probe {
namespace {
constexpr sscl::ThreadId PROBE_PUPPETEER_THREAD_ID = 1;
class DummyPuppeteerComponent
: public sscl::pptr::PuppeteerComponent
{
public:
explicit DummyPuppeteerComponent(
const std::shared_ptr<sscl::PuppeteerThread>& componentThread)
: sscl::pptr::PuppeteerComponent(componentThread)
{}
void handleLoopExceptionHook() override
{
std::cerr << "lcameraDev probe: puppeteer loop exception\n";
}
};
void probePuppeteerMain(
const sscl::PuppeteerThread::EntryFnArguments& args,
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work,
std::promise<std::exception_ptr>& donePromise)
{
sscl::PuppeteerThread& thr = args.usableBeforeJolt;
thr.initializeTls();
sscl::ComponentThread::setPuppeteerThreadId(PROBE_PUPPETEER_THREAD_ID);
std::shared_ptr<sscl::PuppeteerThread> thrPtr =
std::static_pointer_cast<sscl::PuppeteerThread>(thr.shared_from_this());
sscl::ComponentThread::setPuppeteerThread(thrPtr);
try {
work(thrPtr);
donePromise.set_value(nullptr);
}
catch (...) {
donePromise.set_value(std::current_exception());
}
thr.getIoContext().stop();
}
} // namespace
void runOnComponentThread(
const std::function<void(
const std::shared_ptr<sscl::ComponentThread>&)>& work)
{
std::promise<std::exception_ptr> donePromise;
std::future<std::exception_ptr> doneFuture = donePromise.get_future();
DummyPuppeteerComponent dummyComponent{
std::shared_ptr<sscl::PuppeteerThread>()};
std::shared_ptr<sscl::PuppeteerThread> probeThread =
std::make_shared<sscl::PuppeteerThread>(
PROBE_PUPPETEER_THREAD_ID,
"lcameraDev-probe",
[&work, &donePromise](
const sscl::PuppeteerThread::EntryFnArguments& args)
{
probePuppeteerMain(args, work, donePromise);
},
dummyComponent,
nullptr);
dummyComponent.thread = probeThread;
probeThread->thread.join();
std::exception_ptr probeException = doneFuture.get();
if (probeException) {
std::rethrow_exception(probeException);
}
sscl::tests::ProbeComponentThreadHarness harness("lcameraDev-probe");
harness.runSync(work);
}
} // namespace lcamera_dev_probe