| // 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/examples/echo/cpp/fidl.h> |
| #include <fuchsia/debugdata/cpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async/cpp/wait.h> |
| #include <lib/async/dispatcher.h> |
| #include <lib/fidl/cpp/binding_set.h> |
| #include <lib/sys/cpp/testing/enclosing_environment.h> |
| #include <lib/sys/cpp/testing/test_with_environment.h> |
| #include <lib/zx/vmo.h> |
| #include <zircon/processargs.h> |
| |
| #include <memory> |
| |
| #include "src/lib/fxl/strings/string_printf.h" |
| |
| using namespace fuchsia::sys; |
| namespace echo = ::fidl::examples::echo; |
| |
| namespace sys::testing::test { |
| |
| constexpr char kHelperProc[] = |
| "fuchsia-pkg://fuchsia.com/component_cpp_tests#meta/helper_proc.cmx"; |
| constexpr int kNumberOfTries = 3; |
| |
| // helper class that creates and listens on |
| // a socket while appending to a std::stringstream |
| class SocketReader { |
| public: |
| SocketReader() : wait_(this) {} |
| |
| zx::handle OpenSocket() { |
| ZX_ASSERT(!socket_.is_valid()); |
| zx::socket ret; |
| zx::socket::create(0, &ret, &socket_); |
| wait_.set_object(socket_.get()); |
| wait_.set_trigger(ZX_SOCKET_READABLE | ZX_SOCKET_PEER_CLOSED); |
| wait_.Begin(async_get_default_dispatcher()); |
| return zx::handle(std::move(ret)); |
| } |
| |
| std::string GetString() { return stream_.str(); } |
| |
| void OnData(async_dispatcher_t* dispatcher, async::WaitBase* wait, |
| zx_status_t status, const zx_packet_signal_t* signal) { |
| if (status != ZX_OK) { |
| return; |
| } |
| if (signal->observed & ZX_SOCKET_READABLE) { |
| char buff[1024]; |
| size_t actual; |
| status = socket_.read(0, buff, sizeof(buff) - 1, &actual); |
| ASSERT_EQ(status, ZX_OK); |
| buff[actual] = '\0'; |
| stream_ << buff; |
| } |
| |
| if (!(signal->observed & ZX_SOCKET_PEER_CLOSED)) { |
| wait_.Begin(dispatcher); |
| } |
| } |
| |
| private: |
| zx::socket socket_; |
| std::stringstream stream_; |
| async::WaitMethod<SocketReader, &SocketReader::OnData> wait_; |
| }; |
| |
| class EnclosingEnvTest : public TestWithEnvironment { |
| public: |
| // Tries to connect and communicate with echo service |
| bool TryEchoService(const std::unique_ptr<EnclosingEnvironment>& env) { |
| // We give this part of the test 3 shots to complete |
| // this is the safest way to do this and prevent flakiness. |
| // Because EnvironmentServices is listening on a ComponentController channel |
| // to restart the service, we can't control that it'll actually recreate the |
| // service in 100% deterministic order. |
| echo::EchoPtr echo; |
| for (int tries = 0; tries < kNumberOfTries; tries++) { |
| // reset flag again and communicate with the service, |
| // it must be spun back up |
| bool req_done = false; |
| // dismiss old channel |
| |
| // connect again |
| env->ConnectToService(echo.NewRequest()); |
| |
| bool channel_closed = false; |
| echo.set_error_handler( |
| [&](zx_status_t status) { channel_closed = true; }); |
| // talk with the service once and assert it's ok |
| echo->EchoString("hello", [&req_done](::fidl::StringPtr rsp) { |
| EXPECT_EQ(rsp, "hello"); |
| req_done = true; |
| }); |
| RunLoopUntil([&]() { return req_done || channel_closed; }); |
| if (req_done) { |
| return true; |
| } else { |
| std::cerr << "Didn't receive echo response in attempt number " |
| << (tries + 1) << std::endl; |
| } |
| } |
| return false; |
| } |
| }; |
| |
| TEST_F(EnclosingEnvTest, RespawnService) { |
| auto svc = CreateServices(); |
| LaunchInfo linfo; |
| linfo.url = kHelperProc; |
| linfo.arguments.reset({"--echo", "--kill=die"}); |
| svc->AddServiceWithLaunchInfo(std::move(linfo), echo::Echo::Name_); |
| auto env = CreateNewEnclosingEnvironment("test-env", std::move(svc)); |
| WaitForEnclosingEnvToStart(env.get()); |
| // attempt to connect to service: |
| bool req_done = false; |
| bool got_error = false; |
| echo::EchoPtr echo; |
| echo.set_error_handler( |
| [&got_error](zx_status_t status) { got_error = true; }); |
| env->ConnectToService(echo.NewRequest()); |
| |
| // talk with the service once and assert it's done |
| echo->EchoString("hello", [&req_done](::fidl::StringPtr rsp) { |
| ASSERT_EQ(rsp, "hello"); |
| req_done = true; |
| }); |
| RunLoopUntil([&req_done]() { return req_done; }); |
| |
| // reset flag, and send the kill string |
| req_done = false; |
| // talk with the service once and assert it's done |
| echo->EchoString("die", [&req_done](::fidl::StringPtr rsp) { |
| ASSERT_EQ(rsp, "die"); |
| req_done = true; |
| }); |
| |
| // wait until we see the response AND the channel closing |
| RunLoopUntil([&req_done, &got_error]() { return req_done && got_error; }); |
| |
| // Try to communicate with server again, we expect |
| // it to be spun up once more |
| ASSERT_TRUE(TryEchoService(env)); |
| } |
| |
| TEST_F(EnclosingEnvTest, EnclosingEnvOnASeperateThread) { |
| std::unique_ptr<sys::testing::EnclosingEnvironment> env = nullptr; |
| async::Loop loop(&kAsyncLoopConfigNoAttachToThread); |
| loop.StartThread("vfs test thread"); |
| |
| auto svc = |
| sys::testing::EnvironmentServices::Create(real_env(), loop.dispatcher()); |
| |
| LaunchInfo linfo; |
| linfo.url = kHelperProc; |
| linfo.arguments.reset({"--echo", "--kill=die"}); |
| svc->AddServiceWithLaunchInfo(std::move(linfo), echo::Echo::Name_); |
| env = CreateNewEnclosingEnvironment("test-env", std::move(svc)); |
| WaitForEnclosingEnvToStart(env.get()); |
| |
| echo::EchoSyncPtr echo_ptr; |
| env->ConnectToService(echo_ptr.NewRequest()); |
| fidl::StringPtr response; |
| echo_ptr->EchoString("hello1", &response); |
| |
| ASSERT_EQ(response, "hello1"); |
| } |
| |
| TEST_F(EnclosingEnvTest, RespawnServiceWithHandler) { |
| auto svc = CreateServices(); |
| |
| int call_counter = 0; |
| svc->AddServiceWithLaunchInfo( |
| kHelperProc, |
| [&call_counter]() { |
| LaunchInfo linfo; |
| linfo.url = kHelperProc; |
| linfo.arguments.reset({"--echo", "--kill=die"}); |
| call_counter++; |
| return linfo; |
| }, |
| echo::Echo::Name_); |
| auto env = CreateNewEnclosingEnvironment("test-env", std::move(svc)); |
| WaitForEnclosingEnvToStart(env.get()); |
| // attempt to connect to service: |
| bool req_done = false; |
| bool got_error = false; |
| echo::EchoPtr echo; |
| echo.set_error_handler( |
| [&got_error](zx_status_t status) { got_error = true; }); |
| env->ConnectToService(echo.NewRequest()); |
| |
| // talk with the service once and assert it's done |
| echo->EchoString("hello", [&req_done](::fidl::StringPtr rsp) { |
| ASSERT_EQ(rsp, "hello"); |
| req_done = true; |
| }); |
| RunLoopUntil([&req_done]() { return req_done; }); |
| // check that the launch info factory function was called only once |
| EXPECT_EQ(call_counter, 1); |
| |
| // reset flag, and send the kill string |
| req_done = false; |
| // talk with the service once and assert it's done |
| echo->EchoString("die", [&req_done](::fidl::StringPtr rsp) { |
| ASSERT_EQ(rsp, "die"); |
| req_done = true; |
| }); |
| |
| // wait until we see the response AND the channel closing |
| RunLoopUntil([&req_done, &got_error]() { return req_done && got_error; }); |
| |
| // Try to communicate with server again, we expect |
| // it to be spun up once more |
| ASSERT_TRUE(TryEchoService(env)); |
| |
| // check that the launch info factory function was called only TWICE |
| EXPECT_EQ(call_counter, 2); |
| } |
| |
| TEST_F(EnclosingEnvTest, OutErrPassing) { |
| auto svc = CreateServices(); |
| |
| SocketReader cout_reader; |
| SocketReader cerr_reader; |
| svc->AddServiceWithLaunchInfo( |
| kHelperProc, |
| [&cout_reader, &cerr_reader]() { |
| LaunchInfo linfo; |
| linfo.url = kHelperProc; |
| linfo.arguments.reset({"--echo", "--cout=potato", "--cerr=tomato"}); |
| |
| linfo.out = FileDescriptor::New(); |
| linfo.out->type0 = PA_FD; |
| linfo.out->handle0 = cout_reader.OpenSocket(); |
| linfo.err = FileDescriptor::New(); |
| linfo.err->type0 = PA_FD; |
| linfo.err->handle0 = cerr_reader.OpenSocket(); |
| |
| return linfo; |
| }, |
| echo::Echo::Name_); |
| auto env = CreateNewEnclosingEnvironment("test-env", std::move(svc)); |
| WaitForEnclosingEnvToStart(env.get()); |
| // attempt to connect to service: |
| echo::EchoPtr echo; |
| |
| // this should trigger hello_proc to start and |
| // print "potato" to cout and "tomato" to err |
| env->ConnectToService(echo.NewRequest()); |
| |
| // now it's just a matter of waiting for the socket readers to |
| // have seen those strings: |
| RunLoopUntil([&cout_reader, &cerr_reader]() { |
| return cout_reader.GetString().find("potato") != std::string::npos && |
| cerr_reader.GetString().find("tomato") != std::string::npos; |
| }); |
| } |
| |
| class FakeLoader : public fuchsia::sys::Loader { |
| public: |
| FakeLoader() { |
| loader_service_ = std::make_shared<vfs::Service>( |
| [this](zx::channel channel, async_dispatcher_t* dispatcher) { |
| bindings_.AddBinding( |
| this, |
| fidl::InterfaceRequest<fuchsia::sys::Loader>(std::move(channel)), |
| dispatcher); |
| }); |
| } |
| |
| void LoadUrl(std::string url, LoadUrlCallback callback) override { |
| ASSERT_TRUE(!url.empty()); |
| component_urls_.push_back(url); |
| } |
| std::vector<std::string>& component_urls() { return component_urls_; }; |
| |
| std::shared_ptr<vfs::Service> loader_service() { return loader_service_; } |
| |
| private: |
| std::shared_ptr<vfs::Service> loader_service_; |
| fidl::BindingSet<fuchsia::sys::Loader> bindings_; |
| std::vector<std::string> component_urls_; |
| }; |
| |
| TEST_F(EnclosingEnvTest, CanLaunchMoreThanOneService) { |
| FakeLoader loader; |
| EnvironmentServices::ParentOverrides parent_overrides; |
| parent_overrides.loader_service_ = loader.loader_service(); |
| auto svc = CreateServicesWithParentOverrides(std::move(parent_overrides)); |
| |
| std::vector<std::string> urls; |
| std::vector<std::string> svc_names; |
| for (int i = 0; i < 3; i++) { |
| auto url = fxl::StringPrintf( |
| "fuchsia-pkg://fuchsia.com/dummy%d#meta/dummy%d.cmx", i, i); |
| auto svc_name = fxl::StringPrintf("service%d", i); |
| LaunchInfo linfo; |
| linfo.url = url; |
| svc->AddServiceWithLaunchInfo(std::move(linfo), svc_name); |
| urls.push_back(url); |
| svc_names.push_back(svc_name); |
| } |
| auto env = CreateNewEnclosingEnvironment("test-env", std::move(svc)); |
| WaitForEnclosingEnvToStart(env.get()); |
| |
| for (int i = 0; i < 3; i++) { |
| echo::EchoPtr echo; |
| env->ConnectToService(echo.NewRequest(), svc_names[i]); |
| } |
| RunLoopUntil([&loader]() { return loader.component_urls().size() == 3; }); |
| ASSERT_EQ(loader.component_urls(), urls); |
| } |
| |
| class FakeDebugData : public fuchsia::debugdata::DebugData { |
| public: |
| void Publish(std::string data_sink, ::zx::vmo data) override { |
| call_count_++; |
| } |
| |
| void LoadConfig(std::string config_name, |
| LoadConfigCallback callback) override { |
| // not implemented |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::debugdata::DebugData> GetHandler( |
| async_dispatcher_t* dispatcher = nullptr) { |
| return bindings_.GetHandler(this, dispatcher); |
| } |
| |
| uint64_t call_count() const { return call_count_; } |
| |
| private: |
| fidl::BindingSet<fuchsia::debugdata::DebugData> bindings_; |
| uint64_t call_count_ = 0; |
| }; |
| |
| TEST_F(EnclosingEnvTest, DebugDataServicePlumbedCorrectly) { |
| zx::vmo data; |
| fuchsia::debugdata::DebugDataPtr ptr; |
| FakeDebugData debug_data; |
| |
| // Add to first enclosing env and override plumbing |
| EnvironmentServices::ParentOverrides parent_overrides; |
| parent_overrides.debug_data_service_ = |
| std::make_shared<vfs::Service>(debug_data.GetHandler()); |
| auto svc = CreateServicesWithParentOverrides(std::move(parent_overrides)); |
| |
| auto env = CreateNewEnclosingEnvironment("test-env1", std::move(svc)); |
| WaitForEnclosingEnvToStart(env.get()); |
| // make sure count was 0 |
| ASSERT_EQ(0u, debug_data.call_count()); |
| |
| // make sure out fake servcie can be called. |
| env->ConnectToService(ptr.NewRequest()); |
| ASSERT_EQ(ZX_OK, zx::vmo::create(8, 0, &data)); |
| ptr->Publish("data_sink", std::move(data)); |
| |
| RunLoopUntil([&debug_data]() { return debug_data.call_count() == 1; }); |
| |
| // make sure service is automatically plumbed to sub environments |
| auto sub_env = env->CreateNestedEnclosingEnvironment("test-env2"); |
| |
| ptr.Unbind(); |
| |
| sub_env->ConnectToService(ptr.NewRequest()); |
| ASSERT_EQ(ZX_OK, zx::vmo::create(8, 0, &data)); |
| ptr->Publish("data_sink", std::move(data)); |
| |
| RunLoopUntil([&debug_data]() { return debug_data.call_count() == 2; }); |
| } |
| |
| } // namespace sys::testing::test |