blob: b4c39b02187e43cac463176e7a489a3f8a7ff977 [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 <errno.h>
#include <fcntl.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/debugdata/datasink.h>
#include <lib/debugdata/debugdata.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/spawn.h>
#include <lib/fit/defer.h>
#include <lib/zx/clock.h>
#include <lib/zx/job.h>
#include <lib/zx/process.h>
#include <libgen.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <zircon/dlfcn.h>
#include <zircon/process.h>
#include <zircon/processargs.h>
#include <zircon/status.h>
#include <zircon/syscalls.h>
#include <algorithm>
#include <forward_list>
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <fbl/string.h>
#include <fbl/string_printf.h>
#include <fbl/unique_fd.h>
#include <runtests-utils/fuchsia-run-test.h>
#include <runtests-utils/service-proxy-dir.h>
#include "src/storage/lib/vfs/cpp/synchronous_vfs.h"
namespace runtests {
namespace {
// Path to helper binary which can run test as a v2 component. This binary takes
// component url as its parameter.
constexpr char kRunTestSuitePath[] = "/bin/run-test-suite";
fbl::String RootName(const fbl::String& path) {
const size_t i = strspn(path.c_str(), "/");
const char* start = &path.c_str()[i];
const char* end = strchr(start, '/');
if (end == nullptr) {
end = &path.c_str()[path.size()];
}
return fbl::String::Concat({"/", fbl::String(start, end - start)});
}
} // namespace
bool SetUpForTestComponent(const char* test_path, fbl::String* out_component_executor) {
if (IsFuchsiaPkgURI(test_path)) {
const char* last_three_chars_of_url = &(test_path[strlen(test_path) - 3]);
if (0 == strncmp(last_three_chars_of_url, ".cm", 3)) { // v2
*out_component_executor = kRunTestSuitePath;
} else {
fprintf(stderr, "FAILURE: component URL has unexpected format: %s\n", test_path);
return false;
}
} else if (0 == strncmp(test_path, kPkgPrefix, strlen(kPkgPrefix))) {
fprintf(stderr, "FAILURE: Test path '%s' starts with %s, which is not supported.\n", test_path,
kPkgPrefix);
return false;
}
return true;
}
// Run the loop until |deadline|, or until the specified |signal| is asserted on the |handle|.
zx_status_t RunLoopUntilSignalOrDeadline(async::Loop& loop, zx::time deadline, zx_handle_t handle,
zx_signals_t signal) {
async::WaitOnce process_wait(handle, signal);
zx_status_t wait_status = ZX_ERR_TIMED_OUT;
process_wait.Begin(loop.dispatcher(), [&loop, &wait_status](async_dispatcher_t*, async::WaitOnce*,
zx_status_t wait_status_inner,
const zx_packet_signal_t*) {
wait_status = wait_status_inner;
loop.Quit();
});
loop.Run(deadline);
loop.ResetQuit();
return wait_status;
}
std::unique_ptr<Result> RunTest(const char* argv[], const char* output_dir, const char* test_name,
int64_t timeout_msec) {
// The arguments passed to fdio_spawn_etc. May be overridden.
const char** args = argv;
// calculate size of argv
size_t argc = 0;
while (argv[argc] != nullptr) {
argc++;
}
const char* path = argv[0];
fbl::String component_executor;
if (!SetUpForTestComponent(path, &component_executor)) {
return std::make_unique<Result>(path, FAILED_TO_LAUNCH, 0, 0);
}
const char* component_launch_args[argc + 3];
if (component_executor.length() > 0) {
// Check whether the executor is present and print a more helpful error, rather than failing
// later in the fdio_spawn_etc call.
struct stat s;
if (stat(component_executor.c_str(), &s)) {
fprintf(stderr,
"FAILURE: Cannot find '%s', cannot run %s as component."
"binary.\n",
component_executor.c_str(), path);
return std::make_unique<Result>(path, FAILED_TO_LAUNCH, 0, 0);
}
component_launch_args[0] = component_executor.c_str();
int j = 1;
component_launch_args[j] = path;
j++;
for (size_t i = 1; i <= argc; i++) {
component_launch_args[j] = argv[i];
j++;
}
args = component_launch_args;
}
// Truncate the name on the left so the more important stuff on the right part of the path stays
// in the name.
const char* test_name_trunc = test_name;
size_t test_name_length = strlen(test_name_trunc);
if (test_name_length > ZX_MAX_NAME_LEN - 1) {
test_name_trunc += test_name_length - (ZX_MAX_NAME_LEN - 1);
}
fbl::Vector<fdio_spawn_action_t> fdio_actions = {
fdio_spawn_action_t{
.action = FDIO_SPAWN_ACTION_SET_NAME,
.name =
{
.data = test_name_trunc,
},
},
};
std::optional<fs::SynchronousVfs> vfs;
// This must be declared after the vfs so that its destructor gets called before the vfs
// destructor. We do this explicitly at the end of the function in the non-error case, but in
// error cases we just rely on the destructors to clean things up.
async::Loop loop{&kAsyncLoopConfigNoAttachToCurrentThread};
bool data_collection_err_occurred = false;
fbl::unique_fd data_sink_dir_fd;
std::optional<debugdata::Publisher> debug_data_publisher;
std::optional<debugdata::DataSink> debug_data_sink;
debugdata::DataSinkCallback on_data_collection_error_callback = [&](const std::string& error) {
fprintf(stderr, "FAILURE: %s\n", error.c_str());
data_collection_err_occurred = true;
};
debugdata::DataSinkCallback on_data_collection_warning_callback =
[&](const std::string& warning) { fprintf(stderr, "WARNING: %s\n", warning.c_str()); };
auto action_ns_entry = [](const char* prefix, zx_handle_t handle) {
return fdio_spawn_action{
.action = FDIO_SPAWN_ACTION_ADD_NS_ENTRY,
.ns =
{
.prefix = prefix,
.handle = handle,
},
};
};
// If |output_dir| is provided, set up the loader and debugdata services that will be
// used to capture any data published.
uint32_t fdio_flags = FDIO_SPAWN_CLONE_ALL;
if (output_dir != nullptr) {
fdio_flags &= ~FDIO_SPAWN_CLONE_NAMESPACE;
fbl::unique_fd root_dir_fd{open("/", O_RDONLY | O_DIRECTORY)};
if (!root_dir_fd) {
fprintf(stderr, "FAILURE: Could not open root directory /\n");
return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0);
}
data_sink_dir_fd = fbl::unique_fd(open(output_dir, O_RDONLY | O_DIRECTORY));
if (!data_sink_dir_fd) {
fprintf(stderr, "FAILURE: Could not open output directory %s: %s\n", output_dir,
strerror(errno));
return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0);
}
// Setup DebugData service implementation.
debug_data_sink.emplace(data_sink_dir_fd);
debug_data_publisher.emplace(
loop.dispatcher(), std::move(root_dir_fd), [&](std::string data_sink, zx::vmo vmo) {
debug_data_sink->ProcessSingleDebugData(data_sink, std::move(vmo), {},
on_data_collection_error_callback,
on_data_collection_warning_callback);
});
auto node = fbl::MakeRefCounted<fs::Service>(
[dispatcher = loop.dispatcher(),
&debug_data_publisher](fidl::ServerEnd<fuchsia_debugdata::Publisher> channel) {
debug_data_publisher->Bind(std::move(channel), dispatcher);
return ZX_OK;
});
vfs.emplace(loop.dispatcher());
std::tuple<const char*, fuchsia_io::OpenFlags> map[] = {
{"/boot", fuchsia_io::OpenFlags::kRightReadable},
{"/svc", {}},
{"/tmp", fuchsia_io::OpenFlags::kRightReadable | fuchsia_io::OpenFlags::kRightWritable},
};
for (auto [path, flags] : map) {
auto [client_end, server_end] = fidl::Endpoints<fuchsia_io::Directory>::Create();
if (zx_status_t status =
fdio_open(path, static_cast<uint32_t>(flags), server_end.TakeChannel().release());
status != ZX_OK) {
fprintf(stderr, "FAILURE: Could not open directory %s: %s\n", path,
zx_status_get_string(status));
return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0);
}
if (strcmp(path, "/svc") == 0) {
zx::result endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
if (endpoints.is_error()) {
fprintf(stderr, "FAILURE: Could not create endpoints: %s\n", endpoints.status_string());
return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0);
}
fbl::RefPtr proxy_dir = fbl::MakeRefCounted<ServiceProxyDir>(std::move(client_end));
proxy_dir->AddEntry(fidl::DiscoverableProtocolName<fuchsia_debugdata::Publisher>, node);
vfs->ServeDirectory(std::move(proxy_dir), std::move(endpoints->server), fs::Rights::All());
fdio_actions.push_back(action_ns_entry(path, endpoints->client.channel().release()));
} else {
fdio_actions.push_back(action_ns_entry(path, client_end.TakeChannel().release()));
}
}
}
zx_status_t status;
zx::job test_job;
status = zx::job::create(*zx::job::default_job(), 0, &test_job);
if (status != ZX_OK) {
fprintf(stderr, "FAILURE: zx::job::create() returned %d\n", status);
return std::make_unique<Result>(test_name, FAILED_TO_LAUNCH, 0, 0);
}
auto auto_call_kill_job = fit::defer([&test_job]() { test_job.kill(); });
status = test_job.set_property(ZX_PROP_NAME, "run-test", sizeof("run-test"));
if (status != ZX_OK) {
fprintf(stderr, "FAILURE: set_property() returned %d\n", status);
return std::make_unique<Result>(test_name, FAILED_TO_LAUNCH, 0, 0);
}
// The TEST_ROOT_DIR environment variable allows tests that could be stored in
// "/system" or "/boot" to discern where they are running, and modify paths
// accordingly.
//
// TODO(https://fxbug.dev/42107703): The hard-coded set of prefixes is not ideal. Ideally, this
// would instead set the "root" to the parent directory of the "test/"
// subdirectory where globbing was done to collect the set of tests in
// DiscoverAndRunTests(). But then it's not clear what should happen if
// using `-f` to provide a list of paths instead of directories to glob.
const fbl::String root = RootName(path);
// |root_var| must be kept alive for |env_vars| since |env_vars| may hold
// a pointer into it.
fbl::String root_var;
fbl::Vector<const char*> env_vars;
if (root == "/system" || root == "/boot") {
for (size_t i = 0; environ[i] != nullptr; ++i) {
env_vars.push_back(environ[i]);
}
root_var = fbl::String::Concat({"TEST_ROOT_DIR=", root});
env_vars.push_back(root_var.c_str());
env_vars.push_back(nullptr);
}
const char* const* env_vars_p = !env_vars.is_empty() ? env_vars.begin() : nullptr;
zx::process process;
char err_msg[FDIO_SPAWN_ERR_MSG_MAX_LENGTH];
const zx::time start_time = zx::clock::get_monotonic();
status =
fdio_spawn_etc(test_job.get(), fdio_flags, args[0], args, env_vars_p, fdio_actions.size(),
fdio_actions.data(), process.reset_and_get_address(), err_msg);
if (status != ZX_OK) {
fprintf(stderr, "FAILURE: Failed to launch %s: %d (%s): %s\n", test_name, status,
zx_status_get_string(status), err_msg);
return std::make_unique<Result>(test_name, FAILED_TO_LAUNCH, 0, 0);
}
zx::time deadline = zx::time::infinite();
if (timeout_msec) {
deadline = zx::deadline_after(zx::msec(timeout_msec));
}
// Run the loop until the process terminates. Until the process terminates, asynchronously
// handle any debug data that becomes ready.
status = RunLoopUntilSignalOrDeadline(loop, deadline, process.get(), ZX_PROCESS_TERMINATED);
const zx::time end_time = zx::clock::get_monotonic();
const int64_t duration_milliseconds = (end_time - start_time).to_msecs();
if (status != ZX_OK) {
if (status == ZX_ERR_TIMED_OUT) {
fprintf(stderr, "%s timed out\n", test_name);
return std::make_unique<Result>(test_name, TIMED_OUT, 0, duration_milliseconds);
}
fprintf(stderr, "FAILURE: Failed to wait for process exiting %s: %d (%s)\n", test_name, status,
zx_status_get_string(status));
return std::make_unique<Result>(test_name, FAILED_TO_WAIT, 0, duration_milliseconds);
}
// Read the return code.
zx_info_process_t proc_info;
status = process.get_info(ZX_INFO_PROCESS, &proc_info, sizeof(proc_info), nullptr, nullptr);
if (status != ZX_OK) {
fprintf(stderr, "FAILURE: Failed to get process return code %s: %d\n", test_name, status);
return std::make_unique<Result>(test_name, FAILED_TO_RETURN_CODE, 0, duration_milliseconds);
}
// Make a best effort to wait for any other tasks in the loop to terminate.
auto_call_kill_job.call();
status = RunLoopUntilSignalOrDeadline(loop, deadline, test_job.get(), ZX_TASK_TERMINATED);
if (status != ZX_OK) {
if (status == ZX_ERR_TIMED_OUT) {
fprintf(stderr, "WARNING: Timed out waiting for test job to terminate\n");
} else {
fprintf(stderr, "WARNING: Failed to wait for job to terminate: %d (%s)\n", status,
zx_status_get_string(status));
}
}
// Run one more time until there are no unprocessed messages.
loop.ResetQuit();
loop.Run(zx::time(0));
// Tear down the the VFS.
vfs.reset();
// The emitted signature, eg "[runtests][PASSED] /test/name", is used by the CQ/CI testrunners to
// match test names and outcomes. Changes to this format must be matched in
// https://fuchsia.googlesource.com/fuchsia/+/HEAD/tools/testing/runtests/output.go
std::unique_ptr<Result> result;
if (proc_info.return_code == 0) {
result = std::make_unique<Result>(test_name, SUCCESS, 0, duration_milliseconds);
if (data_collection_err_occurred) {
result->launch_status = FAILED_COLLECTING_SINK_DATA;
}
} else {
fprintf(stderr, "%s exited with nonzero status: %" PRId64, test_name, proc_info.return_code);
result = std::make_unique<Result>(test_name, FAILED_NONZERO_RETURN_CODE, proc_info.return_code,
duration_milliseconds);
}
if (debug_data_publisher) {
debug_data_publisher->DrainData();
auto written_files = debug_data_sink->FlushToDirectory(on_data_collection_error_callback,
on_data_collection_warning_callback);
for (auto& [data_sink, files] : written_files) {
std::vector<debugdata::DumpFile> vec_files;
for (auto& [dump_file, tags] : files) {
vec_files.push_back(dump_file);
}
result->data_sinks.emplace(data_sink, std::move(vec_files));
}
}
return result;
}
} // namespace runtests