blob: 98c69eff4d5c37a21b217e3aa84e088c70f7e04d [file]
// Protocol Buffers - Google's data interchange format
// Copyright 2026 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "conformance/test_environment.h"
#include <algorithm>
#include <cstdlib>
#include <memory>
#include <string>
#include <utility>
#include "google/protobuf/descriptor.pb.h"
#include <gtest/gtest.h>
#include "absl/base/nullability.h"
#include "absl/log/absl_check.h"
#include "absl/log/absl_log.h"
#include "absl/status/status.h"
#include "absl/strings/ascii.h" // IWYU pragma: keep
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "conformance/fork_pipe_runner.h"
#include "conformance/global_test_environment.h"
#include "conformance/test_manager.h"
#include "conformance/test_runner.h"
#include "conformance/testee.h"
namespace google {
namespace protobuf {
namespace conformance {
namespace internal {
namespace {
// Whether `path` is absolute on this platform. Only paths that start with a
// slash are absolute on POSIX; Windows also has drive letters ("C:\\...",
// "C:/...") and root-relative backslash paths.
bool IsAbsolutePath(absl::string_view path) {
#ifdef _WIN32
if (path.size() >= 2 && absl::ascii_isalpha(path[0]) && path[1] == ':') {
return true;
}
return absl::StartsWith(path, "/") || absl::StartsWith(path, "\\");
#else
return absl::StartsWith(path, "/");
#endif
}
// The process-global environment. Set and cleared by ConformanceEnvironment's
// constructor and destructor.
ConformanceEnvironment* global_environment = nullptr;
// Whether the current gtest run only executes a subset of the binary's
// non-disabled tests. That happens with --gtest_filter, with test sharding,
// or when --gtest_fail_fast stopped the run early after a failure. Only
// meaningful once RUN_ALL_TESTS() has selected the tests to run.
bool IsPartialRun() {
const testing::UnitTest& unit_test = *testing::UnitTest::GetInstance();
// Under --gtest_fail_fast, the tests after the first failure are skipped but
// still counted as "to run", so an early stop has to be detected separately.
// This is conservative: a failure in the very last test also counts as
// partial, although nothing was skipped.
if (GTEST_FLAG_GET(fail_fast) && unit_test.Failed()) {
return true;
}
// test_to_run_count() already reflects --gtest_filter and sharding. Disabled
// tests are excluded from the baseline, so a normal run isn't "partial" just
// because it skips DISABLED_ tests, while --gtest_also_run_disabled_tests
// still counts as a full run.
return unit_test.test_to_run_count() <
unit_test.total_test_count() - unit_test.disabled_test_count();
}
// Returns the runner the environment owns, if any: `owned_runner` itself, or a
// ForkPipeRunner for `testee_binary`. Null when using the caller's `runner`.
std::unique_ptr<ConformanceTestRunner> TakeOwnedRunner(
ConformanceEnvironmentOptions& options) {
int sources = (options.runner != nullptr) +
(options.owned_runner != nullptr) +
!options.testee_binary.empty();
ABSL_CHECK_EQ(sources, 1)
<< "Exactly one of `runner`, `owned_runner` and `testee_binary` must be "
"set in ConformanceEnvironmentOptions.";
if (options.owned_runner != nullptr) {
return std::move(options.owned_runner);
}
if (!options.testee_binary.empty()) {
return std::make_unique<ForkPipeRunner>(options.testee_binary,
options.testee_args);
}
return nullptr;
}
// The environment handed to gtest by ConformanceEnvironment::Install(). gtest
// deletes it at the end of RUN_ALL_TESTS(); it forwards to the real
// environment, which must outlive that (see ConformanceEnvironment).
class ForwardingEnvironment : public testing::Environment {
public:
explicit ForwardingEnvironment(ConformanceEnvironment* absl_nonnull target)
: target_(target) {}
void SetUp() override { target_->SetUp(); }
void TearDown() override { target_->TearDown(); }
private:
ConformanceEnvironment* const absl_nonnull target_;
};
} // namespace
Statistics Statistics::From(const TestManager& manager) {
Statistics statistics;
statistics.skipped_tests = manager.skipped();
statistics.listed_skips = manager.listed_skips();
statistics.tolerated_failures = manager.tolerated_failures();
statistics.expected_failures = manager.expected_failures();
statistics.unexpected_failures = manager.unexpected_failures();
statistics.expected_successes = manager.expected_successes();
statistics.unexpected_successes = manager.unexpected_successes();
return statistics;
}
Statistics Statistics::operator-(const Statistics& other) const {
Statistics delta;
delta.skipped_tests = skipped_tests - other.skipped_tests;
delta.listed_skips = listed_skips - other.listed_skips;
delta.tolerated_failures = tolerated_failures - other.tolerated_failures;
delta.expected_failures = expected_failures - other.expected_failures;
delta.unexpected_failures = unexpected_failures - other.unexpected_failures;
delta.expected_successes = expected_successes - other.expected_successes;
delta.unexpected_successes =
unexpected_successes - other.unexpected_successes;
return delta;
}
Statistics& Statistics::operator+=(const Statistics& other) {
skipped_tests += other.skipped_tests;
listed_skips += other.listed_skips;
tolerated_failures += other.tolerated_failures;
expected_failures += other.expected_failures;
unexpected_failures += other.unexpected_failures;
expected_successes += other.expected_successes;
unexpected_successes += other.unexpected_successes;
return *this;
}
void Statistics::RecordProperties() const {
testing::Test::RecordProperty("skipped_tests", skipped_tests);
testing::Test::RecordProperty("listed_skips", listed_skips);
testing::Test::RecordProperty("tolerated_failures", tolerated_failures);
testing::Test::RecordProperty("expected_failures", expected_failures);
testing::Test::RecordProperty("unexpected_failures", unexpected_failures);
testing::Test::RecordProperty("expected_successes", expected_successes);
testing::Test::RecordProperty("unexpected_successes", unexpected_successes);
}
TestManager& GetGlobalTestManager() {
return ConformanceEnvironment::Get().test_manager();
}
ConformanceEnvironment::ConformanceEnvironment(
ConformanceEnvironmentOptions options)
: options_(std::move(options)),
owned_runner_(TakeOwnedRunner(options_)),
testee_(std::make_unique<Testee>(
options_.runner != nullptr ? options_.runner : owned_runner_.get())) {
ABSL_CHECK(global_environment == nullptr)
<< "A global ConformanceEnvironment is already set.";
global_environment = this;
// Anything below proto3 (including the legacy runner's EDITION_UNKNOWN
// default) is meaningless, since proto2 and proto3 tests always run. This is
// the one place the maximum edition is clamped; see the option's comment.
options_.maximum_edition = std::max(options_.maximum_edition, EDITION_PROTO3);
test_manager_.set_enforcement_level(options_.enforcement_level);
}
ConformanceEnvironment::~ConformanceEnvironment() {
// TestManager insists on being finalized before destruction. An installed
// environment is never destroyed (see Install()), so this only runs for
// environments constructed by ScopedGlobalConformanceEnvironment in unit
// tests, and those don't necessarily call TearDown(); make sure we don't
// crash on the way out.
test_manager_.Finalize().IgnoreError();
ABSL_CHECK_EQ(global_environment, this);
global_environment = nullptr;
}
ConformanceEnvironment& ConformanceEnvironment::Install(
ConformanceEnvironmentOptions options) {
ABSL_CHECK(global_environment == nullptr)
<< "ConformanceEnvironment::Install() must only be called once.";
// gtest deletes the environments it owns at the end of RUN_ALL_TESTS(), but
// callers (e.g. the merged conformance_test_runner) still need the test
// manager afterwards. So although this class is a testing::Environment, it
// isn't registered with gtest itself: gtest only gets a forwarding proxy,
// and the real environment intentionally lives until the process exits.
// Only the environment object leaks, though; TearDown() releases the testee.
// TODO: b/410122039 - register it directly once the legacy suites are gone.
auto* environment = new ConformanceEnvironment(std::move(options));
testing::AddGlobalTestEnvironment(new ForwardingEnvironment(environment));
return *environment;
}
ConformanceEnvironment& ConformanceEnvironment::Get() {
ABSL_CHECK(global_environment != nullptr)
<< "No ConformanceEnvironment has been installed. Conformance test "
"binaries must call ConformanceEnvironment::Install() before "
"RUN_ALL_TESTS().";
return *global_environment;
}
Testee& ConformanceEnvironment::testee() {
ABSL_CHECK(testee_ != nullptr)
<< "The testee has already been shut down by "
"ConformanceEnvironment::TearDown(); conformance tests can't run "
"after the environment is torn down.";
return *testee_;
}
void ConformanceEnvironment::SetUp() {
for (const std::string& failure_list : options_.failure_list_files) {
absl::Status status = test_manager_.LoadFailureList(failure_list);
ASSERT_TRUE(status.ok())
<< "Failed to load failure list " << failure_list << ": " << status;
}
set_up_succeeded_ = true;
}
void ConformanceEnvironment::TearDown() {
Statistics::From(test_manager_).RecordProperties();
const bool partial_run = partial_run_override_.value_or(IsPartialRun());
// The path the failure list was rewritten to, if --fix succeeded.
std::string fixed_list;
if (options_.fix && set_up_succeeded_) {
if (partial_run) {
ADD_FAILURE()
<< "--fix ignored: only a subset of the tests ran (e.g. because of "
"--gtest_filter, sharding or --gtest_fail_fast), so rewriting the "
"failure list would drop the entries of the tests that didn't "
"run. Rerun with --fix and without the filter.";
} else {
std::string output_file = options_.fix_output_file;
if (output_file.empty()) {
if (options_.failure_list_files.empty()) {
ADD_FAILURE() << "--fix needs --fix_output_file when no "
"--failure_list is given.";
} else if (options_.failure_list_files.size() > 1) {
ADD_FAILURE() << "--fix needs --fix_output_file when more than one "
"--failure_list is given.";
} else if (IsAbsolutePath(options_.failure_list_files[0])) {
output_file = options_.failure_list_files[0];
} else if (const char* workspace =
std::getenv("BUILD_WORKSPACE_DIRECTORY");
workspace != nullptr) {
output_file =
absl::StrCat(workspace, "/", options_.failure_list_files[0]);
} else {
ADD_FAILURE() << "--fix needs either --fix_output_file or "
"$BUILD_WORKSPACE_DIRECTORY (which `bazel run` "
"sets) to locate the failure list to rewrite.";
}
}
if (!output_file.empty()) {
absl::Status status = test_manager_.SaveFailureList(output_file);
if (status.ok()) {
ABSL_LOG(INFO) << "Wrote updated failure list to " << output_file;
fixed_list = output_file;
} else {
ADD_FAILURE() << "Failed to write updated failure list to "
<< output_file << ": " << status;
}
}
}
}
absl::Status status = test_manager_.Finalize();
if (!status.ok() && set_up_succeeded_ &&
options_.check_unseen_expected_failures) {
if (partial_run) {
// Only a subset of the tests ran, so most expected failures were never
// exercised. The legacy runner's --test flag behaves the same way.
ABSL_LOG(INFO) << "Not checking for unseen expected failures because "
"only some of the tests ran (e.g. because of "
"--gtest_filter or sharding):\n"
<< status.message();
} else if (fixed_list.empty()) {
ADD_FAILURE() << status.message()
<< "\nRemove them from the failure list, or rerun with "
"--fix to do so automatically.";
} else {
ADD_FAILURE() << status.message()
<< "\nThey have been dropped from the updated failure list "
"written to "
<< fixed_list << ".";
}
}
// Release the testee: for an owned runner this destroys it (stopping the
// process itself is the runner's job, see ~ForkPipeRunner); for a
// caller-owned runner it drops our reference so the caller may destroy the
// runner right after RUN_ALL_TESTS(); the merged runner relies on this.
// Either way the leaked environment (see Install()) neither keeps a testee
// alive until the process exits nor holds a dangling pointer to one.
testee_.reset();
owned_runner_.reset();
}
} // namespace internal
} // namespace conformance
} // namespace protobuf
} // namespace google