summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibTest/TestRunner.h
diff options
context:
space:
mode:
authorAndrew Kaster <akaster@serenityos.org>2021-06-27 12:57:35 -0600
committerAli Mohammad Pur <Ali.mpfard@gmail.com>2021-06-30 08:18:28 +0430
commit601c9e89de64534451b5be878896d1bdf84762df (patch)
treeaaf1d8350721b499f643198696cc047472271f02 /Userland/Libraries/LibTest/TestRunner.h
parent44a6715584a7bafe6b9b2601d1d44a238dce948c (diff)
downloadserenity-601c9e89de64534451b5be878896d1bdf84762df.zip
Userland+Tests: Split out generic test runner from JS TestRunner
Split out the functionality to gather multiple tests from the filesystem and run them in turn into Test::TestRunner, and leave the JavaScript specific test harness logic in Test::JS::TestRunner and friends.
Diffstat (limited to 'Userland/Libraries/LibTest/TestRunner.h')
-rw-r--r--Userland/Libraries/LibTest/TestRunner.h245
1 files changed, 245 insertions, 0 deletions
diff --git a/Userland/Libraries/LibTest/TestRunner.h b/Userland/Libraries/LibTest/TestRunner.h
new file mode 100644
index 0000000000..558de18408
--- /dev/null
+++ b/Userland/Libraries/LibTest/TestRunner.h
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org>
+ * Copyright (c) 2020-2021, Linus Groh <linusg@serenityos.org>
+ * Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
+ * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Format.h>
+#include <AK/JsonObject.h>
+#include <AK/JsonValue.h>
+#include <AK/QuickSort.h>
+#include <AK/String.h>
+#include <AK/Vector.h>
+#include <LibCore/DirIterator.h>
+#include <LibTest/Results.h>
+#include <fcntl.h>
+#include <sys/time.h>
+
+namespace Test {
+
+class TestRunner {
+public:
+ static TestRunner* the()
+ {
+ return s_the;
+ }
+
+ TestRunner(String test_root, bool print_times, bool print_progress, bool print_json)
+ : m_test_root(move(test_root))
+ , m_print_times(print_times)
+ , m_print_progress(print_progress)
+ , m_print_json(print_json)
+ {
+ VERIFY(!s_the);
+ s_the = this;
+ }
+
+ virtual ~TestRunner() { s_the = nullptr; };
+
+ virtual void run(String test_glob);
+
+ const Test::Counts& counts() const { return m_counts; }
+
+ bool is_printing_progress() const { return m_print_progress; }
+
+protected:
+ static TestRunner* s_the;
+
+ void print_test_results() const;
+ void print_test_results_as_json() const;
+
+ virtual Vector<String> get_test_paths() const = 0;
+ virtual void do_run_single_test(const String&) = 0;
+
+ String m_test_root;
+ bool m_print_times;
+ bool m_print_progress;
+ bool m_print_json;
+
+ double m_total_elapsed_time_in_ms { 0 };
+ Test::Counts m_counts;
+};
+
+inline void cleanup()
+{
+ // Clear the taskbar progress.
+ if (TestRunner::the() && TestRunner::the()->is_printing_progress())
+ warn("\033]9;-1;\033\\");
+}
+
+inline void cleanup_and_exit()
+{
+ cleanup();
+ exit(1);
+}
+
+inline double get_time_in_ms()
+{
+ struct timeval tv1;
+ auto return_code = gettimeofday(&tv1, nullptr);
+ VERIFY(return_code >= 0);
+ return static_cast<double>(tv1.tv_sec) * 1000.0 + static_cast<double>(tv1.tv_usec) / 1000.0;
+}
+
+template<typename Callback>
+inline void iterate_directory_recursively(const String& directory_path, Callback callback)
+{
+ Core::DirIterator directory_iterator(directory_path, Core::DirIterator::Flags::SkipDots);
+
+ while (directory_iterator.has_next()) {
+ auto name = directory_iterator.next_path();
+ struct stat st = {};
+ if (fstatat(directory_iterator.fd(), name.characters(), &st, AT_SYMLINK_NOFOLLOW) < 0)
+ continue;
+ bool is_directory = S_ISDIR(st.st_mode);
+ auto full_path = String::formatted("{}/{}", directory_path, name);
+ if (is_directory && name != "/Fixtures"sv) {
+ iterate_directory_recursively(full_path, callback);
+ } else if (!is_directory) {
+ callback(full_path);
+ }
+ }
+}
+
+inline void TestRunner::run(String test_glob)
+{
+ size_t progress_counter = 0;
+ auto test_paths = get_test_paths();
+ for (auto& path : test_paths) {
+ if (!path.matches(test_glob))
+ continue;
+ ++progress_counter;
+ do_run_single_test(path);
+ if (m_print_progress)
+ warn("\033]9;{};{};\033\\", progress_counter, test_paths.size());
+ }
+
+ if (m_print_progress)
+ warn("\033]9;-1;\033\\");
+
+ if (!m_print_json)
+ print_test_results();
+ else
+ print_test_results_as_json();
+}
+
+enum Modifier {
+ BG_RED,
+ BG_GREEN,
+ FG_RED,
+ FG_GREEN,
+ FG_ORANGE,
+ FG_GRAY,
+ FG_BLACK,
+ FG_BOLD,
+ ITALIC,
+ CLEAR,
+};
+
+inline void print_modifiers(Vector<Modifier> modifiers)
+{
+ for (auto& modifier : modifiers) {
+ auto code = [&] {
+ switch (modifier) {
+ case BG_RED:
+ return "\033[48;2;255;0;102m";
+ case BG_GREEN:
+ return "\033[48;2;102;255;0m";
+ case FG_RED:
+ return "\033[38;2;255;0;102m";
+ case FG_GREEN:
+ return "\033[38;2;102;255;0m";
+ case FG_ORANGE:
+ return "\033[38;2;255;102;0m";
+ case FG_GRAY:
+ return "\033[38;2;135;139;148m";
+ case FG_BLACK:
+ return "\033[30m";
+ case FG_BOLD:
+ return "\033[1m";
+ case ITALIC:
+ return "\033[3m";
+ case CLEAR:
+ return "\033[0m";
+ }
+ VERIFY_NOT_REACHED();
+ }();
+ out("{}", code);
+ }
+}
+
+inline void TestRunner::print_test_results() const
+{
+ out("\nTest Suites: ");
+ if (m_counts.suites_failed) {
+ print_modifiers({ FG_RED });
+ out("{} failed, ", m_counts.suites_failed);
+ print_modifiers({ CLEAR });
+ }
+ if (m_counts.suites_passed) {
+ print_modifiers({ FG_GREEN });
+ out("{} passed, ", m_counts.suites_passed);
+ print_modifiers({ CLEAR });
+ }
+ outln("{} total", m_counts.suites_failed + m_counts.suites_passed);
+
+ out("Tests: ");
+ if (m_counts.tests_failed) {
+ print_modifiers({ FG_RED });
+ out("{} failed, ", m_counts.tests_failed);
+ print_modifiers({ CLEAR });
+ }
+ if (m_counts.tests_skipped) {
+ print_modifiers({ FG_ORANGE });
+ out("{} skipped, ", m_counts.tests_skipped);
+ print_modifiers({ CLEAR });
+ }
+ if (m_counts.tests_passed) {
+ print_modifiers({ FG_GREEN });
+ out("{} passed, ", m_counts.tests_passed);
+ print_modifiers({ CLEAR });
+ }
+ outln("{} total", m_counts.tests_failed + m_counts.tests_skipped + m_counts.tests_passed);
+
+ outln("Files: {} total", m_counts.files_total);
+
+ out("Time: ");
+ if (m_total_elapsed_time_in_ms < 1000.0) {
+ outln("{}ms", static_cast<int>(m_total_elapsed_time_in_ms));
+ } else {
+ outln("{:>.3}s", m_total_elapsed_time_in_ms / 1000.0);
+ }
+ outln();
+}
+
+inline void TestRunner::print_test_results_as_json() const
+{
+ JsonObject suites;
+ suites.set("failed", m_counts.suites_failed);
+ suites.set("passed", m_counts.suites_passed);
+ suites.set("total", m_counts.suites_failed + m_counts.suites_passed);
+
+ JsonObject tests;
+ tests.set("failed", m_counts.tests_failed);
+ tests.set("passed", m_counts.tests_passed);
+ tests.set("skipped", m_counts.tests_skipped);
+ tests.set("total", m_counts.tests_failed + m_counts.tests_passed + m_counts.tests_skipped);
+
+ JsonObject results;
+ results.set("suites", suites);
+ results.set("tests", tests);
+
+ JsonObject root;
+ root.set("results", results);
+ root.set("files_total", m_counts.files_total);
+ root.set("duration", m_total_elapsed_time_in_ms / 1000.0);
+
+ outln("{}", root.to_string());
+}
+
+}