blob: 23b3c34e001185dfff033e89cdc14240e56087ef [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/camera/bin/device_watcher/device_watcher_impl.h"
#include <fidl/fuchsia.camera2.hal/cpp/fidl.h>
#include <fidl/fuchsia.camera3/cpp/fidl.h>
#include <fidl/fuchsia.component/cpp/fidl.h>
#include <fidl/fuchsia.component/cpp/test_base.h>
#include <fidl/fuchsia.hardware.camera/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <optional>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include "src/lib/testing/loop_fixture/test_loop_fixture.h"
namespace camera {
namespace {
using Tag = fuchsia_camera3::WatchDevicesEvent::Tag;
constexpr uint16_t kFakeVendorId = 0xFFFF;
constexpr uint16_t kFakeProductId = 0xABCD;
constexpr char kDevicePath[] = "000";
constexpr char kChildName[] = "csi_camera_device_000";
// Answers GetDeviceInfo, which is how the watcher recognizes a MIPI/CSI camera.
class FakeController : public fidl::Server<fuchsia_camera2_hal::Controller> {
public:
void GetNextConfig(GetNextConfigCompleter::Sync& completer) override {}
void CreateStream(CreateStreamRequest& request, CreateStreamCompleter::Sync& completer) override {
}
void EnableStreaming(EnableStreamingCompleter::Sync& completer) override {}
void DisableStreaming(DisableStreamingCompleter::Sync& completer) override {}
void GetDeviceInfo(GetDeviceInfoCompleter::Sync& completer) override {
fuchsia_camera2::DeviceInfo info;
info.vendor_id(kFakeVendorId);
info.product_id(kFakeProductId);
completer.Reply({{.info = std::move(info)}});
}
};
class FakeDevice : public fidl::Server<fuchsia_hardware_camera::Device> {
public:
explicit FakeDevice(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {}
void GetChannel(GetChannelRequest& request, GetChannelCompleter::Sync& completer) override {}
void GetChannel2(GetChannel2Request& request, GetChannel2Completer::Sync& completer) override {
fidl::BindServer(dispatcher_, std::move(request.server_end()),
std::make_unique<FakeController>());
}
void GetDebugChannel(GetDebugChannelRequest& request,
GetDebugChannelCompleter::Sync& completer) override {}
private:
async_dispatcher_t* dispatcher_;
};
// Records the children the watcher creates and holds their exposed directories. Closing a
// directory is what component manager does when it destroys the child.
class FakeRealm : public fidl::testing::TestBase<fuchsia_component::Realm> {
public:
void CreateChild(CreateChildRequest& request, CreateChildCompleter::Sync& completer) override {
created_children_.push_back(request.decl().name().value_or(""));
if (create_child_error_.has_value()) {
completer.Reply(fit::error(*create_child_error_));
return;
}
completer.Reply(fit::ok());
}
void OpenExposedDir(OpenExposedDirRequest& request,
OpenExposedDirCompleter::Sync& completer) override {
if (open_exposed_dir_error_.has_value()) {
completer.Reply(fit::error(*open_exposed_dir_error_));
return;
}
exposed_dirs_.push_back(std::move(request.exposed_dir()));
completer.Reply(fit::ok());
}
// Simulates component manager destroying the children, as it does when a `single_run` child
// stops or crashes.
void DestroyAllChildren() { exposed_dirs_.clear(); }
const std::vector<std::string>& created_children() const { return created_children_; }
void set_create_child_error(std::optional<fuchsia_component::Error> error) {
create_child_error_ = error;
}
void set_open_exposed_dir_error(fuchsia_component::Error error) {
open_exposed_dir_error_ = error;
}
private:
void NotImplemented_(const std::string& name, fidl::CompleterBase& completer) override {
ADD_FAILURE() << "Unexpected call to Realm." << name;
completer.Close(ZX_ERR_NOT_SUPPORTED);
}
std::vector<std::string> created_children_;
std::vector<fidl::ServerEnd<fuchsia_io::Directory>> exposed_dirs_;
std::optional<fuchsia_component::Error> create_child_error_;
std::optional<fuchsia_component::Error> open_exposed_dir_error_;
};
class DeviceEventHandler : public fidl::AsyncEventHandler<fuchsia_camera3::Device> {
public:
void on_fidl_error(fidl::UnbindInfo info) override { error = info; }
std::optional<fidl::UnbindInfo> error;
};
class DeviceWatcherImplTest : public gtest::TestLoopFixture {
protected:
// Whether the camera is there when the watcher's first scan completes. If not, tests add it.
virtual bool CameraPresentAtFirstScan() const { return true; }
void SetUp() override {
ASSERT_EQ(camera_loop_.StartThread("fake-camera"), ZX_OK);
auto realm = fidl::Endpoints<fuchsia_component::Realm>::Create();
realm_binding_.emplace(dispatcher(), std::move(realm.server), &realm_,
fidl::kIgnoreBindingClosure);
// The watcher talks to the camera synchronously, so the fake camera runs on its own thread.
auto watcher = DeviceWatcherImpl::Create(
std::move(realm.client), dispatcher(),
[this](const std::string& full_path)
-> fpromise::result<fidl::ClientEnd<fuchsia_hardware_camera::Device>, zx_status_t> {
auto endpoints = fidl::Endpoints<fuchsia_hardware_camera::Device>::Create();
fidl::BindServer(camera_loop_.dispatcher(), std::move(endpoints.server),
std::make_unique<FakeDevice>(camera_loop_.dispatcher()));
return fpromise::ok(std::move(endpoints.client));
});
ASSERT_TRUE(watcher.is_ok());
watcher_ = watcher.take_value();
if (CameraPresentAtFirstScan()) {
watcher_->AddDeviceByPath(kDevicePath);
}
watcher_->UpdateClients();
auto client = fidl::Endpoints<fuchsia_camera3::DeviceWatcher>::Create();
watcher_->OnNewRequest(std::move(client.server));
client_.Bind(std::move(client.client), dispatcher());
RunLoopUntilIdle();
}
void TearDown() override {
client_ = {};
watcher_.reset();
realm_binding_.reset();
RunLoopUntilIdle();
camera_loop_.Shutdown();
}
// Starts a WatchDevices call. The reply is stored in |events_|.
void Watch() {
ASSERT_FALSE(watch_pending_);
watch_pending_ = true;
client_->WatchDevices().Then(
[this](fidl::Result<fuchsia_camera3::DeviceWatcher::WatchDevices>& result) {
watch_pending_ = false;
ASSERT_TRUE(result.is_ok()) << result.error_value();
events_ = std::move(result.value().events());
});
}
// Returns the ID of the only device, from the first WatchDevices reply.
uint64_t WatchFirstDevice() {
Watch();
RunLoopUntilIdle();
EXPECT_FALSE(watch_pending_);
EXPECT_EQ(events_.size(), 1u);
if (events_.size() != 1u || events_[0].Which() != Tag::kAdded) {
ADD_FAILURE() << "Expected one added device";
return 0;
}
return events_[0].added().value();
}
async::Loop camera_loop_{&kAsyncLoopConfigNoAttachToCurrentThread};
FakeRealm realm_;
std::optional<fidl::ServerBinding<fuchsia_component::Realm>> realm_binding_;
std::unique_ptr<DeviceWatcherImpl> watcher_;
fidl::Client<fuchsia_camera3::DeviceWatcher> client_;
bool watch_pending_ = false;
std::vector<fuchsia_camera3::WatchDevicesEvent> events_;
};
TEST_F(DeviceWatcherImplTest, RelaunchesDeviceAfterChildIsDestroyed) {
uint64_t first_id = WatchFirstDevice();
EXPECT_EQ(realm_.created_children(), std::vector<std::string>{kChildName});
// The child crashes, so component manager destroys it. Clients see the device go away.
Watch();
realm_.DestroyAllChildren();
RunLoopUntilIdle();
ASSERT_FALSE(watch_pending_);
ASSERT_EQ(events_.size(), 1u);
ASSERT_EQ(events_[0].Which(), Tag::kRemoved);
EXPECT_EQ(events_[0].removed().value(), first_id);
// After one to two seconds (the first backoff delay with jitter), the watcher launches the child
// again and reports it under a new ID.
Watch();
RunLoopFor(zx::sec(2));
ASSERT_FALSE(watch_pending_);
ASSERT_EQ(events_.size(), 1u);
ASSERT_EQ(events_[0].Which(), Tag::kAdded);
EXPECT_NE(events_[0].added().value(), first_id);
EXPECT_EQ(realm_.created_children(), (std::vector<std::string>{kChildName, kChildName}));
}
TEST_F(DeviceWatcherImplTest, ConnectToDestroyedChildClosesRequest) {
uint64_t id = WatchFirstDevice();
// The child is already gone when a client connects, so OpenExposedDir fails. This used to abort
// the watcher (b/244178394).
realm_.set_open_exposed_dir_error(fuchsia_component::Error::kInstanceNotFound);
auto device = fidl::Endpoints<fuchsia_camera3::Device>::Create();
ASSERT_TRUE(client_->ConnectToDevice({{.id = id, .request = std::move(device.server)}}).is_ok());
DeviceEventHandler handler;
fidl::Client<fuchsia_camera3::Device> device_client(std::move(device.client), dispatcher(),
&handler);
RunLoopUntilIdle();
ASSERT_TRUE(handler.error.has_value());
EXPECT_EQ(handler.error->status(), ZX_ERR_UNAVAILABLE);
}
TEST_F(DeviceWatcherImplTest, StopsRelaunchingAfterRepeatedFailures) {
WatchFirstDevice();
realm_.set_create_child_error(fuchsia_component::Error::kInstanceAlreadyExists);
realm_.DestroyAllChildren();
RunLoopFor(zx::min(10));
// One launch at startup, then three relaunch attempts.
EXPECT_EQ(realm_.created_children().size(), 4u);
}
TEST_F(DeviceWatcherImplTest, ResetsRelaunchAttemptsAfterStableRun) {
WatchFirstDevice();
// Spend two of the three relaunch attempts in quick succession.
realm_.DestroyAllChildren();
RunLoopFor(zx::sec(2));
RunLoopUntilIdle();
ASSERT_EQ(realm_.created_children().size(), 2u);
realm_.DestroyAllChildren();
RunLoopFor(zx::sec(4));
RunLoopUntilIdle();
ASSERT_EQ(realm_.created_children().size(), 3u);
// Once the child has run for at least a minute, exiting resets both the backoff delay (back to
// one to two seconds instead of four to eight) and the three-attempt budget.
RunLoopFor(zx::min(1));
realm_.set_create_child_error(fuchsia_component::Error::kInstanceAlreadyExists);
realm_.DestroyAllChildren();
RunLoopFor(zx::sec(2));
RunLoopUntilIdle();
EXPECT_EQ(realm_.created_children().size(), 4u);
RunLoopFor(zx::min(10));
EXPECT_EQ(realm_.created_children().size(), 6u);
}
TEST_F(DeviceWatcherImplTest, ResetsRelaunchAttemptsWhenDeviceIsReAdded) {
WatchFirstDevice();
realm_.set_create_child_error(fuchsia_component::Error::kInstanceAlreadyExists);
realm_.DestroyAllChildren();
RunLoopFor(zx::min(10));
ASSERT_EQ(realm_.created_children().size(), 4u);
// Re-adding the device starts it with a fresh backoff delay and three-attempt budget even if it
// exits again before running for a minute.
realm_.set_create_child_error(std::nullopt);
watcher_->AddDeviceByPath(kDevicePath);
watcher_->UpdateClients();
RunLoopUntilIdle();
ASSERT_EQ(realm_.created_children().size(), 5u);
realm_.set_create_child_error(fuchsia_component::Error::kInstanceAlreadyExists);
realm_.DestroyAllChildren();
RunLoopFor(zx::sec(2));
RunLoopUntilIdle();
EXPECT_EQ(realm_.created_children().size(), 6u);
RunLoopFor(zx::min(10));
EXPECT_EQ(realm_.created_children().size(), 8u);
}
class DeviceWatcherImplLateCameraTest : public DeviceWatcherImplTest {
protected:
bool CameraPresentAtFirstScan() const override { return false; }
};
TEST_F(DeviceWatcherImplLateCameraTest, ReportsCameraAddedAfterFirstScan) {
// The first scan found no camera.
Watch();
RunLoopUntilIdle();
ASSERT_FALSE(watch_pending_);
EXPECT_TRUE(events_.empty());
// The camera driver comes up after the first scan, as it can at boot. The pending WatchDevices
// call returns with the camera, instead of waiting for some later device change.
Watch();
RunLoopUntilIdle();
ASSERT_TRUE(watch_pending_);
watcher_->AddDeviceByPath(kDevicePath);
RunLoopUntilIdle();
ASSERT_FALSE(watch_pending_);
ASSERT_EQ(events_.size(), 1u);
EXPECT_EQ(events_[0].Which(), Tag::kAdded);
EXPECT_EQ(realm_.created_children(), std::vector<std::string>{kChildName});
}
} // namespace
} // namespace camera