Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mocking to tests & test X11 struts #2115

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/gui.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ extern char window_created;

void destroy_window(void);
void create_gc(void);
void set_struts(int);
void set_struts(alignment);
Copy link
Collaborator Author

@Caellian Caellian Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I kept this line like this for a reason, it previously caused compilation to fail because alignment wasn't available to some file (wayland?) that uses it. Note to self to check before merge.


bool out_to_gui(lua::state &l);

Expand Down
2 changes: 1 addition & 1 deletion src/x11.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
45 changes: 30 additions & 15 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
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})
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 ${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)
Expand Down
22 changes: 22 additions & 0 deletions tests/mock/mock.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include "mock.hh"
#include <optional>
#include <utility>

namespace mock {
std::deque<std::unique_ptr<state_change>> _state_changes;

std::deque<std::unique_ptr<state_change>> take_state_changes() {
std::deque<std::unique_ptr<mock::state_change>> result;
std::swap(_state_changes, result);
return result;
}
std::optional<std::unique_ptr<state_change>> 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<state_change> change) {
_state_changes.push_back(std::move(change));
}
} // namespace mock
97 changes: 97 additions & 0 deletions tests/mock/mock.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#ifndef MOCK_HH
#define MOCK_HH

#include <cstdio>
#include <deque>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <type_traits>

namespace mock {

/// Ponyfill for `std::format`.
template <typename... Args>
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_t>(size_s);
std::unique_ptr<char[]> 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;
};

/// Implementation detail; shouldn't be used directly.
extern std::deque<std::unique_ptr<state_change>> _state_changes;

/// Removes all `state_change`s from the queue (clearing it) and returns them.
std::deque<std::unique_ptr<state_change>> take_state_changes();

/// Pops the next `state_change` from the queue, or returns `std::nullopt` if
/// there are none.
std::optional<std::unique_ptr<state_change>> next_state_change();

/// Pushes some `state_change` to the back of the queue.
void push_state_change(std::unique_ptr<state_change> change);

/// Pops some `state_change` of type `T` if it's the next change in the queue.
/// Otherwise it returns `std::nullopt`.
template <typename T>
std::optional<T> next_state_change_t() {
static_assert(std::is_base_of_v<state_change, T>, "T must be a state_change");
auto result = next_state_change();
if (!result.has_value()) { return std::nullopt; }
auto cast_result = dynamic_cast<T*>(result.value().get());
if (!cast_result) {
_state_changes.push_front(std::move(result.value()));
return std::nullopt;
}
return *dynamic_cast<T*>(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<mock::state_change, T>, \
#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<T*>(malloc(sizeof(T))); \
} \
auto cast_result = dynamic_cast<T*>(result.value().get()); \
if (!cast_result) { \
FAIL("expected '" #T "' as next state change, got: " \
<< result.value().get()->debug()); \
return *reinterpret_cast<T*>(malloc(sizeof(T))); \
} else { \
return *dynamic_cast<T*>(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 */
Loading
Loading