| // 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 |