use std::{
error::Error,
fmt,
time::Duration,
};
use qubit_batch::{
BatchExecutionError,
BatchOutcomeBuildError,
BatchOutcomeBuilder,
BatchTaskError,
BatchTaskFailure,
};
use qubit_progress::ProgressCounters;
#[test]
fn test_batch_outcome_builder_builds_valid_outcome() {
let failures = vec![
BatchTaskFailure::new(2, BatchTaskError::panicked("panic")),
BatchTaskFailure::new(1, BatchTaskError::Failed("failed")),
];
let outcome = BatchOutcomeBuilder::builder(3)
.completed_count(3)
.succeeded_count(1)
.failed_count(1)
.panicked_count(1)
.elapsed(Duration::from_millis(5))
.failures(failures)
.build()
.expect("builder should validate consistent counters");
assert_eq!(outcome.task_count(), 3);
assert_eq!(outcome.completed_count(), 3);
assert_eq!(outcome.succeeded_count(), 1);
assert_eq!(outcome.failed_count(), 1);
assert_eq!(outcome.panicked_count(), 1);
assert_eq!(outcome.failure_count(), 2);
assert!(!outcome.is_success());
assert_eq!(outcome.failures()[0].index(), 1);
assert_eq!(outcome.failures()[1].index(), 2);
}
#[test]
fn test_batch_outcome_progress_counters_reflects_terminal_counts() {
let failures = vec![
BatchTaskFailure::new(1, BatchTaskError::Failed("err")),
BatchTaskFailure::new(2, BatchTaskError::panicked("panic")),
];
let outcome = BatchOutcomeBuilder::<&'static str>::builder(5)
.completed_count(4)
.succeeded_count(2)
.failed_count(1)
.panicked_count(1)
.failures(failures)
.build()
.expect("builder should validate consistent counters");
let counters = outcome.progress_counters();
let expected = ProgressCounters::new(Some(5))
.with_completed_count(4)
.with_succeeded_count(2)
.with_failed_count(2);
assert_eq!(counters, expected);
assert_eq!(counters.active_count(), 0);
}
#[test]
fn test_batch_outcome_records_all_failures() {
let failures = vec![
BatchTaskFailure::new(2, BatchTaskError::panicked("panic")),
BatchTaskFailure::new(1, BatchTaskError::Failed("failed")),
];
let outcome = BatchOutcomeBuilder::builder(3)
.completed_count(3)
.succeeded_count(1)
.failed_count(1)
.panicked_count(1)
.elapsed(Duration::from_millis(5))
.failures(failures)
.build()
.expect("builder should be valid");
assert_eq!(outcome.task_count(), 3);
assert_eq!(outcome.completed_count(), 3);
assert_eq!(outcome.succeeded_count(), 1);
assert_eq!(outcome.failed_count(), 1);
assert_eq!(outcome.panicked_count(), 1);
assert_eq!(outcome.failure_count(), 2);
assert!(!outcome.is_success());
assert_eq!(outcome.failures()[0].index(), 1);
assert_eq!(outcome.failures()[1].index(), 2);
assert!(outcome.to_string().contains("BatchOutcome"));
}
#[test]
fn test_batch_outcome_rejects_invalid_counters() {
let error = BatchOutcomeBuilder::<&'static str>::builder(2)
.completed_count(3)
.succeeded_count(3)
.build()
.expect_err("completed count should be invalid");
assert_eq!(
error,
BatchOutcomeBuildError::CompletedCountExceeded {
task_count: 2,
completed_count: 3,
}
);
}
#[test]
fn test_batch_outcome_rejects_failure_detail_mismatches() {
let failure = BatchTaskFailure::new(3, BatchTaskError::Failed("failed"));
assert!(matches!(
BatchOutcomeBuilder::builder(2)
.completed_count(1)
.failed_count(1)
.failures(vec![failure])
.build(),
Err(BatchOutcomeBuildError::FailureIndexOutOfRange { .. })
));
let failure: BatchTaskFailure<&'static str> =
BatchTaskFailure::new(0, BatchTaskError::panicked("panic"));
assert!(matches!(
BatchOutcomeBuilder::builder(2)
.completed_count(1)
.failed_count(1)
.failures(vec![failure])
.build(),
Err(BatchOutcomeBuildError::FailureVariantCountMismatch { .. })
));
assert!(matches!(
BatchOutcomeBuilder::<&'static str>::builder(2)
.completed_count(1)
.failed_count(usize::MAX)
.panicked_count(1)
.build(),
Err(BatchOutcomeBuildError::FailureCountOverflow { .. })
));
assert!(matches!(
BatchOutcomeBuilder::<&'static str>::builder(usize::MAX)
.succeeded_count(usize::MAX)
.failed_count(1)
.build(),
Err(BatchOutcomeBuildError::TerminalCountOverflow { .. })
));
assert!(matches!(
BatchOutcomeBuilder::<&'static str>::builder(2)
.completed_count(1)
.succeeded_count(1)
.failed_count(1)
.build(),
Err(BatchOutcomeBuildError::TerminalCountMismatch { .. })
));
assert!(matches!(
BatchOutcomeBuilder::<&'static str>::builder(2)
.completed_count(1)
.failed_count(1)
.build(),
Err(BatchOutcomeBuildError::FailureDetailCountMismatch { .. })
));
}
#[test]
fn test_batch_outcome_into_failures_and_success_state() {
let outcome = BatchOutcomeBuilder::<&'static str>::builder(1)
.completed_count(1)
.succeeded_count(1)
.build()
.expect("success outcome should be valid");
assert!(outcome.is_success());
assert!(outcome.into_failures().is_empty());
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TestError(&'static str);
impl fmt::Display for TestError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.0)
}
}
impl Error for TestError {}
#[test]
fn test_batch_task_error_helpers_display_and_source() {
let failed = BatchTaskError::Failed(TestError("failed"));
assert!(failed.is_failed());
assert!(!failed.is_panicked());
assert_eq!(failed.to_string(), "task failed: failed");
assert_eq!(failed.source().expect("source").to_string(), "failed");
let panicked = BatchTaskError::<TestError>::panicked("panic");
assert!(!panicked.is_failed());
assert!(panicked.is_panicked());
assert_eq!(panicked.panic_message(), Some("panic"));
assert_eq!(panicked.to_string(), "task panicked: panic");
assert!(panicked.source().is_none());
let panicked_without_message = BatchTaskError::<TestError>::panicked_without_message();
assert_eq!(panicked_without_message.panic_message(), None);
assert_eq!(panicked_without_message.to_string(), "task panicked");
}
#[test]
fn test_batch_task_failure_into_error() {
let failure = BatchTaskFailure::new(4, BatchTaskError::Failed("failed"));
assert_eq!(failure.index(), 4);
assert_eq!(failure.into_error(), BatchTaskError::Failed("failed"));
}
#[test]
fn test_batch_execution_error_accessors() {
let outcome = BatchOutcomeBuilder::<&'static str>::builder(2)
.completed_count(1)
.succeeded_count(1)
.build()
.expect("outcome should be valid");
let shortfall = BatchExecutionError::CountShortfall {
expected: 2,
actual: 1,
outcome: outcome.clone(),
};
assert!(shortfall.is_count_shortfall());
assert!(!shortfall.is_count_exceeded());
assert_eq!(shortfall.outcome().completed_count(), 1);
assert_eq!(
shortfall.to_string(),
"batch task count shortfall: expected 2, actual 1"
);
assert_eq!(shortfall.clone().into_outcome(), outcome);
let exceeded = BatchExecutionError::CountExceeded {
expected: 2,
observed_at_least: 3,
outcome,
};
assert!(!exceeded.is_count_shortfall());
assert!(exceeded.is_count_exceeded());
assert_eq!(exceeded.outcome().completed_count(), 1);
assert_eq!(
exceeded.to_string(),
"batch task count exceeded: expected 2, observed at least 3"
);
assert_eq!(exceeded.into_outcome().completed_count(), 1);
}