winamp/Src/external_dependencies/openmpt-trunk/src/mpt/test/test.hpp
2024-09-24 14:54:57 +02:00

564 lines
19 KiB
C++

/* SPDX-License-Identifier: BSL-1.0 OR BSD-3-Clause */
#ifndef MPT_TEST_TEST_HPP
#define MPT_TEST_TEST_HPP
#include "mpt/base/detect.hpp"
#include "mpt/base/namespace.hpp"
#include "mpt/base/source_location.hpp"
#include <functional>
#include <iostream>
#include <map>
#include <sstream>
#include <stdexcept>
#include <type_traits>
#include <typeinfo>
#include <utility>
#include <variant>
#include <cstddef>
#include <cstdlib>
namespace mpt {
inline namespace MPT_INLINE_NS {
namespace test {
template <typename S, typename T, typename = void>
struct is_to_stream_writable : std::false_type { };
template <typename S, typename T>
struct is_to_stream_writable<S, T, std::void_t<decltype(std::declval<S &>() << std::declval<T>())>> : std::true_type { };
template <typename T>
inline auto format(const T & x) -> typename std::enable_if<mpt::test::is_to_stream_writable<std::ostringstream, T>::value, std::string>::type {
std::ostringstream s;
s << x;
return s.str();
}
template <typename T>
inline auto format(const T & x) -> typename std::enable_if<!mpt::test::is_to_stream_writable<std::ostringstream, T>::value, std::string>::type {
return typeid(x).name();
}
inline std::string get_exception_text() {
std::string result;
try {
// cppcheck false-positive
// cppcheck-suppress rethrowNoCurrentException
throw;
} catch (const std::exception & e) {
result = e.what();
} catch (...) {
result = "unknown exception";
}
return result;
}
struct result_success {
};
struct result_failure {
std::string text{};
};
struct result_unexpected_exception {
std::string text{};
};
struct result {
std::variant<std::monostate, result_success, result_failure, result_unexpected_exception> info{std::monostate{}};
};
struct statistics_counters {
std::size_t total{0};
std::size_t run{0};
std::size_t successes{0};
std::size_t failures{0};
std::size_t unexpected_exceptions{0};
std::size_t completed{0};
constexpr statistics_counters & operator+=(const statistics_counters & other) noexcept {
total += other.total;
run += other.run;
successes += other.successes;
failures += other.failures;
unexpected_exceptions += other.unexpected_exceptions;
completed += other.completed;
return *this;
}
};
struct group_statistics {
statistics_counters tests{};
statistics_counters cases{};
statistics_counters local_cases{};
};
struct global_statistics {
statistics_counters groups{};
statistics_counters tests{};
statistics_counters cases{};
std::map<std::string, group_statistics> individual_group_statistics{};
explicit constexpr operator bool() noexcept {
return succeeded();
}
constexpr bool operator!() noexcept {
return failed();
}
constexpr bool succeeded() noexcept {
return groups.successes == groups.run;
}
constexpr bool failed() noexcept {
return groups.failures > 0 || groups.unexpected_exceptions > 0;
}
};
class reporter_interface {
protected:
virtual ~reporter_interface() = default;
public:
virtual void run_begin(const mpt::source_location & loc) = 0;
virtual void group_begin(const mpt::source_location & loc, const char * name) = 0;
virtual void test_begin(const mpt::source_location & loc, const char * name) = 0;
virtual void case_run(const mpt::source_location & loc) = 0;
virtual void case_run(const mpt::source_location & loc, const char * text_e) = 0;
virtual void case_run(const mpt::source_location & loc, const char * text_ex, const char * text_e) = 0;
virtual void case_run(const mpt::source_location & loc, const char * text_a, const char * text_cmp, const char * text_b) = 0;
virtual void case_result(const mpt::source_location & loc, const mpt::test::result & result) = 0;
virtual void test_end(const mpt::source_location & loc, const char * name, const statistics_counters & counters) = 0;
virtual void group_end(const mpt::source_location & loc, const char * name, const group_statistics & statistics) = 0;
virtual void run_end(const mpt::source_location & loc, const global_statistics & statistics) = 0;
virtual void immediate_breakpoint() = 0;
};
class silent_reporter
: public reporter_interface {
public:
silent_reporter() = default;
~silent_reporter() override = default;
public:
virtual void run_begin(const mpt::source_location &) override {
}
virtual void group_begin(const mpt::source_location &, const char *) override {
}
virtual void test_begin(const mpt::source_location &, const char *) override {
}
virtual void case_run(const mpt::source_location &) override {
}
virtual void case_run(const mpt::source_location &, const char *) override {
}
virtual void case_run(const mpt::source_location &, const char *, const char *) override {
}
virtual void case_run(const mpt::source_location &, const char *, const char *, const char *) override {
}
virtual void case_result(const mpt::source_location &, const mpt::test::result &) override {
}
virtual void test_end(const mpt::source_location &, const char *, const statistics_counters &) override {
}
virtual void group_end(const mpt::source_location &, const char *, const group_statistics &) override {
}
virtual void run_end(const mpt::source_location &, const global_statistics &) override {
}
virtual void immediate_breakpoint() override {
}
};
class simple_reporter : public reporter_interface {
private:
std::ostream & s;
public:
simple_reporter(std::ostream & s_)
: s(s_) {
s.flush();
}
~simple_reporter() override {
s.flush();
}
public:
void run_begin(const mpt::source_location & loc) override {
static_cast<void>(loc);
s << "Running test suite ..." << std::endl;
}
void group_begin(const mpt::source_location & loc, const char * name) override {
static_cast<void>(loc);
s << "Running group '" << name << "' ..." << std::endl;
}
void test_begin(const mpt::source_location & loc, const char * name) override {
static_cast<void>(loc);
s << " Running test '" << name << "' ..." << std::endl;
}
void case_run(const mpt::source_location & loc) override {
static_cast<void>(loc);
s << " Checking ..." << std::endl;
}
void case_run(const mpt::source_location & loc, const char * text_e) override {
static_cast<void>(loc);
s << " Checking '" << text_e << "' ..." << std::endl;
}
void case_run(const mpt::source_location & loc, const char * text_ex, const char * text_e) override {
static_cast<void>(loc);
if (text_ex) {
s << " Checking '" << text_e << " throws " << text_ex << "' ..." << std::endl;
} else {
s << " Checking '" << text_e << " throws' ..." << std::endl;
}
}
void case_run(const mpt::source_location & loc, const char * text_a, const char * text_cmp, const char * text_b) override {
static_cast<void>(loc);
s << " Checking '" << text_a << " " << text_cmp << " " << text_b << "' ..." << std::endl;
}
void case_result(const mpt::source_location & loc, const mpt::test::result & result) override {
static_cast<void>(loc);
s << " Checking done: ";
if (std::holds_alternative<result_success>(result.info)) {
s << "Success.";
} else if (std::holds_alternative<result_failure>(result.info)) {
s << "FAILURE: " << std::get<result_failure>(result.info).text;
} else if (std::holds_alternative<result_unexpected_exception>(result.info)) {
s << "UNEXPECTED EXCEPTION: " << std::get<result_unexpected_exception>(result.info).text;
}
s << std::endl;
}
void test_end(const mpt::source_location & loc, const char * name, const statistics_counters & counters) override {
static_cast<void>(loc);
static_cast<void>(counters);
s << " Running test '" << name << "' done." << std::endl;
}
void group_end(const mpt::source_location & loc, const char * name, const group_statistics & statistics) override {
static_cast<void>(loc);
static_cast<void>(statistics);
s << "Running group '" << name << "' done." << std::endl;
}
void run_end(const mpt::source_location & loc, const global_statistics & statistics) override {
static_cast<void>(loc);
s << "Running test suite done." << std::endl;
s << "groups: " << statistics.groups.total << " | " << statistics.groups.successes << " passed";
if (statistics.groups.failures || statistics.groups.unexpected_exceptions) {
s << " | " << statistics.groups.failures << " FAILED";
if (statistics.groups.unexpected_exceptions) {
s << " | " << statistics.groups.unexpected_exceptions << " UNEXPECTED EXCEPTIONS";
}
}
s << std::endl;
s << "tests: " << statistics.tests.total << " | " << statistics.tests.successes << " passed";
if (statistics.tests.failures || statistics.tests.unexpected_exceptions) {
s << " | " << statistics.tests.failures << " FAILED";
if (statistics.tests.unexpected_exceptions) {
s << " | " << statistics.tests.unexpected_exceptions << " UNEXPECTED EXCEPTIONS";
}
}
s << std::endl;
s << "checks: " << statistics.cases.total << " | " << statistics.cases.successes << " passed";
if (statistics.cases.failures || statistics.cases.unexpected_exceptions) {
s << " | " << statistics.cases.failures << " FAILED";
if (statistics.cases.unexpected_exceptions) {
s << " | " << statistics.cases.unexpected_exceptions << " UNEXPECTED EXCEPTIONS";
}
}
s << std::endl;
}
void immediate_breakpoint() override {
return;
}
};
struct group;
struct context {
mpt::test::group & group;
mpt::test::reporter_interface & reporter;
mpt::test::group_statistics statistics{};
};
using void_context_function = void (*)(mpt::test::context &);
struct group {
group * next{nullptr};
const char * name{""};
void_context_function func{nullptr};
inline group(const char * name_, void_context_function f)
: name(name_)
, func(f) {
next = group_list();
group_list() = this;
}
group_statistics run(mpt::test::reporter_interface & reporter, const mpt::source_location & loc = mpt::source_location::current()) {
mpt::test::context context{*this, reporter};
context.reporter.group_begin(loc, name);
if (func) {
func(context);
}
context.reporter.group_end(loc, name, context.statistics);
return context.statistics;
}
public:
[[nodiscard]] static inline group *& group_list() noexcept {
static group * group_list = nullptr;
return group_list;
}
};
inline global_statistics run_all(mpt::test::reporter_interface & reporter, const mpt::source_location & loc = mpt::source_location::current()) {
global_statistics statistics{};
reporter.run_begin(loc);
for (group * g = group::group_list(); g; g = g->next) {
statistics.groups.total++;
statistics.groups.run++;
group_statistics s = g->run(reporter, loc);
if (s.tests.unexpected_exceptions) {
statistics.groups.unexpected_exceptions++;
} else if (s.tests.failures) {
statistics.groups.failures++;
} else {
statistics.groups.successes++;
}
statistics.tests += s.tests;
statistics.cases += s.cases;
statistics.groups.completed++;
statistics.individual_group_statistics[g->name] = s;
}
reporter.run_end(loc, statistics);
return statistics;
}
struct test {
mpt::test::context & context;
const char * name{""};
mpt::source_location source_location{mpt::source_location::current()};
void (*breakpoint)(void){nullptr};
test(const test &) = delete;
test & operator=(const test &) = delete;
inline test(mpt::test::context & context_, void (*breakpoint_)(void) = nullptr, const mpt::source_location & source_location_ = mpt::source_location::current())
: context(context_)
, source_location(source_location_)
, breakpoint(breakpoint_) {
report_test_begin();
}
inline test(mpt::test::context & context_, const char * name_, void (*breakpoint_)(void) = nullptr, const mpt::source_location & source_location_ = mpt::source_location::current())
: context(context_)
, name(name_)
, source_location(source_location_)
, breakpoint(breakpoint_) {
report_test_begin();
}
inline ~test() {
report_test_end();
}
inline void immediate_breakpoint() {
if (breakpoint) {
breakpoint();
} else {
context.reporter.immediate_breakpoint();
}
}
void report_test_begin() {
context.statistics.tests.total++;
context.statistics.tests.run++;
context.statistics.local_cases = statistics_counters{};
context.reporter.test_begin(source_location, name);
}
void report_run() {
context.statistics.local_cases.total++;
context.statistics.local_cases.run++;
context.reporter.case_run(source_location);
}
void report_run(const char * text_e) {
context.statistics.local_cases.total++;
context.statistics.local_cases.run++;
context.reporter.case_run(source_location, text_e);
}
void report_run(const char * text_ex, const char * text_e) {
context.statistics.local_cases.total++;
context.statistics.local_cases.run++;
context.reporter.case_run(source_location, text_ex, text_e);
}
void report_run(const char * text_a, const char * text_cmp, const char * text_b) {
context.statistics.local_cases.total++;
context.statistics.local_cases.run++;
context.reporter.case_run(source_location, text_a, text_cmp, text_b);
}
void report_result(mpt::test::result result) {
if (std::holds_alternative<result_success>(result.info)) {
context.statistics.local_cases.successes++;
} else if (std::holds_alternative<result_failure>(result.info)) {
context.statistics.local_cases.failures++;
} else if (std::holds_alternative<result_unexpected_exception>(result.info)) {
context.statistics.local_cases.unexpected_exceptions++;
}
context.statistics.local_cases.completed++;
context.reporter.case_result(source_location, result);
}
void report_test_end() {
context.statistics.cases += context.statistics.local_cases;
if (context.statistics.local_cases.unexpected_exceptions) {
context.statistics.tests.unexpected_exceptions++;
} else if (context.statistics.local_cases.failures) {
context.statistics.tests.failures++;
} else {
context.statistics.tests.successes++;
}
context.statistics.tests.completed++;
context.reporter.test_end(source_location, name, context.statistics.local_cases);
}
template <typename Texception, typename Tcallable, typename std::enable_if<std::is_invocable<Tcallable>::value, bool>::type = true>
inline test & expect_throws(Tcallable c, const char * text_ex = nullptr, const char * text_e = nullptr) {
const std::type_info & tiexception = typeid(Texception);
const std::type_info & tic = typeid(decltype(c()));
report_run(text_ex ? text_ex : tiexception.name(), text_e ? text_e : tic.name());
mpt::test::result result;
try {
c();
immediate_breakpoint();
result.info = mpt::test::result_failure{};
} catch (const Texception &) {
result.info = mpt::test::result_success{};
} catch (...) {
immediate_breakpoint();
result.info = mpt::test::result_unexpected_exception{mpt::test::get_exception_text()};
}
report_result(result);
return *this;
}
template <typename Tcallable, typename std::enable_if<std::is_invocable<Tcallable>::value, bool>::type = true>
inline test & expect_throws_any(Tcallable c, const char * text_e = nullptr) {
const std::type_info & tic = typeid(decltype(c()));
report_run(nullptr, text_e ? text_e : tic.name());
mpt::test::result result;
try {
c();
immediate_breakpoint();
result.info = mpt::test::result_failure{};
} catch (...) {
result.info = mpt::test::result_success{};
}
report_result(result);
return *this;
}
template <typename Texpr, typename std::enable_if<std::is_invocable<Texpr>::value, bool>::type = true>
inline test & expect(Texpr e, const char * text_e = nullptr) {
const std::type_info & tie = typeid(decltype(std::invoke(e)));
report_run(text_e ? text_e : tie.name());
mpt::test::result result;
try {
const auto ve = std::invoke(e);
if (!ve) {
immediate_breakpoint();
result.info = mpt::test::result_failure{/*mpt::test::format(ve)*/};
} else {
result.info = mpt::test::result_success{};
}
} catch (...) {
immediate_breakpoint();
result.info = mpt::test::result_unexpected_exception{mpt::test::get_exception_text()};
}
report_result(result);
return *this;
}
template <typename Ta, typename Tcmp, typename Tb, typename std::enable_if<std::is_invocable<Ta>::value, bool>::type = true, typename std::enable_if<std::is_invocable<Tb>::value, bool>::type = true>
inline test & expect(Ta && a, Tcmp cmp, Tb && b, const char * text_a = nullptr, const char * text_cmp = nullptr, const char * text_b = nullptr) {
const std::type_info & tia = typeid(decltype(std::invoke(a)));
const std::type_info & ticmp = typeid(decltype(cmp));
const std::type_info & tib = typeid(decltype(std::invoke(b)));
report_run(text_a ? text_a : tia.name(), text_cmp ? text_cmp : ticmp.name(), text_b ? text_b : tib.name());
mpt::test::result result;
try {
const auto va = std::invoke(a);
const auto vb = std::invoke(b);
if (!cmp(va, vb)) {
immediate_breakpoint();
result.info = mpt::test::result_failure{mpt::test::format(va) + " " + mpt::test::format(cmp) + " " + mpt::test::format(vb)};
} else {
result.info = mpt::test::result_success{};
}
} catch (...) {
immediate_breakpoint();
result.info = mpt::test::result_unexpected_exception{mpt::test::get_exception_text()};
}
report_result(result);
return *this;
}
template <typename Texpr, typename std::enable_if<!std::is_invocable<Texpr>::value, bool>::type = true>
inline test & expect(Texpr && e, const char * text_e = nullptr) {
const std::type_info & tie = typeid(decltype(std::forward<Texpr>(e)));
report_run(text_e ? text_e : tie.name());
mpt::test::result result;
try {
const auto ve = std::forward<Texpr>(e);
if (!ve) {
immediate_breakpoint();
result.info = mpt::test::result_failure{/*mpt::test::format(ve)*/};
} else {
result.info = mpt::test::result_success{};
}
} catch (...) {
immediate_breakpoint();
result.info = mpt::test::result_unexpected_exception{mpt::test::get_exception_text()};
}
report_result(result);
return *this;
}
template <typename Ta, typename Tcmp, typename Tb, typename std::enable_if<!std::is_invocable<Ta>::value, bool>::type = true, typename std::enable_if<!std::is_invocable<Tb>::value, bool>::type = true>
inline test & expect(Ta && a, Tcmp cmp, Tb && b, const char * text_a = nullptr, const char * text_cmp = nullptr, const char * text_b = nullptr) {
const std::type_info & tia = typeid(decltype(std::forward<Ta>(a)));
const std::type_info & ticmp = typeid(decltype(cmp));
const std::type_info & tib = typeid(decltype(std::forward<Tb>(b)));
report_run(text_a ? text_a : tia.name(), text_cmp ? text_cmp : ticmp.name(), text_b ? text_b : tib.name());
mpt::test::result result;
try {
const auto va = std::forward<Ta>(a);
const auto vb = std::forward<Tb>(b);
if (!cmp(va, vb)) {
immediate_breakpoint();
result.info = mpt::test::result_failure{mpt::test::format(va) + " " + mpt::test::format(cmp) + " " + mpt::test::format(vb)};
} else {
result.info = mpt::test::result_success{};
}
} catch (...) {
immediate_breakpoint();
result.info = mpt::test::result_unexpected_exception{mpt::test::get_exception_text()};
}
report_result(result);
return *this;
}
};
} // namespace test
} // namespace MPT_INLINE_NS
} // namespace mpt
#endif // MPT_TEST_TEST_HPP