chromium/components/exo/wayland/wayland_protocol_logger_unittest.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/exo/wayland/wayland_protocol_logger.h"

#include <wayland-server-core.h>
#include <wayland-util.h>
#include <xdg-shell-client-protocol.h>
#include <xdg-shell-server-protocol.h>

#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "components/exo/wayland/test/client_util.h"
#include "components/exo/wayland/test/resource_key.h"
#include "components/exo/wayland/test/server_util.h"
#include "components/exo/wayland/test/wayland_server_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace exo::wayland {
namespace {

class WaylandProtocolLoggerTest : public test::WaylandServerTest {
 public:
  WaylandProtocolLoggerTest() = default;
  WaylandProtocolLoggerTest(const WaylandProtocolLoggerTest&) = delete;
  WaylandProtocolLoggerTest& operator=(const WaylandProtocolLoggerTest&) =
      delete;
  ~WaylandProtocolLoggerTest() override = default;

  // test::WaylandServerTest:
  void SetUp() override {
    WaylandProtocolLogger::SetHandlerFuncForTesting(
        [](void* user_data, wl_protocol_logger_type type,
           const wl_protocol_logger_message* message) {
          messages_.push_back(
              WaylandProtocolLogger::FormatMessage(type, message));
        });
    test::WaylandServerTest::SetUp();

    // Ignore messages from this test case's setup phase.
    messages_.clear();
  }

  // test::WaylandServerTest:
  void TearDown() override {
    messages_.clear();
    test::WaylandServerTest::TearDown();
  }

  static std::vector<std::vector<std::string>> messages_;
};

std::vector<std::vector<std::string>> WaylandProtocolLoggerTest::messages_ = {};

class ClientData : public test::TestClient::CustomData {
 public:
  ClientData()
      : callback(nullptr, &wl_callback_destroy),
        data_source(nullptr, &wl_data_source_destroy),
        pointer(nullptr, &wl_pointer_destroy),
        surface(nullptr, &wl_surface_destroy),
        xdg_surface(nullptr, &xdg_surface_destroy),
        xdg_toplevel(nullptr, &xdg_toplevel_destroy) {}
  ~ClientData() override = default;

  std::unique_ptr<wl_callback, decltype(&wl_callback_destroy)> callback;
  std::unique_ptr<wl_data_source, decltype(&wl_data_source_destroy)>
      data_source;
  std::unique_ptr<wl_pointer, decltype(&wl_pointer_destroy)> pointer;
  std::unique_ptr<wl_surface, decltype(&wl_surface_destroy)> surface;
  std::unique_ptr<xdg_surface, decltype(&xdg_surface_destroy)> xdg_surface;
  std::unique_ptr<xdg_toplevel, decltype(&xdg_toplevel_destroy)> xdg_toplevel;
};

std::string ProxyId(void* proxy) {
  return base::NumberToString(
      wl_proxy_get_id(reinterpret_cast<wl_proxy*>(proxy)));
}

void AddState(wl_array* states, xdg_toplevel_state state) {
  xdg_toplevel_state* value = static_cast<xdg_toplevel_state*>(
      wl_array_add(states, sizeof(xdg_toplevel_state)));
  DCHECK(value);
  *value = state;
}

}  // namespace

TEST_F(WaylandProtocolLoggerTest, LogsBasicRequest) {
  std::string surface_id;
  std::string compositor_id;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->set_data(std::make_unique<ClientData>());
    data->surface.reset(wl_compositor_create_surface(client->compositor()));
    compositor_id = ProxyId(client->compositor());
    surface_id = ProxyId(data->surface.get());
  });

  EXPECT_THAT(messages_,
              ::testing::Contains(::testing::ElementsAre(
                  base::StrCat({"Received request: wl_compositor@",
                                compositor_id, ".create_surface"}),
                  base::StrCat({"new id wl_surface@", surface_id}))));
}

TEST_F(WaylandProtocolLoggerTest, LogsBasicEvent) {
  std::string callback_id;

  EXPECT_THAT(messages_, ::testing::IsEmpty());

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->set_data(std::make_unique<ClientData>());
    data->callback.reset(wl_display_sync(client->display()));
    callback_id = ProxyId(data->callback.get());
  });

  EXPECT_THAT(messages_,
              ::testing::Contains(::testing::Contains(base::StrCat(
                  {"Sent event: wl_callback@", callback_id, ".done"}))));
}

