blob: 61b8bdc31b3360da79dc569b2e12264098787c8f [file] [log] [blame]
// Copyright 2025 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.component.decl/cpp/fidl.h>
#include <fidl/fuchsia.component/cpp/fidl.h>
#include <fidl/fuchsia.element/cpp/fidl.h>
#include <fidl/fuchsia.kernel/cpp/fidl.h>
#include <fidl/fuchsia.process/cpp/fidl.h>
#include <fidl/fuchsia.tracing.provider/cpp/fidl.h>
#include <fidl/fuchsia.ui.display.singleton/cpp/fidl.h>
#include <lib/sys/component/cpp/testing/realm_builder.h>
#include <lib/zx/socket.h>
#include <lib/zx/time.h>
#include <zircon/processargs.h>
#include "relay-api.h"
#include "src/ui/testing/util/portable_ui_test.h"
namespace starnix_input_test {
// Types imported for the realm_builder library.
using component_testing::ChildRef;
using component_testing::Directory;
using component_testing::ParentRef;
using component_testing::Route;
// Alias for Component child name as provided to Realm Builder.
using ChildName = std::string;
// Timeout for reading from input dump socket.
constexpr zx::duration kSocketTimeout = zx::min(3);
struct StdIOSocket {
zx::socket in_socket;
zx::socket out_socket;
};
class StarnixInputTestBase : public ui_testing::PortableUITest {
protected:
struct EvDevPacket {
// The event timestamp received by Starnix, from Fuchsia.
int64_t sec;
int64_t usec;
// * For an overview of the following fields, see
// https://kernel.org/doc/html/latest/input/input.html#event-interface
// * For details on the constants relevant to Starnix input events, see
// https://kernel.org/doc/html/latest/input/event-codes.html
uint16_t type;
uint16_t code;
int32_t value;
};
// To satisfy ::testing::Test
void TearDown() override {
realm_event_handler_.Stop();
ui_testing::PortableUITest::TearDown();
}
// Launches `input_dump.cc`, connecting its `stdout` to `out_socket`.
// Then waits for `input_dump.cc` to report that it is ready to receive
// input events.
StdIOSocket LaunchDumper() {
// Create a socket for communicating with `input_dump`, and store it in
// a collection of `HandleInfo`s.
std::vector<fuchsia_process::HandleInfo> numbered_handles;
zx::socket out_remote_socket;
zx::socket out_socket;
zx::socket in_remote_socket;
zx::socket in_socket;
zx_status_t sock_res;
// stdout
sock_res = zx::socket::create(ZX_SOCKET_DATAGRAM, &out_socket, &out_remote_socket);
FX_CHECK(sock_res == ZX_OK) << "Creating socket failed: " << zx_status_get_string(sock_res);
numbered_handles.push_back(fuchsia_process::HandleInfo{
{.handle = zx::handle(std::move(out_remote_socket)), .id = PA_HND(PA_FD, STDOUT_FILENO)}});
// stdin
sock_res = zx::socket::create(ZX_SOCKET_DATAGRAM, &in_socket, &in_remote_socket);
FX_CHECK(sock_res == ZX_OK) << "Creating socket failed: " << zx_status_get_string(sock_res);
numbered_handles.push_back(fuchsia_process::HandleInfo{
{.handle = zx::handle(std::move(in_remote_socket)), .id = PA_HND(PA_FD, STDIN_FILENO)}});
// Launch the child.
FX_LOGS(INFO) << "Launching input_dump";
std::optional<fidl::Result<fuchsia_component::Realm::CreateChild>> create_child_status;
zx::result<fidl::ClientEnd<fuchsia_component::Realm>> realm_proxy =
realm_root()->component().Connect<fuchsia_component::Realm>();
if (realm_proxy.is_error()) {
FX_LOGS(FATAL) << "Failed to connect to Realm server: "
<< zx_status_get_string(realm_proxy.error_value());
}
realm_client_ =
fidl::Client(std::move(realm_proxy.value()), dispatcher(), &realm_event_handler_);
realm_client_
->CreateChild({fuchsia_component_decl::CollectionRef(
{{.name = "debian_userspace"}}), // Declared in `debian_container.cml`
fuchsia_component_decl::Child(
{{.name = "input_dump",
.url = "#meta/input_dump.cm",
.startup = fuchsia_component_decl::StartupMode::kLazy}}),
// The `ChildArgs` enable tests to read from the stdout of `input_dump.cc`.
fuchsia_component::CreateChildArgs(
{{.numbered_handles = std::move(numbered_handles)}})})
.ThenExactlyOnce([&](auto result) { create_child_status = std::move(result); });
RunLoopUntil([&] { return create_child_status.has_value(); });
// Check that launching succeeded.
const auto& status = create_child_status.value();
FX_CHECK(!status.is_error()) << "CreateChild() returned error " << status.error_value();
return {.in_socket = std::move(in_socket), .out_socket = std::move(out_socket)};
}
void WaitForMessageFromInputDump(zx::socket& out_socket, const std::string& message) {
FX_LOGS(INFO) << "Waiting message " << message << " from input_dump";
auto packet = BlockingReadFromInputDump(out_socket);
ASSERT_EQ(packet, message) << "Got \"" << packet.data() << "\" with size " << packet.size();
}
void WriteMessageToSocket(zx::socket& in_socket, const std::string& message) {
size_t wrote;
in_socket.write(0, message.data(), message.size(), &wrote);
ASSERT_EQ(wrote, message.size());
}
std::vector<EvDevPacket> GetEvDevPackets(zx::socket& out_socket) {
std::vector<EvDevPacket> ev_pkts;
std::string packets = BlockingReadFromInputDump(out_socket);
std::size_t next = packets.find(relay_api::kEventDelimiter);
while (next != std::string::npos) {
packets = packets.substr(next);
EvDevPacket ev_pkt{};
int res = sscanf(packets.data(), relay_api::kEventFormat, &ev_pkt.sec, &ev_pkt.usec,
&ev_pkt.type, &ev_pkt.code, &ev_pkt.value);
FX_CHECK(res == 5) << "Got " << res << " fields, but wanted 5";
ev_pkts.push_back(ev_pkt);
next = packets.find(relay_api::kEventDelimiter, relay_api::kEventDelimiter.size());
}
return ev_pkts;
}
private:
static constexpr auto kDebianRealm = "debian-realm";
static constexpr auto kDebianRealmUrl = "#meta/debian_realm.cm";
class RealmEventHandler : public fidl::AsyncEventHandler<fuchsia_component::Realm> {
public:
// Ignores any later errors on `this`. Used to avoid false-failures during
// test teardown.
void Stop() { running_ = false; }
void on_fidl_error(fidl::UnbindInfo error) override {
if (running_) {
FX_LOGS(FATAL) << "Error on Realm client: " << error;
}
}
private:
bool running_ = true;
};
// To satisfy ui_testing::PortableUITest
std::string GetTestUIStackUrl() override { return "#meta/test-ui-stack.cm"; }
std::vector<std::pair<ChildName, std::string>> GetTestComponents() override {
return {
std::make_pair(kDebianRealm, kDebianRealmUrl),
};
}
std::vector<Route> GetTestRoutes() override {
return {
// Route global capabilities from parent to the Debian realm.
{.capabilities = {Proto<fuchsia_kernel::VmexResource>(), Proto<fuchsia_sysmem::Allocator>(),
Proto<fuchsia_sysmem2::Allocator>(),
Proto<fuchsia_tracing_provider::Registry>()},
.source = ParentRef(),
.targets = {ChildRef{kDebianRealm}}},
{.capabilities =
{
Directory{
.name = "boot-kernel",
.type = fuchsia::component::decl::DependencyType::STRONG,
},
},
.source = ParentRef(),
.targets = {ChildRef{kDebianRealm}}},
// Route capabilities from test-ui-stack to the Debian realm.
{.capabilities = {Proto<fuchsia_ui_composition::Allocator>(),
Proto<fuchsia_ui_composition::Flatland>(),
Proto<fuchsia_ui_display_singleton::Info>(),
Proto<fuchsia_element::GraphicalPresenter>()},
.source = ui_testing::PortableUITest::kTestUIStackRef,
.targets = {ChildRef{kDebianRealm}}},
// Route capabilities from the Debian realm to the parent.
{.capabilities =
{// Allow this test to launch `input_dump` inside the Debian realm.
Proto<fuchsia_component::Realm>()},
.source = ChildRef{kDebianRealm},
.targets = {ParentRef()}},
};
}
// Reads a single piece of data from `input_dump.cc`, via `out_socket`.
//
// There's no framing protocol between these two programs, so calling
// code must run in lock-step with `input_dump.cc`.
//
// In particular: the calling code must not send a second input event
// until the calling code has read the response that `input_dump.cc`
// sent for the first event.
std::string BlockingReadFromInputDump(zx::socket& out_socket) {
std::string buf(relay_api::kMaxPacketLen * relay_api::kDownUpNumPackets, '\0');
size_t n_read{};
zx_status_t res{};
zx_signals_t actual_signals;
FX_LOGS(INFO) << "Waiting for socket to be readable";
res = out_socket.wait_one(ZX_SOCKET_READABLE, zx::deadline_after(kSocketTimeout),
&actual_signals);
FX_CHECK(res == ZX_OK) << "wait_one() returned " << zx_status_get_string(res);
FX_CHECK(actual_signals & ZX_SOCKET_READABLE)
<< "expected signals to include ZX_SOCKET_READABLE, but actual_signals=" << actual_signals;
res = out_socket.read(/* options = */ 0, buf.data(), buf.capacity(), &n_read);
FX_CHECK(res == ZX_OK) << "read() returned " << zx_status_get_string(res);
buf.resize(n_read);
FX_CHECK(buf != relay_api::kFailedMessage);
return buf;
}
template <typename T>
component_testing::Protocol Proto() {
return {fidl::DiscoverableProtocolName<T>};
}
// Resources for communicating with the realm server.
// * `realm_event_handler_` must live at least as long as `realm_client_`
// * `realm_client_` is stored in the fixture to keep `input_dump` alive for the
// duration of the test
RealmEventHandler realm_event_handler_;
fidl::Client<fuchsia_component::Realm> realm_client_;
};
} // namespace starnix_input_test