diff --git a/src/conky.cc b/src/conky.cc index c450365c6..2f76af276 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/gui.h b/src/gui.h index 98ffaf5a8..f64986e90 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/src/x11.cc b/src/x11.cc index db6c7a5ac..6483b7436 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 @@ -1146,7 +1167,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 76a7a8ad2..f0aafe577 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,26 +1,47 @@ 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 mock_sources mock/*.cc) -file(GLOB test_srcs test-*.cc) +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 OS_LINUX) - list(FILTER test_srcs EXCLUDE REGEX ".*linux.*\.cc?") -endif() - -if(NOT OS_DARWIN) - list(FILTER test_srcs EXCLUDE REGEX ".*darwin.*\.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. +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_srcs}) -target_link_libraries(test-conky - PRIVATE Catch2 - PUBLIC conky_core +add_executable(test-conky test-common.cc events.cc ${test_sources}) +target_include_directories(test-conky + PUBLIC + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_BINARY_DIR} + ${conky_includes} ) +target_link_libraries(test-conky Catch2 conky-mock conky_core) catch_discover_tests(test-conky) if(CODE_COVERAGE) diff --git a/tests/events.cc b/tests/events.cc new file mode 100644 index 000000000..ca103725e --- /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 000000000..69aaa16fe --- /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 000000000..47125382a --- /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 new file mode 100644 index 000000000..6abaadf58 --- /dev/null +++ b/tests/mock/mock.cc @@ -0,0 +1,22 @@ +#include "mock.hh" +#include +#include + +namespace mock { +std::deque> __internal::state_changes; + +std::deque> take_state_changes() { + std::deque> result; + std::swap(__internal::state_changes, result); + return result; +} +std::optional> next_state_change() { + 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) { + __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 new file mode 100644 index 000000000..7c45436c6 --- /dev/null +++ b/tests/mock/mock.hh @@ -0,0 +1,106 @@ +#ifndef MOCK_HH +#define MOCK_HH + +#include +#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 { + 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; +}; + +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(); + +/// 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) { + __internal::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 compile. +#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. + +#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 new file mode 100644 index 000000000..3ddf29b4c --- /dev/null +++ b/tests/mock/x11-mock.hh @@ -0,0 +1,364 @@ +#ifndef X11_MOCK_HH +#define X11_MOCK_HH + +#include +#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); +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 { + 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()); + } +}; + +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; + 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 std::byte *data, + std::size_t element_count) + : m_property(property), + m_type(type), + m_format(format), + m_mode(mode), + 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() 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"; + case mock::set_property_mode::PREPEND: + return "prepend"; + case mock::set_property_mode::APPEND: + return "append"; + default: + return "other"; + } + } + 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( + "x11_change_property { property: \"%s\", type: \"%s\", format: %d, " + "mode: %s (%d), data: [...], element_count: %d }", + property_name(), type_name(), m_format, mode_name(), m_mode, + m_element_count); + } +}; +} // 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. 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 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 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 _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 && \ + 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 _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 _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 || \ + 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 _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::int32_t); \ + REQUIRE(element_count == Count); \ + return _COPY_C_ARRAY_TO_CAST(long, std::int32_t, Count, data); \ + } 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 new file mode 100644 index 000000000..053038164 --- /dev/null +++ b/tests/mock/x11.cc @@ -0,0 +1,168 @@ +#include +#include +#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", + "_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; } + } + 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; +} +const std::string_view atom_to_name(Atom atom) { + if (atom > XA_LAST_PREDEFINED && + atom - XA_LAST_PREDEFINED < MOCK_ATOMS.size()) { + return std::string_view(MOCK_ATOMS[atom - XA_LAST_PREDEFINED]); + } else if (atom <= XA_LAST_PREDEFINED) { + return std::string_view(PREDEFINED_ATOMS[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" { +Atom XInternAtom(Display *display, const char *atom_name, int only_if_exists) { + 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 { + 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) { + // 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), + (const std::byte *)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 e5312834c..1dd6ee141 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 000000000..764a7686a --- /dev/null +++ b/tests/test-x11-struts.cc @@ -0,0 +1,153 @@ +/* + * + * 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 +#include +#include "geometry.h" +#include "gui.h" +#include "mock/mock.hh" +#include "mock/x11-mock.hh" +#include "x11.h" + +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 = + 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); + 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 = + 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); + 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], + }; +} + +// 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); + 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(); + } + + 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); + + 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 + window.geometry = rect{}; + workarea = conky::absolute_rect{}; +}