From 44cd89cc04579dc062701858dab817ff741aa01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20=C5=A0vagelj?= Date: Mon, 9 Dec 2024 06:46:38 +0100 Subject: [PATCH 1/6] Add support for mocking to tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tin Švagelj --- src/gui.h | 2 +- tests/CMakeLists.txt | 32 +++++-- tests/mock/mock.cc | 22 +++++ tests/mock/mock.hh | 94 ++++++++++++++++++++ tests/mock/x11-mock.hh | 42 +++++++++ tests/mock/x11.cc | 51 +++++++++++ tests/{test-diskio.cc => test-x11-diskio.cc} | 2 - tests/test-x11-struts.cc | 64 +++++++++++++ 8 files changed, 298 insertions(+), 11 deletions(-) create mode 100644 tests/mock/mock.cc create mode 100644 tests/mock/mock.hh create mode 100644 tests/mock/x11-mock.hh create mode 100644 tests/mock/x11.cc rename tests/{test-diskio.cc => test-x11-diskio.cc} (98%) create mode 100644 tests/test-x11-struts.cc diff --git a/src/gui.h b/src/gui.h index 98ffaf5a8a..f64986e904 100644 --- a/src/gui.h +++ b/src/gui.h @@ -167,7 +167,7 @@ extern char window_created; void destroy_window(void); void create_gc(void); -void set_struts(int); +void set_struts(alignment); bool out_to_gui(lua::state &l); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 76a7a8ad21..155fb23072 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,24 +1,40 @@ include(CTest) -include_directories(${CMAKE_SOURCE_DIR}/src) -include_directories(${CMAKE_BINARY_DIR}) -include_directories(${conky_includes}) +file(GLOB test_sources test-*.cc) -file(GLOB test_srcs test-*.cc) +list(APPEND test_sources ${CMAKE_BINARY_DIR}/config.h) if(NOT OS_LINUX) - list(FILTER test_srcs EXCLUDE REGEX ".*linux.*\.cc?") + list(FILTER test_sources EXCLUDE REGEX ".*linux.*\.cc?") endif() if(NOT OS_DARWIN) - list(FILTER test_srcs EXCLUDE REGEX ".*darwin.*\.cc?") + list(FILTER test_sources EXCLUDE REGEX ".*darwin.*\.cc?") endif() +if(NOT BUILD_X11) + list(FILTER test_sources EXCLUDE REGEX ".*x11.*\.cc?") +endif() + +if(NOT BUILD_WAYLAND) + list(FILTER test_sources EXCLUDE REGEX ".*wayland.*\.cc?") +endif() + +# Mocking works because it's linked before conky_core, so the linker uses mock +# implementations instead of those that are linked later. +file(GLOB mock_sources mock/*.cc) + add_library(Catch2 STATIC catch2/catch_amalgamated.cpp) -add_executable(test-conky test-common.cc ${test_srcs}) +add_executable(test-conky test-common.cc ${test_sources}) +target_include_directories(test-conky + PUBLIC + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_BINARY_DIR} + ${conky_includes} +) target_link_libraries(test-conky - PRIVATE Catch2 + PRIVATE Catch2 ${mock_sources} PUBLIC conky_core ) catch_discover_tests(test-conky) diff --git a/tests/mock/mock.cc b/tests/mock/mock.cc new file mode 100644 index 0000000000..fa8c7d8c7e --- /dev/null +++ b/tests/mock/mock.cc @@ -0,0 +1,22 @@ +#include "mock.hh" +#include +#include + +namespace mock { +std::deque> _state_changes; + +std::deque> take_state_changes() { + std::deque> result; + std::swap(_state_changes, result); + return result; +} +std::optional> next_state_change() { + if (_state_changes.empty()) { return std::nullopt; } + auto front = std::move(_state_changes.front()); + _state_changes.pop_front(); + return front; +} +void push_state_change(std::unique_ptr change) { + _state_changes.push_back(std::move(change)); +} +} // namespace mock \ No newline at end of file diff --git a/tests/mock/mock.hh b/tests/mock/mock.hh new file mode 100644 index 0000000000..2569518e28 --- /dev/null +++ b/tests/mock/mock.hh @@ -0,0 +1,94 @@ +#ifndef MOCK_HH +#define MOCK_HH + +#include +#include +#include +#include +#include +#include + +namespace mock { + +/// Ponyfill for `std::format`. +template +std::string debug_format(const std::string& format, Args... args) { + int size_s = std::snprintf(nullptr, 0, format.c_str(), args...) + + 1; // Extra space for '\0' + if (size_s <= 0) { throw std::runtime_error("error during formatting"); } + auto size = static_cast(size_s); + std::unique_ptr buf(new char[size]); + std::snprintf(buf.get(), size, format.c_str(), args...); + return std::string(buf.get(), + buf.get() + size - 1); // We don't want the '\0' inside +} + +/// Base class of state changes. +/// +/// A state change represents some side effect that mutates system state via +/// mocked functions. +/// +/// For directly accessible globals and fields, those should be used instead. +/// 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; + /// 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; +}; + +/// Implementation detail; shouldn't be used directly. +extern std::deque> _state_changes; + +/// Removes all `state_change`s from the queue (clearing it) and returns them. +std::deque> take_state_changes(); + +/// Pops the next `state_change` from the queue, or returns `std::nullopt` if +/// there are none. +std::optional> next_state_change(); + +/// Pushes some `state_change` to the back of the queue. +void push_state_change(std::unique_ptr change); + +/// Pops some `state_change` of type `T` if it's the next change in the queue. +/// Otherwise it returns `std::nullopt`. +template +std::optional next_state_change_t() { + static_assert(std::is_base_of_v, "T must be a state_change"); + auto result = next_state_change(); + if (!result.has_value()) { return std::nullopt; } + auto cast_result = dynamic_cast(result.value().get()); + if (!cast_result) { + _state_changes.push_front(std::move(result.value())); + return std::nullopt; + } + return *dynamic_cast(result.value().release()); +} +} // 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. +#define EXPECT_NEXT_CHANGE(T) \ + []() { \ + static_assert(std::is_base_of_v, \ + #T " isn't a state_change"); \ + auto result = mock::next_state_change(); \ + if (!result.has_value()) { \ + FAIL("no more state changes; expected '" #T "'"); \ + return *reinterpret_cast(malloc(sizeof(T))); \ + } \ + auto cast_result = dynamic_cast(result.value().get()); \ + if (!cast_result) { \ + FAIL("expected '" #T "' as next state change, got: " \ + << result.value().get()->debug()); \ + return *reinterpret_cast(malloc(sizeof(T))); \ + } else { \ + return *dynamic_cast(result.value().release()); \ + } \ + }(); +// garbage reinterpretation after FAIL doesn't get returned because FAIL stops +// the test. Should be UNREACHABLE, but I have trouble including it. + +#endif /* MOCK_HH */ \ No newline at end of file diff --git a/tests/mock/x11-mock.hh b/tests/mock/x11-mock.hh new file mode 100644 index 0000000000..0eb27ead52 --- /dev/null +++ b/tests/mock/x11-mock.hh @@ -0,0 +1,42 @@ +#ifndef X11_MOCK_HH +#define X11_MOCK_HH + +#include +#include "mock.hh" + +namespace mock { +struct x11_define_atom : public state_change { + std::string name; + + x11_define_atom(std::string name) : name(name) {} + + 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) {} + 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); + } +}; +} // namespace mock + +#endif /* X11_MOCK_HH */ diff --git a/tests/mock/x11.cc b/tests/mock/x11.cc new file mode 100644 index 0000000000..8c7fc28190 --- /dev/null +++ b/tests/mock/x11.cc @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +#include +#include + +#include "mock.hh" +#include "x11-mock.hh" + +static auto MOCK_ATOMS = std::vector{ + "UNKNOWN", + "_NET_WM_STRUT", + "_NET_WM_STRUT_PARTIAL", +}; + +Atom name_to_atom(const char *name) { + for (size_t i = 0; i < MOCK_ATOMS.size(); i++) { + if (std::strcmp(name, MOCK_ATOMS[i]) == 0) { return i; } + } + return 0; +} +std::string atom_to_name(Atom atom) { + if (atom < MOCK_ATOMS.size()) { return std::string(MOCK_ATOMS[atom]); } + return "UNKNOWN"; +} + +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 (value != 0) { + return value; + } else { + MOCK_ATOMS.push_back(strdup(atom_name)); + mock::push_state_change( + std::make_unique(std::string(atom_name))); + return MOCK_ATOMS.size() - 1; + } +} + +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, + nelements)); + return Success; +} +} diff --git a/tests/test-diskio.cc b/tests/test-x11-diskio.cc similarity index 98% rename from tests/test-diskio.cc rename to tests/test-x11-diskio.cc index e5312834c0..1dd6ee141c 100644 --- a/tests/test-diskio.cc +++ b/tests/test-x11-diskio.cc @@ -32,7 +32,6 @@ #include #include -#if BUILD_X11 TEST_CASE("diskiographval returns correct value") { struct text_object obj; @@ -47,4 +46,3 @@ TEST_CASE("diskiographval returns correct value") { delete diskio; } } -#endif diff --git a/tests/test-x11-struts.cc b/tests/test-x11-struts.cc new file mode 100644 index 0000000000..1fbc190434 --- /dev/null +++ b/tests/test-x11-struts.cc @@ -0,0 +1,64 @@ +/* + * + * Conky, a system monitor, based on torsmo + * + * Any original torsmo code is licensed under the BSD license + * + * All code written since the fork of torsmo is licensed under the GPL + * + * Please see COPYING for details + * + * Copyright (c) 2005-2024 Brenden Matthews, Philip Kovacs, et. al. + * (see AUTHORS) + * All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "catch2/catch.hpp" + +#include +#include +#include +#include +#include "catch2/catch_amalgamated.hpp" +#include "config.h" +#include "geometry.h" +#include "gui.h" +#include "mock/mock.hh" +#include "mock/x11-mock.hh" +#include "x11.h" + +using namespace conky; + +TEST_CASE("x11 set_struts sets correct struts") { + // Temporarily initialize used globals + workarea = absolute_rect{vec2i(0, 0), vec2i(600, 800)}; + window.geometry = rect{vec2i(0, 0), vec2i(200, 400)}; + + SECTION("for TOP_LEFT alignment") { + set_struts(alignment::TOP_LEFT); + mock::x11_change_property full = + EXPECT_NEXT_CHANGE(mock::x11_change_property); + REQUIRE(full.property == "_NET_WM_STRUT"); + + mock::x11_change_property partial = + EXPECT_NEXT_CHANGE(mock::x11_change_property); + REQUIRE(partial.property == "_NET_WM_STRUT_PARTIAL"); + } + + // Reset globals + window.geometry = rect{}; + workarea = conky::absolute_rect{}; +} From a0c1502ed2371fcb3af341ad98a5b292849da654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20=C5=A0vagelj?= Date: Mon, 9 Dec 2024 06:58:53 +0100 Subject: [PATCH 2/6] Properly handle predefined X11 atoms in mocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tin Švagelj --- tests/mock/mock.hh | 2 +- tests/mock/x11.cc | 90 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/tests/mock/mock.hh b/tests/mock/mock.hh index 2569518e28..ed3965e8eb 100644 --- a/tests/mock/mock.hh +++ b/tests/mock/mock.hh @@ -91,4 +91,4 @@ std::optional next_state_change_t() { // garbage reinterpretation after FAIL doesn't get returned because FAIL stops // the test. Should be UNREACHABLE, but I have trouble including it. -#endif /* MOCK_HH */ \ No newline at end of file +#endif /* MOCK_HH */ diff --git a/tests/mock/x11.cc b/tests/mock/x11.cc index 8c7fc28190..a2c7f6779c 100644 --- a/tests/mock/x11.cc +++ b/tests/mock/x11.cc @@ -1,14 +1,88 @@ +#include #include #include #include #include #include +#include #include #include "mock.hh" #include "x11-mock.hh" +static const auto PREDEFINED_ATOMS = std::array{ + "NONE", + "PRIMARY", + "SECONDARY", + "ARC", + "ATOM", + "BITMAP", + "CARDINAL", + "COLORMAP", + "CURSOR", + "CUT_BUFFER0", + "CUT_BUFFER1", + "CUT_BUFFER2", + "CUT_BUFFER3", + "CUT_BUFFER4", + "CUT_BUFFER5", + "CUT_BUFFER6", + "CUT_BUFFER7", + "DRAWABLE", + "FONT", + "INTEGER", + "PIXMAP", + "POINT", + "RECTANGLE", + "RESOURCE_MANAGER", + "RGB_COLOR_MAP", + "RGB_BEST_MAP", + "RGB_BLUE_MAP", + "RGB_DEFAULT_MAP", + "RGB_GRAY_MAP", + "RGB_GREEN_MAP", + "RGB_RED_MAP", + "STRING", + "VISUALID", + "WINDOW", + "WM_COMMAND", + "WM_HINTS", + "WM_CLIENT_MACHINE", + "WM_ICON_NAME", + "WM_ICON_SIZE", + "WM_NAME", + "WM_NORMAL_HINTS", + "WM_SIZE_HINTS", + "WM_ZOOM_HINTS", + "MIN_SPACE", + "NORM_SPACE", + "MAX_SPACE", + "END_SPACE", + "SUPERSCRIPT_X", + "SUPERSCRIPT_Y", + "SUBSCRIPT_X", + "SUBSCRIPT_Y", + "UNDERLINE_POSITION", + "UNDERLINE_THICKNESS", + "STRIKEOUT_ASCENT", + "STRIKEOUT_DESCENT", + "ITALIC_ANGLE", + "X_HEIGHT", + "QUAD_WIDTH", + "WEIGHT", + "POINT_SIZE", + "RESOLUTION", + "COPYRIGHT", + "NOTICE", + "FONT_NAME", + "FAMILY_NAME", + "FULL_NAME", + "CAP_HEIGHT", + "WM_CLASS", + "WM_TRANSIENT_FOR", +}; + static auto MOCK_ATOMS = std::vector{ "UNKNOWN", "_NET_WM_STRUT", @@ -16,13 +90,23 @@ static auto MOCK_ATOMS = std::vector{ }; Atom name_to_atom(const char *name) { - for (size_t i = 0; i < MOCK_ATOMS.size(); i++) { - if (std::strcmp(name, MOCK_ATOMS[i]) == 0) { return i; } + for (size_t i = 0; i < PREDEFINED_ATOMS.size(); i++) { + if (std::strcmp(name, PREDEFINED_ATOMS[i]) == 0) { return i; } + } + for (size_t i = 1; i < MOCK_ATOMS.size(); i++) { + if (std::strcmp(name, MOCK_ATOMS[i]) == 0) { + return XA_LAST_PREDEFINED + i; + } } return 0; } std::string atom_to_name(Atom atom) { - if (atom < MOCK_ATOMS.size()) { return std::string(MOCK_ATOMS[atom]); } + if (atom > XA_LAST_PREDEFINED && + atom - XA_LAST_PREDEFINED < MOCK_ATOMS.size()) { + return std::string(MOCK_ATOMS[atom - XA_LAST_PREDEFINED]); + } else if (atom <= XA_LAST_PREDEFINED) { + return std::string(PREDEFINED_ATOMS[atom]); + } return "UNKNOWN"; } From 089f9ce6d139e626a19527ed7f5d6fab9e2f2069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20=C5=A0vagelj?= Date: Mon, 9 Dec 2024 07:45:21 +0100 Subject: [PATCH 3/6] Fix mock linking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix compile issues. Signed-off-by: Tin Švagelj --- tests/CMakeLists.txt | 43 +++++++++++++++++++++---------------------- tests/mock/mock.hh | 1 + 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 155fb23072..014e423147 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,29 +1,31 @@ include(CTest) file(GLOB test_sources test-*.cc) +file(GLOB mock_sources mock/*.cc) -list(APPEND test_sources ${CMAKE_BINARY_DIR}/config.h) - -if(NOT OS_LINUX) - list(FILTER test_sources EXCLUDE REGEX ".*linux.*\.cc?") -endif() - -if(NOT OS_DARWIN) - list(FILTER test_sources EXCLUDE REGEX ".*darwin.*\.cc?") -endif() +macro(EXCLUDING_ANY excluded) + set(__condition "${ARGN}") + string(REGEX MATCH "^IF" __match "${__condition}") + if(__match STREQUAL "") + message(FATAL_ERROR "EXCLUDING_ANY call missing IF keyword") + endif() + unset(__match) + string(REGEX REPLACE "^IF" "" __condition "${__condition}") + if(${__condition}) + list(FILTER test_sources EXCLUDE REGEX ".*${excluded}.*\.(cc|hh)") + list(FILTER mock_sources EXCLUDE REGEX ".*${excluded}.*\.(cc|hh)") + endif() + unset(__condition) +endmacro() -if(NOT BUILD_X11) - list(FILTER test_sources EXCLUDE REGEX ".*x11.*\.cc?") -endif() - -if(NOT BUILD_WAYLAND) - list(FILTER test_sources EXCLUDE REGEX ".*wayland.*\.cc?") -endif() +excluding_any("linux" IF NOT OS_LINUX) +excluding_any("darwin" IF NOT OS_DARWIN) +excluding_any("x11" IF (NOT BUILD_X11) OR OS_DARWIN) +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. -file(GLOB mock_sources mock/*.cc) - +add_library(conky-mock OBJECT ${mock_sources}) add_library(Catch2 STATIC catch2/catch_amalgamated.cpp) add_executable(test-conky test-common.cc ${test_sources}) @@ -33,10 +35,7 @@ target_include_directories(test-conky ${CMAKE_BINARY_DIR} ${conky_includes} ) -target_link_libraries(test-conky - PRIVATE Catch2 ${mock_sources} - PUBLIC conky_core -) +target_link_libraries(test-conky Catch2 conky-mock conky_core) catch_discover_tests(test-conky) if(CODE_COVERAGE) diff --git a/tests/mock/mock.hh b/tests/mock/mock.hh index ed3965e8eb..a5cda413e8 100644 --- a/tests/mock/mock.hh +++ b/tests/mock/mock.hh @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace mock { From 384822e6fa1d6a6be4c70a82ba905af89e706e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20=C5=A0vagelj?= Date: Mon, 9 Dec 2024 18:22:15 +0100 Subject: [PATCH 4/6] Add X11 mocking utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tin Švagelj --- tests/CMakeLists.txt | 4 +- tests/mock/mock.hh | 6 +- tests/mock/x11-mock.hh | 325 +++++++++++++++++++++++++++++++++++++-- tests/mock/x11.cc | 15 +- tests/test-x11-struts.cc | 58 ++++++- 5 files changed, 378 insertions(+), 30 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 014e423147..e45c3fc70e 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 a5cda413e8..e5bf538dae 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 0eb27ead52..a366709c59 100644 --- a/tests/mock/x11-mock.hh +++ b/tests/mock/x11-mock.hh @@ -1,42 +1,333 @@ #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); \ + 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); \ + 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); \ + 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); \ + 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 a2c7f6779c..c2880f2eca 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 1fbc190434..989dd1c304 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 From 1784a59be8b2a837940c3f06579e77924aa7cceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20=C5=A0vagelj?= Date: Mon, 9 Dec 2024 19:21:57 +0100 Subject: [PATCH 5/6] Fix x11_change_property not properly copying data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tin Švagelj --- src/x11.cc | 2 +- tests/CMakeLists.txt | 4 +- tests/mock/x11-mock.hh | 178 +++++++++++++++++++++++---------------- tests/mock/x11.cc | 34 +++++++- tests/test-x11-struts.cc | 12 +-- 5 files changed, 146 insertions(+), 84 deletions(-) diff --git a/src/x11.cc b/src/x11.cc index db6c7a5aca..5cd456b258 100644 --- a/src/x11.cc +++ b/src/x11.cc @@ -1146,7 +1146,7 @@ void set_struts(alignment align) { Atom strut = ATOM(_NET_WM_STRUT); if (strut != None) { - long sizes[STRUT_COUNT] = {0}; + long sizes[STRUT_COUNT] = {}; int display_width = workarea.width(); int display_height = workarea.height(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e45c3fc70e..014e423147 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,10 +25,8 @@ 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(Catch2 STATIC catch2/catch_amalgamated.cpp) - add_library(conky-mock OBJECT ${mock_sources}) -target_link_libraries(conky-mock Catch2) +add_library(Catch2 STATIC catch2/catch_amalgamated.cpp) add_executable(test-conky test-common.cc ${test_sources}) target_include_directories(test-conky diff --git a/tests/mock/x11-mock.hh b/tests/mock/x11-mock.hh index a366709c59..5b96a90988 100644 --- a/tests/mock/x11-mock.hh +++ b/tests/mock/x11-mock.hh @@ -5,9 +5,11 @@ #include #include #include +#include #include #include #include +#include #include "mock.hh" @@ -36,6 +38,9 @@ enum x11_property_type { Atom name_to_atom(const char *name); const std::string_view atom_to_name(Atom atom); +size_t format_size(std::size_t format); +void dump_x11_blob(const std::byte *data, std::size_t format, + std::size_t length); /// Mutation produced by creating new `Atom`s. struct x11_define_atom : public state_change { @@ -62,29 +67,29 @@ class x11_change_property : public state_change { Atom m_type; std::size_t m_format; set_property_mode m_mode; - const unsigned char *m_data; + std::vector 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, + set_property_mode mode, const std::byte *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) {} + m_element_count(element_count), + m_data(std::vector(data, data + format_size(format) * 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() { + Atom property() const { return m_property; } + std::string_view property_name() const { return atom_to_name(m_property); } + Atom type() const { return m_type; } + std::string_view type_name() const { return atom_to_name(m_type); } + std::size_t format() const { return m_format; } + set_property_mode mode() const { return m_mode; } + std::string_view mode_name() const { switch (m_mode) { case mock::set_property_mode::REPLACE: return "replace"; @@ -96,8 +101,8 @@ class x11_change_property : public state_change { return "other"; } } - std::size_t element_count() { return m_element_count; } - const unsigned char *const data() { return m_data; } + std::size_t element_count() const { return m_element_count; } + const std::byte *data() const { return m_data.data(); } std::string debug() { return debug_format( @@ -107,26 +112,24 @@ class x11_change_property : public state_change { m_element_count); } }; - -template -struct always_false : std::false_type {}; } // namespace mock +#define REQUIRE_FORMAT_SIZE(format, T) REQUIRE(format == (sizeof(T) * 8)) + // These are only macros because including Catch2 from mocking causes spurious -// errors. +// errors. I whish they weren't because they're such a pain to write this way. // Originally a single templated function: // // template // const D &expect_x11_data( -// const unsigned char * const data, Atom type, std::size_t format, +// const std::byte* 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 && \ @@ -225,6 +228,21 @@ struct always_false : std::false_type {}; } \ }() +#define _COPY_C_ARRAY_TO_CAST(BaseT, TargetT, Length, source) \ + [&]() { \ + auto values = reinterpret_cast(source); \ + auto result = std::array{}; \ + for (size_t i = 0; i < Length; i++) { \ + if constexpr (std::numeric_limits::max() > \ + std::numeric_limits::max()) { \ + CHECK(values[i] >= std::numeric_limits::min()); \ + CHECK(values[i] <= std::numeric_limits::max()); \ + } \ + result[i] = static_cast(values[i]); \ + } \ + return result; \ + }() + #define EXPECT_X11_ARRAY(data, type, format, element_count, T, Count) \ [&]() { \ if constexpr (std::is_same_v && \ @@ -244,14 +262,14 @@ struct always_false : std::false_type {}; } \ REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ REQUIRE(element_count == Count); \ - return *reinterpret_cast *>(data); \ + return _COPY_C_ARRAY_TO_CAST(long, std::uint32_t, Count, 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); \ + return _COPY_C_ARRAY_TO_CAST(long, std::uint32_t, Count, data); \ } else if constexpr (std::is_same_v) { \ if (!(type == mock::x11_property_type::ATOM || \ type == mock::x11_property_type::BITMAP || \ @@ -266,68 +284,82 @@ struct always_false : std::false_type {}; } \ REQUIRE_FORMAT_SIZE(format, XID); \ REQUIRE(element_count == Count); \ - return *reinterpret_cast *>(data); \ + return _COPY_C_ARRAY_TO_CAST(long, T, Count, 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_FORMAT_SIZE(format, std::int32_t); \ REQUIRE(element_count == Count); \ - return *reinterpret_cast *>(data); \ + return _COPY_C_ARRAY_TO_CAST(long, std::int32_t, Count, 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); \ - 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); \ - 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); \ - 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); \ - return std::vector(data), element_count>; \ - } else { \ - throw "unimplemented conversion"; \ - } \ +#define _COPY_C_ARRAY_TO_VEC(BaseT, TargetT, source, length) \ + [&]() { \ + auto values = reinterpret_cast(source); \ + auto result = std::vector(length); \ + for (const BaseT *it = values; it < values + length; it++) { \ + if constexpr (std::numeric_limits::max() > \ + std::numeric_limits::max()) { \ + CHECK(*it >= std::numeric_limits::min()); \ + CHECK(*it <= std::numeric_limits::max()); \ + } \ + result.push_back(*it); \ + } \ + return result; \ + }() + +#define EXPECT_X11_VEC(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 array; got: " \ + << mock::atom_to_name(type)); \ + } \ + REQUIRE_FORMAT_SIZE(format, std::uint32_t); \ + return _COPY_C_ARRAY_TO_VEC(long, std::uint32_t, 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); \ + return _COPY_C_ARRAY_TO_VEC(long, std::uint32_t, 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); \ + return _COPY_C_ARRAY_TO_VEC(long, XID, 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); \ + return _COPY_C_ARRAY_TO_VEC(long, std::int32_t, data, element_count); \ + } else { \ + throw "unimplemented conversion"; \ + } \ }() #endif /* X11_MOCK_HH */ diff --git a/tests/mock/x11.cc b/tests/mock/x11.cc index c2880f2eca..0530381641 100644 --- a/tests/mock/x11.cc +++ b/tests/mock/x11.cc @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -111,6 +112,33 @@ const std::string_view atom_to_name(Atom atom) { } return "UNKNOWN"; } + +size_t format_size(std::size_t format) { + if (format == 32) { + return sizeof(long); + } else if (format == 16) { + return sizeof(short); + } else if (format == 8) { + return sizeof(char); + } else { + throw "invalid format"; + } +} +void dump_x11_blob(const std::byte *data, std::size_t format, + std::size_t length) { + size_t entry_len = format_size(format); + for (size_t i = 0; i < length * entry_len; i++) { + if (((i + 1) % entry_len) == 1) { printf("%p: ", data + i); } + // Print bytes in order: + // printf("%02x ", data[i]); + // Reorder bytes: + printf("%02x ", (unsigned char)data[((i / entry_len - 1) * entry_len) + + (2 * entry_len - 1 - (i % entry_len))]); + if (i > 0 && ((i + 1) % entry_len) == 0) { puts(""); } + } + printf("Total bytes: %d\n", (int)(length * entry_len)); + puts(""); +} } // namespace mock extern "C" { @@ -130,9 +158,11 @@ Atom XInternAtom(Display *display, const char *atom_name, int only_if_exists) { int XChangeProperty(Display *display, Window w, Atom property, Atom type, int format, int mode, const unsigned char *data, int nelements) { + // printf("Setting %s property data:\n", mock::atom_to_name(property).data()); + // dump_x11_blob((const std::byte *)data, format, nelements); mock::push_state_change(std::make_unique( - property, type, format, static_cast(mode), data, - nelements)); + property, type, format, static_cast(mode), + (const std::byte *)data, nelements)); return Success; } } diff --git a/tests/test-x11-struts.cc b/tests/test-x11-struts.cc index 989dd1c304..747c17f6a4 100644 --- a/tests/test-x11-struts.cc +++ b/tests/test-x11-struts.cc @@ -82,14 +82,14 @@ x11_strut_partial expect_strut_partial(mock::x11_change_property &change) { TEST_CASE("x11 set_struts sets correct struts") { // Temporarily initialize used globals workarea = absolute_rect{vec2i(0, 0), vec2i(600, 800)}; - window.geometry = rect{vec2i(0, 0), vec2i(200, 400)}; + window.geometry = rect{vec2i(0, 0), vec2i(200, 600)}; SECTION("for TOP_LEFT alignment") { set_struts(alignment::TOP_LEFT); mock::x11_change_property full = EXPECT_NEXT_CHANGE(mock::x11_change_property); REQUIRE(full.property_name() == "_NET_WM_STRUT"); - REQUIRE(full.type() == mock::CARDINAL); + REQUIRE(full.type() == mock::x11_property_type::CARDINAL); auto strut_bounds = expect_strut(full); // CHECK(strut_bounds.left == 0); @@ -100,12 +100,14 @@ TEST_CASE("x11 set_struts sets correct struts") { mock::x11_change_property partial = EXPECT_NEXT_CHANGE(mock::x11_change_property); REQUIRE(partial.property_name() == "_NET_WM_STRUT_PARTIAL"); - REQUIRE(partial.type_name() == "CARDINAL"); + REQUIRE(partial.type() == mock::x11_property_type::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.right == + // workarea.width() - window.geometry.width()); // CHECK(strut_partial_bounds.top == 0); - // CHECK(strut_partial_bounds.bottom == workarea.height() - window.geometry.height()); + // CHECK(strut_partial_bounds.bottom == + // workarea.height() - window.geometry.height()); } // Reset globals From 992e67c48e8174260c247ae28fae75510bf9999f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20=C5=A0vagelj?= Date: Mon, 16 Dec 2024 06:40:12 +0100 Subject: [PATCH 6/6] Add display mocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split apart some of existing X11 functions so they can be unit tested. - Improvements of testing code. Signed-off-by: Tin Švagelj --- src/conky.cc | 47 ++++++++------- src/x11.cc | 23 +++++++- tests/CMakeLists.txt | 8 ++- tests/events.cc | 20 +++++++ tests/mock/display-mock.cc | 21 +++++++ tests/mock/display-mock.hh | 116 +++++++++++++++++++++++++++++++++++++ tests/mock/mock.cc | 12 ++-- tests/mock/mock.hh | 15 ++++- tests/mock/x11-mock.hh | 1 - tests/test-x11-struts.cc | 81 +++++++++++++++++++------- 10 files changed, 289 insertions(+), 55 deletions(-) create mode 100644 tests/events.cc create mode 100644 tests/mock/display-mock.cc create mode 100644 tests/mock/display-mock.hh diff --git a/src/conky.cc b/src/conky.cc index c450365c6d..2f76af2768 100644 --- a/src/conky.cc +++ b/src/conky.cc @@ -849,27 +849,7 @@ static int get_string_width_special(char *s, int special_index) { static int text_size_updater(char *s, int special_index); -int last_font_height; -void update_text_area() { - conky::vec2i xy; - - if (display_output() == nullptr || !display_output()->graphical()) { return; } - - /* update text size if it isn't fixed */ -#ifdef OWN_WINDOW - if (fixed_size == 0) -#endif - { - text_size = conky::vec2i(dpi_scale(minimum_width.get(*state)), 0); - last_font_height = font_height(); - for_each_line(text_buffer, text_size_updater); - - text_size = text_size.max(conky::vec2i(text_size.x() + 1, dpi_scale(minimum_height.get(*state)))); - int mw = dpi_scale(maximum_width.get(*state)); - if (mw > 0) text_size = text_size.min(conky::vec2i(mw, text_size.y())); - } - - alignment align = text_alignment.get(*state); +void apply_window_alignment(conky::vec2i &xy, alignment align) { /* get text position on workarea */ switch (vertical_alignment(align)) { case axis_align::START: @@ -897,6 +877,31 @@ void update_text_area() { dpi_scale(gap_x.get(*state))); break; } +} + +int last_font_height; +void update_text_area() { + conky::vec2i xy; + + if (display_output() == nullptr || !display_output()->graphical()) { return; } + + /* update text size if it isn't fixed */ +#ifdef OWN_WINDOW + if (fixed_size == 0) +#endif + { + text_size = conky::vec2i(dpi_scale(minimum_width.get(*state)), 0); + last_font_height = font_height(); + for_each_line(text_buffer, text_size_updater); + + text_size = text_size.max( + conky::vec2i(text_size.x() + 1, dpi_scale(minimum_height.get(*state)))); + int mw = dpi_scale(maximum_width.get(*state)); + if (mw > 0) text_size = text_size.min(conky::vec2i(mw, text_size.y())); + } + + alignment align = text_alignment.get(*state); + apply_window_alignment(xy, align); #ifdef OWN_WINDOW if (align == alignment::NONE) { // Let the WM manage the window xy = window.geometry.pos(); diff --git a/src/x11.cc b/src/x11.cc index 5cd456b258..6483b7436d 100644 --- a/src/x11.cc +++ b/src/x11.cc @@ -1138,7 +1138,28 @@ constexpr size_t operator*(x11_strut index) { return static_cast(index); } -/* reserve window manager space */ +/// Reserve window manager space +/// +/// Both `_NET_WM_STRUT` and `_NET_WM_STRUT_PARTIAL` work in coordinates +/// relative to root window (or sometimes Xinerama/Screen). +/// +/// Values tell the WM which regions of the screen are invalidated. So, a +/// `bottom` value of `30` means that the window reserves bottom 30px of the +/// screen. +/// +/// Because struts aren't handled the best by all WMs when multiple screens are +/// used, horizontal struts (top, bottom) should be preferred because that +/// works well for most multi-screen (horizontal monitor stacking) setups. +/// See: https://gitlab.gnome.org/GNOME/mutter/-/issues/452 +/// +/// Different WMs handle this differently, some adhere to the spec, some don't. +/// Spec compliant (relative to root window edges): +/// - mutter, metacity, openbox, marco, xfwm +/// Non-compliant (relative to edges of xinerama/single monitor): +/// - compiz, kwin, i3, fluxbox +/// +/// Article why KWin doesn't follow the spec: +/// https://blog.martin-graesslin.com/blog/2016/08/panels-on-shared-screen-edges/ void set_struts(alignment align) { // Middle and none align don't have least significant bit set. // Ensures either vertical or horizontal axis are start/end diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 014e423147..f0aafe577d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,9 +26,15 @@ 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}) +target_include_directories(conky-mock + PUBLIC + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_BINARY_DIR} + ${conky_includes} +) add_library(Catch2 STATIC catch2/catch_amalgamated.cpp) -add_executable(test-conky test-common.cc ${test_sources}) +add_executable(test-conky test-common.cc events.cc ${test_sources}) target_include_directories(test-conky PUBLIC ${CMAKE_SOURCE_DIR}/src diff --git a/tests/events.cc b/tests/events.cc new file mode 100644 index 0000000000..ca103725e8 --- /dev/null +++ b/tests/events.cc @@ -0,0 +1,20 @@ +#include "catch2/catch.hpp" +#include "mock/display-mock.hh" +#include "mock/mock.hh" + +class testRunListener : public Catch::EventListenerBase { + public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) { + mock::__internal::init_display_output_mock(); + } + void testRunEnded(Catch::TestRunStats const&) { + mock::__internal::delete_display_output_mock(); + } + void testCaseStarting(Catch::SectionInfo const&) { + mock::__internal::state_changes.clear(); + } +}; + +CATCH_REGISTER_LISTENER(testRunListener) diff --git a/tests/mock/display-mock.cc b/tests/mock/display-mock.cc new file mode 100644 index 0000000000..69aaa16fe2 --- /dev/null +++ b/tests/mock/display-mock.cc @@ -0,0 +1,21 @@ +#include "display-mock.hh" +#include "display-output.hh" + +namespace mock { +display_output_mock *output; + +display_output_mock &get_mock_output() { return *output; } + +namespace __internal { +void init_display_output_mock() { + output = new display_output_mock(); + conky::active_display_outputs.push_back(output); + conky::current_display_outputs.push_back(output); +} +void delete_display_output_mock() { + delete output; + conky::current_display_outputs.clear(); + conky::active_display_outputs.clear(); +} +} // namespace __internal +} // namespace mock \ No newline at end of file diff --git a/tests/mock/display-mock.hh b/tests/mock/display-mock.hh new file mode 100644 index 0000000000..47125382a0 --- /dev/null +++ b/tests/mock/display-mock.hh @@ -0,0 +1,116 @@ +/* + * + * Conky, a system monitor, based on torsmo + * + * Please see COPYING for details + * + * Copyright (C) 2018-2021 François Revol et al. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef DISPLAY_MOCK_HH +#define DISPLAY_MOCK_HH + +#include "colours.h" +#include "display-output.hh" + +namespace mock { +/// These are called by Catch2 events, DO NOT use them directly +namespace __internal { +void init_display_output_mock(); +void delete_display_output_mock(); +} // namespace __internal + +/* + * A base class for mock display output that emulates a GUI. + */ +class display_output_mock : public conky::display_output_base { + // Use `mock::get_mock_output`. + explicit display_output_mock() : conky::display_output_base("mock") {}; + ~display_output_mock() {}; + + public: + float dpi_scale = 1.0; + + // check if available and enabled in settings + bool detect() { return true; } + // connect to DISPLAY and other stuff + bool initialize() { return true; } + bool shutdown() { return true; } + + bool graphical() { return true; }; + bool draw_line_inner_required() { return true; } + + bool main_loop_wait(double t) { return false; } + + void sigterm_cleanup() {} + void cleanup() {} + + // drawing primitives + void set_foreground_color(Colour c) {} + + int calc_text_width(const char *s) { return 0; } + + void begin_draw_text() {} + void end_draw_text() {} + void draw_string(const char *s, int w) {} + void line_inner_done() {} + + // GUI interface + void draw_string_at(int /*x*/, int /*y*/, const char * /*s*/, int /*w*/) {} + // X11 lookalikes + void set_line_style(int /*w*/, bool /*solid*/) {} + void set_dashes(char * /*s*/) {} + void draw_line(int /*x1*/, int /*y1*/, int /*x2*/, int /*y2*/) {} + void draw_rect(int /*x*/, int /*y*/, int /*w*/, int /*h*/) {} + void fill_rect(int /*x*/, int /*y*/, int /*w*/, int /*h*/) {} + void draw_arc(int /*x*/, int /*y*/, int /*w*/, int /*h*/, int /*a1*/, + int /*a2*/) {} + void move_win(int /*x*/, int /*y*/) {} + float get_dpi_scale() { return dpi_scale; }; + + void begin_draw_stuff() {} + void end_draw_stuff() {} + void clear_text(int /*exposures*/) {} + + // font stuff + int font_height(unsigned int) { return 0; } + int font_ascent(unsigned int) { return 0; } + int font_descent(unsigned int) { return 0; } + void setup_fonts(void) {} + void set_font(unsigned int) {} + void free_fonts(bool /*utf8*/) {} + void load_fonts(bool /*utf8*/) {} + + // tty interface + int getx() { return 0; } + int gety() { return 0; } + void gotox(int /*x*/) {} + void gotoy(int /*y*/) {} + void gotoxy(int /*x*/, int /*y*/) {} + + void flush() {} + + protected: + bool active() { return true; } + + friend void __internal::init_display_output_mock(); + friend void __internal::delete_display_output_mock(); +}; + +display_output_mock &get_mock_output(); +} // namespace mock + +#endif /* DISPLAY_MOCK_HH */ diff --git a/tests/mock/mock.cc b/tests/mock/mock.cc index fa8c7d8c7e..6abaadf58c 100644 --- a/tests/mock/mock.cc +++ b/tests/mock/mock.cc @@ -3,20 +3,20 @@ #include namespace mock { -std::deque> _state_changes; +std::deque> __internal::state_changes; std::deque> take_state_changes() { std::deque> result; - std::swap(_state_changes, result); + std::swap(__internal::state_changes, result); return result; } std::optional> next_state_change() { - if (_state_changes.empty()) { return std::nullopt; } - auto front = std::move(_state_changes.front()); - _state_changes.pop_front(); + if (__internal::state_changes.empty()) { return std::nullopt; } + auto front = std::move(__internal::state_changes.front()); + __internal::state_changes.pop_front(); return front; } void push_state_change(std::unique_ptr change) { - _state_changes.push_back(std::move(change)); + __internal::state_changes.push_back(std::move(change)); } } // namespace mock \ No newline at end of file diff --git a/tests/mock/mock.hh b/tests/mock/mock.hh index e5bf538dae..7c45436c62 100644 --- a/tests/mock/mock.hh +++ b/tests/mock/mock.hh @@ -42,8 +42,9 @@ struct state_change { virtual std::string debug() = 0; }; -/// Implementation detail; shouldn't be used directly. -extern std::deque> _state_changes; +namespace __internal { +extern std::deque> state_changes; +} /// Removes all `state_change`s from the queue (clearing it) and returns them. std::deque> take_state_changes(); @@ -64,7 +65,7 @@ std::optional next_state_change_t() { if (!result.has_value()) { return std::nullopt; } auto cast_result = dynamic_cast(result.value().get()); if (!cast_result) { - _state_changes.push_front(std::move(result.value())); + __internal::state_changes.push_front(std::move(result.value())); return std::nullopt; } return *dynamic_cast(result.value().release()); @@ -94,4 +95,12 @@ std::optional next_state_change_t() { // garbage reinterpretation after FAIL doesn't get returned because FAIL stops // the test. Should be UNREACHABLE, but I have trouble including it. +#define EXPECT_NO_MORE_CHANGES() \ + []() { \ + auto length = mock::__internal::state_changes.size(); \ + if (length > 0) { \ + FAIL("expected no more state changes; found: " << length); \ + } \ + }(); + #endif /* MOCK_HH */ diff --git a/tests/mock/x11-mock.hh b/tests/mock/x11-mock.hh index 5b96a90988..3ddf29b4c0 100644 --- a/tests/mock/x11-mock.hh +++ b/tests/mock/x11-mock.hh @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include diff --git a/tests/test-x11-struts.cc b/tests/test-x11-struts.cc index 747c17f6a4..764a7686a0 100644 --- a/tests/test-x11-struts.cc +++ b/tests/test-x11-struts.cc @@ -48,7 +48,10 @@ struct x11_strut { std::uint32_t top; std::uint32_t bottom; }; -x11_strut expect_strut(mock::x11_change_property &change) { +x11_strut expect_strut() { + mock::x11_change_property change = + EXPECT_NEXT_CHANGE(mock::x11_change_property); + REQUIRE(change.property_name() == "_NET_WM_STRUT"); REQUIRE(change.element_count() == 4); auto result = EXPECT_X11_ARRAY(change.data(), change.type(), change.format(), change.element_count(), std::uint32_t, 4); @@ -69,7 +72,10 @@ struct x11_strut_partial { std::uint32_t bottom_start_x; std::uint32_t bottom_end_x; }; -x11_strut_partial expect_strut_partial(mock::x11_change_property &change) { +x11_strut_partial expect_strut_partial() { + mock::x11_change_property change = + EXPECT_NEXT_CHANGE(mock::x11_change_property); + REQUIRE(change.property_name() == "_NET_WM_STRUT_PARTIAL"); REQUIRE(change.element_count() == 12); auto result = EXPECT_X11_ARRAY(change.data(), change.type(), change.format(), change.element_count(), std::uint32_t, 12); @@ -79,35 +85,66 @@ x11_strut_partial expect_strut_partial(mock::x11_change_property &change) { }; } +// from conky.cc +extern conky::vec2i text_size; +extern void apply_window_alignment(conky::vec2i &xy, alignment align); + TEST_CASE("x11 set_struts sets correct struts") { // Temporarily initialize used globals workarea = absolute_rect{vec2i(0, 0), vec2i(600, 800)}; + const auto half_width = workarea.width() / 2; + const auto half_height = workarea.height() / 2; window.geometry = rect{vec2i(0, 0), vec2i(200, 600)}; + auto &xy = *reinterpret_cast(&window.geometry); SECTION("for TOP_LEFT alignment") { set_struts(alignment::TOP_LEFT); - mock::x11_change_property full = - EXPECT_NEXT_CHANGE(mock::x11_change_property); - REQUIRE(full.property_name() == "_NET_WM_STRUT"); - REQUIRE(full.type() == mock::x11_property_type::CARDINAL); + apply_window_alignment(xy, alignment::TOP_LEFT); + auto strut = expect_strut(); + CHECK(strut.left == 0); + CHECK(strut.right == 0); + CHECK(strut.top == window.geometry.end_y()); + CHECK(strut.bottom == 0); + + auto strut_partial = expect_strut_partial(); + CHECK(strut_partial.left == 0); + CHECK(strut_partial.right == 0); + CHECK(strut_partial.top == window.geometry.end_y()); + CHECK(strut_partial.bottom == 0); + CHECK(strut_partial.left_start_y == 0); + CHECK(strut_partial.left_end_y == 0); + CHECK(strut_partial.right_start_y == 0); + CHECK(strut_partial.right_end_y == 0); + CHECK(strut_partial.top_start_x == window.geometry.x()); + CHECK(strut_partial.top_end_x == window.geometry.end_x()); + CHECK(strut_partial.bottom_start_x == 0); + CHECK(strut_partial.bottom_end_x == 0); + EXPECT_NO_MORE_CHANGES(); + } - 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()); + SECTION("for TOP_MIDDLE alignment") { + set_struts(alignment::TOP_MIDDLE); + apply_window_alignment(xy, alignment::TOP_MIDDLE); + auto strut = expect_strut(); + CHECK(strut.left == 0); + CHECK(strut.right == 0); + CHECK(strut.top == window.geometry.end_y()); + CHECK(strut.bottom == 0); - mock::x11_change_property partial = - EXPECT_NEXT_CHANGE(mock::x11_change_property); - REQUIRE(partial.property_name() == "_NET_WM_STRUT_PARTIAL"); - REQUIRE(partial.type() == mock::x11_property_type::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()); + auto strut_partial = expect_strut_partial(); + CHECK(strut_partial.left == 0); + CHECK(strut_partial.right == 0); + CHECK(strut_partial.top == window.geometry.end_y()); + CHECK(strut_partial.bottom == 0); + CHECK(strut_partial.left_start_y == 0); + CHECK(strut_partial.left_end_y == 0); + CHECK(strut_partial.right_start_y == 0); + CHECK(strut_partial.right_end_y == 0); + CHECK(strut_partial.top_start_x == window.geometry.x()); + CHECK(strut_partial.top_end_x == window.geometry.end_x()); + CHECK(strut_partial.bottom_start_x == 0); + CHECK(strut_partial.bottom_end_x == 0); + EXPECT_NO_MORE_CHANGES(); } // Reset globals