| // Copyright 2021 The Chromium OS 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 <ctype.h> |
| #include <iostream> |
| #include <string> |
| |
| #include <gtest/gtest.h> |
| #include <gmock/gmock.h> |
| #include <sys/socket.h> |
| #include <wayland-client.h> |
| #include <wayland-util.h> |
| |
| #include "sommelier.h" // NOLINT(build/include_directory) |
| #include "virtualization/wayland_channel.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) |
| |
| // Help gtest print Wayland message streams on expectation failure. |
| // |
| // This is defined in the test file mostly to avoid the main program depending |
| // on <iostream> and <string> 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<uint32_t*>(w.data + i); |
| uint32_t second_word = *reinterpret_cast<uint32_t*>(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<int>(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; |
| |
| class MockWaylandChannel : public WaylandChannel { |
| public: |
| MockWaylandChannel() {} |
| |
| MOCK_METHOD(int32_t, init, ()); |
| MOCK_METHOD(bool, supports_dmabuf, ()); |
| MOCK_METHOD(int32_t, |
| create_context, |
| (int& out_socket_fd)); // NOLINT(runtime/references) |
| MOCK_METHOD(int32_t, |
| create_pipe, |
| (int& out_pipe_fd)); // NOLINT(runtime/references) |
| MOCK_METHOD(int32_t, send, (const struct WaylandSendReceive& send)); |
| 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) |
| |
| MOCK_METHOD(int32_t, |
| allocate, |
| (const struct WaylandBufferCreateInfo& create_info, |
| struct WaylandBufferCreateOutput& |
| create_output)); // NOLINT(runtime/references) |
| MOCK_METHOD(int32_t, sync, (int dmabuf_fd, uint64_t flags)); |
| MOCK_METHOD(int32_t, |
| handle_pipe, |
| (int read_fd, |
| bool readable, |
| bool& hang_up)); // NOLINT(runtime/references) |
| MOCK_METHOD(size_t, max_send_size, ()); |
| |
| 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("exactly one Wayland message ") + |
| (negation ? "not for" : "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<uint32_t*>(send.data); |
| uint32_t second_word = *reinterpret_cast<uint32_t*>(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 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<char*>(send.data + prefix_len), |
| send.data_size - prefix_len); |
| |
| return data_as_str.find(str) != std::string::npos; |
| } |
| |
| // 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); |
| |
| sl_compositor_init_context(&ctx, registry, 0, kMinHostWlCompositorVersion); |
| EXPECT_NE(ctx.compositor, nullptr); |
| |
| // Fake the Wayland server advertising globals. |
| uint32_t id = 1; |
| sl_registry_handler(&ctx, registry, id++, "xdg_wm_base", |
| XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION); |
| sl_registry_handler(&ctx, registry, id++, "zaura_shell", |
| ZAURA_SURFACE_SET_FULLSCREEN_MODE_SINCE_VERSION); |
| } |
| |
| testing::NiceMock<MockWaylandChannel> mock_wayland_channel_; |
| sl_context ctx; |
| }; |
| |
| // Fixture for unit tests which exercise both Wayland and X11 functionality. |
| class X11Test : public WaylandTest { |
| public: |
| void InitContext() override { |
| WaylandTest::InitContext(); |
| ctx.xwayland = 1; |
| } |
| |
| void Connect() override { |
| WaylandTest::Connect(); |
| ctx.connection = xcb_connect(NULL, NULL); |
| } |
| |
| virtual sl_window* CreateWindowWithoutRole() { |
| xcb_window_t window_id = 1; |
| 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(); |
| wl_surface* surface = |
| wl_compositor_create_surface(ctx.compositor->internal); |
| window->host_surface_id = |
| wl_proxy_get_id(reinterpret_cast<wl_proxy*>(surface)); |
| |
| window->xdg_surface = |
| xdg_wm_base_get_xdg_surface(ctx.xdg_shell->internal, surface); |
| window->xdg_toplevel = xdg_surface_get_toplevel(window->xdg_surface); |
| |
| window->aura_surface = |
| zaura_shell_get_aura_surface(ctx.aura_shell->internal, surface); |
| return window; |
| } |
| }; |
| |
| namespace { |
| uint32_t XdgToplevelId(sl_window* window) { |
| return wl_proxy_get_id(reinterpret_cast<wl_proxy*>(window->xdg_toplevel)); |
| } |
| |
| uint32_t AuraSurfaceId(sl_window* window) { |
| return wl_proxy_get_id(reinterpret_cast<wl_proxy*>(window->aura_surface)); |
| } |
| } // namespace |
| |
| TEST_F(WaylandTest, CanCommitToEmptySurface) { |
| wl_surface* surface = wl_compositor_create_surface(ctx.compositor->internal); |
| wl_surface_commit(surface); |
| } |
| |
| TEST_F(X11Test, TogglesFullscreenOnWmStateFullscreen) { |
| // Arrange: Create an xdg_toplevel surface. Initially it's not fullscreen. |
| sl_window* window = CreateToplevelWindow(); |
| uint32_t xdg_toplevel_id = XdgToplevelId(window); |
| EXPECT_EQ(window->fullscreen, 0); |
| Pump(); // exclude pending messages from EXPECT_CALL()s below |
| |
| // Act: Pretend the window is owned by an X11 client requesting fullscreen. |
| // Sommelier receives the XCB_CLIENT_MESSAGE request due to its role as the |
| // X11 window manager. For test purposes, we skip creating a real X11 |
| // connection and just call directly into the relevant handler. |
| xcb_client_message_event_t event; |
| event.response_type = XCB_CLIENT_MESSAGE; |
| event.format = 32; |
| event.window = window->id; |
| event.type = ctx.atoms[ATOM_NET_WM_STATE].value; |
| event.data.data32[0] = NET_WM_STATE_ADD; |
| event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_FULLSCREEN].value; |
| event.data.data32[2] = 0; |
| event.data.data32[3] = 0; |
| event.data.data32[4] = 0; |
| sl_handle_client_message(&ctx, &event); |
| |
| // Assert: Sommelier records the fullscreen state. |
| EXPECT_EQ(window->fullscreen, 1); |
| // Assert: Sommelier forwards the fullscreen request to Exo. |
| EXPECT_CALL( |
| mock_wayland_channel_, |
| send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_FULLSCREEN))) |
| .RetiresOnSaturation(); |
| Pump(); |
| |
| // Act: Pretend the fictitious X11 client requests non-fullscreen. |
| event.data.data32[0] = NET_WM_STATE_REMOVE; |
| sl_handle_client_message(&ctx, &event); |
| |
| // Assert: Sommelier records the fullscreen state. |
| EXPECT_EQ(window->fullscreen, 0); |
| // Assert: Sommelier forwards the unfullscreen request to Exo. |
| EXPECT_CALL( |
| mock_wayland_channel_, |
| send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_UNSET_FULLSCREEN))) |
| .RetiresOnSaturation(); |
| } |
| |
| TEST_F(X11Test, TogglesMaximizeOnWmStateMaximize) { |
| // Arrange: Create an xdg_toplevel surface. Initially it's not maximized. |
| sl_window* window = CreateToplevelWindow(); |
| uint32_t xdg_toplevel_id = XdgToplevelId(window); |
| EXPECT_EQ(window->maximized, 0); |
| Pump(); // exclude pending messages from EXPECT_CALL()s below |
| |
| // Act: Pretend an X11 client owns the surface, and requests to maximize it. |
| xcb_client_message_event_t event; |
| event.response_type = XCB_CLIENT_MESSAGE; |
| event.format = 32; |
| event.window = window->id; |
| event.type = ctx.atoms[ATOM_NET_WM_STATE].value; |
| event.data.data32[0] = NET_WM_STATE_ADD; |
| event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_HORZ].value; |
| event.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_VERT].value; |
| event.data.data32[3] = 0; |
| event.data.data32[4] = 0; |
| sl_handle_client_message(&ctx, &event); |
| |
| // Assert: Sommelier records the maximized state + forwards to Exo. |
| EXPECT_EQ(window->maximized, 1); |
| EXPECT_CALL( |
| mock_wayland_channel_, |
| send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_MAXIMIZED))) |
| .RetiresOnSaturation(); |
| Pump(); |
| |
| // Act: Pretend the fictitious X11 client requests to unmaximize. |
| event.data.data32[0] = NET_WM_STATE_REMOVE; |
| sl_handle_client_message(&ctx, &event); |
| |
| // Assert: Sommelier records the unmaximized state + forwards to Exo. |
| EXPECT_EQ(window->maximized, 0); |
| EXPECT_CALL( |
| mock_wayland_channel_, |
| send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_UNSET_MAXIMIZED))) |
| .RetiresOnSaturation(); |
| Pump(); |
| } |
| |
| TEST_F(X11Test, CanEnterFullscreenIfAlreadyMaximized) { |
| // Arrange |
| sl_window* window = CreateToplevelWindow(); |
| uint32_t xdg_toplevel_id = XdgToplevelId(window); |
| Pump(); // exclude pending messages from EXPECT_CALL()s below |
| |
| // Act: Pretend an X11 client owns the surface, and requests to maximize it. |
| xcb_client_message_event_t event; |
| event.response_type = XCB_CLIENT_MESSAGE; |
| event.format = 32; |
| event.window = window->id; |
| event.type = ctx.atoms[ATOM_NET_WM_STATE].value; |
| event.data.data32[0] = NET_WM_STATE_ADD; |
| event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_HORZ].value; |
| event.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_MAXIMIZED_VERT].value; |
| event.data.data32[3] = 0; |
| event.data.data32[4] = 0; |
| sl_handle_client_message(&ctx, &event); |
| |
| // Assert: Sommelier records the maximized state + forwards to Exo. |
| EXPECT_EQ(window->maximized, 1); |
| EXPECT_CALL( |
| mock_wayland_channel_, |
| send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_MAXIMIZED))) |
| .RetiresOnSaturation(); |
| Pump(); |
| |
| // Act: Pretend the X11 client requests fullscreen. |
| xcb_client_message_event_t fsevent; |
| fsevent.response_type = XCB_CLIENT_MESSAGE; |
| fsevent.format = 32; |
| fsevent.window = window->id; |
| fsevent.type = ctx.atoms[ATOM_NET_WM_STATE].value; |
| fsevent.data.data32[0] = NET_WM_STATE_ADD; |
| fsevent.data.data32[1] = 0; |
| fsevent.data.data32[2] = ctx.atoms[ATOM_NET_WM_STATE_FULLSCREEN].value; |
| fsevent.data.data32[3] = 0; |
| fsevent.data.data32[4] = 0; |
| sl_handle_client_message(&ctx, &fsevent); |
| |
| // Assert: Sommelier records the fullscreen state + forwards to Exo. |
| EXPECT_EQ(window->fullscreen, 1); |
| EXPECT_CALL( |
| mock_wayland_channel_, |
| send(ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_FULLSCREEN))) |
| .RetiresOnSaturation(); |
| Pump(); |
| } |
| |
| TEST_F(X11Test, UpdatesApplicationIdFromContext) { |
| sl_window* window = CreateToplevelWindow(); |
| Pump(); |
| |
| window->managed = 1; // pretend window is mapped |
| // Should be ignored; global app id from context takes priority. |
| window->app_id_property = "org.chromium.appid.from.window"; |
| |
| ctx.application_id = "org.chromium.appid.from.context"; |
| sl_update_application_id(&ctx, window); |
| EXPECT_CALL(mock_wayland_channel_, |
| send(AllOf(ExactlyOneMessage(AuraSurfaceId(window), |
| ZAURA_SURFACE_SET_APPLICATION_ID), |
| AnyMessageContainsString(ctx.application_id)))) |
| .RetiresOnSaturation(); |
| Pump(); |
| } |
| |
| TEST_F(X11Test, UpdatesApplicationIdFromWindow) { |
| sl_window* window = CreateToplevelWindow(); |
| Pump(); |
| |
| window->managed = 1; // pretend window is mapped |
| window->app_id_property = "org.chromium.appid.from.window"; |
| sl_update_application_id(&ctx, window); |
| EXPECT_CALL(mock_wayland_channel_, |
| send(AllOf(ExactlyOneMessage(AuraSurfaceId(window), |
| ZAURA_SURFACE_SET_APPLICATION_ID), |
| AnyMessageContainsString(window->app_id_property)))) |
| .RetiresOnSaturation(); |
| Pump(); |
| } |
| |
| TEST_F(X11Test, UpdatesApplicationIdFromWindowClass) { |
| sl_window* window = CreateToplevelWindow(); |
| Pump(); |
| |
| window->managed = 1; // pretend window is mapped |
| window->clazz = strdup("very_classy"); // not const, can't use a literal |
| ctx.vm_id = "testvm"; |
| sl_update_application_id(&ctx, window); |
| EXPECT_CALL(mock_wayland_channel_, |
| send(AllOf(ExactlyOneMessage(AuraSurfaceId(window), |
| ZAURA_SURFACE_SET_APPLICATION_ID), |
| AnyMessageContainsString( |
| "org.chromium.testvm.wmclass.very_classy")))) |
| .RetiresOnSaturation(); |
| Pump(); |
| free(window->clazz); |
| } |
| |
| TEST_F(X11Test, UpdatesApplicationIdFromClientLeader) { |
| sl_window* window = CreateToplevelWindow(); |
| Pump(); |
| |
| window->managed = 1; // pretend window is mapped |
| window->client_leader = window->id; |
| ctx.vm_id = "testvm"; |
| sl_update_application_id(&ctx, window); |
| EXPECT_CALL(mock_wayland_channel_, |
| send(AllOf(ExactlyOneMessage(AuraSurfaceId(window), |
| ZAURA_SURFACE_SET_APPLICATION_ID), |
| AnyMessageContainsString( |
| "org.chromium.testvm.wmclientleader.")))) |
| .RetiresOnSaturation(); |
| Pump(); |
| } |
| |
| TEST_F(X11Test, UpdatesApplicationIdFromXid) { |
| sl_window* window = CreateToplevelWindow(); |
| Pump(); |
| |
| window->managed = 1; // pretend window is mapped |
| ctx.vm_id = "testvm"; |
| sl_update_application_id(&ctx, window); |
| EXPECT_CALL(mock_wayland_channel_, |
| send(AllOf(ExactlyOneMessage(AuraSurfaceId(window), |
| ZAURA_SURFACE_SET_APPLICATION_ID), |
| AnyMessageContainsString("org.chromium.testvm.xid.")))) |
| .RetiresOnSaturation(); |
| Pump(); |
| } |
| |
| #ifdef BLACK_SCREEN_FIX |
| TEST_F(X11Test, IconifySuppressesStateChanges) { |
| // Arrange: Create an xdg_toplevel surface. Initially it's not iconified. |
| sl_window* window = CreateToplevelWindow(); |
| uint32_t xdg_toplevel_id = XdgToplevelId(window); |
| EXPECT_EQ(window->iconified, 0); |
| |
| // Act: Pretend an X11 client owns the surface, and requests to iconify it. |
| xcb_client_message_event_t event; |
| event.response_type = XCB_CLIENT_MESSAGE; |
| event.format = 32; |
| event.window = window->id; |
| event.type = ctx.atoms[ATOM_WM_CHANGE_STATE].value; |
| event.data.data32[0] = WM_STATE_ICONIC; |
| sl_handle_client_message(&ctx, &event); |
| Pump(); |
| |
| // Assert: Sommelier records the iconified state. |
| EXPECT_EQ(window->iconified, 1); |
| |
| // Act: Pretend the surface is requested to be fullscreened. |
| event.type = ctx.atoms[ATOM_NET_WM_STATE].value; |
| event.data.data32[0] = NET_WM_STATE_ADD; |
| event.data.data32[1] = ctx.atoms[ATOM_NET_WM_STATE_FULLSCREEN].value; |
| event.data.data32[2] = 0; |
| event.data.data32[3] = 0; |
| event.data.data32[4] = 0; |
| sl_handle_client_message(&ctx, &event); |
| |
| // Assert: Sommelier should not send the fullscreen call as we are iconified. |
| EXPECT_CALL( |
| mock_wayland_channel_, |
| send((ExactlyOneMessage(xdg_toplevel_id, XDG_TOPLEVEL_SET_FULLSCREEN)))) |
| .Times(0); |
| Pump(); |
| |
| // Act: Pretend the surface receives focus. |
| xcb_focus_in_event_t focus_event; |
| focus_event.response_type = XCB_FOCUS_IN; |
| focus_event.event = window->id; |
| sl_handle_focus_in(&ctx, &focus_event); |
| |
| // Assert: The window is deiconified. |
| EXPECT_EQ(window->iconified, 0); |
| } |
| #endif |
| |
| } // 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(); |
| } |