| // Copyright 2021 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 "suite.h" |
| |
| #include <fuchsia/debugdata/cpp/fidl.h> |
| #include <fuchsia/io/cpp/fidl.h> |
| #include <fuchsia/logger/cpp/fidl.h> |
| #include <fuchsia/sys/cpp/fidl.h> |
| #include <fuchsia/test/cpp/fidl.h> |
| #include <lib/async/cpp/executor.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/async/dispatcher.h> |
| #include <lib/fidl/cpp/interface_handle.h> |
| #include <lib/fidl/cpp/interface_ptr.h> |
| #include <lib/fidl/cpp/interface_request.h> |
| #include <lib/fpromise/barrier.h> |
| #include <lib/fpromise/bridge.h> |
| #include <lib/fpromise/promise.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/zx/socket.h> |
| #include <zircon/processargs.h> |
| #include <zircon/status.h> |
| |
| #include <map> |
| #include <memory> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include <src/lib/fxl/strings/string_printf.h> |
| #include <src/sys/run_test_component/component.h> |
| |
| static const char kTestCaseName[] = "legacy_test"; |
| constexpr char kEnvPrefix[] = "test_env_"; |
| |
| namespace { |
| |
| std::unique_ptr<run::OutputCollector> AddOutputFileDescriptor( |
| int fileno, zx::socket sock, async_dispatcher_t* dispatcher, |
| fuchsia::sys::FileDescriptorPtr* out_file_descriptor) { |
| auto output_collector = run::OutputCollector::Create(); |
| fuchsia::sys::FileDescriptorPtr file_descriptor = |
| std::make_unique<fuchsia::sys::FileDescriptor>(); |
| file_descriptor->type0 = PA_HND(PA_FD, fileno); |
| file_descriptor->handle0 = output_collector->TakeServer(); |
| *out_file_descriptor = std::move(file_descriptor); |
| output_collector->CollectOutput( |
| [sock = std::move(sock)](const std::string& s) { |
| auto status = sock.write(0 /* options */, s.c_str(), s.length(), nullptr); |
| if (status != ZX_OK) { |
| FX_LOGS(WARNING) << "Cannot write output to socket: " << zx_status_get_string(status) |
| << "\n Output:" << s; |
| } |
| }, |
| dispatcher); |
| |
| return output_collector; |
| } |
| |
| void send_failure(fidl::InterfacePtr<fuchsia::test::CaseListener>& case_listener) { |
| fuchsia::test::Result result; |
| result.set_status(fuchsia::test::Status::FAILED); |
| case_listener->Finished(std::move(result)); |
| } |
| |
| } // namespace |
| |
| Suite::Suite(std::shared_ptr<sys::ServiceDirectory> parent_env_svc, |
| fuchsia::sys::EnvironmentPtr parent_env, |
| std::shared_ptr<run::TestMetadata> test_metadata, |
| std::shared_ptr<sys::ServiceDirectory> test_component_svc, std::string legacy_url, |
| async_dispatcher_t* dispatcher) |
| : parent_env_(std::move(parent_env)), |
| parent_env_svc_(std::move(parent_env_svc)), |
| test_component_svc_(std::move(test_component_svc)), |
| test_metadata_(std::move(test_metadata)), |
| legacy_url_(std::move(legacy_url)), |
| test_components_(std::make_shared<ComponentMap>()), |
| dispatcher_(dispatcher), |
| executor_(dispatcher) {} |
| |
| Suite::~Suite() = default; |
| |
| Suite::CaseIterator::CaseIterator(fidl::InterfaceRequest<fuchsia::test::CaseIterator> request, |
| async_dispatcher_t* dispatcher, |
| fit::function<void(CaseIterator*)> done_callback) |
| : binding_(this, std::move(request), dispatcher), done_callback_(std::move(done_callback)) {} |
| |
| void Suite::CaseIterator::GetNext(GetNextCallback callback) { |
| if (get_next_call_count == 0) { |
| fuchsia::test::Case test_case; |
| test_case.set_name(std::string(kTestCaseName)); |
| test_case.set_enabled(true); |
| std::vector<fuchsia::test::Case> cases; |
| cases.push_back(std::move(test_case)); |
| callback(std::move(cases)); |
| get_next_call_count++; |
| |
| } else { |
| std::vector<fuchsia::test::Case> cases; |
| callback(std::move(cases)); |
| // this would be removed |
| done_callback_(this); |
| } |
| } |
| |
| void Suite::GetTests(fidl::InterfaceRequest<fuchsia::test::CaseIterator> iterator) { |
| auto case_iterator = std::make_unique<CaseIterator>( |
| std::move(iterator), dispatcher_, |
| [this](CaseIterator* case_iterator) { RemoveCaseInterator(case_iterator); }); |
| case_iterators_.emplace(case_iterator.get(), std::move(case_iterator)); |
| } |
| |
| std::unique_ptr<Suite::CaseIterator> Suite::RemoveCaseInterator(CaseIterator* case_iterator) { |
| auto it = case_iterators_.find(case_iterator); |
| std::unique_ptr<Suite::CaseIterator> case_iterator_ptr; |
| if (it != case_iterators_.end()) { |
| case_iterator_ptr = std::move(it->second); |
| case_iterators_.erase(it); |
| } |
| return case_iterator_ptr; |
| } |
| |
| void Suite::Run(std::vector<fuchsia::test::Invocation> tests, fuchsia::test::RunOptions options, |
| fidl::InterfaceHandle<fuchsia::test::RunListener> listener) { |
| auto listener_proxy = listener.Bind(); |
| std::vector<std::string> args; |
| if (options.has_arguments()) { |
| args = std::move(*options.mutable_arguments()); |
| } |
| |
| if (options.has_parallel()) { |
| FX_LOGS(WARNING) << "Ignoring 'parallel'. Pass test specific flags, eg: for rust test pass in " |
| "--test-threads=" |
| << options.parallel(); |
| } |
| |
| fpromise::barrier barrier; |
| for (auto it = tests.begin(); it != tests.end(); it++) { |
| auto invocation = std::move(*it); |
| std::string test_case_name; |
| if (invocation.has_name()) { |
| test_case_name = invocation.name(); |
| } |
| zx::socket out, err, out_client, err_client; |
| auto status = zx::socket::create(0, &out, &out_client); |
| if (status != ZX_OK) { |
| FX_LOGS(FATAL) << "cannot create socket: " << zx_status_get_string(status); |
| } |
| status = zx::socket::create(0, &err, &err_client); |
| if (status != ZX_OK) { |
| FX_LOGS(FATAL) << "cannot create socket: " << zx_status_get_string(status); |
| } |
| |
| fidl::InterfacePtr<fuchsia::test::CaseListener> case_listener; |
| |
| fuchsia::test::StdHandles std_handles; |
| std_handles.set_err(std::move(err_client)); |
| std_handles.set_out(std::move(out_client)); |
| |
| listener_proxy->OnTestCaseStarted(std::move(invocation), std::move(std_handles), |
| case_listener.NewRequest()); |
| if (test_case_name != kTestCaseName) { |
| std::string msg = fxl::StringPrintf("Invalid test case, expected: %s, got: %s\n", |
| kTestCaseName, test_case_name.c_str()); |
| err.write(0, msg.c_str(), msg.length(), nullptr); |
| fuchsia::test::Result result; |
| result.set_status(fuchsia::test::Status::FAILED); |
| case_listener->Finished(std::move(result)); |
| } else { |
| auto promise = RunTest(std::move(out), std::move(err), args, std::move(case_listener)) |
| .wrap_with(barrier); |
| executor_.schedule_task(std::move(promise)); |
| } |
| } |
| |
| auto sync_promise = fpromise::make_promise([listener_proxy = std::move(listener_proxy)] { |
| FX_LOGS(INFO) << "Sending OnFinished for legacy tests"; |
| listener_proxy->OnFinished(); |
| }); |
| executor_.schedule_task(barrier.sync().and_then(std::move(sync_promise))); |
| } |
| |
| fpromise::promise<> Suite::RunTest(zx::socket out, zx::socket err, |
| const std::vector<std::string>& arguments, |
| fidl::InterfacePtr<fuchsia::test::CaseListener> case_listener) { |
| sys::testing::EnvironmentServices::ParentOverrides parent_overrides; |
| parent_overrides.debug_data_publisher_service_ = |
| std::make_shared<vfs::Service>([namespace_services = test_component_svc_]( |
| 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)); |
| |
| const auto& services = test_metadata_->services(); |
| |
| // Add these services to the environment if they are not injected and test |
| // doesn't request sys version of them. |
| std::set<std::string> services_to_add = {fuchsia::logger::LogSink::Name_, |
| fuchsia::logger::Log::Name_, |
| fuchsia::diagnostics::ArchiveAccessor::Name_}; |
| |
| for (auto& service : services) { |
| fuchsia::sys::LaunchInfo info; |
| auto status = service.second.Clone(&info); |
| if (status != ZX_OK) { |
| FX_LOGS(WARNING) << "Cannot clone launch info: " << _zx_status_get_string(status); |
| send_failure(case_listener); |
| return fpromise::make_ok_promise(); |
| } |
| test_env_services->AddServiceWithLaunchInfo(std::move(info), service.first); |
| // remove from the set if found. |
| services_to_add.erase(service.first); |
| } |
| |
| auto& requested_system_services = test_metadata_->system_services(); |
| for (auto& service : requested_system_services) { |
| // remove from the set if found. |
| services_to_add.erase(service); |
| test_env_services->AllowParentService(service); |
| } |
| // 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)); |
| |
| for (const auto& service : services_to_add) { |
| test_env_services->AddService<void>( |
| [namespace_services = test_component_svc_, service](fidl::InterfaceRequest<void> request) { |
| namespace_services->Connect(service, request.TakeChannel()); |
| }, |
| service); |
| } |
| |
| fuchsia::sys::EnvironmentOptions env_opt; |
| std::string env_label = fxl::StringPrintf("%s%08x", kEnvPrefix, env_rand_suffix); |
| env_opt.delete_storage_on_death = true; |
| |
| auto enclosing_env = sys::testing::EnclosingEnvironment::Create( |
| env_label, parent_env_, std::move(test_env_services), env_opt); |
| |
| auto launcher = enclosing_env->launcher_ptr(); |
| |
| auto info = fuchsia::sys::LaunchInfo{.url = legacy_url_, .arguments = arguments}; |
| auto out_collector = |
| AddOutputFileDescriptor(STDOUT_FILENO, std::move(out), dispatcher_, &info.out); |
| auto err_collector = |
| AddOutputFileDescriptor(STDERR_FILENO, std::move(err), dispatcher_, &info.err); |
| |
| auto svc = sys::ServiceDirectory::CreateWithRequest(&info.directory_request); |
| fuchsia::sys::ComponentControllerPtr contoller; |
| launcher->CreateComponent(std::move(info), contoller.NewRequest()); |
| auto test_component = std::make_unique<run::Component>( |
| std::move(out_collector), std::move(err_collector), std::move(contoller), std::move(svc)); |
| fpromise::bridge<> bridge; |
| test_component->controller().events().OnTerminated = |
| [this, url = legacy_url_, enclosing_env = std::move(enclosing_env), |
| completer = std::move(bridge.completer), case_listener = std::move(case_listener), |
| this_ptr = test_component.get()]( |
| int64_t return_code, fuchsia::sys::TerminationReason termination_reason) mutable { |
| if (termination_reason != fuchsia::sys::TerminationReason::EXITED) { |
| FX_LOGS(WARNING) << "Test " << url << " failed with " |
| << sys::HumanReadableTerminationReason(termination_reason); |
| } |
| |
| FX_LOGS(INFO) << "Legacy test exited with return code " << return_code |
| << ", collecting stdout"; |
| |
| auto status = |
| return_code == 0 ? fuchsia::test::Status::PASSED : fuchsia::test::Status::FAILED; |
| |
| auto output_promise = this_ptr->SignalWhenOutputCollected(); |
| |
| FX_LOGS(INFO) << "Killing environment for legacy test"; |
| fpromise::bridge<> bridge; |
| enclosing_env->Kill(bridge.completer.bind()); |
| auto promise = bridge.consumer.promise().and_then( |
| [enclosing_env = std::move(enclosing_env), case_listener = std::move(case_listener), |
| status = status, completer = std::move(completer)]() mutable { |
| fuchsia::test::Result result; |
| result.set_status(status); |
| FX_LOGS(INFO) << "Sending finished event for legacy tests"; |
| case_listener->Finished(std::move(result)); |
| completer.complete_ok(); |
| }); |
| this->executor_.schedule_task(output_promise.and_then([this, this_ptr]() { |
| FX_LOGS(INFO) << "Done collecting standard output for legacy tests"; |
| RemoveComponent(this_ptr); |
| })); |
| this->executor_.schedule_task(std::move(promise)); |
| }; |
| |
| test_components_->emplace(test_component.get(), std::move(test_component)); |
| |
| return bridge.consumer.promise(); |
| } |
| |
| std::unique_ptr<run::Component> Suite::RemoveComponent(run::Component* ptr) { |
| if (!test_components_) { |
| return nullptr; |
| } |
| auto it = test_components_->find(ptr); |
| std::unique_ptr<run::Component> test_component_ptr; |
| if (it != test_components_->end()) { |
| test_component_ptr = std::move(it->second); |
| test_components_->erase(it); |
| } |
| return test_component_ptr; |
| } |