blob: f03815eab8239714f08cfd13802c96eb0ad70fd1 [file] [log] [blame]
// Copyright 2018 The Chromium 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 "fuchsia/engine/context_provider_impl.h"
#include <fuchsia/web/cpp/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fidl/cpp/binding.h>
#include <zircon/processargs.h>
#include <zircon/types.h>
#include <functional>
#include <string>
#include <utility>
#include <vector>
#include "base/base_paths_fuchsia.h"
#include "base/base_switches.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/fuchsia/default_job.h"
#include "base/fuchsia/file_utils.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/service_directory.h"
#include "base/message_loop/message_loop.h"
#include "base/path_service.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "fuchsia/engine/common.h"
#include "fuchsia/engine/fake_context.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
namespace {
constexpr char kTestDataFileIn[] = "DataFileIn";
constexpr char kTestDataFileOut[] = "DataFileOut";
constexpr char kUrl[] = "chrome://:emorhc";
constexpr char kTitle[] = "Palindrome";
MULTIPROCESS_TEST_MAIN(SpawnContextServer) {
base::MessageLoopForIO message_loop;
base::FilePath data_dir;
CHECK(base::PathService::Get(base::DIR_APP_DATA, &data_dir));
if (!data_dir.empty()) {
if (base::PathExists(data_dir.AppendASCII(kTestDataFileIn))) {
auto out_file = data_dir.AppendASCII(kTestDataFileOut);
EXPECT_EQ(base::WriteFile(out_file, nullptr, 0), 0);
}
}
fidl::InterfaceRequest<fuchsia::web::Context> fuchsia_context(
zx::channel(zx_take_startup_handle(kContextRequestHandleId)));
CHECK(fuchsia_context);
FakeContext context;
fidl::Binding<fuchsia::web::Context> context_binding(
&context, std::move(fuchsia_context));
// When a Frame's NavigationEventListener is bound, immediately broadcast a
// navigation event to its listeners.
context.set_on_create_frame_callback(
base::BindRepeating([](FakeFrame* frame) {
frame->set_on_set_listener_callback(base::BindOnce(
[](FakeFrame* frame) {
fuchsia::web::NavigationState state;
state.set_url(kUrl);
state.set_title(kTitle);
frame->listener()->OnNavigationStateChanged(std::move(state),
[]() {});
},
frame));
}));
// Quit the process when the context is destroyed.
base::RunLoop run_loop;
context_binding.set_error_handler([&run_loop](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
run_loop.Quit();
});
run_loop.Run();
return 0;
}
base::Process LaunchFakeContextProcess(const base::CommandLine& command_line,
const base::LaunchOptions& options) {
base::LaunchOptions options_with_tmp = options;
options_with_tmp.paths_to_clone.push_back(base::FilePath("/tmp"));
return base::SpawnMultiProcessTestChild("SpawnContextServer", command_line,
options_with_tmp);
}
} // namespace
class ContextProviderImplTest : public base::MultiProcessTest {
public:
ContextProviderImplTest()
: provider_(std::make_unique<ContextProviderImpl>()) {
provider_->SetLaunchCallbackForTest(
base::BindRepeating(&LaunchFakeContextProcess));
bindings_.AddBinding(provider_.get(), provider_ptr_.NewRequest());
}
~ContextProviderImplTest() override {
provider_ptr_.Unbind();
base::RunLoop().RunUntilIdle();
}
// Check if a Context is responsive by creating a Frame from it and then
// listening for an event.
void CheckContextResponsive(
fidl::InterfacePtr<fuchsia::web::Context>* context) {
// Call a Context method and wait for it to invoke a listener call.
base::RunLoop run_loop;
context->set_error_handler([&run_loop](zx_status_t status) {
ZX_LOG(ERROR, status) << " Context lost.";
ADD_FAILURE();
run_loop.Quit();
});
fuchsia::web::FramePtr frame_ptr;
frame_ptr.set_error_handler([&run_loop](zx_status_t status) {
ZX_LOG(ERROR, status) << " Frame lost.";
ADD_FAILURE();
run_loop.Quit();
});
(*context)->CreateFrame(frame_ptr.NewRequest());
// Create a Frame and expect to see a navigation event.
CapturingNavigationStateObserver change_listener(run_loop.QuitClosure());
fidl::Binding<fuchsia::web::NavigationEventListener>
change_listener_binding(&change_listener);
frame_ptr->SetNavigationEventListener(change_listener_binding.NewBinding());
run_loop.Run();
ASSERT_TRUE(change_listener.captured_state()->has_url());
EXPECT_EQ(change_listener.captured_state()->url(), kUrl);
ASSERT_TRUE(change_listener.captured_state()->has_title());
EXPECT_EQ(change_listener.captured_state()->title(), kTitle);
}
fuchsia::web::CreateContextParams BuildCreateContextParams() {
fidl::InterfaceHandle<fuchsia::io::Directory> directory;
zx_status_t result =
fdio_service_connect(base::fuchsia::kServiceDirectoryPath,
directory.NewRequest().TakeChannel().release());
ZX_CHECK(result == ZX_OK, result) << "Failed to open /svc";
fuchsia::web::CreateContextParams output;
output.set_service_directory(std::move(directory));
return output;
}
// Checks that the Context channel was dropped.
void CheckContextUnresponsive(
fidl::InterfacePtr<fuchsia::web::Context>* context) {
base::RunLoop run_loop;
context->set_error_handler([&run_loop](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
run_loop.Quit();
});
fuchsia::web::FramePtr frame;
(*context)->CreateFrame(frame.NewRequest());
// The error handler should be called here.
run_loop.Run();
}
protected:
base::MessageLoopForIO message_loop_;
std::unique_ptr<ContextProviderImpl> provider_;
fuchsia::web::ContextProviderPtr provider_ptr_;
fidl::BindingSet<fuchsia::web::ContextProvider> bindings_;
private:
struct CapturingNavigationStateObserver
: public fuchsia::web::NavigationEventListener {
public:
explicit CapturingNavigationStateObserver(base::OnceClosure on_change_cb)
: on_change_cb_(std::move(on_change_cb)) {}
~CapturingNavigationStateObserver() override = default;
void OnNavigationStateChanged(
fuchsia::web::NavigationState change,
OnNavigationStateChangedCallback callback) override {
captured_state_ = std::move(change);
std::move(on_change_cb_).Run();
}
fuchsia::web::NavigationState* captured_state() { return &captured_state_; }
private:
base::OnceClosure on_change_cb_;
fuchsia::web::NavigationState captured_state_;
};
DISALLOW_COPY_AND_ASSIGN(ContextProviderImplTest);
};
TEST_F(ContextProviderImplTest, LaunchContext) {
// Connect to a new context process.
fidl::InterfacePtr<fuchsia::web::Context> context;
fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
provider_ptr_->Create(std::move(create_params), context.NewRequest());
CheckContextResponsive(&context);
}
TEST_F(ContextProviderImplTest, MultipleConcurrentClients) {
// Bind a Provider connection, and create a Context from it.
fuchsia::web::ContextProviderPtr provider_1_ptr;
bindings_.AddBinding(provider_.get(), provider_1_ptr.NewRequest());
fuchsia::web::ContextPtr context_1;
provider_1_ptr->Create(BuildCreateContextParams(), context_1.NewRequest());
// Do the same on another Provider connection.
fuchsia::web::ContextProviderPtr provider_2_ptr;
bindings_.AddBinding(provider_.get(), provider_2_ptr.NewRequest());
fuchsia::web::ContextPtr context_2;
provider_2_ptr->Create(BuildCreateContextParams(), context_2.NewRequest());
CheckContextResponsive(&context_1);
CheckContextResponsive(&context_2);
// Ensure that the initial ContextProvider connection is still usable, by
// creating and verifying another Context from it.
fuchsia::web::ContextPtr context_3;
provider_2_ptr->Create(BuildCreateContextParams(), context_3.NewRequest());
CheckContextResponsive(&context_3);
}
TEST_F(ContextProviderImplTest, WithProfileDir) {
base::ScopedTempDir profile_temp_dir;
// Connect to a new context process.
fidl::InterfacePtr<fuchsia::web::Context> context;
fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
// Setup data dir.
EXPECT_TRUE(profile_temp_dir.CreateUniqueTempDir());
ASSERT_EQ(
base::WriteFile(profile_temp_dir.GetPath().AppendASCII(kTestDataFileIn),
nullptr, 0),
0);
// Pass a handle data dir to the context.
create_params.set_data_directory(
base::fuchsia::OpenDirectory(profile_temp_dir.GetPath()));
provider_ptr_->Create(std::move(create_params), context.NewRequest());
CheckContextResponsive(&context);
// Verify that the context process can write to the data dir.
EXPECT_TRUE(base::PathExists(
profile_temp_dir.GetPath().AppendASCII(kTestDataFileOut)));
}
TEST_F(ContextProviderImplTest, FailsDataDirectoryIsFile) {
base::FilePath temp_file_path;
// Connect to a new context process.
fidl::InterfacePtr<fuchsia::web::Context> context;
fuchsia::web::CreateContextParams create_params = BuildCreateContextParams();
// Pass in a handle to a file instead of a directory.
CHECK(base::CreateTemporaryFile(&temp_file_path));
create_params.set_data_directory(
base::fuchsia::OpenDirectory(temp_file_path));
provider_ptr_->Create(std::move(create_params), context.NewRequest());
CheckContextUnresponsive(&context);
}
static bool WaitUntilJobIsEmpty(zx::unowned_job job, zx::duration timeout) {
zx_signals_t observed = 0;
zx_status_t status =
job->wait_one(ZX_JOB_NO_JOBS, zx::deadline_after(timeout), &observed);
ZX_CHECK(status == ZX_OK || status == ZX_ERR_TIMED_OUT, status);
return observed & ZX_JOB_NO_JOBS;
}
// Regression test for https://crbug.com/927403 (Job leak per-Context).
TEST_F(ContextProviderImplTest, CleansUpContextJobs) {
// Replace the default job with one that is guaranteed to be empty.
zx::job job;
ASSERT_EQ(base::GetDefaultJob()->duplicate(ZX_RIGHT_SAME_RIGHTS, &job),
ZX_OK);
base::ScopedDefaultJobForTest empty_default_job(std::move(job));
// Bind to the ContextProvider.
fuchsia::web::ContextProviderPtr provider;
bindings_.AddBinding(provider_.get(), provider.NewRequest());
// Verify that our current default job is still empty.
ASSERT_TRUE(WaitUntilJobIsEmpty(base::GetDefaultJob(), zx::duration()));
// Create a Context and verify that it is functional.
fuchsia::web::ContextPtr context;
provider->Create(BuildCreateContextParams(), context.NewRequest());
CheckContextResponsive(&context);
// Verify that there is at least one job under our default job.
ASSERT_FALSE(WaitUntilJobIsEmpty(base::GetDefaultJob(), zx::duration()));
// Detach from the Context and ContextProvider, and spin the loop to allow
// those to be handled.
context.Unbind();
provider.Unbind();
base::RunLoop().RunUntilIdle();
// Wait until the default job signals that it no longer contains any child
// jobs; this should occur shortly after the Context process terminates.
EXPECT_TRUE(WaitUntilJobIsEmpty(
base::GetDefaultJob(),
zx::duration(TestTimeouts::action_timeout().InNanoseconds())));
}