blob: 22aa00854f057439469af01ebbc6c614c047f6d4 [file] [log] [blame]
// 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 <lib/fxl/strings/string_printf.h>
#include <zircon/processargs.h>
#include "fidl/examples/echo/cpp/fidl.h"
#include "lib/async/cpp/wait.h"
#include "lib/component/cpp/testing/test_with_environment.h"
using namespace fuchsia::sys;
namespace echo = ::fidl::examples::echo;
namespace component::testing::test {
constexpr char kHelperProc[] =
"fuchsia-pkg://fuchsia.com/lib_component_test#meta/helper_proc.cmx";
constexpr zx::duration kTimeout = zx::sec(2);
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 based on the waits above.
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());
// 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;
});
if (RunLoopWithTimeoutOrUntil([&req_done]() { return req_done; },
kTimeout)) {
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));
ASSERT_TRUE(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;
});
ASSERT_TRUE(
RunLoopWithTimeoutOrUntil([&req_done]() { return req_done; }, kTimeout));
// 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
ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
[&req_done, &got_error]() { return req_done && got_error; }, kTimeout));
// Try to communicate with server again, we expect
// it to be spun up once more
ASSERT_TRUE(TryEchoService(env));
}
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));
ASSERT_TRUE(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;
});
ASSERT_TRUE(
RunLoopWithTimeoutOrUntil([&req_done]() { return req_done; }, kTimeout));
// 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
ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
[&req_done, &got_error]() { return req_done && got_error; }, kTimeout));
// 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_FDIO_SOCKET;
linfo.out->handle0 = cout_reader.OpenSocket();
linfo.err = FileDescriptor::New();
linfo.err->type0 = PA_FDIO_SOCKET;
linfo.err->handle0 = cerr_reader.OpenSocket();
return linfo;
},
echo::Echo::Name_);
auto env = CreateNewEnclosingEnvironment("test-env", std::move(svc));
ASSERT_TRUE(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:
ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
[&cout_reader, &cerr_reader]() {
return cout_reader.GetString().find("potato") != std::string::npos &&
cerr_reader.GetString().find("tomato") != std::string::npos;
},
kTimeout));
}
class FakeLoader : public fuchsia::sys::Loader {
public:
FakeLoader() {
loader_service_ =
fbl::AdoptRef(new fs::Service([this](zx::channel channel) {
bindings_.AddBinding(
this,
fidl::InterfaceRequest<fuchsia::sys::Loader>(std::move(channel)));
return ZX_OK;
}));
}
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_; };
fbl::RefPtr<fs::Service> loader_service() { return loader_service_; }
private:
fbl::RefPtr<fs::Service> loader_service_;
fidl::BindingSet<fuchsia::sys::Loader> bindings_;
std::vector<std::string> component_urls_;
};
TEST_F(EnclosingEnvTest, CanLaunchMoreThanOneService) {
FakeLoader loader;
auto loader_service = loader.loader_service();
auto svc = CreateServicesWithCustomLoader(loader_service);
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));
ASSERT_TRUE(WaitForEnclosingEnvToStart(env.get()));
for (int i = 0; i < 3; i++) {
echo::EchoPtr echo;
env->ConnectToService(echo.NewRequest(), svc_names[i]);
}
ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
[&loader]() { return loader.component_urls().size() == 3; }, kTimeout))
<< loader.component_urls().size();
ASSERT_EQ(loader.component_urls(), urls);
}
} // namespace component::testing::test