Files
salmanoff/commonLibs/lcameraDev/tests/lcameraDev_configure_hil_tests.cpp
T
hayodea 7a47f2bd49 lcameraDev: honor opt-planar when selecting YUV capture format.
Pass fullPlanarIsOptional through session configure so optional planar
mode can succeed with packed YUYV; extend unit and configure HIL coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 11:01:40 -04:00

636 lines
17 KiB
C++

#include <boostAsioLinkageFix.h>
#include <catalogHelpers.h>
#include <cameraSession.h>
#include <lcameraDev.h>
#include <gtest/gtest.h>
#include <spinscale/co/nonViralTaskNursery.h>
#include <support/bakedDeviceCatalog.h>
#include <support/exceptionAssertions.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 *configureWidthEnvVar = "LCAMERADEV_CONFIGURE_W";
constexpr const char *configureHeightEnvVar = "LCAMERADEV_CONFIGURE_H";
constexpr const char *defaultMachineTag = "dell-laptop";
constexpr unsigned defaultConfigureWidth = 640;
constexpr unsigned defaultConfigureHeight = 480;
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;
}
unsigned unsignedFromEnvironmentOrDefault(
const char *envVar,
unsigned defaultValue)
{
const char *value = std::getenv(envVar);
if (value == nullptr || std::string(value).empty()) {
return defaultValue;
}
return static_cast<unsigned>(std::stoul(value));
}
LcameraDevCameraModeRequest configureRequestFromEnvironment()
{
LcameraDevCameraModeRequest request;
request.width = unsignedFromEnvironmentOrDefault(
configureWidthEnvVar,
defaultConfigureWidth);
request.height = unsignedFromEnvironmentOrDefault(
configureHeightEnvVar,
defaultConfigureHeight);
request.colourSpace = LcameraDevColourSpace::Yuv;
request.fullPlanarIsOptional = false;
return request;
}
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 configureCInd(
std::exception_ptr& exceptionStorage,
std::function<void()> callerLambda,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
const LcameraDevCameraModeRequest& request,
LcameraDevConfiguredCameraMode& configuredMode)
{
(void)callerLambda;
configuredMode =
co_await lcameraDev_configureSessionModeCReq(deviceSession, request);
if (exceptionStorage) {
std::rethrow_exception(exceptionStorage);
}
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-configure-hil");
harness.runSync(
[&work](const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
lcameraDev_main(componentThread);
work(componentThread);
lcameraDev_exit();
});
}
void runNonViralNurseryRethrowingOnComponentThread(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const std::function<sscl::co::NonViralNonPostingInvoker(
sscl::co::NonViralTaskNursery::Slot::Lease&)>& invokerFactory)
{
std::exception_ptr slotException;
sscl::co::NonViralTaskNursery nursery;
nursery.openAdmission();
nursery.launch(
invokerFactory,
[&slotException](std::exception_ptr& exceptionPtr)
{
slotException = exceptionPtr;
});
nursery.closeAdmission();
nursery.syncAwaitAllSettlements(componentThread->getIoContext());
if (slotException) {
std::rethrow_exception(slotException);
}
}
bool configureSessionOrExpectPlanarFailure(
const std::shared_ptr<sscl::ComponentThread>& componentThread,
const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
const LcameraDevCameraModeRequest& request,
LcameraDevConfiguredCameraMode& configuredMode,
const char *profileTag)
{
try {
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&deviceSession, &request, &configuredMode](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
deviceSession,
request,
configuredMode);
});
EXPECT_TRUE(configuredMode.isFullyPlanar) << profileTag;
EXPECT_GE(configuredMode.width, 1u) << profileTag;
EXPECT_GE(configuredMode.height, 1u) << profileTag;
return true;
}
catch (const std::exception& exception)
{
sscl::tests::expectExceptionMessageContains(
exception,
"planar");
return false;
}
}
void configureProfileExpectingPlanarOrExplicitFailure(
const test_fixtures::BakedCameraProfile *profile,
const LcameraDevCameraModeRequest& request,
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
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;
LcameraDevConfiguredCameraMode configuredMode;
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
request,
configuredMode,
profile->profileTag);
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
}
class LcameraDevConfigureHilTest : 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());
configureRequest = configureRequestFromEnvironment();
if (requiredProfiles.empty()) {
GTEST_SKIP() << "No baked profiles for machine tag "
<< machineTag;
}
}
const test_fixtures::BakedCameraProfile *findProfile(
const char *profileTag) const
{
for (const test_fixtures::BakedCameraProfile *profile :
requiredProfiles)
{
if (std::string(profile->profileTag) == profileTag) {
return profile;
}
}
return nullptr;
}
std::string machineTag;
std::vector<const test_fixtures::BakedCameraProfile *> requiredProfiles;
LcameraDevCameraModeRequest configureRequest;
};
TEST_F(LcameraDevConfigureHilTest, ConfigureUsbHdmiYuvRequiresPlanar)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("usb_hdmi_camera");
if (!profile) {
GTEST_SKIP() << "usb_hdmi_camera profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileExpectingPlanarOrExplicitFailure(
profile,
configureRequest,
componentThread);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureIntegratedWebcamYuv)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileExpectingPlanarOrExplicitFailure(
profile,
configureRequest,
componentThread);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfiguredModeMatchesRequestDimensions)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
bool skipForMissingPlanarYuv = false;
runLcameraDevMainAndNurseryTask(
[this, profile, &skipForMissingPlanarYuv](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
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);
});
LcameraDevConfiguredCameraMode configuredMode;
const bool configureSucceeded =
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
configureRequest,
configuredMode,
profile->profileTag);
if (!configureSucceeded) {
skipForMissingPlanarYuv = true;
}
else
{
EXPECT_GE(configuredMode.width, 1u);
EXPECT_GE(configuredMode.height, 1u);
EXPECT_LE(
configuredMode.width,
configureRequest.width);
EXPECT_LE(
configuredMode.height,
configureRequest.height);
}
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
});
if (skipForMissingPlanarYuv) {
GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
}
}
TEST_F(LcameraDevConfigureHilTest, IdenticalReconfigureIsNoOp)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
bool skipForMissingPlanarYuv = false;
runLcameraDevMainAndNurseryTask(
[this, profile, &skipForMissingPlanarYuv](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
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);
});
LcameraDevConfiguredCameraMode firstMode;
LcameraDevConfiguredCameraMode secondMode;
const bool firstConfigureSucceeded =
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
configureRequest,
firstMode,
profile->profileTag);
if (!firstConfigureSucceeded) {
skipForMissingPlanarYuv = true;
}
else
{
const int configureCallCountAfterFirst =
createResult.deviceSession
->getLibcameraConfigureCallCount();
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&createResult, &secondMode, this](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
configureRequest,
secondMode);
});
EXPECT_EQ(
createResult.deviceSession
->getLibcameraConfigureCallCount(),
configureCallCountAfterFirst);
EXPECT_EQ(secondMode.pixelFormatName, firstMode.pixelFormatName);
EXPECT_EQ(secondMode.width, firstMode.width);
EXPECT_EQ(secondMode.height, firstMode.height);
}
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
});
if (skipForMissingPlanarYuv) {
GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
}
}
TEST_F(LcameraDevConfigureHilTest, ConflictingReconfigureThrows)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
bool skipForMissingPlanarYuv = false;
runLcameraDevMainAndNurseryTask(
[this, profile, &skipForMissingPlanarYuv](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
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);
});
LcameraDevConfiguredCameraMode configuredMode;
const bool configureSucceeded =
configureSessionOrExpectPlanarFailure(
componentThread,
createResult.deviceSession,
configureRequest,
configuredMode,
profile->profileTag);
if (!configureSucceeded) {
skipForMissingPlanarYuv = true;
}
else
{
LcameraDevCameraModeRequest conflictingRequest =
configureRequest;
conflictingRequest.width = configureRequest.width + 64;
EXPECT_THROW(
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&createResult, &conflictingRequest](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
LcameraDevConfiguredCameraMode ignoredMode;
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
conflictingRequest,
ignoredMode);
}),
std::runtime_error);
}
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
});
if (skipForMissingPlanarYuv) {
GTEST_SKIP() << "Camera lacks fully planar YUV at requested resolution";
}
}
void configureProfileWithOptPlanarExpectingYuyv(
const test_fixtures::BakedCameraProfile *profile,
const LcameraDevCameraModeRequest& request,
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
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;
LcameraDevConfiguredCameraMode configuredMode;
LcameraDevCameraModeRequest optPlanarRequest = request;
optPlanarRequest.fullPlanarIsOptional = true;
runNonViralNurseryRethrowingOnComponentThread(
componentThread,
[&createResult, &optPlanarRequest, &configuredMode](
sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return configureCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession,
optPlanarRequest,
configuredMode);
});
EXPECT_FALSE(configuredMode.isFullyPlanar) << profile->profileTag;
EXPECT_EQ(configuredMode.planeCount, 1u) << profile->profileTag;
EXPECT_EQ(configuredMode.pixelFormatName, "YUYV") << profile->profileTag;
EXPECT_GE(configuredMode.width, 1u) << profile->profileTag;
EXPECT_GE(configuredMode.height, 1u) << profile->profileTag;
sscl::tests::runNonViralNurseryOnComponentThread(
componentThread,
[&createResult](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
{
return releaseCInd(
lease.getExceptionStorage(),
lease.getCallerLambda(),
createResult.deviceSession);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureUsbHdmiYuvWithOptPlanarSelectsYuyv)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("usb_hdmi_camera");
if (!profile) {
GTEST_SKIP() << "usb_hdmi_camera profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileWithOptPlanarExpectingYuyv(
profile,
configureRequest,
componentThread);
});
}
TEST_F(LcameraDevConfigureHilTest, ConfigureIntegratedWebcamYuvWithOptPlanarSelectsYuyv)
{
const test_fixtures::BakedCameraProfile *profile =
findProfile("integrated_webcam");
if (!profile) {
GTEST_SKIP() << "integrated_webcam profile not available";
}
runLcameraDevMainAndNurseryTask(
[this, profile](
const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
configureProfileWithOptPlanarExpectingYuyv(
profile,
configureRequest,
componentThread);
});
}
} // namespace
} // namespace lcamera_dev