diff --git a/BUILD.gn b/BUILD.gn index 09dabe5..38a5903 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -152,9 +152,14 @@ executable("sommelier") { } if (use.test) { - executable("sommelier_test") { - sources = [ "sommelier_test.cc" ] - + static_library("libsommelier-test") { + sources = [ + "sommelier-test.cc", + "sommelier-window-test.cc", + "testing/mock-wayland-channel.cc", + "testing/sommelier-test-util.cc", + ] + include_dirs = [ "testing" ] pkg_deps = [ "pixman-1" ] # gnlint: disable=GnLintCommonTesting @@ -163,8 +168,16 @@ if (use.test) { "gtest", "pixman-1", ] + } + + executable("sommelier_test") { + sources = [ "sommelier-test-main.cc" ] + defines = sommelier_defines - deps = [ ":libsommelier" ] + deps = [ + ":libsommelier", + ":libsommelier-test", + ] } } diff --git a/meson.build b/meson.build index aa16f58..4cefdd3 100644 --- a/meson.build +++ b/meson.build @@ -196,10 +196,16 @@ executable('sommelier', ) if get_option('with_tests') + testing_include_directory = include_directories('testing') + sommelier_test = executable('sommelier_test', install: true, sources: [ - 'sommelier_test.cc', + 'sommelier-test.cc', + 'sommelier-test-main.cc', + 'sommelier-window-test.cc', + 'testing/mock-wayland-channel.cc', + 'testing/sommelier-test-util.cc', ] + wl_outs, link_with: libsommelier, dependencies: [ @@ -208,7 +214,7 @@ if get_option('with_tests') dependency('pixman-1') ], cpp_args: cpp_args + sommelier_defines, - include_directories: includes, + include_directories: includes + testing_include_directory, ) test('sommelier_test', sommelier_test) diff --git a/sommelier-test-main.cc b/sommelier-test-main.cc new file mode 100644 index 0000000..fe983c7 --- /dev/null +++ b/sommelier-test-main.cc @@ -0,0 +1,12 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + testing::GTEST_FLAG(throw_on_failure) = true; + // TODO(nverne): set up logging? + return RUN_ALL_TESTS(); +} diff --git a/sommelier-test.cc b/sommelier-test.cc new file mode 100644 index 0000000..6263971 --- /dev/null +++ b/sommelier-test.cc @@ -0,0 +1,30 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "sommelier.h" // NOLINT(build/include_directory) +#include "sommelier-util.h" // NOLINT(build/include_directory) +#include "testing/wayland-test-base.h" // NOLINT(build/include_directory) + +namespace vm_tools { +namespace sommelier { + +using WaylandTest = WaylandTestBase; + +TEST_F(WaylandTest, CanCommitToEmptySurface) { + wl_surface* surface = wl_compositor_create_surface(ctx.compositor->internal); + wl_surface_commit(surface); +} + +} // namespace sommelier +} // namespace vm_tools diff --git a/sommelier_test.cc b/sommelier-window-test.cc similarity index 54% rename from sommelier_test.cc rename to sommelier-window-test.cc index 0322b27..6bcbb3f 100644 --- a/sommelier_test.cc +++ b/sommelier-window-test.cc @@ -1,16 +1,11 @@ -// Copyright 2021 The ChromiumOS Authors +// Copyright 2023 The ChromiumOS Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include -#include -#include -#include -#include +#include "testing/x11-test-base.h" -#include #include -#include +#include #include #include #include @@ -18,538 +13,14 @@ #include #include -#include "sommelier.h" // NOLINT(build/include_directory) -#include "sommelier-util.h" // NOLINT(build/include_directory) -#include "virtualization/wayland_channel.h" // NOLINT(build/include_directory) -#include "xcb/mock-xcb-shim.h" - -#include "aura-shell-client-protocol.h" // NOLINT(build/include_directory) -#include "xdg-shell-client-protocol.h" // NOLINT(build/include_directory) - -// Help gtest print Wayland message streams on expectation failure. -// -// This is defined in the test file mostly to avoid the main program depending -// on and merely for testing purposes. Also, it doesn't -// print the entire struct, just the data buffer, so it's not a complete -// representation of the object. -std::ostream& operator<<(std::ostream& os, const WaylandSendReceive& w) { - // Partially decode the data buffer. The content of messages is not decoded, - // except their object ID and opcode. - size_t i = 0; - while (i < w.data_size) { - uint32_t object_id = *reinterpret_cast(w.data + i); - uint32_t second_word = *reinterpret_cast(w.data + i + 4); - uint16_t message_size_in_bytes = second_word >> 16; - uint16_t opcode = second_word & 0xffff; - os << "[object ID " << object_id << ", opcode " << opcode << ", length " - << message_size_in_bytes; - - uint16_t size = MIN(message_size_in_bytes, w.data_size - i); - if (size > sizeof(uint32_t) * 2) { - os << ", args=["; - for (int j = sizeof(uint32_t) * 2; j < size; ++j) { - char byte = w.data[i + j]; - if (isprint(byte)) { - os << byte; - } else { - os << "\\" << static_cast(byte); - } - } - os << "]"; - } - os << "]"; - i += message_size_in_bytes; - } - if (i != w.data_size) { - os << "[WARNING: " << (w.data_size - i) << "undecoded trailing bytes]"; - } - - return os; -} - namespace vm_tools { namespace sommelier { using ::testing::_; using ::testing::AllOf; -using ::testing::DoAll; -using ::testing::NiceMock; using ::testing::PrintToString; -using ::testing::Return; -using ::testing::SetArgPointee; -namespace { -uint32_t XdgToplevelId(sl_window* window) { - assert(window->xdg_toplevel); - return wl_proxy_get_id(reinterpret_cast(window->xdg_toplevel)); -} - -uint32_t AuraSurfaceId(sl_window* window) { - assert(window->aura_surface); - return wl_proxy_get_id(reinterpret_cast(window->aura_surface)); -} - -uint32_t AuraToplevelId(sl_window* window) { - assert(window->aura_toplevel); - return wl_proxy_get_id(reinterpret_cast(window->aura_toplevel)); -} - -// This family of functions retrieves Sommelier's listeners for events received -// from the host, so we can call them directly in the test rather than -// (a) exporting the actual functions (which are typically static), or (b) -// creating a fake host compositor to dispatch events via libwayland -// (unnecessarily complicated). -const zaura_toplevel_listener* HostEventHandler( - struct zaura_toplevel* aura_toplevel) { - const void* listener = - wl_proxy_get_listener(reinterpret_cast(aura_toplevel)); - EXPECT_NE(listener, nullptr); - return static_cast(listener); -} - -const xdg_surface_listener* HostEventHandler(struct xdg_surface* xdg_surface) { - const void* listener = - wl_proxy_get_listener(reinterpret_cast(xdg_surface)); - EXPECT_NE(listener, nullptr); - return static_cast(listener); -} - -const xdg_toplevel_listener* HostEventHandler( - struct xdg_toplevel* xdg_toplevel) { - const void* listener = - wl_proxy_get_listener(reinterpret_cast(xdg_toplevel)); - EXPECT_NE(listener, nullptr); - return static_cast(listener); -} - -const wl_output_listener* HostEventHandler(struct wl_output* output) { - const void* listener = - wl_proxy_get_listener(reinterpret_cast(output)); - EXPECT_NE(listener, nullptr); - return static_cast(listener); -} - -const zaura_output_listener* HostEventHandler(struct zaura_output* output) { - const void* listener = - wl_proxy_get_listener(reinterpret_cast(output)); - EXPECT_NE(listener, nullptr); - return static_cast(listener); -} - -} // namespace - -// Mock of Sommelier's Wayland connection to the host compositor. -class MockWaylandChannel : public WaylandChannel { - public: - MockWaylandChannel() {} - - MOCK_METHOD(int32_t, init, (), (override)); - MOCK_METHOD(bool, supports_dmabuf, (), (override)); - MOCK_METHOD(int32_t, - create_context, - (int& out_socket_fd), - (override)); // NOLINT(runtime/references) - MOCK_METHOD(int32_t, - create_pipe, - (int& out_pipe_fd), - (override)); // NOLINT(runtime/references) - MOCK_METHOD(int32_t, - send, - (const struct WaylandSendReceive& send), - (override)); - MOCK_METHOD( - int32_t, - handle_channel_event, - (enum WaylandChannelEvent & event_type, // NOLINT(runtime/references) - struct WaylandSendReceive& receive, // NOLINT(runtime/references) - int& out_read_pipe), // NOLINT(runtime/references) - (override)); - - MOCK_METHOD(int32_t, - allocate, - (const struct WaylandBufferCreateInfo& create_info, - struct WaylandBufferCreateOutput& - create_output), // NOLINT(runtime/references) - (override)); - MOCK_METHOD(int32_t, sync, (int dmabuf_fd, uint64_t flags), (override)); - MOCK_METHOD(int32_t, - handle_pipe, - (int read_fd, - bool readable, - bool& hang_up), // NOLINT(runtime/references) - (override)); - MOCK_METHOD(size_t, max_send_size, (), (override)); - - protected: - ~MockWaylandChannel() override {} -}; - -// Match a WaylandSendReceive buffer containing exactly one Wayland message -// with given object ID and opcode. -MATCHER_P2(ExactlyOneMessage, - object_id, - opcode, - std::string(negation ? "not " : "") + - "exactly one Wayland message for object ID " + - PrintToString(object_id) + ", opcode " + PrintToString(opcode)) { - const struct WaylandSendReceive& send = arg; - if (send.data_size < sizeof(uint32_t) * 2) { - // Malformed packet (too short) - return false; - } - - uint32_t actual_object_id = *reinterpret_cast(send.data); - uint32_t second_word = *reinterpret_cast(send.data + 4); - uint16_t message_size_in_bytes = second_word >> 16; - uint16_t actual_opcode = second_word & 0xffff; - - // ID and opcode must match expectation, and we must see exactly one message - // with the indicated length. - return object_id == actual_object_id && opcode == actual_opcode && - message_size_in_bytes == send.data_size; -}; - -// Match a WaylandSendReceive buffer containing at least one Wayland message -// with given object ID and opcode. -MATCHER_P2(AtLeastOneMessage, - object_id, - opcode, - std::string(negation ? "no Wayland messages " - : "at least one Wayland message ") + - "for object ID " + PrintToString(object_id) + ", opcode " + - PrintToString(opcode)) { - const struct WaylandSendReceive& send = arg; - if (send.data_size < sizeof(uint32_t) * 2) { - // Malformed packet (too short) - return false; - } - for (uint32_t i = 0; i < send.data_size;) { - uint32_t actual_object_id = *reinterpret_cast(send.data + i); - uint32_t second_word = *reinterpret_cast(send.data + i + 4); - uint16_t message_size_in_bytes = second_word >> 16; - uint16_t actual_opcode = second_word & 0xffff; - if (i + message_size_in_bytes > send.data_size) { - // Malformed packet (stated message size overflows buffer) - break; - } - if (object_id == actual_object_id && opcode == actual_opcode) { - return true; - } - i += message_size_in_bytes; - } - return false; -} - -// Match a WaylandSendReceive buffer containing a string. -// TODO(cpelling): This is currently very naive; it doesn't respect -// boundaries between messages or their arguments. Fix me. -MATCHER_P(AnyMessageContainsString, - str, - std::string("a Wayland message containing string ") + str) { - const struct WaylandSendReceive& send = arg; - size_t prefix_len = sizeof(uint32_t) * 2; - std::string data_as_str(reinterpret_cast(send.data + prefix_len), - send.data_size - prefix_len); - - return data_as_str.find(str) != std::string::npos; -} - -// Create a Wayland client and connect it to Sommelier's Wayland server. -// -// Sets up an actual Wayland client which connects over a Unix socket, -// and can make Wayland requests in the same way as a regular client. -// However, it has no event loop so doesn't process events. -class FakeWaylandClient { - public: - explicit FakeWaylandClient(struct sl_context* ctx) { - // Create a socket pair for libwayland-server and libwayland-client - // to communicate over. - int rv = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv); - errno_assert(!rv); - // wl_client takes ownership of its file descriptor - client = wl_client_create(ctx->host_display, sv[0]); - errno_assert(!!client); - sl_set_display_implementation(ctx, client); - client_display = wl_display_connect_to_fd(sv[1]); - EXPECT_NE(client_display, nullptr); - - client_registry = wl_display_get_registry(client_display); - compositor = static_cast(wl_registry_bind( - client_registry, GlobalName(ctx, &wl_compositor_interface), - &wl_compositor_interface, WL_COMPOSITOR_CREATE_SURFACE_SINCE_VERSION)); - wl_display_flush(client_display); - } - - ~FakeWaylandClient() { - wl_display_disconnect(client_display); - client_display = nullptr; - wl_client_destroy(client); - client = nullptr; - } - - // Bind to every advertised wl_output and return how many were bound. - unsigned int BindToWlOutputs(struct sl_context* ctx) { - unsigned int bound = 0; - struct sl_global* global; - wl_list_for_each(global, &ctx->globals, link) { - if (global->interface == &wl_output_interface) { - outputs.push_back(static_cast( - wl_registry_bind(client_registry, global->name, global->interface, - WL_OUTPUT_DONE_SINCE_VERSION))); - bound++; - } - } - wl_display_flush(client_display); - return bound; - } - - // Create a surface and return its ID - uint32_t CreateSurface() { - struct wl_surface* surface = wl_compositor_create_surface(compositor); - wl_display_flush(client_display); - return wl_proxy_get_id(reinterpret_cast(surface)); - } - - // Represents the client from the server's (Sommelier's) end. - struct wl_client* client = nullptr; - - std::vector outputs; - - protected: - // Find the "name" of Sommelier's global for a particular interface, - // so our fake client can bind to it. This is cheating (normally - // these names would come from wl_registry.global events) but - // easier than setting up a proper event loop for this fake client. - uint32_t GlobalName(struct sl_context* ctx, - const struct wl_interface* for_interface) { - struct sl_global* global; - wl_list_for_each(global, &ctx->globals, link) { - if (global->interface == for_interface) { - return global->name; - } - } - assert(false); - return 0; - } - - int sv[2]; - - // Represents the server (Sommelier) from the client end. - struct wl_display* client_display = nullptr; - struct wl_registry* client_registry = nullptr; - struct wl_compositor* compositor = nullptr; -}; - -// Properties of a fake output (monitor) to advertise. -struct OutputConfig { - int32_t x = 0; - int32_t y = 0; - int32_t physical_width_mm = 400; - int32_t physical_height_mm = 225; - int32_t width_pixels = 1920; - int32_t height_pixels = 1080; - int32_t transform = WL_OUTPUT_TRANSFORM_NORMAL; - int32_t scale = 1; - int32_t output_scale = 1000; -}; - -// Fixture for tests which exercise only Wayland functionality. -class WaylandTest : public ::testing::Test { - public: - void SetUp() override { - ON_CALL(mock_wayland_channel_, create_context(_)).WillByDefault(Return(0)); - ON_CALL(mock_wayland_channel_, max_send_size()) - .WillByDefault(Return(DEFAULT_BUFFER_SIZE)); - EXPECT_CALL(mock_wayland_channel_, init).Times(1); - sl_context_init_default(&ctx); - ctx.host_display = wl_display_create(); - assert(ctx.host_display); - - ctx.channel = &mock_wayland_channel_; - EXPECT_TRUE(sl_context_init_wayland_channel( - &ctx, wl_display_get_event_loop(ctx.host_display), false)); - - InitContext(); - Connect(); - } - - void TearDown() override { - // Process any pending messages before the test exits. - Pump(); - - // TODO(cpelling): Destroy context and any created windows? - } - - // Flush and dispatch Wayland client calls to the mock host. - // - // Called by default in TearDown(), but you can also trigger it midway - // through the test. - // - // If you call `EXPECT_CALL(mock_wayland_channel_, send)` before Pump(), the - // expectations won't trigger until the Pump() call. - // - // Conversely, calling Pump() before - // `EXPECT_CALL(mock_wayland_channel_, send)` is useful to flush out - // init messages not relevant to your test case. - void Pump() { - wl_display_flush(ctx.display); - wl_event_loop_dispatch(wl_display_get_event_loop(ctx.host_display), 0); - } - - protected: - // Allow subclasses to customize the context prior to Connect(). - virtual void InitContext() {} - - // Set up the Wayland connection, compositor and registry. - virtual void Connect() { - ctx.display = wl_display_connect_to_fd(ctx.virtwl_display_fd); - wl_registry* registry = wl_display_get_registry(ctx.display); - - // Fake the host compositor advertising globals. - sl_registry_handler(&ctx, registry, next_server_id++, "wl_compositor", - kMinHostWlCompositorVersion); - EXPECT_NE(ctx.compositor, nullptr); - sl_registry_handler(&ctx, registry, next_server_id++, "xdg_wm_base", - XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION); - sl_registry_handler(&ctx, registry, next_server_id++, "zaura_shell", - ZAURA_TOPLEVEL_SET_WINDOW_BOUNDS_SINCE_VERSION); - } - - // Set up one or more fake outputs for the test. - void AdvertiseOutputs(FakeWaylandClient* client, - std::vector outputs) { - // The host compositor should advertise a wl_output global for each output. - // Sommelier will handle this by forwarding the globals to its client. - for (const auto& output : outputs) { - UNUSED(output); // suppress -Wunused-variable - uint32_t output_id = next_server_id++; - sl_registry_handler(&ctx, wl_display_get_registry(ctx.display), output_id, - "wl_output", WL_OUTPUT_DONE_SINCE_VERSION); - } - - // host_outputs populates when Sommelier's client binds to those globals. - EXPECT_EQ(client->BindToWlOutputs(&ctx), outputs.size()); - Pump(); // process the bind requests - - // Now the outputs are populated, we can advertise their settings. - sl_host_output* host_output; - uint32_t i = 0; - wl_list_for_each(host_output, &ctx.host_outputs, link) { - ConfigureOutput(host_output, outputs[i]); - i++; - } - // host_outputs should be the requested length. - EXPECT_EQ(i, outputs.size()); - } - - void ConfigureOutput(sl_host_output* host_output, - const OutputConfig& config) { - // This is mimicking components/exo/wayland/output_metrics.cc - uint32_t flags = ZAURA_OUTPUT_SCALE_PROPERTY_CURRENT; - if (config.output_scale == 1000) { - flags |= ZAURA_OUTPUT_SCALE_PROPERTY_PREFERRED; - } - HostEventHandler(host_output->aura_output) - ->scale(nullptr, host_output->aura_output, flags, config.output_scale); - HostEventHandler(host_output->proxy) - ->geometry(nullptr, host_output->proxy, config.x, config.y, - config.physical_width_mm, config.physical_height_mm, - WL_OUTPUT_SUBPIXEL_NONE, "ACME Corp", "Generic Monitor", - config.transform); - HostEventHandler(host_output->proxy) - ->mode(nullptr, host_output->proxy, - WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED, - config.width_pixels, config.height_pixels, 60); - HostEventHandler(host_output->proxy) - ->scale(nullptr, host_output->proxy, config.scale); - HostEventHandler(host_output->proxy)->done(nullptr, host_output->proxy); - Pump(); - } - - testing::NiceMock mock_wayland_channel_; - sl_context ctx; - - // IDs allocated by the server are in the range [0xff000000, 0xffffffff]. - uint32_t next_server_id = 0xff000000; -}; - -// Fixture for unit tests which exercise both Wayland and X11 functionality. -class X11Test : public WaylandTest { - public: - void InitContext() override { - WaylandTest::InitContext(); - ctx.xwayland = 1; - - // Create a fake screen with somewhat plausible values. - // Some of these are not realistic because they refer to things not present - // in the mocked X environment (such as specifying a root window with ID 0). - ctx.screen = static_cast(malloc(sizeof(xcb_screen_t))); - ctx.screen->root = 0x0; - ctx.screen->default_colormap = 0x0; - ctx.screen->white_pixel = 0x00ffffff; - ctx.screen->black_pixel = 0x00000000; - ctx.screen->current_input_masks = 0x005a0000; - ctx.screen->width_in_pixels = 1920; - ctx.screen->height_in_pixels = 1080; - ctx.screen->width_in_millimeters = 508; - ctx.screen->height_in_millimeters = 285; - ctx.screen->min_installed_maps = 1; - ctx.screen->max_installed_maps = 1; - ctx.screen->root_visual = 0x0; - ctx.screen->backing_stores = 0x01; - ctx.screen->save_unders = 0; - ctx.screen->root_depth = 24; - ctx.screen->allowed_depths_len = 0; - } - - void Connect() override { - set_xcb_shim(&xcb); - WaylandTest::Connect(); - - // Pretend Xwayland has connected to Sommelier as a Wayland client. - xwayland = std::make_unique(&ctx); - ctx.client = xwayland->client; - - // TODO(cpelling): mock out more of xcb so this isn't needed - ctx.connection = xcb_connect(nullptr, nullptr); - } - - ~X11Test() override { set_xcb_shim(nullptr); } - - uint32_t GenerateId() { - static uint32_t id = 0; - return ++id; - } - - virtual sl_window* CreateWindowWithoutRole() { - xcb_window_t window_id = GenerateId(); - sl_create_window(&ctx, window_id, 0, 0, 800, 600, 0); - sl_window* window = sl_lookup_window(&ctx, window_id); - EXPECT_NE(window, nullptr); - return window; - } - - virtual sl_window* CreateToplevelWindow() { - sl_window* window = CreateWindowWithoutRole(); - - // Pretend we created a frame window too - window->frame_id = GenerateId(); - - window->host_surface_id = xwayland->CreateSurface(); - sl_window_update(window); - Pump(); - return window; - } - - protected: - NiceMock xcb; - std::unique_ptr xwayland; -}; - -TEST_F(WaylandTest, CanCommitToEmptySurface) { - wl_surface* surface = wl_compositor_create_surface(ctx.compositor->internal); - wl_surface_commit(surface); -} +using X11Test = X11TestBase; TEST_F(X11Test, TogglesFullscreenOnWmStateFullscreen) { // Arrange: Create an xdg_toplevel surface. Initially it's not fullscreen. @@ -1161,10 +632,3 @@ TEST_F(X11Test, X11ConfigureRequestWithoutPositionIsNotForwardedToAuraHost) { } // namespace sommelier } // namespace vm_tools - -int main(int argc, char** argv) { - testing::InitGoogleTest(&argc, argv); - testing::GTEST_FLAG(throw_on_failure) = true; - // TODO(nverne): set up logging? - return RUN_ALL_TESTS(); -} diff --git a/testing/mock-wayland-channel.cc b/testing/mock-wayland-channel.cc new file mode 100644 index 0000000..bd9d888 --- /dev/null +++ b/testing/mock-wayland-channel.cc @@ -0,0 +1,42 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "mock-wayland-channel.h" // NOLINT(build/include_directory) + +#include + +std::ostream& operator<<(std::ostream& os, const WaylandSendReceive& w) { + // Partially decode the data buffer. The content of messages is not decoded, + // except their object ID and opcode. + size_t i = 0; + while (i < w.data_size) { + uint32_t object_id = *reinterpret_cast(w.data + i); + uint32_t second_word = *reinterpret_cast(w.data + i + 4); + uint16_t message_size_in_bytes = second_word >> 16; + uint16_t opcode = second_word & 0xffff; + os << "[object ID " << object_id << ", opcode " << opcode << ", length " + << message_size_in_bytes; + + uint16_t size = MIN(message_size_in_bytes, w.data_size - i); + if (size > sizeof(uint32_t) * 2) { + os << ", args=["; + for (int j = sizeof(uint32_t) * 2; j < size; ++j) { + char byte = w.data[i + j]; + if (isprint(byte)) { + os << byte; + } else { + os << "\\" << static_cast(byte); + } + } + os << "]"; + } + os << "]"; + i += message_size_in_bytes; + } + if (i != w.data_size) { + os << "[WARNING: " << (w.data_size - i) << "undecoded trailing bytes]"; + } + + return os; +} diff --git a/testing/mock-wayland-channel.h b/testing/mock-wayland-channel.h new file mode 100644 index 0000000..1d67fb1 --- /dev/null +++ b/testing/mock-wayland-channel.h @@ -0,0 +1,146 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef VM_TOOLS_SOMMELIER_TESTING_MOCK_WAYLAND_CHANNEL_H_ +#define VM_TOOLS_SOMMELIER_TESTING_MOCK_WAYLAND_CHANNEL_H_ + +#include +#include + +#include "../sommelier-ctx.h" // NOLINT(build/include_directory) +#include "../virtualization/wayland_channel.h" // NOLINT(build/include_directory) + +// Help gtest print Wayland message streams on expectation failure. +// +// This is defined in the test file mostly to avoid the main program depending +// on and merely for testing purposes. Also, it doesn't +// print the entire struct, just the data buffer, so it's not a complete +// representation of the object. +std::ostream& operator<<(std::ostream& os, const WaylandSendReceive& w); + +namespace vm_tools { +namespace sommelier { + +using ::testing::PrintToString; + +// Mock of Sommelier's Wayland connection to the host compositor. +class MockWaylandChannel : public WaylandChannel { + public: + MockWaylandChannel() {} + + MOCK_METHOD(int32_t, init, (), (override)); + MOCK_METHOD(bool, supports_dmabuf, (), (override)); + MOCK_METHOD(int32_t, + create_context, + (int& out_socket_fd), + (override)); // NOLINT(runtime/references) + MOCK_METHOD(int32_t, + create_pipe, + (int& out_pipe_fd), + (override)); // NOLINT(runtime/references) + MOCK_METHOD(int32_t, + send, + (const struct WaylandSendReceive& send), + (override)); + MOCK_METHOD( + int32_t, + handle_channel_event, + (enum WaylandChannelEvent & event_type, // NOLINT(runtime/references) + struct WaylandSendReceive& receive, // NOLINT(runtime/references) + int& out_read_pipe), // NOLINT(runtime/references) + (override)); + + MOCK_METHOD(int32_t, + allocate, + (const struct WaylandBufferCreateInfo& create_info, + struct WaylandBufferCreateOutput& + create_output), // NOLINT(runtime/references) + (override)); + MOCK_METHOD(int32_t, sync, (int dmabuf_fd, uint64_t flags), (override)); + MOCK_METHOD(int32_t, + handle_pipe, + (int read_fd, + bool readable, + bool& hang_up), // NOLINT(runtime/references) + (override)); + MOCK_METHOD(size_t, max_send_size, (), (override)); + + protected: + ~MockWaylandChannel() override {} +}; + +// Match a WaylandSendReceive buffer containing exactly one Wayland message +// with given object ID and opcode. +MATCHER_P2(ExactlyOneMessage, + object_id, + opcode, + std::string(negation ? "not " : "") + + "exactly one Wayland message for object ID " + + PrintToString(object_id) + ", opcode " + PrintToString(opcode)) { + const struct WaylandSendReceive& send = arg; + if (send.data_size < sizeof(uint32_t) * 2) { + // Malformed packet (too short) + return false; + } + + uint32_t actual_object_id = *reinterpret_cast(send.data); + uint32_t second_word = *reinterpret_cast(send.data + 4); + uint16_t message_size_in_bytes = second_word >> 16; + uint16_t actual_opcode = second_word & 0xffff; + + // ID and opcode must match expectation, and we must see exactly one message + // with the indicated length. + return object_id == actual_object_id && opcode == actual_opcode && + message_size_in_bytes == send.data_size; +}; + +// Match a WaylandSendReceive buffer containing at least one Wayland message +// with given object ID and opcode. +MATCHER_P2(AtLeastOneMessage, + object_id, + opcode, + std::string(negation ? "no Wayland messages " + : "at least one Wayland message ") + + "for object ID " + PrintToString(object_id) + ", opcode " + + PrintToString(opcode)) { + const struct WaylandSendReceive& send = arg; + if (send.data_size < sizeof(uint32_t) * 2) { + // Malformed packet (too short) + return false; + } + for (uint32_t i = 0; i < send.data_size;) { + uint32_t actual_object_id = *reinterpret_cast(send.data + i); + uint32_t second_word = *reinterpret_cast(send.data + i + 4); + uint16_t message_size_in_bytes = second_word >> 16; + uint16_t actual_opcode = second_word & 0xffff; + if (i + message_size_in_bytes > send.data_size) { + // Malformed packet (stated message size overflows buffer) + break; + } + if (object_id == actual_object_id && opcode == actual_opcode) { + return true; + } + i += message_size_in_bytes; + } + return false; +} + +// Match a WaylandSendReceive buffer containing a string. +// TODO(cpelling): This is currently very naive; it doesn't respect +// boundaries between messages or their arguments. Fix me. +MATCHER_P(AnyMessageContainsString, + str, + std::string("a Wayland message containing string ") + str) { + const struct WaylandSendReceive& send = arg; + size_t prefix_len = sizeof(uint32_t) * 2; + std::string data_as_str(reinterpret_cast(send.data + prefix_len), + send.data_size - prefix_len); + + return data_as_str.find(str) != std::string::npos; +} + +} // namespace sommelier +} // namespace vm_tools + +#endif // VM_TOOLS_SOMMELIER_TESTING_MOCK_WAYLAND_CHANNEL_H_ diff --git a/testing/sommelier-test-util.cc b/testing/sommelier-test-util.cc new file mode 100644 index 0000000..96d0dbb --- /dev/null +++ b/testing/sommelier-test-util.cc @@ -0,0 +1,65 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sommelier-test-util.h" // NOLINT(build/include_directory) + +#include + +namespace vm_tools { +namespace sommelier { + +const zaura_toplevel_listener* HostEventHandler( + struct zaura_toplevel* aura_toplevel) { + const void* listener = + wl_proxy_get_listener(reinterpret_cast(aura_toplevel)); + EXPECT_NE(listener, nullptr); + return static_cast(listener); +} + +const xdg_surface_listener* HostEventHandler(struct xdg_surface* xdg_surface) { + const void* listener = + wl_proxy_get_listener(reinterpret_cast(xdg_surface)); + EXPECT_NE(listener, nullptr); + return static_cast(listener); +} + +const xdg_toplevel_listener* HostEventHandler( + struct xdg_toplevel* xdg_toplevel) { + const void* listener = + wl_proxy_get_listener(reinterpret_cast(xdg_toplevel)); + EXPECT_NE(listener, nullptr); + return static_cast(listener); +} + +const wl_output_listener* HostEventHandler(struct wl_output* output) { + const void* listener = + wl_proxy_get_listener(reinterpret_cast(output)); + EXPECT_NE(listener, nullptr); + return static_cast(listener); +} + +const zaura_output_listener* HostEventHandler(struct zaura_output* output) { + const void* listener = + wl_proxy_get_listener(reinterpret_cast(output)); + EXPECT_NE(listener, nullptr); + return static_cast(listener); +} + +uint32_t XdgToplevelId(sl_window* window) { + assert(window->xdg_toplevel); + return wl_proxy_get_id(reinterpret_cast(window->xdg_toplevel)); +} + +uint32_t AuraSurfaceId(sl_window* window) { + assert(window->aura_surface); + return wl_proxy_get_id(reinterpret_cast(window->aura_surface)); +} + +uint32_t AuraToplevelId(sl_window* window) { + assert(window->aura_toplevel); + return wl_proxy_get_id(reinterpret_cast(window->aura_toplevel)); +} + +} // namespace sommelier +} // namespace vm_tools diff --git a/testing/sommelier-test-util.h b/testing/sommelier-test-util.h new file mode 100644 index 0000000..9e9b90b --- /dev/null +++ b/testing/sommelier-test-util.h @@ -0,0 +1,41 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef VM_TOOLS_SOMMELIER_TESTING_SOMMELIER_TEST_UTIL_H_ +#define VM_TOOLS_SOMMELIER_TESTING_SOMMELIER_TEST_UTIL_H_ + +#include + +#include "../sommelier.h" // NOLINT(build/include_directory) +#include "aura-shell-client-protocol.h" // NOLINT(build/include_directory) +#include "xdg-shell-client-protocol.h" // NOLINT(build/include_directory) + +namespace vm_tools { +namespace sommelier { + +// This family of functions retrieves Sommelier's listeners for events received +// from the host, so we can call them directly in the test rather than +// (a) exporting the actual functions (which are typically static), or (b) +// creating a fake host compositor to dispatch events via libwayland +// (unnecessarily complicated). +const zaura_toplevel_listener* HostEventHandler( + struct zaura_toplevel* aura_toplevel); + +const xdg_surface_listener* HostEventHandler(struct xdg_surface* xdg_surface); + +const xdg_toplevel_listener* HostEventHandler( + struct xdg_toplevel* xdg_toplevel); + +const wl_output_listener* HostEventHandler(struct wl_output* output); + +const zaura_output_listener* HostEventHandler(struct zaura_output* output); + +uint32_t XdgToplevelId(sl_window* window); +uint32_t AuraSurfaceId(sl_window* window); +uint32_t AuraToplevelId(sl_window* window); + +} // namespace sommelier +} // namespace vm_tools + +#endif // VM_TOOLS_SOMMELIER_TESTING_SOMMELIER_TEST_UTIL_H_ diff --git a/testing/wayland-test-base.h b/testing/wayland-test-base.h new file mode 100644 index 0000000..91380f8 --- /dev/null +++ b/testing/wayland-test-base.h @@ -0,0 +1,246 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef VM_TOOLS_SOMMELIER_TESTING_WAYLAND_TEST_BASE_H_ +#define VM_TOOLS_SOMMELIER_TESTING_WAYLAND_TEST_BASE_H_ + +#include +#include +#include + +#include "../sommelier.h" // NOLINT(build/include_directory) +#include "aura-shell-client-protocol.h" // NOLINT(build/include_directory) +#include "mock-wayland-channel.h" // NOLINT(build/include_directory) +#include "sommelier-test-util.h" // NOLINT(build/include_directory) +#include "xdg-shell-client-protocol.h" // NOLINT(build/include_directory) + +namespace vm_tools { +namespace sommelier { + +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Return; + +// Create a Wayland client and connect it to Sommelier's Wayland server. +// +// Sets up an actual Wayland client which connects over a Unix socket, +// and can make Wayland requests in the same way as a regular client. +// However, it has no event loop so doesn't process events. +class FakeWaylandClient { + public: + explicit FakeWaylandClient(struct sl_context* ctx) { + // Create a socket pair for libwayland-server and libwayland-client + // to communicate over. + int rv = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv); + errno_assert(!rv); + // wl_client takes ownership of its file descriptor + client = wl_client_create(ctx->host_display, sv[0]); + errno_assert(!!client); + sl_set_display_implementation(ctx, client); + client_display = wl_display_connect_to_fd(sv[1]); + EXPECT_NE(client_display, nullptr); + + client_registry = wl_display_get_registry(client_display); + compositor = static_cast(wl_registry_bind( + client_registry, GlobalName(ctx, &wl_compositor_interface), + &wl_compositor_interface, WL_COMPOSITOR_CREATE_SURFACE_SINCE_VERSION)); + wl_display_flush(client_display); + } + + ~FakeWaylandClient() { + wl_display_disconnect(client_display); + client_display = nullptr; + wl_client_destroy(client); + client = nullptr; + } + + // Bind to every advertised wl_output and return how many were bound. + unsigned int BindToWlOutputs(struct sl_context* ctx) { + unsigned int bound = 0; + struct sl_global* global; + wl_list_for_each(global, &ctx->globals, link) { + if (global->interface == &wl_output_interface) { + outputs.push_back(static_cast( + wl_registry_bind(client_registry, global->name, global->interface, + WL_OUTPUT_DONE_SINCE_VERSION))); + bound++; + } + } + wl_display_flush(client_display); + return bound; + } + + // Create a surface and return its ID + uint32_t CreateSurface() { + struct wl_surface* surface = wl_compositor_create_surface(compositor); + wl_display_flush(client_display); + return wl_proxy_get_id(reinterpret_cast(surface)); + } + + // Represents the client from the server's (Sommelier's) end. + struct wl_client* client = nullptr; + + std::vector outputs; + + protected: + // Find the "name" of Sommelier's global for a particular interface, + // so our fake client can bind to it. This is cheating (normally + // these names would come from wl_registry.global events) but + // easier than setting up a proper event loop for this fake client. + uint32_t GlobalName(struct sl_context* ctx, + const struct wl_interface* for_interface) { + struct sl_global* global; + wl_list_for_each(global, &ctx->globals, link) { + if (global->interface == for_interface) { + return global->name; + } + } + assert(false); + return 0; + } + + int sv[2]; + + // Represents the server (Sommelier) from the client end. + struct wl_display* client_display = nullptr; + struct wl_registry* client_registry = nullptr; + struct wl_compositor* compositor = nullptr; +}; + +// Properties of a fake output (monitor) to advertise. +struct OutputConfig { + int32_t x = 0; + int32_t y = 0; + int32_t physical_width_mm = 400; + int32_t physical_height_mm = 225; + int32_t width_pixels = 1920; + int32_t height_pixels = 1080; + int32_t transform = WL_OUTPUT_TRANSFORM_NORMAL; + int32_t scale = 1; + int32_t output_scale = 1000; +}; + +// Fixture for tests which exercise only Wayland functionality. +class WaylandTestBase : public ::testing::Test { + public: + void SetUp() override { + ON_CALL(mock_wayland_channel_, create_context(_)).WillByDefault(Return(0)); + ON_CALL(mock_wayland_channel_, max_send_size()) + .WillByDefault(Return(DEFAULT_BUFFER_SIZE)); + EXPECT_CALL(mock_wayland_channel_, init).Times(1); + sl_context_init_default(&ctx); + ctx.host_display = wl_display_create(); + assert(ctx.host_display); + + ctx.channel = &mock_wayland_channel_; + EXPECT_TRUE(sl_context_init_wayland_channel( + &ctx, wl_display_get_event_loop(ctx.host_display), false)); + + InitContext(); + Connect(); + } + + void TearDown() override { + // Process any pending messages before the test exits. + Pump(); + + // TODO(cpelling): Destroy context and any created windows? + } + + // Flush and dispatch Wayland client calls to the mock host. + // + // Called by default in TearDown(), but you can also trigger it midway + // through the test. + // + // If you call `EXPECT_CALL(mock_wayland_channel_, send)` before Pump(), the + // expectations won't trigger until the Pump() call. + // + // Conversely, calling Pump() before + // `EXPECT_CALL(mock_wayland_channel_, send)` is useful to flush out + // init messages not relevant to your test case. + void Pump() { + wl_display_flush(ctx.display); + wl_event_loop_dispatch(wl_display_get_event_loop(ctx.host_display), 0); + } + + protected: + // Allow subclasses to customize the context prior to Connect(). + virtual void InitContext() {} + + // Set up the Wayland connection, compositor and registry. + virtual void Connect() { + ctx.display = wl_display_connect_to_fd(ctx.virtwl_display_fd); + wl_registry* registry = wl_display_get_registry(ctx.display); + + // Fake the host compositor advertising globals. + sl_registry_handler(&ctx, registry, next_server_id++, "wl_compositor", + kMinHostWlCompositorVersion); + EXPECT_NE(ctx.compositor, nullptr); + sl_registry_handler(&ctx, registry, next_server_id++, "xdg_wm_base", + XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION); + sl_registry_handler(&ctx, registry, next_server_id++, "zaura_shell", + ZAURA_TOPLEVEL_SET_WINDOW_BOUNDS_SINCE_VERSION); + } + + // Set up one or more fake outputs for the test. + void AdvertiseOutputs(FakeWaylandClient* client, + std::vector outputs) { + // The host compositor should advertise a wl_output global for each output. + // Sommelier will handle this by forwarding the globals to its client. + for (const auto& output : outputs) { + UNUSED(output); // suppress -Wunused-variable + uint32_t output_id = next_server_id++; + sl_registry_handler(&ctx, wl_display_get_registry(ctx.display), output_id, + "wl_output", WL_OUTPUT_DONE_SINCE_VERSION); + } + + // host_outputs populates when Sommelier's client binds to those globals. + EXPECT_EQ(client->BindToWlOutputs(&ctx), outputs.size()); + Pump(); // process the bind requests + + // Now the outputs are populated, we can advertise their settings. + sl_host_output* host_output; + uint32_t i = 0; + wl_list_for_each(host_output, &ctx.host_outputs, link) { + ConfigureOutput(host_output, outputs[i]); + i++; + } + // host_outputs should be the requested length. + EXPECT_EQ(i, outputs.size()); + } + + void ConfigureOutput(sl_host_output* host_output, + const OutputConfig& config) { + // This is mimicking components/exo/wayland/output_metrics.cc + uint32_t flags = ZAURA_OUTPUT_SCALE_PROPERTY_CURRENT; + if (config.output_scale == 1000) { + flags |= ZAURA_OUTPUT_SCALE_PROPERTY_PREFERRED; + } + HostEventHandler(host_output->aura_output) + ->scale(nullptr, host_output->aura_output, flags, config.output_scale); + HostEventHandler(host_output->proxy) + ->geometry(nullptr, host_output->proxy, config.x, config.y, + config.physical_width_mm, config.physical_height_mm, + WL_OUTPUT_SUBPIXEL_NONE, "ACME Corp", "Generic Monitor", + config.transform); + HostEventHandler(host_output->proxy) + ->mode(nullptr, host_output->proxy, + WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED, + config.width_pixels, config.height_pixels, 60); + HostEventHandler(host_output->proxy) + ->scale(nullptr, host_output->proxy, config.scale); + HostEventHandler(host_output->proxy)->done(nullptr, host_output->proxy); + Pump(); + } + + NiceMock mock_wayland_channel_; + sl_context ctx; + + // IDs allocated by the server are in the range [0xff000000, 0xffffffff]. + uint32_t next_server_id = 0xff000000; +}; +} // namespace sommelier +} // namespace vm_tools + +#endif // VM_TOOLS_SOMMELIER_TESTING_WAYLAND_TEST_BASE_H_ diff --git a/testing/x11-test-base.h b/testing/x11-test-base.h new file mode 100644 index 0000000..fa4d9c8 --- /dev/null +++ b/testing/x11-test-base.h @@ -0,0 +1,92 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef VM_TOOLS_SOMMELIER_TESTING_X11_TEST_BASE_H_ +#define VM_TOOLS_SOMMELIER_TESTING_X11_TEST_BASE_H_ + +#include + +#include "../xcb/mock-xcb-shim.h" +#include "wayland-test-base.h" // NOLINT(build/include_directory) + +namespace vm_tools { +namespace sommelier { + +// Fixture for unit tests which exercise both Wayland and X11 functionality. +class X11TestBase : public WaylandTestBase { + public: + void InitContext() override { + WaylandTestBase::InitContext(); + ctx.xwayland = 1; + + // Create a fake screen with somewhat plausible values. + // Some of these are not realistic because they refer to things not present + // in the mocked X environment (such as specifying a root window with ID 0). + ctx.screen = static_cast(malloc(sizeof(xcb_screen_t))); + ctx.screen->root = 0x0; + ctx.screen->default_colormap = 0x0; + ctx.screen->white_pixel = 0x00ffffff; + ctx.screen->black_pixel = 0x00000000; + ctx.screen->current_input_masks = 0x005a0000; + ctx.screen->width_in_pixels = 1920; + ctx.screen->height_in_pixels = 1080; + ctx.screen->width_in_millimeters = 508; + ctx.screen->height_in_millimeters = 285; + ctx.screen->min_installed_maps = 1; + ctx.screen->max_installed_maps = 1; + ctx.screen->root_visual = 0x0; + ctx.screen->backing_stores = 0x01; + ctx.screen->save_unders = 0; + ctx.screen->root_depth = 24; + ctx.screen->allowed_depths_len = 0; + } + + void Connect() override { + set_xcb_shim(&xcb); + WaylandTestBase::Connect(); + + // Pretend Xwayland has connected to Sommelier as a Wayland client. + xwayland = std::make_unique(&ctx); + ctx.client = xwayland->client; + + // TODO(cpelling): mock out more of xcb so this isn't needed + ctx.connection = xcb_connect(nullptr, nullptr); + } + + ~X11TestBase() override { set_xcb_shim(nullptr); } + + uint32_t GenerateId() { + static uint32_t id = 0; + return ++id; + } + + virtual sl_window* CreateWindowWithoutRole() { + xcb_window_t window_id = GenerateId(); + sl_create_window(&ctx, window_id, 0, 0, 800, 600, 0); + sl_window* window = sl_lookup_window(&ctx, window_id); + EXPECT_NE(window, nullptr); + return window; + } + + virtual sl_window* CreateToplevelWindow() { + sl_window* window = CreateWindowWithoutRole(); + + // Pretend we created a frame window too + window->frame_id = GenerateId(); + + window->host_surface_id = xwayland->CreateSurface(); + sl_window_update(window); + Pump(); + return window; + } + + protected: + NiceMock xcb; + std::unique_ptr xwayland; +}; + +} // namespace sommelier +} // namespace vm_tools + +#endif // VM_TOOLS_SOMMELIER_TESTING_X11_TEST_BASE_H_