#include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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 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 callerLambda, const std::shared_ptr& 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 callerLambda, const std::shared_ptr& deviceSession) { (void)exceptionStorage; (void)callerLambda; co_await lcameraDev_releaseDeviceCReq(deviceSession); co_return; } void runLcameraDevMainAndNurseryTask( const std::function&)>& work) { sscl::tests::ProbeComponentThreadHarness harness("lcameraDev-configure-hil"); harness.runSync( [&work](const std::shared_ptr& componentThread) { lcameraDev_main(componentThread); work(componentThread); lcameraDev_exit(); }); } void runNonViralNurseryRethrowingOnComponentThread( const std::shared_ptr& componentThread, const std::function& 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& componentThread, const std::shared_ptr& 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& 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 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& 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& 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& 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& 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& 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& 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& 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& componentThread) { configureProfileWithOptPlanarExpectingYuyv( profile, configureRequest, componentThread); }); } } // namespace } // namespace lcamera_dev