blob: 744741b44c02fe7230ee9c3f56533be810c44987 [file] [log] [blame]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/zxdb/console/script_runner.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include "src/developer/debug/shared/message_loop.h"
#include "src/developer/debug/shared/string_util.h"
#include "src/developer/debug/zxdb/common/fuzzy_matcher.h"
#include "src/developer/debug/zxdb/console/output_buffer.h"
#include "src/lib/fxl/strings/trim.h"
namespace zxdb {
ScriptRunner::ScriptRunner(Session* session, Console* console)
: console_(console), weak_factory_(this) {}
ScriptRunner::~ScriptRunner() = default;
void ScriptRunner::Run(const std::filesystem::path& path, CompletionCallback cb) {
script_path_ = path;
completion_cb_ = std::move(cb);
script_file_ = std::ifstream(script_path_);
if (!script_file_) {
Fail("Failed to open " + script_path_.string());
return;
}
console_->output_observers().AddObserver(this);
debug::MessageLoop::Current()->PostTimer(
FROM_HERE, timeout_s_ * 1000, [weak_this = weak_factory_.GetWeakPtr()]() {
if (weak_this && !weak_this->has_failed_ && weak_this->completion_cb_) {
std::string error_msg = "Timeout waiting for pattern \"" +
weak_this->expected_output_pattern_ + "\" in the output:\n";
weak_this->Fail(error_msg);
}
});
ProcessScriptLines();
}
void ScriptRunner::OnOutput(const OutputBuffer& output) {
if (has_failed_) {
return;
}
std::string output_str = output.AsString();
collected_output_.append(output_str);
if (!collected_output_.empty() && collected_output_.back() != '\n') {
collected_output_.push_back('\n');
}
FuzzyMatcher matcher(collected_output_);
while (!expected_output_pattern_.empty() &&
matcher.MatchesLine(expected_output_pattern_, allow_out_of_order_output_)) {
expected_output_pattern_.clear();
ProcessScriptLines();
}
if (script_file_.eof() && expected_output_pattern_.empty()) {
console_->output_observers().RemoveObserver(this);
Complete(true);
}
}
void ScriptRunner::ProcessScriptLines() {
if (!expected_output_pattern_.empty())
return;
std::string line;
while (std::getline(script_file_, line)) {
line_number_++;
if (line.empty()) {
continue;
}
// Inputs.
if (debug::StringStartsWith(line, "[zxdb]")) {
std::string command = std::string(fxl::TrimString(line.substr(6), " "));
DispatchNextCommandWhenReady(command);
return;
} else if (debug::StringStartsWith(line, "##")) {
// Inline directives.
std::string directive = std::string(fxl::TrimString(line.substr(2), " "));
if (debug::StringStartsWith(directive, "allow-out-of-order-output")) {
allow_out_of_order_output_ = true;
}
continue;
} else if (debug::StringStartsWith(line, "#")) {
// Comment.
continue;
}
// Expected outputs.
expected_output_pattern_ = line;
return;
}
}
void ScriptRunner::DispatchNextCommandWhenReady(const std::string& command) {
if (!expected_output_pattern_.empty()) {
// Still waiting on output from the last dispatched command.
debug::MessageLoop::Current()->PostTask(FROM_HERE,
[weak_this = weak_factory_.GetWeakPtr(), command]() {
if (weak_this)
weak_this->DispatchNextCommandWhenReady(command);
});
return;
}
debug::MessageLoop::Current()->PostTask(FROM_HERE,
[weak_this = weak_factory_.GetWeakPtr(), command]() {
if (!weak_this)
return;
weak_this->collected_output_.clear();
weak_this->allow_out_of_order_output_ = false;
// Fetch the first line of expected output.
weak_this->ProcessScriptLines();
weak_this->console_->ProcessInputLine(command);
});
}
OutputBuffer ScriptRunner::GetFailureContext() {
return "Expected: \"" + expected_output_pattern_ + "\" on script line #" +
std::to_string(line_number_) + " but got output:\n" +
"============================= BEGIN OUTPUT =============================\n" +
collected_output_ +
"============================== END OUTPUT ==============================\n" +
"You may want to check the output order in the script matches the generated "
"output, or use ## allow-out-of-order-output. See https://fuchsia.dev/fuchsia-src/development/debugger/scripting "
"for more examples.";
}
void ScriptRunner::Fail(const std::string& message) {
has_failed_ = true;
// We cannot defer unregistering ourselves as an observer until |Complete| since we will begin
// receiving output events from ourselves.
console_->output_observers().RemoveObserver(this);
console_->Output(Err(message + "\n"));
console_->Output(GetFailureContext());
Complete(false);
}
void ScriptRunner::Complete(bool success) {
if (completion_cb_) {
auto cb = std::move(completion_cb_);
cb(success);
}
}
} // namespace zxdb