diff --git a/BUILD.gn b/BUILD.gn index a703451..84ea356 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -174,9 +174,11 @@ if (use.test) { "sommelier-test.cc", "sommelier-transform-test.cc", "sommelier-window-test.cc", + "sommelier-x11event-test.cc", "sommelier-xdg-shell-test.cc", "testing/mock-wayland-channel.cc", "testing/sommelier-test-util.cc", + "xcb/fake-xcb-shim.cc", ] include_dirs = [ "testing" ] diff --git a/meson.build b/meson.build index 6198ab0..f4c36ae 100644 --- a/meson.build +++ b/meson.build @@ -221,9 +221,11 @@ if get_option('with_tests') 'sommelier-transform-test.cc', 'sommelier-output-test.cc', 'sommelier-window-test.cc', + 'sommelier-x11event-test.cc', 'sommelier-xdg-shell-test.cc', 'testing/mock-wayland-channel.cc', 'testing/sommelier-test-util.cc', + 'xcb/fake-xcb-shim.cc', ] + wl_outs + shim_outs + gamepad_testing, link_with: libsommelier, dependencies: [ diff --git a/sommelier-output-test.cc b/sommelier-output-test.cc index fc6ddc8..641015a 100644 --- a/sommelier-output-test.cc +++ b/sommelier-output-test.cc @@ -167,7 +167,7 @@ TEST_F(X11Test, OutputsPositionedCorrectlyAfterRemovingLeftOutput) { EXPECT_EQ(output->virt_y, 0); // outputs has length 2. - EXPECT_EQ(ctx.host_outputs.size(), 2); + EXPECT_EQ(ctx.host_outputs.size(), 2u); } TEST_F(X11Test, OutputsPositionedCorrectlyAfterRemovingMiddleOutput) { @@ -201,7 +201,7 @@ TEST_F(X11Test, OutputsPositionedCorrectlyAfterRemovingMiddleOutput) { EXPECT_EQ(output->virt_y, 0); // outputs has length 2. - EXPECT_EQ(ctx.host_outputs.size(), 2); + EXPECT_EQ(ctx.host_outputs.size(), 2u); } TEST_F(X11Test, OtherOutputUnchangedAfterRemovingRightOutput) { @@ -222,7 +222,7 @@ TEST_F(X11Test, OtherOutputUnchangedAfterRemovingRightOutput) { EXPECT_EQ(output->virt_x, 0); EXPECT_EQ(output->virt_y, 0); // outputs has length 1. - EXPECT_EQ(ctx.host_outputs.size(), 1); + EXPECT_EQ(ctx.host_outputs.size(), 1u); } TEST_F(X11Test, RotatingOutputsShiftsNeighbouringOutputs) { diff --git a/sommelier-x11event-test.cc b/sommelier-x11event-test.cc new file mode 100644 index 0000000..023a946 --- /dev/null +++ b/sommelier-x11event-test.cc @@ -0,0 +1,113 @@ +// 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 "testing/x11-test-base.h" +#include "xcb/fake-xcb-shim.h" + +namespace vm_tools { +namespace sommelier { + +using X11EventTest = X11TestBase; + +TEST_F(X11EventTest, MapRequestCreatesFrameWindow) { + sl_window* window = CreateWindowWithoutRole(); + xcb_map_request_event_t event; + event.response_type = XCB_MAP_REQUEST; + event.window = window->id; + EXPECT_EQ(window->frame_id, XCB_WINDOW_NONE); + + EXPECT_CALL(xcb, generate_id).WillOnce(testing::Return(456)); + sl_handle_map_request(&ctx, &event); + + EXPECT_EQ(window->frame_id, 456u); +} + +TEST_F(X11EventTest, MapRequestIssuesMapWindow) { + sl_window* window = CreateWindowWithoutRole(); + xcb_map_request_event_t event; + event.response_type = XCB_MAP_REQUEST; + event.window = window->id; + + EXPECT_CALL(xcb, generate_id).WillOnce(testing::Return(456)); + EXPECT_CALL(xcb, map_window(testing::_, window->id)).Times(1); + EXPECT_CALL(xcb, map_window(testing::_, 456u)).Times(1); + + sl_handle_map_request(&ctx, &event); +} + +TEST_F(X11EventTest, MapRequestGetsWmName) { + std::string windowName("Fred"); + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, windowName.size(), + windowName.c_str()); + EXPECT_EQ(window->name, nullptr); + + xcb_map_request_event_t event; + event.response_type = XCB_MAP_REQUEST; + event.window = window->id; + sl_handle_map_request(&ctx, &event); + + EXPECT_EQ(window->name, windowName); +} + +TEST_F(X11EventTest, ListensToWmNameChanges) { + std::string windowName("Fred"); + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, windowName.size(), + windowName.c_str()); + + xcb_property_notify_event_t event; + event.response_type = XCB_PROPERTY_NOTIFY; + event.window = window->id; + event.atom = XCB_ATOM_WM_NAME; + event.state = XCB_PROPERTY_NEW_VALUE; + sl_handle_property_notify(&ctx, &event); + + EXPECT_EQ(window->name, windowName); +} + +TEST_F(X11EventTest, NetWmNameOverridesWmname) { + std::string boringWindowName("Fred"); + std::string fancyWindowName("I ♥️ Unicode 🦄🌈"); + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, + boringWindowName.size(), boringWindowName.c_str()); + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + ctx.atoms[ATOM_NET_WM_NAME].value, XCB_ATOM_STRING, 8, + fancyWindowName.size(), fancyWindowName.c_str()); + + xcb_property_notify_event_t event; + event.response_type = XCB_PROPERTY_NOTIFY; + event.window = window->id; + event.atom = XCB_ATOM_WM_NAME; + event.state = XCB_PROPERTY_NEW_VALUE; + sl_handle_property_notify(&ctx, &event); + + event.atom = ctx.atoms[ATOM_NET_WM_NAME].value; + sl_handle_property_notify(&ctx, &event); + + EXPECT_EQ(window->name, fancyWindowName); +} + +} // namespace sommelier +} // namespace vm_tools diff --git a/testing/x11-test-base.h b/testing/x11-test-base.h index 9b3e32f..0a5884c 100644 --- a/testing/x11-test-base.h +++ b/testing/x11-test-base.h @@ -20,6 +20,10 @@ class X11TestBase : public WaylandTestBase { WaylandTestBase::InitContext(); ctx.xwayland = 1; + // Always delegate ID generation to the fake XCB shim, even for test cases + // that never use the fake for anything else. This prevents ID collisions. + xcb.DelegateIdGenerationToFake(); + // 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). @@ -56,13 +60,8 @@ class X11TestBase : public WaylandTestBase { ~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(); + xcb_window_t window_id = xcb.generate_id(ctx.connection); sl_create_window(&ctx, window_id, 0, 0, 800, 600, 0); sl_window* window = sl_lookup_window(&ctx, window_id); EXPECT_NE(window, nullptr); @@ -73,7 +72,7 @@ class X11TestBase : public WaylandTestBase { sl_window* window = CreateWindowWithoutRole(); // Pretend we created a frame window too - window->frame_id = GenerateId(); + window->frame_id = xcb.generate_id(ctx.connection); window->host_surface_id = SurfaceId(xwayland->CreateSurface()); sl_window_update(window); diff --git a/xcb/fake-xcb-shim.cc b/xcb/fake-xcb-shim.cc new file mode 100644 index 0000000..30391bc --- /dev/null +++ b/xcb/fake-xcb-shim.cc @@ -0,0 +1,191 @@ +// 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 "fake-xcb-shim.h" // NOLINT(build/include_directory) + +xcb_connection_t* FakeXcbShim::connect(const char* displayname, int* screenp) { + return nullptr; +} + +uint32_t FakeXcbShim::generate_id(xcb_connection_t* c) { + return next_id_++; +} + +xcb_void_cookie_t FakeXcbShim::create_window(xcb_connection_t* c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t border_width, + uint16_t _class, + xcb_visualid_t visual, + uint32_t value_mask, + const void* value_list) { + std::pair::iterator, bool> + result = windows_.try_emplace(wid, FakeWindow{ + .depth = depth, + .wid = wid, + .parent = parent, + .x = x, + .y = y, + .width = width, + .height = height, + .border_width = border_width, + ._class = _class, + .visual = visual, + }); + // Window should not already exist. + EXPECT_TRUE(result.second); + return {}; +} + +xcb_void_cookie_t FakeXcbShim::reparent_window(xcb_connection_t* c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y) { + windows_.at(window).parent = parent; + return {}; +} + +xcb_void_cookie_t FakeXcbShim::map_window(xcb_connection_t* c, + xcb_window_t window) { + windows_.at(window).mapped = true; + return {}; +} + +xcb_void_cookie_t FakeXcbShim::configure_window(xcb_connection_t* c, + xcb_window_t window, + uint16_t value_mask, + const void* value_list) { + ADD_FAILURE() << "unimplemented"; + return {}; +} + +xcb_void_cookie_t FakeXcbShim::change_property(xcb_connection_t* c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t data_len, + const void* data) { + // prepend/append not implemented + EXPECT_EQ(mode, XCB_PROP_MODE_REPLACE); + + // The real API returns BadWindow errors if an invalid window ID is passed, + // but throwing an exception is sufficient for the purposes of our tests. + uint32_t bytes_per_element = format / 8; + windows_.at(window).properties_[property] = FakeProperty{ + .type = type, + .format = format, + .data = std::vector( + (unsigned char*)data, + (unsigned char*)data + data_len * bytes_per_element), + }; + return {}; +} + +xcb_void_cookie_t FakeXcbShim::send_event(xcb_connection_t* c, + uint8_t propagate, + xcb_window_t destination, + uint32_t event_mask, + const char* event) { + ADD_FAILURE() << "unimplemented"; + return {}; +} + +xcb_void_cookie_t FakeXcbShim::change_window_attributes( + xcb_connection_t* c, + xcb_window_t window, + uint32_t value_mask, + const void* value_list) { + ADD_FAILURE() << "unimplemented"; + return {}; +} + +xcb_get_geometry_cookie_t FakeXcbShim::get_geometry(xcb_connection_t* c, + xcb_drawable_t drawable) { + ADD_FAILURE() << "unimplemented"; + return {}; +} + +xcb_get_geometry_reply_t* FakeXcbShim::get_geometry_reply( + xcb_connection_t* c, + xcb_get_geometry_cookie_t cookie, + xcb_generic_error_t** e) { + ADD_FAILURE() << "unimplemented"; + return {}; +} + +xcb_get_property_cookie_t FakeXcbShim::get_property(xcb_connection_t* c, + uint8_t _delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t long_offset, + uint32_t long_length) { + xcb_get_property_cookie_t cookie; + cookie.sequence = next_cookie_++; + + PropertyRequestData request; + request.window = window; + request.property = property; + + property_requests_[cookie.sequence] = request; + return cookie; +} + +xcb_get_property_reply_t* FakeXcbShim::get_property_reply( + xcb_connection_t* c, + xcb_get_property_cookie_t cookie, + xcb_generic_error_t** e) { + auto w = windows_.at(property_requests_[cookie.sequence].window); + xcb_atom_t property = property_requests_[cookie.sequence].property; + auto it = w.properties_.find(property); + + // The caller is expected to call free() on the return value, so we must use + // malloc() here. + xcb_get_property_reply_t* reply = static_cast( + malloc(sizeof(xcb_get_property_reply_t))); + EXPECT_TRUE(reply); + if (it == w.properties_.end()) { + reply->format = 0; + reply->sequence = 0; + reply->type = 0; + reply->length = 0; + } else { + reply->format = it->second.format; + reply->sequence = cookie.sequence; + reply->type = it->second.type; + reply->length = it->second.data.size(); + } + return reply; +} + +void* FakeXcbShim::get_property_value(const xcb_get_property_reply_t* r) { + if (!r->sequence) { + // Property was not found. This isn't an error case, just return null. + return nullptr; + } + const FakeProperty& property = + windows_.at(property_requests_[r->sequence].window) + .properties_.at(property_requests_[r->sequence].property); + void* buffer = malloc(property.data.size()); + EXPECT_TRUE(buffer); + memcpy(buffer, property.data.data(), property.data.size()); + return buffer; +} + +int FakeXcbShim::get_property_value_length(const xcb_get_property_reply_t* r) { + return r->length; +} diff --git a/xcb/fake-xcb-shim.h b/xcb/fake-xcb-shim.h new file mode 100644 index 0000000..a36c2be --- /dev/null +++ b/xcb/fake-xcb-shim.h @@ -0,0 +1,131 @@ +// 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_XCB_FAKE_XCB_SHIM_H_ +#define VM_TOOLS_SOMMELIER_XCB_FAKE_XCB_SHIM_H_ + +#include +#include + +#include "xcb-shim.h" // NOLINT(build/include_directory) + +class FakeProperty { + public: + xcb_atom_t type; + uint8_t format; + std::vector data; +}; + +class FakeWindow { + public: + uint8_t depth; + xcb_window_t wid; + xcb_window_t parent; + int16_t x; + int16_t y; + uint16_t width; + uint16_t height; + uint16_t border_width; + uint16_t _class; + bool mapped = false; + xcb_visualid_t visual; + + std::unordered_map properties_; +}; + +// Metadata of a `xcb_get_property` request, stored so we can look up +// the requested information for get_property_reply, get_property_value, +// get_property_value_length. +class PropertyRequestData { + public: + xcb_window_t window; + xcb_atom_t property; +}; + +// Partial fake of the XCB API. +// +// Supports some very basic window management and property getting/setting. +// Some methods are unimplemented and will assert if called. +class FakeXcbShim : public XcbShim { + public: + xcb_connection_t* connect(const char* displayname, int* screenp) override; + uint32_t generate_id(xcb_connection_t* c) override; + xcb_void_cookie_t create_window(xcb_connection_t* c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t border_width, + uint16_t _class, + xcb_visualid_t visual, + uint32_t value_mask, + const void* value_list) override; + xcb_void_cookie_t reparent_window(xcb_connection_t* c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y) override; + xcb_void_cookie_t map_window(xcb_connection_t* c, + xcb_window_t window) override; + xcb_void_cookie_t configure_window(xcb_connection_t* c, + xcb_window_t window, + uint16_t value_mask, + const void* value_list) override; + xcb_void_cookie_t change_property(xcb_connection_t* c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t data_len, + const void* data) override; + xcb_void_cookie_t send_event(xcb_connection_t* c, + uint8_t propagate, + xcb_window_t destination, + uint32_t event_mask, + const char* event) override; + xcb_void_cookie_t change_window_attributes(xcb_connection_t* c, + xcb_window_t window, + uint32_t value_mask, + const void* value_list) override; + xcb_get_geometry_cookie_t get_geometry(xcb_connection_t* c, + xcb_drawable_t drawable) override; + xcb_get_geometry_reply_t* get_geometry_reply( + xcb_connection_t* c, + xcb_get_geometry_cookie_t cookie, + xcb_generic_error_t** e) override; + xcb_get_property_cookie_t get_property(xcb_connection_t* c, + uint8_t _delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t long_offset, + uint32_t long_length) override; + xcb_get_property_reply_t* get_property_reply( + xcb_connection_t* c, + xcb_get_property_cookie_t cookie, + xcb_generic_error_t** e) override; + void* get_property_value(const xcb_get_property_reply_t* r) override; + int get_property_value_length(const xcb_get_property_reply_t* r) override; + + private: + uint32_t next_id_ = 1; + + // Keep track of windows and their properties in the faked X11 environment. + std::unordered_map windows_; + + // State tracking for X11 property requests. Each call to `get_property` + // returns a xcb_get_property_cookie_t, whose `sequence` member is set to an + // incrementing ID number. The metadata of the property request is stored in + // this map associated to that ID, so we can retrieve it later. + std::unordered_map property_requests_; + + // ID to assign to the next xcb_get_property_cookie_t we allocate. + unsigned int next_cookie_ = 1; +}; + +#endif // VM_TOOLS_SOMMELIER_XCB_FAKE_XCB_SHIM_H_ diff --git a/xcb/mock-xcb-shim.h b/xcb/mock-xcb-shim.h index e83ba61..e7ba658 100644 --- a/xcb/mock-xcb-shim.h +++ b/xcb/mock-xcb-shim.h @@ -7,7 +7,8 @@ #include -#include "xcb-shim.h" // NOLINT(build/include_directory) +#include "fake-xcb-shim.h" // NOLINT(build/include_directory) +#include "xcb-shim.h" // NOLINT(build/include_directory) class MockXcbShim : public XcbShim { public: @@ -125,6 +126,78 @@ class MockXcbShim : public XcbShim { get_property_value_length, (const xcb_get_property_reply_t* r), (override)); + + // It's best to centralize ID generation in the fake, even for test cases + // that never use the fake for anything else. This prevents ID collisions. + void DelegateIdGenerationToFake() { + ON_CALL(*this, generate_id).WillByDefault([this](xcb_connection_t* c) { + return fake_.generate_id(c); + }); + } + + // Some interactions with XCB, such as getting properties, are too complex to + // mock. In this case we can delegate to a fake to get semi-realistic + // behaviour. + // + // TODO(cpelling): Build a complete X11 fake instead. + void DelegateToFake() { + ON_CALL(*this, generate_id).WillByDefault([this](xcb_connection_t* c) { + return fake_.generate_id(c); + }); + ON_CALL(*this, create_window) + .WillByDefault([this](xcb_connection_t* c, uint8_t depth, + xcb_window_t wid, xcb_window_t parent, int16_t x, + int16_t y, uint16_t width, uint16_t height, + uint16_t border_width, uint16_t _class, + xcb_visualid_t visual, uint32_t value_mask, + const void* value_list) { + return fake_.create_window(c, depth, wid, parent, x, y, width, height, + border_width, _class, visual, value_mask, + value_list); + }); + ON_CALL(*this, reparent_window) + .WillByDefault([this](xcb_connection_t* c, xcb_window_t window, + xcb_window_t parent, int16_t x, int16_t y) { + return fake_.reparent_window(c, window, parent, x, y); + }); + ON_CALL(*this, map_window) + .WillByDefault([this](xcb_connection_t* c, xcb_window_t window) { + return fake_.map_window(c, window); + }); + ON_CALL(*this, change_property) + .WillByDefault([this](xcb_connection_t* c, uint8_t mode, + xcb_window_t window, xcb_atom_t property, + xcb_atom_t type, uint8_t format, + uint32_t data_len, const void* data) { + return fake_.change_property(c, mode, window, property, type, format, + data_len, data); + }); + ON_CALL(*this, get_property) + .WillByDefault([this](xcb_connection_t* c, uint8_t _delete, + xcb_window_t window, xcb_atom_t property, + xcb_atom_t type, uint32_t long_offset, + uint32_t long_length) { + return fake_.get_property(c, _delete, window, property, type, + long_offset, long_length); + }); + ON_CALL(*this, get_property_reply) + .WillByDefault([this](xcb_connection_t* c, + xcb_get_property_cookie_t cookie, + xcb_generic_error_t** e) { + return fake_.get_property_reply(c, cookie, e); + }); + ON_CALL(*this, get_property_value) + .WillByDefault([this](const xcb_get_property_reply_t* r) { + return fake_.get_property_value(r); + }); + ON_CALL(*this, get_property_value_length) + .WillByDefault([this](const xcb_get_property_reply_t* r) { + return fake_.get_property_value_length(r); + }); + } + + private: + FakeXcbShim fake_; }; #endif // VM_TOOLS_SOMMELIER_XCB_MOCK_XCB_SHIM_H_