rusty-pv 0.1.0

Pipe viewer — a Rust port of Andrew Wood's `pv(1)` with progress bar, ETA, rate display, token-bucket rate limiting, IEC/SI unit math, SIGWINCH-aware terminal redraw, SIGUSR1 size refresh, multi-instance cursor coordination, and a typed library API.
Documentation
//! US4 (Library API) integration tests.

use rusty_pv::{Progress, PvBuilder, PvError, Reporter, UnitSystem};
use static_assertions::assert_impl_all;
use std::io::Cursor;
use std::time::Duration;

#[test]
fn send_sync_clone_bounds_sc028() {
    assert_impl_all!(rusty_pv::Pv: Send);
    assert_impl_all!(PvBuilder: Send);
    assert_impl_all!(Progress: Send, Sync);
    assert_impl_all!(PvError: Send, Sync);
}

#[test]
fn copy_through_in_memory_buffer_returns_bytes_transferred() {
    let src = vec![0xABu8; 4096];
    let mut reader = Cursor::new(src.clone());
    let mut writer = Vec::with_capacity(src.len());
    let pv = PvBuilder::new().build();
    let n = pv.copy(&mut reader, &mut writer).unwrap();
    assert_eq!(n, src.len() as u64);
    assert_eq!(writer, src);
}

#[test]
fn builder_three_permutations_yield_equivalent_pv_sc029() {
    let canonical = PvBuilder::new()
        .total_bytes(1024)
        .rate_limit(500)
        .buffer_size(2048)
        .interval(Duration::from_millis(50))
        .name("alpha")
        .build();
    let reverse = PvBuilder::new()
        .name("alpha")
        .interval(Duration::from_millis(50))
        .buffer_size(2048)
        .rate_limit(500)
        .total_bytes(1024)
        .build();
    let shuffled = PvBuilder::new()
        .name("alpha")
        .buffer_size(2048)
        .total_bytes(1024)
        .interval(Duration::from_millis(50))
        .rate_limit(500)
        .build();

    for p in [&canonical, &reverse, &shuffled] {
        assert_eq!(p.total_bytes(), Some(1024));
        assert_eq!(p.rate_limit(), Some(500));
        assert_eq!(p.buffer_size(), 2048);
        assert_eq!(p.interval(), Duration::from_millis(50));
        assert_eq!(p.name(), Some("alpha"));
    }
}

#[test]
fn custom_reporter_receives_at_least_one_update() {
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};

    struct Counter(Arc<AtomicUsize>);
    impl Reporter for Counter {
        fn report(&mut self, _progress: &Progress) {
            self.0.fetch_add(1, Ordering::Relaxed);
        }
    }
    let count = Arc::new(AtomicUsize::new(0));
    let pv = PvBuilder::new()
        .interval(Duration::from_millis(1))
        .reporter(Box::new(Counter(count.clone())))
        .build();
    let mut reader = Cursor::new(vec![0u8; 65536]);
    let mut writer = Vec::new();
    pv.copy(&mut reader, &mut writer).unwrap();
    assert!(count.load(Ordering::Relaxed) >= 1);
}

#[test]
fn unit_system_iec_default_si_opt_in_sc023() {
    assert_eq!(UnitSystem::Iec.format_bytes(1024), "1.00KiB");
    assert_eq!(UnitSystem::Si.format_bytes(1000), "1.00kB");
    assert_eq!(UnitSystem::Iec.format_rate(1_048_576.0), "1.00MiB/s");
    assert_eq!(UnitSystem::Si.format_rate(1_000_000.0), "1.00MB/s");
}

#[test]
fn parse_size_iec_and_si() {
    use rusty_pv::units::parse_size;
    assert_eq!(parse_size("1K", UnitSystem::Iec), Some(1024));
    assert_eq!(parse_size("1k", UnitSystem::Si), Some(1000));
    assert_eq!(parse_size("1.5M", UnitSystem::Iec), Some(1_572_864));
    assert_eq!(parse_size("garbage", UnitSystem::Iec), None);
}

#[test]
fn ema_alpha_is_locked_at_three_tenths_sc031() {
    use rusty_pv::ema::EMA_ALPHA;
    assert!(
        (EMA_ALPHA - 0.3).abs() < f64::EPSILON,
        "EMA α MUST be locked at 0.3 — changing it requires a MAJOR semver bump (FR-005)"
    );
}

#[test]
fn pv_error_supports_source_chain() {
    use std::error::Error;
    let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "test");
    let pv_err: PvError = io_err.into();
    assert!(pv_err.source().is_some());
}