TEST_F(WaylandProtocolLoggerTest, LogsObjects) {
  std::string xdg_wm_base_id;
  std::string xdg_surface_id;
  std::string surface_id;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->set_data(std::make_unique<ClientData>());
    xdg_wm_base_id = ProxyId(client->xdg_wm_base());
    data->surface.reset(wl_compositor_create_surface(client->compositor()));
    data->xdg_surface.reset(xdg_wm_base_get_xdg_surface(client->xdg_wm_base(),
                                                        data->surface.get()));
    surface_id = ProxyId(data->surface.get());
    xdg_surface_id = ProxyId(data->xdg_surface.get());
  });

  EXPECT_THAT(messages_,
              ::testing::Contains(::testing::ElementsAre(
                  base::StrCat({"Received request: xdg_wm_base@",
                                xdg_wm_base_id, ".get_xdg_surface"}),
                  base::StrCat({"new id xdg_surface@", xdg_surface_id}),
                  base::StrCat({"wl_surface@", surface_id}))));
}

TEST_F(WaylandProtocolLoggerTest, LogsStrings) {
  constexpr const char* window_title = "🦄🌈 I ♥️ UTF-8 😋";
  std::string xdg_toplevel_id;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->set_data(std::make_unique<ClientData>());
    data->surface.reset(wl_compositor_create_surface(client->compositor()));
    data->xdg_surface.reset(xdg_wm_base_get_xdg_surface(client->xdg_wm_base(),
                                                        data->surface.get()));
    data->xdg_toplevel.reset(xdg_surface_get_toplevel(data->xdg_surface.get()));
    xdg_toplevel_set_title(data->xdg_toplevel.get(), window_title);
    xdg_toplevel_id = ProxyId(data->xdg_toplevel.get());
  });

  EXPECT_THAT(messages_, ::testing::Contains(::testing::ElementsAre(
                             base::StrCat({"Received request: xdg_toplevel@",
                                           xdg_toplevel_id, ".set_title"}),
                             window_title)));
}

// TODO(b/335767378): triggers dangling pointer detection.
TEST_F(WaylandProtocolLoggerTest, DISABLED_LogsObjectsAndStrings) {
  std::string display_id;
  constexpr uint32_t fake_error_code = 0x12345678u;
  constexpr const char* fake_error = "🦄🌈 Fake error 😋";
  EXPECT_THAT(messages_, ::testing::IsEmpty());

  PostToClientAndWait([&](test::TestClient* client) {
    wl_resource_post_error(test::server_util::LookUpResource(
                               server_.get(), test::client_util::GetResourceKey(
                                                  client->display())),
                           fake_error_code, "%s", fake_error);
    display_id = ProxyId(client->display());
  });

  EXPECT_THAT(
      messages_,
      ::testing::Contains(::testing::ElementsAre(
          base::StrCat({"Sent event: wl_display@", display_id, ".error"}),
          base::StrCat({"wl_display@", display_id}),
          base::NumberToString(fake_error_code), fake_error)));
}

TEST_F(WaylandProtocolLoggerTest, LogsFileDescriptors) {
  std::string data_source_id;
  test::ResourceKey data_source_resource_key;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->set_data(std::make_unique<ClientData>());
    data->data_source.reset(wl_data_device_manager_create_data_source(
        client->data_device_manager()));
    data_source_id = ProxyId(data->data_source.get());
    data_source_resource_key =
        test::client_util::GetResourceKey(data->data_source.get());
  });

  wl_resource* data_source_resource = test::server_util::LookUpResource(
      server_.get(), data_source_resource_key);
  EXPECT_NE(data_source_resource, nullptr);

  // Actually sending a wl_data_source::send event requires a valid file
  // descriptor, which requires some ceremony to set up properly.
  // Instead, call the message formatting utility directly.
  const struct wl_interface* arg_types[2] = {nullptr, nullptr};
  wl_message message = {
      .name = "send", .signature = "sh", .types = &arg_types[0]};
  wl_argument args[] = {{.s = "text/plain"}, {.h = 12345}};
  wl_protocol_logger_message logger_message = {
      .resource = data_source_resource,
      .message_opcode = WL_DATA_SOURCE_SEND,
      .message = &message,
      .arguments_count = 2,
      .arguments = &args[0],
  };

  auto result = WaylandProtocolLogger::FormatMessage(WL_PROTOCOL_LOGGER_EVENT,
                                                     &logger_message);
  EXPECT_THAT(result, ::testing::ElementsAre(
                          base::StrCat({"Sent event: wl_data_source@",
                                        data_source_id, ".send"}),
                          "text/plain", "fd 12345"));
}

