use std::fmt::{self, Display};
use std::iter::FromIterator;
use crate::frontend::RunCode;
use crate::hints::Hints;
use crate::runner::repeatedly::Regression;
use crate::runner::{self, Error, repeatedly::Counterexample};
use crate::stats::Stats;
use crate::{Limit, Seed};
#[derive(Debug, Clone)]
pub struct Formatting {
pub stats_max_value_count: Option<usize>,
pub stats_percent_precision: usize,
}
impl Default for Formatting {
fn default() -> Self {
Self {
stats_max_value_count: Some(20),
stats_percent_precision: 2,
}
}
}
struct DisplayFromFn<F>(F);
impl<F: Fn(&mut fmt::Formatter) -> fmt::Result> Display for DisplayFromFn<F> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(self.0)(f)
}
}
fn impl_display<'a>(fmt: impl Fn(&mut fmt::Formatter) -> fmt::Result + 'a) -> impl Display + 'a {
DisplayFromFn(fmt)
}
pub fn display_run_once_report<'a>(
run_code: &'a RunCode,
seed: Option<Seed>,
report: &'a runner::once::Report,
formatting: &'a Formatting,
) -> impl Display + 'a {
impl_display(move |f| {
let passed = report.error.is_none();
write_run_once_headline(f, passed)?;
write!(f, "\n\n")?;
write_run_once_section(f, run_code, seed, report)?;
if let Some(ref stats) = report.stats {
writeln!(f)?;
write_stats_section(f, stats, formatting)?;
}
Ok(())
})
}
pub fn display_run_repeatedly_report<'a>(
seed: Seed,
config: &'a runner::repeatedly::Config,
report: &'a runner::repeatedly::Report,
formatting: &'a Formatting,
) -> impl Display + 'a {
impl_display(move |f| {
let counterexample = &report.counterexample;
let passed = report.counterexample.is_none();
write_run_repeatedly_headline(f, passed, report.passes)?;
write!(f, "\n\n")?;
write_run_repeatedly_parameters_section(f, seed, config)?;
if let Some(ref stats) = report.stats {
writeln!(f)?;
write_stats_section(f, stats, formatting)?;
};
if let Some(counterexample) = counterexample {
let hints_enabled = cfg!(feature = "hints") && config.hints_enabled;
writeln!(f)?;
write_counterexample_section(f, hints_enabled, counterexample)?;
};
Ok(())
})
}
fn write_run_once_headline(f: &mut fmt::Formatter, passed: bool) -> fmt::Result {
let text = if passed {
"The test passed."
} else {
"The test failed."
};
write!(f, "{}", text)
}
fn write_run_once_section(
f: &mut fmt::Formatter,
run_code: &RunCode,
seed: Option<Seed>,
report: &runner::once::Report,
) -> fmt::Result {
write_section(
f,
"Run",
impl_display(|f| {
write_run_code_item(f, 0, run_code)?;
if let Some(seed) = seed {
write_seed_item(f, 0, seed)?;
};
write_limit_item(f, 0, run_code.limit)?;
if let Some(ref hints) = report.hints {
write_hints_item(f, 0, hints)?;
}
if let Some(ref error) = report.error {
write_error_item(f, 0, error)?;
};
Ok(())
}),
)
}
fn write_run_repeatedly_headline(f: &mut fmt::Formatter, passed: bool, passes: u64) -> fmt::Result {
let suffix = if passed {
"The test withstood"
} else {
"The test failed after"
};
write!(f, "{} {} passes.", suffix, passes)
}
fn write_run_repeatedly_parameters_section(
f: &mut fmt::Formatter,
seed: Seed,
config: &runner::repeatedly::Config,
) -> fmt::Result {
write_section(
f,
"Config",
impl_display(|f| {
write_regressions_item(f, 0, &config.regressions)?;
write_seed_item(f, 0, seed)?;
write_key_value_item(f, 0, "start limit", config.start_limit.0)?;
write_key_value_item(f, 0, "end limit", config.end_limit.0)?;
write_key_value_item(f, 0, "passes", config.passes)
}),
)
}
fn write_stats_section(
f: &mut fmt::Formatter,
stats: &Stats,
formatting: &Formatting,
) -> fmt::Result {
write_section(
f,
"Stats",
impl_display(|f| {
if stats.0.is_empty() {
write_item(f, 0, "No stats has been collected.")
} else {
for (key, stat) in &stats.0 {
let total = stat.total_counter().value().filter(|&n| n != 0);
let mut values = Vec::from_iter(stat.0.clone());
values.sort_by(|&(_, c1), &(_, c2)| c1.cmp(&c2).reverse());
let max_value_count = formatting.stats_max_value_count;
let omitted_value_count = match max_value_count {
None => 0,
Some(max_value_count) => {
let omitted_value_count = values.len().saturating_sub(max_value_count);
values.truncate(max_value_count);
omitted_value_count
}
};
write_key_item(f, 0, key)?;
for (value, counter) in values.into_iter() {
let count = counter.value();
let numerator = count.and_then(|count| count.checked_mul(100));
let percent = numerator.and_then(move |numerator| {
total.map(move |total| numerator as f64 / total as f64)
});
write_key_value_item(
f,
1,
impl_display(|f| {
match percent {
None => write!(f, "ovf")?,
Some(percent) => {
let percent_precision = formatting.stats_percent_precision;
write!(f, "{:.n$}", percent, n = percent_precision)?
}
};
write!(f, "% (")?;
write_some_or(f, count, "ovf")?;
write!(f, ")")
}),
value,
)?;
}
if omitted_value_count != 0 {
write_item(
f,
1,
impl_display(|f| {
write!(f, "{} values were omitted", omitted_value_count)
}),
)?;
}
}
Ok(())
}
}),
)
}
fn write_counterexample_section(
f: &mut fmt::Formatter,
hints_enabled: bool,
counterexample: &Counterexample,
) -> fmt::Result {
write_section(
f,
"Counterexample",
impl_display(|f| {
let run_code = RunCode {
prng: counterexample.prng.clone(),
limit: counterexample.limit,
};
write_run_code_item(f, 0, &run_code)?;
write_limit_item(f, 0, counterexample.limit)?;
match counterexample.hints {
None if !hints_enabled => (),
None => write_item(
f,
0,
"Hints could not be collected afterwards, test is not deterministic.",
)?,
Some(ref hints) => write_hints_item(f, 0, hints)?,
};
write_error_item(f, 0, &counterexample.error)
}),
)
}
fn write_section(
f: &mut fmt::Formatter,
title: &'static str,
content: impl Display,
) -> fmt::Result {
writeln!(f, "# {}", title)?;
write!(f, "{}", content)
}
fn write_run_code_item(f: &mut fmt::Formatter, indent: usize, run_code: &RunCode) -> fmt::Result {
write_key_value_item(
f,
indent,
"run code",
impl_display(|f| write!(f, "{run_code}")),
)
}
fn write_regressions_item(
f: &mut fmt::Formatter,
indent: usize,
regressions: &[Regression],
) -> fmt::Result {
if !regressions.is_empty() {
write_key_item(f, indent, "regressions")?;
for regression in regressions {
let run_code = RunCode {
prng: regression.prng.clone(),
limit: regression.limit,
};
write_item(f, indent + 1, impl_display(|f| write!(f, "{run_code}")))?;
}
}
Ok(())
}
fn write_seed_item(f: &mut fmt::Formatter, indent: usize, seed: Seed) -> fmt::Result {
write_key_value_item(f, indent, "seed", seed.0)
}
fn write_limit_item(f: &mut fmt::Formatter, indent: usize, limit: Limit) -> fmt::Result {
write_key_value_item(f, indent, "limit", limit.0)
}
fn write_hints_item(f: &mut fmt::Formatter, indent: usize, hints: &Hints) -> fmt::Result {
if hints.0.is_empty() {
write_item(f, indent, "No hints has been collected.")
} else {
write_key_item(f, indent, "hints")?;
let hint_ident_start = indent.saturating_add(1);
for hint in &hints.0 {
let hint_ident = hint_ident_start.saturating_add(hint.indent);
write_item(f, hint_ident, &hint.text)?;
}
Ok(())
}
}
fn write_error_item(f: &mut fmt::Formatter, indent: usize, error: &Error) -> fmt::Result {
let err = &error.0;
let string_repr = if let Some(string) = err.downcast_ref::<String>() {
Some(string.as_str())
} else if let Some(&str) = err.downcast_ref::<&str>() {
Some(str)
} else {
None
};
match string_repr {
None => write_item(
f,
indent,
"The error has an unknown type and cannot be displayed.",
),
Some(string_repr) => write_key_value_item(f, indent, "error", string_repr),
}
}
fn write_key_item(f: &mut fmt::Formatter, indent: usize, key: impl Display) -> fmt::Result {
write_item(f, indent, impl_display(|f| write!(f, "{}:", key)))
}
fn write_key_value_item(
f: &mut fmt::Formatter,
indent: usize,
key: impl Display,
value: impl Display,
) -> fmt::Result {
write_item(f, indent, impl_display(|f| write!(f, "{}: {}", key, value)))
}
fn write_item(f: &mut fmt::Formatter, indent: usize, content: impl Display) -> fmt::Result {
for _ in 0..indent {
write!(f, "\t")?;
}
writeln!(f, "- {}", content)
}
fn write_some_or(
f: &mut fmt::Formatter,
content: Option<impl Display>,
none: &'static str,
) -> fmt::Result {
match content {
None => write!(f, "{}", none),
Some(content) => write!(f, "{}", content),
}
}
#[cfg(test)]
mod tests {
use crate::frontend::RunCode;
use crate::hints::{Hint, Hints};
use crate::runner::repeatedly::Regression;
use crate::runner::{self, Error, repeatedly::Counterexample};
use crate::stats::{Counter, Stat, Stats};
use crate::{Limit, Prng};
use super::*;
fn contains_line(text: &str, expected_line: &str) -> bool {
text.lines().any(|line| line == expected_line)
}
fn example_hints() -> Hints {
Hints(vec![
Hint {
indent: 0,
text: "Uh".into(),
},
Hint {
indent: 1,
text: "Ah".into(),
},
Hint {
indent: 0,
text: "Ih".into(),
},
])
}
fn example_run_code(seed: u64) -> RunCode {
RunCode {
prng: Prng::from_seed(seed.into()),
limit: Limit(71),
}
}
fn example_error() -> Error {
Error(Box::new("Something bad happened!"))
}
fn example_run_repeatedly_config() -> runner::repeatedly::Config {
runner::repeatedly::Config {
regressions: Vec::new(),
start_limit: 0.into(),
end_limit: 100.into(),
passes: 200,
hints_enabled: true,
stats_enabled: false,
}
}
#[test]
fn display_run_once_report_passed_with_hints_example() {
let run_code = example_run_code(42);
let report = runner::once::Report {
hints: Some(example_hints()),
stats: None,
error: None,
};
let formatting = Formatting::default();
let expected = format!(
"\
The test passed.
# Run
- run code: {run_code}
- limit: 71
- hints:
\t- Uh
\t\t- Ah
\t- Ih
"
);
let actual = format!(
"{}",
display_run_once_report(&run_code, None, &report, &formatting,)
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_once_report_passed_with_seed_example() {
let run_code = example_run_code(42);
let report = runner::once::Report {
hints: None,
stats: None,
error: None,
};
let formatting = Formatting::default();
let expected = format!(
"\
The test passed.
# Run
- run code: {run_code}
- seed: 42
- limit: 71
"
);
let actual = format!(
"{}",
display_run_once_report(&run_code, Some(42.into()), &report, &formatting,)
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_once_report_passed_with_stats_example() {
let run_code = example_run_code(42);
let stats = Stats(
vec![(
"foo",
Stat(
vec![
("a".into(), Counter::Value(10)),
("b".into(), Counter::Value(20)),
]
.into_iter()
.collect(),
),
)]
.into_iter()
.collect(),
);
let report = runner::once::Report {
hints: None,
stats: Some(stats),
error: None,
};
let formatting = Formatting::default();
let expected = format!(
"\
The test passed.
# Run
- run code: {run_code}
- limit: 71
# Stats
- foo:
\t- 66.67% (20): b
\t- 33.33% (10): a
"
);
let actual = format!(
"{}",
display_run_once_report(&run_code, None, &report, &formatting,)
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_once_report_failed_example() {
let run_code = example_run_code(42);
let report = runner::once::Report {
hints: Some(example_hints()),
stats: None,
error: Some(example_error()),
};
let formatting = Formatting::default();
let expected = format!(
"\
The test failed.
# Run
- run code: {run_code}
- limit: 71
- hints:
\t- Uh
\t\t- Ah
\t- Ih
- error: Something bad happened!
"
);
let actual = format!(
"{}",
display_run_once_report(&run_code, None, &report, &formatting,)
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_repeatedly_report_passed_example() {
let seed = 42.into();
let config = example_run_repeatedly_config();
let report = runner::repeatedly::Report {
passes: 200,
stats: None,
counterexample: None,
};
let formatting = Formatting::default();
let expected = "\
The test withstood 200 passes.
# Config
- seed: 42
- start limit: 0
- end limit: 100
- passes: 200
";
let actual = format!(
"{}",
display_run_repeatedly_report(seed, &config, &report, &formatting,)
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_repeatedly_report_passed_with_regressions_example() {
let seed = 42.into();
let mut config = example_run_repeatedly_config();
let run_code_1 = example_run_code(123);
let run_code_2 = example_run_code(321);
config.regressions.push(Regression {
prng: run_code_1.prng.clone(),
limit: run_code_1.limit,
});
config.regressions.push(Regression {
prng: run_code_2.prng.clone(),
limit: run_code_2.limit,
});
let report = runner::repeatedly::Report {
passes: 200,
stats: None,
counterexample: None,
};
let formatting = Formatting::default();
let expected = format!(
"\
The test withstood 200 passes.
# Config
- regressions:
\t- {run_code_1}
\t- {run_code_2}
- seed: 42
- start limit: 0
- end limit: 100
- passes: 200
"
);
let actual = format!(
"{}",
display_run_repeatedly_report(seed, &config, &report, &formatting,)
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_repeatedly_report_failed_example() {
let seed = 42.into();
let config = example_run_repeatedly_config();
let run_code = example_run_code(42);
let report = runner::repeatedly::Report {
passes: 123,
stats: None,
counterexample: Some(Counterexample {
prng: run_code.prng.clone(),
limit: run_code.limit,
hints: Some(example_hints()),
error: example_error(),
}),
};
let formatting = Formatting::default();
let expected = format!(
"\
The test failed after 123 passes.
# Config
- seed: 42
- start limit: 0
- end limit: 100
- passes: 200
# Counterexample
- run code: {run_code}
- limit: 71
- hints:
\t- Uh
\t\t- Ah
\t- Ih
- error: Something bad happened!
"
);
let actual = format!(
"{}",
display_run_repeatedly_report(seed, &config, &report, &formatting,)
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_repeatedly_report_detects_missing_hints() {
if cfg!(feature = "hints") {
let seed = 42.into();
let config = example_run_repeatedly_config();
let report = runner::repeatedly::Report {
passes: 123,
stats: None,
counterexample: Some(Counterexample {
prng: Prng::from_seed(42.into()),
limit: Limit(71),
hints: None,
error: Error(Box::new("Something bad happened!")),
}),
};
let formatting = Formatting::default();
let actual = format!(
"{}",
display_run_repeatedly_report(seed, &config, &report, &formatting,)
);
assert!(contains_line(
&actual,
"- Hints could not be collected afterwards, test is not deterministic.",
));
}
}
#[test]
fn display_run_repeatedly_report_detects_empty_hints() {
if cfg!(feature = "hints") {
let seed = 42.into();
let config = runner::repeatedly::Config {
hints_enabled: true,
..example_run_repeatedly_config()
};
let report = runner::repeatedly::Report {
passes: 123,
stats: None,
counterexample: Some(Counterexample {
prng: Prng::from_seed(42.into()),
limit: Limit(71),
hints: Some(Hints::new()),
error: Error(Box::new("Something bad happened!")),
}),
};
let formatting = Formatting::default();
let actual = format!(
"{}",
display_run_repeatedly_report(seed, &config, &report, &formatting,)
);
assert!(contains_line(&actual, "- No hints has been collected.",));
}
}
#[test]
fn stats_section_example() {
let stats = Stats(
vec![
(
"foo",
Stat(
vec![
("a".into(), Counter::Value(10)),
("b".into(), Counter::Value(20)),
]
.into_iter()
.collect(),
),
),
(
"bar",
Stat(
vec![
("x".into(), Counter::Value(10)),
("y".into(), Counter::Overflow),
]
.into_iter()
.collect(),
),
),
(
"foobar",
Stat(vec![("i".into(), Counter::Value(0))].into_iter().collect()),
),
(
"foofoo",
Stat(
vec![
("x1".into(), Counter::Value(25)),
("x2".into(), Counter::Value(10)),
("x3".into(), Counter::Value(5)),
("x4".into(), Counter::Value(50)),
("x5".into(), Counter::Value(10)),
]
.into_iter()
.collect(),
),
),
]
.into_iter()
.collect(),
);
let formatting = Formatting {
stats_max_value_count: Some(3),
stats_percent_precision: 2,
};
let expected = "\
# Stats
- bar:
\t- ovf% (ovf): y
\t- ovf% (10): x
- foo:
\t- 66.67% (20): b
\t- 33.33% (10): a
- foobar:
\t- ovf% (0): i
- foofoo:
\t- 50.00% (50): x4
\t- 25.00% (25): x1
\t- 10.00% (10): x2
\t- 2 values were omitted
";
let actual = format!(
"{}",
impl_display(|f| write_stats_section(f, &stats, &formatting))
);
assert_eq!(expected, actual);
}
#[test]
fn display_run_repeatedly_report_detects_empty_stats() {
if cfg!(feature = "hints") {
let seed = 42.into();
let config = runner::repeatedly::Config {
stats_enabled: true,
..example_run_repeatedly_config()
};
let report = runner::repeatedly::Report {
passes: 123,
stats: Some(Stats::new()),
counterexample: None,
};
let formatting = Formatting::default();
let actual = format!(
"{}",
display_run_repeatedly_report(seed, &config, &report, &formatting,)
);
assert!(contains_line(&actual, "- No stats has been collected.",));
}
}
}