diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 014e42314..e45c3fc70 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,9 +25,11 @@ excluding_any("wayland" IF NOT BUILD_WAYLAND) # Mocking works because it's linked before conky_core, so the linker uses mock # implementations instead of those that are linked later. -add_library(conky-mock OBJECT ${mock_sources}) add_library(Catch2 STATIC catch2/catch_amalgamated.cpp) +add_library(conky-mock OBJECT ${mock_sources}) +target_link_libraries(conky-mock Catch2) + add_executable(test-conky test-common.cc ${test_sources}) target_include_directories(test-conky PUBLIC diff --git a/tests/mock/mock.hh b/tests/mock/mock.hh index a5cda413e..e5bf538da 100644 --- a/tests/mock/mock.hh +++ b/tests/mock/mock.hh @@ -33,8 +33,10 @@ std::string debug_format(const std::string& format, Args... args) { /// This is intended for cases where some library function is internally invoked /// but would fail if conditions only present at runtime aren't met. struct state_change { - public: virtual ~state_change() = default; + + static std::string change_name() { return "state_change"; } + /// Returns a string representation of this state change with information /// necessary to differentiate it from other variants of the same type. virtual std::string debug() = 0; @@ -70,7 +72,7 @@ std::optional next_state_change_t() { } // namespace mock /// A variant of `mock::next_state_change_t` that integrates into Catch2. -/// It's a macro because using `FAIL` outside of a test doesn't work. +/// It's a macro because using `FAIL` outside of a test doesn't compile. #define EXPECT_NEXT_CHANGE(T) \ []() { \ static_assert(std::is_base_of_v, \ diff --git a/tests/mock/x11-mock.hh b/tests/mock/x11-mock.hh index 0eb27ead5..a56f62879 100644 --- a/tests/mock/x11-mock.hh +++ b/tests/mock/x11-mock.hh @@ -1,42 +1,337 @@ #ifndef X11_MOCK_HH #define X11_MOCK_HH +#include +#include +#include +#include +#include #include +#include + #include "mock.hh" namespace mock { + +enum x11_property_type { + ARC = XA_ARC, + ATOM = XA_ATOM, + BITMAP = XA_BITMAP, + CARDINAL = XA_CARDINAL, + COLORMAP = XA_COLORMAP, + CURSOR = XA_CURSOR, + DRAWABLE = XA_DRAWABLE, + FONT = XA_FONT, + INTEGER = XA_INTEGER, + PIXMAP = XA_PIXMAP, + POINT = XA_POINT, + RGB_COLOR_MAP = XA_RGB_COLOR_MAP, + RECTANGLE = XA_RECTANGLE, + STRING = XA_STRING, + VISUALID = XA_VISUALID, + WINDOW = XA_WINDOW, + WM_HINTS = XA_WM_HINTS, + WM_SIZE_HINTS = XA_WM_SIZE_HINTS, +}; + +Atom name_to_atom(const char *name); +const std::string_view atom_to_name(Atom atom); + +/// Mutation produced by creating new `Atom`s. struct x11_define_atom : public state_change { std::string name; x11_define_atom(std::string name) : name(name) {} + static std::string change_name() { return "x11_define_atom"; } + std::string debug() { return debug_format("x11_define_atom { name: \"%s\" }", name.c_str()); } }; -struct x11_change_property : public state_change { - std::string property; - std::string type; - int format; - int mode; - const unsigned char *data; - size_t element_count; - - x11_change_property(std::string property, std::string type, int format, - int mode, const unsigned char *data, size_t element_count) - : property(property), - type(type), - format(format), - mode(mode), - data(data) {} +enum class set_property_mode { + REPLACE = PropModeReplace, + PREPEND = PropModePrepend, + APPEND = PropModeAppend, +}; + +/// Mutation produced by calls to XChangeProperty. +class x11_change_property : public state_change { + Atom m_property; + Atom m_type; + std::size_t m_format; + set_property_mode m_mode; + const unsigned char *m_data; + std::size_t m_element_count; + + public: + x11_change_property(Atom property, Atom type, std::size_t format, + set_property_mode mode, const unsigned char *data, + std::size_t element_count) + : m_property(property), + m_type(type), + m_format(format), + m_mode(mode), + m_data(data), + m_element_count(element_count) {} + + static std::string change_name() { return "x11_change_property"; } + + Atom property() { return m_property; } + std::string_view property_name() { return atom_to_name(m_property); } + Atom type() { return m_type; } + std::string_view type_name() { return atom_to_name(m_type); } + std::size_t format() { return m_format; } + set_property_mode mode() { return m_mode; } + std::string_view mode_name() { + switch (m_mode) { + case mock::set_property_mode::REPLACE: + return "replace"; + case mock::set_property_mode::PREPEND: + return "prepend"; + case mock::set_property_mode::APPEND: + return "append"; + default: + return "other"; + } + } + std::size_t element_count() { return m_element_count; } + const unsigned char *const data() { return m_data; } + std::string debug() { return debug_format( "x11_change_property { property: \"%s\", type: \"%s\", format: %d, " - "mode: %d, data: [...], element_count: %d }", - property.c_str(), type.c_str(), format, mode, element_count); + "mode: %s (%d), data: [...], element_count: %d }", + property_name(), type_name(), m_format, mode_name(), m_mode, + m_element_count); } }; + +template +struct always_false : std::false_type {}; } // namespace mock +// These are only macros because including Catch2 from mocking causes spurious +// errors. + +// Originally a single templated function: +// +// template +// const D &expect_x11_data( +// const unsigned char * const data, Atom type, std::size_t format, +// std::size_t element_count +// ) {...} +// +// It is a somewhat large blob, but most of it will be compiled away. The only +// downside is that lambdas must return owned values. + +#define REQUIRE_FORMAT_SIZE(format, T) REQUIRE(format == (sizeof(T) * 8)) +#define EXPECT_X11_VALUE(data, type, format, element_count, T) \ + []() { \ + if constexpr (std::is_same_v && \ + std::is_same_v) { \ + if (!(type == mock::x11_property_type::ATOM || \ + type == mock::x11_property_type::BITMAP || \ + type == mock::x11_property_type::CARDINAL || \ + type == mock::x11_property_type::PIXMAP || \ + type == mock::x11_property_type::COLORMAP || \ + type == mock::x11_property_type::CURSOR || \ + type == mock::x11_property_type::DRAWABLE || \ + type == mock::x11_property_type::FONT || \ + type == mock::x11_property_type::VISUALID || \ + type == mock::x11_property_type::WINDOW)) { \ + FAIL( \ + "expected unsigned long data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::CARDINAL) { \ + FAIL("expected CARDINAL data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v) { \ + if (!(type == mock::x11_property_type::ATOM || \ + type == mock::x11_property_type::BITMAP || \ + type == mock::x11_property_type::PIXMAP || \ + type == mock::x11_property_type::COLORMAP || \ + type == mock::x11_property_type::CURSOR || \ + type == mock::x11_property_type::DRAWABLE || \ + type == mock::x11_property_type::FONT || \ + type == mock::x11_property_type::VISUALID || \ + type == mock::x11_property_type::WINDOW)) { \ + FAIL("expected XID data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, XID); \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::INTEGER) { \ + FAIL("expected INTEGER data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::int32_t); \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::RECTANGLE) { \ + FAIL("expected RECTANGLE data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, short); \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::ARC) { \ + FAIL("expected ARC data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, short); \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::RGB_COLOR_MAP) { \ + FAIL( \ + "expected RGB_COLOR_MAP data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, short); \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v || \ + std::is_same_v) { \ + if (type != mock::x11_property_type::STRING) { \ + FAIL("expected STRING data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, char); \ + return T(reinterpret_cast(data), element_count); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::WM_HINTS) { \ + FAIL("expected WM_HINTS data; got: " << mock::atom_to_name(type)); \ + } \ + /* TODO: Not sure: REQUIRE_FORMAT_SIZE(format, unsigned long); */ \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::WM_SIZE_HINTS) { \ + FAIL( \ + "expected WM_SIZE_HINTS data; got: " << mock::atom_to_name(type)); \ + } \ + /* TODO: Not sure: REQUIRE_FORMAT_SIZE(format, unsigned long); */ \ + REQUIRE(element_count == 1); \ + return *reinterpret_cast(data); \ + } else { \ + throw "unimplemented conversion" \ + } \ + }() + +#define EXPECT_X11_ARRAY(data, type, format, element_count, T, Count) \ + [&]() { \ + if constexpr (std::is_same_v && \ + std::is_same_v) { \ + if (!(type == mock::x11_property_type::ATOM || \ + type == mock::x11_property_type::BITMAP || \ + type == mock::x11_property_type::CARDINAL || \ + type == mock::x11_property_type::PIXMAP || \ + type == mock::x11_property_type::COLORMAP || \ + type == mock::x11_property_type::CURSOR || \ + type == mock::x11_property_type::DRAWABLE || \ + type == mock::x11_property_type::FONT || \ + type == mock::x11_property_type::VISUALID || \ + type == mock::x11_property_type::WINDOW)) { \ + FAIL("expected unsigned long array; got: " \ + << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + REQUIRE(element_count == Count); \ + return *reinterpret_cast *>(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::CARDINAL) { \ + FAIL("expected CARDINAL array; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + REQUIRE(element_count == Count); \ + return *reinterpret_cast *>(data); \ + } else if constexpr (std::is_same_v) { \ + if (!(type == mock::x11_property_type::ATOM || \ + type == mock::x11_property_type::BITMAP || \ + type == mock::x11_property_type::PIXMAP || \ + type == mock::x11_property_type::COLORMAP || \ + type == mock::x11_property_type::CURSOR || \ + type == mock::x11_property_type::DRAWABLE || \ + type == mock::x11_property_type::FONT || \ + type == mock::x11_property_type::VISUALID || \ + type == mock::x11_property_type::WINDOW)) { \ + FAIL("expected XID data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, XID); \ + REQUIRE(element_count == Count); \ + return *reinterpret_cast *>(data); \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::INTEGER) { \ + FAIL("expected INTEGER array; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + REQUIRE(element_count == Count); \ + return *reinterpret_cast *>(data); \ + } else { \ + throw "unimplemented conversion"; \ + } \ + }() + +#define EXPECT_X11_VEC(data, type, format, element_count, T) \ + [&]() { \ + REQUIRE(sizeof(T) == format); \ + if constexpr (std::is_same_v && \ + std::is_same_v) { \ + if (!(type == mock::x11_property_type::ATOM || \ + type == mock::x11_property_type::BITMAP || \ + type == mock::x11_property_type::CARDINAL || \ + type == mock::x11_property_type::PIXMAP || \ + type == mock::x11_property_type::COLORMAP || \ + type == mock::x11_property_type::CURSOR || \ + type == mock::x11_property_type::DRAWABLE || \ + type == mock::x11_property_type::FONT || \ + type == mock::x11_property_type::VISUALID || \ + type == mock::x11_property_type::WINDOW)) { \ + FAIL("expected unsigned long array; got: " \ + << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + REQUIRE(element_count == Count); \ + return std::vector(data), element_count>; \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::CARDINAL) { \ + FAIL("expected CARDINAL array; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + REQUIRE(element_count == Count); \ + return std::vector(data), element_count>; \ + } else if constexpr (std::is_same_v) { \ + if (!(type == mock::x11_property_type::ATOM || \ + type == mock::x11_property_type::BITMAP || \ + type == mock::x11_property_type::PIXMAP || \ + type == mock::x11_property_type::COLORMAP || \ + type == mock::x11_property_type::CURSOR || \ + type == mock::x11_property_type::DRAWABLE || \ + type == mock::x11_property_type::FONT || \ + type == mock::x11_property_type::VISUALID || \ + type == mock::x11_property_type::WINDOW)) { \ + FAIL("expected XID data; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, XID); \ + REQUIRE(element_count == Count); \ + return std::vector(data), element_count>; \ + } else if constexpr (std::is_same_v) { \ + if (type != mock::x11_property_type::INTEGER) { \ + FAIL("expected INTEGER array; got: " << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::int32_t); \ + REQUIRE(element_count == Count); \ + return std::vector(data), element_count>; \ + } else { \ + throw "unimplemented conversion"; \ + } \ + }() + #endif /* X11_MOCK_HH */ diff --git a/tests/mock/x11.cc b/tests/mock/x11.cc index a2c7f6779..c2880f2ec 100644 --- a/tests/mock/x11.cc +++ b/tests/mock/x11.cc @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -89,6 +90,7 @@ static auto MOCK_ATOMS = std::vector{ "_NET_WM_STRUT_PARTIAL", }; +namespace mock { Atom name_to_atom(const char *name) { for (size_t i = 0; i < PREDEFINED_ATOMS.size(); i++) { if (std::strcmp(name, PREDEFINED_ATOMS[i]) == 0) { return i; } @@ -100,20 +102,21 @@ Atom name_to_atom(const char *name) { } return 0; } -std::string atom_to_name(Atom atom) { +const std::string_view atom_to_name(Atom atom) { if (atom > XA_LAST_PREDEFINED && atom - XA_LAST_PREDEFINED < MOCK_ATOMS.size()) { - return std::string(MOCK_ATOMS[atom - XA_LAST_PREDEFINED]); + return std::string_view(MOCK_ATOMS[atom - XA_LAST_PREDEFINED]); } else if (atom <= XA_LAST_PREDEFINED) { - return std::string(PREDEFINED_ATOMS[atom]); + return std::string_view(PREDEFINED_ATOMS[atom]); } return "UNKNOWN"; } +} // namespace mock extern "C" { Atom XInternAtom(Display *display, const char *atom_name, int only_if_exists) { - if (only_if_exists) { return name_to_atom(atom_name); } - const auto value = name_to_atom(atom_name); + if (only_if_exists) { return mock::name_to_atom(atom_name); } + const auto value = mock::name_to_atom(atom_name); if (value != 0) { return value; } else { @@ -128,7 +131,7 @@ int XChangeProperty(Display *display, Window w, Atom property, Atom type, int format, int mode, const unsigned char *data, int nelements) { mock::push_state_change(std::make_unique( - atom_to_name(property), atom_to_name(type), format, mode, data, + property, type, format, static_cast(mode), data, nelements)); return Success; } diff --git a/tests/test-x11-struts.cc b/tests/test-x11-struts.cc index 1fbc19043..989dd1c30 100644 --- a/tests/test-x11-struts.cc +++ b/tests/test-x11-struts.cc @@ -31,9 +31,9 @@ #include #include #include +#include +#include #include -#include "catch2/catch_amalgamated.hpp" -#include "config.h" #include "geometry.h" #include "gui.h" #include "mock/mock.hh" @@ -42,6 +42,43 @@ using namespace conky; +struct x11_strut { + std::uint32_t left; + std::uint32_t right; + std::uint32_t top; + std::uint32_t bottom; +}; +x11_strut expect_strut(mock::x11_change_property &change) { + REQUIRE(change.element_count() == 4); + auto result = EXPECT_X11_ARRAY(change.data(), change.type(), change.format(), + change.element_count(), std::uint32_t, 4); + return x11_strut{result[0], result[1], result[2], result[3]}; +} + +struct x11_strut_partial { + std::uint32_t left; + std::uint32_t right; + std::uint32_t top; + std::uint32_t bottom; + std::uint32_t left_start_y; + std::uint32_t left_end_y; + std::uint32_t right_start_y; + std::uint32_t right_end_y; + std::uint32_t top_start_x; + std::uint32_t top_end_x; + std::uint32_t bottom_start_x; + std::uint32_t bottom_end_x; +}; +x11_strut_partial expect_strut_partial(mock::x11_change_property &change) { + REQUIRE(change.element_count() == 12); + auto result = EXPECT_X11_ARRAY(change.data(), change.type(), change.format(), + change.element_count(), std::uint32_t, 12); + return x11_strut_partial{ + result[0], result[1], result[2], result[3], result[4], result[5], + result[6], result[7], result[8], result[9], result[10], result[11], + }; +} + TEST_CASE("x11 set_struts sets correct struts") { // Temporarily initialize used globals workarea = absolute_rect{vec2i(0, 0), vec2i(600, 800)}; @@ -51,11 +88,24 @@ TEST_CASE("x11 set_struts sets correct struts") { set_struts(alignment::TOP_LEFT); mock::x11_change_property full = EXPECT_NEXT_CHANGE(mock::x11_change_property); - REQUIRE(full.property == "_NET_WM_STRUT"); + REQUIRE(full.property_name() == "_NET_WM_STRUT"); + REQUIRE(full.type() == mock::CARDINAL); + + auto strut_bounds = expect_strut(full); + // CHECK(strut_bounds.left == 0); + // CHECK(strut_bounds.right == workarea.width() - window.geometry.width()); + // CHECK(strut_bounds.top == 0); + // CHECK(strut_bounds.bottom == workarea.height() - window.geometry.height()); mock::x11_change_property partial = EXPECT_NEXT_CHANGE(mock::x11_change_property); - REQUIRE(partial.property == "_NET_WM_STRUT_PARTIAL"); + REQUIRE(partial.property_name() == "_NET_WM_STRUT_PARTIAL"); + REQUIRE(partial.type_name() == "CARDINAL"); + auto strut_partial_bounds = expect_strut_partial(partial); + // CHECK(strut_partial_bounds.left == 0); + // CHECK(strut_partial_bounds.right == workarea.width() - window.geometry.width()); + // CHECK(strut_partial_bounds.top == 0); + // CHECK(strut_partial_bounds.bottom == workarea.height() - window.geometry.height()); } // Reset globals