From 107f55581d9f6ae7a8679478b386093b4bd25f74 Mon Sep 17 00:00:00 2001 From: Ali Mohammad Pur Date: Mon, 20 Apr 2026 01:33:27 +0200 Subject: [PATCH] LibTest: Make the testrunner show a nice progress bar test-web-style The old output format is preserved under non-tty. --- Libraries/LibTest/TestRunner.h | 114 ++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/Libraries/LibTest/TestRunner.h b/Libraries/LibTest/TestRunner.h index ce0407bf70..129ef5f1ca 100644 --- a/Libraries/LibTest/TestRunner.h +++ b/Libraries/LibTest/TestRunner.h @@ -10,18 +10,40 @@ #pragma once +#include #include #include #include #include #include #include +#include #include +#include #include #include +#include + +#if !defined(AK_OS_WINDOWS) +# include +#endif + +#if defined(AK_OS_MACOS) || defined(AK_OS_BSD_GENERIC) +# include +#endif + namespace Test { +[[maybe_unused]] inline auto const& s_invocation_cwd = [] { +#if !defined(AK_OS_WINDOWS) + char buf[PATH_MAX]; + if (getcwd(buf, sizeof(buf))) + return ByteString(buf); +#endif + return ByteString {}; +}(); + class TestRunner { public: static TestRunner* the() @@ -68,6 +90,9 @@ protected: virtual void do_run_single_test(ByteString const&, size_t current_test_index, size_t num_tests) = 0; virtual Vector const* get_failed_test_names() const { return nullptr; } + void render_live_display(size_t completed, size_t total); + bool begin_live_display(); + ByteString m_test_root; bool m_print_times; bool m_print_progress; @@ -77,6 +102,9 @@ protected: double m_total_elapsed_time_in_ms { 0 }; Test::Counts m_counts; Optional> m_suites; + + LiveDisplay m_live_display; + ByteString m_current_test_label; }; inline void cleanup() @@ -92,19 +120,91 @@ inline void cleanup() exit(1); } +inline bool TestRunner::begin_live_display() +{ +#if defined(AK_OS_WINDOWS) + return false; +#else +# if defined(__GLIBC__) + char const* program = program_invocation_short_name; +# elif defined(AK_OS_MACOS) || defined(AK_OS_BSD_GENERIC) + char const* program = ::getprogname(); +# else + char const* program = nullptr; +# endif + if (!program || !*program) + program = "test-runner"; + auto const& cwd = s_invocation_cwd; + ByteString log_path = cwd.is_empty() + ? ByteString::formatted("./{}.log", program) + : ByteString::formatted("{}/{}.log", cwd, program); + + return m_live_display.begin({ .reserved_lines = 3, .log_file_path = move(log_path) }); +#endif +} + +inline void TestRunner::render_live_display(size_t completed, size_t total) +{ + m_live_display.render([&](LiveDisplay::RenderTarget& t) { + t.lines( + [&] { + if (!m_current_test_label.is_empty()) + t.label("⏺ "sv, m_current_test_label); + else + t.label("⏺ (idle)"sv, {}, { .prefix = LiveDisplay::Gray, .text = LiveDisplay::None }); + }, + [&] { + t.counter({ + { .label = "Pass"sv, .color = LiveDisplay::Green, .value = m_counts.tests_passed }, + { .label = "Fail"sv, .color = LiveDisplay::Red, .value = m_counts.tests_failed }, + { .label = "Skipped"sv, .color = LiveDisplay::Gray, .value = m_counts.tests_skipped }, + { .label = "XFail"sv, .color = LiveDisplay::Yellow, .value = m_counts.tests_expected_failed }, + }); + }, + [&] { + t.progress_bar(completed, total); + }); + }); +} + inline void TestRunner::run(ReadonlySpan test_globs) { - size_t progress_counter = 0; auto test_paths = get_test_paths(); + size_t total_tests = 0; + for (auto& path : test_paths) { + if (any_of(test_globs, [&](auto& glob) { return path.matches(glob); })) + ++total_tests; + } + + bool live_display_enabled = !m_print_json && stdout_is_tty() && begin_live_display(); + + if (live_display_enabled) + render_live_display(0, total_tests); + + size_t progress_counter = 0; for (auto& path : test_paths) { if (!any_of(test_globs, [&](auto& glob) { return path.matches(glob); })) continue; ++progress_counter; - do_run_single_test(path, progress_counter, test_paths.size()); + + if (live_display_enabled) { + auto label = LexicalPath::relative_path(path, m_test_root); + m_current_test_label = label.has_value() ? label.release_value() : path; + render_live_display(progress_counter - 1, total_tests); + } + + do_run_single_test(path, progress_counter, total_tests); + + if (live_display_enabled) + render_live_display(progress_counter, total_tests); + if (m_print_progress) - warn("\033]9;{};{};\033\\", progress_counter, test_paths.size()); + warn("\033]9;{};{};\033\\", progress_counter, total_tests); } + ByteString saved_log_path = m_live_display.log_file_path(); + m_live_display.end(); + if (m_print_progress) warn("\033]9;-1;\033\\"); @@ -112,6 +212,9 @@ inline void TestRunner::run(ReadonlySpan test_globs) print_test_results(); else print_test_results_as_json(); + + if (live_display_enabled && !saved_log_path.is_empty()) + outln("Full test output: {}", saved_log_path); } enum Modifier { @@ -129,6 +232,11 @@ enum Modifier { inline void print_modifiers(Vector modifiers) { + // Strip ANSI escapes for non-tty output. +#if !defined(AK_OS_WINDOWS) + if (!isatty(STDOUT_FILENO)) + return; +#endif for (auto& modifier : modifiers) { auto code = [&] { switch (modifier) {