| // 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/cpp/caller.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/io.h> |
| #include <lib/fdio/namespace.h> |
| #include <lib/fdio/spawn.h> |
| #include <lib/fidl-async/cpp/bind.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/lib/storage/vfs/cpp/synchronous_vfs.h" |
| |
| namespace runtests { |
| |
| namespace { |
| |
| // Path to helper binary which can run test as a component. This binary takes |
| // component url as its parameter. |
| constexpr char kRunTestComponentPath[] = "/bin/run-test-component"; |
| |
| // 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, "cmx", 3)) { // v1 component |
| *out_component_executor = kRunTestComponentPath; |
| } else 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, const char* realm_label) { |
| // 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; |
| fbl::String realm_label_arg; |
| |
| if (!SetUpForTestComponent(path, &component_executor)) { |
| return std::make_unique<Result>(path, FAILED_TO_LAUNCH, 0, 0); |
| } |
| |
| const char* component_launch_args[argc + 4]; |
| if (realm_label != nullptr) { |
| realm_label_arg = fbl::String::Concat({"--realm-label=", realm_label}); |
| } |
| 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; |
| if (realm_label != nullptr) { |
| component_launch_args[j] = realm_label_arg.c_str(); |
| j++; |
| } |
| 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*, int> map[] = { |
| {"/boot", O_RDONLY}, |
| {"/svc", O_RDONLY}, |
| {"/tmp", O_RDWR}, |
| }; |
| for (auto [path, flags] : map) { |
| fbl::unique_fd fd{open(path, flags | O_DIRECTORY)}; |
| if (!fd) { |
| fprintf(stderr, "FAILURE: Could not open directory %s: %s\n", path, strerror(errno)); |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0); |
| } |
| fdio_cpp::FdioCaller caller(std::move(fd)); |
| zx::status result = caller.take_directory(); |
| if (result.is_error()) { |
| fprintf(stderr, "FAILURE: Could not take directory %s channel: %s\n", path, |
| result.status_string()); |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0); |
| } |
| fidl::ClientEnd client_end = std::move(result).value(); |
| |
| if (strcmp(path, "/svc") == 0) { |
| zx::status 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(fxbug.dev/3260): 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(files.begin(), files.end()); |
| result->data_sinks.emplace(data_sink, std::move(vec_files)); |
| } |
| } |
| |
| return result; |
| } |
| |
| } // namespace runtests |