build(rust): vendor googletest crate

This commit was merged in pull request #76.
This commit is contained in:
alexion committed 2026-09-19 14:53:22 -04:00
1 parent a6fe84190e
commit 6887b104d6
576 files changed
+284290 -2

No files matched your search

@@ -0,0 +1,626 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
borrow::Cow,
fmt::{Result, Write},
};
/// Number of space used to indent lines when no alignement is required.
pub(crate) const INDENTATION_SIZE: usize = 2;
/// A list of [`Block`] possibly rendered with a [`Decoration`].
///
/// This is the top-level renderable component, corresponding to the description
/// or match explanation of a single matcher.
///
/// The constituent [`Block`] of a `List` can be decorated with either bullets
/// (`* `) or enumeration (`0. `, `1. `, ...). This is controlled via the
/// methods [`List::bullet_list`] and [`List::enumerate`]. By default, there is
/// no decoration.
///
/// A `List` can be constructed as follows:
///
/// * [`Default::default()`] constructs an empty `List`.
/// * [`Iterator::collect()`] on an [`Iterator`] of [`Block`].
/// * [`Iterator::collect()`] on an [`Iterator`] of `String`, which produces a
/// [`Block::Literal`] for each `String`.
/// * [`Iterator::collect()`] on an [`Iterator`] of `List`, which produces a
/// [`Block::Nested`] for each `List`.
#[derive(Debug, Default)]
pub(crate) struct List(Vec<Block>, Decoration);
impl List {
/// Render this instance using the formatter `f`.
///
/// Indent each line of output by `indentation` spaces.
pub(crate) fn render(&self, f: &mut dyn Write, indentation: usize) -> Result {
self.render_with_prefix(f, indentation, "".into())
}
/// Append a new [`Block`] containing `literal`.
///
/// The input `literal` is split into lines so that each line will be
/// indented correctly.
pub(crate) fn push_literal(&mut self, literal: Cow<'static, str>) {
self.0.push(literal.into());
}
/// Append a new [`Block`] containing `inner` as a nested [`List`].
pub(crate) fn push_nested(&mut self, inner: List) {
self.0.push(Block::Nested(inner));
}
/// Render each [`Block`] of this instance preceded with a bullet "* ".
pub(crate) fn bullet_list(self) -> Self {
Self(self.0, Decoration::Bullet)
}
/// Render each [`Block`] of this instance preceded with its 0-based index.
pub(crate) fn enumerate(self) -> Self {
Self(self.0, Decoration::Enumerate)
}
/// Return the number of [`Block`] in this instance.
pub(crate) fn len(&self) -> usize {
self.0.len()
}
/// Return `true` if there are no [`Block`] in this instance, `false`
/// otherwise.
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Append a new [`List`] in the last element which must be a
/// [`Block::Nested`]. Panic if `self` is empty or the last element is
/// not [`Block::Nested`].
pub(crate) fn push_at_end(&mut self, list: List) {
if let Some(Block::Nested(self_list)) = self.0.last_mut() {
self_list.push_nested(list);
} else {
panic!("pushing elements at the end of {self:#?} which last element is not Nested")
}
}
fn render_with_prefix(
&self,
f: &mut dyn Write,
indentation: usize,
prefix: Cow<'static, str>,
) -> Result {
if self.0.is_empty() {
return Ok(());
}
let enumeration_padding = self.enumeration_padding();
self.0[0].render(
f,
indentation,
self.full_prefix(0, enumeration_padding, &prefix).into(),
)?;
for (index, block) in self.0[1..].iter().enumerate() {
writeln!(f)?;
block.render(
f,
indentation + prefix.len(),
self.prefix(index + 1, enumeration_padding),
)?;
}
Ok(())
}
fn full_prefix(&self, index: usize, enumeration_padding: usize, prior_prefix: &str) -> String {
format!("{prior_prefix}{}", self.prefix(index, enumeration_padding))
}
fn prefix(&self, index: usize, enumeration_padding: usize) -> Cow<'static, str> {
match self.1 {
Decoration::None => "".into(),
Decoration::Bullet => "* ".into(),
Decoration::Enumerate => format!("{index:>enumeration_padding$}. ").into(),
}
}
fn enumeration_padding(&self) -> usize {
match self.1 {
Decoration::None => 0,
Decoration::Bullet => 0,
Decoration::Enumerate => {
if self.0.len() > 1 {
((self.0.len() - 1) as f64).log10().floor() as usize + 1
} else {
// Avoid negative logarithm when there is only 0 or 1 element.
1
}
}
}
}
}
impl FromIterator<Block> for List {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = Block>,
{
Self(iter.into_iter().collect(), Decoration::None)
}
}
impl<ElementT: Into<Cow<'static, str>>> FromIterator<ElementT> for List {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = ElementT>,
{
Self(iter.into_iter().map(|b| b.into().into()).collect(), Decoration::None)
}
}
impl FromIterator<List> for List {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = List>,
{
Self(iter.into_iter().map(Block::nested).collect(), Decoration::None)
}
}
/// A sequence of [`Fragment`] or a nested [`List`].
///
/// This may be rendered with a prefix specified by the [`Decoration`] of the
/// containing [`List`]. In this case, all lines are indented to align with the
/// first character of the first line of the block.
#[derive(Debug)]
enum Block {
/// A block of text.
///
/// Each constituent [`Fragment`] contains one line of text. The lines are
/// indented uniformly to the current indentation of this block when
/// rendered.
Literal(Vec<Fragment>),
/// A nested [`List`].
///
/// The [`List`] is rendered recursively at the next level of indentation.
Nested(List),
}
impl Block {
fn nested(inner: List) -> Self {
Self::Nested(inner)
}
fn render(&self, f: &mut dyn Write, indentation: usize, prefix: Cow<'static, str>) -> Result {
match self {
Self::Literal(fragments) => {
if fragments.is_empty() {
return Ok(());
}
write!(f, "{:indentation$}{prefix}", "")?;
fragments[0].render(f)?;
let block_indentation = indentation + prefix.as_ref().len();
for fragment in &fragments[1..] {
writeln!(f)?;
write!(f, "{:block_indentation$}", "")?;
fragment.render(f)?;
}
Ok(())
}
Self::Nested(inner) => inner.render_with_prefix(
f,
indentation + INDENTATION_SIZE.saturating_sub(prefix.len()),
prefix,
),
}
}
}
impl From<String> for Block {
fn from(value: String) -> Self {
Block::Literal(value.lines().map(|v| Fragment(v.to_string().into())).collect())
}
}
impl From<&'static str> for Block {
fn from(value: &'static str) -> Self {
Block::Literal(value.lines().map(|v| Fragment(v.into())).collect())
}
}
impl From<Cow<'static, str>> for Block {
fn from(value: Cow<'static, str>) -> Self {
match value {
Cow::Borrowed(value) => value.into(),
Cow::Owned(value) => value.into(),
}
}
}
/// A string representing one line of a description or match explanation.
#[derive(Debug)]
struct Fragment(Cow<'static, str>);
impl Fragment {
fn render(&self, f: &mut dyn Write) -> Result {
write!(f, "{}", self.0)
}
}
/// The decoration which appears on [`Block`] of a [`List`] when rendered.
#[derive(Debug, Default)]
enum Decoration {
/// No decoration on each [`Block`]. The default.
#[default]
None,
/// Each [`Block`] is preceded by a bullet (`* `).
Bullet,
/// Each [`Block`] is preceded by its index in the [`List`] (`0. `, `1. `,
/// ...).
Enumerate,
}
#[cfg(test)]
mod tests {
use super::{Block, Fragment, List};
use crate::prelude::*;
use crate::Result;
use indoc::indoc;
#[test]
fn renders_fragment() -> Result<()> {
let fragment = Fragment("A fragment".into());
let mut result = String::new();
fragment.render(&mut result)?;
verify_that!(result, eq("A fragment"))
}
#[test]
fn renders_empty_block() -> Result<()> {
let block = Block::Literal(vec![]);
let mut result = String::new();
block.render(&mut result, 0, "".into())?;
verify_that!(result, eq(""))
}
#[test]
fn renders_block_with_one_fragment() -> Result<()> {
let block: Block = "A fragment".into();
let mut result = String::new();
block.render(&mut result, 0, "".into())?;
verify_that!(result, eq("A fragment"))
}
#[test]
fn renders_block_with_two_fragments() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 0, "".into())?;
verify_that!(result, eq("A fragment\nAnother fragment"))
}
#[test]
fn renders_indented_block() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 2, "".into())?;
verify_that!(result, eq(" A fragment\n Another fragment"))
}
#[test]
fn renders_block_with_prefix() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 0, "* ".into())?;
verify_that!(result, eq("* A fragment\n Another fragment"))
}
#[test]
fn renders_indented_block_with_prefix() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 2, "* ".into())?;
verify_that!(result, eq(" * A fragment\n Another fragment"))
}
#[test]
fn renders_empty_list() -> Result<()> {
let list = list(vec![]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(""))
}
#[test]
fn renders_plain_list_with_one_block() -> Result<()> {
let list = list(vec!["A fragment".into()]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment"))
}
#[test]
fn renders_plain_list_with_two_blocks() -> Result<()> {
let list = list(vec!["A fragment".into(), "A fragment in a second block".into()]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment\nA fragment in a second block"))
}
#[test]
fn renders_plain_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec!["A fragment\nA second fragment".into()]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment\nA second fragment"))
}
#[test]
fn renders_nested_plain_list_with_one_block() -> Result<()> {
let list = list(vec![Block::nested(list(vec!["A fragment".into()]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment"))
}
#[test]
fn renders_nested_plain_list_with_two_blocks() -> Result<()> {
let list = list(vec![Block::nested(list(vec![
"A fragment".into(),
"A fragment in a second block".into(),
]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment\n A fragment in a second block"))
}
#[test]
fn renders_nested_plain_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec![Block::nested(list(vec!["A fragment\nA second fragment".into()]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment\n A second fragment"))
}
#[test]
fn renders_bulleted_list_with_one_block() -> Result<()> {
let list = list(vec!["A fragment".into()]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* A fragment"))
}
#[test]
fn renders_bulleted_list_with_two_blocks() -> Result<()> {
let list =
list(vec!["A fragment".into(), "A fragment in a second block".into()]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* A fragment\n* A fragment in a second block"))
}
#[test]
fn renders_bulleted_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec!["A fragment\nA second fragment".into()]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* A fragment\n A second fragment"))
}
#[test]
fn renders_nested_bulleted_list_with_one_block() -> Result<()> {
let list = list(vec![Block::nested(list(vec!["A fragment".into()]).bullet_list())]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" * A fragment"))
}
#[test]
fn renders_nested_bulleted_list_with_two_blocks() -> Result<()> {
let list = list(vec![Block::nested(
list(vec!["A fragment".into(), "A fragment in a second block".into()]).bullet_list(),
)]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" * A fragment\n * A fragment in a second block"))
}
#[test]
fn renders_nested_bulleted_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec![Block::nested(
list(vec!["A fragment\nA second fragment".into()]).bullet_list(),
)]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" * A fragment\n A second fragment"))
}
#[test]
fn renders_enumerated_list_with_one_block() -> Result<()> {
let list = list(vec!["A fragment".into()]).enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("0. A fragment"))
}
#[test]
fn renders_enumerated_list_with_two_blocks() -> Result<()> {
let list =
list(vec!["A fragment".into(), "A fragment in a second block".into()]).enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("0. A fragment\n1. A fragment in a second block"))
}
#[test]
fn renders_enumerated_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec!["A fragment\nA second fragment".into()]).enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("0. A fragment\n A second fragment"))
}
#[test]
fn aligns_renders_enumerated_list_with_more_than_ten_blocks() -> Result<()> {
let list =
(0..11).map(|i| Block::from(format!("Fragment {i}"))).collect::<List>().enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(
result,
eq(indoc! {"
0. Fragment 0
1. Fragment 1
2. Fragment 2
3. Fragment 3
4. Fragment 4
5. Fragment 5
6. Fragment 6
7. Fragment 7
8. Fragment 8
9. Fragment 9
10. Fragment 10"})
)
}
#[test]
fn renders_fragment_plus_nested_plain_list_with_one_block() -> Result<()> {
let list =
list(vec!["A fragment".into(), Block::nested(list(vec!["Another fragment".into()]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment\n Another fragment"))
}
#[test]
fn renders_double_nested_plain_list_with_one_block() -> Result<()> {
let list =
list(vec![Block::nested(list(vec![Block::nested(list(vec!["A fragment".into()]))]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment"))
}
#[test]
fn renders_headers_plus_double_nested_plain_list() -> Result<()> {
let list = list(vec![
"First header".into(),
Block::nested(list(vec![
"Second header".into(),
Block::nested(list(vec!["A fragment".into()])),
])),
]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("First header\n Second header\n A fragment"))
}
#[test]
fn renders_double_nested_bulleted_list() -> Result<()> {
let list =
list(vec![Block::nested(list(vec!["A fragment".into()]).bullet_list())]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* * A fragment"))
}
#[test]
fn renders_nested_enumeration_with_two_blocks_inside_bulleted_list() -> Result<()> {
let list =
list(vec![Block::nested(list(vec!["Block 1".into(), "Block 2".into()]).enumerate())])
.bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* 0. Block 1\n 1. Block 2"))
}
#[test]
fn renders_nested_enumeration_with_block_with_two_fragments_inside_bulleted_list() -> Result<()>
{
let list = list(vec![Block::nested(
list(vec!["A fragment\nAnother fragment".into()]).enumerate(),
)])
.bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* 0. A fragment\n Another fragment"))
}
fn list(blocks: Vec<Block>) -> List {
List(blocks, super::Decoration::None)
}
}
@@ -0,0 +1,312 @@
//! Pattern matches using glob wildcards `*` and `?` with the
//! linear-time algorithm <https://research.swtch.com/glob>.
use std::iter::Peekable;
use std::str::Chars;
pub struct Pattern(String);
impl Pattern {
/// Creates a new pattern matcher. Each pattern consists of
/// regular characters, single-character wildcards `'?'`, and
/// multi-character wildcards `'*'`.
pub fn new(pattern: String) -> Self {
Self(pattern)
}
/// Returns true if and only if the wildcard pattern matches the
/// string.
pub fn matches(&self, string: &str) -> bool {
let processor = Processor {
pattern: self.0.chars().peekable(),
string: string.chars().peekable(),
restart: None,
};
processor.process()
}
}
type PeekableChars<'a> = Peekable<Chars<'a>>;
/// Represents the state we need to restart search from a star wildcard (`*`).
struct Restart<'a> {
pattern_next: PeekableChars<'a>,
string_next: PeekableChars<'a>,
}
/// The runtime state for glob matching.
struct Processor<'a> {
pattern: PeekableChars<'a>,
string: PeekableChars<'a>,
restart: Option<Restart<'a>>,
}
/// Represents what to do after any step through the processor.
enum StepOutcome {
Proceed,
TryRestart,
}
impl Processor<'_> {
/// Runs the pattern matching until we find an unrecoverable
/// mismatch, or the input is consumed.
fn process(mut self) -> bool {
while self.is_unfinished() {
let outcome = self.step();
if let StepOutcome::TryRestart = outcome {
let restarted = self.try_restart();
if !restarted {
return false;
}
}
}
true
}
fn is_unfinished(&mut self) -> bool {
self.pattern.peek().is_some() || self.string.peek().is_some()
}
/// Takes a single step forward, and returns whether to proceed or try to
/// restart.
fn step(&mut self) -> StepOutcome {
match self.pattern.peek() {
Some('?') => self.step_question_wildcard(),
Some('*') => self.step_star_wildcard(),
Some(pattern_ch) => {
let pattern_ch = *pattern_ch;
self.step_ordinary_character(pattern_ch)
}
None => StepOutcome::TryRestart,
}
}
/// Match any single character.
fn step_question_wildcard(&mut self) -> StepOutcome {
if self.string.peek().is_some() {
_ = self.pattern.next();
_ = self.string.next();
StepOutcome::Proceed
} else {
StepOutcome::TryRestart
}
}
/// Match zero or more characters. Start by skipping over the
/// wildcard and matching zero characters from string. If that
/// fails, restart and match one more character than the last
/// attempt.
fn step_star_wildcard(&mut self) -> StepOutcome {
self.restart = if self.string.peek().is_none() {
// Subtle: if the string is already exhausted, we mark
// that we can't restart.
None
} else {
let pattern_next = self.pattern.clone();
let mut string_next = self.string.clone();
string_next.next();
Some(Restart { pattern_next, string_next })
};
_ = self.pattern.next();
StepOutcome::Proceed
}
/// Match an ordinary (non-wildcard) character.
fn step_ordinary_character(&mut self, pattern_ch: char) -> StepOutcome {
if self.string.peek() == Some(&pattern_ch) {
_ = self.pattern.next();
_ = self.string.next();
StepOutcome::Proceed
} else {
StepOutcome::TryRestart
}
}
/// Try to restart from failing to match a character. If true, the
/// matching can restart.
fn try_restart(&mut self) -> bool {
if let Some(Restart { pattern_next, string_next }) = &self.restart {
self.pattern = pattern_next.clone();
self.string = string_next.clone();
true
} else {
false
}
}
}
/// Returns true if `s` contains glob wildcards.
pub fn is_glob_pattern(s: &str) -> bool {
s.contains(['?', '*'])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn simple_character_match() -> Result<()> {
verify_that!(Pattern::new("a".into()).matches("a"), is_true())
}
#[test]
fn simple_character_mismatch() -> Result<()> {
verify_that!(Pattern::new("b".into()).matches("a"), is_false())
}
#[test]
fn simple_word_mismatch() -> Result<()> {
verify_that!(Pattern::new("judgement".into()).matches("judgment"), is_false())
}
#[test]
fn question_match() -> Result<()> {
verify_that!(Pattern::new("?".into()).matches("a"), is_true())
}
#[test]
fn simple_word_question_match() -> Result<()> {
let pattern = Pattern::new("judg?ment".into());
verify_that!(pattern.matches("judgment"), is_false())?;
verify_that!(pattern.matches("judgement"), is_true())?;
verify_that!(pattern.matches("judge ment"), is_false())?;
Ok(())
}
#[test]
fn question_mismatch() -> Result<()> {
let pattern = Pattern::new("?".into());
verify_that!(pattern.matches(""), is_false())?;
verify_that!(pattern.matches("aa"), is_false())?;
Ok(())
}
#[test]
fn glob_on_empty() -> Result<()> {
verify_that!(Pattern::new("*".into()).matches(""), is_true())?;
verify_that!(Pattern::new("**".into()).matches(""), is_true())?;
Ok(())
}
#[test]
fn glob_prefix() -> Result<()> {
let pattern = Pattern::new("*a".into());
verify_that!(pattern.matches(""), is_false())?;
verify_that!(pattern.matches("a"), is_true())?;
verify_that!(pattern.matches("ba"), is_true())?;
verify_that!(pattern.matches("bba"), is_true())?;
verify_that!(pattern.matches("bbab"), is_false())?;
Ok(())
}
#[test]
fn glob_within() -> Result<()> {
let pattern = Pattern::new("b*a".into());
verify_that!(pattern.matches(""), is_false())?;
verify_that!(pattern.matches("b"), is_false())?;
verify_that!(pattern.matches("bb"), is_false())?;
verify_that!(pattern.matches("ba"), is_true())?;
verify_that!(pattern.matches("bbbba"), is_true())?;
verify_that!(pattern.matches("baa"), is_true())?;
Ok(())
}
#[test]
fn glob_suffix() -> Result<()> {
let pattern = Pattern::new("ba*".into());
verify_that!(pattern.matches(""), is_false())?;
verify_that!(pattern.matches("b"), is_false())?;
verify_that!(pattern.matches("bb"), is_false())?;
verify_that!(pattern.matches("ba"), is_true())?;
verify_that!(pattern.matches("baa"), is_true())?;
verify_that!(pattern.matches("bab"), is_true())?;
verify_that!(pattern.matches("bba"), is_false())?;
verify_that!(pattern.matches("bbbba"), is_false())?;
Ok(())
}
#[test]
fn redundant_stars() -> Result<()> {
let pattern = Pattern::new("**a".into());
verify_that!(pattern.matches(""), is_false())?;
verify_that!(pattern.matches("a"), is_true())?;
verify_that!(pattern.matches("ba"), is_true())?;
verify_that!(pattern.matches("bba"), is_true())?;
verify_that!(pattern.matches("bbab"), is_false())?;
verify_that!(pattern.matches("bbaba"), is_true())?;
Ok(())
}
#[test]
fn star_question_star_case() -> Result<()> {
let pattern = Pattern::new("*?*".into());
verify_that!(pattern.matches(""), is_false())?;
verify_that!(pattern.matches("a"), is_true())?;
verify_that!(pattern.matches("aa"), is_true())?;
verify_that!(pattern.matches("aaa"), is_true())?;
Ok(())
}
#[test]
fn another_case_finding_two_separated_a() -> Result<()> {
let pattern = Pattern::new("*a?a*".into());
verify_that!(pattern.matches(""), is_false())?;
verify_that!(pattern.matches("a"), is_false())?;
verify_that!(pattern.matches("aa"), is_false())?;
verify_that!(pattern.matches("aaa"), is_true())?;
verify_that!(pattern.matches("aba"), is_true())?;
verify_that!(pattern.matches("baba"), is_true())?;
verify_that!(pattern.matches("abab"), is_true())?;
verify_that!(pattern.matches("babab"), is_true())?;
Ok(())
}
#[test]
fn banana() -> Result<()> {
let pattern = Pattern::new("b?n???".into());
verify_that!(pattern.matches("banana"), is_true())?;
verify_that!(pattern.matches("binary"), is_true())?;
verify_that!(pattern.matches("bundle"), is_true())?;
verify_that!(pattern.matches("bindir"), is_true())?;
verify_that!(pattern.matches("bananas"), is_false())?;
verify_that!(pattern.matches("bucket"), is_false())?;
verify_that!(pattern.matches("budget"), is_false())?;
verify_that!(pattern.matches("bazzar"), is_false())?;
verify_that!(pattern.matches("burger"), is_false())?;
Ok(())
}
#[test]
fn glob_word() -> Result<()> {
let word = Pattern::new("*word*".into());
verify_that!(word.matches("bird"), is_false())?;
verify_that!(word.matches("This is a wordy sentence"), is_true())?;
verify_that!(word.matches("word soup"), is_true())?;
verify_that!(word.matches("bird is the word"), is_true())?;
verify_that!(word.matches("word"), is_true())?;
Ok(())
}
#[test]
fn degenerate_glob() -> Result<()> {
verify_that!(
Pattern::new("************************************************.*".into())
.matches("this is a test.com"),
is_true()
)
}
#[test]
fn degenerate_glob_2_mismatch() -> Result<()> {
// The first example from https://research.swtch.com/glob, with N=1000.
const N: usize = 1000;
let long_pattern = ["a*"; N].into_iter().collect::<String>() + "b";
let long_string = ["a"; N].into_iter().collect::<String>();
verify_that!(Pattern::new(long_pattern).matches(&long_string), is_false())
}
}
@@ -0,0 +1,23 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![doc(hidden)]
pub(crate) mod description_renderer;
pub mod glob;
pub mod scoped_trace;
pub mod test_data;
pub mod test_filter;
pub mod test_outcome;
pub mod test_sharding;
@@ -0,0 +1,126 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cell::RefCell;
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT_TRACE_ID: AtomicUsize = AtomicUsize::new(0);
/// Information about a scoped trace.
#[derive(Clone, Debug)]
pub struct TraceInfo {
pub id: usize,
pub file: &'static str,
pub line: u32,
pub message: String,
}
thread_local! {
static TRACE_STACK: RefCell<Vec<TraceInfo>> = const { RefCell::new(Vec::new()) };
}
/// RAII guard to manage the push and pop of trace information.
///
/// This struct is `!Send` and `!Sync` to prevent it from being held across
/// `.await` points in async tests, which would cause incorrect trace tracking
/// if the task moves between threads.
#[doc(hidden)]
pub struct ScopedTraceGuard {
id: usize,
_phantom: std::marker::PhantomData<*mut ()>,
}
impl ScopedTraceGuard {
#[doc(hidden)]
#[track_caller]
pub fn new(message: String) -> Self {
let caller = std::panic::Location::caller();
let id = NEXT_TRACE_ID.fetch_add(1, Ordering::Relaxed);
TRACE_STACK.with(|stack| {
// Use try_borrow_mut to avoid double panic if called during unwinding.
if let Ok(mut s) = stack.try_borrow_mut() {
s.push(TraceInfo { id, file: caller.file(), line: caller.line(), message });
}
});
Self { id, _phantom: std::marker::PhantomData }
}
}
impl Drop for ScopedTraceGuard {
fn drop(&mut self) {
TRACE_STACK.with(|stack| {
// Use try_borrow_mut to avoid double panic if called during unwinding.
if let Ok(mut s) = stack.try_borrow_mut() {
if let Some(pos) = s.iter().rposition(|t| t.id == self.id) {
s.remove(pos);
}
}
});
}
}
/// Retrieves a clone of the current thread's trace stack.
pub fn get_scoped_traces() -> Vec<TraceInfo> {
TRACE_STACK.with(|stack| stack.try_borrow().map(|s| s.clone()).unwrap_or_default())
}
// Test-only state and helpers, hidden from production API.
#[cfg(test)]
pub(crate) mod test_helpers {
use super::*;
use std::cell::Cell;
thread_local! {
pub static CAPTURED_TRACES_IN_HOOK: RefCell<Vec<TraceInfo>> = const { RefCell::new(Vec::new()) };
pub static USE_CAPTURE_HOOK: Cell<bool> = const { Cell::new(false) };
}
pub fn enable_capture_in_hook(enable: bool) {
USE_CAPTURE_HOOK.with(|v| v.set(enable));
}
pub fn get_captured_traces_in_hook() -> Vec<TraceInfo> {
CAPTURED_TRACES_IN_HOOK.with(|v| v.borrow().clone())
}
pub fn clear_captured_traces_in_hook() {
CAPTURED_TRACES_IN_HOOK.with(|v| v.borrow_mut().clear());
}
}
#[cfg(test)]
mod tests {
use super::test_helpers::*;
use super::*;
#[test]
fn test_scoped_trace_fatal() {
enable_capture_in_hook(true);
clear_captured_traces_in_hook();
// Ensure hook is installed
crate::internal::test_outcome::TestOutcome::init_current_test_outcome();
let _ = std::panic::catch_unwind(|| {
let _guard = ScopedTraceGuard::new("Second trace".to_string());
panic!("Intentional panic");
});
let captured = get_captured_traces_in_hook();
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].message, "Second trace");
enable_capture_in_hook(false);
}
}
@@ -0,0 +1,29 @@
//! This is for testing only to provide fully-qualified struct paths to macros.
#[doc(hidden)]
#[derive(Debug)]
pub struct TestStruct {
#[doc(hidden)]
pub value: u32,
}
impl TestStruct {
#[doc(hidden)]
pub fn get_value(&self) -> u32 {
self.value
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct GenericTestStruct<T> {
#[doc(hidden)]
pub value: T,
}
impl<T> GenericTestStruct<T> {
#[doc(hidden)]
pub fn get_value<U>(&self, a: U) -> U {
a
}
}
@@ -0,0 +1,261 @@
//! Implements the googletest test filtering protocol.
//!
//! The Google test filtering protocol consists of the following
//! environment variable:
//!
//! * TESTBRIDGE_TEST_ONLY: string passed from --test_filter
//!
//! The format of a filter is a ‘,‘-separated list of wildcard
//! patterns (called the positive patterns) optionally followed by a
//! ‘-’ and another ‘,‘-separated pattern list (called the negative
//! patterns). A test matches the filter if and only if it matches any
//! of the positive patterns but does not match any of the negative
//! patterns. (Note that this is a deliberate devation from GTest
//! C++, which uses colons to separate the patterns, as colons will
//! unfortunately clash with Rust's "::" namespacing operator.)
//!
//! As an example: "*mount*-*doom*" will accept any string that contains the
//! substring "mount", as long as it also doesn't contain "doom"
use crate::internal::glob::{is_glob_pattern, Pattern};
use std::sync::OnceLock;
static TEST_FILTER: OnceLock<Box<dyn TestFilter + Send + Sync>> = OnceLock::new();
pub fn test_should_run(test_name: &str) -> bool {
let test_filter = TEST_FILTER.get_or_init(|| {
if let Ok(testbridge_test_only) = std::env::var("TESTBRIDGE_TEST_ONLY") {
Box::new(get_test_filter(&testbridge_test_only))
} else {
Box::new(AcceptAll)
}
});
test_filter.filter(test_name)
}
trait TestFilter {
/// Returns true if the test should run.
fn filter(&self, test_name: &str) -> bool;
}
struct AcceptAll;
impl TestFilter for AcceptAll {
fn filter(&self, _test_name: &str) -> bool {
true
}
}
struct Equals(String);
impl TestFilter for Equals {
fn filter(&self, test_name: &str) -> bool {
test_name == self.0
}
}
struct Matches(Pattern);
impl TestFilter for Matches {
fn filter(&self, test_name: &str) -> bool {
self.0.matches(test_name)
}
}
struct Collection {
// The positive portion:
positive_equals: Box<[Equals]>,
positive_matches: Box<[Matches]>,
// The negative portion:
negative_equals: Box<[Equals]>,
negative_matches: Box<[Matches]>,
}
impl TestFilter for Collection {
fn filter(&self, test_name: &str) -> bool {
(self.positive_equals.iter().any(|f| f.filter(test_name))
|| self.positive_matches.iter().any(|f| f.filter(test_name)))
&& (!self.negative_equals.iter().any(|f| f.filter(test_name)))
&& (!self.negative_matches.iter().any(|f| f.filter(test_name)))
}
}
fn get_test_filter(testbridge_test_only: &str) -> Collection {
let positive_negative: Vec<&str> = testbridge_test_only.splitn(2, '-').collect();
let (positive_with_globs, positive_literals): (Vec<_>, Vec<_>) = {
let positive = positive_negative[0];
if positive.is_empty() {
// Forces the empty positive filter to accept everything:
(vec!["*"], vec![])
} else {
positive.split(',').partition(|s| is_glob_pattern(s))
}
};
let (negative_with_globs, negative_literals): (Vec<_>, Vec<_>) = match positive_negative.get(1)
{
Some(negative) if !negative.is_empty() => {
negative.split(',').partition(|s| is_glob_pattern(s))
}
_ => (vec![], vec![]),
};
Collection {
positive_equals: positive_literals.into_iter().map(|s| Equals(s.to_string())).collect(),
positive_matches: positive_with_globs
.into_iter()
.map(|s| Matches(Pattern::new(s.to_string())))
.collect(),
negative_equals: negative_literals.into_iter().map(|s| Equals(s.to_string())).collect(),
negative_matches: negative_with_globs
.into_iter()
.map(|s| Matches(Pattern::new(s.to_string())))
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn accept_all_accepts_all() -> Result<()> {
let filter = AcceptAll;
verify_that!(filter.filter(""), is_true())?;
verify_that!(filter.filter("abcdefg"), is_true())?;
Ok(())
}
#[test]
fn empty_filter_accepts_all() -> Result<()> {
let filter = get_test_filter("");
verify_that!(filter.filter(""), is_true())?;
verify_that!(filter.filter("abcdefg"), is_true())?;
Ok(())
}
#[test]
fn empty_negation_filter_accepts_all() -> Result<()> {
let filter = get_test_filter("-");
verify_that!(filter.filter(""), is_true())?;
verify_that!(filter.filter("abcdefg"), is_true())?;
Ok(())
}
#[test]
fn simple_literal_filter() -> Result<()> {
let filter = get_test_filter("*magic*");
verify_that!(filter.filter("this_is_magic"), is_true())?;
verify_that!(filter.filter(""), is_false())?;
verify_that!(filter.filter("magic"), is_true())?;
verify_that!(filter.filter("science"), is_false())?;
Ok(())
}
#[test]
fn star_globs() -> Result<()> {
let filter = get_test_filter("a*b");
verify_that!(filter.filter(""), is_false())?;
verify_that!(filter.filter("a"), is_false())?;
verify_that!(filter.filter("ab"), is_true())?;
verify_that!(filter.filter("a b"), is_true())?;
verify_that!(filter.filter("b"), is_false())?;
verify_that!(filter.filter("b a"), is_false())?;
Ok(())
}
#[test]
fn question_globs() -> Result<()> {
let filter = get_test_filter("a?b");
verify_that!(filter.filter(""), is_false())?;
verify_that!(filter.filter("a"), is_false())?;
verify_that!(filter.filter("ab"), is_false())?;
verify_that!(filter.filter("aXb"), is_true())?;
verify_that!(filter.filter("a b"), is_true())?;
verify_that!(filter.filter("b"), is_false())?;
verify_that!(filter.filter("b a"), is_false())?;
Ok(())
}
#[test]
fn collection() -> Result<()> {
let filter = get_test_filter("a,b");
verify_that!(filter.filter(""), is_false())?;
verify_that!(filter.filter("a"), is_true())?;
verify_that!(filter.filter("ab"), is_false())?;
verify_that!(filter.filter("a b"), is_false())?;
verify_that!(filter.filter("b"), is_true())?;
verify_that!(filter.filter("b a"), is_false())?;
verify_that!(filter.filter("c"), is_false())?;
Ok(())
}
#[test]
fn collection_with_globs() -> Result<()> {
let filter = get_test_filter("*test1*,*test2*");
verify_that!(filter.filter(""), is_false())?;
verify_that!(filter.filter("this is test1"), is_true())?;
verify_that!(filter.filter("and test2 is it"), is_true())?;
verify_that!(filter.filter("but test3 is not"), is_false())?;
Ok(())
}
#[test]
fn collection_with_globs_negation() -> Result<()> {
let filter = get_test_filter("*test*-*testbad");
verify_that!(filter.filter(""), is_false())?;
verify_that!(filter.filter("module"), is_false())?;
verify_that!(filter.filter("module::my_test1"), is_true())?;
verify_that!(filter.filter("module::my_test2"), is_true())?;
verify_that!(filter.filter("module::my_testbad"), is_false())?;
Ok(())
}
#[test]
fn mount_doom() -> Result<()> {
let filter = get_test_filter("*mount*-*doom*");
verify_that!(filter.filter(""), is_false())?;
verify_that!(filter.filter("mount rushmore"), is_true())?;
verify_that!(filter.filter("doom mount"), is_false())?;
verify_that!(filter.filter("dismount"), is_true())?;
verify_that!(filter.filter("mountains of moria"), is_true())?;
verify_that!(filter.filter("frodo and sam went to mount doom"), is_false())?;
Ok(())
}
#[test]
fn collection_with_only_negation() -> Result<()> {
let filter = get_test_filter("-testbad1,testbad2");
verify_that!(filter.filter(""), is_true())?;
verify_that!(filter.filter("test"), is_true())?;
verify_that!(filter.filter("testbad1"), is_false())?;
verify_that!(filter.filter("testbad2"), is_false())?;
verify_that!(filter.filter("testbad3"), is_true())?;
Ok(())
}
#[test]
fn magic_words_a_and_e() -> Result<()> {
let filter = get_test_filter("a*,e*-abracadabra,elbereth,avada kedavra");
verify_that!(filter.filter("alakazam"), is_true())?;
verify_that!(filter.filter("abracadabra"), is_false())?;
verify_that!(filter.filter("avada kedavra"), is_false())?;
verify_that!(filter.filter("enchantment"), is_true())?;
verify_that!(filter.filter("elbereth"), is_false())?;
verify_that!(filter.filter("expecto patronum"), is_true())?;
verify_that!(filter.filter("fuego"), is_false())?;
Ok(())
}
}
@@ -0,0 +1,325 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cell::{RefCell, RefMut};
use std::fmt::{Debug, Display, Error, Formatter};
use std::sync::OnceLock;
use std::thread_local;
/// The outcome hitherto of running a test.
///
/// This is kept as a running record as the test progresses. One can access it
/// with `TestOutcome::with_current_test_outcome`.
///
/// **For internal use only. API stablility is not guaranteed!**
#[doc(hidden)]
pub enum TestOutcome {
/// The test ran or is currently running and no assertions have failed.
Success,
/// The test ran or is currently running and at least one assertion has
/// failed.
Failure,
}
thread_local! {
static CURRENT_TEST_OUTCOME: RefCell<Option<TestOutcome>> = const { RefCell::new(None) };
}
impl TestOutcome {
/// Resets the current test's [`TestOutcome`].
///
/// This is intended only for use by the attribute macro `#[gtest]`.
///
/// **For internal use only. API stablility is not guaranteed!**
#[doc(hidden)]
pub fn init_current_test_outcome() {
static INSTALL_HOOK: OnceLock<()> = OnceLock::new();
INSTALL_HOOK.get_or_init(|| {
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let traces = crate::internal::scoped_trace::get_scoped_traces();
if !traces.is_empty() {
#[cfg(test)]
{
use crate::internal::scoped_trace::test_helpers::*;
let use_capture = USE_CAPTURE_HOOK.with(|v| v.get());
if use_capture {
let _ = CAPTURED_TRACES_IN_HOOK
.with(|v| v.try_borrow_mut().map(|mut b| *b = traces.clone()));
prev_hook(info);
return;
}
}
eprintln!("Google Test trace:");
for trace in traces.iter().rev() {
eprintln!(" {}:{}: {}", trace.file, trace.line, trace.message);
}
}
prev_hook(info);
}));
});
Self::with_current_test_outcome(|mut current_test_outcome| {
*current_test_outcome = Some(TestOutcome::Success);
})
}
/// Evaluates the current test's [`TestOutcome`], producing a suitable
/// `Result`.
///
/// The parameter `result` is the value returned by the test function
/// itself. This returns `Result::Err` with a `Display`-formatted string of
/// the error if `result` is `Result::Err`.
///
/// Otherwise, this returns `Result::Err` precisely when a test failure has
/// been recorded with
/// [`and_log_failure`](crate::GoogleTestSupport::and_log_failure).
///
/// **For internal use only. API stablility is not guaranteed!**
#[doc(hidden)]
pub fn close_current_test_outcome<E: Display>(
inner_result: Result<(), E>,
) -> Result<(), TestFailure> {
TestOutcome::with_current_test_outcome(|mut outcome| {
let outer_result = match &*outcome {
Some(TestOutcome::Success) => match inner_result {
Ok(()) => Ok(()),
Err(_) => Err(TestFailure),
},
Some(TestOutcome::Failure) => Err(TestFailure),
None => {
panic!("No test context found. This indicates a bug in GoogleTest.")
}
};
if let Err(fatal_assertion_failure) = inner_result {
println!("{fatal_assertion_failure}");
}
*outcome = None;
outer_result
})
}
/// Returns a `Result` corresponding to the outcome of the currently running
/// test.
#[track_caller]
pub(crate) fn get_current_test_outcome() -> Result<(), TestAssertionFailure> {
TestOutcome::with_current_test_outcome(|mut outcome| {
let outcome = outcome
.as_mut()
.expect("No test context found. This indicates a bug in GoogleTest.");
match outcome {
TestOutcome::Success => Ok(()),
TestOutcome::Failure => Err(TestAssertionFailure::create("Test failed".into())),
}
})
}
/// Records that the currently running test has failed.
fn fail_current_test() {
TestOutcome::with_current_test_outcome(|mut outcome| {
let outcome = outcome
.as_mut()
.expect("No test context found. This indicates a bug in GoogleTest.");
*outcome = TestOutcome::Failure;
})
}
/// Runs `action` with the [`TestOutcome`] for the currently running test.
///
/// This is primarily intended for use by assertion macros like
/// `expect_that!`.
fn with_current_test_outcome<T>(action: impl FnOnce(RefMut<Option<TestOutcome>>) -> T) -> T {
CURRENT_TEST_OUTCOME.with(|current_test_outcome| action(current_test_outcome.borrow_mut()))
}
/// Ensure that there is a test context present and panic if there is not.
pub(crate) fn ensure_test_context_present() {
if FAILURE_REPORTER_HOOK.get().is_some() {
// Bypassed because an external runner hook is registered.
return;
}
TestOutcome::with_current_test_outcome(|outcome| {
outcome.as_ref().expect(
"
No test context found.
* Did you annotate the test with gtest?
* Is the assertion running in the original test thread?
",
);
})
}
}
/// A marking struct indicating that a test has failed.
///
/// This exists to implement the [Error][std::error::Error] trait. It displays
/// to a message indicating that the actual test assertion failure messages are
/// in the text above.
pub struct TestFailure;
impl std::error::Error for TestFailure {}
impl std::fmt::Debug for TestFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
writeln!(f, "See failure output above")?;
Ok(())
}
}
impl std::fmt::Display for TestFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
writeln!(f, "See failure output above")?;
Ok(())
}
}
/// A report that a single test assertion failed.
///
/// **For internal use only. API stablility is not guaranteed!**
///
/// See pub type Result in googletest/src/lib.rs for the public alias.
#[doc(hidden)]
#[derive(Clone)]
pub struct TestAssertionFailure {
/// A human-readable formatted string describing the error.
pub description: String,
pub custom_message: Option<String>,
pub traces: Vec<crate::internal::scoped_trace::TraceInfo>,
location: Location,
}
/// A code location.
///
/// `std::panic::Location` does not provide a constructor, hence we cannot
/// construct a fake value.
///
/// **For internal use only. API stablility is not guaranteed!**
#[doc(hidden)]
#[derive(Clone)]
enum Location {
Real(&'static std::panic::Location<'static>),
Fake { file: &'static str, line: u32, column: u32 },
}
impl Display for Location {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Location::Real(l) => write!(f, "{l}"),
Location::Fake { file, line, column } => write!(f, "{file}:{line}:{column}"),
}
}
}
/// A hook to capture non fatal test failures. This is a global static, and it
/// is initialized exactly once.
#[doc(hidden)]
static FAILURE_REPORTER_HOOK: OnceLock<fn(&TestAssertionFailure)> = OnceLock::new();
/// Initializes the failure reporter hook.
///
/// This is a global static, and it is initialized at most once and returns true
/// if the initialization was successful.
/// If the hook is already set, this will do nothing and return false.
#[doc(hidden)]
pub fn set_failure_reporter_hook_if_not_set(capture_fn: fn(&TestAssertionFailure)) -> bool {
FAILURE_REPORTER_HOOK.set(capture_fn).is_ok()
}
impl TestAssertionFailure {
/// Creates a new instance with the given `description`.
///
/// **For internal use only. API stablility is not guaranteed!**
#[track_caller]
pub fn create(description: String) -> Self {
Self {
description,
custom_message: None,
traces: crate::internal::scoped_trace::get_scoped_traces(),
location: Location::Real(std::panic::Location::caller()),
}
}
/// Set `location`` to a fake value.
///
/// **For internal use only. API stablility is not guaranteed!**
pub fn with_fake_location(mut self, file: &'static str, line: u32, column: u32) -> Self {
self.location = Location::Fake { file, line, column };
self
}
pub(crate) fn log(&self) {
println!("{self}");
if let Some(capture_fn) = FAILURE_REPORTER_HOOK.get() {
capture_fn(self);
return;
}
TestOutcome::fail_current_test();
}
/// Returns the file name of the location.
pub fn file(&self) -> &'static str {
match self.location {
Location::Real(l) => l.file(),
Location::Fake { file, .. } => file,
}
}
/// Returns the line number of the location.
pub fn line(&self) -> u32 {
match self.location {
Location::Real(l) => l.line(),
Location::Fake { line, .. } => line,
}
}
}
impl Display for TestAssertionFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
writeln!(f, "{}", self.description)?;
if let Some(custom_message) = &self.custom_message {
writeln!(f, "{custom_message}")?;
}
if !self.traces.is_empty() {
writeln!(f, "Google Test trace:")?;
for trace in self.traces.iter().rev() {
writeln!(f, " {}:{}: {}", trace.file, trace.line, trace.message)?;
}
}
writeln!(f, " at {}", self.location)
}
}
// The standard Rust test harness outputs the TestAssertionFailure with the
// Debug trait. We want the output to be formatted, so we use a custom Debug
// implementation which defers to Display.
impl Debug for TestAssertionFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
Display::fmt(self, f)
}
}
impl<T: std::error::Error> From<T> for TestAssertionFailure {
#[track_caller]
fn from(value: T) -> Self {
TestAssertionFailure::create(format!("{value}"))
}
}
#[cfg(feature = "proptest")]
impl From<TestAssertionFailure> for proptest::test_runner::TestCaseError {
fn from(value: TestAssertionFailure) -> Self {
proptest::test_runner::TestCaseError::Fail(format!("{value}").into())
}
}
@@ -0,0 +1,107 @@
/// This module implements the googletest test sharding protocol. The Google
/// sharding protocol consists of the following environment variables:
///
/// * GTEST_TOTAL_SHARDS: total number of shards.
/// * GTEST_SHARD_INDEX: number of this shard
/// * GTEST_SHARD_STATUS_FILE: touch this file to indicate support for sharding.
///
/// See also <https://google.github.io/googletest/advanced.html>
use std::cell::OnceCell;
use std::env::{var, var_os};
use std::ffi::{OsStr, OsString};
use std::fs::{self, File};
use std::num::NonZeroU64;
use std::path::{Path, PathBuf};
/// Environment variable specifying the total number of test shards.
const TEST_TOTAL_SHARDS: &[&str] = &["GTEST_TOTAL_SHARDS", "TEST_TOTAL_SHARDS"];
/// Environment variable specifyign the index of this test shard.
const TEST_SHARD_INDEX: &[&str] = &["GTEST_SHARD_INDEX", "TEST_SHARD_INDEX"];
/// Environment variable specifying the name of the file we create (or cause a
/// timestamp change on) to indicate that we support the sharding protocol.
const TEST_SHARD_STATUS_FILE: &[&str] = &["GTEST_SHARD_STATUS_FILE", "TEST_SHARD_STATUS_FILE"];
thread_local! {
static SHARDING: OnceCell<Sharding> = const { OnceCell::new() };
}
struct Sharding {
this_shard: u64,
total_shards: NonZeroU64,
}
impl Default for Sharding {
fn default() -> Self {
Self { this_shard: 0, total_shards: NonZeroU64::MIN }
}
}
pub fn test_should_run(test_case_hash: u64) -> bool {
SHARDING.with(|sharding_cell| {
sharding_cell.get_or_init(Sharding::from_environment).test_should_run(test_case_hash)
})
}
fn get_var(keys: &[&str]) -> Option<String> {
for key in keys {
if let Ok(value) = var(OsStr::new(key)) {
return Some(value);
}
}
None
}
fn get_var_os(keys: &[&str]) -> Option<OsString> {
for key in keys {
if let Some(value) = var_os(OsStr::new(key)) {
return Some(value);
}
}
None
}
impl Sharding {
fn test_should_run(&self, test_case_hash: u64) -> bool {
(test_case_hash % self.total_shards.get()) == self.this_shard
}
fn from_environment() -> Sharding {
let this_shard: Option<u64> =
{ get_var(TEST_SHARD_INDEX).and_then(|value| value.parse().ok()) };
let total_shards: Option<NonZeroU64> = {
get_var(TEST_TOTAL_SHARDS)
.and_then(|value| value.parse().ok())
.and_then(NonZeroU64::new)
};
match (this_shard, total_shards) {
(Some(this_shard), Some(total_shards)) if this_shard < total_shards.get() => {
if let Some(name) = get_var_os(TEST_SHARD_STATUS_FILE) {
let pathbuf = PathBuf::from(name);
if let Err(e) = create_status_file(&pathbuf) {
eprintln!(
"failed to create $GTEST_SHARD_STATUS_FILE file {}: {}",
pathbuf.display(),
e
);
}
}
Sharding { this_shard, total_shards }
}
_ => Sharding::default(),
}
}
}
fn create_status_file(path: &Path) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
File::create(path).map(|_| ())
}