diff --git a/AK/TestSuite.h b/AK/TestSuite.h new file mode 100644 index 0000000000..73f166d0c4 --- /dev/null +++ b/AK/TestSuite.h @@ -0,0 +1,241 @@ +#pragma once + +#include "AKString.h" +#include "Function.h" +#include "NonnullRefPtrVector.h" +#include + +namespace AK { + +class TestElapsedTimer { + typedef std::chrono::high_resolution_clock clock; + +public: + TestElapsedTimer() { restart(); } + void restart() { m_started = clock::now(); } + int64_t elapsed() + { + auto end = clock::now(); + auto elapsed = end - m_started; + return std::chrono::duration_cast(elapsed).count(); + } + +private: + std::chrono::time_point m_started; +}; + +class TestException { +public: + TestException(const String& file, int line, const String& s) + : file(file) + , line(line) + , reason(s) + { + } + + String to_string() const + { + String outfile = file; + // ### + //auto slash = file.lastIndexOf("/"); + //if (slash > 0) { + // outfile = outfile.right(outfile.length() - slash - 1); + //} + return String::format("%s:%d: %s", outfile.characters(), line, reason.characters()); + } + +private: + String file; + int line = 0; + String reason; +}; + +typedef AK::Function TestFunction; + +class TestCase : public RefCounted { +public: + TestCase(const String& name, TestFunction&& fn, bool is_benchmark) + : m_name(name) + , m_function(move(fn)) + , m_is_benchmark(is_benchmark) + { + } + + bool is_benchmark() const { return m_is_benchmark; } + const String& name() const { return m_name; } + const TestFunction& func() const { return m_function; } + +private: + String m_name; + TestFunction m_function; + bool m_is_benchmark; +}; + +class TestSuite { +public: + static TestSuite* instance() + { + if (s_global == nullptr) + s_global = new TestSuite(); + return s_global; + } + void run(const NonnullRefPtrVector& tests); + void main(const String& suite_name, int argc, char** argv); + NonnullRefPtrVector find_cases(const String& search, bool find_tests, bool find_benchmarks); + void add_case(const NonnullRefPtr& test_case) + { + m_cases.append(test_case); + } + +private: + static TestSuite* s_global; + NonnullRefPtrVector m_cases; + uint64_t m_testtime = 0; + uint64_t m_benchtime = 0; + String m_suite_name; +}; + +void TestSuite::main(const String& suite_name, int argc, char** argv) +{ + m_suite_name = suite_name; + bool find_tests = true; + bool find_benchmarks = true; + + String search_string; + for (int i = 1; i < argc; i++) { + if (!String(argv[i]).starts_with("--")) { + search_string = argv[i]; + } else if (String(argv[i]) == String("--bench")) { + find_tests = false; + } else if (String(argv[i]) == String("--test")) { + find_benchmarks = false; + } else if (String(argv[i]) == String("--help")) { + dbg() << "Available tests for " << suite_name << ":"; + const auto& tests = find_cases("*", true, false); + for (const auto& t : tests) { + dbg() << "\t" << t.name(); + } + dbg() << "Available benchmarks for " << suite_name << ":"; + const auto& benches = find_cases("*", false, true); + for (const auto& t : benches) { + dbg() << "\t" << t.name(); + } + exit(0); + } + } + + const auto& matches = find_cases(search_string, find_tests, find_benchmarks); + if (matches.size() == 0) { + dbg() << "0 matches when searching for " << search_string << " (out of " << m_cases.size() << ")"; + exit(1); + } + dbg() << "Running " << matches.size() << " cases out of " << m_cases.size(); + run(matches); +} + +NonnullRefPtrVector TestSuite::find_cases(const String& search, bool find_tests, bool find_benchmarks) +{ + NonnullRefPtrVector matches; + for (const auto& t : m_cases) { + if (!search.is_empty() && !t.name().matches(search, String::CaseSensitivity::CaseInsensitive)) { + continue; + } + + if (!find_tests && !t.is_benchmark()) { + continue; + } + if (!find_benchmarks && t.is_benchmark()) { + continue; + } + + matches.append(t); + } + return matches; +} + +void TestSuite::run(const NonnullRefPtrVector& tests) +{ + int test_count = 0; + int benchmark_count = 0; + TestElapsedTimer global_timer; + for (const auto& t : tests) { + dbg() << "START Running " << (t.is_benchmark() ? "benchmark" : "test") << " " << t.name(); + TestElapsedTimer timer; + try { + t.func(); + } catch (const TestException& t) { + fprintf(stderr, "\033[31;1mFAIL\033[0m: %s\n", t.to_string().characters()); + exit(1); + } + auto time = timer.elapsed(); + fprintf(stderr, "\033[32;1mPASS\033[0m: %d ms running %s %s\n", (int)time, (t.is_benchmark() ? "benchmark" : "test"), t.name().characters()); + if (t.is_benchmark()) { + m_benchtime += time; + benchmark_count++; + } else { + m_testtime += time; + test_count++; + } + } + dbg() << "Finished " << test_count << " tests and " << benchmark_count << " benchmarks in " << (int)global_timer.elapsed() << " ms (" + << (int)m_testtime << " tests, " << (int)m_benchtime << " benchmarks, " << int(global_timer.elapsed() - (m_testtime + m_benchtime)) << " other)"; +} + +} + +using AK::TestCase; +using AK::TestException; +using AK::TestSuite; + +#define xstr(s) ___str(s) +#define ___str(s) #s + +#define TESTCASE_TYPE_NAME(x) TestCase_##x + +/*! Define a test case function. */ +#define TEST_CASE(x) \ + static void x(); \ + struct TESTCASE_TYPE_NAME(x) { \ + TESTCASE_TYPE_NAME(x) \ + () { TestSuite::instance()->add_case(adopt(*new TestCase(___str(x), x, false))); } \ + }; \ + static struct TESTCASE_TYPE_NAME(x) TESTCASE_TYPE_NAME(x); \ + static void x() + +#define BENCHMARK_TYPE_NAME(x) TestCase_##x + +#define BENCHMARK_CASE(x) \ + static void x(); \ + struct BENCHMARK_TYPE_NAME(x) { \ + BENCHMARK_TYPE_NAME(x) \ + () { TestSuite::instance()->add_case(adopt(*new TestCase(___str(x), x, true))); } \ + }; \ + static struct BENCHMARK_TYPE_NAME(x) BENCHMARK_TYPE_NAME(x); \ + static void x() + +/*! Define the main function of the testsuite. All TEST_CASE functions will be executed. */ +#define TEST_MAIN(SuiteName) \ + TestSuite* TestSuite::s_global = nullptr; \ + template \ + constexpr size_t compiletime_lenof(const char(&)[N]) \ + { \ + return N - 1; \ + } \ + int main(int argc, char** argv) \ + { \ + static_assert(compiletime_lenof(___str(SuiteName)) != 0, "Set SuiteName"); \ + TestSuite::instance()->main(___str(SuiteName), argc, argv); \ + } + +#define assertEqual(one, two) \ + do { \ + auto ___aev1 = one; \ + auto ___aev2 = two; \ + if (___aev1 != ___aev2) { \ + const auto& msg = String::format("\033[31;1mFAIL\033[0m: assertEqual(" ___str(one) ", " ___str(two) ") failed"); \ + } \ + } while (0) + +#define EXPECT_EQ(one, two) assertEqual(one, two) + +#define EXPECT(one) assertEqual(one, true) diff --git a/AK/Tests/Makefile b/AK/Tests/Makefile index ecfc22750e..04acb7a270 100644 --- a/AK/Tests/Makefile +++ b/AK/Tests/Makefile @@ -4,20 +4,20 @@ all: $(PROGRAMS) CXXFLAGS = -std=c++17 -Wall -Wextra -TestString: TestString.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp TestHelpers.h - $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestString.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp +TestString: TestString.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../TestSuite.h ../LogStream.cpp + $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestString.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../LogStream.cpp -TestQueue: TestQueue.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp TestHelpers.h - $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestQueue.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp +TestQueue: TestQueue.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../TestSuite.h ../LogStream.cpp + $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestQueue.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../LogStream.cpp -TestVector: TestVector.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp TestHelpers.h - $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestVector.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp +TestVector: TestVector.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../TestSuite.h ../LogStream.cpp + $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestVector.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../LogStream.cpp -TestHashMap: TestHashMap.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp TestHelpers.h - $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestHashMap.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp +TestHashMap: TestHashMap.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../TestSuite.h ../LogStream.cpp + $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestHashMap.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../LogStream.cpp -TestJSON: TestJSON.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp TestHelpers.h ../JsonObject.cpp ../JsonValue.cpp ../JsonArray.cpp ../JsonParser.cpp - $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestJSON.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../JsonObject.cpp ../JsonValue.cpp ../JsonArray.cpp ../JsonParser.cpp +TestJSON: TestJSON.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../TestSuite.h ../LogStream.cpp ../JsonObject.cpp ../JsonValue.cpp ../JsonArray.cpp ../JsonParser.cpp + $(CXX) $(CXXFLAGS) -I../ -I../../ -o $@ TestJSON.cpp ../String.cpp ../StringImpl.cpp ../StringBuilder.cpp ../StringView.cpp ../LogStream.cpp ../JsonObject.cpp ../JsonValue.cpp ../JsonArray.cpp ../JsonParser.cpp clean: rm -f $(PROGRAMS) diff --git a/AK/Tests/TestHashMap.cpp b/AK/Tests/TestHashMap.cpp index 55455b1d16..06ada64459 100644 --- a/AK/Tests/TestHashMap.cpp +++ b/AK/Tests/TestHashMap.cpp @@ -1,27 +1,46 @@ -#include "TestHelpers.h" +#include #include #include -typedef HashMap IntIntMap; - -int main() +TEST_CASE(construct) { + typedef HashMap IntIntMap; EXPECT(IntIntMap().is_empty()); - EXPECT(IntIntMap().size() == 0); + EXPECT_EQ(IntIntMap().size(), 0); +} +TEST_CASE(populate) +{ HashMap number_to_string; number_to_string.set(1, "One"); number_to_string.set(2, "Two"); number_to_string.set(3, "Three"); - + EXPECT_EQ(number_to_string.is_empty(), false); EXPECT_EQ(number_to_string.size(), 3); +} + +TEST_CASE(range_loop) +{ + HashMap number_to_string; + number_to_string.set(1, "One"); + number_to_string.set(2, "Two"); + number_to_string.set(3, "Three"); int loop_counter = 0; for (auto& it : number_to_string) { - EXPECT(!it.value.is_null()); + EXPECT_EQ(it.value.is_null(), false); ++loop_counter; } + EXPECT_EQ(loop_counter, 3); +} + +TEST_CASE(map_remove) +{ + HashMap number_to_string; + number_to_string.set(1, "One"); + number_to_string.set(2, "Two"); + number_to_string.set(3, "Three"); number_to_string.remove(1); EXPECT_EQ(number_to_string.size(), 2); @@ -30,16 +49,16 @@ int main() number_to_string.remove(3); EXPECT_EQ(number_to_string.size(), 1); EXPECT(number_to_string.find(3) == number_to_string.end()); - - EXPECT_EQ(loop_counter, 3); - - { - HashMap casemap; - EXPECT_EQ(String("nickserv").to_lowercase(), String("NickServ").to_lowercase()); - casemap.set("nickserv", 3); - casemap.set("NickServ", 3); - EXPECT_EQ(casemap.size(), 1); - } - - return 0; + EXPECT(number_to_string.find(2) != number_to_string.end()); } + +TEST_CASE(case_insensitive) +{ + HashMap casemap; + EXPECT_EQ(String("nickserv").to_lowercase(), String("NickServ").to_lowercase()); + casemap.set("nickserv", 3); + casemap.set("NickServ", 3); + EXPECT_EQ(casemap.size(), 1); +} + +TEST_MAIN(HashMap) diff --git a/AK/Tests/TestHelpers.h b/AK/Tests/TestHelpers.h deleted file mode 100644 index 311eb6ad1f..0000000000 --- a/AK/Tests/TestHelpers.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include -#include - -#define LOG_FAIL(cond) \ - fprintf(stderr, "\033[31;1mFAIL\033[0m: " #cond "\n") - -#define LOG_PASS(cond) \ - fprintf(stderr, "\033[32;1mPASS\033[0m: " #cond "\n") - -#define LOG_FAIL_EQ(cond, expected_value, actual_value) \ - fprintf(stderr, "\033[31;1mFAIL\033[0m: " #cond " should be " #expected_value ", got "); \ - stringify_for_test(actual_value); \ - fprintf(stderr, "\n") - -#define LOG_PASS_EQ(cond, expected_value) \ - fprintf(stderr, "\033[32;1mPASS\033[0m: " #cond " should be " #expected_value " and it is\n") - -#define EXPECT_EQ(expr, expected_value) \ - do { \ - auto result = (expr); \ - if (!(result == expected_value)) { \ - LOG_FAIL_EQ(expr, expected_value, result); \ - } else { \ - LOG_PASS_EQ(expr, expected_value); \ - } \ - } while(0) - -#define EXPECT(cond) \ - do { \ - if (!(cond)) { \ - LOG_FAIL(cond); \ - } else { \ - LOG_PASS(cond); \ - } \ - } while(0) - -inline void stringify_for_test(int value) -{ - fprintf(stderr, "%d", value); -} - -inline void stringify_for_test(unsigned value) -{ - fprintf(stderr, "%u", value); -} - -inline void stringify_for_test(const char* value) -{ - fprintf(stderr, "%s", value); -} - -inline void stringify_for_test(char value) -{ - fprintf(stderr, "%c", value); -} - -inline void stringify_for_test(const AK::String& string) -{ - stringify_for_test(string.characters()); -} - -inline void stringify_for_test(const AK::StringImpl& string) -{ - stringify_for_test(string.characters()); -} - diff --git a/AK/Tests/TestJSON.cpp b/AK/Tests/TestJSON.cpp index 72fb007de5..875ba6b99a 100644 --- a/AK/Tests/TestJSON.cpp +++ b/AK/Tests/TestJSON.cpp @@ -1,4 +1,4 @@ -#include "TestHelpers.h" +#include #include #include #include @@ -6,9 +6,7 @@ #include #include -typedef HashMap IntIntMap; - -int main() +TEST_CASE(load_form) { FILE* fp = fopen("../../Base/home/anon/test.frm", "r"); ASSERT(fp); @@ -42,6 +40,6 @@ int main() //dbgprintf("Set property %s.%s to '%s'\n", widget_class.characters(), property_name.characters(), property_value.serialized().characters()); }); }); - - return 0; } + +TEST_MAIN(JSON) diff --git a/AK/Tests/TestQueue.cpp b/AK/Tests/TestQueue.cpp index a604074215..23d30f5de2 100644 --- a/AK/Tests/TestQueue.cpp +++ b/AK/Tests/TestQueue.cpp @@ -1,12 +1,15 @@ -#include "TestHelpers.h" +#include #include #include -int main() +TEST_CASE(construct) { EXPECT(Queue().is_empty()); EXPECT(Queue().size() == 0); +} +TEST_CASE(populate_int) +{ Queue ints; ints.enqueue(1); ints.enqueue(2); @@ -18,7 +21,10 @@ int main() EXPECT_EQ(ints.size(), 1); EXPECT_EQ(ints.dequeue(), 3); EXPECT_EQ(ints.size(), 0); +} +TEST_CASE(populate_string) +{ Queue strings; strings.enqueue("ABC"); strings.enqueue("DEF"); @@ -26,6 +32,12 @@ int main() EXPECT_EQ(strings.dequeue(), "ABC"); EXPECT_EQ(strings.dequeue(), "DEF"); EXPECT(strings.is_empty()); +} + +TEST_CASE(order) +{ + Queue strings; + EXPECT(strings.is_empty()); for (int i = 0; i < 10000; ++i) { strings.enqueue(String::number(i)); @@ -38,6 +50,6 @@ int main() } EXPECT(strings.is_empty()); - - return 0; } + +TEST_MAIN(Queue) diff --git a/AK/Tests/TestString.cpp b/AK/Tests/TestString.cpp index 808d0904be..a8a926d1ec 100644 --- a/AK/Tests/TestString.cpp +++ b/AK/Tests/TestString.cpp @@ -1,7 +1,7 @@ -#include "TestHelpers.h" +#include #include -int main() +TEST_CASE(construct_empty) { EXPECT(String().is_null()); EXPECT(String().is_empty()); @@ -9,22 +9,29 @@ int main() EXPECT(!String("").is_null()); EXPECT(String("").is_empty()); - EXPECT(String("").characters()); + EXPECT(String("").characters() != nullptr); EXPECT(String("").impl() == String::empty().impl()); +} +TEST_CASE(construct_contents) +{ String test_string = "ABCDEF"; EXPECT(!test_string.is_empty()); EXPECT(!test_string.is_null()); EXPECT_EQ(test_string.length(), 6); EXPECT_EQ(test_string.length(), (int)strlen(test_string.characters())); - EXPECT(test_string.characters()); + EXPECT(test_string.characters() != nullptr); EXPECT(!strcmp(test_string.characters(), "ABCDEF")); EXPECT(test_string == "ABCDEF"); EXPECT(test_string != "ABCDE"); EXPECT(test_string != "ABCDEFG"); +} +TEST_CASE(compare) +{ + String test_string = "ABCDEF"; EXPECT("a" < String("b")); EXPECT(!("a" > String("b"))); EXPECT("b" > String("a")); @@ -33,36 +40,70 @@ int main() EXPECT(!("a" >= String("b"))); EXPECT("a" <= String("a")); EXPECT(!("b" <= String("a"))); +} +TEST_CASE(index_access) +{ + String test_string = "ABCDEF"; EXPECT_EQ(test_string[0], 'A'); EXPECT_EQ(test_string[1], 'B'); +} +TEST_CASE(starts_with) +{ + String test_string = "ABCDEF"; EXPECT(test_string.starts_with("AB")); EXPECT(test_string.starts_with("ABCDEF")); EXPECT(!test_string.starts_with("DEF")); +} +TEST_CASE(ends_with) +{ + String test_string = "ABCDEF"; EXPECT(test_string.ends_with("EF")); EXPECT(test_string.ends_with("ABCDEF")); EXPECT(!test_string.ends_with("ABC")); +} +TEST_CASE(copy_string) +{ + String test_string = "ABCDEF"; auto test_string_copy = test_string; EXPECT_EQ(test_string, test_string_copy); EXPECT_EQ(test_string.characters(), test_string_copy.characters()); +} +TEST_CASE(move_string) +{ + String test_string = "ABCDEF"; + auto test_string_copy = test_string; auto test_string_move = move(test_string_copy); EXPECT_EQ(test_string, test_string_move); EXPECT(test_string_copy.is_null()); +} +TEST_CASE(repeated) +{ EXPECT_EQ(String::repeated('x', 0), ""); EXPECT_EQ(String::repeated('x', 1), "x"); EXPECT_EQ(String::repeated('x', 2), "xx"); +} +TEST_CASE(to_int) +{ bool ok; EXPECT(String("123").to_int(ok) == 123 && ok); EXPECT(String("-123").to_int(ok) == -123 && ok); - - EXPECT(String("ABC").to_lowercase() == "abc"); - EXPECT(String("AbC").to_uppercase() == "ABC"); - - return 0; } + +TEST_CASE(to_lowercase) +{ + EXPECT(String("ABC").to_lowercase() == "abc"); +} + +TEST_CASE(to_uppercase) +{ + EXPECT(String("AbC").to_uppercase() == "ABC"); +} + +TEST_MAIN(String) diff --git a/AK/Tests/TestVector.cpp b/AK/Tests/TestVector.cpp index d8f8fa405c..ad281d3b0c 100644 --- a/AK/Tests/TestVector.cpp +++ b/AK/Tests/TestVector.cpp @@ -1,12 +1,15 @@ -#include "TestHelpers.h" +#include #include #include -int main() +TEST_CASE(construct) { EXPECT(Vector().is_empty()); EXPECT(Vector().size() == 0); +} +TEST_CASE(ints) +{ Vector ints; ints.append(1); ints.append(2); @@ -21,7 +24,10 @@ int main() ints.clear(); EXPECT_EQ(ints.size(), 0); +} +TEST_CASE(strings) +{ Vector strings; strings.append("ABC"); strings.append("DEF"); @@ -40,22 +46,23 @@ int main() ++loop_counter; } EXPECT_EQ(loop_counter, 2); - - { - Vector strings; - strings.append("abc"); - strings.append("def"); - strings.append("ghi"); - - strings.insert_before_matching("f-g", [](auto& entry) { - return "f-g" < entry; - }); - - EXPECT_EQ(strings[0], "abc"); - EXPECT_EQ(strings[1], "def"); - EXPECT_EQ(strings[2], "f-g"); - EXPECT_EQ(strings[3], "ghi"); - } - - return 0; } + +TEST_CASE(strings_insert_ordered) +{ + Vector strings; + strings.append("abc"); + strings.append("def"); + strings.append("ghi"); + + strings.insert_before_matching("f-g", [](auto& entry) { + return "f-g" < entry; + }); + + EXPECT_EQ(strings[0], "abc"); + EXPECT_EQ(strings[1], "def"); + EXPECT_EQ(strings[2], "f-g"); + EXPECT_EQ(strings[3], "ghi"); +} + +TEST_MAIN(Vector)