Skip to content

Commit b8d251a

Browse files
Googlercopybara-github
authored andcommitted
googletest: support the bazel test sharding protocol
PiperOrigin-RevId: 717986884
1 parent 21f2948 commit b8d251a

File tree

10 files changed

+186
-11
lines changed

10 files changed

+186
-11
lines changed

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

googletest/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
[package]
1616
name = "googletest"
17-
version = "0.13.0"
17+
version = "0.13.1"
1818
keywords = ["unit", "matcher", "testing", "assertions"]
1919
categories = ["development-tools", "development-tools::testing"]
2020
description = "A rich assertion and matcher library inspired by GoogleTest for C++"
@@ -31,7 +31,7 @@ authors = [
3131
]
3232

3333
[dependencies]
34-
googletest_macro = { path = "../googletest_macro", version = "0.13.0" }
34+
googletest_macro = { path = "../googletest_macro", version = "0.13.1" }
3535
anyhow = { version = "1", optional = true }
3636
num-traits = "0.2.17"
3737
proptest = { version = "1.2.0", optional = true }

googletest/src/internal/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
pub(crate) mod description_renderer;
1818
pub mod test_outcome;
19+
pub mod test_sharding;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/// This module implements the googletest test sharding protocol. The Google
2+
/// sharding protocol consists of the following environment variables:
3+
///
4+
/// * GTEST_TOTAL_SHARDS: total number of shards.
5+
/// * GTEST_SHARD_INDEX: number of this shard
6+
/// * GTEST_SHARD_STATUS_FILE: touch this file to indicate support for sharding.
7+
///
8+
/// See also <https://google.github.io/googletest/advanced.html>
9+
use std::cell::OnceCell;
10+
use std::env::{var, var_os};
11+
use std::ffi::OsStr;
12+
use std::fs::{self, File};
13+
use std::num::NonZeroU64;
14+
use std::path::{Path, PathBuf};
15+
16+
/// Environment variable specifying the total number of test shards.
17+
const TEST_TOTAL_SHARDS: &str = "GTEST_TOTAL_SHARDS";
18+
19+
/// Environment variable specifyign the index of this test shard.
20+
const TEST_SHARD_INDEX: &str = "GTEST_SHARD_INDEX";
21+
22+
/// Environment variable specifying the name of the file we create (or cause a
23+
/// timestamp change on) to indicate that we support the sharding protocol.
24+
const TEST_SHARD_STATUS_FILE: &str = "GTEST_SHARD_STATUS_FILE";
25+
26+
thread_local! {
27+
static SHARDING: OnceCell<Sharding> = const { OnceCell::new() };
28+
}
29+
30+
struct Sharding {
31+
this_shard: u64,
32+
total_shards: NonZeroU64,
33+
}
34+
35+
impl Default for Sharding {
36+
fn default() -> Self {
37+
Self { this_shard: 0, total_shards: NonZeroU64::MIN }
38+
}
39+
}
40+
41+
pub fn test_should_run(test_case_hash: u64) -> bool {
42+
SHARDING.with(|sharding_cell| {
43+
sharding_cell.get_or_init(Sharding::from_environment).test_should_run(test_case_hash)
44+
})
45+
}
46+
47+
impl Sharding {
48+
fn test_should_run(&self, test_case_hash: u64) -> bool {
49+
(test_case_hash % self.total_shards.get()) == self.this_shard
50+
}
51+
52+
fn from_environment() -> Sharding {
53+
let this_shard: Option<u64> =
54+
{ var(OsStr::new(TEST_SHARD_INDEX)).ok().and_then(|value| value.parse().ok()) };
55+
let total_shards: Option<NonZeroU64> = {
56+
var(OsStr::new(TEST_TOTAL_SHARDS))
57+
.ok()
58+
.and_then(|value| value.parse().ok())
59+
.and_then(NonZeroU64::new)
60+
};
61+
62+
match (this_shard, total_shards) {
63+
(Some(this_shard), Some(total_shards)) if this_shard < total_shards.get() => {
64+
if let Some(name) = var_os(OsStr::new(TEST_SHARD_STATUS_FILE)) {
65+
let pathbuf = PathBuf::from(name);
66+
if let Err(e) = create_status_file(&pathbuf) {
67+
eprintln!(
68+
"failed to create {} file {}: {}",
69+
TEST_SHARD_STATUS_FILE,
70+
pathbuf.display(),
71+
e
72+
);
73+
}
74+
}
75+
76+
Sharding { this_shard, total_shards }
77+
}
78+
_ => Sharding::default(),
79+
}
80+
}
81+
}
82+
83+
fn create_status_file(path: &Path) -> std::io::Result<()> {
84+
if let Some(parent) = path.parent() {
85+
fs::create_dir_all(parent)?;
86+
}
87+
88+
File::create(path).map(|_| ())
89+
}

googletest_macro/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
[package]
1616
name = "googletest_macro"
17-
version = "0.13.0"
17+
version = "0.13.1"
1818
keywords = ["unit", "matcher", "testing", "assertions"]
1919
categories = ["development-tools", "development-tools::testing"]
2020
description = "Procedural macros for GoogleTest Rust"

