| // 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 <fidl/fuchsia.gpu.magma/cpp/test_base.h> |
| #include <fidl/fuchsia.hardware.goldfish/cpp/test_base.h> |
| #include <fidl/fuchsia.io/cpp/wire.h> |
| #include <fidl/fuchsia.memorypressure/cpp/test_base.h> |
| #include <fidl/fuchsia.vulkan.loader/cpp/test_base.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/namespace.h> |
| #include <lib/zx/vmo.h> |
| |
| #include <fbl/unique_fd.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/graphics/bin/vulkan_loader/app.h" |
| #include "src/graphics/bin/vulkan_loader/goldfish_device.h" |
| #include "src/graphics/bin/vulkan_loader/icd_component.h" |
| #include "src/graphics/bin/vulkan_loader/magma_dependency_injection.h" |
| #include "src/graphics/bin/vulkan_loader/magma_device.h" |
| #include "src/graphics/bin/vulkan_loader/structured_config_lib.h" |
| #include "src/lib/json_parser/json_parser.h" |
| #include "src/storage/lib/vfs/cpp/pseudo_dir.h" |
| #include "src/storage/lib/vfs/cpp/service.h" |
| #include "src/storage/lib/vfs/cpp/synchronous_vfs.h" |
| #include "src/storage/lib/vfs/cpp/vfs_types.h" |
| |
| class LoaderUnittest : public ::testing::Test { |
| using Config = structured_config_lib::Config; |
| |
| protected: |
| const inspect::Inspector& inspector() const { return inspector_; } |
| |
| Config& config() { |
| ZX_ASSERT_MSG(!app_, "Can only modify config before app() is instantiated."); |
| return config_; |
| } |
| |
| LoaderApp* app() { |
| if (!app_) { |
| app_ = std::make_unique<LoaderApp>(&outgoing_dir_, dispatcher(), config_); |
| } |
| return app_.get(); |
| } |
| |
| async_dispatcher_t* dispatcher() const { return loop_.dispatcher(); } |
| |
| void RunLoopUntil(fit::function<bool()> condition) { |
| while (!condition() && loop_.Run(zx::time::infinite(), true) == ZX_OK) { |
| } |
| } |
| |
| void TearDown() override { |
| // We have to shutdown the loop before destroying app_ as some tasks may hold deferred actions |
| // that reference the LoaderApp. |
| loop_.Shutdown(); |
| } |
| |
| private: |
| static Config GetDefaultConfig() { |
| Config config; |
| config.allow_goldfish_icd() = true; |
| config.allow_lavapipe_icd() = true; |
| config.allow_magma_icds() = true; |
| return config; |
| } |
| |
| async::Loop loop_ = async::Loop(&kAsyncLoopConfigAttachToCurrentThread); |
| Config config_ = GetDefaultConfig(); |
| inspect::Inspector inspector_; |
| component::OutgoingDirectory outgoing_dir_ = component::OutgoingDirectory(dispatcher()); |
| std::unique_ptr<LoaderApp> app_; |
| }; |
| |
| class FakeMagmaDevice : public fidl::testing::TestBase<fuchsia_gpu_magma::CombinedDevice> { |
| public: |
| explicit FakeMagmaDevice(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {} |
| |
| void CloseAll() { bindings_.CloseAll(ZX_OK); } |
| |
| auto ProtocolConnector() { |
| return [this](fidl::ServerEnd<fuchsia_gpu_magma::CombinedDevice> server_end) -> zx_status_t { |
| bindings_.AddBinding(dispatcher_, std::move(server_end), this, fidl::kIgnoreBindingClosure); |
| return ZX_OK; |
| }; |
| } |
| |
| private: |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "unexpected call to " << name; |
| } |
| |
| void GetIcdList(GetIcdListCompleter::Sync& completer) override { |
| fuchsia_gpu_magma::IcdInfo info; |
| info.component_url() = "a"; |
| info.flags() = fuchsia_gpu_magma::IcdFlags::kSupportsVulkan; |
| std::vector<fuchsia_gpu_magma::IcdInfo> vec; |
| vec.push_back(std::move(info)); |
| info.component_url() = "b"; |
| info.flags() = fuchsia_gpu_magma::IcdFlags::kSupportsOpencl; |
| vec.push_back(std::move(info)); |
| completer.Reply(vec); |
| } |
| |
| async_dispatcher_t* dispatcher_; |
| fidl::ServerBindingGroup<fuchsia_gpu_magma::CombinedDevice> bindings_; |
| }; |
| |
| TEST_F(LoaderUnittest, MagmaDevice) { |
| async::Loop vfs_loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| fs::SynchronousVfs vfs(vfs_loop.dispatcher()); |
| FakeMagmaDevice magma_device(vfs_loop.dispatcher()); |
| auto root = fbl::MakeRefCounted<fs::PseudoDir>(); |
| const char* kDeviceNodeName = "dev"; |
| ASSERT_EQ(root->AddEntry(kDeviceNodeName, |
| fbl::MakeRefCounted<fs::Service>(magma_device.ProtocolConnector())), |
| ZX_OK); |
| vfs_loop.StartThread("vfs-loop"); |
| zx::result endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>(); |
| ASSERT_TRUE(endpoints.is_ok()) << endpoints.status_string(); |
| auto [client, server] = std::move(*endpoints); |
| ASSERT_EQ(vfs.ServeDirectory(root, std::move(server), fs::Rights::ReadOnly()), ZX_OK); |
| |
| zx::result device = MagmaDevice::Create(app(), client, kDeviceNodeName, &inspector().GetRoot()); |
| ASSERT_TRUE(device.is_ok()) << device.status_string(); |
| auto* device_ptr = (*device).get(); |
| |
| app()->AddDevice(std::move(*device)); |
| RunLoopUntil([device_ptr]() { return device_ptr->icd_count() > 0; }); |
| ASSERT_EQ(1u, app()->device_count()); |
| |
| // Only 1 ICD listed supports Vulkan. |
| const IcdList& icd_list = app()->devices()[0]->icd_list(); |
| EXPECT_EQ(1u, icd_list.ComponentCount()); |
| |
| async::PostTask(vfs_loop.dispatcher(), [&magma_device]() { magma_device.CloseAll(); }); |
| RunLoopUntil([this]() { return app()->device_count() == 0; }); |
| EXPECT_EQ(0u, app()->device_count()); |
| vfs_loop.Shutdown(); |
| } |
| |
| class FakeGoldfishDevice : public fidl::testing::TestBase<fuchsia_hardware_goldfish::PipeDevice> { |
| private: |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "unexpected call to " << name; |
| } |
| }; |
| |
| class FakeGoldfishController |
| : public fidl::testing::TestBase<fuchsia_hardware_goldfish::Controller> { |
| public: |
| explicit FakeGoldfishController(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {} |
| |
| auto ProtocolConnector() { |
| return |
| [this](fidl::ServerEnd<fuchsia_hardware_goldfish::Controller> server_end) -> zx_status_t { |
| controller_bindings_.AddBinding(dispatcher_, std::move(server_end), this, |
| fidl::kIgnoreBindingClosure); |
| return ZX_OK; |
| }; |
| } |
| |
| void CloseAll() { |
| controller_bindings_.CloseAll(ZX_OK); |
| bindings_.CloseAll(ZX_OK); |
| } |
| size_t PipeDeviceBindingsSize() { return bindings_.size(); } |
| size_t ControllerBindingsSize() { return controller_bindings_.size(); } |
| |
| private: |
| void OpenSession(OpenSessionRequest& request, OpenSessionCompleter::Sync& completer) override { |
| bindings_.AddBinding(dispatcher_, std::move(request.session()), &device_, |
| fidl::kIgnoreBindingClosure); |
| } |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "unexpected call to " << name; |
| } |
| |
| async_dispatcher_t* dispatcher_; |
| fidl::ServerBindingGroup<fuchsia_hardware_goldfish::Controller> controller_bindings_; |
| fidl::ServerBindingGroup<fuchsia_hardware_goldfish::PipeDevice> bindings_; |
| FakeGoldfishDevice device_; |
| }; |
| |
| TEST_F(LoaderUnittest, GoldfishDevice) { |
| async::Loop vfs_loop(&kAsyncLoopConfigNoAttachToCurrentThread); |
| fs::SynchronousVfs vfs(vfs_loop.dispatcher()); |
| auto root = fbl::MakeRefCounted<fs::PseudoDir>(); |
| FakeGoldfishController goldfish_device(vfs_loop.dispatcher()); |
| const char* kDeviceNodeName = "dev"; |
| ASSERT_EQ(root->AddEntry(kDeviceNodeName, |
| fbl::MakeRefCounted<fs::Service>(goldfish_device.ProtocolConnector())), |
| ZX_OK); |
| vfs_loop.StartThread("vfs-loop"); |
| zx::result endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>(); |
| ASSERT_TRUE(endpoints.is_ok()) << endpoints.status_string(); |
| auto& [client, server] = endpoints.value(); |
| vfs.ServeDirectory(root, std::move(server), fs::Rights::ReadOnly()); |
| |
| auto device = GoldfishDevice::Create(app(), client, kDeviceNodeName, &inspector().GetRoot()); |
| ASSERT_TRUE(device); |
| auto device_ptr = device.get(); |
| |
| app()->AddDevice(std::move(device)); |
| RunLoopUntil([&device_ptr]() { return device_ptr->icd_count() > 0; }); |
| EXPECT_EQ(1u, app()->device_count()); |
| |
| async::PostTask(vfs_loop.dispatcher(), [&]() { |
| // The request to connect to the goldfish device may still be pending. |
| // Remove the "dev" entry to ensure that pending requests are canceled and |
| // aren't passed on the FakeGoldfishDevice. |
| EXPECT_EQ(root->RemoveEntry(kDeviceNodeName), ZX_OK); |
| goldfish_device.CloseAll(); |
| }); |
| // Wait until the loader detects that the goldfish device has gone away. |
| RunLoopUntil([this]() { return app()->device_count() == 0; }); |
| EXPECT_EQ(0u, app()->device_count()); |
| vfs_loop.Shutdown(); |
| EXPECT_EQ(0u, goldfish_device.PipeDeviceBindingsSize()); |
| EXPECT_EQ(0u, goldfish_device.ControllerBindingsSize()); |
| } |
| |
| TEST_F(LoaderUnittest, LavapipeDeviceAllowed) { |
| config().allow_goldfish_icd() = false; |
| config().allow_lavapipe_icd() = true; |
| config().allow_magma_icds() = false; |
| zx_status_t status = app()->InitDeviceWatcher(); |
| ASSERT_EQ(status, ZX_OK) << zx_status_get_string(status); |
| EXPECT_EQ(1U, app()->device_count()); |
| } |
| |
| TEST_F(LoaderUnittest, LavapipeDeviceDisallowed) { |
| config().allow_goldfish_icd() = false; |
| config().allow_lavapipe_icd() = false; |
| config().allow_magma_icds() = false; |
| zx_status_t status = app()->InitDeviceWatcher(); |
| ASSERT_EQ(status, ZX_OK) << zx_status_get_string(status); |
| EXPECT_EQ(0U, app()->device_count()); |
| } |
| |
| TEST(Icd, BadMetadata) { |
| json::JSONParser parser; |
| auto good_doc = parser.ParseFromString(R"({ |
| "file_path": "bin/pkg-server", |
| "version": 1, |
| "manifest_path": "data" |
| })", |
| "test1"); |
| EXPECT_TRUE(IcdComponent::ValidateMetadataJson("a", good_doc)); |
| |
| auto bad_doc1 = parser.ParseFromString(R"({ |
| "file_path": "bin/pkg-server", |
| "version": 2, |
| "manifest_path": "data" |
| })", |
| "tests2"); |
| EXPECT_FALSE(IcdComponent::ValidateMetadataJson("b", bad_doc1)); |
| |
| auto bad_doc2 = parser.ParseFromString(R"({ |
| "version": 1, |
| "manifest_path": "data" |
| })", |
| "test3"); |
| EXPECT_FALSE(IcdComponent::ValidateMetadataJson("c", bad_doc2)); |
| |
| auto bad_doc3 = parser.ParseFromString(R"({ |
| "file_path": 1, |
| "version": 1, |
| "manifest_path": "data" |
| })", |
| "tests4"); |
| EXPECT_FALSE(IcdComponent::ValidateMetadataJson("d", bad_doc3)); |
| } |
| |
| TEST(Icd, BadManifest) { |
| json::JSONParser parser; |
| auto good_doc = parser.ParseFromString(R"( |
| { |
| "ICD": { |
| "api_version": "1.1.0", |
| "library_path": "libvulkan_fake.so" |
| }, |
| "file_format_version": "1.0.0" |
| })", |
| "test1"); |
| EXPECT_TRUE(IcdComponent::ValidateManifestJson("a", good_doc)); |
| |
| auto bad_doc1 = parser.ParseFromString(R"( |
| { |
| "ICD": { |
| "api_version": "1.1.0", |
| }, |
| "file_format_version": "1.0.0" |
| })", |
| "test1"); |
| EXPECT_FALSE(IcdComponent::ValidateManifestJson("a", bad_doc1)); |
| } |
| |
| class FakeMemoryPressureProvider |
| : public fidl::testing::TestBase<fuchsia_memorypressure::Provider> { |
| public: |
| zx::result<fidl::ClientEnd<fuchsia_memorypressure::Provider>> Bind( |
| async_dispatcher_t* dispatcher) { |
| if (binding_) { |
| return zx::error(ZX_ERR_ALREADY_BOUND); |
| } |
| zx::result endpoints = fidl::CreateEndpoints<fuchsia_memorypressure::Provider>(); |
| if (endpoints.is_error()) { |
| return endpoints.take_error(); |
| } |
| binding_ = fidl::BindServer(dispatcher, std::move(endpoints->server), this); |
| return zx::ok(std::move(endpoints->client)); |
| } |
| |
| void Close() { binding_->Close(ZX_OK); } |
| |
| private: |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "unexpected call to " << name; |
| } |
| |
| void RegisterWatcher(RegisterWatcherRequest& request, |
| RegisterWatcherCompleter::Sync& completer) override { |
| auto result = |
| fidl::WireCall(request.watcher())->OnLevelChanged(fuchsia_memorypressure::Level::kCritical); |
| if (!result.ok()) { |
| GTEST_FAIL() << "Failed to set memory pressure level: " << result; |
| } |
| } |
| |
| std::optional<fidl::ServerBindingRef<fuchsia_memorypressure::Provider>> binding_; |
| }; |
| |
| class FakeMagmaDependencyInjection |
| : public fidl::testing::TestBase<fuchsia_gpu_magma::DependencyInjection> { |
| public: |
| explicit FakeMagmaDependencyInjection(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {} |
| |
| auto ProtocolConnector() { |
| return [this]( |
| fidl::ServerEnd<fuchsia_gpu_magma::DependencyInjection> server_end) -> zx_status_t { |
| bindings_.AddBinding(dispatcher_, std::move(server_end), this, fidl::kIgnoreBindingClosure); |
| return ZX_OK; |
| }; |
| } |
| |
| bool GotMemoryPressureProvider() const { return got_memory_pressure_provider_; } |
| |
| void CloseAll() { bindings_.CloseAll(ZX_OK); } |
| |
| private: |
| void NotImplemented_(const std::string& name, ::fidl::CompleterBase& completer) override { |
| ADD_FAILURE() << "unexpected call to " << name; |
| } |
| |
| void SetMemoryPressureProvider(SetMemoryPressureProviderRequest& request, |
| SetMemoryPressureProviderCompleter::Sync& completer) override { |
| if (!request.provider().is_valid()) { |
| GTEST_FAIL() << "Got invalid handle to fuchsia.memorypressure/Provider protocol."; |
| } |
| got_memory_pressure_provider_ = true; |
| } |
| |
| async_dispatcher_t* dispatcher_; |
| fidl::ServerBindingGroup<fuchsia_gpu_magma::DependencyInjection> bindings_; |
| bool got_memory_pressure_provider_ = false; |
| }; |
| |
| TEST_F(LoaderUnittest, MagmaDependencyInjection) { |
| FakeMemoryPressureProvider provider; |
| |
| fs::SynchronousVfs vfs(dispatcher()); |
| auto root = fbl::MakeRefCounted<fs::PseudoDir>(); |
| |
| FakeMagmaDependencyInjection magma_dependency_injection(dispatcher()); |
| ASSERT_EQ(root->AddEntry("000", fbl::MakeRefCounted<fs::Service>( |
| magma_dependency_injection.ProtocolConnector())), |
| ZX_OK); |
| auto gpu_dir = fidl::CreateEndpoints<fuchsia_io::Directory>(); |
| ASSERT_EQ(vfs.ServeDirectory(root, std::move(gpu_dir->server), fs::Rights::ReadOnly()), ZX_OK); |
| |
| fdio_ns_t* ns; |
| EXPECT_EQ(ZX_OK, fdio_ns_get_installed(&ns)); |
| const char* kDependencyInjectionPath = "/dev/class/gpu-dependency-injection"; |
| EXPECT_EQ(ZX_OK, |
| fdio_ns_bind(ns, kDependencyInjectionPath, gpu_dir->client.TakeChannel().release())); |
| auto defer_unbind = fit::defer([&]() { fdio_ns_unbind(ns, kDependencyInjectionPath); }); |
| |
| zx::result provider_client = provider.Bind(dispatcher()); |
| ASSERT_TRUE(provider_client.is_ok()) << provider_client.status_string(); |
| zx::result dependency_injection = MagmaDependencyInjection::Create(std::move(*provider_client)); |
| ASSERT_TRUE(dependency_injection.is_ok()) << dependency_injection.status_string(); |
| |
| // Wait for the GPU dependency injection code to detect the device and call the method on it. |
| RunLoopUntil([&magma_dependency_injection]() { |
| return magma_dependency_injection.GotMemoryPressureProvider(); |
| }); |
| } |