TEST_F(WaylandProtocolLoggerTest, LogsFixedPointNumbers) {
  // Arrange: Create a wl_pointer client object, wait for server to receive.
  std::string pointer_id;
  test::ResourceKey pointer_resource_key;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->set_data(std::make_unique<ClientData>());
    data->pointer.reset(wl_seat_get_pointer(client->seat()));
    pointer_id = ProxyId(data->pointer.get());
    pointer_resource_key =
        test::client_util::GetResourceKey(data->pointer.get());
  });

  // Act: Send a motion event
  wl_resource* pointer_resource =
      test::server_util::LookUpResource(server_.get(), pointer_resource_key);
  EXPECT_NE(pointer_resource, nullptr);

  wl_pointer_send_motion(pointer_resource, 123, wl_fixed_from_double(1234.5),
                         wl_fixed_from_double(0));

  // Assert: Motion event correctly logged
  EXPECT_THAT(
      messages_,
      ::testing::Contains(::testing::ElementsAre(
          base::StrCat({"Sent event: wl_pointer@", pointer_id, ".motion"}),
          "123", "1234.5", "0")));
}

TEST_F(WaylandProtocolLoggerTest, LogsArrays) {
  std::string xdg_toplevel_id;
  test::ResourceKey xdg_toplevel_resource_key;

  PostToClientAndWait([&](test::TestClient* client) {
    auto* data = client->set_data(std::make_unique<ClientData>());
    data->surface.reset(wl_compositor_create_surface(client->compositor()));
    data->xdg_surface.reset(xdg_wm_base_get_xdg_surface(client->xdg_wm_base(),
                                                        data->surface.get()));
    data->xdg_toplevel.reset(xdg_surface_get_toplevel(data->xdg_surface.get()));
    xdg_toplevel_id = ProxyId(data->xdg_toplevel.get());
    xdg_toplevel_resource_key =
        test::client_util::GetResourceKey(data->xdg_toplevel.get());
  });

  wl_resource* xdg_toplevel_resource = test::server_util::LookUpResource(
      server_.get(), xdg_toplevel_resource_key);
  EXPECT_NE(xdg_toplevel_resource, nullptr);

  // Act: Send a typical configure event.
  {
    messages_.clear();
    wl_array states;
    wl_array_init(&states);
    AddState(&states, XDG_TOPLEVEL_STATE_ACTIVATED);
    AddState(&states, XDG_TOPLEVEL_STATE_FULLSCREEN);
    xdg_toplevel_send_configure(xdg_toplevel_resource, 1024, 768, &states);
    wl_array_release(&states);
  }

  // Assert: Short arrays print correctly.
  EXPECT_THAT(messages_,
              ::testing::Contains(::testing::ElementsAre(
                  base::StrCat({"Sent event: xdg_toplevel@", xdg_toplevel_id,
                                ".configure"}),
                  "1024", "768", "array[8 bytes]{0400000002000000}")));

  // Act: Send a very long array.
  {
    messages_.clear();
    wl_array states;
    wl_array_init(&states);
    for (int i = 0; i < 14; i++) {
      AddState(&states, XDG_TOPLEVEL_STATE_ACTIVATED);
    }
    xdg_toplevel_send_configure(xdg_toplevel_resource, 1024, 768, &states);
    wl_array_release(&states);
  }

  // Assert: Long arrays truncate as expected.
  EXPECT_THAT(messages_,
              ::testing::Contains(::testing::ElementsAre(
                  base::StrCat({"Sent event: xdg_toplevel@", xdg_toplevel_id,
                                ".configure"}),
                  "1024", "768",
                  "array[56 bytes]"
                  "{04000000040000000400000004000000040000000400000004000000"
                  "0400000004000000040000000400000004000000...}")));
}

}  // namespace exo::wayland