use super::{
PruneKind, PrunePlan, PruneResult, RecordedRunInfo, RecordedRunStatus,
SnapshotWithReplayability,
run_id_index::RunIdIndex,
store::{CompletedRunStats, ReplayabilityStatus, StressCompletedRunStats},
tree::{RunInfo, RunTree, TreeIterItem},
};
use crate::{
helpers::{ThemeCharacters, plural},
redact::{Redactor, SizeDisplay},
};
use camino::Utf8Path;
use chrono::{DateTime, Utc};
use owo_colors::{OwoColorize, Style};
use quick_junit::ReportUuid;
use std::{collections::HashMap, error::Error, fmt};
use swrite::{SWrite, swrite};
#[derive(Clone, Debug, Default)]
pub struct Styles {
pub run_id_prefix: Style,
pub run_id_rest: Style,
pub timestamp: Style,
pub duration: Style,
pub size: Style,
pub count: Style,
pub passed: Style,
pub failed: Style,
pub cancelled: Style,
pub label: Style,
pub section: Style,
}
impl Styles {
pub fn colorize(&mut self) {
self.run_id_prefix = Style::new().bold().purple();
self.run_id_rest = Style::new().bright_black();
self.timestamp = Style::new();
self.duration = Style::new();
self.size = Style::new();
self.count = Style::new().bold();
self.passed = Style::new().bold().green();
self.failed = Style::new().bold().red();
self.cancelled = Style::new().bold().yellow();
self.label = Style::new().bold();
self.section = Style::new().bold();
}
pub fn format_run_id(&self, run_id: ReportUuid, run_id_index: Option<&RunIdIndex>) -> String {
let run_id_str = run_id.to_string();
if let Some(index) = run_id_index
&& let Some(prefix_info) = index.shortest_unique_prefix(run_id)
{
let prefix_len = prefix_info.prefix.len().min(run_id_str.len());
let (prefix_part, rest_part) = run_id_str.split_at(prefix_len);
format!(
"{}{}",
prefix_part.style(self.run_id_prefix),
rest_part.style(self.run_id_rest),
)
} else {
run_id_str.style(self.label).to_string()
}
}
pub fn format_run_id_short(
&self,
run_id: ReportUuid,
run_id_index: Option<&RunIdIndex>,
) -> String {
let full_short: String = run_id.to_string().chars().take(8).collect();
if let Some(index) = run_id_index
&& let Some(prefix_info) = index.shortest_unique_prefix(run_id)
{
let prefix_len = prefix_info.prefix.len().min(8);
let (prefix_part, rest_part) = full_short.split_at(prefix_len);
format!(
"{}{}",
prefix_part.style(self.run_id_prefix),
rest_part.style(self.run_id_rest),
)
} else {
full_short.style(self.label).to_string()
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct RunListAlignment {
pub passed_width: usize,
pub size_width: usize,
pub tree_prefix_width: usize,
}
impl RunListAlignment {
const MIN_SIZE_WIDTH: usize = 9;
pub fn from_runs(runs: &[RecordedRunInfo]) -> Self {
let passed_width = runs
.iter()
.map(|run| run.status.passed_count_width())
.max()
.unwrap_or(1);
let size_width = runs
.iter()
.map(|run| SizeDisplay(run.sizes.total_compressed()).display_width())
.max()
.unwrap_or(Self::MIN_SIZE_WIDTH)
.max(Self::MIN_SIZE_WIDTH);
Self {
passed_width,
size_width,
tree_prefix_width: 0,
}
}
pub fn from_runs_with_total(runs: &[RecordedRunInfo], total_size_bytes: u64) -> Self {
let passed_width = runs
.iter()
.map(|run| run.status.passed_count_width())
.max()
.unwrap_or(1);
let max_run_size_width = runs
.iter()
.map(|run| SizeDisplay(run.sizes.total_compressed()).display_width())
.max()
.unwrap_or(0);
let total_size_width = SizeDisplay(total_size_bytes).display_width();
let size_width = max_run_size_width
.max(total_size_width)
.max(Self::MIN_SIZE_WIDTH);
Self {
passed_width,
size_width,
tree_prefix_width: 0,
}
}
pub(super) fn with_tree(mut self, tree: &RunTree) -> Self {
self.tree_prefix_width = tree
.iter()
.map(|item| item.tree_prefix_width())
.max()
.unwrap_or(0);
self
}
}
#[derive(Clone, Debug)]
pub struct DisplayPruneResult<'a> {
pub(super) result: &'a PruneResult,
pub(super) styles: &'a Styles,
}
impl fmt::Display for DisplayPruneResult<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let result = self.result;
if result.deleted_count == 0 && result.orphans_deleted == 0 {
if result.errors.is_empty() {
writeln!(f, "no runs to prune")?;
} else {
writeln!(
f,
"no runs pruned ({} {} occurred)",
result.errors.len().style(self.styles.count),
plural::errors_str(result.errors.len()),
)?;
}
} else {
let orphan_suffix = if result.orphans_deleted > 0 {
format!(
", {} {}",
result.orphans_deleted.style(self.styles.count),
plural::orphans_str(result.orphans_deleted)
)
} else {
String::new()
};
let error_suffix = if result.errors.is_empty() {
String::new()
} else {
format!(
" ({} {} occurred)",
result.errors.len().style(self.styles.count),
plural::errors_str(result.errors.len()),
)
};
writeln!(
f,
"pruned {} {}{}, freed {}{}",
result.deleted_count.style(self.styles.count),
plural::runs_str(result.deleted_count),
orphan_suffix,
SizeDisplay(result.freed_bytes),
error_suffix,
)?;
}
if result.kind == PruneKind::Explicit && !result.errors.is_empty() {
writeln!(f)?;
writeln!(f, "errors:")?;
for error in &result.errors {
write!(f, " - {error}")?;
let mut curr = error.source();
while let Some(source) = curr {
write!(f, ": {source}")?;
curr = source.source();
}
writeln!(f)?;
}
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct DisplayPrunePlan<'a> {
pub(super) plan: &'a PrunePlan,
pub(super) run_id_index: &'a RunIdIndex,
pub(super) styles: &'a Styles,
pub(super) redactor: &'a Redactor,
}
impl fmt::Display for DisplayPrunePlan<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let plan = self.plan;
if plan.runs().is_empty() {
writeln!(f, "no runs would be pruned")
} else {
writeln!(
f,
"would prune {} {}, freeing {}:\n",
plan.runs().len().style(self.styles.count),
plural::runs_str(plan.runs().len()),
self.redactor.redact_size(plan.total_bytes())
)?;
let alignment = RunListAlignment::from_runs(plan.runs());
let replayable = ReplayabilityStatus::Replayable;
for run in plan.runs() {
writeln!(
f,
"{}",
run.display(
self.run_id_index,
&replayable,
alignment,
self.styles,
self.redactor
)
)?;
}
Ok(())
}
}
}
#[derive(Clone, Debug)]
pub struct DisplayRecordedRunInfo<'a> {
run: &'a RecordedRunInfo,
run_id_index: &'a RunIdIndex,
replayability: &'a ReplayabilityStatus,
alignment: RunListAlignment,
styles: &'a Styles,
redactor: &'a Redactor,
prefix: &'a str,
run_id_padding: usize,
}
impl fmt::Display for DisplayRecordedRunInfo<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let run = self.run;
let run_id_display = self
.styles
.format_run_id_short(run.run_id, Some(self.run_id_index));
let status_display = self.format_status();
let timestamp_display = self.redactor.redact_timestamp(&run.started_at);
let duration_display = self.redactor.redact_store_duration(run.duration_secs);
let size_display = self.redactor.redact_size(run.sizes.total_compressed());
write!(
f,
"{}{}{:padding$} {} {} {:>width$} {}",
self.prefix,
run_id_display,
"",
timestamp_display.style(self.styles.timestamp),
duration_display.style(self.styles.duration),
size_display.style(self.styles.size),
status_display,
padding = self.run_id_padding,
width = self.alignment.size_width,
)?;
match self.replayability {
ReplayabilityStatus::Replayable => {}
ReplayabilityStatus::NotReplayable(_) => {
write!(f, " ({})", "not replayable".style(self.styles.failed))?;
}
ReplayabilityStatus::Incomplete => {
}
}
Ok(())
}
}
impl<'a> DisplayRecordedRunInfo<'a> {
pub(super) fn new(
run: &'a RecordedRunInfo,
run_id_index: &'a RunIdIndex,
replayability: &'a ReplayabilityStatus,
alignment: RunListAlignment,
styles: &'a Styles,
redactor: &'a Redactor,
) -> Self {
Self {
run,
run_id_index,
replayability,
alignment,
styles,
redactor,
prefix: " ",
run_id_padding: 0,
}
}
pub(super) fn with_tree_formatting(mut self, prefix: &'a str, run_id_padding: usize) -> Self {
self.prefix = prefix;
self.run_id_padding = run_id_padding;
self
}
fn format_status(&self) -> String {
match &self.run.status {
RecordedRunStatus::Incomplete => {
format!(
"{:>width$} {}",
"",
"incomplete".style(self.styles.cancelled),
width = self.alignment.passed_width,
)
}
RecordedRunStatus::Unknown => {
format!(
"{:>width$} {}",
"",
"unknown".style(self.styles.cancelled),
width = self.alignment.passed_width,
)
}
RecordedRunStatus::Completed(stats) => self.format_normal_stats(stats, false),
RecordedRunStatus::Cancelled(stats) => self.format_normal_stats(stats, true),
RecordedRunStatus::StressCompleted(stats) => self.format_stress_stats(stats, false),
RecordedRunStatus::StressCancelled(stats) => self.format_stress_stats(stats, true),
}
}
fn format_normal_stats(&self, stats: &CompletedRunStats, cancelled: bool) -> String {
if stats.initial_run_count == 0 {
return format!(
"{:>width$} {}",
0.style(self.styles.count),
"passed".style(self.styles.cancelled),
width = self.alignment.passed_width,
);
}
let mut result = String::new();
swrite!(
result,
"{:>width$} {}",
stats.passed.style(self.styles.count),
"passed".style(self.styles.passed),
width = self.alignment.passed_width,
);
if stats.failed > 0 {
swrite!(
result,
" / {} {}",
stats.failed.style(self.styles.count),
"failed".style(self.styles.failed),
);
}
let not_run = stats
.initial_run_count
.saturating_sub(stats.passed)
.saturating_sub(stats.failed);
if not_run > 0 {
swrite!(
result,
" / {} {}",
not_run.style(self.styles.count),
"not run".style(self.styles.cancelled),
);
}
if cancelled {
swrite!(result, " {}", "(cancelled)".style(self.styles.cancelled));
}
result
}
fn format_stress_stats(&self, stats: &StressCompletedRunStats, cancelled: bool) -> String {
let mut result = String::new();
swrite!(
result,
"{:>width$} {} {}",
stats.success_count.style(self.styles.count),
"passed".style(self.styles.passed),
plural::iterations_str(stats.success_count),
width = self.alignment.passed_width,
);
if stats.failed_count > 0 {
swrite!(
result,
" / {} {}",
stats.failed_count.style(self.styles.count),
"failed".style(self.styles.failed),
);
}
if let Some(initial) = stats.initial_iteration_count {
let not_run = initial
.get()
.saturating_sub(stats.success_count)
.saturating_sub(stats.failed_count);
if not_run > 0 {
swrite!(
result,
" / {} {}",
not_run.style(self.styles.count),
"not run".style(self.styles.cancelled),
);
}
}
if cancelled {
swrite!(result, " {}", "(cancelled)".style(self.styles.cancelled));
}
result
}
}
pub struct DisplayRecordedRunInfoDetailed<'a> {
run: &'a RecordedRunInfo,
run_id_index: &'a RunIdIndex,
replayability: &'a ReplayabilityStatus,
now: DateTime<Utc>,
styles: &'a Styles,
theme_characters: &'a ThemeCharacters,
redactor: &'a Redactor,
}
impl<'a> DisplayRecordedRunInfoDetailed<'a> {
pub(super) fn new(
run: &'a RecordedRunInfo,
run_id_index: &'a RunIdIndex,
replayability: &'a ReplayabilityStatus,
now: DateTime<Utc>,
styles: &'a Styles,
theme_characters: &'a ThemeCharacters,
redactor: &'a Redactor,
) -> Self {
Self {
run,
run_id_index,
replayability,
now,
styles,
theme_characters,
redactor,
}
}
fn format_run_id(&self) -> String {
self.format_run_id_with_prefix(self.run.run_id)
}
fn format_run_id_with_prefix(&self, run_id: ReportUuid) -> String {
self.styles.format_run_id(run_id, Some(self.run_id_index))
}
fn write_field(
&self,
f: &mut fmt::Formatter<'_>,
label: &str,
value: impl fmt::Display,
) -> fmt::Result {
writeln!(
f,
" {:18}{}",
format!("{}:", label).style(self.styles.label),
value,
)
}
fn write_status_field(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status_str = self.run.status.short_status_str();
let exit_code = self.run.status.exit_code();
match exit_code {
Some(code) => {
let exit_code_style = if code == 0 {
self.styles.passed
} else {
self.styles.failed
};
self.write_field(
f,
"status",
format!("{} (exit code {})", status_str, code.style(exit_code_style)),
)
}
None => self.write_field(f, "status", status_str),
}
}
fn write_replayable(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.replayability {
ReplayabilityStatus::Replayable => {
self.write_field(f, "replayable", "yes".style(self.styles.passed))
}
ReplayabilityStatus::NotReplayable(reasons) => {
let mut reasons_str = String::new();
for reason in reasons {
if !reasons_str.is_empty() {
swrite!(reasons_str, ", {reason}");
} else {
swrite!(reasons_str, "{reason}");
}
}
self.write_field(
f,
"replayable",
format!("{}: {}", "no".style(self.styles.failed), reasons_str),
)
}
ReplayabilityStatus::Incomplete => self.write_field(
f,
"replayable",
format!(
"{}: run is incomplete (archive may be partial)",
"maybe".style(self.styles.cancelled)
),
),
}
}
fn write_stats_section(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.run.status {
RecordedRunStatus::Incomplete | RecordedRunStatus::Unknown => {
Ok(())
}
RecordedRunStatus::Completed(stats) | RecordedRunStatus::Cancelled(stats) => {
writeln!(f, " {}:", "tests".style(self.styles.section))?;
writeln!(
f,
" {:16}{}",
"passed:".style(self.styles.label),
stats.passed.style(self.styles.passed),
)?;
if stats.failed > 0 {
writeln!(
f,
" {:16}{}",
"failed:".style(self.styles.label),
stats.failed.style(self.styles.failed),
)?;
}
let not_run = stats
.initial_run_count
.saturating_sub(stats.passed)
.saturating_sub(stats.failed);
if not_run > 0 {
writeln!(
f,
" {:16}{}",
"not run:".style(self.styles.label),
not_run.style(self.styles.cancelled),
)?;
}
writeln!(f)
}
RecordedRunStatus::StressCompleted(stats)
| RecordedRunStatus::StressCancelled(stats) => {
writeln!(f, " {}:", "iterations".style(self.styles.section))?;
writeln!(
f,
" {:16}{}",
"passed:".style(self.styles.label),
stats.success_count.style(self.styles.passed),
)?;
if stats.failed_count > 0 {
writeln!(
f,
" {:16}{}",
"failed:".style(self.styles.label),
stats.failed_count.style(self.styles.failed),
)?;
}
if let Some(initial) = stats.initial_iteration_count {
let not_run = initial
.get()
.saturating_sub(stats.success_count)
.saturating_sub(stats.failed_count);
if not_run > 0 {
writeln!(
f,
" {:16}{}",
"not run:".style(self.styles.label),
not_run.style(self.styles.cancelled),
)?;
}
}
writeln!(f)
}
}
}
fn write_sizes_section(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
" {:18}{:>10} {:>12} {:>7}",
"sizes:".style(self.styles.section),
"compressed".style(self.styles.label),
"uncompressed".style(self.styles.label),
"entries".style(self.styles.label),
)?;
let sizes = &self.run.sizes;
writeln!(
f,
" {:16}{:>10} {:>12} {:>7}",
"log".style(self.styles.label),
self.redactor
.redact_size(sizes.log.compressed)
.style(self.styles.size),
self.redactor
.redact_size(sizes.log.uncompressed)
.style(self.styles.size),
sizes.log.entries.style(self.styles.size),
)?;
writeln!(
f,
" {:16}{:>10} {:>12} {:>7}",
"store".style(self.styles.label),
self.redactor
.redact_size(sizes.store.compressed)
.style(self.styles.size),
self.redactor
.redact_size(sizes.store.uncompressed)
.style(self.styles.size),
sizes.store.entries.style(self.styles.size),
)?;
writeln!(
f,
" {:16}{} {} {}",
"",
self.theme_characters.hbar(10),
self.theme_characters.hbar(12),
self.theme_characters.hbar(7),
)?;
writeln!(
f,
" {:16}{:>10} {:>12} {:>7}",
"total".style(self.styles.section),
self.redactor
.redact_size(sizes.total_compressed())
.style(self.styles.size),
self.redactor
.redact_size(sizes.total_uncompressed())
.style(self.styles.size),
sizes.total_entries().style(self.styles.size),
)
}
fn format_env_vars(&self) -> String {
self.redactor.redact_env_vars(&self.run.env_vars)
}
fn format_cli_args(&self) -> String {
self.redactor.redact_cli_args(&self.run.cli_args)
}
}
impl fmt::Display for DisplayRecordedRunInfoDetailed<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let run = self.run;
writeln!(
f,
"{} {}",
"Run".style(self.styles.section),
self.format_run_id()
)?;
writeln!(f)?;
self.write_field(
f,
"nextest version",
self.redactor.redact_version(&run.nextest_version),
)?;
if let Some(parent_run_id) = run.parent_run_id {
self.write_field(
f,
"parent run",
self.format_run_id_with_prefix(parent_run_id),
)?;
}
if !run.cli_args.is_empty() {
self.write_field(f, "command", self.format_cli_args())?;
}
if !run.env_vars.is_empty() {
self.write_field(f, "env", self.format_env_vars())?;
}
self.write_status_field(f)?;
let timestamp = self.redactor.redact_detailed_timestamp(&run.started_at);
let relative_duration = self
.now
.signed_duration_since(run.started_at.with_timezone(&Utc))
.to_std()
.unwrap_or(std::time::Duration::ZERO);
let relative_display = self.redactor.redact_relative_duration(relative_duration);
self.write_field(
f,
"started at",
format!(
"{} ({} ago)",
timestamp,
relative_display.style(self.styles.count)
),
)?;
let last_written_timestamp = self
.redactor
.redact_detailed_timestamp(&run.last_written_at);
let last_written_relative_duration = self
.now
.signed_duration_since(run.last_written_at.with_timezone(&Utc))
.to_std()
.unwrap_or(std::time::Duration::ZERO);
let last_written_relative_display = self
.redactor
.redact_relative_duration(last_written_relative_duration);
self.write_field(
f,
"last written at",
format!(
"{} ({} ago)",
last_written_timestamp,
last_written_relative_display.style(self.styles.count)
),
)?;
self.write_field(
f,
"duration",
self.redactor.redact_detailed_duration(run.duration_secs),
)?;
self.write_replayable(f)?;
writeln!(f)?;
self.write_stats_section(f)?;
self.write_sizes_section(f)?;
Ok(())
}
}
pub struct DisplayRunList<'a> {
snapshot_with_replayability: &'a SnapshotWithReplayability<'a>,
store_path: Option<&'a Utf8Path>,
styles: &'a Styles,
theme_characters: &'a ThemeCharacters,
redactor: &'a Redactor,
}
impl<'a> DisplayRunList<'a> {
pub fn new(
snapshot_with_replayability: &'a SnapshotWithReplayability<'a>,
store_path: Option<&'a Utf8Path>,
styles: &'a Styles,
theme_characters: &'a ThemeCharacters,
redactor: &'a Redactor,
) -> Self {
Self {
snapshot_with_replayability,
store_path,
styles,
theme_characters,
redactor,
}
}
}
impl fmt::Display for DisplayRunList<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let snapshot = self.snapshot_with_replayability.snapshot();
if let Some(path) = self.store_path {
writeln!(f, "{}: {}\n", "store".style(self.styles.count), path)?;
}
if snapshot.run_count() == 0 {
return Ok(());
}
writeln!(
f,
"{} recorded {}:\n",
snapshot.run_count().style(self.styles.count),
plural::runs_str(snapshot.run_count()),
)?;
let tree = RunTree::build(
&snapshot
.runs()
.iter()
.map(|run| RunInfo {
run_id: run.run_id,
parent_run_id: run.parent_run_id,
started_at: run.started_at,
})
.collect::<Vec<_>>(),
);
let alignment =
RunListAlignment::from_runs_with_total(snapshot.runs(), snapshot.total_size())
.with_tree(&tree);
let latest_run_id = self.snapshot_with_replayability.latest_run_id();
let run_map: HashMap<_, _> = snapshot.runs().iter().map(|r| (r.run_id, r)).collect();
for item in tree.iter() {
let prefix = format_tree_prefix(self.theme_characters, item);
let run_id_padding = (alignment.tree_prefix_width - item.tree_prefix_width()) * 2;
match item.run_id {
Some(run_id) => {
let run = run_map
.get(&run_id)
.expect("run ID from tree should exist in snapshot");
let replayability = self
.snapshot_with_replayability
.get_replayability(run.run_id);
let display = DisplayRecordedRunInfo::new(
run,
snapshot.run_id_index(),
replayability,
alignment,
self.styles,
self.redactor,
)
.with_tree_formatting(&prefix, run_id_padding);
if Some(run.run_id) == latest_run_id {
writeln!(f, "{} *{}", display, "latest".style(self.styles.count))?;
} else {
writeln!(f, "{}", display)?;
}
}
None => {
let virtual_padding = run_id_padding + 5;
writeln!(
f,
"{}{}{:padding$} {}",
prefix,
"???".style(self.styles.run_id_rest),
"",
"(pruned parent)".style(self.styles.cancelled),
padding = virtual_padding,
)?;
}
}
}
let first_col_width = 2 + alignment.tree_prefix_width * 2 + 8;
let total_line_spacing = first_col_width + 2 + 19 + 2 + 10 + 2;
writeln!(
f,
"{:spacing$}{}",
"",
self.theme_characters.hbar(alignment.size_width),
spacing = total_line_spacing,
)?;
let size_display = self.redactor.redact_size(snapshot.total_size());
let size_formatted = format!("{:>width$}", size_display, width = alignment.size_width);
writeln!(
f,
"{:spacing$}{}",
"",
size_formatted.style(self.styles.size),
spacing = total_line_spacing,
)?;
Ok(())
}
}
fn format_tree_prefix(theme_characters: &ThemeCharacters, item: &TreeIterItem) -> String {
let mut prefix = String::new();
prefix.push_str(" ");
if item.depth == 0 {
return prefix;
}
for &has_continuation in &item.continuation_flags {
if has_continuation {
prefix.push_str(theme_characters.tree_continuation());
} else {
prefix.push_str(theme_characters.tree_space());
}
}
if !item.is_only_child {
if item.is_last {
prefix.push_str(theme_characters.tree_last());
} else {
prefix.push_str(theme_characters.tree_branch());
}
}
prefix
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
errors::RecordPruneError,
helpers::ThemeCharacters,
record::{
CompletedRunStats, ComponentSizes, NonReplayableReason, PruneKind, PrunePlan,
PruneResult, RecordedRunStatus, RecordedSizes, RunStoreSnapshot,
SnapshotWithReplayability, StressCompletedRunStats,
format::{STORE_FORMAT_VERSION, StoreFormatMajorVersion, StoreVersionIncompatibility},
run_id_index::RunIdIndex,
},
redact::Redactor,
};
use chrono::{DateTime, Utc};
use semver::Version;
use std::{collections::BTreeMap, num::NonZero};
fn test_now() -> DateTime<Utc> {
DateTime::parse_from_rfc3339("2024-06-25T13:00:30+00:00")
.expect("valid datetime")
.with_timezone(&Utc)
}
fn make_run_info(
uuid: &str,
version: &str,
started_at: &str,
total_compressed_size: u64,
status: RecordedRunStatus,
) -> RecordedRunInfo {
make_run_info_with_duration(
uuid,
version,
started_at,
total_compressed_size,
1.0,
status,
)
}
fn make_run_info_with_duration(
uuid: &str,
version: &str,
started_at: &str,
total_compressed_size: u64,
duration_secs: f64,
status: RecordedRunStatus,
) -> RecordedRunInfo {
let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
RecordedRunInfo {
run_id: uuid.parse().expect("valid UUID"),
store_format_version: STORE_FORMAT_VERSION,
nextest_version: Version::parse(version).expect("valid version"),
started_at,
last_written_at: started_at,
duration_secs: Some(duration_secs),
cli_args: Vec::new(),
build_scope_args: Vec::new(),
env_vars: BTreeMap::new(),
parent_run_id: None,
sizes: RecordedSizes {
log: ComponentSizes::default(),
store: ComponentSizes {
compressed: total_compressed_size,
uncompressed: total_compressed_size * 3,
entries: 0,
},
},
status,
}
}
fn make_run_info_with_cli_env(
uuid: &str,
version: &str,
started_at: &str,
cli_args: Vec<String>,
env_vars: BTreeMap<String, String>,
status: RecordedRunStatus,
) -> RecordedRunInfo {
make_run_info_with_parent(uuid, version, started_at, cli_args, env_vars, None, status)
}
fn make_run_info_with_parent(
uuid: &str,
version: &str,
started_at: &str,
cli_args: Vec<String>,
env_vars: BTreeMap<String, String>,
parent_run_id: Option<&str>,
status: RecordedRunStatus,
) -> RecordedRunInfo {
let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
RecordedRunInfo {
run_id: uuid.parse().expect("valid UUID"),
store_format_version: STORE_FORMAT_VERSION,
nextest_version: Version::parse(version).expect("valid version"),
started_at,
last_written_at: started_at,
duration_secs: Some(12.345),
cli_args,
build_scope_args: Vec::new(),
env_vars,
parent_run_id: parent_run_id.map(|s| s.parse().expect("valid UUID")),
sizes: RecordedSizes {
log: ComponentSizes {
compressed: 1024,
uncompressed: 4096,
entries: 100,
},
store: ComponentSizes {
compressed: 51200,
uncompressed: 204800,
entries: 42,
},
},
status,
}
}
#[test]
fn test_display_prune_result_nothing_to_prune() {
let result = PruneResult::default();
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"no runs to prune");
}
#[test]
fn test_display_prune_result_nothing_pruned_with_error() {
let result = PruneResult {
kind: PruneKind::Implicit,
deleted_count: 0,
orphans_deleted: 0,
freed_bytes: 0,
errors: vec![RecordPruneError::DeleteOrphan {
path: "/some/path".into(),
error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
}],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"no runs pruned (1 error occurred)");
}
#[test]
fn test_display_prune_result_single_run() {
let result = PruneResult {
kind: PruneKind::Implicit,
deleted_count: 1,
orphans_deleted: 0,
freed_bytes: 1024,
errors: vec![],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 1 run, freed 1 KB");
}
#[test]
fn test_display_prune_result_multiple_runs() {
let result = PruneResult {
kind: PruneKind::Implicit,
deleted_count: 3,
orphans_deleted: 0,
freed_bytes: 5 * 1024 * 1024,
errors: vec![],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 3 runs, freed 5.0 MB");
}
#[test]
fn test_display_prune_result_with_orphan() {
let result = PruneResult {
kind: PruneKind::Implicit,
deleted_count: 2,
orphans_deleted: 1,
freed_bytes: 3 * 1024 * 1024,
errors: vec![],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 2 runs, 1 orphan, freed 3.0 MB");
}
#[test]
fn test_display_prune_result_with_multiple_orphans() {
let result = PruneResult {
kind: PruneKind::Implicit,
deleted_count: 1,
orphans_deleted: 3,
freed_bytes: 2 * 1024 * 1024,
errors: vec![],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 1 run, 3 orphans, freed 2.0 MB");
}
#[test]
fn test_display_prune_result_with_errors_implicit() {
let result = PruneResult {
kind: PruneKind::Implicit,
deleted_count: 2,
orphans_deleted: 0,
freed_bytes: 1024 * 1024,
errors: vec![
RecordPruneError::DeleteOrphan {
path: "/path1".into(),
error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
},
RecordPruneError::DeleteOrphan {
path: "/path2".into(),
error: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
},
],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 2 runs, freed 1.0 MB (2 errors occurred)");
}
#[test]
fn test_display_prune_result_with_errors_explicit() {
let result = PruneResult {
kind: PruneKind::Explicit,
deleted_count: 2,
orphans_deleted: 0,
freed_bytes: 1024 * 1024,
errors: vec![
RecordPruneError::DeleteOrphan {
path: "/path1".into(),
error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
},
RecordPruneError::DeleteOrphan {
path: "/path2".into(),
error: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
},
],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"
pruned 2 runs, freed 1.0 MB (2 errors occurred)
errors:
- error deleting orphaned directory `/path1`: denied
- error deleting orphaned directory `/path2`: not found
");
}
#[test]
fn test_display_prune_result_full() {
let result = PruneResult {
kind: PruneKind::Implicit,
deleted_count: 5,
orphans_deleted: 2,
freed_bytes: 10 * 1024 * 1024,
errors: vec![RecordPruneError::DeleteOrphan {
path: "/orphan".into(),
error: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
}],
};
insta::assert_snapshot!(result.display(&Styles::default()).to_string(), @"pruned 5 runs, 2 orphans, freed 10.0 MB (1 error occurred)");
}
#[test]
fn test_display_recorded_run_info_completed() {
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440000",
"0.9.100",
"2024-06-15T10:30:00+00:00",
102400,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 100,
passed: 95,
failed: 5,
exit_code: 100,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-15 10:30:00 1.000s 100 KB 95 passed / 5 failed"
);
}
#[test]
fn test_display_recorded_run_info_incomplete() {
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440001",
"0.9.101",
"2024-06-16T11:00:00+00:00",
51200,
RecordedRunStatus::Incomplete,
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Incomplete, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-16 11:00:00 1.000s 50 KB incomplete"
);
}
#[test]
fn test_display_recorded_run_info_not_run() {
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440005",
"0.9.105",
"2024-06-20T15:00:00+00:00",
75000,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 17,
passed: 10,
failed: 6,
exit_code: 100,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-20 15:00:00 1.000s 73 KB 10 passed / 6 failed / 1 not run"
);
}
#[test]
fn test_display_recorded_run_info_no_tests() {
let run = make_run_info(
"550e8400-e29b-41d4-a716-44665544000c",
"0.9.112",
"2024-06-23T16:00:00+00:00",
5000,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 0,
passed: 0,
failed: 0,
exit_code: 0,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-23 16:00:00 1.000s 4 KB 0 passed"
);
}
#[test]
fn test_display_recorded_run_info_stress_completed() {
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440010",
"0.9.120",
"2024-06-25T10:00:00+00:00",
150000,
RecordedRunStatus::StressCompleted(StressCompletedRunStats {
initial_iteration_count: NonZero::new(100),
success_count: 100,
failed_count: 0,
exit_code: 0,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-25 10:00:00 1.000s 146 KB 100 passed iterations"
);
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440011",
"0.9.120",
"2024-06-25T11:00:00+00:00",
150000,
RecordedRunStatus::StressCompleted(StressCompletedRunStats {
initial_iteration_count: NonZero::new(100),
success_count: 95,
failed_count: 5,
exit_code: 0,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-25 11:00:00 1.000s 146 KB 95 passed iterations / 5 failed"
);
}
#[test]
fn test_display_recorded_run_info_stress_cancelled() {
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440012",
"0.9.120",
"2024-06-25T12:00:00+00:00",
100000,
RecordedRunStatus::StressCancelled(StressCompletedRunStats {
initial_iteration_count: NonZero::new(100),
success_count: 50,
failed_count: 10,
exit_code: 0,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-25 12:00:00 1.000s 97 KB 50 passed iterations / 10 failed / 40 not run (cancelled)"
);
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440013",
"0.9.120",
"2024-06-25T13:00:00+00:00",
100000,
RecordedRunStatus::StressCancelled(StressCompletedRunStats {
initial_iteration_count: None,
success_count: 50,
failed_count: 10,
exit_code: 0,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let alignment = RunListAlignment::from_runs(runs);
insta::assert_snapshot!(
run.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-25 13:00:00 1.000s 97 KB 50 passed iterations / 10 failed (cancelled)"
);
}
#[test]
fn test_display_alignment_multiple_runs() {
let runs = vec![
make_run_info(
"550e8400-e29b-41d4-a716-446655440006",
"0.9.106",
"2024-06-21T10:00:00+00:00",
100000,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 559,
passed: 559,
failed: 0,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-446655440007",
"0.9.107",
"2024-06-21T11:00:00+00:00",
50000,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 51,
passed: 51,
failed: 0,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-446655440008",
"0.9.108",
"2024-06-21T12:00:00+00:00",
30000,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 17,
passed: 10,
failed: 6,
exit_code: 0,
}),
),
];
let index = RunIdIndex::new(&runs);
let alignment = RunListAlignment::from_runs(&runs);
insta::assert_snapshot!(
runs[0]
.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-21 10:00:00 1.000s 97 KB 559 passed"
);
insta::assert_snapshot!(
runs[1]
.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-21 11:00:00 1.000s 48 KB 51 passed"
);
insta::assert_snapshot!(
runs[2]
.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-21 12:00:00 1.000s 29 KB 10 passed / 6 failed / 1 not run"
);
}
#[test]
fn test_display_stress_stats_alignment() {
let runs = vec![
make_run_info(
"550e8400-e29b-41d4-a716-446655440009",
"0.9.109",
"2024-06-22T10:00:00+00:00",
200000,
RecordedRunStatus::StressCompleted(StressCompletedRunStats {
initial_iteration_count: NonZero::new(1000),
success_count: 1000,
failed_count: 0,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-44665544000a",
"0.9.110",
"2024-06-22T11:00:00+00:00",
100000,
RecordedRunStatus::StressCompleted(StressCompletedRunStats {
initial_iteration_count: NonZero::new(100),
success_count: 95,
failed_count: 5,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-44665544000b",
"0.9.111",
"2024-06-22T12:00:00+00:00",
80000,
RecordedRunStatus::StressCancelled(StressCompletedRunStats {
initial_iteration_count: NonZero::new(500),
success_count: 45,
failed_count: 5,
exit_code: 0,
}),
),
];
let index = RunIdIndex::new(&runs);
let alignment = RunListAlignment::from_runs(&runs);
insta::assert_snapshot!(
runs[0]
.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-22 10:00:00 1.000s 195 KB 1000 passed iterations"
);
insta::assert_snapshot!(
runs[1]
.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-22 11:00:00 1.000s 97 KB 95 passed iterations / 5 failed"
);
insta::assert_snapshot!(
runs[2]
.display(&index, &ReplayabilityStatus::Replayable, alignment, &Styles::default(), &Redactor::noop())
.to_string(),
@" 550e8400 2024-06-22 12:00:00 1.000s 78 KB 45 passed iterations / 5 failed / 450 not run (cancelled)"
);
}
#[test]
fn test_display_prune_plan_empty() {
let plan = PrunePlan::new(vec![]);
let index = RunIdIndex::new(&[]);
insta::assert_snapshot!(
plan.display(&index, &Styles::default(), &Redactor::noop())
.to_string(),
@"no runs would be pruned"
);
}
#[test]
fn test_display_prune_plan_single_run() {
let runs = vec![make_run_info(
"550e8400-e29b-41d4-a716-446655440002",
"0.9.102",
"2024-06-17T12:00:00+00:00",
2048 * 1024,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 50,
passed: 50,
failed: 0,
exit_code: 0,
}),
)];
let index = RunIdIndex::new(&runs);
let plan = PrunePlan::new(runs);
insta::assert_snapshot!(
plan.display(&index, &Styles::default(), &Redactor::noop())
.to_string(),
@"
would prune 1 run, freeing 2.0 MB:
550e8400 2024-06-17 12:00:00 1.000s 2.0 MB 50 passed
"
);
}
#[test]
fn test_display_prune_plan_multiple_runs() {
let runs = vec![
make_run_info(
"550e8400-e29b-41d4-a716-446655440003",
"0.9.103",
"2024-06-18T13:00:00+00:00",
1024 * 1024,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 100,
passed: 100,
failed: 0,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-446655440004",
"0.9.104",
"2024-06-19T14:00:00+00:00",
512 * 1024,
RecordedRunStatus::Incomplete,
),
];
let index = RunIdIndex::new(&runs);
let plan = PrunePlan::new(runs);
insta::assert_snapshot!(
plan.display(&index, &Styles::default(), &Redactor::noop())
.to_string(),
@"
would prune 2 runs, freeing 1.5 MB:
550e8400 2024-06-18 13:00:00 1.000s 1.0 MB 100 passed
550e8400 2024-06-19 14:00:00 1.000s 512 KB incomplete
"
);
}
#[test]
fn test_display_run_list() {
let theme_characters = ThemeCharacters::default();
let runs = vec![
make_run_info(
"550e8400-e29b-41d4-a716-446655440001",
"0.9.101",
"2024-06-15T10:00:00+00:00",
50 * 1024,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 10,
passed: 10,
failed: 0,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-446655440002",
"0.9.102",
"2024-06-16T11:00:00+00:00",
75 * 1024,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 20,
passed: 18,
failed: 2,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-446655440003",
"0.9.103",
"2024-06-17T12:00:00+00:00",
100 * 1024,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 30,
passed: 30,
failed: 0,
exit_code: 0,
}),
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
insta::assert_snapshot!(
"normal_sizes",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
let runs = vec![
make_run_info(
"550e8400-e29b-41d4-a716-446655440001",
"0.9.101",
"2024-06-15T10:00:00+00:00",
1024, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 5,
passed: 5,
failed: 0,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-446655440002",
"0.9.102",
"2024-06-16T11:00:00+00:00",
99 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 10,
passed: 10,
failed: 0,
exit_code: 0,
}),
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
insta::assert_snapshot!(
"small_sizes",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
let runs = vec![
make_run_info(
"550e8400-e29b-41d4-a716-446655440001",
"0.9.101",
"2024-06-15T10:00:00+00:00",
1_000_000 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 100,
passed: 100,
failed: 0,
exit_code: 0,
}),
),
make_run_info(
"550e8400-e29b-41d4-a716-446655440002",
"0.9.102",
"2024-06-16T11:00:00+00:00",
10_000_000 * 1024, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 200,
passed: 200,
failed: 0,
exit_code: 0,
}),
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
insta::assert_snapshot!(
"large_sizes",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
let runs = vec![
make_run_info_with_duration(
"550e8400-e29b-41d4-a716-446655440001",
"0.9.101",
"2024-06-15T10:00:00+00:00",
50 * 1024,
0.123, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 5,
passed: 5,
failed: 0,
exit_code: 0,
}),
),
make_run_info_with_duration(
"550e8400-e29b-41d4-a716-446655440002",
"0.9.102",
"2024-06-16T11:00:00+00:00",
75 * 1024,
9.876, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 10,
passed: 10,
failed: 0,
exit_code: 0,
}),
),
make_run_info_with_duration(
"550e8400-e29b-41d4-a716-446655440003",
"0.9.103",
"2024-06-17T12:00:00+00:00",
100 * 1024,
42.5, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 20,
passed: 20,
failed: 0,
exit_code: 0,
}),
),
make_run_info_with_duration(
"550e8400-e29b-41d4-a716-446655440004",
"0.9.104",
"2024-06-18T13:00:00+00:00",
125 * 1024,
987.654, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 30,
passed: 28,
failed: 2,
exit_code: 0,
}),
),
make_run_info_with_duration(
"550e8400-e29b-41d4-a716-446655440005",
"0.9.105",
"2024-06-19T14:00:00+00:00",
150 * 1024,
12345.678, RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 50,
passed: 50,
failed: 0,
exit_code: 0,
}),
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
insta::assert_snapshot!(
"varying_durations",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_display_detailed() {
let cli_args = vec![
"cargo".to_string(),
"nextest".to_string(),
"run".to_string(),
"--workspace".to_string(),
"--features".to_string(),
"foo bar".to_string(),
];
let env_vars = BTreeMap::from([
("CARGO_HOME".to_string(), "/home/user/.cargo".to_string()),
("NEXTEST_PROFILE".to_string(), "ci".to_string()),
]);
let run_with_cli_env = make_run_info_with_cli_env(
"550e8400-e29b-41d4-a716-446655440000",
"0.9.122",
"2024-06-15T10:30:00+00:00",
cli_args,
env_vars,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 100,
passed: 95,
failed: 5,
exit_code: 100,
}),
);
let run_empty = make_run_info_with_cli_env(
"550e8400-e29b-41d4-a716-446655440001",
"0.9.122",
"2024-06-16T11:00:00+00:00",
Vec::new(),
BTreeMap::new(),
RecordedRunStatus::Incomplete,
);
let stress_all_passed = make_run_info_with_cli_env(
"550e8400-e29b-41d4-a716-446655440010",
"0.9.122",
"2024-06-25T10:00:00+00:00",
Vec::new(),
BTreeMap::new(),
RecordedRunStatus::StressCompleted(StressCompletedRunStats {
initial_iteration_count: NonZero::new(100),
success_count: 100,
failed_count: 0,
exit_code: 0,
}),
);
let stress_with_failures = make_run_info_with_cli_env(
"550e8400-e29b-41d4-a716-446655440011",
"0.9.122",
"2024-06-25T11:00:00+00:00",
Vec::new(),
BTreeMap::new(),
RecordedRunStatus::StressCompleted(StressCompletedRunStats {
initial_iteration_count: NonZero::new(100),
success_count: 95,
failed_count: 5,
exit_code: 0,
}),
);
let stress_cancelled = make_run_info_with_cli_env(
"550e8400-e29b-41d4-a716-446655440012",
"0.9.122",
"2024-06-25T12:00:00+00:00",
Vec::new(),
BTreeMap::new(),
RecordedRunStatus::StressCancelled(StressCompletedRunStats {
initial_iteration_count: NonZero::new(100),
success_count: 50,
failed_count: 10,
exit_code: 0,
}),
);
let stress_cancelled_no_initial = make_run_info_with_cli_env(
"550e8400-e29b-41d4-a716-446655440013",
"0.9.122",
"2024-06-25T13:00:00+00:00",
Vec::new(),
BTreeMap::new(),
RecordedRunStatus::StressCancelled(StressCompletedRunStats {
initial_iteration_count: None,
success_count: 50,
failed_count: 10,
exit_code: 0,
}),
);
let runs = [
run_with_cli_env,
run_empty,
stress_all_passed,
stress_with_failures,
stress_cancelled,
stress_cancelled_no_initial,
];
let index = RunIdIndex::new(&runs);
let theme_characters = ThemeCharacters::default();
let redactor = Redactor::noop();
let now = test_now();
let replayable = ReplayabilityStatus::Replayable;
let incomplete = ReplayabilityStatus::Incomplete;
insta::assert_snapshot!(
"with_cli_and_env",
runs[0]
.display_detailed(
&index,
&replayable,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
insta::assert_snapshot!(
"empty_cli_and_env",
runs[1]
.display_detailed(
&index,
&incomplete,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
insta::assert_snapshot!(
"stress_all_passed",
runs[2]
.display_detailed(
&index,
&replayable,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
insta::assert_snapshot!(
"stress_with_failures",
runs[3]
.display_detailed(
&index,
&replayable,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
insta::assert_snapshot!(
"stress_cancelled",
runs[4]
.display_detailed(
&index,
&replayable,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
insta::assert_snapshot!(
"stress_cancelled_no_initial",
runs[5]
.display_detailed(
&index,
&replayable,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
}
#[test]
fn test_display_detailed_with_parent_run() {
let parent_run = make_run_info_with_cli_env(
"550e8400-e29b-41d4-a716-446655440000",
"0.9.122",
"2024-06-15T10:00:00+00:00",
Vec::new(),
BTreeMap::new(),
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 100,
passed: 95,
failed: 5,
exit_code: 100,
}),
);
let child_run = make_run_info_with_parent(
"660e8400-e29b-41d4-a716-446655440001",
"0.9.122",
"2024-06-15T11:00:00+00:00",
vec![
"cargo".to_string(),
"nextest".to_string(),
"run".to_string(),
"--rerun".to_string(),
],
BTreeMap::new(),
Some("550e8400-e29b-41d4-a716-446655440000"),
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 5,
passed: 5,
failed: 0,
exit_code: 0,
}),
);
let runs = [parent_run, child_run];
let index = RunIdIndex::new(&runs);
let theme_characters = ThemeCharacters::default();
let redactor = Redactor::noop();
let now = test_now();
let replayable = ReplayabilityStatus::Replayable;
insta::assert_snapshot!(
"with_parent_run",
runs[1]
.display_detailed(
&index,
&replayable,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
}
#[test]
fn test_display_replayability_statuses() {
let run = make_run_info(
"550e8400-e29b-41d4-a716-446655440000",
"0.9.100",
"2024-06-15T10:30:00+00:00",
102400,
RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: 100,
passed: 100,
failed: 0,
exit_code: 0,
}),
);
let runs = std::slice::from_ref(&run);
let index = RunIdIndex::new(runs);
let theme_characters = ThemeCharacters::default();
let redactor = Redactor::noop();
let now = test_now();
let definitely_replayable = ReplayabilityStatus::Replayable;
insta::assert_snapshot!(
"replayability_yes",
run.display_detailed(
&index,
&definitely_replayable,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
let format_too_new = ReplayabilityStatus::NotReplayable(vec![
NonReplayableReason::StoreVersionIncompatible {
incompatibility: StoreVersionIncompatibility::RecordingTooNew {
recording_major: StoreFormatMajorVersion::new(5),
supported_major: StoreFormatMajorVersion::new(1),
},
},
]);
insta::assert_snapshot!(
"replayability_format_too_new",
run.display_detailed(
&index,
&format_too_new,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
let missing_store =
ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::MissingStoreZip]);
insta::assert_snapshot!(
"replayability_missing_store_zip",
run.display_detailed(
&index,
&missing_store,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
let missing_log =
ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::MissingRunLog]);
insta::assert_snapshot!(
"replayability_missing_run_log",
run.display_detailed(
&index,
&missing_log,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
let status_unknown =
ReplayabilityStatus::NotReplayable(vec![NonReplayableReason::StatusUnknown]);
insta::assert_snapshot!(
"replayability_status_unknown",
run.display_detailed(
&index,
&status_unknown,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
let incomplete = ReplayabilityStatus::Incomplete;
insta::assert_snapshot!(
"replayability_incomplete",
run.display_detailed(
&index,
&incomplete,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
let multiple_blocking = ReplayabilityStatus::NotReplayable(vec![
NonReplayableReason::MissingStoreZip,
NonReplayableReason::MissingRunLog,
]);
insta::assert_snapshot!(
"replayability_multiple_blocking",
run.display_detailed(
&index,
&multiple_blocking,
now,
&Styles::default(),
&theme_characters,
&redactor
)
.to_string()
);
}
fn make_run_for_tree(
uuid: &str,
started_at: &str,
parent_run_id: Option<&str>,
size_kb: u64,
passed: usize,
failed: usize,
) -> RecordedRunInfo {
let started_at = DateTime::parse_from_rfc3339(started_at).expect("valid datetime");
RecordedRunInfo {
run_id: uuid.parse().expect("valid UUID"),
store_format_version: STORE_FORMAT_VERSION,
nextest_version: Version::parse("0.9.100").expect("valid version"),
started_at,
last_written_at: started_at,
duration_secs: Some(1.0),
cli_args: Vec::new(),
build_scope_args: Vec::new(),
env_vars: BTreeMap::new(),
parent_run_id: parent_run_id.map(|s| s.parse().expect("valid UUID")),
sizes: RecordedSizes {
log: ComponentSizes::default(),
store: ComponentSizes {
compressed: size_kb * 1024,
uncompressed: size_kb * 1024 * 3,
entries: 0,
},
},
status: RecordedRunStatus::Completed(CompletedRunStats {
initial_run_count: passed + failed,
passed,
failed,
exit_code: if failed > 0 { 1 } else { 0 },
}),
}
}
#[test]
fn test_tree_linear_chain() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
60,
8,
2,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000002-0000-0000-0000-000000000002"),
70,
10,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_linear_chain",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_branching() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
60,
5,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
70,
3,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_branching",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_pruned_parent() {
let runs = vec![make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"), 60,
5,
0,
)];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_pruned_parent",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_mixed_independent_and_chain() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
None,
60,
8,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000002-0000-0000-0000-000000000002"),
70,
5,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_mixed_independent_and_chain",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_deep_chain() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
60,
10,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000002-0000-0000-0000-000000000002"),
70,
10,
0,
),
make_run_for_tree(
"50000004-0000-0000-0000-000000000004",
"2024-06-15T13:00:00+00:00",
Some("50000003-0000-0000-0000-000000000003"),
80,
10,
0,
),
make_run_for_tree(
"50000005-0000-0000-0000-000000000005",
"2024-06-15T14:00:00+00:00",
Some("50000004-0000-0000-0000-000000000004"),
90,
10,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_deep_chain",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_branching_with_chains() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
60,
8,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000002-0000-0000-0000-000000000002"),
70,
5,
0,
),
make_run_for_tree(
"50000004-0000-0000-0000-000000000004",
"2024-06-15T13:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
80,
3,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_branching_with_chains",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_continuation_flags() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T13:00:00+00:00", Some("50000001-0000-0000-0000-000000000001"),
60,
8,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T14:00:00+00:00",
Some("50000002-0000-0000-0000-000000000002"),
70,
5,
0,
),
make_run_for_tree(
"50000004-0000-0000-0000-000000000004",
"2024-06-15T11:00:00+00:00", Some("50000001-0000-0000-0000-000000000001"),
80,
3,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_continuation_flags",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_pruned_parent_with_chain() {
let runs = vec![
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"), 60,
8,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000002-0000-0000-0000-000000000002"),
70,
5,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_pruned_parent_with_chain",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_pruned_parent_with_multiple_children() {
let runs = vec![
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"), 60,
5,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"), 70,
3,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_pruned_parent_with_multiple_children",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_unicode_characters() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
60,
8,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T12:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
70,
5,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let mut theme_characters = ThemeCharacters::default();
theme_characters.use_unicode();
insta::assert_snapshot!(
"tree_unicode_characters",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
#[test]
fn test_tree_compressed_with_branching() {
let runs = vec![
make_run_for_tree(
"50000001-0000-0000-0000-000000000001",
"2024-06-15T10:00:00+00:00",
None,
50,
10,
0,
),
make_run_for_tree(
"50000002-0000-0000-0000-000000000002",
"2024-06-15T11:00:00+00:00",
Some("50000001-0000-0000-0000-000000000001"),
60,
8,
0,
),
make_run_for_tree(
"50000003-0000-0000-0000-000000000003",
"2024-06-15T14:00:00+00:00", Some("50000002-0000-0000-0000-000000000002"),
70,
5,
0,
),
make_run_for_tree(
"50000004-0000-0000-0000-000000000004",
"2024-06-15T15:00:00+00:00",
Some("50000003-0000-0000-0000-000000000003"),
80,
3,
0,
),
make_run_for_tree(
"50000005-0000-0000-0000-000000000005",
"2024-06-15T12:00:00+00:00", Some("50000002-0000-0000-0000-000000000002"),
90,
2,
0,
),
];
let snapshot = RunStoreSnapshot::new_for_test(runs);
let snapshot_with_replayability = SnapshotWithReplayability::new_for_test(&snapshot);
let theme_characters = ThemeCharacters::default();
insta::assert_snapshot!(
"tree_compressed_with_branching",
DisplayRunList::new(
&snapshot_with_replayability,
None,
&Styles::default(),
&theme_characters,
&Redactor::noop()
)
.to_string()
);
}
}