blob: 98dfe398584e17e8dea34ffef61cf280123d779b [file] [log] [blame] [edit]
// Copyright 2022 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/e2e_tests/script_test.h"
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <string>
#include <string_view>
#include <gtest/gtest.h>
#include "src/developer/debug/e2e_tests/fuzzy_matcher.h"
#include "src/developer/debug/shared/string_util.h"
#include "src/developer/debug/zxdb/common/host_util.h"
#include "src/lib/fxl/strings/trim.h"
namespace zxdb {
namespace {
constexpr uint64_t kDefaultTimeout = 3; // in seconds
constexpr std::string_view kBuildType = ZXDB_E2E_TESTS_BUILD_TYPE;
} // namespace
void ScriptTest::TestBody() {
script_file_ = std::ifstream(script_path_);
ASSERT_TRUE(script_file_) << "Fail to open " << script_path_;
// Process directives first.
uint64_t timeout = kDefaultTimeout;
std::string line;
while (std::getline(script_file_, line)) {
line_number_++;
if (line.empty())
continue;
if (debug::StringStartsWith(line, "##")) {
std::string directive = std::string(fxl::TrimString(line.substr(2), " "));
if (debug::StringStartsWith(directive, "require ")) {
std::string requirement = directive.substr(8);
if (kBuildType.find(requirement) == std::string::npos) {
GTEST_SKIP() << "Skipped because of unmet requirement " << requirement;
}
} else if (debug::StringStartsWith(directive, "set timeout ")) {
timeout = std::stoul(directive.substr(12));
} else {
GTEST_FAIL() << "Unknown directive: " << directive;
}
} else if (debug::StringStartsWith(line, "#")) {
continue;
} else {
// Put the line back.
script_file_.seekg(-static_cast<int>(line.size() + 1), std::ios::cur);
line_number_--;
break;
}
}
// Adjust timeout when running on bots so we're less likely to flake.
if (std::getenv("BUILDBUCKET_ID")) {
timeout *= 5;
}
std::string error_msg;
loop().PostTimer(FROM_HERE, timeout * 1000, [&]() {
error_msg = "Failed to find pattern \"" + expected_output_pattern_ +
"\" in the output:\n"
"============================= BEGIN OUTPUT =============================\n" +
output_for_debug_ +
"============================== END OUTPUT ==============================";
loop().QuitNow();
});
console().output_observers().AddObserver(this);
ProcessScriptLines();
loop().Run();
console().output_observers().RemoveObserver(this);
if (!error_msg.empty()) {
ADD_FAILURE_AT(script_path_.c_str(), line_number_) << error_msg;
}
}
void ScriptTest::OnOutput(const OutputBuffer& output) {
output_for_debug_ += output.AsString();
if (!output_for_debug_.empty() && output_for_debug_.back() != '\n') {
output_for_debug_ += '\n';
}
// We get outputs in chunks, so the entire output of one command may appear in a single output
// buffer we receive. Meanwhile the expected outputs given by the script are parsed line by line.
// So, we want to exhaustively match this chunk of real output until we either reach a new command
// or the end of the file.
FuzzyMatcher matcher(output.AsString());
bool match = matcher.MatchesLine(expected_output_pattern_, allow_out_of_order_output_);
while (match && ProcessScriptLines()) {
match = matcher.MatchesLine(expected_output_pattern_, allow_out_of_order_output_);
}
if (match) {
// We got at least one match, we're done processing this command's output.
processing_ = false;
}
// ProcessScriptLines will return false when we get to EOF or a new command. If it's EOF, we're
// done.
if (script_file_.eof()) {
loop().PostTask(FROM_HERE, [this]() { loop().QuitNow(); });
}
}
bool ScriptTest::ProcessScriptLines() {
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);
// Indicate that calling code should stop processing output so it can be matched against the
// new command.
return false;
} 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 true;
}
return false;
}
void ScriptTest::DispatchNextCommandWhenReady(const std::string& command) {
if (processing_) {
loop().PostTask(FROM_HERE, [this, command]() { DispatchNextCommandWhenReady(command); });
return;
}
// Always defer ProcessInputLine because it could trigger OnOutput synchronously.
loop().PostTask(FROM_HERE, [this, command]() {
// Make sure we update all of our state before issuing the command.
output_for_debug_.clear();
allow_out_of_order_output_ = false;
processing_ = true;
// Fetch the first line of expected output, we may process directives here so it's important to
// do this after resetting our internal state above.
ProcessScriptLines();
console().ProcessInputLine(command);
});
}
void ScriptTest::OnTestExited(const std::string& url) {
// Insert a definitive marker for a test component being completed. Scripts that use `run-test`
// will want to depend on this output so we remain listening for test_runner messages until it has
// completely shutdown.
loop().PostTask(FROM_HERE, [this, url]() { console().Output("Test Done: " + url, false); });
}
void ScriptTest::RegisterScriptTests() {
std::filesystem::path test_scripts_dir =
(std::filesystem::path(GetSelfPath()).parent_path() / ZXDB_E2E_TESTS_SCRIPTS_DIR)
.lexically_normal();
for (const auto& entry : std::filesystem::directory_iterator(test_scripts_dir)) {
if (entry.path().extension() == ".script") {
::testing::RegisterTest("ScriptTest", entry.path().stem().c_str(), nullptr, nullptr,
entry.path().c_str(), 0,
[=]() { return new ScriptTest(entry.path()); });
}
}
}
} // namespace zxdb