blob: c25e341de6dbb609938f56b30092710592a0c425 [file] [edit]
#include "conformance/result_ledger.h"
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <ios>
#include <string>
#include <utility>
#include <vector>
#include "absl/algorithm/container.h"
#include "absl/container/btree_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/log/absl_check.h"
#include "absl/log/absl_log.h"
#include "absl/status/status.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "conformance/failure_list.h"
#include "conformance/result_record.h"
#include "conformance/testee.h"
namespace google {
namespace protobuf {
namespace conformance {
namespace internal {
namespace {
bool InsertUniqueTest(absl::flat_hash_set<std::string>& set,
absl::string_view value) {
if (!set.emplace(value).second) {
return false;
}
return true;
}
void IncrementIfUnique(bool unique, int& counter) {
if (unique) {
++counter;
}
}
// Formats an entry line for SaveFailureList(): the test name padded to
// `alignment`, then " # " and the message, or the name alone if the line has
// no message. A comment or blank line is only stripped.
std::string ReformatLine(size_t alignment, absl::string_view line) {
size_t comment_pos = line.find('#');
absl::string_view test_name =
absl::StripAsciiWhitespace(line.substr(0, comment_pos));
if (test_name.empty()) {
return std::string(absl::StripAsciiWhitespace(line));
}
absl::string_view message;
if (comment_pos != absl::string_view::npos) {
message = absl::StripAsciiWhitespace(line.substr(comment_pos + 1));
}
if (message.empty()) {
return std::string(test_name);
}
ABSL_CHECK_GE(alignment, test_name.length());
std::string whitespace(alignment - test_name.length(), ' ');
return absl::StrCat(test_name, whitespace, " # ", message);
}
// The failure list line for `test_name` failing with `message`, which may be
// empty; ReformatLine() aligns it.
std::string EntryLine(absl::string_view test_name, absl::string_view message) {
if (message.empty()) return std::string(test_name);
return absl::StrCat(test_name, " # ", message);
}
// The test name (or wildcard) of a failure list line, i.e. what precedes its
// comment, with all whitespace removed: the key entries are loaded and looked
// up by, so that LoadFailureList() and AppendSavedLines() agree on it.
std::string EntryKeyOf(absl::string_view line) {
std::string key(line.substr(0, line.find('#')));
key.erase(std::remove_if(key.begin(), key.end(), absl::ascii_isspace),
key.end());
return key;
}
// The test name of a failure list line as written (an entry as listed, see
// EntryKeyOf() for the key it is looked up by), or empty for a comment or
// blank line.
absl::string_view EntryOf(absl::string_view line) {
return absl::StripAsciiWhitespace(line.substr(0, line.find('#')));
}
// The copybara markers that delimit a region the open source export drops.
// They are spelled in two pieces so that the export's scrubber, which
// processes every file, doesn't take these literals for markers.
constexpr absl::string_view kStripBeginMarker =
"copybara:"
"strip_begin";
constexpr absl::string_view kStripEndMarker =
"copybara:"
"strip_end";
// Whether `line` is a comment whose text starts with `marker`, e.g. a
// `# <kStripBeginMarker>` line (copybara allows text after the marker).
bool IsCommentStartingWith(absl::string_view line, absl::string_view marker) {
line = absl::StripAsciiWhitespace(line);
if (!absl::ConsumePrefix(&line, "#")) return false;
return absl::StartsWith(absl::StripLeadingAsciiWhitespace(line), marker);
}
bool IsStripBegin(absl::string_view line) {
return IsCommentStartingWith(line, kStripBeginMarker);
}
// Also matches the `strip_end_and_replace_begin` marker, which closes the
// block as well.
bool IsStripEnd(absl::string_view line) {
return IsCommentStartingWith(line, kStripEndMarker);
}
// A line SaveFailureList() writes, with the alignment scope it belongs to: 0
// for the lines outside copybara strip blocks, k > 0 for the lines of the k-th
// block (its marker lines included).
struct SavedLine {
std::string text;
size_t scope;
};
} // namespace
ResultLedger::~ResultLedger() {
if (!finalized_) {
ABSL_LOG(FATAL)
<< "ResultLedger::Finalize() was not called before destruction.";
}
}
absl::Status ResultLedger::LoadFailureList(absl::string_view filename) {
std::ifstream infile(std::string{filename});
if (!infile.is_open()) {
return absl::InternalError(
absl::StrCat("Couldn't open failure list file: ", filename));
}
for (std::string line; std::getline(infile, line);) {
failure_list_lines_.push_back(line);
std::string test_name = EntryKeyOf(line);
if (test_name.empty()) { // Skip empty lines and comments.
continue;
}
// Find our failure message if it exists. Will be set to an empty string
// if no message is found. Empty failure messages also pass our tests.
size_t comment_pos = line.find('#');
std::string message;
if (comment_pos != std::string::npos) {
message = line.substr(comment_pos + 1); // +1 to skip the delimiter
// If we had only whitespace after the delimiter, we will have an empty
// failure message and the test will still pass.
message = std::string(absl::StripAsciiWhitespace(message));
}
if (absl::Status status = AddExpectedFailure(test_name, message);
!status.ok()) {
return status;
}
}
return absl::OkStatus();
}
absl::Status ResultLedger::AddExpectedFailure(
absl::string_view test_name, absl::string_view failure_message) {
if (absl::Status status = failure_list_.Add(test_name, failure_message);
!status.ok()) {
return status;
}
ABSL_CHECK(unseen_expected_failures_.emplace(test_name).second);
ABSL_CHECK(unmatched_expected_failures_.emplace(test_name).second);
return absl::OkStatus();
}
absl::Status ResultLedger::SaveFailureList(absl::string_view filename) const {
std::ofstream outfile(std::string{filename}, std::ios::binary);
if (!outfile.is_open()) {
return absl::InternalError(absl::StrCat(
"Couldn't open failure list file for writing: ", filename));
}
// The loaded lines with unseen entries dropped and changed entries rewritten
// (see AppendSavedLines()), each tagged with its alignment scope; the new
// failures are merged in below. A copybara strip block runs from a
// strip_begin line to the next strip_end line; a strip_begin with no
// strip_end after it, a strip_end outside a block and a strip_begin inside
// one are ordinary comments, as they are to LoadFailureList().
std::vector<SavedLine> lines;
size_t blocks = 0;
size_t scope = 0;
for (size_t i = 0; i < failure_list_lines_.size(); ++i) {
const std::string& line = failure_list_lines_[i];
if (scope == 0 && IsStripBegin(line) &&
std::any_of(
failure_list_lines_.begin() + static_cast<std::ptrdiff_t>(i),
failure_list_lines_.end(), IsStripEnd)) {
scope = ++blocks;
}
std::vector<std::string> saved;
AppendSavedLines(line, saved);
for (std::string& text : saved) {
lines.push_back({std::move(text), scope});
}
if (scope != 0 && IsStripEnd(line)) {
scope = 0;
}
}
// Calculate the alignment of each scope: the longest name among its lines
// and, outside the blocks, the new failures.
std::vector<size_t> alignment(blocks + 1, 0);
for (const SavedLine& line : lines) {
alignment[line.scope] =
std::max(alignment[line.scope], EntryOf(line.text).length());
}
for (const auto& failure : new_failures_) {
alignment[0] = std::max(alignment[0], failure.first.length());
}
// Output the lines, inserting the new failures by name among the lines
// outside the blocks.
auto to_add = new_failures_.begin();
for (const SavedLine& line : lines) {
if (line.scope == 0) {
absl::string_view test_name = EntryOf(line.text);
while (to_add != new_failures_.end() && test_name > to_add->first) {
outfile << ReformatLine(alignment[0],
EntryLine(to_add->first, to_add->second))
<< "\n";
++to_add;
}
}
outfile << ReformatLine(alignment[line.scope], line.text) << "\n";
}
// Add any remaining new failures.
while (to_add != new_failures_.end()) {
outfile << ReformatLine(alignment[0],
EntryLine(to_add->first, to_add->second))
<< "\n";
++to_add;
}
outfile.close();
if (!outfile.good()) {
return absl::InternalError(
absl::StrCat("Failed to write failure list file: ", filename));
}
return absl::OkStatus();
}
void ResultLedger::AppendSavedLines(absl::string_view line,
std::vector<std::string>& lines) const {
const std::string entry = EntryKeyOf(line);
if (entry.empty()) {
// A comment or blank line.
lines.emplace_back(line);
return;
}
auto it = entry_matches_.find(entry);
if (it == entry_matches_.end()) {
// No test matched the entry with a verdict: it is unseen and goes.
return;
}
const absl::btree_map<std::string, MatchOutcome>& matches = it->second;
// Kept verbatim unless a matched test succeeded, was skipped or failed with
// a different message.
const bool changed = absl::c_any_of(matches, [](const auto& match) {
return match.second.kind != MatchOutcome::kExpectedFailure;
});
if (!changed) {
lines.emplace_back(line);
return;
}
// Kept as the entry (a wildcard included) with the new message if every
// matched test failed with that same message.
const MatchOutcome& first = matches.begin()->second;
const bool same_new_message =
first.kind == MatchOutcome::kOtherFailure &&
absl::c_all_of(matches, [&](const auto& match) {
return match.second.kind == MatchOutcome::kOtherFailure &&
match.second.message == first.message;
});
if (same_new_message) {
lines.push_back(EntryLine(entry, first.message));
return;
}
// Otherwise the matched set diverged: one line per matched test that still
// fails (which for an exact entry that succeeded or was skipped is none).
const std::string expected_message = *failure_list_.ExpectedMessage(entry);
for (const auto& [test_name, outcome] : matches) {
switch (outcome.kind) {
case MatchOutcome::kSuccess:
case MatchOutcome::kSkip:
break;
case MatchOutcome::kOtherFailure:
lines.push_back(EntryLine(test_name, outcome.message));
break;
case MatchOutcome::kExpectedFailure:
lines.push_back(EntryLine(test_name, expected_message));
break;
}
}
}
absl::Status ResultLedger::ReportSuccess(absl::string_view test_name) {
bool unique = InsertUniqueTest(seen_tests_, test_name);
absl::optional<std::string> failure_match = MarkMatched(test_name);
if (failure_match.has_value()) {
// This was expected to fail, but it succeeded.
IncrementIfUnique(unique, number_of_matches_[*failure_match]);
IncrementIfUnique(unique, unexpected_successes_);
if (unique) {
unexpected_success_matches_[test_name] = *failure_match;
}
unseen_expected_failures_.erase(*failure_match);
entry_matches_[*failure_match][test_name] = {
/*kind=*/MatchOutcome::kSuccess};
} else {
// This wasn't expected to fail.
IncrementIfUnique(unique, expected_successes_);
}
return failure_list_.VerdictOnSuccess(test_name);
}
absl::Status ResultLedger::ReportFailure(absl::string_view test_name,
TestPriority priority,
absl::string_view failure_message) {
bool unique = InsertUniqueTest(seen_tests_, test_name);
absl::optional<std::string> failure_match = MarkMatched(test_name);
std::string formatted_failure_message = FormatFailureMessage(failure_message);
// Counts the failure as unexpected and records it for UnexpectedFailures().
auto record_unexpected_failure = [&] {
IncrementIfUnique(unique, unexpected_failures_);
if (unique) {
unexpected_failure_messages_[test_name] = formatted_failure_message;
}
};
if (!failure_match.has_value()) {
if (priority > failure_list_.enforcement_level()) {
// Tolerated: neither a failure nor a skip, and not written to the
// failure list; only counted.
IncrementIfUnique(unique, tolerated_failures_);
} else {
// This was not expected to fail.
record_unexpected_failure();
new_failures_[test_name] = formatted_failure_message;
}
return failure_list_.VerdictOnFailure(test_name, priority, failure_message);
}
// The entry is seen either way: Finalize() is about entries no test
// reached, and a message mismatch is already reported by the verdict, by
// UnexpectedFailures() and by SaveFailureList().
unseen_expected_failures_.erase(*failure_match);
if (!absl::StartsWith(formatted_failure_message,
*failure_list_.ExpectedMessage(*failure_match))) {
record_unexpected_failure();
entry_matches_[*failure_match][test_name] = {
/*kind=*/MatchOutcome::kOtherFailure,
/*message=*/formatted_failure_message};
} else {
entry_matches_[*failure_match][test_name] = {
/*kind=*/MatchOutcome::kExpectedFailure};
IncrementIfUnique(unique, number_of_matches_[*failure_match]);
IncrementIfUnique(unique, expected_failures_);
}
return failure_list_.VerdictOnFailure(test_name, priority, failure_message);
}
absl::Status ResultLedger::ReportSkip(absl::string_view test_name,
absl::string_view skip_reason) {
bool unique = InsertUniqueTest(seen_tests_, test_name);
IncrementIfUnique(unique, skipped_);
absl::optional<std::string> failure_match = MarkMatched(test_name);
if (!failure_match.has_value()) {
return absl::OkStatus();
}
// A skipped test is a listed test that didn't fail, so its match is not an
// unseen entry for Finalize() but is dropped by SaveFailureList(), like a
// success (the verdict asks for the same by hand). It doesn't count toward
// the entry's wildcard expansion cap (number_of_matches_) either, unlike in
// the legacy runner, which counted every test a wildcard matched: the cap
// is about real outcomes hidden by a wildcard, and a skip isn't one.
unseen_expected_failures_.erase(*failure_match);
entry_matches_[*failure_match][test_name] = {/*kind=*/MatchOutcome::kSkip};
IncrementIfUnique(unique, listed_skips_);
if (unique) {
listed_skip_matches_[test_name] = *failure_match;
}
return failure_list_.VerdictOnSkip(test_name, skip_reason);
}
void ResultLedger::ReportNotSelected(absl::string_view test_name) {
seen_tests_.emplace(test_name);
MarkMatched(test_name);
}
void ResultLedger::Report(absl::string_view test_name,
const ResultRecord& record) {
switch (record.status) {
case ResultRecord::Status::kPass:
ReportSuccess(test_name).IgnoreError();
break;
case ResultRecord::Status::kFail:
case ResultRecord::Status::kCrash:
ReportFailure(test_name, record.priority, record.message).IgnoreError();
break;
case ResultRecord::Status::kSkip:
ReportSkip(test_name, record.message).IgnoreError();
break;
case ResultRecord::Status::kNotSelected:
ReportNotSelected(test_name);
break;
}
}
absl::optional<std::string> ResultLedger::MarkMatched(
absl::string_view test_name) {
absl::optional<std::string> failure_match =
failure_list_.MatchingEntry(test_name);
if (failure_match.has_value()) {
unmatched_expected_failures_.erase(*failure_match);
}
return failure_match;
}
std::vector<std::string> ResultLedger::OverexpandedWildcards() const {
std::vector<std::string> entries;
for (const auto& [entry, matches] : number_of_matches_) {
if (matches > kMaximumWildcardExpansions) entries.push_back(entry);
}
absl::c_sort(entries);
return entries;
}
std::vector<std::string> ResultLedger::UnseenExpectedFailures() const {
std::vector<std::string> unseen(unseen_expected_failures_.begin(),
unseen_expected_failures_.end());
absl::c_sort(unseen);
return unseen;
}
std::vector<std::string> ResultLedger::UnmatchedExpectedFailures() const {
std::vector<std::string> unmatched(unmatched_expected_failures_.begin(),
unmatched_expected_failures_.end());
absl::c_sort(unmatched);
return unmatched;
}
std::vector<UnexpectedResult> ResultLedger::UnexpectedFailures() const {
std::vector<UnexpectedResult> failures;
failures.reserve(unexpected_failure_messages_.size());
// The map is ordered by test name.
for (const auto& [test_name, failure_message] :
unexpected_failure_messages_) {
failures.push_back(
{/*test_name=*/test_name, /*failure_message=*/failure_message});
}
return failures;
}
std::vector<UnexpectedResult> ResultLedger::UnexpectedSuccesses() const {
std::vector<UnexpectedResult> successes;
successes.reserve(unexpected_success_matches_.size());
// The map is ordered by test name.
for (const auto& [test_name, matched_entry] : unexpected_success_matches_) {
successes.push_back(
{/*test_name=*/test_name,
/*failure_message=*/*failure_list_.ExpectedMessage(matched_entry),
/*matched_entry=*/matched_entry});
}
return successes;
}
std::vector<std::pair<std::string, std::string>> ResultLedger::ListedSkips()
const {
// The map is ordered by test name.
return std::vector<std::pair<std::string, std::string>>(
listed_skip_matches_.begin(), listed_skip_matches_.end());
}
absl::Status ResultLedger::Finalize() {
finalized_ = true;
std::vector<std::string> unseen = UnseenExpectedFailures();
if (!unseen.empty()) {
return absl::FailedPreconditionError(
absl::StrCat("The following expected failures were not seen: ",
absl::StrJoin(unseen, ", ")));
}
return absl::OkStatus();
}
} // namespace internal
} // namespace conformance
} // namespace protobuf
} // namespace google