build(rust): vendor googletest crate
This commit was merged in pull request #76.
This commit is contained in:
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(|_| ())
|
||||
}
|
||||
Reference in new issue
Block a user