7a47f2bd49
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>
636 lines
17 KiB
C++
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
|