| #include "conformance/test_manager.h" | |
| #include <algorithm> | |
| #include <cctype> | |
| #include <cstddef> | |
| #include <fstream> | |
| #include <ios> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/algorithm/container.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/testee.h" | |
| namespace google { | |
| namespace protobuf { | |
| namespace conformance { | |
| namespace internal { | |
| namespace { | |
| // How many failures one wildcard entry may cover. As in the legacy runner the | |
| // check precedes the count of the current match, so the limit is reached one | |
| // failure later than the number says: the (kMaximumWildcardExpansions + 2)th | |
| // is the first one reported. | |
| constexpr int kMaximumWildcardExpansions = 20; | |
| constexpr int kFailureMessageLengthLimit = 128; | |
| 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; | |
| } | |
| } | |
| void Normalize(std::string& input) { | |
| input.erase(std::remove(input.begin(), input.end(), '\n'), input.end()); | |
| } | |
| // Sets up a failure message properly for our failure lists. | |
| std::string FormatFailureMessage(absl::string_view input) { | |
| std::string result(absl::StripAsciiWhitespace(input)); | |
| // Remove newlines | |
| Normalize(result); | |
| // Truncate failure message if needed | |
| if (result.length() > kFailureMessageLengthLimit) { | |
| result = result.substr(0, kFailureMessageLengthLimit); | |
| } | |
| return result; | |
| } | |
| 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 = | |
| absl::StripAsciiWhitespace(line.substr(comment_pos + 1)); | |
| ABSL_CHECK_GE(alignment, test_name.length()); | |
| std::string whitespace(alignment - test_name.length(), ' '); | |
| return absl::StrCat(test_name, whitespace, " # ", message); | |
| } | |
| } // namespace | |
| TestManager::~TestManager() { | |
| if (!finalized_) { | |
| ABSL_LOG(FATAL) | |
| << "TestManager::Finalize() was not called before destruction."; | |
| } | |
| } | |
| absl::Status TestManager::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); | |
| // Remove comments. | |
| std::string test_name = line.substr(0, line.find('#')); | |
| test_name.erase( | |
| std::remove_if(test_name.begin(), test_name.end(), ::isspace), | |
| test_name.end()); | |
| if (test_name.empty()) { // Skip empty lines. | |
| continue; | |
| } | |
| // If we remove whitespace from the beginning of a line, and what we have | |
| // left at first is a '#', then we have a comment. | |
| if (test_name[0] != '#') { | |
| // 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 TestManager::AddExpectedFailure( | |
| absl::string_view test_name, absl::string_view failure_message) { | |
| if (absl::Status status = expected_failure_list_.Insert(test_name); | |
| !status.ok()) { | |
| return status; | |
| } | |
| // Normalize the message the same way reported failures are normalized so | |
| // that the two compare equal in ReportFailure. | |
| ABSL_CHECK(expected_failure_messages_ | |
| .emplace(test_name, FormatFailureMessage(failure_message)) | |
| .second); | |
| ABSL_CHECK(unseen_expected_failures_.emplace(test_name).second); | |
| ABSL_CHECK(unmatched_expected_failures_.emplace(test_name).second); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status TestManager::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)); | |
| } | |
| // Calculate alignment. | |
| size_t alignment = 0; | |
| for (const std::string& line : failure_list_lines_) { | |
| absl::string_view test_name = absl::StripAsciiWhitespace( | |
| absl::string_view(line).substr(0, line.find('#'))); | |
| alignment = std::max(alignment, test_name.length()); | |
| } | |
| for (const auto& failure : new_failures_) { | |
| alignment = std::max(alignment, failure.first.length()); | |
| } | |
| // Output existing failure list, stripping out unseen tests and inserting | |
| // new failures. | |
| auto to_add = new_failures_.begin(); | |
| for (const std::string& line : failure_list_lines_) { | |
| absl::string_view test_name = absl::StripAsciiWhitespace( | |
| absl::string_view(line).substr(0, line.find('#'))); | |
| if (unseen_expected_failures_.contains(test_name) || | |
| seen_unexpected_successes_.contains(test_name)) { | |
| continue; | |
| } | |
| while (to_add != new_failures_.end() && test_name > to_add->first) { | |
| outfile << ReformatLine(alignment, absl::StrCat(to_add->first, " # ", | |
| to_add->second)) | |
| << "\n"; | |
| ++to_add; | |
| } | |
| outfile << ReformatLine(alignment, line) << "\n"; | |
| } | |
| // Add any remaining new failures. | |
| while (to_add != new_failures_.end()) { | |
| outfile << ReformatLine(alignment, | |
| absl::StrCat(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(); | |
| } | |
| absl::Status TestManager::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); | |
| seen_unexpected_successes_.insert(*failure_match); | |
| return absl::FailedPreconditionError(absl::StrCat( | |
| "test ", test_name, " (matched to ", *failure_match, | |
| ") is in the failure list, but test succeeded. Remove its match from " | |
| "the failure list.")); | |
| } | |
| // This wasn't expected to fail. | |
| IncrementIfUnique(unique, expected_successes_); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status TestManager::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 > enforcement_level_) { | |
| // Tolerated: neither a failure nor a skip, and not written to the | |
| // failure list; only counted. | |
| IncrementIfUnique(unique, tolerated_failures_); | |
| ABSL_LOG(WARNING) << "WARNING, test=" << test_name << ": " | |
| << absl::StripTrailingAsciiWhitespace(failure_message); | |
| return absl::OkStatus(); | |
| } | |
| // This was not expected to fail. | |
| record_unexpected_failure(); | |
| new_failures_[test_name] = formatted_failure_message; | |
| return absl::FailedPreconditionError( | |
| absl::StrCat("Unexpected failure for test: ", test_name)); | |
| } | |
| // Mirror the legacy runner, which only requires the actual failure message to | |
| // start with the expected one. | |
| const std::string& expected_failure_message = | |
| expected_failure_messages_.at(*failure_match); | |
| if (!absl::StartsWith(formatted_failure_message, expected_failure_message)) { | |
| record_unexpected_failure(); | |
| // TODO: b/563659620 - Keying the replacement by the (possibly wildcard) | |
| // entry duplicates the line if another test later matches the entry with | |
| // the expected message: with `foo.*.bar # abc` listed, | |
| // ReportFailure("foo.a.bar", "xyz") + ReportFailure("foo.b.bar", "abc") | |
| // makes SaveFailureList() keep the original line and add a second | |
| // `foo.*.bar` line, which LoadFailureList() then rejects. | |
| new_failures_[*failure_match] = formatted_failure_message; | |
| return absl::FailedPreconditionError(absl::StrCat( | |
| "Unexpected failure message for test: ", test_name, " expected: ", | |
| expected_failure_message, " actual: ", formatted_failure_message)); | |
| } | |
| unseen_expected_failures_.erase(*failure_match); | |
| if (number_of_matches_[*failure_match] > kMaximumWildcardExpansions) { | |
| record_unexpected_failure(); | |
| return absl::FailedPreconditionError( | |
| absl::StrCat("The wildcard ", *failure_match, | |
| " served as matches to too many test " | |
| "names exceeding the max amount of ", | |
| kMaximumWildcardExpansions, " for test: ", test_name)); | |
| } | |
| IncrementIfUnique(unique, number_of_matches_[*failure_match]); | |
| IncrementIfUnique(unique, expected_failures_); | |
| ABSL_LOG(INFO) << "Ignoring expected failure for test " << test_name << ": " | |
| << absl::StripTrailingAsciiWhitespace(failure_message); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status TestManager::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 skip says nothing about whether the entry is still needed, so it is | |
| // neither reported as unseen by Finalize() nor dropped by SaveFailureList(). | |
| // Nor does it count toward the entry's wildcard expansion cap | |
| // (number_of_matches_), 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); | |
| IncrementIfUnique(unique, listed_skips_); | |
| if (unique) { | |
| listed_skip_matches_[test_name] = *failure_match; | |
| } | |
| return absl::FailedPreconditionError(absl::StrCat( | |
| "test ", test_name, " (matched to ", *failure_match, | |
| ") is in the failure list but was skipped by the testee: ", skip_reason, | |
| ". Remove its match from the failure list.")); | |
| } | |
| void TestManager::ReportNotSelected(absl::string_view test_name) { | |
| seen_tests_.emplace(test_name); | |
| MarkMatched(test_name); | |
| } | |
| absl::optional<std::string> TestManager::MarkMatched( | |
| absl::string_view test_name) { | |
| absl::optional<std::string> failure_match = | |
| expected_failure_list_.WalkDownMatch(test_name); | |
| if (failure_match.has_value()) { | |
| unmatched_expected_failures_.erase(*failure_match); | |
| } | |
| return failure_match; | |
| } | |
| std::vector<std::string> TestManager::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> TestManager::UnmatchedExpectedFailures() const { | |
| std::vector<std::string> unmatched(unmatched_expected_failures_.begin(), | |
| unmatched_expected_failures_.end()); | |
| absl::c_sort(unmatched); | |
| return unmatched; | |
| } | |
| std::vector<UnexpectedResult> TestManager::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> TestManager::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=*/expected_failure_messages_.at(matched_entry), | |
| /*matched_entry=*/matched_entry}); | |
| } | |
| return successes; | |
| } | |
| std::vector<std::pair<std::string, std::string>> TestManager::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 TestManager::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 |