srctree

Robin Linden parent 0cae9173 ee699b26
etest: Add an etest implementation without any global state

etest/BUILD added: 255, removed: 25, total 230
@@ -4,11 +4,11 @@ load("//bzl:defs.bzl", "cc_xfail_test")
 
cc_library(
name = "etest",
srcs = ["etest.cpp"],
hdrs = [
"cxx_compat.h",
"etest.h",
],
srcs = glob(
include = ["*.cpp"],
exclude = ["*_test.cpp"],
),
hdrs = glob(["*.h"]),
copts = HASTUR_COPTS,
visibility = ["//visibility:public"],
)
 
etest/etest.cpp added: 255, removed: 25, total 230
@@ -28,14 +28,14 @@ namespace {
 
int assertion_failures = 0;
 
struct Test {
struct OldTest {
std::string name;
std::function<void()> body;
};
 
struct Registry {
std::vector<Test> tests;
std::vector<Test> disabled_tests;
std::vector<OldTest> tests;
std::vector<OldTest> disabled_tests;
};
 
Registry &registry() {
@@ -54,7 +54,7 @@ int run_all_tests(RunOptions const &opts) noexcept {
auto const &tests = registry().tests;
auto const &disabled = registry().disabled_tests;
 
std::vector<Test> tests_to_run;
std::vector<OldTest> tests_to_run;
std::ranges::copy(begin(tests), end(tests), std::back_inserter(tests_to_run));
 
std::cout << tests.size() + disabled.size() << " test(s) registered";
 
etest/etest.h added: 255, removed: 25, total 230
@@ -6,8 +6,8 @@
#define ETEST_TEST_H_
 
#include "etest/cxx_compat.h"
#include "etest/etest2.h"
 
#include <concepts>
#include <functional>
#include <optional>
#include <sstream>
@@ -17,16 +17,6 @@
 
namespace etest {
 
template<typename T>
concept Printable = // Hack comment to get clang-format 15 and 16 to agree on the indentation.
requires(std::ostream &os, T t) {
{ os << t } -> std::same_as<std::ostream &>;
};
 
struct RunOptions {
bool run_disabled_tests{false};
};
 
[[nodiscard]] int run_all_tests(RunOptions const & = {}) noexcept;
 
void test(std::string name, std::function<void()> body) noexcept;
 
filename was Deleted added: 255, removed: 25, total 230
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2021-2023 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
#include "etest/etest2.h"
 
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <sstream>
#include <utility>
 
#if defined(_MSC_VER)
// MSVC doesn't seem to have a way of disabling exceptions.
#define ETEST_EXCEPTIONS
#elif defined(__EXCEPTIONS)
// __EXCEPTIONS is set in gcc and Clang unless -fno-exceptions is used.
#define ETEST_EXCEPTIONS
#endif
 
namespace etest {
namespace {
struct TestFailure : public std::exception {};
 
struct Actions : public IActions {
// Weak test requirement. Allows the test to continue even if the check fails.
void expect(
bool b, std::optional<std::string_view> log_message, etest::source_location const &loc) noexcept override {
if (b) {
return;
}
 
++assertion_failures;
test_log << " expectation failure at " << loc.file_name() << "(" << loc.line() << ":" << loc.column() << ")\n";
 
if (log_message) {
test_log << *log_message << "\n\n";
}
}
 
// Hard test requirement. Stops the test (using an exception) if the check fails.
void require(bool b, std::optional<std::string_view> log_message, etest::source_location const &loc) override {
if (b) {
return;
}
 
test_log << " requirement failure at " << loc.file_name() << "(" << loc.line() << ":" << loc.column() << ")\n";
 
if (log_message) {
test_log << *log_message << "\n\n";
}
 
#ifdef ETEST_EXCEPTIONS
throw TestFailure{};
#else
std::abort();
#endif
}
 
std::stringstream test_log;
int assertion_failures{0};
};
} // namespace
 
int Suite::run(RunOptions const &opts) {
std::vector<Test> tests_to_run;
std::ranges::copy(begin(tests_), end(tests_), std::back_inserter(tests_to_run));
 
std::cout << tests_.size() + disabled_tests_.size() << " test(s) registered";
if (disabled_tests_.size() == 0) {
std::cout << "." << std::endl;
} else {
std::cout << ", " << disabled_tests_.size() << " disabled." << std::endl;
if (opts.run_disabled_tests) {
std::ranges::copy(begin(disabled_tests_), end(disabled_tests_), std::back_inserter(tests_to_run));
}
}
 
if (tests_to_run.empty()) {
return 1;
}
 
// TODO(robinlinden): std::ranges once clang-cl supports it. Last tested
// with LLVM 15.0.0.
auto const longest_name = std::max_element(tests_to_run.begin(),
tests_to_run.end(),
[](auto const &a, auto const &b) { return a.name.size() < b.name.size(); });
 
bool failure = false;
for (auto const &test : tests_to_run) {
std::cout << std::left << std::setw(longest_name->name.size()) << test.name << ": ";
 
Actions a{};
#ifdef ETEST_EXCEPTIONS
try {
test.body(a);
} catch (TestFailure const &) {
a.assertion_failures += 1;
} catch (std::exception const &e) {
a.assertion_failures += 1;
a.test_log << "Unhandled exception in test body: " << e.what() << '\n';
} catch (...) {
a.assertion_failures += 1;
a.test_log << "Unhandled unknown exception in test body.\n";
}
#else
test.body(a);
#endif
 
if (a.assertion_failures == 0) {
std::cout << "\u001b[32mPASSED\u001b[0m\n";
} else {
failure = true;
std::cout << "\u001b[31;1mFAILED\u001b[0m\n";
std::cout << std::move(a.test_log).str();
}
 
std::cout << std::flush;
}
 
return failure ? 1 : 0;
}
 
} // namespace etest
 
filename was Deleted added: 255, removed: 25, total 230
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2021-2023 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
#ifndef ETEST_ETEST2_H_
#define ETEST_ETEST2_H_
 
#include "etest/cxx_compat.h"
 
#include <concepts>
#include <functional>
#include <iosfwd>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
 
namespace etest {
// NOLINTBEGIN(google-default-arguments): Some things like log messages and
// source locations are optional.
 
template<typename T>
concept Printable = // Hack comment to get clang-format 15 and 16 to agree on the indentation.
requires(std::ostream &os, T t) {
{ os << t } -> std::same_as<std::ostream &>;
};
 
struct RunOptions {
bool run_disabled_tests{false};
};
 
class IActions {
public:
virtual ~IActions() = default;
 
// Weak test requirement. Allows the test to continue even if the check fails.
virtual void expect(bool,
std::optional<std::string_view> log_message = std::nullopt,
etest::source_location const &loc = etest::source_location::current()) noexcept = 0;
 
// Hard test requirement. Stops the test (using an exception) if the check fails.
virtual void require(bool,
std::optional<std::string_view> log_message = std::nullopt,
etest::source_location const &loc = etest::source_location::current()) = 0;
 
// Weak test requirement. Prints the types compared on failure (if printable).
template<Printable T, Printable U>
void expect_eq(T const &a,
U const &b,
std::optional<std::string_view> log_message = std::nullopt,
etest::source_location const &loc = etest::source_location::current()) noexcept {
std::stringstream ss;
ss << a << " !=\n" << b;
expect(a == b, log_message ? std::move(log_message) : std::move(ss).str(), loc);
}
 
template<typename T, typename U>
void expect_eq(T const &a,
U const &b,
std::optional<std::string_view> log_message = std::nullopt,
etest::source_location const &loc = etest::source_location::current()) noexcept {
expect(a == b, std::move(log_message), loc);
}
 
// Hard test requirement. Prints the types compared on failure (if printable).
template<Printable T, Printable U>
void require_eq(T const &a,
U const &b,
std::optional<std::string_view> log_message = std::nullopt,
etest::source_location const &loc = etest::source_location::current()) {
std::stringstream ss;
ss << a << " !=\n" << b;
require(a == b, log_message ? std::move(log_message) : std::move(ss).str(), loc);
}
 
template<typename T, typename U>
void require_eq(T const &a,
U const &b,
std::optional<std::string_view> log_message = std::nullopt,
etest::source_location const &loc = etest::source_location::current()) {
require(a == b, std::move(log_message), loc);
}
};
 
struct Test {
std::string name;
std::function<void(IActions &)> body;
};
 
class Suite {
public:
explicit Suite(std::string name) : name_(std::move(name)) {}
 
void add_test(std::string name, std::function<void(IActions &)> test) {
tests_.push_back({std::move(name), std::move(test)});
}
 
void disabled_test(std::string name, std::function<void(IActions &)> test) {
disabled_tests_.push_back({std::move(name), std::move(test)});
}
 
[[nodiscard]] int run(RunOptions const &);
 
private:
std::string name_{};
std::vector<Test> tests_{};
std::vector<Test> disabled_tests_{};
};
 
// NOLINTEND(google-default-arguments)
} // namespace etest
 
#endif