| // Copyright 2026 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 <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/component/incoming/cpp/protocol.h> |
| #include <lib/component/outgoing/cpp/outgoing_directory.h> |
| #include <lib/driver/debug/cpp/debug_server.h> |
| #include <lib/driver/outgoing/cpp/outgoing_directory.h> |
| |
| #include <gtest/gtest.h> |
| #include <src/lib/testing/loop_fixture/real_loop_fixture.h> |
| #include <src/lib/testing/predicates/status.h> |
| |
| namespace { |
| |
| class DebugServerTest : public gtest::RealLoopFixture { |
| protected: |
| void SetUp() override { |
| server_ = std::make_unique<fdf_debug::DebugServer>(); |
| |
| // 1. Register StringCommandHandler (sync) |
| server_->RegisterCommand( |
| "ping", "Ping the driver.", "[-c <count>]", |
| [](cpp20::span<const std::string> args) -> fit::result<std::string, std::string> { |
| int count = 1; |
| for (size_t i = 0; i < args.size(); ++i) { |
| if (args[i] == "-c" && i + 1 < args.size()) { |
| count = std::stoi(std::string(args[++i])); |
| } else { |
| return fit::error("invalid flag"); |
| } |
| } |
| std::string out; |
| for (int i = 1; i <= count; ++i) { |
| out += "ping " + std::to_string(i) + "\n"; |
| } |
| return fit::ok(out); |
| }); |
| |
| // 2. Register FullCommandHandler |
| server_->RegisterCommand("reset", "Reset the driver state.", "[--hard]", |
| [](cpp20::span<const std::string> args) -> fdf_debug::ExecutionResult { |
| bool hard = false; |
| for (size_t i = 0; i < args.size(); ++i) { |
| if (args[i] == "--hard") { |
| hard = true; |
| } else { |
| fdf_debug::ExecutionResult err; |
| err.exit_code() = 2; |
| err.stdout_() = ""; |
| err.stderr_() = |
| "Unrecognized option: " + std::string(args[i]) + "\n"; |
| return err; |
| } |
| } |
| fdf_debug::ExecutionResult res; |
| res.exit_code() = 0; |
| res.stdout_() = hard ? "hard reset\n" : "soft reset\n"; |
| res.stderr_() = ""; |
| return res; |
| }); |
| |
| // 3. Register AsyncCommandHandler |
| server_->RegisterCommand("async_cmd", "An asynchronous command.", |
| [](cpp20::span<const std::string> args, |
| fit::callback<void(fdf_debug::ExecutionResult)> completer) { |
| fdf_debug::ExecutionResult res; |
| res.exit_code() = 0; |
| res.stdout_() = "async success\n"; |
| res.stderr_() = ""; |
| completer(std::move(res)); |
| }); |
| |
| // 4. Register fault command that fails with handler error |
| server_->RegisterCommand( |
| "fault", "Induce driver fault.", |
| [](cpp20::span<const std::string> args) -> fit::result<std::string, std::string> { |
| return fit::error("driver internal fault\n"); |
| }); |
| |
| // 5. Register simple string command |
| server_->RegisterCommand( |
| "simple", "Simple string command.", |
| [](cpp20::span<const std::string> args) -> std::string { return "simple output\n"; }); |
| |
| // 6. Register status string command |
| server_->RegisterCommand("status_fail", "Fails with zx_status_t.", |
| [](cpp20::span<const std::string> args) -> zx::result<std::string> { |
| return zx::error(ZX_ERR_NOT_FOUND); |
| }); |
| } |
| |
| fidl::Client<fuchsia_driver_debug::Debug> CreateClient() { |
| auto endpoints = fidl::Endpoints<fuchsia_driver_debug::Debug>::Create(); |
| server_->Bind(dispatcher(), std::move(endpoints.server)); |
| return fidl::Client<fuchsia_driver_debug::Debug>(std::move(endpoints.client), dispatcher()); |
| } |
| |
| std::vector<fuchsia_driver_debug::CommandInfo> ListCommands( |
| fidl::Client<fuchsia_driver_debug::Debug>& client) { |
| std::optional<fidl::Result<fuchsia_driver_debug::Debug::ListCommands>> response; |
| client->ListCommands().ThenExactlyOnce( |
| [&](fidl::Result<fuchsia_driver_debug::Debug::ListCommands>& result) { |
| response = std::move(result); |
| }); |
| RunLoopUntil([&] { return response.has_value(); }); |
| EXPECT_TRUE(response->is_ok()); |
| if (!response->is_ok()) { |
| return {}; |
| } |
| return std::move((*response)->commands()); |
| } |
| |
| static std::string ReadSocket(const zx::socket& socket) { |
| std::string out; |
| char buf[512]; |
| while (true) { |
| size_t actual = 0; |
| zx_status_t status = socket.read(0, buf, sizeof(buf), &actual); |
| if (status == ZX_OK) { |
| out.append(buf, actual); |
| } else { |
| break; |
| } |
| } |
| return out; |
| } |
| |
| fdf_debug::ExecutionResult Execute(fidl::Client<fuchsia_driver_debug::Debug>& client, |
| std::vector<std::string> args) { |
| zx::socket stdout_client, stdout_server; |
| EXPECT_EQ(zx::socket::create(ZX_SOCKET_STREAM, &stdout_client, &stdout_server), ZX_OK); |
| zx::socket stderr_client, stderr_server; |
| EXPECT_EQ(zx::socket::create(ZX_SOCKET_STREAM, &stderr_client, &stderr_server), ZX_OK); |
| |
| std::optional<fidl::Result<fuchsia_driver_debug::Debug::Execute>> response; |
| fuchsia_driver_debug::DebugExecuteRequest request; |
| request.args() = std::move(args); |
| request.stdout_() = std::move(stdout_server); |
| request.stderr_() = std::move(stderr_server); |
| |
| client->Execute(std::move(request)) |
| .ThenExactlyOnce([&](fidl::Result<fuchsia_driver_debug::Debug::Execute>& result) { |
| response = std::move(result); |
| }); |
| RunLoopUntil([&] { return response.has_value(); }); |
| EXPECT_TRUE(response->is_ok()); |
| if (!response->is_ok()) { |
| return {}; |
| } |
| |
| fdf_debug::ExecutionResult result; |
| result.exit_code() = (*response)->exit_code(); |
| result.stdout_() = ReadSocket(stdout_client); |
| result.stderr_() = ReadSocket(stderr_client); |
| return result; |
| } |
| |
| std::unique_ptr<fdf_debug::DebugServer> server_; |
| }; |
| |
| TEST_F(DebugServerTest, ListCommands) { |
| auto client = CreateClient(); |
| auto commands = ListCommands(client); |
| ASSERT_EQ(commands.size(), 6u); |
| |
| EXPECT_EQ(commands[0].name().value(), "ping"); |
| EXPECT_EQ(commands[0].description().value(), "Ping the driver."); |
| |
| EXPECT_EQ(commands[1].name().value(), "reset"); |
| EXPECT_EQ(commands[1].description().value(), "Reset the driver state."); |
| |
| EXPECT_EQ(commands[2].name().value(), "async_cmd"); |
| EXPECT_EQ(commands[2].description().value(), "An asynchronous command."); |
| |
| EXPECT_EQ(commands[3].name().value(), "fault"); |
| EXPECT_EQ(commands[3].description().value(), "Induce driver fault."); |
| |
| EXPECT_EQ(commands[4].name().value(), "simple"); |
| EXPECT_EQ(commands[4].description().value(), "Simple string command."); |
| |
| EXPECT_EQ(commands[5].name().value(), "status_fail"); |
| EXPECT_EQ(commands[5].description().value(), "Fails with zx_status_t."); |
| } |
| |
| TEST_F(DebugServerTest, ExecuteStringCommand) { |
| auto client = CreateClient(); |
| |
| auto result = Execute(client, {"ping", "-c", "2"}); |
| EXPECT_EQ(result.exit_code(), 0); |
| EXPECT_EQ(result.stdout_(), "ping 1\nping 2\n"); |
| EXPECT_EQ(result.stderr_(), ""); |
| } |
| |
| TEST_F(DebugServerTest, ExecuteFullCommand) { |
| auto client = CreateClient(); |
| |
| auto result = Execute(client, {"reset", "--hard"}); |
| EXPECT_EQ(result.exit_code(), 0); |
| EXPECT_EQ(result.stdout_(), "hard reset\n"); |
| EXPECT_EQ(result.stderr_(), ""); |
| |
| auto soft_res = Execute(client, {"reset"}); |
| EXPECT_EQ(soft_res.exit_code(), 0); |
| EXPECT_EQ(soft_res.stdout_(), "soft reset\n"); |
| EXPECT_EQ(soft_res.stderr_(), ""); |
| } |
| |
| TEST_F(DebugServerTest, ExecuteAsyncCommand) { |
| auto client = CreateClient(); |
| |
| auto result = Execute(client, {"async_cmd"}); |
| EXPECT_EQ(result.exit_code(), 0); |
| EXPECT_EQ(result.stdout_(), "async success\n"); |
| EXPECT_EQ(result.stderr_(), ""); |
| } |
| |
| TEST_F(DebugServerTest, ExecuteSimpleStringCommand) { |
| auto client = CreateClient(); |
| |
| auto result = Execute(client, {"simple"}); |
| EXPECT_EQ(result.exit_code(), 0); |
| EXPECT_EQ(result.stdout_(), "simple output\n"); |
| EXPECT_EQ(result.stderr_(), ""); |
| } |
| |
| TEST_F(DebugServerTest, TopLevelHelp) { |
| auto client = CreateClient(); |
| |
| auto res1 = Execute(client, {"--help"}); |
| EXPECT_EQ(res1.exit_code(), 0); |
| EXPECT_NE(res1.stdout_().find("Usage: debug <command> [<args>]"), std::string::npos); |
| EXPECT_NE(res1.stdout_().find("ping"), std::string::npos); |
| EXPECT_NE(res1.stdout_().find("reset"), std::string::npos); |
| EXPECT_EQ(res1.stderr_(), ""); |
| |
| auto res2 = Execute(client, {}); |
| EXPECT_EQ(res2.exit_code(), 0); |
| EXPECT_NE(res2.stdout_().find("Usage: debug <command> [<args>]"), std::string::npos); |
| |
| auto res3 = Execute(client, {"-h"}); |
| EXPECT_EQ(res3.exit_code(), 0); |
| EXPECT_NE(res3.stdout_().find("Usage: debug <command> [<args>]"), std::string::npos); |
| } |
| |
| TEST_F(DebugServerTest, CommandHelp) { |
| auto client = CreateClient(); |
| |
| auto res1 = Execute(client, {"ping", "--help"}); |
| EXPECT_EQ(res1.exit_code(), 0); |
| EXPECT_NE(res1.stdout_().find("Usage: debug ping [-c <count>]"), std::string::npos); |
| EXPECT_NE(res1.stdout_().find("Ping the driver."), std::string::npos); |
| EXPECT_EQ(res1.stderr_(), ""); |
| |
| auto res2 = Execute(client, {"reset", "-h"}); |
| EXPECT_EQ(res2.exit_code(), 0); |
| EXPECT_NE(res2.stdout_().find("Usage: debug reset [--hard]"), std::string::npos); |
| EXPECT_NE(res2.stdout_().find("Reset the driver state."), std::string::npos); |
| EXPECT_EQ(res2.stderr_(), ""); |
| } |
| |
| TEST_F(DebugServerTest, UnknownCommandError) { |
| auto client = CreateClient(); |
| |
| auto result = Execute(client, {"unknown_cmd"}); |
| EXPECT_EQ(result.exit_code(), 2); |
| EXPECT_EQ(result.stdout_(), ""); |
| EXPECT_NE(result.stderr_().find("Unrecognized command: unknown_cmd"), std::string::npos); |
| } |
| |
| TEST_F(DebugServerTest, HandlerError) { |
| auto client = CreateClient(); |
| |
| auto result = Execute(client, {"fault"}); |
| EXPECT_EQ(result.exit_code(), 1); |
| EXPECT_EQ(result.stdout_(), ""); |
| EXPECT_NE(result.stderr_().find("driver internal fault"), std::string::npos); |
| |
| auto status_result = Execute(client, {"status_fail"}); |
| EXPECT_EQ(status_result.exit_code(), 1); |
| EXPECT_EQ(status_result.stdout_(), ""); |
| EXPECT_NE(status_result.stderr_().find("ZX_ERR_NOT_FOUND"), std::string::npos); |
| } |
| |
| TEST_F(DebugServerTest, MultipleClients) { |
| auto client1 = CreateClient(); |
| auto client2 = CreateClient(); |
| |
| auto res1 = Execute(client1, {"ping"}); |
| EXPECT_EQ(res1.exit_code(), 0); |
| EXPECT_EQ(res1.stdout_(), "ping 1\n"); |
| |
| auto res2 = Execute(client2, {"reset"}); |
| EXPECT_EQ(res2.exit_code(), 0); |
| EXPECT_EQ(res2.stdout_(), "soft reset\n"); |
| } |
| |
| TEST_F(DebugServerTest, ServeComponentOutgoingDirectory) { |
| component::OutgoingDirectory outgoing(dispatcher()); |
| ASSERT_OK(server_->Serve(dispatcher(), outgoing)); |
| |
| auto endpoints = fidl::Endpoints<fuchsia_io::Directory>::Create(); |
| ASSERT_OK(outgoing.Serve(std::move(endpoints.server))); |
| |
| auto client_end = component::ConnectAt<fuchsia_driver_debug::Debug>( |
| endpoints.client, "svc/fuchsia.driver.debug.Debug"); |
| ASSERT_OK(client_end); |
| |
| fidl::Client client(std::move(*client_end), dispatcher()); |
| auto commands = ListCommands(client); |
| EXPECT_EQ(commands.size(), 6u); |
| } |
| |
| } // namespace |