Serenity Operating System
at master 413 lines 16 kB view raw
1/* 2 * Copyright (c) 2021, Andrew Kaster <akaster@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/LexicalPath.h> 8#include <LibCore/ArgsParser.h> 9#include <LibCore/ConfigFile.h> 10#include <LibCore/DeprecatedFile.h> 11#include <LibCore/System.h> 12#include <LibCoredump/Backtrace.h> 13#include <LibMain/Main.h> 14#include <LibRegex/Regex.h> 15#include <LibTest/TestRunner.h> 16#include <signal.h> 17#include <spawn.h> 18#include <stdlib.h> 19#include <sys/wait.h> 20#include <unistd.h> 21 22namespace Test { 23TestRunner* TestRunner::s_the = nullptr; 24} 25using Test::get_time_in_ms; 26using Test::print_modifiers; 27 28struct FileResult { 29 LexicalPath file_path; 30 double time_taken { 0 }; 31 Test::Result result { Test::Result::Pass }; 32 int stdout_err_fd { -1 }; 33 pid_t child_pid { 0 }; 34}; 35 36DeprecatedString g_currently_running_test; 37 38class TestRunner : public ::Test::TestRunner { 39public: 40 TestRunner(DeprecatedString test_root, Regex<PosixExtended> exclude_regex, NonnullRefPtr<Core::ConfigFile> config, Regex<PosixExtended> skip_regex, bool run_skipped_tests, bool print_progress, bool print_json, bool print_all_output, bool print_times = true) 41 : ::Test::TestRunner(move(test_root), print_times, print_progress, print_json) 42 , m_exclude_regex(move(exclude_regex)) 43 , m_config(move(config)) 44 , m_skip_regex(move(skip_regex)) 45 , m_run_skipped_tests(run_skipped_tests) 46 , m_print_all_output(print_all_output) 47 { 48 if (!run_skipped_tests) { 49 m_skip_directories = m_config->read_entry("Global", "SkipDirectories", "").split(' '); 50 m_skip_files = m_config->read_entry("Global", "SkipTests", "").split(' '); 51 } 52 } 53 54 virtual ~TestRunner() = default; 55 56protected: 57 virtual void do_run_single_test(DeprecatedString const& test_path, size_t current_text_index, size_t num_tests) override; 58 virtual Vector<DeprecatedString> get_test_paths() const override; 59 virtual Vector<DeprecatedString> const* get_failed_test_names() const override { return &m_failed_test_names; } 60 61 virtual FileResult run_test_file(DeprecatedString const& test_path); 62 63 bool should_skip_test(LexicalPath const& test_path); 64 65 Regex<PosixExtended> m_exclude_regex; 66 NonnullRefPtr<Core::ConfigFile> m_config; 67 Vector<DeprecatedString> m_skip_directories; 68 Vector<DeprecatedString> m_skip_files; 69 Vector<DeprecatedString> m_failed_test_names; 70 Regex<PosixExtended> m_skip_regex; 71 bool m_run_skipped_tests { false }; 72 bool m_print_all_output { false }; 73}; 74 75Vector<DeprecatedString> TestRunner::get_test_paths() const 76{ 77 Vector<DeprecatedString> paths; 78 Test::iterate_directory_recursively(m_test_root, [&](DeprecatedString const& file_path) { 79 if (access(file_path.characters(), R_OK | X_OK) != 0) 80 return; 81 auto result = m_exclude_regex.match(file_path, PosixFlags::Global); 82 if (!result.success) // must NOT match the regex to be a valid test file 83 paths.append(file_path); 84 }); 85 quick_sort(paths); 86 return paths; 87} 88 89bool TestRunner::should_skip_test(LexicalPath const& test_path) 90{ 91 if (m_run_skipped_tests) 92 return false; 93 94 for (DeprecatedString const& dir : m_skip_directories) { 95 if (test_path.dirname().contains(dir)) 96 return true; 97 } 98 for (DeprecatedString const& file : m_skip_files) { 99 if (test_path.basename().contains(file)) 100 return true; 101 } 102 auto result = m_skip_regex.match(test_path.basename(), PosixFlags::Global); 103 if (result.success) 104 return true; 105 106 return false; 107} 108 109void TestRunner::do_run_single_test(DeprecatedString const& test_path, size_t current_test_index, size_t num_tests) 110{ 111 g_currently_running_test = test_path; 112 auto test_relative_path = LexicalPath::relative_path(test_path, m_test_root); 113 outln(" START {} ({}/{})", test_relative_path, current_test_index, num_tests); 114 fflush(stdout); // we really want to see the start text in case the test hangs 115 auto test_result = run_test_file(test_path); 116 117 switch (test_result.result) { 118 case Test::Result::Pass: 119 ++m_counts.tests_passed; 120 break; 121 case Test::Result::Skip: 122 ++m_counts.tests_skipped; 123 break; 124 case Test::Result::Fail: 125 ++m_counts.tests_failed; 126 break; 127 case Test::Result::Crashed: 128 ++m_counts.tests_failed; // FIXME: tests_crashed 129 break; 130 } 131 if (test_result.result != Test::Result::Skip) 132 ++m_counts.files_total; 133 134 m_total_elapsed_time_in_ms += test_result.time_taken; 135 136 bool crashed_or_failed = test_result.result == Test::Result::Fail || test_result.result == Test::Result::Crashed; 137 bool print_stdout_stderr = crashed_or_failed || m_print_all_output; 138 if (crashed_or_failed) { 139 m_failed_test_names.append(test_path); 140 print_modifiers({ Test::BG_RED, Test::FG_BLACK, Test::FG_BOLD }); 141 out("{}", test_result.result == Test::Result::Fail ? " FAIL " : "CRASHED"); 142 print_modifiers({ Test::CLEAR }); 143 if (test_result.result == Test::Result::Crashed) { 144 auto pid_search_string = DeprecatedString::formatted("_{}_", test_result.child_pid); 145 Core::DirIterator iterator("/tmp/coredump"sv); 146 if (!iterator.has_error()) { 147 while (iterator.has_next()) { 148 auto path = iterator.next_full_path(); 149 if (!path.contains(pid_search_string)) 150 continue; 151 152 auto reader = Coredump::Reader::create(path); 153 if (!reader) 154 break; 155 156 dbgln("Last crash backtrace for {} (was pid {}):", test_path, test_result.child_pid); 157 reader->for_each_thread_info([&](auto thread_info) { 158 Coredump::Backtrace thread_backtrace(*reader, thread_info); 159 auto tid = thread_info.tid; // Note: Yoinking this out of the struct because we can't pass a reference to it (as it's a misaligned field in a packed struct) 160 dbgln("Thread {}", tid); 161 for (auto const& entry : thread_backtrace.entries()) 162 dbgln("- {}", entry.to_deprecated_string(true)); 163 return IterationDecision::Continue; 164 }); 165 break; 166 } 167 } 168 } 169 } else { 170 print_modifiers({ Test::BG_GREEN, Test::FG_BLACK, Test::FG_BOLD }); 171 out(" PASS "); 172 print_modifiers({ Test::CLEAR }); 173 } 174 175 out(" {}", test_relative_path); 176 177 print_modifiers({ Test::CLEAR, Test::ITALIC, Test::FG_GRAY }); 178 if (test_result.time_taken < 1000) { 179 outln(" ({}ms)", static_cast<int>(test_result.time_taken)); 180 } else { 181 outln(" ({:3}s)", test_result.time_taken / 1000.0); 182 } 183 print_modifiers({ Test::CLEAR }); 184 185 if (test_result.result != Test::Result::Pass) { 186 print_modifiers({ Test::FG_GRAY, Test::FG_BOLD }); 187 out(" Test: "); 188 if (crashed_or_failed) { 189 print_modifiers({ Test::CLEAR, Test::FG_RED }); 190 outln("{} ({})", test_result.file_path.basename(), test_result.result == Test::Result::Fail ? "failed" : "crashed"); 191 } else { 192 print_modifiers({ Test::CLEAR, Test::FG_ORANGE }); 193 outln("{} (skipped)", test_result.file_path.basename()); 194 } 195 print_modifiers({ Test::CLEAR }); 196 } 197 198 // Make sure our clear modifiers goes through before we dump file output via write(2) 199 fflush(stdout); 200 201 if (print_stdout_stderr && test_result.stdout_err_fd > 0) { 202 int ret = lseek(test_result.stdout_err_fd, 0, SEEK_SET); 203 VERIFY(ret == 0); 204 for (;;) { 205 char buf[32768]; 206 ssize_t nread = read(test_result.stdout_err_fd, buf, sizeof(buf)); 207 if (nread == 0) 208 break; 209 if (nread < 0) { 210 perror("read"); 211 break; 212 } 213 size_t already_written = 0; 214 while (already_written < (size_t)nread) { 215 ssize_t nwritten = write(STDOUT_FILENO, buf + already_written, nread - already_written); 216 if (nwritten < 0) { 217 perror("write"); 218 break; 219 } 220 already_written += nwritten; 221 } 222 } 223 } 224 225 close(test_result.stdout_err_fd); 226} 227 228FileResult TestRunner::run_test_file(DeprecatedString const& test_path) 229{ 230 double start_time = get_time_in_ms(); 231 232 auto path_for_test = LexicalPath(test_path); 233 if (should_skip_test(path_for_test)) { 234 return FileResult { move(path_for_test), 0.0, Test::Result::Skip, -1 }; 235 } 236 237 // FIXME: actual error handling, mark test as :yaksplode: if any are bad instead of VERIFY 238 posix_spawn_file_actions_t file_actions; 239 posix_spawn_file_actions_init(&file_actions); 240 char child_out_err_path[] = "/tmp/run-tests.XXXXXX"; 241 int child_out_err_file = mkstemp(child_out_err_path); 242 VERIFY(child_out_err_file >= 0); 243 244 DeprecatedString dirname = path_for_test.dirname(); 245 DeprecatedString basename = path_for_test.basename(); 246 247 (void)posix_spawn_file_actions_adddup2(&file_actions, child_out_err_file, STDOUT_FILENO); 248 (void)posix_spawn_file_actions_adddup2(&file_actions, child_out_err_file, STDERR_FILENO); 249 (void)posix_spawn_file_actions_addchdir(&file_actions, dirname.characters()); 250 251 Vector<char const*, 4> argv; 252 argv.append(basename.characters()); 253 auto extra_args = m_config->read_entry(path_for_test.basename(), "Arguments", "").split(' '); 254 for (auto& arg : extra_args) 255 argv.append(arg.characters()); 256 argv.append(nullptr); 257 258 pid_t child_pid = -1; 259 // FIXME: Do we really want to copy test runner's entire env? 260 int ret = posix_spawn(&child_pid, test_path.characters(), &file_actions, nullptr, const_cast<char* const*>(argv.data()), environ); 261 VERIFY(ret == 0); 262 VERIFY(child_pid > 0); 263 264 int wstatus; 265 266 Test::Result test_result = Test::Result::Fail; 267 for (size_t num_waits = 0; num_waits < 2; ++num_waits) { 268 ret = waitpid(child_pid, &wstatus, 0); // intentionally not setting WCONTINUED 269 if (ret != child_pid) 270 break; // we'll end up with a failure 271 272 if (WIFEXITED(wstatus)) { 273 if (wstatus == 0) { 274 test_result = Test::Result::Pass; 275 } 276 break; 277 } else if (WIFSIGNALED(wstatus)) { 278 test_result = Test::Result::Crashed; 279 break; 280 } else if (WIFSTOPPED(wstatus)) { 281 outln("{} was stopped unexpectedly, sending SIGCONT", test_path); 282 kill(child_pid, SIGCONT); 283 } 284 } 285 286 // Remove the child's stdout from /tmp. This does cause the temp file to be observable 287 // while the test is executing, but if it hangs that might even be a bonus :) 288 ret = unlink(child_out_err_path); 289 VERIFY(ret == 0); 290 291 return FileResult { move(path_for_test), get_time_in_ms() - start_time, test_result, child_out_err_file, child_pid }; 292} 293 294ErrorOr<int> serenity_main(Main::Arguments arguments) 295{ 296 297 auto program_name = LexicalPath::basename(arguments.strings[0]); 298 299#ifdef SIGINFO 300 TRY(Core::System::signal(SIGINFO, [](int) { 301 static char buffer[4096]; 302 auto& counts = ::Test::TestRunner::the()->counts(); 303 int len = snprintf(buffer, sizeof(buffer), "Pass: %d, Fail: %d, Skip: %d\nCurrent test: %s\n", counts.tests_passed, counts.tests_failed, counts.tests_skipped, g_currently_running_test.characters()); 304 write(STDOUT_FILENO, buffer, len); 305 })); 306#endif 307 308 bool print_progress = 309#ifdef AK_OS_SERENITY 310 true; // Use OSC 9 to print progress 311#else 312 false; 313#endif 314 bool print_json = false; 315 bool print_all_output = false; 316 bool run_benchmarks = false; 317 bool run_skipped_tests = false; 318 StringView specified_test_root; 319 DeprecatedString test_glob; 320 DeprecatedString exclude_pattern; 321 DeprecatedString config_file; 322 323 Core::ArgsParser args_parser; 324 args_parser.add_option(Core::ArgsParser::Option { 325 .argument_mode = Core::ArgsParser::OptionArgumentMode::Required, 326 .help_string = "Show progress with OSC 9 (true, false)", 327 .long_name = "show-progress", 328 .short_name = 'p', 329 .accept_value = [&](StringView str) { 330 if ("true"sv == str) 331 print_progress = true; 332 else if ("false"sv == str) 333 print_progress = false; 334 else 335 return false; 336 return true; 337 }, 338 }); 339 args_parser.add_option(print_json, "Show results as JSON", "json", 'j'); 340 args_parser.add_option(print_all_output, "Show all test output", "verbose", 'v'); 341 args_parser.add_option(run_benchmarks, "Run benchmarks as well", "benchmarks", 'b'); 342 args_parser.add_option(run_skipped_tests, "Run all matching tests, even those marked as 'skip'", "all", 'a'); 343 args_parser.add_option(test_glob, "Only run tests matching the given glob", "filter", 'f', "glob"); 344 args_parser.add_option(exclude_pattern, "Regular expression to use to exclude paths from being considered tests", "exclude-pattern", 'e', "pattern"); 345 args_parser.add_option(config_file, "Configuration file to use", "config-file", 'c', "filename"); 346 args_parser.add_positional_argument(specified_test_root, "Tests root directory", "path", Core::ArgsParser::Required::No); 347 args_parser.parse(arguments); 348 349 test_glob = DeprecatedString::formatted("*{}*", test_glob); 350 351 if (getenv("DISABLE_DBG_OUTPUT")) { 352 AK::set_debug_enabled(false); 353 } 354 355 // Make UBSAN deadly for all tests we run by default. 356 TRY(Core::System::setenv("UBSAN_OPTIONS"sv, "halt_on_error=1"sv, true)); 357 358 if (!run_benchmarks) 359 TRY(Core::System::setenv("TESTS_ONLY"sv, "1"sv, true)); 360 361 DeprecatedString test_root; 362 363 if (!specified_test_root.is_empty()) { 364 test_root = DeprecatedString { specified_test_root }; 365 } else { 366 test_root = "/usr/Tests"; 367 } 368 if (!Core::DeprecatedFile::is_directory(test_root)) { 369 warnln("Test root is not a directory: {}", test_root); 370 return 1; 371 } 372 373 test_root = Core::DeprecatedFile::real_path_for(test_root); 374 375 auto void_or_error = Core::System::chdir(test_root); 376 if (void_or_error.is_error()) { 377 warnln("chdir failed: {}", void_or_error.error()); 378 return void_or_error.release_error(); 379 } 380 381 auto config_or_error = config_file.is_empty() ? Core::ConfigFile::open_for_app("Tests") : Core::ConfigFile::open(config_file); 382 if (config_or_error.is_error()) { 383 warnln("Failed to open configuration file ({}): {}", config_file.is_empty() ? "User config for Tests" : config_file.characters(), config_or_error.error()); 384 return config_or_error.release_error(); 385 } 386 auto config = config_or_error.release_value(); 387 388 if (config->num_groups() == 0) 389 warnln("Empty configuration file ({}) loaded!", config_file.is_empty() ? "User config for Tests" : config_file.characters()); 390 391 if (exclude_pattern.is_empty()) 392 exclude_pattern = config->read_entry("Global", "NotTestsPattern", "$^"); // default is match nothing (aka match end then beginning) 393 394 Regex<PosixExtended> exclude_regex(exclude_pattern, {}); 395 if (exclude_regex.parser_result.error != regex::Error::NoError) { 396 warnln("Exclude pattern \"{}\" is invalid", exclude_pattern); 397 return 1; 398 } 399 400 // we need to preconfigure this, because we can't autoinitialize Regex types 401 // in the Testrunner 402 auto skip_regex_pattern = config->read_entry("Global", "SkipRegex", "$^"); 403 Regex<PosixExtended> skip_regex { skip_regex_pattern, {} }; 404 if (skip_regex.parser_result.error != regex::Error::NoError) { 405 warnln("SkipRegex pattern \"{}\" is invalid", skip_regex_pattern); 406 return 1; 407 } 408 409 TestRunner test_runner(test_root, move(exclude_regex), move(config), move(skip_regex), run_skipped_tests, print_progress, print_json, print_all_output); 410 test_runner.run(test_glob); 411 412 return test_runner.counts().tests_failed; 413}