qubit-batch 0.8.1

One-shot batch execution and processing with sequential and scoped parallel utilities
Documentation
/*******************************************************************************
 *
 *    Copyright (c) 2025 - 2026 Haixing Hu.
 *
 *    SPDX-License-Identifier: Apache-2.0
 *
 *    Licensed under the Apache License, Version 2.0.
 *
 ******************************************************************************/
//! Tests for batch outcomes and execution state.

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);
}