googletest_macro/src/lib.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ pub fn gtest(
7676
input: proc_macro::TokenStream,
7777
) -> proc_macro::TokenStream {
7878
let ItemFn { attrs, sig, block, .. } = parse_macro_input!(input as ItemFn);
79+
let test_case_hash: u64 = {
80+
use std::collections::hash_map::DefaultHasher;
81+
use std::hash::{Hash, Hasher};
82+
let mut h = DefaultHasher::new();
83+
84+
// Only consider attrs and name for stability. Changing the function body should
85+
// not affect the test case distribution.
86+
attrs.hash(&mut h);
87+
sig.ident.hash(&mut h);
88+
h.finish()
89+
};
90+
7991
let (outer_return_type, trailer) = if attrs
8092
.iter()
8193
.any(|attr| attr.path().is_ident("should_panic"))
@@ -168,10 +180,14 @@ pub fn gtest(
168180
#(#attrs)*
169181
#outer_sig -> #outer_return_type {
170182
#maybe_closure
171-
use googletest::internal::test_outcome::TestOutcome;
172-
TestOutcome::init_current_test_outcome();
173-
let result: #invocation_result_type = #invocation;
174-
TestOutcome::close_current_test_outcome(#result)
183+
if googletest::internal::test_sharding::test_should_run(#test_case_hash) {
184+
use googletest::internal::test_outcome::TestOutcome;
185+
TestOutcome::init_current_test_outcome();
186+
let result: #invocation_result_type = #invocation;
187+
TestOutcome::close_current_test_outcome(#result)
188+
} else {
189+
Ok(())
190+
}
175191
#trailer
176192
}
177193
};
@@ -184,6 +200,7 @@ pub fn gtest(
184200
#function
185201
}
186202
};
203+
187204
output.into()
188205
}
189206

integration_tests/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
[package]
1616
name = "integration_tests"
17-
version = "0.13.0"
17+
version = "0.13.1"
1818
description = "Integration tests for GoogleTest Rust"
1919
repository = "https://github.com/google/googletest-rust"
2020
license = "Apache-2.0"
@@ -33,13 +33,19 @@ anyhow = "1"
3333
indoc = "2"
3434
rstest = "0.18"
3535
rustversion = "1.0.14"
36+
tempfile = "3.10.1"
3637
tokio = { version = "1.34", features = ["time", "macros", "rt"] }
3738

3839
[[bin]]
3940
name = "integration_tests"
4041
path = "src/integration_tests.rs"
4142
test = false
4243

44+
[[bin]]
45+
name = "always_fails"
46+
path = "src/always_fails.rs"
47+
test = false
48+
4349
[[bin]]
4450
name = "assert_predicate_with_failure"
4551
path = "src/assert_predicate_with_failure.rs"

integration_tests/src/always_fails.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
fn main() {}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use googletest::prelude::*;
20+
21+
#[gtest]
22+
fn always_fails() -> Result<()> {
23+
verify_that!(2, eq(3))
24+
}
25+
}

integration_tests/src/integration_tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,41 @@ mod tests {
743743
verify_that!(output, contains_regex("Did you annotate the test with gtest?"))
744744
}
745745

746+
#[gtest]
747+
fn always_fails_test_always_fails() -> Result<()> {
748+
let status = run_external_process("always_fails").status()?;
749+
750+
verify_that!(status.success(), eq(false))
751+
}
752+
753+
#[gtest]
754+
fn always_fails_test_respects_sharding() -> Result<()> {
755+
fn execute_test(shard: u64, total_shards: u64) -> Result<bool> {
756+
let status_file = tempfile::tempdir()?.path().join("shard_status_file");
757+
let gtest_total_shards = format!("{total_shards}");
758+
let gtest_shard_index = format!("{shard}");
759+
760+
let success = run_external_process("always_fails")
761+
.env("GTEST_TOTAL_SHARDS", gtest_total_shards)
762+
.env("GTEST_SHARD_INDEX", gtest_shard_index)
763+
.env("GTEST_SHARD_STATUS_FILE", &status_file)
764+
.status()?
765+
.success();
766+
767+
verify_that!(status_file.exists(), eq(true))?;
768+
Ok(success)
769+
}
770+
771+
// The test case should only run in one shard.
772+
let results = [execute_test(0, 3)?, execute_test(1, 3)?, execute_test(2, 3)?];
773+
let successes = results.iter().filter(|b| **b).count();
774+
let failures = results.iter().filter(|b| !**b).count();
775+
776+
expect_that!(successes, eq(2));
777+
expect_that!(failures, eq(1));
778+
Ok(())
779+
}
780+
746781
#[gtest]
747782
fn verify_true_when_true_returns_ok() {
748783
assert!(verify_true!("test" == "test").is_ok())

run_integration_tests.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ set -e
2424

2525
INTEGRATION_TEST_BINARIES=(
2626
"integration_tests"
27+
"always_fails"
2728
"assert_predicate_with_failure"
2829
"assertion_failure_in_subroutine"
2930
"assertion_failures_with_short_structured_actual_values"

0 commit comments

Comments
 (0)