| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/fuchsia_component_support/dynamic_component_host.h" |
| |
| #include <fuchsia/component/cpp/fidl_test_base.h> |
| #include <lib/sys/cpp/component_context.h> |
| #include <lib/sys/cpp/service_directory.h> |
| #include <lib/vfs/cpp/pseudo_dir.h> |
| #include <lib/vfs/cpp/service.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/fuchsia/process_context.h" |
| #include "base/fuchsia/scoped_service_binding.h" |
| #include "base/fuchsia/test_component_context_for_process.h" |
| #include "base/test/bind.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/task_environment.h" |
| #include "components/fuchsia_component_support/mock_realm.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace fuchsia_component_support { |
| |
| namespace { |
| |
| using testing::_; |
| |
| MATCHER_P(EqCollectionRef, name, "") { |
| return arg.name == name; |
| } |
| |
| MATCHER_P2(EqChildDecl, name, url, "") { |
| return arg.has_name() && arg.name() == name && arg.has_url() && |
| arg.url() == url; |
| } |
| |
| MATCHER_P2(EqChildRef, name, collection, "") { |
| return arg.name == name && arg.collection == collection; |
| } |
| |
| // Verifies that `create_child_args` includes a dynamic offer for "/svc", and |
| // returns a channel connected to it, if so. |
| fidl::InterfaceHandle<fuchsia::io::Directory> GetSvcFromChildArgs( |
| fuchsia::component::CreateChildArgs& create_child_args) { |
| if (!create_child_args.has_dynamic_offers()) { |
| return nullptr; |
| } |
| |
| for (auto& offer : create_child_args.dynamic_offers()) { |
| if (!offer.is_directory()) { |
| continue; |
| } |
| |
| const auto& directory_offer = offer.directory(); |
| if (!directory_offer.has_source_name() || |
| !directory_offer.has_target_name()) { |
| return nullptr; |
| } |
| if (directory_offer.target_name() != "svc") { |
| continue; |
| } |
| |
| // Connect to the outgoing directory root, to use to look up the service |
| // capability. |
| fidl::InterfacePtr<fuchsia::io::Directory> root_dir; |
| base::ComponentContextForProcess()->outgoing()->root_dir()->Serve( |
| fuchsia::io::OpenFlags::RIGHT_READABLE | |
| fuchsia::io::OpenFlags::RIGHT_WRITABLE | |
| fuchsia::io::OpenFlags::DIRECTORY, |
| root_dir.NewRequest().TakeChannel()); |
| |
| // Determine the capability path, relative to the outgoing directory of |
| // the calling process, and request to open it. |
| // The channel will be closed as soon as the Open() call is processed, |
| // if the path cannot be resolved. |
| base::FilePath path(directory_offer.source_name()); |
| if (directory_offer.has_subdir()) { |
| path = path.Append(directory_offer.subdir()); |
| } |
| fidl::InterfaceHandle<fuchsia::io::Node> services_handle; |
| root_dir->Open(fuchsia::io::OpenFlags::RIGHT_READABLE | |
| fuchsia::io::OpenFlags::RIGHT_WRITABLE | |
| fuchsia::io::OpenFlags::DIRECTORY, |
| {}, path.value(), services_handle.NewRequest()); |
| return fidl::InterfaceHandle<fuchsia::io::Directory>( |
| services_handle.TakeChannel()); |
| } |
| |
| return nullptr; |
| } |
| |
| bool HasPeerClosedHandle( |
| const fidl::InterfaceHandle<fuchsia::io::Directory>& handle) { |
| return handle.channel().wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time(), |
| nullptr) != ZX_ERR_TIMED_OUT; |
| } |
| |
| constexpr char kTestCollection[] = "test_collection"; |
| constexpr char kTestChildId[] = "test-child-id"; |
| constexpr char kTestComponentUrl[] = "dummy:url"; |
| |
| class DynamicComponentHostTest : public testing::Test { |
| protected: |
| DynamicComponentHostTest() : realm_(test_context_.additional_services()) { |
| // By default simply reply indicating success, from Create/DestroyChild. |
| ON_CALL(realm_, CreateChild) |
| .WillByDefault( |
| [](fuchsia::component::decl::CollectionRef, |
| fuchsia::component::decl::Child, |
| fuchsia::component::CreateChildArgs, |
| fuchsia::component::Realm::CreateChildCallback callback) { |
| callback({}); |
| }); |
| ON_CALL(realm_, DestroyChild) |
| .WillByDefault( |
| [](fuchsia::component::decl::ChildRef, |
| fuchsia::component::Realm::DestroyChildCallback callback) { |
| callback({}); |
| }); |
| |
| // By default connect exposed directory requests to `exposed_`, to simplify |
| // tests for exposed capabilities. |
| ON_CALL(realm_, OpenExposedDir) |
| .WillByDefault( |
| [this](fuchsia::component::decl::ChildRef, |
| fidl::InterfaceRequest<fuchsia::io::Directory> exposed_dir, |
| fuchsia::component::Realm::OpenExposedDirCallback callback) { |
| exposed_.Serve(fuchsia::io::OpenFlags::RIGHT_READABLE | |
| fuchsia::io::OpenFlags::RIGHT_WRITABLE, |
| exposed_dir.TakeChannel()); |
| callback({}); |
| }); |
| } |
| |
| // Sets expectations on CreateChild(), OpenExposedDir() and DestroyChild() |
| // being called, in that order, without expecting particular parameters. |
| void ExpectCreateOpenAndDestroy() { |
| testing::InSequence s; |
| EXPECT_CALL(realm_, CreateChild(_, _, _, _)); |
| EXPECT_CALL(realm_, OpenExposedDir(_, _, _)); |
| EXPECT_CALL(realm_, DestroyChild(_, _)); |
| } |
| |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::SingleThreadTaskEnvironment::MainThreadType::IO}; |
| |
| base::TestComponentContextForProcess test_context_; |
| |
| testing::StrictMock<MockRealm> realm_; |
| |
| // Used to fake the "exposed dir" of the component. |
| vfs::PseudoDir exposed_; |
| }; |
| |
| TEST_F(DynamicComponentHostTest, Basic) { |
| ExpectCreateOpenAndDestroy(); |
| |
| // Create and then immediately teardown the component. |
| { |
| DynamicComponentHost component(kTestCollection, kTestChildId, |
| kTestComponentUrl, base::DoNothing(), |
| nullptr); |
| } |
| |
| // Spin the loop to allow the calls to reach `realm_`. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(DynamicComponentHostTest, CollectionAndChildName) { |
| { |
| testing::InSequence s; |
| EXPECT_CALL( |
| realm_, |
| CreateChild(EqCollectionRef(kTestCollection), |
| EqChildDecl(kTestChildId, kTestComponentUrl), _, _)); |
| EXPECT_CALL(realm_, OpenExposedDir( |
| EqChildRef(kTestChildId, kTestCollection), _, _)); |
| EXPECT_CALL(realm_, |
| DestroyChild(EqChildRef(kTestChildId, kTestCollection), _)); |
| } |
| |
| { |
| DynamicComponentHost component(kTestCollection, kTestChildId, |
| kTestComponentUrl, base::DoNothing(), |
| nullptr); |
| } |
| |
| // Spin the loop to allow the calls to reach `realm_`. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(DynamicComponentHostTest, OnTeardownCalledOnBinderClose) { |
| ExpectCreateOpenAndDestroy(); |
| |
| // Publish a fake Binder to the `exposed_` directory. |
| fidl::InterfaceRequest<fuchsia::component::Binder> binder_request; |
| exposed_.AddEntry( |
| fuchsia::component::Binder::Name_, |
| std::make_unique<vfs::Service>( |
| [&binder_request](zx::channel request, async_dispatcher_t*) { |
| binder_request = fidl::InterfaceRequest<fuchsia::component::Binder>( |
| std::move(request)); |
| })); |
| |
| { |
| DynamicComponentHost component( |
| kTestCollection, kTestChildId, kTestComponentUrl, |
| base::MakeExpectedRunClosure(FROM_HERE), nullptr); |
| |
| // Spin the loop to process calls to `realm_` and `exposed_`. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(binder_request); |
| |
| // Close `binder_request` and spin the loop to allow that to be observed. |
| binder_request = nullptr; |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Spin the loop to allow remaining calls to reach `realm_`. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(DynamicComponentHostTest, |
| OnTeardownNotCalledIfDestroyedBeforeBinderClose) { |
| ExpectCreateOpenAndDestroy(); |
| |
| // Create and immediately teardown the component, so that Binder teardown |
| // will not be observed until after the DynamicComponentHost has gone. |
| { |
| DynamicComponentHost component( |
| kTestCollection, kTestChildId, kTestComponentUrl, |
| base::MakeExpectedNotRunClosure(FROM_HERE), nullptr); |
| } |
| |
| // Spin the loop to allow remaining calls to reach `realm_`. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(DynamicComponentHostTest, WithoutServiceDirectory) { |
| // Capture the `CreateChildArgs` from the `Realm.CreateChild()` call. |
| fuchsia::component::CreateChildArgs create_child_args; |
| { |
| testing::InSequence s; |
| EXPECT_CALL(realm_, CreateChild(_, _, _, _)) |
| .WillOnce([&create_child_args]( |
| fuchsia::component::decl::CollectionRef, |
| fuchsia::component::decl::Child, |
| fuchsia::component::CreateChildArgs args, |
| fuchsia::component::Realm::CreateChildCallback callback) { |
| create_child_args = std::move(args); |
| callback({}); |
| }); |
| EXPECT_CALL(realm_, OpenExposedDir(_, _, _)); |
| EXPECT_CALL(realm_, DestroyChild(_, _)); |
| } |
| |
| { |
| DynamicComponentHost component(kTestCollection, kTestChildId, |
| kTestComponentUrl, base::DoNothing(), |
| nullptr); |
| |
| // Spin the event loop to process the `CreateChild()` call. |
| base::RunLoop().RunUntilIdle(); |
| |
| // Verify that no "svc" directory is offered in the `CreateChildArgs`. |
| fidl::InterfaceHandle<fuchsia::io::Directory> svc_handle = |
| GetSvcFromChildArgs(create_child_args); |
| EXPECT_FALSE(svc_handle); |
| } |
| |
| // Spin the loop to allow the teardown calls to reach `realm_`. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(DynamicComponentHostTest, WithServiceDirectory) { |
| // Capture the `CreateChildArgs` from the `Realm.CreateChild()` call. |
| fuchsia::component::CreateChildArgs create_child_args; |
| { |
| testing::InSequence s; |
| EXPECT_CALL(realm_, CreateChild(_, _, _, _)) |
| .WillOnce([&create_child_args]( |
| fuchsia::component::decl::CollectionRef, |
| fuchsia::component::decl::Child, |
| fuchsia::component::CreateChildArgs args, |
| fuchsia::component::Realm::CreateChildCallback callback) { |
| create_child_args = std::move(args); |
| callback({}); |
| }); |
| EXPECT_CALL(realm_, OpenExposedDir(_, _, _)); |
| EXPECT_CALL(realm_, DestroyChild(_, _)); |
| } |
| |
| { |
| // Create a directory handle for the service directory. |
| fidl::InterfaceHandle<fuchsia::io::Directory> handle; |
| vfs::PseudoDir service_directory; |
| service_directory.Serve(fuchsia::io::OpenFlags::RIGHT_READABLE | |
| fuchsia::io::OpenFlags::RIGHT_WRITABLE, |
| handle.NewRequest().TakeChannel()); |
| |
| DynamicComponentHost component(kTestCollection, kTestChildId, |
| kTestComponentUrl, base::DoNothing(), |
| std::move(handle)); |
| |
| // Spin the event loop to process the `CreateChild()` call. |
| base::RunLoop().RunUntilIdle(); |
| |
| // Verify that a "svc" directory was offered in the `CreateChildArgs`. |
| fidl::InterfaceHandle<fuchsia::io::Directory> svc_handle = |
| GetSvcFromChildArgs(create_child_args); |
| EXPECT_TRUE(svc_handle); |
| |
| // Spin the event loop to allow the Open() of the directory attempted by |
| // GetSvcFromChildArgs() to be processed, then verify that the `svc_handle` |
| // was not closed by the peer. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_FALSE(HasPeerClosedHandle(svc_handle)); |
| } |
| |
| // Spin the loop to allow teardown calls to reach `realm_`. |
| base::RunLoop().RunUntilIdle(); |
| |
| // Verify that the "svc" directory offered in the `CreateChildArgs` is no |
| // longer available after the `DynamicComponentHost` has been destroyed. |
| fidl::InterfaceHandle<fuchsia::io::Directory> svc_handle = |
| GetSvcFromChildArgs(create_child_args); |
| EXPECT_TRUE(svc_handle); |
| |
| // Spin the loop to allow the Open() of the "svc" directory to be processed. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(HasPeerClosedHandle(svc_handle)); |
| } |
| |
| } // namespace |
| |
| } // namespace fuchsia_component_support |