Keep a small harness-status.txt file in the test-web results directory and refresh it whenever tests start, finish, or the run stops early. This leaves the last known active views, counts, and remaining test count behind even if the harness does not reach its normal result generation path. Also report helper-process log write failures instead of treating them as "no logs". That makes artifact gaps visible instead of silently losing the only clue from a helper process failure.
342 lines
13 KiB
C++
342 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2026, The Ladybird Developers
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "TestRunCapture.h"
|
|
#include "Application.h"
|
|
#include "TestWeb.h"
|
|
#include "TestWebView.h"
|
|
|
|
#include <AK/LexicalPath.h>
|
|
#include <LibCore/System.h>
|
|
|
|
#if !defined(AK_OS_WINDOWS)
|
|
# include <sys/wait.h>
|
|
#endif
|
|
|
|
namespace TestWeb {
|
|
|
|
static ByteString format_elapsed_time(UnixDateTime run_start_time);
|
|
static ByteString format_exit_status(Optional<int> exit_status);
|
|
static void setup_capture_notifier(RefPtr<Core::Notifier>& notifier, int fd, bool drain_available, Function<void(StringView)> on_output);
|
|
static bool drain_capture_output(int fd, bool drain_available, Function<void(StringView)> const& on_output);
|
|
|
|
TestRunCapture::TestRunCapture()
|
|
: m_run_started_at(UnixDateTime::now())
|
|
{
|
|
ByteString helper_logs_path = LexicalPath::join(Application::the().results_directory, "helper-process-logs.html"sv).string();
|
|
m_helper_output = CaptureFile { move(helper_logs_path) };
|
|
|
|
auto& process_manager = WebView::Application::process_manager();
|
|
process_manager.on_process_added = [this](WebView::Process& process) {
|
|
setup_output_capture_for_helper_process(process);
|
|
};
|
|
m_previous_on_process_exited = move(process_manager.on_process_exited);
|
|
process_manager.on_process_exited = [this](WebView::Process&& process, Optional<int> exit_status) {
|
|
consume_helper_capture(process.pid());
|
|
log_helper_message(
|
|
{ process.type(), process.pid() },
|
|
m_stderr_capture.original_fd.value_or(STDERR_FILENO),
|
|
ByteString::formatted("test-web: process exited: {}\n", format_exit_status(exit_status)));
|
|
m_previous_on_process_exited(move(process), exit_status);
|
|
};
|
|
|
|
process_manager.for_each_process([this](WebView::Process& process) {
|
|
setup_output_capture_for_helper_process(process);
|
|
});
|
|
|
|
#ifndef AK_OS_WINDOWS
|
|
auto dup_stderr_fd = MUST(Core::System::dup(STDERR_FILENO));
|
|
auto pipe_fds = MUST(Core::System::pipe2(O_CLOEXEC));
|
|
auto read_fd = pipe_fds[0];
|
|
auto write_fd = pipe_fds[1];
|
|
auto current_flags = Core::System::fcntl(read_fd, F_GETFL);
|
|
if (!current_flags.is_error())
|
|
(void)Core::System::fcntl(read_fd, F_SETFL, current_flags.value() | O_NONBLOCK);
|
|
|
|
(void)fflush(stderr);
|
|
if (::dup2(write_fd, STDERR_FILENO) < 0) {
|
|
MUST(Core::System::close(read_fd));
|
|
MUST(Core::System::close(write_fd));
|
|
MUST(Core::System::close(dup_stderr_fd));
|
|
return;
|
|
}
|
|
MUST(Core::System::close(write_fd));
|
|
|
|
m_stderr_capture.original_fd = dup_stderr_fd;
|
|
m_stderr_capture.reader = MUST(Core::File::adopt_fd(read_fd, Core::File::OpenMode::Read));
|
|
int browser_pid = Core::System::getpid();
|
|
setup_capture_notifier(m_stderr_capture.notifier, m_stderr_capture.reader->fd(), true, [this, browser_pid](StringView message) {
|
|
log_helper_message({ WebView::ProcessType::Browser, browser_pid }, m_stderr_capture.original_fd.value_or(STDERR_FILENO), message);
|
|
});
|
|
#endif
|
|
}
|
|
|
|
TestRunCapture::~TestRunCapture()
|
|
{
|
|
restore_stderr();
|
|
WebView::Application::process_manager().on_process_added = {};
|
|
WebView::Application::process_manager().on_process_exited = move(m_previous_on_process_exited);
|
|
}
|
|
|
|
TestRunCapture::ViewOutputCapture* TestRunCapture::output_capture_for_view(TestWebView const& view)
|
|
{
|
|
auto capture = m_test_output_captures.get(&view);
|
|
if (!capture.has_value())
|
|
return nullptr;
|
|
return capture.value();
|
|
}
|
|
|
|
void TestRunCapture::log_helper_message(HelperOutputSource source, int tee_fd, StringView message)
|
|
{
|
|
if (Application::the().verbosity >= Application::VERBOSITY_LEVEL_LOG_TEST_OUTPUT)
|
|
(void)Core::System::write(tee_fd, message.bytes());
|
|
|
|
bool const should_append_header = !m_last_helper_source.has_value()
|
|
|| m_last_helper_source->type != source.type
|
|
|| m_last_helper_source->pid != source.pid;
|
|
|
|
if (should_append_header) {
|
|
if (m_last_helper_source.has_value())
|
|
m_helper_output.write("\n"sv);
|
|
|
|
auto elapsed_time = format_elapsed_time(m_run_started_at);
|
|
ByteString header = ByteString::formatted("===== {} ({}) at {} =====\n", WebView::process_name_from_type(source.type), source.pid, elapsed_time);
|
|
m_helper_output.write(header);
|
|
m_last_helper_source = source;
|
|
}
|
|
|
|
m_helper_output.write(message);
|
|
}
|
|
|
|
static ByteString format_exit_status(Optional<int> exit_status)
|
|
{
|
|
if (!exit_status.has_value())
|
|
return "unknown status"sv;
|
|
|
|
#if defined(AK_OS_WINDOWS)
|
|
return ByteString::formatted("status {}", *exit_status);
|
|
#else
|
|
if (WIFEXITED(*exit_status))
|
|
return ByteString::formatted("status {}", WEXITSTATUS(*exit_status));
|
|
if (WIFSIGNALED(*exit_status))
|
|
return ByteString::formatted("signal {}", WTERMSIG(*exit_status));
|
|
if (WIFSTOPPED(*exit_status))
|
|
return ByteString::formatted("stopped by signal {}", WSTOPSIG(*exit_status));
|
|
return ByteString::formatted("raw wait status {}", *exit_status);
|
|
#endif
|
|
}
|
|
|
|
void TestRunCapture::setup_output_capture_for_view(TestWebView& view, ViewOutputCapture& view_capture)
|
|
{
|
|
auto process = Application::the().find_process(view.web_content_pid());
|
|
if (!process.has_value())
|
|
return;
|
|
|
|
auto& output_capture = process->output_capture();
|
|
if (!output_capture.stdout_file && !output_capture.stderr_file)
|
|
return;
|
|
|
|
if (output_capture.stdout_file) {
|
|
setup_capture_notifier(view_capture.stdout_notifier, output_capture.stdout_file->fd(), false, [&view_capture](StringView message) {
|
|
if (Application::the().verbosity >= Application::VERBOSITY_LEVEL_LOG_TEST_OUTPUT)
|
|
(void)Core::System::write(STDOUT_FILENO, message.bytes());
|
|
view_capture.output.write(message);
|
|
});
|
|
}
|
|
|
|
if (output_capture.stderr_file) {
|
|
setup_capture_notifier(view_capture.stderr_notifier, output_capture.stderr_file->fd(), false, [this, &view_capture](StringView message) {
|
|
if (Application::the().verbosity >= Application::VERBOSITY_LEVEL_LOG_TEST_OUTPUT)
|
|
(void)Core::System::write(m_stderr_capture.original_fd.value_or(STDERR_FILENO), message.bytes());
|
|
view_capture.output.write(message);
|
|
});
|
|
}
|
|
}
|
|
|
|
void TestRunCapture::begin_test_output_capture(TestWebView& view, Test const& test)
|
|
{
|
|
destroy_view_capture_of(view);
|
|
|
|
auto view_capture = make<ViewOutputCapture>();
|
|
ByteString output_path = ByteString::formatted("{}.logs.html", LexicalPath::join(Application::the().results_directory, test.safe_relative_path).string());
|
|
view_capture->output = CaptureFile { move(output_path) };
|
|
|
|
setup_output_capture_for_view(view, *view_capture);
|
|
m_test_output_captures.set(&view, move(view_capture));
|
|
}
|
|
|
|
void TestRunCapture::rebind_test_output_capture(TestWebView& view)
|
|
{
|
|
auto* capture = output_capture_for_view(view);
|
|
if (!capture)
|
|
return;
|
|
|
|
setup_output_capture_for_view(view, *capture);
|
|
}
|
|
|
|
void TestRunCapture::write_test_output(TestWebView const& view)
|
|
{
|
|
auto* capture = output_capture_for_view(view);
|
|
if (!capture)
|
|
return;
|
|
|
|
(void)capture->output.transfer_to_output_file();
|
|
destroy_view_capture_of(view);
|
|
}
|
|
|
|
bool TestRunCapture::write_helper_process_output()
|
|
{
|
|
restore_stderr();
|
|
|
|
auto result = m_helper_output.transfer_to_output_file();
|
|
if (result.is_error()) {
|
|
warnln("Failed to write helper process logs: {}", result.error());
|
|
return false;
|
|
}
|
|
return result.value();
|
|
}
|
|
|
|
void TestRunCapture::restore_stderr()
|
|
{
|
|
if (!m_stderr_capture.original_fd.has_value() || !m_stderr_capture.reader)
|
|
return;
|
|
|
|
(void)fflush(stderr);
|
|
int browser_pid = Core::System::getpid();
|
|
(void)drain_capture_output(m_stderr_capture.reader->fd(), true, [this, browser_pid](StringView message) {
|
|
log_helper_message({ WebView::ProcessType::Browser, browser_pid }, m_stderr_capture.original_fd.value_or(STDERR_FILENO), message);
|
|
});
|
|
auto original_stderr_fd = m_stderr_capture.original_fd.release_value();
|
|
|
|
#ifndef AK_OS_WINDOWS
|
|
(void)::dup2(original_stderr_fd, STDERR_FILENO);
|
|
#endif
|
|
MUST(Core::System::close(original_stderr_fd));
|
|
|
|
if (m_stderr_capture.notifier)
|
|
m_stderr_capture.notifier->close();
|
|
m_stderr_capture.notifier = nullptr;
|
|
m_stderr_capture.reader = nullptr;
|
|
}
|
|
|
|
void TestRunCapture::setup_output_capture_for_helper_process(WebView::Process& process)
|
|
{
|
|
if (process.type() == WebView::ProcessType::Browser || process.type() == WebView::ProcessType::WebContent)
|
|
return;
|
|
|
|
auto const pid = process.pid();
|
|
if (m_helper_output_captures.contains(pid))
|
|
return;
|
|
|
|
auto& output_capture = process.output_capture();
|
|
if (!output_capture.stdout_file && !output_capture.stderr_file)
|
|
return;
|
|
|
|
auto helper_capture = make<HelperOutputCapture>();
|
|
helper_capture->type = process.type();
|
|
helper_capture->pid = pid;
|
|
auto* helper_capture_ptr = helper_capture.ptr();
|
|
|
|
if (output_capture.stdout_file) {
|
|
helper_capture->stdout_reader = move(output_capture.stdout_file);
|
|
setup_capture_notifier(helper_capture->stdout_notifier, helper_capture->stdout_reader->fd(), false, [this, capture = helper_capture_ptr](StringView message) {
|
|
log_helper_message({ capture->type, capture->pid }, STDOUT_FILENO, message);
|
|
});
|
|
}
|
|
if (output_capture.stderr_file) {
|
|
helper_capture->stderr_reader = move(output_capture.stderr_file);
|
|
setup_capture_notifier(helper_capture->stderr_notifier, helper_capture->stderr_reader->fd(), false, [this, capture = helper_capture_ptr](StringView message) {
|
|
log_helper_message(
|
|
{ capture->type, capture->pid },
|
|
m_stderr_capture.original_fd.value_or(STDERR_FILENO),
|
|
message);
|
|
});
|
|
}
|
|
m_helper_output_captures.set(pid, move(helper_capture));
|
|
}
|
|
|
|
static ByteString format_elapsed_time(UnixDateTime run_start_time)
|
|
{
|
|
auto elapsed_milliseconds = AK::max((UnixDateTime::now() - run_start_time).to_truncated_milliseconds(), 0);
|
|
i64 total_seconds = elapsed_milliseconds / 1000;
|
|
i64 centiseconds = (elapsed_milliseconds / 10) % 100;
|
|
i64 minutes = total_seconds / 60;
|
|
i64 seconds = total_seconds % 60;
|
|
if (minutes > 0)
|
|
return ByteString::formatted("{}:{:02}.{:02}s", minutes, seconds, centiseconds);
|
|
return ByteString::formatted("{}.{:02}s", seconds, centiseconds);
|
|
}
|
|
|
|
static void setup_capture_notifier(RefPtr<Core::Notifier>& notifier, int fd, bool drain_available, Function<void(StringView)> on_output)
|
|
{
|
|
notifier = Core::Notifier::construct(fd, Core::Notifier::Type::Read);
|
|
auto* notifier_slot = ¬ifier;
|
|
notifier->on_activation = [fd, drain_available, on_output = move(on_output), notifier_slot]() mutable {
|
|
bool const reached_eof = drain_capture_output(fd, drain_available, on_output);
|
|
if (reached_eof && *notifier_slot)
|
|
(*notifier_slot)->set_enabled(false);
|
|
};
|
|
}
|
|
|
|
static bool drain_capture_output(int fd, bool drain_available, Function<void(StringView)> const& on_output)
|
|
{
|
|
for (;;) {
|
|
char buffer[4096];
|
|
auto nread = Core::System::read(fd, Bytes { buffer, sizeof(buffer) });
|
|
if (nread.is_error())
|
|
return false;
|
|
|
|
if (nread.value() > 0) {
|
|
on_output(StringView { buffer, nread.value() });
|
|
if (!drain_available)
|
|
return false;
|
|
|
|
continue;
|
|
}
|
|
|
|
return nread.value() == 0;
|
|
}
|
|
}
|
|
|
|
void TestRunCapture::consume_helper_capture(pid_t pid)
|
|
{
|
|
auto capture = m_helper_output_captures.take(pid);
|
|
if (!capture.has_value())
|
|
return;
|
|
|
|
auto helper_capture = capture.release_value();
|
|
|
|
helper_capture->stdout_notifier->set_enabled(false);
|
|
helper_capture->stderr_notifier->set_enabled(false);
|
|
|
|
if (helper_capture->stdout_reader) {
|
|
(void)drain_capture_output(helper_capture->stdout_reader->fd(), true, [this, type = helper_capture->type, pid = helper_capture->pid](StringView message) {
|
|
log_helper_message({ type, pid }, STDOUT_FILENO, message);
|
|
});
|
|
}
|
|
if (helper_capture->stderr_reader) {
|
|
(void)drain_capture_output(helper_capture->stderr_reader->fd(), true, [this, type = helper_capture->type, pid = helper_capture->pid](StringView message) {
|
|
log_helper_message({ type, pid }, m_stderr_capture.original_fd.value_or(STDERR_FILENO), message);
|
|
});
|
|
}
|
|
helper_capture->stdout_notifier->close();
|
|
helper_capture->stderr_notifier->close();
|
|
}
|
|
|
|
void TestRunCapture::destroy_view_capture_of(TestWebView const& view)
|
|
{
|
|
auto capture = m_test_output_captures.take(&view);
|
|
if (!capture.has_value())
|
|
return;
|
|
|
|
auto view_capture = capture.release_value();
|
|
if (view_capture->stdout_notifier)
|
|
view_capture->stdout_notifier->close();
|
|
if (view_capture->stderr_notifier)
|
|
view_capture->stderr_notifier->close();
|
|
}
|
|
|
|
}
|