| // Copyright 2019 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 <fidl/fuchsia.device.manager/cpp/wire.h> |
| #include <fidl/fuchsia.driver.framework/cpp/wire.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/default.h> |
| #include <lib/async_patterns/testing/cpp/dispatcher_bound.h> |
| #include <lib/ddk/driver.h> |
| #include <lib/fit/defer.h> |
| |
| #include <fbl/auto_lock.h> |
| #include <zxtest/zxtest.h> |
| |
| #include "src/devices/bin/driver_host/device_controller_connection.h" |
| #include "src/devices/bin/driver_host/driver_host.h" |
| #include "src/devices/bin/driver_host/zx_device.h" |
| |
| namespace { |
| |
| namespace fdf = fuchsia_driver_framework; |
| |
| using TestAddCompositeNodeSpecCallback = |
| fit::function<void(fuchsia_device_manager::wire::CompositeNodeSpecDescriptor)>; |
| |
| class FakeCoordinator : public fidl::WireServer<fuchsia_device_manager::Coordinator> { |
| public: |
| zx_status_t Connect(fidl::ServerEnd<fuchsia_device_manager::Coordinator> request) { |
| bindings_.AddBinding(async_get_default_dispatcher(), std::move(request), this, |
| fidl::kIgnoreBindingClosure); |
| return ZX_OK; |
| } |
| |
| void AddDevice(AddDeviceRequestView request, AddDeviceCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| void ScheduleRemove(ScheduleRemoveRequestView request, |
| ScheduleRemoveCompleter::Sync& completer) override {} |
| void AddCompositeDevice(AddCompositeDeviceRequestView request, |
| AddCompositeDeviceCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| void AddCompositeNodeSpec(AddCompositeNodeSpecRequestView request, |
| AddCompositeNodeSpecCompleter::Sync& completer) override { |
| spec_callback_(request->spec); |
| completer.ReplySuccess(); |
| } |
| void BindDevice(BindDeviceRequestView request, BindDeviceCompleter::Sync& completer) override { |
| bind_count_++; |
| completer.ReplyError(ZX_OK); |
| } |
| void GetTopologicalPath(GetTopologicalPathCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| void LoadFirmware(LoadFirmwareRequestView request, |
| LoadFirmwareCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| void GetMetadata(GetMetadataRequestView request, GetMetadataCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| void GetMetadataSize(GetMetadataSizeRequestView request, |
| GetMetadataSizeCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| void AddMetadata(AddMetadataRequestView request, AddMetadataCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| void ScheduleUnbindChildren(ScheduleUnbindChildrenCompleter::Sync& completer) override { |
| completer.ReplySuccess(unbind_children_value_); |
| } |
| void ConnectFidlProtocol(ConnectFidlProtocolRequestView request, |
| ConnectFidlProtocolCompleter::Sync& completer) override { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| uint32_t bind_count() { return bind_count_; } |
| void set_unbind_children_value(bool unbind_children_value) { |
| unbind_children_value_ = unbind_children_value; |
| } |
| |
| zx_status_t set_spec_callback(TestAddCompositeNodeSpecCallback callback) { |
| spec_callback_ = std::move(callback); |
| return ZX_OK; |
| } |
| |
| private: |
| uint32_t bind_count_ = 0; |
| bool unbind_children_value_ = false; |
| TestAddCompositeNodeSpecCallback spec_callback_; |
| fidl::ServerBindingGroup<fuchsia_device_manager::Coordinator> bindings_; |
| }; |
| |
| class CoreTest : public zxtest::Test { |
| protected: |
| CoreTest() |
| : ctx_(&kAsyncLoopConfigNoAttachToCurrentThread), |
| coordinator_loop_(&kAsyncLoopConfigNoAttachToCurrentThread) {} |
| |
| ~CoreTest() { internal::RegisterContextForApi(nullptr); } |
| |
| void SetUp() override { |
| // The coordinator loop needs its own thread because the driver host makes blocking calls to it. |
| coordinator_loop_.StartThread("driver_host-test-coordinator-loop"); |
| internal::RegisterContextForApi(&ctx_); |
| ASSERT_OK(zx_driver::Create("core-test", ctx_.inspect().drivers(), &drv_)); |
| |
| auto driver = Driver::Create(drv_.get()); |
| ASSERT_OK(driver.status_value()); |
| driver_obj_ = *std::move(driver); |
| } |
| |
| // Dummy TearDown method. |
| void TearDown() override { coordinator_loop_.Shutdown(); } |
| |
| void Connect(fbl::RefPtr<zx_device> device) { |
| auto coordinator_endpoints = fidl::CreateEndpoints<fuchsia_device_manager::Coordinator>(); |
| ASSERT_OK(coordinator_endpoints.status_value()); |
| |
| fidl::WireSharedClient client(std::move(coordinator_endpoints->client), |
| ctx_.loop().dispatcher()); |
| |
| auto controller_endpoints = fidl::CreateEndpoints<fuchsia_device_manager::DeviceController>(); |
| ASSERT_OK(controller_endpoints.status_value()); |
| |
| auto conn = DeviceControllerConnection::Create(&ctx_, device, std::move(client)); |
| |
| DeviceControllerConnection::Bind(std::move(conn), std::move(controller_endpoints->server), |
| ctx_.loop().dispatcher()); |
| |
| ASSERT_OK( |
| coordinator_.SyncCall(&FakeCoordinator::Connect, std::move(coordinator_endpoints->server))); |
| |
| clients_.push_back(controller_endpoints->client.TakeChannel()); |
| } |
| |
| // This simulates receiving an unbind and remove request from the devcoordinator. |
| void UnbindDevice(fbl::RefPtr<zx_device> dev) { |
| fbl::AutoLock lock(&ctx_.api_lock()); |
| ctx_.DeviceUnbind(dev); |
| // DeviceCompleteRemoval() will drop the device reference added by device_add(). |
| // Since we never called device_add(), we should increment the reference count here. |
| fbl::RefPtr<zx_device_t> dev_add_ref(dev.get()); |
| [[maybe_unused]] auto ptr = fbl::ExportToRawPtr(&dev_add_ref); |
| dev->removal_cb = [](zx_status_t) {}; |
| ctx_.DeviceCompleteRemoval(dev); |
| } |
| |
| std::vector<zx::channel> clients_; |
| DriverHostContext ctx_; |
| fbl::RefPtr<zx_driver> drv_; |
| fbl::RefPtr<Driver> driver_obj_; |
| |
| async::Loop coordinator_loop_; |
| async_patterns::TestDispatcherBound<FakeCoordinator> coordinator_{coordinator_loop_.dispatcher(), |
| std::in_place}; |
| }; |
| |
| void Remove(fbl::RefPtr<zx_device_t> dev) { |
| dev->set_flag(DEV_FLAG_DEAD); |
| std::optional<zx_status_t> removal_status; |
| { |
| fbl::AutoLock lock(&dev->driver_host_context()->api_lock()); |
| dev->removal_cb = [&removal_status](zx_status_t status) { removal_status = status; }; |
| dev->driver_host_context()->DriverManagerRemove(dev); |
| } |
| ASSERT_OK(dev->driver_host_context()->loop().RunUntilIdle()); |
| ASSERT_TRUE(removal_status.has_value()); |
| ASSERT_OK(removal_status.value()); |
| } |
| |
| TEST_F(CoreTest, ConnectFidlReturnsError) { |
| fbl::RefPtr<zx_device> dev; |
| ASSERT_OK(zx_device::Create(&ctx_, "test", driver_obj_, &dev)); |
| auto remove = fit::defer([&dev]() { Remove(dev); }); |
| |
| ASSERT_NO_FATAL_FAILURE(Connect(dev)); |
| |
| zx::result endpoints = fidl::CreateEndpoints<fuchsia_device::Controller>(); |
| ASSERT_OK(endpoints); |
| ASSERT_STATUS(ZX_ERR_NOT_SUPPORTED, |
| device_connect_fidl_protocol(dev.get(), "test-protocol", |
| endpoints->server.TakeChannel().release())); |
| } |
| |
| TEST_F(CoreTest, LastDeviceUnbindStopsAsyncLoop) { |
| EXPECT_EQ(0, driver_obj_->device_count()); |
| { |
| fbl::RefPtr<zx_device> dev; |
| ASSERT_OK(zx_device::Create(&ctx_, "test", driver_obj_, &dev)); |
| |
| EXPECT_EQ(1, driver_obj_->device_count()); |
| ASSERT_FALSE(driver_obj_->IsDispatcherShutdown()); |
| // Mark the device as "added" so that we try and call the release op on the device (and shut |
| // down its dispatcher). |
| dev->set_flag(DEV_FLAG_ADDED); |
| |
| ASSERT_NO_FATAL_FAILURE(Connect(dev)); |
| |
| dev->unbind_cb = [](zx_status_t) {}; |
| UnbindDevice(dev); |
| |
| // Clean up the DeviceControllerConnection we set up in Connect(). |
| clients_.clear(); |
| ASSERT_OK(ctx_.loop().RunUntilIdle()); |
| // Here the dev will go out of scope and fbl_recycle() will be called. |
| } |
| |
| EXPECT_EQ(0, driver_obj_->device_count()); |
| |
| ASSERT_TRUE(driver_obj_->IsDispatcherShutdown()); |
| } |
| |
| TEST_F(CoreTest, RebindNoChildren) { |
| fbl::RefPtr<zx_device> dev; |
| ASSERT_OK(zx_device::Create(&ctx_, "test", driver_obj_, &dev)); |
| auto remove = fit::defer([&dev]() { Remove(dev); }); |
| |
| ASSERT_NO_FATAL_FAILURE(Connect(dev)); |
| |
| EXPECT_OK(dev->Rebind()); |
| EXPECT_EQ(coordinator_.SyncCall(&FakeCoordinator::bind_count), 1); |
| } |
| |
| TEST_F(CoreTest, SystemPowerStateMapping) { |
| fbl::RefPtr<zx_device> dev; |
| ASSERT_OK(zx_device::Create(&ctx_, "test", driver_obj_, &dev)); |
| auto remove = fit::defer([&dev]() { Remove(dev); }); |
| |
| ASSERT_NO_FATAL_FAILURE(Connect(dev)); |
| |
| ASSERT_OK(dev->SetPowerStates(internal::kDeviceDefaultPowerStates, |
| std::size(internal::kDeviceDefaultPowerStates))); |
| |
| // Use the default system power state mapping, but set `performance_state` values to be |
| // incrementally increasing so the test can verify we select the correct one |
| zx_device::SystemPowerStateMapping states_mapping(internal::kDeviceDefaultStateMapping); |
| for (size_t i = 0; i < states_mapping.size(); i++) { |
| states_mapping[i].performance_state = (uint32_t)i; |
| } |
| ASSERT_OK(dev->SetSystemPowerStateMapping(states_mapping)); |
| |
| fuchsia_device::wire::SystemPowerStateInfo state_info; |
| uint8_t suspend_reason = DEVICE_SUSPEND_REASON_SELECTIVE_SUSPEND; |
| |
| ASSERT_OK(dev->get_dev_power_state_from_mapping(DEVICE_SUSPEND_FLAG_REBOOT, &state_info, |
| &suspend_reason)); |
| ASSERT_EQ(suspend_reason, DEVICE_SUSPEND_REASON_REBOOT); |
| ASSERT_EQ(state_info.performance_state, 1); |
| |
| ASSERT_OK(dev->get_dev_power_state_from_mapping(DEVICE_SUSPEND_FLAG_REBOOT_BOOTLOADER, |
| &state_info, &suspend_reason)); |
| ASSERT_EQ(suspend_reason, DEVICE_SUSPEND_REASON_REBOOT_BOOTLOADER); |
| ASSERT_EQ(state_info.performance_state, 2); |
| |
| ASSERT_OK(dev->get_dev_power_state_from_mapping(DEVICE_SUSPEND_FLAG_REBOOT_RECOVERY, &state_info, |
| &suspend_reason)); |
| ASSERT_EQ(suspend_reason, DEVICE_SUSPEND_REASON_REBOOT_RECOVERY); |
| ASSERT_EQ(state_info.performance_state, 3); |
| |
| ASSERT_OK(dev->get_dev_power_state_from_mapping(DEVICE_SUSPEND_FLAG_POWEROFF, &state_info, |
| &suspend_reason)); |
| ASSERT_EQ(suspend_reason, DEVICE_SUSPEND_REASON_POWEROFF); |
| ASSERT_EQ(state_info.performance_state, 4); |
| |
| ASSERT_OK(dev->get_dev_power_state_from_mapping(DEVICE_SUSPEND_FLAG_MEXEC, &state_info, |
| &suspend_reason)); |
| ASSERT_EQ(suspend_reason, DEVICE_SUSPEND_REASON_MEXEC); |
| ASSERT_EQ(state_info.performance_state, 5); |
| |
| ASSERT_OK(dev->get_dev_power_state_from_mapping(DEVICE_SUSPEND_FLAG_SUSPEND_RAM, &state_info, |
| &suspend_reason)); |
| ASSERT_EQ(suspend_reason, DEVICE_SUSPEND_REASON_SUSPEND_RAM); |
| ASSERT_EQ(state_info.performance_state, 6); |
| |
| ASSERT_OK(dev->get_dev_power_state_from_mapping(DEVICE_SUSPEND_FLAG_REBOOT_KERNEL_INITIATED, |
| &state_info, &suspend_reason)); |
| ASSERT_EQ(suspend_reason, DEVICE_SUSPEND_REASON_REBOOT_KERNEL_INITIATED); |
| ASSERT_EQ(state_info.performance_state, 7); |
| } |
| |
| TEST_F(CoreTest, RebindHasOneChild) { |
| uint32_t unbind_count = 0; |
| coordinator_.SyncCall(&FakeCoordinator::set_unbind_children_value, true); |
| |
| fbl::RefPtr<zx_device> parent; |
| ASSERT_OK(zx_device::Create(&ctx_, "parent", driver_obj_, &parent)); |
| auto remove = fit::defer([&parent]() { Remove(parent); }); |
| ASSERT_NO_FATAL_FAILURE(Connect(parent)); |
| parent->set_ops({ |
| .unbind = [](void* ctx) { *static_cast<uint32_t*>(ctx) += 1; }, |
| }); |
| parent->set_ctx(&unbind_count); |
| { |
| fbl::RefPtr<zx_device> child; |
| ASSERT_OK(zx_device::Create(&ctx_, "child", driver_obj_, &child)); |
| ASSERT_NO_FATAL_FAILURE(Connect(child)); |
| child->set_ops(parent->ops()); |
| child->set_ctx(&unbind_count); |
| parent->add_child(child.get()); |
| child->set_parent(parent); |
| |
| EXPECT_OK(parent->Rebind()); |
| EXPECT_EQ(coordinator_.SyncCall(&FakeCoordinator::bind_count), 0); |
| ASSERT_NO_FATAL_FAILURE(UnbindDevice(child)); |
| EXPECT_EQ(unbind_count, 1); |
| |
| child->set_flag(DEV_FLAG_DEAD); |
| } |
| |
| ASSERT_OK(ctx_.loop().RunUntilIdle()); |
| EXPECT_EQ(coordinator_.SyncCall(&FakeCoordinator::bind_count), 1); |
| } |
| |
| TEST_F(CoreTest, RebindHasMultipleChildren) { |
| uint32_t unbind_count = 0; |
| coordinator_.SyncCall(&FakeCoordinator::set_unbind_children_value, true); |
| |
| fbl::RefPtr<zx_device> parent; |
| ASSERT_OK(zx_device::Create(&ctx_, "parent", driver_obj_, &parent)); |
| auto remove = fit::defer([&parent]() { Remove(parent); }); |
| |
| ASSERT_NO_FATAL_FAILURE(Connect(parent)); |
| parent->set_ops({ |
| .unbind = [](void* ctx) { *static_cast<uint32_t*>(ctx) += 1; }, |
| }); |
| parent->set_ctx(&unbind_count); |
| { |
| std::array<fbl::RefPtr<zx_device>, 5> children; |
| for (auto& child : children) { |
| ASSERT_OK(zx_device::Create(&ctx_, "child", driver_obj_, &child)); |
| ASSERT_NO_FATAL_FAILURE(Connect(child)); |
| child->set_ops(parent->ops()); |
| child->set_ctx(&unbind_count); |
| parent->add_child(child.get()); |
| child->set_parent(parent); |
| } |
| |
| EXPECT_OK(parent->Rebind()); |
| |
| for (auto& child : children) { |
| EXPECT_EQ(coordinator_.SyncCall(&FakeCoordinator::bind_count), 0); |
| ASSERT_NO_FATAL_FAILURE(UnbindDevice(child)); |
| } |
| |
| EXPECT_EQ(unbind_count, children.size()); |
| |
| for (auto& child : children) { |
| child->set_flag(DEV_FLAG_DEAD); |
| } |
| } |
| ctx_.loop().RunUntilIdle(); |
| EXPECT_EQ(coordinator_.SyncCall(&FakeCoordinator::bind_count), 1); |
| } |
| |
| TEST_F(CoreTest, AddCompositeNodeSpec) { |
| fbl::RefPtr<zx_device> dev; |
| ASSERT_OK(zx_device::Create(&ctx_, "test", driver_obj_, &dev)); |
| auto remove = fit::defer([&dev]() { Remove(dev); }); |
| |
| ASSERT_NO_FATAL_FAILURE(Connect(dev)); |
| |
| TestAddCompositeNodeSpecCallback test_callback = |
| [](fuchsia_device_manager::wire::CompositeNodeSpecDescriptor spec) { |
| ASSERT_EQ(2, spec.parents.count()); |
| |
| // Check the first node. |
| auto node_result_1 = spec.parents.at(0); |
| ASSERT_EQ(2, node_result_1.bind_rules.count()); |
| |
| auto parent_1_bind_rule_1_result = node_result_1.bind_rules.at(0); |
| EXPECT_EQ(2, parent_1_bind_rule_1_result.key.int_value()); |
| EXPECT_EQ(fdf::wire::Condition::kAccept, node_result_1.bind_rules.at(0).condition); |
| ASSERT_EQ(2, parent_1_bind_rule_1_result.values.count()); |
| EXPECT_EQ(1, parent_1_bind_rule_1_result.values.at(0).int_value()); |
| EXPECT_EQ(30, parent_1_bind_rule_1_result.values.at(1).int_value()); |
| |
| auto parent_1_bind_rule_2_result = node_result_1.bind_rules.at(1); |
| EXPECT_EQ(10, parent_1_bind_rule_2_result.key.int_value()); |
| EXPECT_EQ(fdf::wire::Condition::kReject, node_result_1.bind_rules.at(1).condition); |
| ASSERT_EQ(1, parent_1_bind_rule_2_result.values.count()); |
| EXPECT_EQ(3, parent_1_bind_rule_2_result.values.at(0).int_value()); |
| |
| auto parent_1_props_result = node_result_1.properties; |
| EXPECT_EQ(2, parent_1_props_result.count()); |
| ASSERT_EQ(100, parent_1_props_result.at(0).key.int_value()); |
| ASSERT_FALSE(parent_1_props_result.at(0).value.bool_value()); |
| ASSERT_STREQ("kinglet", parent_1_props_result.at(1).key.string_value()); |
| ASSERT_EQ(20, parent_1_props_result.at(1).value.int_value()); |
| |
| // Check the second node. |
| auto node_result_2 = spec.parents.at(1); |
| ASSERT_EQ(2, node_result_2.bind_rules.count()); |
| |
| auto parent_2_bind_rule_1 = node_result_2.bind_rules.at(0); |
| EXPECT_EQ(12, parent_2_bind_rule_1.key.int_value()); |
| EXPECT_EQ(fdf::wire::Condition::kReject, parent_2_bind_rule_1.condition); |
| ASSERT_EQ(1, parent_2_bind_rule_1.values.count()); |
| EXPECT_EQ(false, parent_2_bind_rule_1.values.at(0).bool_value()); |
| |
| auto parent_2_bind_rule_2 = node_result_2.bind_rules.at(1); |
| EXPECT_STREQ("curlew", parent_2_bind_rule_2.key.string_value().get()); |
| EXPECT_EQ(fdf::wire::Condition::kReject, parent_2_bind_rule_2.condition); |
| ASSERT_EQ(2, parent_2_bind_rule_2.values.count()); |
| EXPECT_STREQ("willet", parent_2_bind_rule_2.values.at(0).string_value().get()); |
| EXPECT_STREQ("sanderling", parent_2_bind_rule_2.values.at(1).string_value().get()); |
| |
| auto parent_2_prop_result = node_result_2.properties; |
| EXPECT_EQ(1, parent_2_prop_result.count()); |
| ASSERT_EQ(100, parent_2_prop_result.at(0).key.int_value()); |
| ASSERT_TRUE(parent_2_prop_result.at(0).value.bool_value()); |
| }; |
| |
| ASSERT_OK(coordinator_.SyncCall(&FakeCoordinator::set_spec_callback, std::move(test_callback))); |
| |
| const device_bind_prop_value_t parent_1_bind_rules_values_1[] = { |
| device_bind_prop_int_val(1), |
| device_bind_prop_int_val(30), |
| }; |
| |
| const device_bind_prop_value_t parent_1_bind_rules_values_2[] = { |
| device_bind_prop_int_val(3), |
| }; |
| |
| const bind_rule_t parent_1_bind_rules[] = { |
| { |
| .key = device_bind_prop_int_key(2), |
| .condition = DEVICE_BIND_RULE_CONDITION_ACCEPT, |
| .values = parent_1_bind_rules_values_1, |
| .values_count = std::size(parent_1_bind_rules_values_1), |
| }, |
| { |
| .key = device_bind_prop_int_key(10), |
| .condition = DEVICE_BIND_RULE_CONDITION_REJECT, |
| .values = parent_1_bind_rules_values_2, |
| .values_count = std::size(parent_1_bind_rules_values_2), |
| }, |
| }; |
| |
| const device_bind_prop_t parent_1_properties[] = {{ |
| .key = device_bind_prop_int_key(100), |
| .value = device_bind_prop_bool_val(false), |
| }, |
| |
| { |
| .key = device_bind_prop_str_key("kinglet"), |
| .value = device_bind_prop_int_val(20), |
| }}; |
| |
| const parent_spec_t parent_1{ |
| .bind_rules = parent_1_bind_rules, |
| .bind_rule_count = std::size(parent_1_bind_rules), |
| .properties = parent_1_properties, |
| .property_count = std::size(parent_1_properties), |
| }; |
| |
| const device_bind_prop_value_t parent_2_props_values_1[] = { |
| device_bind_prop_bool_val(false), |
| }; |
| |
| const device_bind_prop_value_t parent_2_props_values_2[] = { |
| device_bind_prop_str_val("willet"), |
| device_bind_prop_str_val("sanderling"), |
| }; |
| |
| const bind_rule_t parent_2_props[] = { |
| { |
| .key = device_bind_prop_int_key(12), |
| .condition = DEVICE_BIND_RULE_CONDITION_REJECT, |
| .values = parent_2_props_values_1, |
| .values_count = std::size(parent_2_props_values_1), |
| }, |
| { |
| .key = device_bind_prop_str_key("curlew"), |
| .condition = DEVICE_BIND_RULE_CONDITION_REJECT, |
| .values = parent_2_props_values_2, |
| .values_count = std::size(parent_2_props_values_2), |
| }, |
| }; |
| |
| const device_bind_prop_t parent_2_properties[] = {{ |
| .key = device_bind_prop_int_key(100), |
| .value = device_bind_prop_bool_val(true), |
| }}; |
| |
| const parent_spec_t parent_2{ |
| .bind_rules = parent_2_props, |
| .bind_rule_count = std::size(parent_2_props), |
| .properties = parent_2_properties, |
| .property_count = std::size(parent_2_properties), |
| }; |
| |
| const parent_spec_t parents[] = { |
| parent_1, |
| parent_2, |
| }; |
| |
| const composite_node_spec_t spec = { |
| .parents = parents, |
| .parent_count = std::size(parents), |
| .metadata_list = nullptr, |
| .metadata_count = 0, |
| }; |
| |
| EXPECT_EQ(ZX_OK, device_add_composite_spec(dev.get(), "test_composite", &spec)); |
| } |
| |
| } // namespace |