blob: b2defb65ce2899e40ddbf3c20bbb60664c71d782 [file] [log] [blame]
// Copyright 2018 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 "garnet/bin/appmgr/component_controller_impl.h"
#include "garnet/bin/appmgr/realm.h"
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <fstream>
#include <string>
#include <vector>
#include <fidl/examples/echo/cpp/fidl.h>
#include <fs/pseudo-dir.h>
#include <fs/synchronous-vfs.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/default.h>
#include <lib/fdio/util.h>
#include "garnet/bin/appmgr/util.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "lib/component/cpp/testing/test_util.h"
#include "lib/component/cpp/testing/test_with_environment.h"
#include "lib/fidl/cpp/binding_set.h"
#include "lib/fxl/files/scoped_temp_dir.h"
#include "lib/fxl/logging.h"
namespace component {
namespace {
using fuchsia::sys::TerminationReason;
using ::testing::AnyOf;
using ::testing::Eq;
using testing::CloneFileDescriptor;
using testing::EnclosingEnvironment;
using testing::TestWithEnvironment;
class RealmTest : public TestWithEnvironment {
protected:
void SetUp() override {
TestWithEnvironment::SetUp();
OpenNewOutFile();
}
void OpenNewOutFile() {
ASSERT_TRUE(tmp_dir_.NewTempFile(&out_file_));
outf_ = fileno(std::fopen(out_file_.c_str(), "w"));
}
std::string ReadOutFile() {
std::string out;
if (!files::ReadFileToString(out_file_, &out)) {
FXL_LOG(ERROR) << "Could not read output file " << out_file_;
return "";
}
return out;
}
fuchsia::sys::LaunchInfo CreateLaunchInfo(
const std::string& url, const std::vector<std::string>& args = {}) {
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = url;
for (const auto& a : args) {
launch_info.arguments.push_back(a);
}
launch_info.out = CloneFileDescriptor(outf_);
launch_info.err = CloneFileDescriptor(STDERR_FILENO);
return launch_info;
}
fuchsia::sys::ComponentControllerPtr RunComponent(
EnclosingEnvironment* enclosing_environment, const std::string& url,
const std::vector<std::string>& args = {}) {
return enclosing_environment->CreateComponent(
CreateLaunchInfo(url, std::move(args)));
}
private:
files::ScopedTempDir tmp_dir_;
std::string out_file_;
int outf_;
};
constexpr char kRealm[] = "realmintegrationtest";
const auto kTimeout = zx::sec(5);
TEST_F(RealmTest, Resolve) {
auto enclosing_environment =
CreateNewEnclosingEnvironment(kRealm, CreateServices());
fidl::InterfacePtr<fuchsia::process::Resolver> resolver;
enclosing_environment->ConnectToService(resolver.NewRequest());
bool wait = false;
resolver->Resolve(
fidl::StringPtr("fuchsia-pkg://fuchsia.com/appmgr_integration_tests#test/"
"appmgr_realm_integration_tests"),
[&wait](zx_status_t status, zx::vmo binary,
fidl::InterfaceHandle<fuchsia::ldsvc::Loader> loader) {
wait = true;
ASSERT_EQ(ZX_OK, status);
std::string expect;
// One day, when this test is not run in the shell realm, it should
// read:
// files::ReadFileToString("/pkg/test/appmgr_realm_integration_tests",
// &expect);
files::ReadFileToString(
"/pkgfs/packages/appmgr_integration_tests/0/test/"
"appmgr_realm_integration_tests",
&expect);
ASSERT_FALSE(expect.empty());
std::vector<char> buf(expect.length());
ASSERT_EQ(ZX_OK, binary.read(buf.data(), 0, buf.size()));
std::string actual(buf.begin(), buf.end());
ASSERT_EQ(expect, actual);
});
EXPECT_TRUE(RunLoopWithTimeoutOrUntil([&wait] { return wait; }, kTimeout));
}
TEST_F(RealmTest, LaunchNonExistentComponent) {
auto env_services = CreateServices();
auto enclosing_environment =
CreateNewEnclosingEnvironment(kRealm, std::move(env_services));
ASSERT_TRUE(WaitForEnclosingEnvToStart(enclosing_environment.get()));
// try to launch file url.
auto controller1 =
RunComponent(enclosing_environment.get(), "does_not_exist");
bool wait = false;
controller1.events().OnTerminated =
[&wait](int64_t return_code, fuchsia::sys::TerminationReason reason) {
wait = true;
EXPECT_EQ(reason, fuchsia::sys::TerminationReason::PACKAGE_NOT_FOUND);
};
EXPECT_TRUE(RunLoopWithTimeoutOrUntil([&wait] { return wait; }, kTimeout));
// try to launch pkg url.
auto controller2 =
RunComponent(enclosing_environment.get(),
"fuchsia-pkg://fuchsia.com/does_not_exist#meta/some.cmx");
wait = false;
controller2.events().OnTerminated =
[&wait](int64_t return_code, fuchsia::sys::TerminationReason reason) {
wait = true;
EXPECT_EQ(reason, fuchsia::sys::TerminationReason::PACKAGE_NOT_FOUND);
};
EXPECT_TRUE(RunLoopWithTimeoutOrUntil([&wait] { return wait; }, kTimeout));
}
// This test exercises the fact that two components should be in separate jobs,
// and thus when one component controller kills its job due to a .Kill() call
// the other component should run uninterrupted.
TEST_F(RealmTest, CreateTwoKillOne) {
// launch component as a service.
auto env_services = CreateServices();
ASSERT_EQ(ZX_OK, env_services->AddServiceWithLaunchInfo(
CreateLaunchInfo("fuchsia-pkg://fuchsia.com/echo2_server_cpp#meta/echo2_server_cpp.cmx"),
fidl::examples::echo::Echo::Name_));
auto enclosing_environment =
CreateNewEnclosingEnvironment(kRealm, std::move(env_services));
ASSERT_TRUE(WaitForEnclosingEnvToStart(enclosing_environment.get()));
// launch component normally
auto controller1 =
RunComponent(enclosing_environment.get(), "fuchsia-pkg://fuchsia.com/echo2_server_cpp#meta/echo2_server_cpp.cmx");
// make sure echo service is running.
fidl::examples::echo::EchoPtr echo;
enclosing_environment->ConnectToService(echo.NewRequest());
const std::string message = "CreateTwoKillOne";
fidl::StringPtr ret_msg = "";
echo->EchoString(message,
[&](::fidl::StringPtr retval) { ret_msg = retval; });
ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return std::string(ret_msg) == message; }, kTimeout));
// Kill one of the two components, make sure it's exited via Wait
bool wait = false;
controller1.events().OnTerminated =
[&wait](int64_t return_code, fuchsia::sys::TerminationReason reason) {
wait = true;
};
controller1->Kill();
EXPECT_TRUE(RunLoopWithTimeoutOrUntil([&wait] { return wait; }, kTimeout));
// Make sure the second component is still running.
ret_msg = "";
echo->EchoString(message,
[&](::fidl::StringPtr retval) { ret_msg = retval; });
ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return std::string(ret_msg) == message; }, kTimeout));
}
TEST_F(RealmTest, KillRealmKillsComponent) {
auto env_services = CreateServices();
ASSERT_EQ(ZX_OK, env_services->AddServiceWithLaunchInfo(
CreateLaunchInfo("fuchsia-pkg://fuchsia.com/echo2_server_cpp#meta/echo2_server_cpp.cmx"),
fidl::examples::echo::Echo::Name_));
auto enclosing_environment =
CreateNewEnclosingEnvironment(kRealm, std::move(env_services));
ASSERT_TRUE(WaitForEnclosingEnvToStart(enclosing_environment.get()));
// make sure echo service is running.
fidl::examples::echo::EchoPtr echo;
enclosing_environment->ConnectToService(echo.NewRequest());
const std::string message = "CreateTwoKillOne";
fidl::StringPtr ret_msg = "";
echo->EchoString(message,
[&](::fidl::StringPtr retval) { ret_msg = retval; });
ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return std::string(ret_msg) == message; }, kTimeout));
bool killed = false;
echo.set_error_handler([&](zx_status_t status) { killed = true; });
enclosing_environment->Kill();
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return enclosing_environment->is_running(); }, kTimeout));
// send a msg, without that error handler won't be called.
echo->EchoString(message,
[&](::fidl::StringPtr retval) { ret_msg = retval; });
EXPECT_TRUE(RunLoopWithTimeoutOrUntil([&] { return killed; }, kTimeout));
}
TEST_F(RealmTest, EnvironmentControllerRequired) {
fuchsia::sys::EnvironmentPtr env;
real_env()->CreateNestedEnvironment(
env.NewRequest(), /* controller = */ nullptr, kRealm,
/* additional_services = */ nullptr, fuchsia::sys::EnvironmentOptions{});
zx_status_t env_status = ZX_OK;
env.set_error_handler([&](zx_status_t status) { env_status = status; });
EXPECT_TRUE(
RunLoopWithTimeoutOrUntil([&] { return env_status != ZX_OK; }, kTimeout));
}
TEST_F(RealmTest, EnvironmentLabelRequired) {
// Can't use EnclosingEnvironment here since there's no way to discern between
// 'not yet created' and 'failed to create'. This also lets use check the
// specific status returned.
fuchsia::sys::EnvironmentPtr env;
fuchsia::sys::EnvironmentControllerPtr env_controller;
zx_status_t env_status = ZX_OK;
zx_status_t env_controller_status = ZX_OK;
env.set_error_handler([&](zx_status_t status) { env_status = status; });
env_controller.set_error_handler(
[&](zx_status_t status) { env_controller_status = status; });
real_env()->CreateNestedEnvironment(
env.NewRequest(), env_controller.NewRequest(), /* label = */ "",
/* additional_services = */ nullptr, fuchsia::sys::EnvironmentOptions{});
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return env_status == ZX_ERR_INVALID_ARGS; }, kTimeout));
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return env_controller_status == ZX_ERR_INVALID_ARGS; }, kTimeout));
}
TEST_F(RealmTest, EnvironmentLabelMustBeUnique) {
// Create first environment with label kRealm using EnclosingEnvironment since
// that's easy.
auto enclosing_environment =
CreateNewEnclosingEnvironment(kRealm, CreateServices());
// Can't use EnclosingEnvironment here since there's no way to discern between
// 'not yet created' and 'failed to create'. This also lets us check the
// specific status returned.
fuchsia::sys::EnvironmentPtr env;
fuchsia::sys::EnvironmentControllerPtr env_controller;
zx_status_t env_status, env_controller_status;
env.set_error_handler([&](zx_status_t status) { env_status = status; });
env_controller.set_error_handler(
[&](zx_status_t status) { env_controller_status = status; });
// Same environment label as EnclosingEnvironment created above.
real_env()->CreateNestedEnvironment(
env.NewRequest(), env_controller.NewRequest(), kRealm, nullptr,
fuchsia::sys::EnvironmentOptions{});
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return env_status == ZX_ERR_BAD_STATE; }, kTimeout));
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[&] { return env_controller_status == ZX_ERR_BAD_STATE; }, kTimeout));
}
class RealmFakeLoaderTest : public RealmTest, public fuchsia::sys::Loader {
protected:
RealmFakeLoaderTest() {
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;
}));
enclosing_environment_ = CreateNewEnclosingEnvironment(
kRealm, CreateServicesWithCustomLoader(loader_service_));
}
void LoadUrl(std::string url, LoadUrlCallback callback) override {
ASSERT_TRUE(component_url_.empty());
component_url_ = url;
}
bool WaitForComponentLoad() {
return RunLoopWithTimeoutOrUntil([this] { return !component_url_.empty(); },
kTimeout);
}
const std::string& component_url() const { return component_url_; }
std::unique_ptr<EnclosingEnvironment> enclosing_environment_;
private:
fbl::RefPtr<fs::Service> loader_service_;
fidl::BindingSet<fuchsia::sys::Loader> bindings_;
std::string component_url_;
};
TEST_F(RealmFakeLoaderTest, CreateWebComponent_HTTP) {
RunComponent(enclosing_environment_.get(), "http://example.com");
ASSERT_TRUE(WaitForComponentLoad());
EXPECT_THAT(
component_url(),
Eq("fuchsia-pkg://fuchsia.com/web_runner#meta/web_runner.cmx"));
}
TEST_F(RealmFakeLoaderTest, CreateWebComponent_HTTPS) {
RunComponent(enclosing_environment_.get(), "https://example.com");
ASSERT_TRUE(WaitForComponentLoad());
EXPECT_THAT(
component_url(),
Eq("fuchsia-pkg://fuchsia.com/web_runner#meta/web_runner.cmx"));
}
TEST_F(RealmFakeLoaderTest, CreateCastComponent_CAST) {
RunComponent(enclosing_environment_.get(), "cast://a12345/");
ASSERT_TRUE(WaitForComponentLoad());
EXPECT_EQ("fuchsia-pkg://fuchsia.com/cast_runner#meta/cast_runner.cmx",
component_url());
}
TEST_F(RealmFakeLoaderTest, CreateCastComponent_CASTS) {
RunComponent(enclosing_environment_.get(), "casts://a12345/");
ASSERT_TRUE(WaitForComponentLoad());
EXPECT_EQ("fuchsia-pkg://fuchsia.com/cast_runner#meta/cast_runner.cmx",
component_url());
}
TEST_F(RealmFakeLoaderTest, CreateInvalidComponent) {
TerminationReason reason = TerminationReason::UNKNOWN;
int64_t return_code = INT64_MAX;
auto controller =
RunComponent(enclosing_environment_.get(), "garbage://test");
controller.events().OnTerminated = [&](int64_t err, TerminationReason r) {
return_code = err;
reason = r;
};
ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return return_code < INT64_MAX; },
kTimeout));
EXPECT_EQ(TerminationReason::URL_INVALID, reason);
EXPECT_EQ(-1, return_code);
}
} // namespace
} // namespace component