diff --git a/unit_test_framework.hpp b/unit_test_framework.hpp new file mode 100644 index 0000000..f4ec447 --- /dev/null +++ b/unit_test_framework.hpp @@ -0,0 +1,751 @@ +#ifndef UNIT_TEST_FRAMEWORK_HPP +#define UNIT_TEST_FRAMEWORK_HPP + +// C++ unit-test framework inspired by xUnit and its derivatives. +// +// Authors: +// James Perretta +// Amir Kamil +// +// See https://github.com/eecs280staff/unit_test_framework for +// documentation and updates. +// +// License information: +// +// The MIT License (MIT) +// +// Copyright 2017-2024 James Perretta and Amir Kamil +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, copy, +// modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if UNIT_TEST_ENABLE_REGEXP +# include +#endif + +// For compatibility with Visual Studio +#include // ciso646 removed in C++20 + +// For demangling type names +#if defined(__clang__) || defined(__GLIBCXX__) || defined(__GLIBCPP__) +# include +# include +#endif + + +// Place the following line of code in your test file to generate a +// main() function: +// TEST_MAIN() + + +#define TEST(name) \ + static void name(); \ + static unit_test_framework::TestRegisterer \ + register_##name((#name), name); \ + static void name() + +#define TEST_MAIN() \ + int main(int argc, char** argv) { \ + return \ + unit_test_framework::TestSuite::get().run_tests(argc, argv); \ + } \ + TEST_SUITE_INSTANCE(); + +#define ASSERT_EQUAL(first, second) \ + unit_test_framework::Assertions::assert_equal( \ + (first), (second), __LINE__, \ + "ASSERT_EQUAL(" #first ", " #second ")" \ + ); + +#define ASSERT_NOT_EQUAL(first, second) \ + unit_test_framework::Assertions::assert_not_equal( \ + (first), (second), __LINE__, \ + "ASSERT_NOT_EQUAL(" #first ", " #second ")" \ + ); + +#define ASSERT_SEQUENCE_EQUAL(first, second) \ + unit_test_framework::Assertions::assert_sequence_equal( \ + (first), (second), __LINE__, \ + "ASSERT_SEQUENCE_EQUAL(" #first ", " #second ")" \ + ); + +#define ASSERT_TRUE(value) \ + unit_test_framework::Assertions::assert_true( \ + (value), __LINE__, "ASSERT_TRUE(" #value ")" \ + ); + +#define ASSERT_FALSE(value) \ + unit_test_framework::Assertions::assert_false( \ + (value), __LINE__, "ASSERT_FALSE(" #value ")" \ + ); + +#define ASSERT_ALMOST_EQUAL(first, second, precision) \ + unit_test_framework::Assertions::assert_almost_equal( \ + (first), (second), (precision), __LINE__, \ + "ASSERT_ALMOST_EQUAL(" #first ", " #second ", " \ + #precision ")" \ + ); + +// ----------------------------------------------------------------------------- + +namespace unit_test_framework { + + using Test_func_t = void (*)(); + + class ExitSuite : public std::exception { + public: + ExitSuite(int status_ = 0) : status(status_) {} + int status; + }; + + class SetComplete { + public: + SetComplete(bool& incomplete_) : incomplete(incomplete_) { + incomplete = true; + } + ~SetComplete() { + incomplete = false; + } + + private: + bool& incomplete; + }; + + class TestFailure { + public: + TestFailure(std::string reason, int line_number, const char* assertion_text) + : reason_m(std::move(reason)), line_number_m(line_number), + assertion_text_m(assertion_text) {} + + std::ostream& print(std::ostream& os) const { + os << "In " << assertion_text_m << ", line " << line_number_m << ":\n" + << reason_m << '\n'; + return os; + } + + std::string to_string() const { + std::ostringstream oss; + print(oss); + return oss.str(); + } + + friend std::ostream& operator<<(std::ostream& os, + const TestFailure& test_failure) { + return test_failure.print(os); + } + + private: + std::string reason_m; + int line_number_m; + const char* assertion_text_m; + }; + + // --------------------------------------------------------------------------- + + class Diagnostic { + public: + // Demangles a string produced by std::type_info::name. + static std::string demangle(const char* typeinfo_name) { +#if defined(__clang__) || defined(__GLIBCXX__) || defined(__GLIBCPP__) + int status = 0; + char* demangled = + abi::__cxa_demangle(typeinfo_name, nullptr, nullptr, &status); + if (status == 0) { + std::string result = demangled; + std::free(demangled); + return result; + } + else { + return typeinfo_name; + } +#else + return typeinfo_name; +#endif // defined(__clang__) || defined(__GLIBCXX__) || defined(__GLIBCPP__) + } + + // Attempts to print the given object to the given stream. + // If T has an available stream insertion operator overload, that + // operator is used. Otherwise, a generic representation of the object + // is printed to os. + template + static std::ostream& print(std::ostream& os, const T& t) { + // The extra parameters are needed so that the first overload of + // print_helper is preferred, followed by the third one. + return print_helper(os, t, 0, 0); + } + + private: + // This version of print_helper will be called when T has an available + // stream insertion operator overload. + template + static auto print_helper(std::ostream& os, const T& t, int, int) + -> decltype(os << t)& { + return os << t; + } + + // This version of print_helper will be called when T is a pair. + template + static auto print_helper(std::ostream& os, + const std::pair& t, int, + int) + -> decltype(print(os, t.first), print(os, t.second))& { + os << '('; + print(os, t.first); + os << ','; + print(os, t.second); + return os << ')'; + } + + // Helper function to print a sequence. + template + static auto print_sequence_helper(std::ostream &os, const Sequence& seq) + -> decltype(print(os, (*std::begin(seq), *std::end(seq))))& { + if (std::begin(seq) == std::end(seq)) { + return os << "{}"; + } + + auto it = std::begin(seq); + os << "{ "; + print(os, *it); + for (++it; it != std::end(seq); ++it) { + os << ", "; + print(os, *it); + } + return os << " }"; + } + + // This version of print_helper will be called when T is a sequence. + template + static auto print_helper(std::ostream& os, const Sequence& seq, int, ...) + -> decltype(print(os, *seq.begin()), print(os, *seq.end()))& { + return print_sequence_helper(os, seq); + } + + // This version of print_helper will be called when T is a non-char array. + // This is separate from the sequence overload so that printing an + // array as a sequence is preferred over printing it as a pointer + // (using the first overload). + template + static std::ostream& print_helper(std::ostream& os, const Elem (&arr)[N], + int, int) { + return print_sequence_helper(os, arr); + } + + // This version of print_helper will be called when T is a char array. + // If the array contains a null terminator, it is printed as a string. + // Otherwise, it is printed as a sequence. + template + static std::ostream& print_helper(std::ostream& os, const char (&arr)[N], + int, int) { + for (std::size_t i = 0; i < N; ++i) { + if (!arr[i]) { + return os << arr; + } + } + return print_sequence_helper(os, arr); + } + + // This version of print_helper will be called when T does not have an + // available stream insertion operator overload. + template + static std::ostream& print_helper(std::ostream& os, const T&, ...) { + return os << "<" << demangle(typeid(T).name()) << " object>"; + } + }; + + // --------------------------------------------------------------------------- + + struct TestCase { + TestCase(const std::string& name_, Test_func_t test_func_) + : name(name_), test_func(test_func_) {} + + void run(bool quiet_mode) { + try { + if (not quiet_mode) { + std::cout << "Running test: " << name << std::endl; + } + + test_func(); + + if (not quiet_mode) { + std::cout << "PASS" << std::endl; + } + } + catch (TestFailure& failure) { + failure_msg = failure.to_string(); + + if (not quiet_mode) { + std::cout << "FAIL" << std::endl; + } + } + catch (std::exception& e) { + std::ostringstream oss; + oss << "Uncaught " << Diagnostic::demangle(typeid(e).name()) + << " in test \"" << name << "\": \n"; + oss << e.what() << '\n'; + exception_msg = oss.str(); + + if (not quiet_mode) { + std::cout << "ERROR" << std::endl; + } + } + } + + void print(bool quiet_mode) { + if (quiet_mode) { + std::cout << name << ": "; + } + else { + std::cout << "** Test case \"" << name << "\": "; + } + + if (not failure_msg.empty()) { + std::cout << "FAIL" << std::endl; + if (not quiet_mode) { + std::cout << failure_msg << std::endl; + } + } + else if (not exception_msg.empty()) { + std::cout << "ERROR" << std::endl; + if (not quiet_mode) { + std::cout << exception_msg << std::endl; + } + } + else { + std::cout << "PASS" << std::endl; + } + } + + std::string name; + Test_func_t test_func; + std::string failure_msg{}; + std::string exception_msg{}; + }; + + + class TestSuite { + public: + static TestSuite& get() { + if (not instance) { + instance = new TestSuite; + } + return *instance; + } + + void add_test(const std::string& test_name, Test_func_t test) { + tests_.insert({test_name, TestCase{test_name, test}}); + } + + int run_tests(int argc, char** argv) { + SetComplete completer(TestSuite::incomplete); + std::vector test_names_to_run; + try { + test_names_to_run = get_test_names_to_run(argc, argv); + } + catch (ExitSuite& e) { + return e.status; + } + + for (auto test_name : test_names_to_run) { + if (tests_.find(test_name) == end(tests_)) { + throw std::runtime_error("Test " + test_name + " not found"); + } + } + + for (auto test_name : test_names_to_run) { + tests_.at(test_name).run(quiet_mode); + } + + std::cout << "\n*** Results ***" << std::endl; + for (auto test_name : test_names_to_run) { + tests_.at(test_name).print(quiet_mode); + } + + auto num_failures = + std::count_if(tests_.begin(), tests_.end(), + [](std::pair test_pair) { + return not test_pair.second.failure_msg.empty(); + }); + auto num_errors = + std::count_if(tests_.begin(), tests_.end(), + [](std::pair test_pair) { + return not test_pair.second.exception_msg.empty(); + }); + + if (not quiet_mode) { + std::cout << "*** Summary ***" << std::endl; + std::cout << "Out of " << test_names_to_run.size() + << " tests run:" << std::endl; + std::cout << num_failures << " failure(s), " << num_errors + << " error(s)" << std::endl; + } + + if (num_failures == 0 and num_errors == 0) { + return 0; + } + return 1; + } + + void print_results(); + + void enable_quiet_mode() { + quiet_mode = true; + } + + std::ostream& print_test_names(std::ostream& os) { + for (const auto& test_pair : tests_) { + os << test_pair.first << '\n'; + } + return os; + } + + friend class TestSuiteDestroyer; + + private: + TestSuite() { + auto func = []() { + if (TestSuite::incomplete) { + std::cout << "ERROR: premature call to exit()" << std::endl; + std::abort(); + } + }; + std::atexit(func); +#ifdef _GLIBCXX_HAVE_AT_QUICK_EXIT + std::at_quick_exit(func); +#endif + } + TestSuite(const TestSuite&) = delete; + bool operator=(const TestSuite&) = delete; + ~TestSuite() {} + + std::vector get_test_names_to_run(int argc, char** argv) { + std::vector test_names_to_run; +#if UNIT_TEST_ENABLE_REGEXP + bool regexp_matching = false; +#endif + for (auto i = 1; i < argc; ++i) { + if (argv[i] == std::string("--show_test_names") or + argv[i] == std::string("-n")) { + + TestSuite::get().print_test_names(std::cout); + std::cout << std::flush; + throw ExitSuite(); + } + else if (argv[i] == std::string("--quiet") or + argv[i] == std::string("-q")) { + TestSuite::get().enable_quiet_mode(); + } +#if UNIT_TEST_ENABLE_REGEXP + else if (argv[i] == std::string("--regexp") or + argv[i] == std::string("-e")) { + regexp_matching = true; + } +#endif + else if (argv[i] == std::string("--help") or + argv[i] == std::string("-h")) { + std::cout << "usage: " << argv[0] +#if UNIT_TEST_ENABLE_REGEXP + << " [-h] [-e] [-n] [-q] [[TEST_NAME] ...]\n"; +#else + << " [-h] [-n] [-q] [[TEST_NAME] ...]\n"; +#endif + std::cout + << "optional arguments:\n" + << " -h, --help\t\t show this help message and exit\n" +#if UNIT_TEST_ENABLE_REGEXP + << " -e, --regexp\t\t treat TEST_NAME as a regular expression\n" +#endif + << " -n, --show_test_names\t print the names of all " + "discovered test cases and exit\n" + << " -q, --quiet\t\t print a reduced summary of test results\n" + << " TEST_NAME ...\t\t run only the test cases whose names " + "are " + "listed here. Note: If no test names are specified, all " + "discovered tests are run by default." + << std::endl; + + throw ExitSuite(); + } + else { + test_names_to_run.push_back(argv[i]); + } + } + + if (test_names_to_run.empty()) { + std::transform( + std::begin(tests_), std::end(tests_), + std::back_inserter(test_names_to_run), + [](const std::pair& p) { + return p.first; + }); + } +#if UNIT_TEST_ENABLE_REGEXP + else if (regexp_matching) { + std::ostringstream pattern; + for (auto iter = test_names_to_run.begin(); + iter != test_names_to_run.end(); ++iter) { + if (iter != test_names_to_run.begin()) { + pattern << "|"; + } + pattern << "(" << *iter << ")"; + } + std::regex name_regex{pattern.str()}; + test_names_to_run.clear(); + for (const auto& test_pair : tests_) { + if (std::regex_match(test_pair.first, name_regex)) { + test_names_to_run.push_back(test_pair.first); + } + } + } +#endif + return test_names_to_run; + } + + static TestSuite* instance; + std::map tests_; + + bool quiet_mode = false; + static bool incomplete; + }; + + class TestSuiteDestroyer { + public: + ~TestSuiteDestroyer() { + delete TestSuite::instance; + } + }; + + class TestRegisterer { + public: + TestRegisterer(const std::string& test_name, Test_func_t test) { + TestSuite::get().add_test(test_name, test); + } + }; + + // --------------------------------------------------------------------------- + + class Assertions { + private: + // Template logic to produce a static assertion failure when comparing + // incomparable types. + template + struct is_equality_comparable : std::false_type {}; + + template + using enable_if_equality_comparable = typename std::enable_if< + std::is_same() == + std::declval())>::value and + std::is_same() != + std::declval())>::value and + (!std::is_array::type>::value or + !std::is_array::type>::value), + void>::type; + + template + struct is_equality_comparable> + : std::true_type {}; + + // Overloads for equality comparisons. + template + static bool safe_equals_helper(const First& first, const Second& second) { + return first == second; + } + + template + static bool safe_not_equals_helper(const First& first, + const Second& second) { + return first != second; + } + + // Allow size_t to correctly be compared to int. + static bool safe_equals_helper(std::size_t first, int second) { + return second >= 0 && static_cast(first) == second; + } + + static bool safe_equals_helper(int first, std::size_t second) { + return first >= 0 && first == static_cast(second); + } + + static bool safe_not_equals_helper(std::size_t first, int second) { + return second < 0 || static_cast(first) != second; + } + + static bool safe_not_equals_helper(int first, std::size_t second) { + return first < 0 || first != static_cast(second); + } + + template + struct safe_equals { + static_assert(is_equality_comparable::value, + "types cannot be compared with == and !="); + static bool equals(const First& first, const Second& second) { + return safe_equals_helper(first, second); + } + static bool not_equals(const First& first, const Second& second) { + return safe_not_equals_helper(first, second); + } + }; + + public: + template + static void assert_equal(First&& first, Second&& second, + int line_number, + const char* assertion_text) { + if (safe_equals::equals(first, second)) { + return; + } + std::ostringstream reason; + Diagnostic::print(reason, first); + reason << " != "; + Diagnostic::print(reason, second); + throw TestFailure(reason.str(), line_number, assertion_text); + } + + template + static void assert_not_equal(First&& first, Second&& second, + int line_number, + const char* assertion_text) { + if (safe_equals::not_equals(first, second)) { + return; + } + std::ostringstream reason; + + reason << "Values unexpectedly equal: "; + Diagnostic::print(reason, first); + reason << " == "; + Diagnostic::print(reason, second); + throw TestFailure(reason.str(), line_number, assertion_text); + } + + template + static void assert_sequence_equal(First&& first, Second&& second, + int line_number, + const char* assertion_text) { + using std::begin; + using std::end; + auto it1 = begin(first); + auto it2 = begin(second); + auto end1 = end(first); + auto end2 = end(second); + auto len1 = std::distance(it1, end1); + auto len2 = std::distance(it2, end2); + if (len1 != len2) { // different number of elements + std::ostringstream reason; + Diagnostic::print(reason, first); + reason << " != "; + Diagnostic::print(reason, second); + reason << " (sizes differ: " << len1 << " != " << len2 << ")"; + throw TestFailure(reason.str(), line_number, assertion_text); + } + + bool equal = true; + std::size_t position = 0; + for (; it1 != end1 and it2 != end2; ++it1, ++it2, ++position) { + if (not safe_equals::equals(*it1, *it2)) { + equal = false; + break; + } + } + + if (not equal) { + std::ostringstream reason; + Diagnostic::print(reason, first); + reason << " != "; + Diagnostic::print(reason, second); + reason << " (elements at position " << position << " differ: "; + Diagnostic::print(reason, *it1); + reason << " != "; + Diagnostic::print(reason, *it2); + reason << ")"; + throw TestFailure(reason.str(), line_number, assertion_text); + } + } + + + static void assert_true(bool value, int line_number, + const char* assertion_text) { + if (value) { + return; + } + std::ostringstream reason; + reason << "Expected true, but was false"; + throw TestFailure(reason.str(), line_number, assertion_text); + } + + static void assert_false(bool value, int line_number, + const char* assertion_text) { + if (not value) { + return; + } + std::ostringstream reason; + reason << "Expected false, but was true"; + throw TestFailure(reason.str(), line_number, assertion_text); + } + + static void assert_almost_equal(double first, double second, + double precision, int line_number, + const char* assertion_text) { + if (std::abs(first - second) <= precision) { + return; + } + std::ostringstream reason; + // For now, we'll just set the precision arbitrarily high. + // In the future, we may decide to add an option to configure + // the output precision. + reason.precision(20); + reason << "Values too far apart: " << first << " and " << second; + throw TestFailure(reason.str(), line_number, assertion_text); + } + }; + +} // namespace unit_test_framework + +//------------------------------------------------------------------------------ + +// THIS IS PART OF A WORKAROUND TO DEAL WITH STATIC INITIALIZATION +// SHENANIGANS. +// DO NOT CHANGE THIS UNLESS YOU REEEEALLY KNOW WHAT YOU'RE DOING. +// CONTACT akamil@umich.edu or jameslp@umich.edu IF YOU HAVE QUESTIONS +// ABOUT THIS. +#define TEST_SUITE_INSTANCE() \ + static unit_test_framework::TestSuiteDestroyer destroyer; \ + bool unit_test_framework::TestSuite::incomplete = false; \ + unit_test_framework::TestSuite* \ + unit_test_framework::TestSuite::instance = \ + &unit_test_framework::TestSuite::get() + +#endif // UNIT_TEST_FRAMEWORK_HPP