tests: split into separate binaries

This commit is contained in:
Austin Horstman
2024-06-08 22:30:01 -05:00
parent 87eaa75b8a
commit 58e7abba2c
8 changed files with 37 additions and 15 deletions

45
test/utils/JsonParser.cpp Normal file
View File

@ -0,0 +1,45 @@
#include "util/json.hpp"
#if __has_include(<catch2/catch_test_macros.hpp>)
#include <catch2/catch_test_macros.hpp>
#else
#include <catch2/catch.hpp>
#endif
TEST_CASE("Simple json", "[json]") {
SECTION("Parse simple json") {
std::string stringToTest = R"({"number": 5, "string": "test"})";
waybar::util::JsonParser parser;
Json::Value jsonValue = parser.parse(stringToTest);
REQUIRE(jsonValue["number"].asInt() == 5);
REQUIRE(jsonValue["string"].asString() == "test");
}
}
TEST_CASE("Json with unicode", "[json]") {
SECTION("Parse json with unicode") {
std::string stringToTest = R"({"test": "\xab"})";
waybar::util::JsonParser parser;
Json::Value jsonValue = parser.parse(stringToTest);
// compare with "\u00ab" because "\xab" is replaced with "\u00ab" in the parser
REQUIRE(jsonValue["test"].asString() == "\u00ab");
}
}
TEST_CASE("Json with emoji", "[json]") {
SECTION("Parse json with emoji") {
std::string stringToTest = R"({"test": "😊"})";
waybar::util::JsonParser parser;
Json::Value jsonValue = parser.parse(stringToTest);
REQUIRE(jsonValue["test"].asString() == "😊");
}
}
TEST_CASE("Json with chinese characters", "[json]") {
SECTION("Parse json with chinese characters") {
std::string stringToTest = R"({"test": ""})";
waybar::util::JsonParser parser;
Json::Value jsonValue = parser.parse(stringToTest);
REQUIRE(jsonValue["test"].asString() == "你好");
}
}

143
test/utils/SafeSignal.cpp Normal file
View File

@ -0,0 +1,143 @@
#include "util/SafeSignal.hpp"
#include <glibmm.h>
#if __has_include(<catch2/catch_test_macros.hpp>)
#include <catch2/catch_test_macros.hpp>
#else
#include <catch2/catch.hpp>
#endif
#include <thread>
#include <type_traits>
#include "fixtures/GlibTestsFixture.hpp"
using namespace waybar;
template <typename T>
using remove_cvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
/**
* Basic sanity test for SafeSignal:
* check that type deduction works, events are delivered and the order is right
* Running this with -fsanitize=thread should not fail
*/
TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal basic functionality", "[signal][thread][util]") {
const int NUM_EVENTS = 100;
int count = 0;
int last_value = 0;
SafeSignal<int, std::string> test_signal;
const auto main_tid = std::this_thread::get_id();
std::thread producer;
// timeout the test in 500ms
setTimeout(500);
test_signal.connect([&](auto val, auto str) {
static_assert(std::is_same<int, decltype(val)>::value);
static_assert(std::is_same<std::string, decltype(str)>::value);
// check that we're in the same thread as the main loop
REQUIRE(std::this_thread::get_id() == main_tid);
// check event order
REQUIRE(val == last_value + 1);
last_value = val;
if (++count >= NUM_EVENTS) {
this->quit();
};
});
run([&]() {
// check that events from the same thread are delivered and processed synchronously
test_signal.emit(1, "test");
REQUIRE(count == 1);
// start another thread and generate events
producer = std::thread([&]() {
for (auto i = 2; i <= NUM_EVENTS; ++i) {
test_signal.emit(i, "test");
}
});
});
producer.join();
REQUIRE(count == NUM_EVENTS);
}
template <typename T>
struct TestObject {
T value;
unsigned copied = 0;
unsigned moved = 0;
TestObject(const T& v) : value(v){};
~TestObject() = default;
TestObject(const TestObject& other)
: value(other.value), copied(other.copied + 1), moved(other.moved) {}
TestObject(TestObject&& other) noexcept
: value(std::move(other.value)),
copied(std::exchange(other.copied, 0)),
moved(std::exchange(other.moved, 0) + 1) {}
TestObject& operator=(const TestObject& other) {
value = other.value;
copied = other.copied + 1;
moved = other.moved;
return *this;
}
TestObject& operator=(TestObject&& other) noexcept {
value = std::move(other.value);
copied = std::exchange(other.copied, 0);
moved = std::exchange(other.moved, 0) + 1;
return *this;
}
bool operator==(T other) const { return value == other; }
operator T() const { return value; }
};
/*
* Check the number of copies/moves performed on the object passed through SafeSignal
*/
TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal copy/move counter", "[signal][thread][util]") {
const int NUM_EVENTS = 3;
int count = 0;
SafeSignal<TestObject<int>> test_signal;
std::thread producer;
// timeout the test in 500ms
setTimeout(500);
test_signal.connect([&](auto& val) {
static_assert(std::is_same<TestObject<int>, remove_cvref_t<decltype(val)>>::value);
/* explicit move in the producer thread */
REQUIRE(val.moved <= 1);
/* copy within the SafeSignal queuing code */
REQUIRE(val.copied <= 1);
if (++count >= NUM_EVENTS) {
this->quit();
};
});
run([&]() {
test_signal.emit(1);
REQUIRE(count == 1);
producer = std::thread([&]() {
for (auto i = 2; i <= NUM_EVENTS; ++i) {
TestObject<int> t{i};
// check that signal.emit accepts moved objects
test_signal.emit(std::move(t));
}
});
});
producer.join();
REQUIRE(count == NUM_EVENTS);
}

