use anyhow::Result;
use beet_utils::prelude::GlobFilter;
use clap::Parser;
use clap::ValueEnum;
use glob::Pattern;
use std::str::FromStr;
use test::ShouldPanic;
use test::TestDesc;
use test::TestDescAndFn;
#[allow(unused_imports)]
extern crate test;
#[derive(Debug, Default, Clone, Parser)]
pub struct TestRunnerConfig {
#[command(flatten)]
pub filter: GlobFilter,
#[arg(trailing_var_arg = true,value_parser = GlobFilter::parse_glob_pattern)]
pub also_include: Vec<Pattern>,
#[arg(long)]
pub ignored: bool,
#[arg(long)]
pub include_ignored: bool,
#[arg(long, default_value_t = true)]
pub nocapture: bool,
#[arg(long)]
pub exclude_should_panic: bool,
#[arg(short, long)]
pub watch: bool,
#[arg(short, long)]
pub snap: bool,
#[arg(short, long)]
pub quiet: bool,
#[clap(long, value_enum, default_value_t)]
pub format: OutputFormat,
#[arg(long)]
pub test_threads: Option<usize>,
#[arg(long)]
pub e2e: bool,
}
impl TestRunnerConfig {
fn parse_inner(mut args: Self) -> Self {
args.filter.include.extend(
std::mem::take(&mut args.also_include)
.into_iter()
.filter(|p| {
!p.as_str().starts_with("--")
&& !p.as_str().starts_with("-")
}),
);
args.filter.wrap_all_with_wildcard();
args
}
pub fn from_env_args() -> Self { Self::parse_inner(Self::parse()) }
pub fn from_raw_args(args: impl Iterator<Item = String>) -> Self {
Self::parse_inner(Self::parse_from(args))
}
pub fn should_not_run(&self, test: &TestDescAndFn) -> Option<&'static str> {
if let Some(ignore_message) =
self.should_not_run_ignore_flags(&test.desc)
{
Some(ignore_message)
} else if !self.passes_exclude_should_panic(&test.desc) {
Some("test should panic")
} else if !self.passes_filters(&test.desc) {
Some("test does not match filter")
} else {
None
}
}
fn should_not_run_ignore_flags(
&self,
desc: &TestDesc,
) -> Option<&'static str> {
if self.include_ignored {
None
} else if self.ignored && !desc.ignore {
Some("ignoring tests without #[ignore]")
} else if !self.include_ignored && desc.ignore {
if let Some(ignore_message) = desc.ignore_message {
Some(ignore_message)
} else {
Some("test is ignored")
}
} else {
None
}
}
fn passes_exclude_should_panic(&self, desc: &TestDesc) -> bool {
if !self.exclude_should_panic {
return true;
}
match desc.should_panic {
ShouldPanic::No => true,
ShouldPanic::Yes => false,
ShouldPanic::YesWithMessage(_) => false,
}
}
fn passes_filters(&self, desc: &TestDesc) -> bool {
let file = desc.source_file;
let name = desc.name.to_string();
(self.filter.passes_include(&file) || self.filter.passes_include(&name))
&& self.filter.passes_exclude(&file)
&& self.filter.passes_exclude(&name)
}
}
impl std::fmt::Display for TestRunnerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut messages = Vec::new();
if self.watch {
messages.push(format!("watch: true"));
}
if self.format != OutputFormat::File {
messages.push(format!("format: {}", self.format));
}
if !self.filter.is_empty() {
messages.push(self.filter.to_string());
}
if self.quiet {
messages.push(format!("quiet: true"));
}
if let Some(threads) = self.test_threads {
messages.push(format!("test threads: {threads}"));
}
write!(f, "{}\n", messages.join("\n"))
}
}
#[derive(Debug, Clone, Default, PartialEq, ValueEnum)]
pub enum OutputFormat {
#[default]
File,
Case,
Vanilla,
}
impl FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"file" => Ok(Self::File),
"case" => Ok(Self::Case),
"vanilla" => Ok(Self::Vanilla),
_ => Err(format!("unknown output format: {}", s)),
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputFormat::File => write!(f, "file"),
OutputFormat::Case => write!(f, "case"),
OutputFormat::Vanilla => write!(f, "vanilla"),
}
}
}