Files
ladybird/Libraries/LibTest/JavaScriptTestRunnerMain.cpp
Andreas Kling 6acc1d36ae LibTest: Add verbose test file output
Add -v/--verbose support to JavaScript test runners so they print each
test file immediately before running it. This makes hard crashes and
process traps easy to map back to the file that triggered them.
2026-05-03 13:10:48 +02:00

225 lines
7.8 KiB
C++

/*
* 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>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/Environment.h>
#include <LibCore/System.h>
#include <LibFileSystem/FileSystem.h>
#include <LibJS/Bytecode/Debug.h>
#include <LibTest/JavaScriptTestRunner.h>
#include <signal.h>
#include <stdio.h>
namespace Test {
TestRunner* ::Test::TestRunner::s_the = nullptr;
namespace JS {
GC_DEFINE_ALLOCATOR(TestRunnerGlobalObject);
RefPtr<::JS::VM> g_vm;
bool g_collect_on_every_allocation = false;
ByteString g_currently_running_test;
HashMap<Utf16String, FunctionWithLength> s_exposed_global_functions;
Function<void()> g_main_hook;
HashMap<bool*, Tuple<ByteString, ByteString, char>> g_extra_args;
IntermediateRunFileResult (*g_run_file)(ByteString const&, JS::Realm&, JS::ExecutionContext&) = nullptr;
ByteString g_test_root;
int g_test_argc;
char** g_test_argv;
} // namespace JS
} // namespace Test
using namespace Test::JS;
static StringView g_program_name { "test-js"sv };
static bool set_abort_action(void (*function)(int))
{
#if defined(AK_OS_WINDOWS)
auto rc = signal(SIGABRT, function);
if (rc == SIG_ERR) {
perror("sigaction");
return false;
}
return true;
#else
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_flags = 0;
act.sa_handler = function;
int rc = sigaction(SIGABRT, &act, nullptr);
if (rc < 0) {
perror("sigaction");
return false;
}
return true;
#endif
}
static void handle_sigabrt(int)
{
dbgln("{}: SIGABRT received, cleaning up.", g_program_name);
Test::cleanup();
if (!set_abort_action(SIG_DFL))
exit(1);
abort();
}
int main(int argc, char** argv)
{
Vector<StringView> arguments;
arguments.ensure_capacity(argc);
for (auto i = 0; i < argc; ++i)
arguments.append({ argv[i], strlen(argv[i]) });
g_test_argc = argc;
g_test_argv = argv;
auto program_name = LexicalPath::basename(argv[0]);
g_program_name = program_name;
if (!set_abort_action(handle_sigabrt))
return 1;
#ifdef SIGINFO
signal(SIGINFO, [](int) {
static char buffer[4096];
auto& counts = ::Test::TestRunner::the()->counts();
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());
write(STDOUT_FILENO, buffer, len);
});
#endif
bool print_times = false;
bool print_progress = false;
bool print_json = false;
bool per_file = false;
bool print_each_test = false;
StringView specified_test_root;
ByteString common_path;
Vector<ByteString> test_globs;
Core::ArgsParser args_parser;
args_parser.add_option(print_times, "Show duration of each test", "show-time", 't');
args_parser.add_option(Core::ArgsParser::Option {
.argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
.help_string = "Show progress with OSC 9 (true, false)",
.long_name = "show-progress",
.short_name = 'p',
.accept_value = [&](StringView str) {
if ("true"sv == str)
print_progress = true;
else if ("false"sv == str)
print_progress = false;
else
return false;
return true;
},
});
args_parser.add_option(print_json, "Show results as JSON", "json", 'j');
args_parser.add_option(per_file, "Show detailed per-file results as JSON (implies -j)", "per-file");
args_parser.add_option(print_each_test, "Print each test file before running it", "verbose", 'v');
args_parser.add_option(g_collect_on_every_allocation, "Collect garbage after every allocation", "collect-often", 'g');
args_parser.add_option(JS::Bytecode::g_dump_bytecode, "Dump the bytecode", "dump-bytecode", 'd');
args_parser.add_option(test_globs, "Only run tests matching the given glob", "filter", 'f', "glob");
for (auto& entry : g_extra_args)
args_parser.add_option(*entry.key, entry.value.get<0>().characters(), entry.value.get<1>().characters(), entry.value.get<2>());
args_parser.add_positional_argument(specified_test_root, "Tests root directory", "path", Core::ArgsParser::Required::No);
args_parser.add_positional_argument(common_path, "Path to tests-common.js", "common-path", Core::ArgsParser::Required::No);
args_parser.parse(arguments);
if (per_file)
print_json = true;
for (auto& glob : test_globs)
glob = ByteString::formatted("*{}*", glob);
if (test_globs.is_empty())
test_globs.append("*"sv);
if (Core::Environment::has("DISABLE_DBG_OUTPUT"sv)) {
AK::set_debug_enabled(false);
}
ByteString test_root;
if (!specified_test_root.is_empty()) {
test_root = ByteString { specified_test_root };
} else {
auto ladybird_source_dir = Core::Environment::get("LADYBIRD_SOURCE_DIR"sv);
if (!ladybird_source_dir.has_value()) {
warnln("No test root given, {} requires the LADYBIRD_SOURCE_DIR environment variable to be set", g_program_name);
return 1;
}
test_root = LexicalPath::join(*ladybird_source_dir, g_test_root_fragment).string();
common_path = LexicalPath::join(*ladybird_source_dir, "Tests"sv, "LibJS"sv, "Runtime"sv, "test-common.js"sv).string();
}
if (!FileSystem::is_directory(test_root)) {
warnln("Test root is not a directory: {}", test_root);
return 1;
}
if (common_path.is_empty()) {
auto ladybird_source_dir = Core::Environment::get("LADYBIRD_SOURCE_DIR"sv);
if (!ladybird_source_dir.has_value()) {
warnln("No test root given, {} requires the LADYBIRD_SOURCE_DIR environment variable to be set", g_program_name);
return 1;
}
common_path = LexicalPath::join(*ladybird_source_dir, "Tests"sv, "LibJS"sv, "Runtime"sv, "test-common.js"sv).string();
}
auto test_root_or_error = FileSystem::real_path(test_root);
if (test_root_or_error.is_error()) {
warnln("Failed to resolve test root: {}", test_root_or_error.error());
return 1;
}
test_root = test_root_or_error.release_value();
auto common_path_or_error = FileSystem::real_path(common_path);
if (common_path_or_error.is_error()) {
warnln("Failed to resolve common path: {}", common_path_or_error.error());
return 1;
}
common_path = common_path_or_error.release_value();
if (auto err = Core::System::chdir(test_root); err.is_error()) {
warnln("chdir failed: {}", err.error());
return 1;
}
if (g_main_hook)
g_main_hook();
if (!g_vm) {
g_vm = JS::VM::create();
g_vm->set_dynamic_imports_allowed(true);
// Configure the test VM to support additional import attributes
// This allows tests to use import attributes beyond just "type"
Test::JS::g_vm->host_get_supported_import_attributes = []() -> Vector<Utf16String> {
return {
"type"_utf16,
"key"_utf16, // Used in modules/import-with-attributes.mjs test
"key1"_utf16, // Used in modules/basic-modules.js
"key2"_utf16, // Used in modules/import-with-attributes.mjs test
"default"_utf16, // Used in modules/import-with-attributes.mjs test
};
};
}
Test::JS::TestRunner test_runner(test_root, common_path, print_times, print_progress, print_json, per_file, print_each_test);
test_runner.run(test_globs);
g_vm = nullptr;
return test_runner.counts().tests_failed > 0 ? 1 : 0;
}