| // 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 <runtests-utils/fuchsia-run-test.h> |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <libgen.h> |
| #include <stdio.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include <fbl/auto_call.h> |
| #include <fbl/string.h> |
| #include <fbl/string_printf.h> |
| #include <fbl/unique_fd.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/fdio/io.h> |
| #include <lib/fdio/spawn.h> |
| #include <lib/zx/job.h> |
| #include <lib/zx/process.h> |
| #include <loader-service/loader-service.h> |
| #include <zircon/dlfcn.h> |
| #include <zircon/process.h> |
| #include <zircon/processargs.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| |
| #include <utility> |
| |
| namespace runtests { |
| |
| // 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"; |
| |
| 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; |
| } |
| |
| void TestFileComponentInfo(const fbl::String path, |
| fbl::String* component_url_out, |
| fbl::String* cmx_file_path_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); |
| *cmx_file_path_out = fbl::StringPrintf( |
| "%s/meta/%s.cmx", folder_path.c_str(), test_file_name.c_str()); |
| *component_url_out = |
| fbl::StringPrintf("fuchsia-pkg://fuchsia.com/%s#meta/%s.cmx", |
| package_name_str.c_str(), test_file_name.c_str()); |
| } |
| |
| namespace { |
| |
| struct DataSinkDump { |
| fbl::String sink_name; |
| zx::vmo file_data; |
| }; |
| |
| struct LoaderServiceState { |
| fbl::unique_fd root_dir_fd; |
| fbl::Vector<DataSinkDump> data; |
| }; |
| |
| // This is a default set of library paths. |
| // |
| // Unfortunately this is duplicated in loader-service. We could get rid of this duplication by |
| // delegating to the existing loader service over FIDL for everything except PublishDataSink(), |
| // but the added complexity doesn't seem worth it. |
| const char* const kLibPaths[] = {"system/lib", "boot/lib"}; |
| |
| // This is a helper specifically for the C API boundary with the implementation code. |
| zx_status_t VmoFromFd(fbl::unique_fd fd, const char* file_name, zx_handle_t* out) { |
| zx_status_t status = fdio_get_vmo_clone(fd.get(), out); |
| if (status != ZX_OK) { |
| return status; |
| } |
| return zx_object_set_property(*out, ZX_PROP_NAME, file_name, strlen(file_name)); |
| } |
| |
| zx_status_t LoadObject(void* ctx, const char* name, zx_handle_t* out) { |
| const auto state = static_cast<LoaderServiceState*>(ctx); |
| for (const auto libdir : kLibPaths) { |
| char path[PATH_MAX]; |
| if (snprintf(path, sizeof(path), "%s/%s", libdir, name) < 0) { |
| return ZX_ERR_INTERNAL; |
| } |
| fbl::unique_fd fd(openat(state->root_dir_fd.get(), path, O_RDONLY)); |
| if (fd) { |
| return VmoFromFd(std::move(fd), name, out); |
| } |
| } |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| zx_status_t LoadAbspath(void* ctx, const char* path, zx_handle_t* out) { |
| const auto state = static_cast<LoaderServiceState*>(ctx); |
| fbl::unique_fd fd(openat(state->root_dir_fd.get(), path, O_RDONLY)); |
| if (fd) { |
| return VmoFromFd(std::move(fd), path, out); |
| } |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| zx_status_t PublishDataSink(void* ctx, const char* sink_name, zx_handle_t vmo) { |
| const auto state = static_cast<LoaderServiceState*>(ctx); |
| state->data.push_back({ sink_name, zx::vmo{vmo} }); |
| return ZX_OK; |
| } |
| |
| void Finalizer(void* ctx) { |
| const auto state = static_cast<LoaderServiceState*>(ctx); |
| delete state; |
| } |
| |
| const loader_service_ops_t fd_ops = { |
| .load_object = LoadObject, |
| .load_abspath = LoadAbspath, |
| .publish_data_sink = PublishDataSink, |
| .finalizer = Finalizer, |
| }; |
| |
| // To avoid creating a separate service thread for each test, we have a global |
| // instance of the async loop which is shared by all tests and their loader services. |
| std::unique_ptr<async::Loop> loop; |
| |
| } // namespace |
| |
| std::unique_ptr<Result> FuchsiaRunTest(const char* argv[], |
| const char* output_dir, |
| const char* output_filename) { |
| // The arguments passed to fdio_spaws_etc. May be overridden. |
| const char** args = argv; |
| // calculate size of argv |
| size_t argc = 0; |
| while (argv[argc] != nullptr) { |
| argc++; |
| } |
| |
| // Values used when running the test as a component. |
| const char* component_launch_args[argc + 2]; |
| fbl::String component_url; |
| fbl::String cmx_file_path; |
| |
| const char* path = argv[0]; |
| |
| TestFileComponentInfo(path, &component_url, &cmx_file_path); |
| struct stat s; |
| // 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 (cmx_file_path != "" && stat(cmx_file_path.c_str(), &s) == 0) { |
| if (stat(kRunTestComponentPath, &s) == 0) { |
| component_launch_args[0] = kRunTestComponentPath; |
| 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; |
| } else { |
| // TODO(anmittal): Make this a error once we have a stable |
| // system and we can run all tests as components. |
| fprintf(stderr, |
| "WARNING: Cannot find '%s', running '%s' as normal test " |
| "binary.", |
| kRunTestComponentPath, path); |
| } |
| } |
| |
| fbl::Vector<fdio_spawn_action_t> fdio_actions = { |
| fdio_spawn_action_t{.action = FDIO_SPAWN_ACTION_SET_NAME, |
| .name = {.data = path}}, |
| }; |
| |
| LoaderServiceState* state = nullptr; |
| zx_handle_t svc_handle = ZX_HANDLE_INVALID; |
| loader_service_t* loader_service = nullptr; |
| auto release_service = fbl::MakeAutoCall([&]() { |
| if (loader_service != nullptr) { |
| loader_service_release(loader_service); |
| } |
| }); |
| |
| if (output_dir != nullptr) { |
| state = new LoaderServiceState(); |
| // If |output_dir| is provided, set up the loader service that will be |
| // used to capture any data published. |
| state->root_dir_fd.reset(open("/", O_RDONLY | O_DIRECTORY)); |
| if (!state->root_dir_fd) { |
| printf("FAILURE: Could not open root directory /\n"); |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0); |
| } |
| |
| if (!loop) { |
| loop.reset(new async::Loop(&kAsyncLoopConfigNoAttachToThread)); |
| if (loop->StartThread("loader-service") != ZX_OK) { |
| printf("FAILURE: cannot start message loop\n"); |
| loop.reset(); |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0); |
| } |
| } |
| |
| if (loader_service_create(loop->dispatcher(), &fd_ops, state, &loader_service) != ZX_OK) { |
| printf("FAILURE: cannot create loader service\n"); |
| delete state; |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0); |
| } |
| |
| if (loader_service_connect(loader_service, &svc_handle) != ZX_OK) { |
| printf("FAILURE: cannot connect loader service\n"); |
| return std::make_unique<Result>(path, FAILED_UNKNOWN, 0); |
| } |
| |
| fdio_actions.push_back( |
| fdio_spawn_action{.action = FDIO_SPAWN_ACTION_ADD_HANDLE, |
| .h = {.id = PA_LDSVC_LOADER, .handle = svc_handle}}); |
| } |
| |
| // 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>(path, FAILED_TO_LAUNCH, 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_status_t status = ZX_OK; |
| 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>(path, FAILED_TO_LAUNCH, 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>(path, FAILED_TO_LAUNCH, 0); |
| } |
| |
| 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]; |
| status = fdio_spawn_etc(test_job.get(), FDIO_SPAWN_CLONE_ALL, |
| args[0], args, nullptr, |
| fdio_actions.size(), fdio_actions.get(), |
| process.reset_and_get_address(), err_msg); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Failed to launch %s: %d (%s): %s\n", path, |
| status, zx_status_get_string(status), err_msg); |
| return std::make_unique<Result>(path, FAILED_TO_LAUNCH, 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>(path, FAILED_DURING_IO, 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); |
| } |
| if (fclose(output_file)) { |
| fprintf(stderr, "FAILURE: Could not close %s: %s", output_filename, |
| strerror(errno)); |
| return std::make_unique<Result>(path, FAILED_DURING_IO, 0); |
| } |
| } |
| status = |
| process.wait_one(ZX_PROCESS_TERMINATED, zx::time::infinite(), nullptr); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Failed to wait for process exiting %s: %d\n", |
| path, status); |
| return std::make_unique<Result>(path, FAILED_TO_WAIT, 0); |
| } |
| |
| // 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", |
| path, status); |
| return std::make_unique<Result>(path, FAILED_TO_RETURN_CODE, 0); |
| } |
| |
| std::unique_ptr<Result> result; |
| if (proc_info.return_code == 0) { |
| fprintf(stderr, "PASSED: %s passed\n", path); |
| result = std::make_unique<Result>(path, SUCCESS, 0); |
| } else { |
| fprintf(stderr, "FAILURE: %s exited with nonzero status: %" PRId64 "\n", |
| path, proc_info.return_code); |
| result = std::make_unique<Result>(path, FAILED_NONZERO_RETURN_CODE, |
| proc_info.return_code); |
| } |
| |
| if (output_dir == nullptr) { |
| return result; |
| } |
| |
| // Make sure that all job processes are dead before touching any data. |
| auto_call_kill_job.call(); |
| |
| 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 : state->data) { |
| 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)); |
| if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| continue; |
| } |
| 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)); |
| if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| continue; |
| } |
| |
| uint64_t size; |
| zx_status_t status = data.file_data.get_size(&size); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Cannot get VMO size for data-sink \"%s\": %s\n", |
| data.sink_name.c_str(), zx_status_get_string(status)); |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| continue; |
| } |
| |
| uintptr_t mapping; |
| status = zx::vmar::root_self()->map(0, data.file_data, 0, size, |
| ZX_VM_PERM_READ, &mapping); |
| if (status != ZX_OK) { |
| fprintf(stderr, "FAILURE: Cannot map VMO of %" PRIu64 " for data-sink \"%s\": %s\n", |
| size, data.sink_name.c_str(), zx_status_get_string(status)); |
| if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| continue; |
| } |
| auto unmap_vmar = fbl::MakeAutoCall([&]() { zx::vmar::root_self()->unmap(mapping, size); }); |
| |
| 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) { |
| if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| continue; |
| } |
| 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)); |
| if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| continue; |
| } |
| // 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)); |
| |
| uint8_t *buf = reinterpret_cast<uint8_t *>(mapping); |
| ssize_t count = size; |
| ssize_t len = 0; |
| while (count > 0 && (len = write(fd.get(), buf, count)) != count) { |
| if (len == -1) { |
| fprintf(stderr, "FAILURE: Cannot write data to \"%s\": %s\n", |
| dump_file.c_str(), strerror(errno)); |
| if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| break; |
| } |
| count -= len; |
| buf += len; |
| } |
| if (len == -1) { |
| continue; |
| } |
| |
| char name[ZX_MAX_NAME_LEN]; |
| status = data.file_data.get_property(ZX_PROP_NAME, name, sizeof(name)); |
| if (status != ZX_OK) { |
| if (result->return_code == 0) { |
| result->launch_status = FAILED_COLLECTING_SINK_DATA; |
| } |
| continue; |
| } |
| if (name[0] == '\0') { |
| snprintf(name, sizeof(name), "unnamed.%" PRIu64, info.koid); |
| } |
| |
| Result::HashTable::iterator it; |
| result->data_sinks.insert_or_find(std::make_unique<DataSink>(data.sink_name), &it); |
| it->files.push_back(DumpFile{name, dump_file}); |
| } |
| |
| return result; |
| } |
| |
| } // namespace runtests |