| // Copyright 2019 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 "src/media/audio/lib/test/hermetic_audio_environment.h" |
| |
| #include <fuchsia/inspect/cpp/fidl.h> |
| #include <fuchsia/media/audio/cpp/fidl.h> |
| #include <fuchsia/media/cpp/fidl.h> |
| #include <fuchsia/media/tuning/cpp/fidl.h> |
| #include <fuchsia/scheduler/cpp/fidl.h> |
| #include <fuchsia/thermal/cpp/fidl.h> |
| #include <fuchsia/ultrasound/cpp/fidl.h> |
| #include <fuchsia/virtualaudio/cpp/fidl.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/inspect/service/cpp/reader.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <zircon/device/vfs.h> |
| #include <zircon/status.h> |
| |
| #include <test/thermal/cpp/fidl.h> |
| |
| #include "src/lib/files/directory.h" |
| #include "src/lib/files/glob.h" |
| #include "src/lib/fxl/strings/substitute.h" |
| |
| namespace media::audio::test { |
| namespace { |
| |
| // The label used for our hermetic audio environment. |
| constexpr const char kIsolatedEnvironmentLabel[] = "hermetic_audio_test"; |
| |
| // The IsolatedDevmgr will expose a fuchsia.io.Directory protocol under this service name in the |
| // devmgrs public directory. |
| constexpr const char kIsolatedDevmgrServiceName[] = "fuchsia.media.AudioTestDevmgr"; |
| |
| std::function<fuchsia::sys::LaunchInfo()> LaunchInfoWithIsolatedDevmgrForUrl( |
| std::string url, std::shared_ptr<sys::ServiceDirectory> services, |
| std::string config_data_path = "", |
| std::vector<std::string> arguments = std::vector<std::string>()) { |
| return [url, services = std::move(services), config_data_path, arguments] { |
| zx::channel devfs = services->Connect<fuchsia::io::Directory>(kIsolatedDevmgrServiceName) |
| .Unbind() |
| .TakeChannel(); |
| fuchsia::sys::LaunchInfo launch_info; |
| launch_info.url = url; |
| launch_info.flat_namespace = fuchsia::sys::FlatNamespace::New(); |
| launch_info.flat_namespace->paths.push_back("/dev"); |
| launch_info.flat_namespace->directories.push_back(std::move(devfs)); |
| |
| if (!arguments.empty()) { |
| launch_info.arguments = arguments; |
| } |
| |
| zx::channel config_data; |
| if (config_data_path != "") { |
| FX_LOGS(INFO) << "Using path '" << config_data_path << "' for /config/data directory for " |
| << url << "."; |
| zx::channel remote; |
| zx::channel::create(0, &config_data, &remote); |
| zx_status_t status = fdio_open( |
| config_data_path.c_str(), |
| fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_FLAG_DIRECTORY, remote.release()); |
| if (status == ZX_OK) { |
| launch_info.flat_namespace->paths.push_back("/config/data"); |
| launch_info.flat_namespace->directories.push_back(std::move(config_data)); |
| } else { |
| FX_PLOGS(ERROR, status) << "Unable to open '" << config_data_path << "."; |
| } |
| } else { |
| FX_LOGS(INFO) << "No config_data provided for " << url; |
| } |
| |
| return launch_info; |
| }; |
| } |
| |
| std::string ComponentManifestFromURL(std::string component_url) { |
| const char* kMeta = "#meta/"; |
| auto k = component_url.find(kMeta); |
| FX_CHECK(k != std::string::npos); |
| return component_url.substr(k + strlen(kMeta)); |
| } |
| |
| } // namespace |
| |
| // Runs a thread with a dedicated loop for managing the enclosing environment. We use a thread here |
| // for a few reasons. First, and most importantly, we want to share a hermetic audio_core instance |
| // across all the tests in a test suite. To do this, we need to provide the EnclosingEnvironment |
| // with an async loop that is not scoped to the lifetime of a single test (as is done when using |
| // gtest::RealLoopFixture). |
| // |
| // Secondly, if we reuse the primary test loop we can under some circumstances run into deadlock |
| // when, for example, using a sync pointer since that will block the async loop before the backing |
| // service has a chance to be created. |
| void HermeticAudioEnvironment::EnvironmentMain(HermeticAudioEnvironment* env) { |
| async::Loop loop{&kAsyncLoopConfigAttachToCurrentThread}; |
| env->StartEnvThread(&loop); |
| loop.Run(); |
| } |
| |
| HermeticAudioEnvironment::HermeticAudioEnvironment(Options options) : options_(options) { |
| // Create the thread here to ensure the rest of the class has fully initialized before starting |
| // the new thread, which takes a reference to |this|. |
| env_thread_ = std::thread(EnvironmentMain, this); |
| |
| // Wait for the worker thread to start and finish some initialization. |
| std::unique_lock<std::mutex> lock(mutex_); |
| while (!hermetic_environment_ || !hermetic_environment_->is_running()) { |
| cv_.wait(lock); |
| } |
| |
| // IsolatedDevmgr will not serve any messages on the directory until /dev/test/virtual_audio |
| // is ready. Run a simple Describe operation to ensure the devmgr is ready for traffic. |
| // |
| // Note we specifically use the |TextFixture| overrides of the virtual methods. This is needed |
| // because some test fixtures override these methods and include some asserts that will not |
| // be valid when this is run. |
| fuchsia::io::DirectorySyncPtr devfs_dir; |
| devmgr_services_->Connect(devfs_dir.NewRequest(), kIsolatedDevmgrServiceName); |
| fuchsia::io::NodeInfo info; |
| zx_status_t status = devfs_dir->Describe(&info); |
| FX_CHECK(status == ZX_OK) << status; |
| } |
| |
| void HermeticAudioEnvironment::StartEnvThread(async::Loop* loop) { |
| FX_CHECK(loop_ == nullptr); |
| loop_ = loop; |
| auto real_services = sys::ServiceDirectory::CreateFromNamespace(); |
| auto real_env = real_services->Connect<fuchsia::sys::Environment>(); |
| |
| fuchsia::sys::LaunchInfo devmgr_launch_info; |
| // This URL should be made more flexible for future tests. |
| devmgr_launch_info.url = options_.devmgr_url; |
| devmgr_services_ = |
| sys::ServiceDirectory::CreateWithRequest(&devmgr_launch_info.directory_request); |
| |
| // Launch AudioTestDevmgr per-environment. |
| fuchsia::sys::LauncherPtr launcher; |
| real_env->GetLauncher(launcher.NewRequest()); |
| launcher->CreateComponent(std::move(devmgr_launch_info), controller_.NewRequest()); |
| |
| // The '_nodevfs' cmx files are needed to allow us to map in our isolated devmgr under /dev for |
| // each component, otherwise these components would still be provided the shared/global devmgr. |
| std::string audio_core_url = options_.audio_core_base_url; |
| if (options_.audio_core_config_data_path != "") { |
| // When a custom config is specified, don't bother loading the default config data. |
| audio_core_url += "#meta/audio_core_nodevfs_noconfigdata.cmx"; |
| } else { |
| audio_core_url += "#meta/audio_core_nodevfs.cmx"; |
| } |
| // clang-format off |
| std::string virtual_audio_url = |
| "fuchsia-pkg://fuchsia.com/virtual-audio-service-for-test#meta/virtual_audio_service_nodevfs.cmx"; |
| // clang-format on |
| |
| std::string thermal_test_control_url = |
| "fuchsia-pkg://fuchsia.com/thermal-test-control#meta/thermal_test_control.cmx"; |
| |
| // Add in the services that will be available in our hermetic environment. |
| struct ComponentLaunchInfo { |
| ComponentType type; |
| std::string url; |
| std::function<fuchsia::sys::LaunchInfo()> launch_info; |
| std::vector<const char*> service_names; |
| }; |
| std::vector<ComponentLaunchInfo> to_launch{ |
| { |
| .type = kAudioCoreComponent, |
| .url = audio_core_url, |
| .launch_info = LaunchInfoWithIsolatedDevmgrForUrl(audio_core_url, devmgr_services_, |
| options_.audio_core_config_data_path, |
| options_.audio_core_arguments), |
| .service_names = |
| { |
| fuchsia::media::ActivityReporter::Name_, |
| fuchsia::media::Audio::Name_, |
| fuchsia::media::AudioCore::Name_, |
| fuchsia::media::AudioDeviceEnumerator::Name_, |
| fuchsia::media::tuning::AudioTuner::Name_, |
| fuchsia::media::UsageGainReporter::Name_, |
| fuchsia::media::UsageReporter::Name_, |
| fuchsia::media::audio::EffectsController::Name_, |
| fuchsia::ultrasound::Factory::Name_, |
| }, |
| }, |
| { |
| .type = kVirtualAudioComponent, |
| .url = virtual_audio_url, |
| .launch_info = LaunchInfoWithIsolatedDevmgrForUrl(virtual_audio_url, devmgr_services_), |
| .service_names = |
| { |
| fuchsia::virtualaudio::Control::Name_, |
| fuchsia::virtualaudio::Input::Name_, |
| fuchsia::virtualaudio::Output::Name_, |
| }, |
| }, |
| { |
| .type = kThermalTestControlComponent, |
| .url = thermal_test_control_url, |
| .launch_info = |
| LaunchInfoWithIsolatedDevmgrForUrl(thermal_test_control_url, devmgr_services_), |
| .service_names = |
| { |
| fuchsia::thermal::Controller::Name_, |
| ::test::thermal::Control::Name_, |
| }, |
| }, |
| }; |
| |
| auto services = sys::testing::EnvironmentServices::Create(real_env); |
| for (auto& c : to_launch) { |
| component_urls_[c.type] = c.url; |
| for (auto n : c.service_names) { |
| services->AddServiceWithLaunchInfo(c.url, c.launch_info, n); |
| } |
| } |
| services->AllowParentService("fuchsia.logger.LogSink"); |
| services->AllowParentService("fuchsia.tracing.provider.Registry"); |
| services->AllowParentService(fuchsia::scheduler::ProfileProvider::Name_); |
| for (const auto& service : options_.extra_allowed_parent_services) { |
| services->AllowParentService(service); |
| } |
| |
| std::unique_lock<std::mutex> lock(mutex_); |
| hermetic_environment_ = sys::testing::EnclosingEnvironment::Create( |
| kIsolatedEnvironmentLabel, real_env, std::move(services), {}); |
| hermetic_environment_->SetRunningChangedCallback([this](bool running) { |
| std::unique_lock<std::mutex> lock(mutex_); |
| if (running) { |
| cv_.notify_all(); |
| } |
| }); |
| } |
| |
| HermeticAudioEnvironment::~HermeticAudioEnvironment() { |
| FX_CHECK(loop_); |
| async::PostTask(loop_->dispatcher(), [loop = loop_] { loop->Quit(); }); |
| env_thread_.join(); |
| } |
| |
| const inspect::Hierarchy HermeticAudioEnvironment::ReadInspect(ComponentType component_type) { |
| auto it = component_urls_.find(component_type); |
| FX_CHECK(it != component_urls_.end()) << "unknown component " << component_type; |
| |
| files::Glob glob(fxl::Substitute("/hub/r/$0/*/c/$1/*/out/diagnostics/fuchsia.inspect.Tree", |
| kIsolatedEnvironmentLabel, |
| ComponentManifestFromURL(it->second))); |
| FX_CHECK(glob.size() == 1) << "could not find unique fuchsia.inspect.Tree, found " << glob.size() |
| << " matches"; |
| |
| auto path = std::string(*glob.begin()); |
| fuchsia::inspect::TreeSyncPtr tree; |
| auto status = fdio_service_connect(path.c_str(), tree.NewRequest().TakeChannel().release()); |
| FX_CHECK(status == ZX_OK) << "could not connect to fuchsia.inspect.Tree: " << status; |
| |
| fuchsia::inspect::TreeContent c; |
| status = tree->GetContent(&c); |
| FX_CHECK(status == ZX_OK) << "could not get VMO from fuchsia.inspect.Tree" << status; |
| FX_CHECK(c.has_buffer()); |
| |
| return inspect::ReadFromVmo(c.buffer().vmo).take_value(); |
| } |
| |
| } // namespace media::audio::test |