s3rm-rs 1.3.2

Fast Amazon S3 object deletion tool.
Documentation
// Property-based tests for the progress indicator.
//
// Feature: s3rm-rs, Property 31: Progress Reporting
// For any deletion operation in progress, the tool should display progress
// information including objects deleted, unless quiet mode is enabled.
// **Validates: Requirements 7.1, 7.3, 7.4**

#[cfg(test)]
mod tests {
    use crate::indicator::show_indicator;
    use proptest::prelude::*;
    use s3rm_rs::types::DeletionStatistics;
    use std::time::Duration;

    /// Generate a random sequence of DeletionStatistics events.
    fn arb_stats_sequence() -> impl Strategy<Value = Vec<DeletionStatistics>> {
        prop::collection::vec(
            prop_oneof![
                any::<u16>().prop_map(|_| {
                    DeletionStatistics::DeleteComplete {
                        key: "key".to_string(),
                    }
                }),
                (1u64..=10_000u64).prop_map(DeletionStatistics::DeleteBytes),
                any::<u16>().prop_map(|_| {
                    DeletionStatistics::DeleteError {
                        key: "err".to_string(),
                    }
                }),
                any::<u16>().prop_map(|_| {
                    DeletionStatistics::DeleteSkip {
                        key: "skip".to_string(),
                    }
                }),
            ],
            0..50,
        )
    }

    /// Compute expected indicator summary counts from a stats sequence.
    fn expected_counts(stats: &[DeletionStatistics]) -> (u64, u64, u64, u64) {
        let mut deletes = 0u64;
        let mut bytes = 0u64;
        let mut errors = 0u64;
        let mut skips = 0u64;
        for s in stats {
            match s {
                DeletionStatistics::DeleteComplete { .. } => deletes += 1,
                DeletionStatistics::DeleteBytes(b) => bytes += *b,
                DeletionStatistics::DeleteError { .. } => errors += 1,
                DeletionStatistics::DeleteSkip { .. } => skips += 1,
            }
        }
        (deletes, bytes, errors, skips)
    }

    // -- Property 31: Progress Reporting --

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(50))]

        /// Feature: s3rm-rs, Property 31: Progress Reporting
        /// **Validates: Requirements 7.1, 7.3, 7.4**
        ///
        /// The show_indicator task MUST complete (not hang) for any sequence
        /// of DeletionStatistics events once the channel is closed, and the
        /// returned summary must accurately reflect the input events.
        #[test]
        fn prop_indicator_completes_for_any_stats(
            stats in arb_stats_sequence(),
            show_progress in proptest::bool::ANY,
            show_result in proptest::bool::ANY,
        ) {
            let (exp_del, exp_bytes, exp_err, exp_skip) = expected_counts(&stats);

            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .unwrap();

            rt.block_on(async {
                let (sender, receiver) = async_channel::unbounded();

                for s in &stats {
                    sender.send(s.clone()).await.unwrap();
                }

                drop(sender); // Close channel to trigger summary

                let handle = show_indicator(
                    receiver,
                    show_progress,
                    show_result,
                    false, // dry_run
                );

                // Must complete within 5 seconds
                let summary = tokio::time::timeout(Duration::from_secs(5), handle)
                    .await
                    .expect("indicator should complete within timeout")
                    .expect("indicator task should not panic");

                prop_assert_eq!(summary.total_delete_count, exp_del);
                prop_assert_eq!(summary.total_delete_bytes, exp_bytes);
                prop_assert_eq!(summary.total_error_count, exp_err);
                prop_assert_eq!(summary.total_skip_count, exp_skip);

                Ok(())
            })?;
        }

        /// Feature: s3rm-rs, Property 31: Progress Reporting (quiet mode)
        /// **Validates: Requirements 7.4**
        ///
        /// When both show_progress and show_result are false (quiet mode),
        /// the indicator still processes stats correctly and completes.
        #[test]
        fn prop_indicator_quiet_mode_completes(
            stats in arb_stats_sequence(),
        ) {
            let (exp_del, exp_bytes, exp_err, exp_skip) = expected_counts(&stats);

            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .unwrap();

            rt.block_on(async {
                let (sender, receiver) = async_channel::unbounded();

                for s in &stats {
                    sender.send(s.clone()).await.unwrap();
                }

                drop(sender);

                let handle = show_indicator(receiver, false, false, false);

                let summary = tokio::time::timeout(Duration::from_secs(5), handle)
                    .await
                    .expect("indicator should complete within timeout")
                    .expect("indicator task should not panic");

                prop_assert_eq!(summary.total_delete_count, exp_del);
                prop_assert_eq!(summary.total_delete_bytes, exp_bytes);
                prop_assert_eq!(summary.total_error_count, exp_err);
                prop_assert_eq!(summary.total_skip_count, exp_skip);

                Ok(())
            })?;
        }

        /// Feature: s3rm-rs, Property 31: Progress Reporting (dry-run)
        /// **Validates: Requirements 7.1**
        ///
        /// In dry-run mode, the indicator still completes correctly and
        /// counts are accurate. Throughput counters are zeroed in dry-run
        /// mode (verified in the display, not in the summary).
        #[test]
        fn prop_indicator_dry_run_completes(
            stats in arb_stats_sequence(),
        ) {
            let (exp_del, exp_bytes, exp_err, exp_skip) = expected_counts(&stats);

            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .unwrap();

            rt.block_on(async {
                let (sender, receiver) = async_channel::unbounded();

                for s in &stats {
                    sender.send(s.clone()).await.unwrap();
                }

                drop(sender);

                let handle = show_indicator(
                    receiver,
                    false,
                    false,
                    true, // dry_run = true
                );

                let summary = tokio::time::timeout(Duration::from_secs(5), handle)
                    .await
                    .expect("indicator should complete within timeout")
                    .expect("indicator task should not panic");

                prop_assert_eq!(summary.total_delete_count, exp_del);
                prop_assert_eq!(summary.total_delete_bytes, exp_bytes);
                prop_assert_eq!(summary.total_error_count, exp_err);
                prop_assert_eq!(summary.total_skip_count, exp_skip);

                Ok(())
            })?;
        }
    }
}