| // 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 <fuchsia/io/llcpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/debugdata/debugdata.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/fzl/vmo-mapper.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 <string> |
| #include <utility> |
| |
| #include <fbl/auto_call.h> |
| #include <fbl/string.h> |
| #include <fbl/string_printf.h> |
| #include <fbl/unique_fd.h> |
| #include <fs/synchronous_vfs.h> |
| #include <loader-service/loader-service.h> |
| #include <runtests-utils/fuchsia-run-test.h> |
| #include <runtests-utils/service-proxy-dir.h> |
| |
| namespace fio = ::llcpp::fuchsia::io; |
| |
| 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 DirectoryName(const fbl::String& path) { |
| char* cpath = strndup(path.data(), path.length()); |
| fbl::String ret(dirname(cpath)); |
| free(cpath); |
| return ret; |
| } |
| |
| fbl::String BaseName(const fbl::String& path) { |
| char* cpath = strndup(path.data(), path.length()); |
| fbl::String ret(basename(cpath)); |
| free(cpath); |
| return ret; |
| } |
| |
| 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)}); |
| } |
| |
| std::optional<DumpFile> ProcessDataSinkDump(debugdata::DataSinkDump& data, |
| fbl::unique_fd& data_sink_dir_fd, const char* path) { |
| zx_status_t status; |
| |
| if (mkdirat(data_sink_dir_fd.get(), data.sink_name.c_str(), 0777) != 0 && errno != EEXIST) { |
| fprintf(stderr, "FAILURE: cannot mkdir \"%s\" for data-sink: %s\n", data.sink_name.c_str(), |
| strerror(errno)); |
| return {}; |
| } |
| fbl::unique_fd sink_dir_fd{ |
| openat(data_sink_dir_fd.get(), data.sink_name.c_str(), O_RDONLY | O_DIRECTORY)}; |
| if (!sink_dir_fd) { |
| fprintf(stderr, "FAILURE: cannot open data-sink directory \"%s\": %s\n", data.sink_name.c_str(), |
| strerror(errno)); |
| return {}; |
| } |
| |
| zx_info_handle_basic_t info; |
| status = data.file_data.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); |
| if (status != ZX_OK) { |
| return {}; |
| } |
| |
| char name[ZX_MAX_NAME_LEN]; |
| status = data.file_data.get_property(ZX_PROP_NAME, name, sizeof(name)); |
| if (status != ZX_OK || name[0] == '\0') { |
| snprintf(name, sizeof(name), "unnamed.%" PRIu64, info.koid); |
| } |
| |
| uint64_t size; |
| status = data.file_data.get_size(&size); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Cannot get size of VMO \"%s\" for data-sink \"%s\": %s\n", name, |
| data.sink_name.c_str(), zx_status_get_string(status)); |
| return {}; |
| } |
| |
| fzl::VmoMapper mapper; |
| if (size > 0) { |
| zx_status_t status = mapper.Map(data.file_data, 0, size, ZX_VM_PERM_READ); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Cannot map VMO \"%s\" for data-sink \"%s\": %s\n", name, |
| data.sink_name.c_str(), zx_status_get_string(status)); |
| return {}; |
| } |
| } else { |
| fprintf(stderr, "WARNING: Empty VMO \"%s\" published for data-sink \"%s\"\n", name, |
| data.sink_name.c_str()); |
| } |
| |
| char filename[ZX_MAX_NAME_LEN]; |
| snprintf(filename, sizeof(filename), "%s.%" PRIu64, data.sink_name.c_str(), info.koid); |
| |
| fbl::unique_fd fd{openat(sink_dir_fd.get(), filename, O_WRONLY | O_CREAT | O_EXCL, 0666)}; |
| if (!fd) { |
| fprintf(stderr, "FAILURE: Cannot open data-sink file \"%s\": %s\n", data.sink_name.c_str(), |
| strerror(errno)); |
| return {}; |
| } |
| // Strip any leading slashes (including a sequence of slashes) so the dump |
| // file path is a relative to directory that contains the summary file. |
| size_t i = strspn(path, "/"); |
| auto dump_file = JoinPath(&path[i], JoinPath(data.sink_name, filename)); |
| |
| auto* buf = reinterpret_cast<uint8_t*>(mapper.start()); |
| ssize_t count = size; |
| while (count > 0) { |
| ssize_t len = write(fd.get(), buf, count); |
| if (len == -1) { |
| fprintf(stderr, "FAILURE: Cannot write data to \"%s\": %s\n", dump_file.c_str(), |
| strerror(errno)); |
| return {}; |
| } |
| count -= len; |
| buf += len; |
| } |
| |
| return DumpFile{name, dump_file.c_str()}; |
| } |
| |
| } // namespace |
| |
| void TestFileComponentInfo(const fbl::String& path, ComponentInfo* v1_info_out, |
| ComponentInfo* v2_info_out) { |
| if (strncmp(path.c_str(), kPkgPrefix, strlen(kPkgPrefix)) != 0) { |
| return; |
| } |
| |
| // Consume suffixes of the form |
| // "test/<test filename>" or "test/disabled/<test filename>" |
| bool is_disabled = (strstr(path.c_str(), "/disabled/") != nullptr); |
| const auto folder_path = is_disabled ? DirectoryName(DirectoryName(DirectoryName(path))) |
| : DirectoryName(DirectoryName(path)); |
| |
| // folder_path should also start with |kPkgPrefix| and should not be equal |
| // to |kPkgPrefix|. |
| if (strncmp(folder_path.c_str(), kPkgPrefix, strlen(kPkgPrefix)) != 0 || |
| folder_path == fbl::String(kPkgPrefix)) { |
| return; |
| } |
| |
| const char* package_name = path.c_str() + strlen(kPkgPrefix); |
| // find occurence of first '/' |
| size_t i = 0; |
| while (package_name[i] != '\0' && package_name[i] != '/') { |
| i++; |
| } |
| const fbl::String package_name_str(package_name, i); |
| const auto test_file_name = BaseName(path); |
| *v1_info_out = { |
| .component_url = fbl::StringPrintf("fuchsia-pkg://fuchsia.com/%s#meta/%s.cmx", |
| package_name_str.c_str(), test_file_name.c_str()), |
| .manifest_path = |
| fbl::StringPrintf("%s/meta/%s.cmx", folder_path.c_str(), test_file_name.c_str())}; |
| *v2_info_out = { |
| .component_url = fbl::StringPrintf("fuchsia-pkg://fuchsia.com/%s#meta/%s.cm", |
| package_name_str.c_str(), test_file_name.c_str()), |
| .manifest_path = |
| fbl::StringPrintf("%s/meta/%s.cm", folder_path.c_str(), test_file_name.c_str())}; |
| ; |
| } |
| |
| // If test is a component, this function will find appropriate component executor and modify launch |
| // arguments. |
| // Retuns: |
| // |true|: if test is not a component, or if test is a component and it can find correct component |
| // executor. |
| // |false|: if setup fails. |
| bool SetUpForTestComponent(const char* argv[], size_t argc, fbl::String* out_component_url, |
| fbl::String* out_component_executor) { |
| // Values used when running the test as a component. |
| ComponentInfo v1_info, v2_info; |
| const char* test_path = argv[0]; |
| |
| TestFileComponentInfo(test_path, &v1_info, &v2_info); |
| // If we get a non empty |cmx_file_path|, check that it exists, and if |
| // present launch the test as component using generated |component_url|. |
| if (v1_info.manifest_path == "") { |
| // test is not a component. |
| return true; |
| } |
| |
| struct stat s; |
| |
| const char* component_executor = ""; |
| fbl::String component_url; |
| |
| // cmx file is present |
| if (stat(v1_info.manifest_path.c_str(), &s) == 0) { |
| component_executor = kRunTestComponentPath; |
| component_url = v1_info.component_url; |
| } else if (stat(v2_info.manifest_path.c_str(), &s) == 0) { |
| // cm file is present |
| component_executor = kRunTestSuitePath; |
| component_url = v2_info.component_url; |
| } else { |
| // Can't find either cmx or cm file, this test is not a component. |
| return true; |
| } |
| |
| // Check whether the executor is present and print a more helpful error, rather than failing later |
| // in the fdio_spawn_etc call |
| if (stat(component_executor, &s) == 0) { |
| *out_component_executor = component_executor; |
| *out_component_url = std::move(component_url); |
| } else { |
| fprintf(stderr, |
| "FAILURE: Cannot find '%s', cannot run %s as component." |
| "binary.", |
| component_executor, test_path); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| std::unique_ptr<Result> RunTest(const char* argv[], const char* output_dir, |
| const char* output_filename, const char* test_name, |
| uint64_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_url; |
| fbl::String component_executor; |
| |
| if (!SetUpForTestComponent(argv, argc, &component_url, &component_executor)) { |
| return std::make_unique<Result>(path, FAILED_TO_LAUNCH, 0, 0); |
| } |
| |
| const char* component_launch_args[argc + 2]; |
| if (component_url.length() > 0) { |
| component_launch_args[0] = component_executor.c_str(); |
| component_launch_args[1] = component_url.c_str(); |
| for (size_t i = 1; i <= argc; i++) { |
| component_launch_args[1 + i] = argv[i]; |
| } |
| 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}}, |
| }; |
| |
| zx_status_t status; |
| zx::channel svc_proxy_req; |
| fbl::RefPtr<ServiceProxyDir> proxy_dir; |
| std::unique_ptr<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}; |
| std::unique_ptr<debugdata::DebugData> debug_data; |
| |
| // Export the root namespace. |
| fdio_flat_namespace_t* flat; |
| if ((status = fdio_ns_export_root(&flat)) != ZX_OK) { |
| fprintf(stderr, "FAILURE: Cannot export root namespace: %s\n", zx_status_get_string(status)); |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0); |
| } |
| |
| 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. |
| if (output_dir != nullptr) { |
| 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); |
| } |
| |
| zx::channel svc_proxy; |
| status = zx::channel::create(0, &svc_proxy, &svc_proxy_req); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Cannot create channel: %s\n", zx_status_get_string(status)); |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0, 0); |
| } |
| |
| zx::channel svc_handle; |
| for (size_t i = 0; i < flat->count; ++i) { |
| if (!strcmp(flat->path[i], "/svc")) { |
| // Save the current /svc handle... |
| svc_handle.reset(flat->handle[i]); |
| // ...and replace it with the proxy /svc. |
| fdio_actions.push_back(action_ns_entry("/svc", svc_proxy_req.get())); |
| } else { |
| fdio_actions.push_back(action_ns_entry(flat->path[i], flat->handle[i])); |
| } |
| } |
| |
| // Setup DebugData service implementation. |
| debug_data = std::make_unique<debugdata::DebugData>(std::move(root_dir_fd)); |
| |
| // Setup proxy dir. |
| proxy_dir = fbl::MakeRefCounted<ServiceProxyDir>(std::move(svc_handle)); |
| auto node = fbl::MakeRefCounted<fs::Service>( |
| [dispatcher = loop.dispatcher(), debug_data = debug_data.get()](zx::channel channel) { |
| return fidl::Bind(dispatcher, std::move(channel), debug_data); |
| }); |
| proxy_dir->AddEntry(::llcpp::fuchsia::debugdata::DebugData::Name, node); |
| |
| // Setup VFS. |
| vfs = std::make_unique<fs::SynchronousVfs>(loop.dispatcher()); |
| vfs->ServeDirectory(std::move(proxy_dir), std::move(svc_proxy), fs::Rights::ReadWrite()); |
| loop.StartThread(); |
| } else { |
| for (size_t i = 0; i < flat->count; ++i) { |
| fdio_actions.push_back(action_ns_entry(flat->path[i], flat->handle[i])); |
| } |
| } |
| |
| // If |output_filename| is provided, prepare the file descriptors that will |
| // be used to tee the stdout/stderr of the test into the associated file. |
| fbl::unique_fd fds[2]; |
| if (output_filename != nullptr) { |
| int temp_fds[2] = {-1, -1}; |
| if (pipe(temp_fds)) { |
| fprintf(stderr, "FAILURE: Failed to create pipe: %s\n", strerror(errno)); |
| return std::make_unique<Result>(test_name, FAILED_TO_LAUNCH, 0, 0); |
| } |
| fds[0].reset(temp_fds[0]); |
| fds[1].reset(temp_fds[1]); |
| |
| fdio_actions.push_back( |
| fdio_spawn_action{.action = FDIO_SPAWN_ACTION_CLONE_FD, |
| .fd = {.local_fd = fds[1].get(), .target_fd = STDOUT_FILENO}}); |
| fdio_actions.push_back( |
| fdio_spawn_action{.action = FDIO_SPAWN_ACTION_TRANSFER_FD, |
| .fd = {.local_fd = fds[1].get(), .target_fd = STDERR_FILENO}}); |
| } |
| 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 = fbl::MakeAutoCall([&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(BLD-463): 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; |
| |
| fds[1].release(); // To avoid double close since fdio_spawn_etc() closes it. |
| 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_SPAWN_CLONE_ALL & ~FDIO_SPAWN_CLONE_NAMESPACE, |
| 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); |
| } |
| |
| // Tee output. |
| if (output_filename != nullptr) { |
| FILE* output_file = fopen(output_filename, "w"); |
| if (output_file == nullptr) { |
| fprintf(stderr, "FAILURE: Could not open output file at %s: %s\n", output_filename, |
| strerror(errno)); |
| return std::make_unique<Result>(test_name, FAILED_DURING_IO, 0, 0); |
| } |
| char buf[1024]; |
| ssize_t bytes_read = 0; |
| while ((bytes_read = read(fds[0].get(), buf, sizeof(buf))) > 0) { |
| fwrite(buf, 1, bytes_read, output_file); |
| fwrite(buf, 1, bytes_read, stdout); |
| } |
| fflush(stdout); |
| fflush(stderr); |
| fflush(output_file); |
| if (fclose(output_file)) { |
| fprintf(stderr, "FAILURE: Could not close %s: %s", output_filename, strerror(errno)); |
| return std::make_unique<Result>(test_name, FAILED_DURING_IO, 0, 0); |
| } |
| } |
| |
| zx::time deadline = zx::time::infinite(); |
| if (timeout_msec) { |
| deadline = zx::deadline_after(zx::msec(timeout_msec)); |
| } |
| status = process.wait_one(ZX_PROCESS_TERMINATED, deadline, nullptr); |
| const zx::time end_time = zx::clock::get_monotonic(); |
| const int64_t duration_milliseconds = (end_time - start_time).to_msecs(); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Failed to wait for process exiting %s: %d\n", test_name, status); |
| if (status == ZX_ERR_TIMED_OUT) { |
| return std::make_unique<Result>(test_name, TIMED_OUT, 0, duration_milliseconds); |
| } |
| 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); |
| } |
| |
| std::unique_ptr<Result> result; |
| if (proc_info.return_code == 0) { |
| fprintf(stderr, "PASSED: %s passed\n", test_name); |
| result = std::make_unique<Result>(test_name, SUCCESS, 0, duration_milliseconds); |
| } else { |
| fprintf(stderr, "FAILURE: %s exited with nonzero status: %" PRId64 "\n", test_name, |
| proc_info.return_code); |
| result = std::make_unique<Result>(test_name, FAILED_NONZERO_RETURN_CODE, proc_info.return_code, |
| duration_milliseconds); |
| } |
| |
| if (output_dir == nullptr) { |
| return result; |
| } |
| |
| // Make sure that all job processes are dead before touching any data. |
| auto_call_kill_job.call(); |
| |
| // Stop the loop. |
| loop.Quit(); |
| |
| // Wait for any unfinished work to be completed. |
| loop.JoinThreads(); |
| |
| // Run one more time until there are no unprocessed messages. |
| loop.ResetQuit(); |
| loop.Run(zx::time(0)); |
| |
| // Tear down the the VFS. |
| vfs.reset(); |
| |
| fbl::unique_fd data_sink_dir_fd{open(output_dir, O_RDONLY | O_DIRECTORY)}; |
| if (!data_sink_dir_fd) { |
| printf("FAILURE: Could not open output directory %s: %s\n", output_dir, strerror(errno)); |
| return result; |
| } |
| |
| for (auto& data : debug_data->GetData()) { |
| if (auto dump_file = ProcessDataSinkDump(data, data_sink_dir_fd, path)) { |
| result->data_sinks[data.sink_name].push_back(*dump_file); |
| } else if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| } |
| |
| return result; |
| } |
| |
| } // namespace runtests |