View File

@ -0,0 +1,100 @@
#include "util/css_reload_helper.hpp"
#include <map>
#if __has_include(<catch2/catch_test_macros.hpp>)
#include <catch2/catch_test_macros.hpp>
#else
#include <catch2/catch.hpp>
#endif
class CssReloadHelperTest : public waybar::CssReloadHelper {
public:
CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this]() { callback(); }) {}
void callback() { m_callbackCounter++; }
protected:
std::string getFileContents(const std::string& filename) override {
return m_fileContents[filename];
}
std::string findPath(const std::string& filename) override { return filename; }
void setFileContents(const std::string& filename, const std::string& contents) {
m_fileContents[filename] = contents;
}
int getCallbackCounter() const { return m_callbackCounter; }
private:
int m_callbackCounter{};
std::map<std::string, std::string> m_fileContents;
};
TEST_CASE_METHOD(CssReloadHelperTest, "parse_imports", "[util][css_reload_helper]") {
SECTION("no imports") {
setFileContents("/tmp/waybar_test.css", "body { color: red; }");
auto files = parseImports("/tmp/waybar_test.css");
REQUIRE(files.size() == 1);
CHECK(files[0] == "/tmp/waybar_test.css");
}
SECTION("single import") {
setFileContents("/tmp/waybar_test.css", "@import 'test.css';");
setFileContents("test.css", "body { color: red; }");
auto files = parseImports("/tmp/waybar_test.css");
std::sort(files.begin(), files.end());
REQUIRE(files.size() == 2);
CHECK(files[0] == "/tmp/waybar_test.css");
CHECK(files[1] == "test.css");
}
SECTION("multiple imports") {
setFileContents("/tmp/waybar_test.css", "@import 'test.css'; @import 'test2.css';");
setFileContents("test.css", "body { color: red; }");
setFileContents("test2.css", "body { color: blue; }");
auto files = parseImports("/tmp/waybar_test.css");
std::sort(files.begin(), files.end());
REQUIRE(files.size() == 3);
CHECK(files[0] == "/tmp/waybar_test.css");
CHECK(files[1] == "test.css");
CHECK(files[2] == "test2.css");
}
SECTION("nested imports") {
setFileContents("/tmp/waybar_test.css", "@import 'test.css';");
setFileContents("test.css", "@import 'test2.css';");
setFileContents("test2.css", "body { color: red; }");
auto files = parseImports("/tmp/waybar_test.css");
std::sort(files.begin(), files.end());
REQUIRE(files.size() == 3);
CHECK(files[0] == "/tmp/waybar_test.css");
CHECK(files[1] == "test.css");
CHECK(files[2] == "test2.css");
}
SECTION("circular imports") {
setFileContents("/tmp/waybar_test.css", "@import 'test.css';");
setFileContents("test.css", "@import 'test2.css';");
setFileContents("test2.css", "@import 'test.css';");
auto files = parseImports("/tmp/waybar_test.css");
std::sort(files.begin(), files.end());
REQUIRE(files.size() == 3);
CHECK(files[0] == "/tmp/waybar_test.css");
CHECK(files[1] == "test.css");
CHECK(files[2] == "test2.css");
}
SECTION("empty") {
setFileContents("/tmp/waybar_test.css", "");
auto files = parseImports("/tmp/waybar_test.css");
REQUIRE(files.size() == 1);
CHECK(files[0] == "/tmp/waybar_test.css");
}
SECTION("empty name") {
auto files = parseImports("");
REQUIRE(files.empty());
}
}

