blob: c9b0796ee74f811bab1cc7360dd8eefffb1b907e [file] [log] [blame]
// Copyright 2022 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 "test-tool-process.h"
#include <dirent.h>
#include <fcntl.h>
#include <lib/stdcompat/string_view.h>
#include <unistd.h>
#include <zircon/assert.h>
#include <cstdlib>
#include <filesystem>
#include <gtest/gtest.h>
#include "piped-command.h"
#ifdef __Fuchsia__
#include <fidl/fuchsia.boot/cpp/wire.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <fidl/fuchsia.kernel/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fdio/directory.h>
#include <lib/fidl/cpp/wire/server.h>
#include <lib/zxdump/task.h>
#include <zircon/syscalls/object.h>
#include "src/storage/lib/vfs/cpp/pseudo_dir.h"
#include "src/storage/lib/vfs/cpp/service.h"
#include "src/storage/lib/vfs/cpp/synchronous_vfs.h"
#else
#include <libgen.h>
#include <sys/wait.h>
#endif
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
namespace zxdump::testing {
using namespace std::literals;
std::string GetTmpDir() {
#ifndef __Fuchsia__
if (const char* tmpdir = getenv("TMPDIR")) {
std::string dir(tmpdir);
if (!dir.empty() && dir.back() != '/') {
dir += '/';
}
if (!dir.empty()) {
return dir;
}
}
#endif
return "/tmp/";
}
std::string ToolPath(std::string tool) {
#ifdef __Fuchsia__
return std::string("/pkg/bin/") + std::move(tool);
#else
std::filesystem::path path;
#if defined(__APPLE__)
uint32_t length = PATH_MAX;
char self_path[PATH_MAX];
char self_path_symlink[PATH_MAX];
_NSGetExecutablePath(self_path_symlink, &length);
path = dirname(realpath(self_path_symlink, self_path));
#elif defined(__linux__)
char self_path[PATH_MAX];
path = dirname(realpath("/proc/self/exe", self_path));
#else
#error unknown platform.
#endif
return path / tool;
#endif
}
std::string TestToolProcess::FilePathForTool(const TestToolProcess::File& file) const {
ZX_ASSERT(!tmp_path_.empty());
#ifdef __Fuchsia__
// The tool process runs in a sandbox where /tmp/ is actually our tmp_path_.
return "/tmp/" + file.name_;
#else
// The tool runs in the same filesystem namespace as this test code.
return tmp_path_ + file.name_;
#endif
}
std::string TestToolProcess::FilePathForRunner(const std::string& name) const {
ZX_ASSERT(!tmp_path_.empty());
return tmp_path_ + name;
}
std::string TestToolProcess::FilePathForRunner(const TestToolProcess::File& file) const {
return FilePathForRunner(file.name_);
}
#ifdef __Fuchsia__
// The tool process runs with a sandbox namespace that has only its own special
// /tmp and /svc. Its /tmp is mapped to the tmp_path_ subdirectory. Its /svc
// contains only fuchsia.kernel.RootJob pointing at this fake service that just
// gives the test program's own job instead of the real root job.
template <class Protocol, class Handle>
class SandboxGetServer final : public fidl::WireServer<Protocol> {
public:
using typename fidl::WireServer<Protocol>::GetCompleter;
void Init(zx::unowned<Handle> handle) { handle_ = handle; }
void Get(typename GetCompleter::Sync& completer) override {
Handle handle;
zx_status_t status = handle_->duplicate(ZX_RIGHT_SAME_RIGHTS, &handle);
EXPECT_EQ(status, ZX_OK) << zx_status_get_string(status);
completer.Reply(std::move(handle));
}
private:
zx::unowned<Handle> handle_;
};
class TestToolProcess::SandboxLoop {
public:
void Init(zx::unowned_job job, zx::unowned_resource resource,
fidl::ClientEnd<fuchsia_io::Directory>& out_svc) {
loop_.emplace(&kAsyncLoopConfigNoAttachToCurrentThread);
zx_status_t status = loop_->StartThread("TestToolProcess::SandboxLoop");
ASSERT_EQ(status, ZX_OK) << zx_status_get_string(status);
vfs_.emplace(loop_->dispatcher());
svc_dir_ = fbl::MakeRefCounted<fs::PseudoDir>();
AddSvcEntry<fuchsia_kernel::RootJob, &SandboxLoop::root_job_server_>(*job);
AddSvcEntry<fuchsia_boot::RootResource, &SandboxLoop::root_resource_server_>(*resource);
auto [svc_client, svc_server] = fidl::Endpoints<fuchsia_io::Directory>::Create();
status = vfs_->ServeDirectory(svc_dir_, std::move(svc_server));
ASSERT_EQ(status, ZX_OK) << zx_status_get_string(status);
out_svc = std::move(svc_client);
}
~SandboxLoop() {
if (loop_) {
loop_->Shutdown();
}
}
private:
template <class Protocol, auto Member, class Handle>
void AddSvcEntry(const Handle& handle) {
if (handle) {
(this->*Member).Init(handle.borrow());
zx_status_t status = svc_dir_->AddEntry(
fidl::DiscoverableProtocolName<Protocol>,
fbl::MakeRefCounted<fs::Service>(
[this](fidl::ServerEnd<Protocol> request) -> zx_status_t {
fidl::BindServer(loop_->dispatcher(), std::move(request), &(this->*Member));
return ZX_OK;
}));
ASSERT_EQ(status, ZX_OK) << zx_status_get_string(status);
}
}
std::optional<async::Loop> loop_;
std::optional<fs::SynchronousVfs> vfs_;
fbl::RefPtr<fs::PseudoDir> svc_dir_;
SandboxGetServer<fuchsia_kernel::RootJob, zx::job> root_job_server_;
SandboxGetServer<fuchsia_boot::RootResource, zx::resource> root_resource_server_;
};
// Set the spawn actions to populate the namespace for the tool with only its
// own private /tmp and /svc endpoints.
void TestToolProcess::SandboxCommand(PipedCommand& command) {
std::vector<fdio_spawn_action_t> actions;
zx::channel client, server;
ASSERT_EQ(ZX_OK, zx::channel::create(0, &client, &server));
ASSERT_EQ(ZX_OK, fdio_open(tmp_path_.c_str(),
static_cast<uint32_t>(fuchsia_io::OpenFlags::kRightReadable |
fuchsia_io::OpenFlags::kRightWritable |
fuchsia_io::OpenFlags::kDirectory),
server.release()));
actions.push_back({.action = FDIO_SPAWN_ACTION_ADD_NS_ENTRY,
.ns = {
.prefix = "/tmp",
.handle = client.release(),
}});
fidl::ClientEnd<fuchsia_io::Directory> svc;
sandbox_loop_ = std::make_unique<TestToolProcess::SandboxLoop>();
ASSERT_NO_FATAL_FAILURE(sandbox_loop_->Init(job_->borrow(), resource_->borrow(), svc));
actions.push_back({.action = FDIO_SPAWN_ACTION_ADD_NS_ENTRY,
.ns = {
.prefix = "/svc",
.handle = svc.TakeChannel().release(),
}});
command.SetSpawnActions(FDIO_SPAWN_CLONE_ALL & ~FDIO_SPAWN_CLONE_NAMESPACE, std::move(actions));
}
#endif // __Fuchsia__
std::thread SendPipeWorker(fbl::unique_fd fd, std::string contents) {
return std::thread([fd = std::move(fd), contents = std::move(contents)]() mutable {
while (!contents.empty()) {
ssize_t n = write(fd.get(), contents.data(), contents.size());
if (n < 0) {
break;
}
contents.erase(contents.begin(), contents.begin() + n);
}
});
}
std::thread CollectPipeWorker(fbl::unique_fd fd, std::string& result) {
return std::thread([fd = std::move(fd), &result]() mutable {
char buf[PIPE_BUF];
while (true) {
ssize_t n = read(fd.get(), buf, sizeof(buf));
if (n <= 0) {
break;
}
result.append(buf, static_cast<size_t>(n));
}
});
}
fbl::unique_fd TestToolProcess::File::CreateInput() {
fbl::unique_fd fd{
open(owner_->FilePathForRunner(*this).c_str(), O_RDWR | O_CREAT | O_TRUNC | O_EXCL, 0666)};
EXPECT_TRUE(fd) << owner_->FilePathForRunner(*this).c_str() << ": " << strerror(errno);
if (fd) {
EXPECT_EQ(fcntl(fd.get(), F_SETFD, FD_CLOEXEC), 0);
}
return fd;
}
void TestToolProcess::File::CreateInput(std::string_view text) {
fbl::unique_fd fd = CreateInput();
while (!text.empty()) {
ssize_t n = write(fd.get(), text.data(), text.size());
ASSERT_GT(n, 0) << owner_->FilePathForRunner(*this) << ": " << strerror(errno);
text.remove_prefix(static_cast<size_t>(n));
}
}
fbl::unique_fd TestToolProcess::File::OpenOutput() {
fbl::unique_fd fd{open(owner_->FilePathForRunner(*this).c_str(), O_RDONLY)};
if (fd) {
EXPECT_EQ(fcntl(fd.get(), F_SETFD, FD_CLOEXEC), 0);
}
return fd;
}
std::string TestToolProcess::File::OutputContents() {
std::string contents;
char buf[BUFSIZ];
ssize_t nread;
fbl::unique_fd fd = OpenOutput();
while ((nread = read(fd.get(), buf, sizeof(buf))) > 0) {
contents.append(buf, static_cast<size_t>(nread));
}
EXPECT_GE(nread, 0) << strerror(errno);
return contents;
}
TestToolProcess::File TestToolProcess::File::NoFile() {
auto it = owner_->files_.begin();
while (it != owner_->files_.end() && std::addressof(*it) != this) {
++it;
}
EXPECT_NE(it, owner_->files_.end());
File result = std::move(*this);
result.owner_->files_.erase(it);
return result;
}
TestToolProcess::File::~File() = default;
void TestToolProcess::Start(const std::string& tool, const std::vector<std::string>& args) {
ZX_ASSERT(!tmp_path_.empty());
PipedCommand command;
auto redirect = [&](int number, fbl::unique_fd& tool_fd, bool read) {
if (tool_fd) {
command.Redirect(number, std::move(tool_fd));
} else {
int pipe_fd[2];
ASSERT_EQ(pipe(pipe_fd), 0) << strerror(errno);
ASSERT_EQ(fcntl(pipe_fd[0], F_SETFD, FD_CLOEXEC), 0) << strerror(errno);
ASSERT_EQ(fcntl(pipe_fd[1], F_SETFD, FD_CLOEXEC), 0) << strerror(errno);
tool_fd.reset(pipe_fd[read ? 0 : 1]);
command.Redirect(number, fbl::unique_fd{pipe_fd[read ? 1 : 0]});
}
};
redirect(STDIN_FILENO, tool_stdin_, false);
redirect(STDOUT_FILENO, tool_stdout_, true);
redirect(STDERR_FILENO, tool_stderr_, true);
#ifdef __Fuchsia__
ASSERT_NO_FATAL_FAILURE(SandboxCommand(command));
#endif
auto result = command.Start(ToolPath(tool), args);
ASSERT_TRUE(result.is_ok()) << result.error_value();
process_ = std::move(command).process();
}
void TestToolProcess::Finish(int& status) {
#ifdef __Fuchsia__
ASSERT_TRUE(process_);
zx_signals_t signals = 0;
ASSERT_EQ(process_.wait_one(ZX_PROCESS_TERMINATED, zx::time::infinite(), &signals), ZX_OK);
ASSERT_TRUE(signals & ZX_PROCESS_TERMINATED);
zx_info_process_t info;
ASSERT_EQ(process_.get_info(ZX_INFO_PROCESS, &info, sizeof(info), nullptr, nullptr), ZX_OK);
status = static_cast<int>(info.return_code);
process_.reset();
#else
ASSERT_NE(process_, -1);
ASSERT_EQ(waitpid(process_, &status, 0), process_);
status = WIFEXITED(status) ? WEXITSTATUS(status) : -WTERMSIG(status);
process_ = -1;
#endif
}
TestToolProcess::TestToolProcess() = default;
void TestToolProcess::Init() {
tmp_path_ = GetTmpDir() + "tool-tmp.";
int n = 1;
while (mkdir((tmp_path_ + std::to_string(n)).c_str(), 0777) < 0) {
EXPECT_EQ(errno, EEXIST) << strerror(errno);
++n;
}
tmp_path_ += std::to_string(n) + '/';
clear_tmp_ = true;
}
void TestToolProcess::Init(std::string_view tmp_path) { tmp_path_ = tmp_path; }
TestToolProcess::~TestToolProcess() {
bool live = false;
#ifdef __Fuchsia__
live = process_.is_valid();
#else
live = process_ != -1;
#endif
if (live) {
int status = -1;
Finish(status);
EXPECT_EQ(status, 0);
}
if (stdin_thread_.joinable()) {
stdin_thread_.join();
}
if (stdout_thread_.joinable()) {
stdout_thread_.join();
}
if (stderr_thread_.joinable()) {
stderr_thread_.join();
}
for (const File& file : files_) {
std::string path = FilePathForRunner(file);
EXPECT_EQ(remove(path.c_str()), 0) << file.name() << " as " << path << ": " << strerror(errno);
}
if (clear_tmp_) {
EXPECT_EQ(tmp_path_.back(), '/');
tmp_path_.resize(tmp_path_.size() - 1); // Remove trailing slash.
if (rmdir(tmp_path_.c_str()) != 0) {
EXPECT_EQ(errno, ENOTEMPTY) << tmp_path_ << ": " << strerror(errno);
// Emit more complaints with unexpected directory contents if any.
if (DIR* dir = opendir(tmp_path_.c_str())) {
while (const dirent* d = readdir(dir)) {
EXPECT_TRUE(std::string_view(".") == d->d_name || std::string_view("..") == d->d_name)
<< "left in " << tmp_path_ << ": " << d->d_name;
}
}
}
}
}
void TestToolProcess::SendStdin(std::string contents) {
ASSERT_TRUE(tool_stdin_);
ASSERT_FALSE(stdin_thread_.joinable());
stdin_thread_ = SendPipeWorker(std::move(tool_stdin_), std::move(contents));
}
void TestToolProcess::CollectStdout() {
ASSERT_TRUE(tool_stdout_);
ASSERT_FALSE(stdout_thread_.joinable());
stdout_thread_ = CollectPipeWorker(std::move(tool_stdout_), collected_stdout_);
}
void TestToolProcess::CollectStderr() {
ASSERT_TRUE(tool_stderr_);
ASSERT_FALSE(stderr_thread_.joinable());
stderr_thread_ = CollectPipeWorker(std::move(tool_stderr_), collected_stderr_);
}
std::string TestToolProcess::collected_stdout() {
#ifdef __Fuchsia__
EXPECT_FALSE(process_);
#else
EXPECT_EQ(process_, -1);
#endif
EXPECT_TRUE(stdout_thread_.joinable());
stdout_thread_.join();
return std::move(collected_stdout_);
}
std::string TestToolProcess::collected_stderr() {
#ifdef __Fuchsia__
EXPECT_FALSE(process_);
#else
EXPECT_EQ(process_, -1);
#endif
EXPECT_TRUE(stderr_thread_.joinable());
stderr_thread_.join();
return std::move(collected_stderr_);
}
TestToolProcess::File& TestToolProcess::MakeFile(std::string_view name, std::string_view suffix) {
File file;
file.owner_ = this;
file.name_ = "test.";
file.name_ += name;
file.name_ += '.';
int n = 1;
while (std::filesystem::exists(FilePathForRunner(file.name_ + std::to_string(n)) +
std::string(suffix))) {
++n;
}
file.name_ += std::to_string(n);
file.name_ += suffix;
files_.push_back(std::move(file));
return files_.back();
}
TestToolProcess::File& TestToolProcess::File::ZstdCompress() const {
File& zstd_file = owner_->MakeFile(name_, kZstdSuffix);
TestToolProcess zstd_tool;
zstd_tool.Init(owner_->tmp_path());
std::vector<std::string> args({
"-1"s,
name(),
"-o"s,
zstd_file.name(),
});
zstd_tool.Start("zstd", args);
int status;
zstd_tool.Finish(status);
EXPECT_EQ(status, EXIT_SUCCESS);
return zstd_file;
}
TestToolProcess::File& TestToolProcess::File::ZstdDecompress() const {
ZX_ASSERT(cpp20::ends_with(std::string_view(name_), kZstdSuffix));
File& plain_file = owner_->MakeFile(name_.substr(0, name_.size() - kZstdSuffix.size()));
TestToolProcess zstd_tool;
zstd_tool.Init(owner_->tmp_path());
std::vector<std::string> args({
"-d"s,
name(),
"-o"s,
plain_file.name(),
});
zstd_tool.Start("zstd"s, args);
int status;
zstd_tool.Finish(status);
EXPECT_EQ(status, EXIT_SUCCESS);
return plain_file;
}
} // namespace zxdump::testing