blob: 14f946bd797a965be64f8f6fb19f3634f4bdac0b [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 <fuchsia/debugdata/cpp/fidl.h>
#include <fuchsia/diagnostics/cpp/fidl.h>
#include <fuchsia/diagnostics/test/cpp/fidl.h>
#include <fuchsia/logger/cpp/fidl.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fidl/cpp/interface_ptr.h>
#include <lib/fit/function.h>
#include <lib/sys/cpp/file_descriptor.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/sys/cpp/termination_reason.h>
#include <lib/sys/cpp/testing/enclosing_environment.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/vfs/cpp/service.h>
#include <lib/zx/time.h>
#include <stdio.h>
#include <zircon/assert.h>
#include <zircon/errors.h>
#include <zircon/status.h>
#include <zircon/syscalls.h>
#include <zircon/types.h>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "lib/fidl/cpp/interface_request.h"
#include "lib/zx/object_traits.h"
#include "src/lib/files/file.h"
#include "src/lib/files/glob.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/sys/run_test_component/component.h"
#include "src/sys/run_test_component/log_collector.h"
#include "src/sys/run_test_component/run_test_component.h"
#include "src/sys/run_test_component/sys_tests.h"
#include "src/sys/run_test_component/test_metadata.h"
using fuchsia::sys::TerminationReason;
namespace {
constexpr char kEnvPrefix[] = "test_env_";
const uint64_t kMillisInSec = 1000UL;
const uint64_t kMicrosInSec = 1000000UL;
const uint64_t kNanosInSec = 1000000000UL;
const std::string max_severity_config_path =
void PrintUsage() {
fprintf(stderr, R"(
Usage: run_test_component [--realm-label=<label>] [--timeout=<seconds>] [--min-severity-logs=string] [--max-log-severity=string] <test_url>|<test_matcher> -- [arguments...]
*test_url* takes the form of component manifest URL which uniquely
identifies a test component. Example:
if *test_matcher* is provided, this tool will use component index
to find matching component. If multiple urls are found, it will
print corresponding component URLs and exit. If there is only
one match, it will generate a component URL and execute the test.
run_test_component run_test_component_unit
will match fuchsia-pkg:// and run it.
By default each test component will be run in an environment with
transient storage and a randomly-generated identifier, ensuring that
the tests have no persisted side-effects. If --realm-label is
specified then the test will run in a persisted realm with that label,
allowing files to be provide to, or retrieve from, the test, e.g. for
diagnostic purposes.
If --timeout is specified, test would be killed in <timeout> secs and
run_test_component will exit with -ZX_ERR_TIMED_OUT.
If --max-log-severity is passed, then the test will fail if it produces logs with higher severity.
For more information see:
By default when installing log listener, all logs are collected. To filter
by higher severity please pass severity: TRACE, DEBUG, INFO, WARN, ERROR, FATAL.
example: run-test-component --min-severity-logs=WARN <url>
bool ConnectToSysEnvironment(zx::channel request) {
std::string current_env;
files::ReadFileToString("/hub/name", &current_env);
std::string svc_path = "/hub/svc";
if (current_env == "app") {
files::Glob glob("/hub/r/sys/*/svc");
if (glob.size() != 1) {
fprintf(stderr, "Cannot run test. Something wrong with hub.");
return false;
svc_path = *(glob.begin());
} else if (current_env != "sys") {
"Cannot run test in sys environment as this utility was "
"started in '%s' environment",
return false;
// launch test
zx::channel h1, h2;
zx_status_t status;
if ((status = zx::channel::create(0, &h1, &h2)) != ZX_OK) {
fprintf(stderr, "Cannot create channel, status: %d", status);
return false;
if ((status = fdio_service_connect(svc_path.c_str(), h1.release())) != ZX_OK) {
fprintf(stderr, "Cannot connect to %s, status: %d", svc_path.c_str(), status);
return false;
if ((status = fdio_service_connect_at(h2.get(), fuchsia::sys::Environment::Name_,
request.release())) != ZX_OK) {
fprintf(stderr, "Cannot connect to env service, status: %d", status);
return false;
return true;
std::string join_tags(std::vector<std::string> tags) {
std::ostringstream stream;
for (size_t i = 0; i < tags.size(); ++i) {
if (i != 0) {
stream << ",";
stream << tags[i];
return stream.str();
std::string log_level(int32_t severity) {
switch (severity) {
case syslog::LOG_TRACE:
return "TRACE";
case syslog::LOG_DEBUG:
return "DEBUG";
case syslog::LOG_INFO:
return "INFO";
case syslog::LOG_WARNING:
return "WARNING";
case syslog::LOG_ERROR:
return "ERROR";
case syslog::LOG_FATAL:
return "FATAL";
if (severity > syslog::LOG_DEBUG && severity < syslog::LOG_INFO) {
std::ostringstream stream;
stream << "VLOG(" << -(syslog::LOG_INFO - severity) << ")";
return stream.str();
return "INVALID";
std::unique_ptr<run::Component> launch_archivist(const fuchsia::sys::LauncherPtr& launcher,
async_dispatcher_t* dispatcher) {
fuchsia::sys::LaunchInfo launch_info{
.url =
launch_info.arguments.emplace({"--v1", "no-log-connector"});
return run::Component::Launch(launcher, std::move(launch_info), dispatcher);
void print_log_message(const std::shared_ptr<fuchsia::logger::LogMessage>& log) {
auto time = log->time;
printf("[%05ld.%06ld][%ld][%ld][%s] %s: %s\n", time / kNanosInSec,
(time / kMillisInSec) % kMicrosInSec, log->pid, log->tid, join_tags(log->tags).c_str(),
log_level(log->severity).c_str(), log->msg.c_str());
void print_dropped_log_count(const std::shared_ptr<fuchsia::logger::LogMessage>& log) {
auto time = log->time;
printf("[%05ld.%06ld][%ld][%ld][%s] WARNING: Dropped logs count: %u\n", time / kNanosInSec,
(time / kMillisInSec) % kMicrosInSec, log->pid, log->tid, join_tags(log->tags).c_str(),
} // namespace
int main(int argc, const char** argv) {
// Services which we get from /svc. They might be different depending on in which shell this
// binary is launched from, so can't use it to create underlying environment.
auto namespace_services = sys::ServiceDirectory::CreateFromNamespace();
async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
auto parse_result = run::ParseArgs(namespace_services, argc, argv);
if (parse_result.error) {
if (parse_result.error_msg != "") {
fprintf(stderr, "%s\n", parse_result.error_msg.c_str());
return 1;
if (parse_result.matching_urls.size() > 1) {
fprintf(stderr, "Found multiple matching components. Did you mean?\n");
for (auto url : parse_result.matching_urls) {
fprintf(stderr, "%s\n", url.c_str());
return 1;
} else if (parse_result.matching_urls.size() == 1) {
fprintf(stdout, "Found one matching component. Running: %s\n",
std::string program_name = parse_result.launch_info.url;
// We make a request to the resolver API to ensure that the on-disk package
// data is up to date before continuing to try and parse the CMX file.
// TODO(raggi): replace this with fuchsia.pkg.Resolver, once it is stable.
fuchsia::sys::LoaderSyncPtr loader;
zx_status_t status = namespace_services->Connect<fuchsia::sys::Loader>(loader.NewRequest());
if (status != ZX_OK) {
fprintf(stderr, "connect to %s failed: %s. Can not continue.\n", fuchsia::sys::Loader::Name_,
return 1;
fuchsia::sys::PackagePtr pkg;
status = loader->LoadUrl(program_name, &pkg);
if (status != ZX_OK) {
fprintf(stderr, "Failed to load %s: %s\n", program_name.c_str(), zx_status_get_string(status));
return 1;
if (!pkg) {
fprintf(stderr, "Got no package for %s\n", program_name.c_str());
return 1;
if (!pkg->data) {
fprintf(stderr, "Got no package metadata for %s\n", program_name.c_str());
return 1;
uint64_t size = pkg->data->size;
std::string cmx_str(size, ' ');
status = pkg->data->, 0, size);
if (status != ZX_OK) {
fprintf(stderr, "error reading cmx file from vmo %s: %s\n", program_name.c_str(),
return 1;
run::TestMetadata test_metadata;
if (!test_metadata.ParseFromString(cmx_str, program_name)) {
fprintf(stderr, "Error parsing cmx %s: %s\n", program_name.c_str(),
return 1;
fuchsia::sys::EnvironmentPtr parent_env;
fuchsia::sys::LauncherPtr launcher;
std::unique_ptr<sys::testing::EnclosingEnvironment> diagnostics_enclosing_env;
std::unique_ptr<sys::testing::EnclosingEnvironment> enclosing_env;
std::vector<std::shared_ptr<fuchsia::logger::LogMessage>> restricted_logs;
auto max_severity_allowed = parse_result.max_log_severity;
bool restrict_logs = max_severity_allowed != syslog::LOG_FATAL;
auto log_collector = std::make_unique<run::LogCollector>(
[dispather = loop.dispatcher(), restrict_logs, max_severity_allowed,
&restricted_logs](fuchsia::logger::LogMessage log) {
static std::map<uint64_t, uint32_t> dropped_logs_map;
auto log_wrapper = std::make_shared<fuchsia::logger::LogMessage>(std::move(log));
if (restrict_logs && log_wrapper->severity > max_severity_allowed) {
if (log_wrapper->dropped_logs > 0) {
auto dropped_logs =
dropped_logs_map[log_wrapper->pid]; // default initializer will set it
// to zero if key is not found.
if (log_wrapper->dropped_logs > dropped_logs) {
dropped_logs_map[log_wrapper->pid] = log_wrapper->dropped_logs;
async::PostTask(dispather, [log = std::move(log_wrapper)]() mutable {
std::unique_ptr<run::Component> archivist_component = nullptr;
if (run::should_run_in_sys(parse_result.launch_info.url)) {
if (test_metadata.HasServices()) {
"Cannot run this test in sys/root environment as it defines "
"services in its '%s' facets\n",
return 1;
if (!ConnectToSysEnvironment(parent_env.NewRequest().TakeChannel())) {
return 1;
} else {
// Our bots run tests in zircon shell which do not have all required services, so create the
// test environment from `parent_env` (i.e. the sys environment) instead of the services in
// the namespace. But pass DebugData from the namespace because it is not available in
// `parent_env`.
sys::testing::EnvironmentServices::ParentOverrides parent_overrides;
parent_overrides.debug_data_publisher_service_ =
std::make_shared<vfs::Service>([namespace_services = namespace_services](
zx::channel channel, async_dispatcher_t* /*unused*/) {
namespace_services->Connect(fuchsia::debugdata::Publisher::Name_, std::move(channel));
auto test_env_services = sys::testing::EnvironmentServices::CreateWithParentOverrides(
parent_env, std::move(parent_overrides));
auto services = test_metadata.TakeServices();
bool collect_logs = true;
bool offer_collected_logs = true;
bool offer_diagnostics_archive = true;
for (auto& service : services) {
test_env_services->AddServiceWithLaunchInfo(std::move(service.second), service.first);
if (service.first == fuchsia::logger::LogSink::Name_) {
// don't add log sink service if test component is injecting it.
collect_logs = false;
if (service.first == fuchsia::logger::Log::Name_) {
// don't add log service if test component is injecting it.
offer_collected_logs = false;
if (service.first == fuchsia::diagnostics::ArchiveAccessor::Name_) {
// don't add ArchiveAccessor if test component is injecting it.
offer_diagnostics_archive = false;
auto& requested_system_services = test_metadata.system_services();
for (auto& service : requested_system_services) {
if (service == fuchsia::logger::LogSink::Name_) {
// don't add log sink service if test component is using system service.
collect_logs = false;
if (service == fuchsia::logger::Log::Name_) {
// don't add log service if test component is using system service.
offer_collected_logs = false;
if (service == fuchsia::diagnostics::ArchiveAccessor::Name_) {
// don't add ArchiveAccessor if test component is using system service.
offer_diagnostics_archive = false;
// Compute a common random suffix for the environments created to run the test.
uint32_t env_rand_suffix;
zx_cprng_draw(&env_rand_suffix, sizeof(env_rand_suffix));
if (collect_logs || offer_collected_logs || offer_diagnostics_archive) {
// create a nested diagnostics realm and launch the archivist if it'll be used
std::string env_label = fxl::StringPrintf("%s%08x", "diagnostics_", env_rand_suffix);
diagnostics_enclosing_env = sys::testing::EnclosingEnvironment::Create(
std::move(env_label), parent_env, sys::testing::EnvironmentServices::Create(parent_env),
fuchsia::sys::EnvironmentOptions{.inherit_parent_services = true,
.use_parent_runners = true,
.delete_storage_on_death = true});
archivist_component =
launch_archivist(diagnostics_enclosing_env->launcher_ptr(), loop.dispatcher());
if (collect_logs) {
ZX_ASSERT(archivist_component != nullptr);
[archivist_svc = archivist_component->svc()](
fidl::InterfaceRequest<fuchsia::logger::LogSink> request) {
if (offer_collected_logs) {
ZX_ASSERT(archivist_component != nullptr);
[archivist_svc =
archivist_component->svc()](fidl::InterfaceRequest<fuchsia::logger::Log> request) {
if (offer_diagnostics_archive) {
ZX_ASSERT(archivist_component != nullptr);
[archivist_svc = archivist_component->svc()](
fidl::InterfaceRequest<fuchsia::diagnostics::ArchiveAccessor> request) {
auto& system_services = test_metadata.system_services();
for (auto& service : system_services) {
// By default run tests in a realm with a random name and transient storage.
// Callers may specify a static realm label through which to exchange files
// with the test component.
std::string env_label = std::move(parse_result.realm_label);
fuchsia::sys::EnvironmentOptions env_opt;
if (env_label.empty()) {
env_label = fxl::StringPrintf("%s%08x", kEnvPrefix, env_rand_suffix);
env_opt.delete_storage_on_death = true;
enclosing_env = sys::testing::EnclosingEnvironment::Create(
env_label, parent_env, std::move(test_env_services), std::move(env_opt));
if (collect_logs) {
ZX_ASSERT(archivist_component != nullptr);
// this will launch the service and also collect logs.
auto log_ptr = archivist_component->svc()->Connect<fuchsia::logger::Log>();
fidl::InterfaceHandle<fuchsia::logger::LogListenerSafe> log_listener;
auto options = std::make_unique<fuchsia::logger::LogFilterOptions>();
options->min_severity =
log_collector->Bind(log_listener.NewRequest(), loop.dispatcher());
log_ptr->ListenSafe(std::move(log_listener), std::move(options));
launcher = enclosing_env->launcher_ptr();
printf("Running test in realm: %s\n", env_label.c_str());
auto test_component =
run::Component::Launch(launcher, std::move(parse_result.launch_info), loop.dispatcher());
int ret_code = 1;
bool timed_out = false;
std::unique_ptr<async::TaskClosure> timeout_task;
if (parse_result.timeout > 0) {
timeout_task = std::make_unique<async::TaskClosure>(
[&test_component, &program_name, &loop, &timed_out, &ret_code]() {
timed_out = true;
ret_code = -ZX_ERR_TIMED_OUT;
fprintf(stderr, "%s canceled due to timeout.\n", program_name.c_str());
timeout_task->PostDelayed(loop.dispatcher(), zx::sec(parse_result.timeout));
test_component->controller().events().OnTerminated =
[&ret_code, &program_name, &loop, &timed_out](int64_t return_code,
TerminationReason termination_reason) {
// component was killed due to timeout, don't collect results.
if (timed_out) {
if (termination_reason != TerminationReason::EXITED) {
fprintf(stderr, "%s: %s\n", program_name.c_str(),
ret_code = static_cast<int>(return_code);
// make sure timeout is not executed after test finishes.
if (timeout_task && timeout_task->is_pending()) {
// Wait and process all messages in the queue.
if (archivist_component) {
enclosing_env->Kill([&loop]() { loop.Quit(); });
// collect all logs
log_collector->NotifyOnUnBind([&loop]() { loop.Quit(); });
auto archivist_ptr =
// now that archivist is dead, make sure to collect its output
if (!restricted_logs.empty() && ret_code == 0) {
printf("\nTest %s produced unexpected high-severity logs:\n", program_name.c_str());
for (const auto& log : restricted_logs) {
"Failing this test. See: "
ret_code = 1;
return ret_code;