188
test/utils/date.cpp Normal file
View File

@ -0,0 +1,188 @@
#include "util/date.hpp"
#include <ctime>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#if __has_include(<catch2/catch_test_macros.hpp>)
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#else
#include <catch2/catch.hpp>
#endif
#ifndef SKIP
#define SKIP(...) \
WARN(__VA_ARGS__); \
return
#endif
using namespace std::literals::chrono_literals;
namespace fmt_lib = waybar::util::date::format;
/*
* Check that the date/time formatter with locale and timezone support is working as expected.
*/
const zoned_time<std::chrono::seconds> TEST_TIME{
"UTC", local_days{Monday[1] / January / 2022} + 13h + 4min + 5s};
/*
* Check if the date formatted with LC_TIME=en_US is within expectations.
*
* The check expects Glibc output style and will fail with FreeBSD (different implementation)
* or musl (no implementation).
*/
static const bool LC_TIME_is_sane = []() {
try {
std::stringstream ss;
ss.imbue(std::locale("en_US.UTF-8"));
time_t t = 1641211200;
std::tm tm = *std::gmtime(&t);
ss << std::put_time(&tm, "%x %X");
return ss.str() == "01/03/2022 12:00:00 PM";
} catch (std::exception &) {
return false;
}
}();
TEST_CASE("Format UTC time", "[clock][util]") {
const auto loc = std::locale("C");
const auto tm = TEST_TIME;
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified
#endif
CHECK(fmt_lib::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 13:04:05 2022 UTC");
CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103130405");
if (!LC_TIME_is_sane) {
SKIP("Locale support check failed, skip tests");
}
/* Test a few locales that are most likely to be present */
SECTION("US locale") {
try {
const auto loc = std::locale("en_US.UTF-8");
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified
CHECK_THAT(fmt_lib::format(loc, "{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM"));
CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "01/03/2022 01:04:05 PM");
#else
CHECK(fmt_lib::format(loc, "{:%F %r}", tm) == "2022-01-03 01:04:05 PM");
#endif
CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103130405");
} catch (const std::runtime_error &) {
WARN("Locale en_US not found, skip tests");
}
}
SECTION("GB locale") {
try {
const auto loc = std::locale("en_GB.UTF-8");
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified
CHECK_THAT(fmt_lib::format(loc, "{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 13:04:05"));
CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "03/01/22 13:04:05");
#else
CHECK(fmt_lib::format(loc, "{:%F %T}", tm) == "2022-01-03 13:04:05");
#endif
CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103130405");
} catch (const std::runtime_error &) {
WARN("Locale en_GB not found, skip tests");
}
}
SECTION("Global locale") {
try {
const auto loc = std::locale::global(std::locale("en_US.UTF-8"));
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format("{}", tm).empty()); // no format specified
CHECK_THAT(fmt_lib::format("{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM"));
CHECK(fmt_lib::format("{:%x %X}", tm) == "01/03/2022 01:04:05 PM");
#else
CHECK(fmt_lib::format("{:%F %r}", tm) == "2022-01-03 01:04:05 PM");
#endif
CHECK(fmt_lib::format("{:%Y%m%d%H%M%S}", tm) == "20220103130405");
std::locale::global(loc);
} catch (const std::runtime_error &) {
WARN("Locale en_US not found, skip tests");
}
}
}
TEST_CASE("Format zoned time", "[clock][util]") {
const auto loc = std::locale("C");
const auto tm = zoned_time{"America/New_York", TEST_TIME};
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified
#endif
CHECK(fmt_lib::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 08:04:05 2022 EST");
CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103080405");
if (!LC_TIME_is_sane) {
SKIP("Locale support check failed, skip tests");
}
/* Test a few locales that are most likely to be present */
SECTION("US locale") {
try {
const auto loc = std::locale("en_US.UTF-8");
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified
CHECK_THAT(fmt_lib::format(loc, "{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM"));
CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "01/03/2022 08:04:05 AM");
#else
CHECK(fmt_lib::format(loc, "{:%F %r}", tm) == "2022-01-03 08:04:05 AM");
#endif
CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103080405");
} catch (const std::runtime_error &) {
WARN("Locale en_US not found, skip tests");
}
}
SECTION("GB locale") {
try {
const auto loc = std::locale("en_GB.UTF-8");
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified
CHECK_THAT(fmt_lib::format(loc, "{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05"));
CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "03/01/22 08:04:05");
#else
CHECK(fmt_lib::format(loc, "{:%F %T}", tm) == "2022-01-03 08:04:05");
#endif
CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103080405");
} catch (const std::runtime_error &) {
WARN("Locale en_GB not found, skip tests");
}
}
SECTION("Global locale") {
try {
const auto loc = std::locale::global(std::locale("en_US.UTF-8"));
#if not HAVE_CHRONO_TIMEZONES
CHECK(fmt_lib::format("{}", tm).empty()); // no format specified
CHECK_THAT(fmt_lib::format("{:%c}", tm), // HowardHinnant/date#704
Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM"));
CHECK(fmt_lib::format("{:%x %X}", tm) == "01/03/2022 08:04:05 AM");
#else
CHECK(fmt_lib::format("{:%F %r}", tm) == "2022-01-03 08:04:05 AM");
#endif
CHECK(fmt_lib::format("{:%Y%m%d%H%M%S}", tm) == "20220103080405");
std::locale::global(loc);
} catch (const std::runtime_error &) {
WARN("Locale en_US not found, skip tests");
}
}
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <glibmm/main.h>
/**
* Minimal Glib application to be used for tests that require Glib main loop
*/
class GlibTestsFixture : public sigc::trackable {
public:
GlibTestsFixture() : main_loop_{Glib::MainLoop::create()} {}
void setTimeout(int timeout) {
Glib::signal_timeout().connect_once([]() { throw std::runtime_error("Test timed out"); },
timeout);
}
void run(std::function<void()> fn) {
Glib::signal_idle().connect_once(fn);
main_loop_->run();
}
void quit() { main_loop_->quit(); }
protected:
Glib::RefPtr<Glib::MainLoop> main_loop_;
};

36
test/utils/meson.build Normal file
View File

@ -0,0 +1,36 @@
test_inc = include_directories('../../include')
test_dep = [
catch2,
fmt,
gtkmm,
jsoncpp,
spdlog,
]
test_src = files(
'../main.cpp',
'../config.cpp',
'../../src/config.cpp',
'JsonParser.cpp',
'SafeSignal.cpp',
'css_reload_helper.cpp',
'../../src/util/css_reload_helper.cpp',
)
if tz_dep.found()
test_dep += tz_dep
test_src += files('date.cpp')
endif
utils_test = executable(
'utils_test',
test_src,
dependencies: test_dep,
include_directories: test_inc,
)
test(
'utils',
utils_test,
workdir: meson.project_source_root(),
)