Skip to main content

openentropy_core/
pool.rs

1//! Multi-source entropy pool with health monitoring.
2//!
3//! Architecture:
4//! 1. Auto-discover available sources on this machine
5//! 2. Collect raw entropy from each source in parallel
6//! 3. Concatenate source bytes into a shared buffer
7//! 4. Apply conditioning (Raw / VonNeumann / SHA-256) on output
8//! 5. Continuous health monitoring per source
9//! 6. Graceful degradation when sources fail
10//! 7. Thread-safe for concurrent access
11
12use std::collections::{HashMap, HashSet};
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, Instant};
15
16use sha2::{Digest, Sha256};
17
18use crate::conditioning::{quick_min_entropy, quick_shannon};
19use crate::source::{EntropySource, SourceState};
20
21/// Thread-safe multi-source entropy pool.
22pub struct EntropyPool {
23    sources: Vec<Arc<Mutex<SourceState>>>,
24    buffer: Mutex<Vec<u8>>,
25    state: Mutex<[u8; 32]>,
26    counter: Mutex<u64>,
27    total_output: Mutex<u64>,
28    // Per-source collection coordination for timeout-safe parallel collection.
29    in_flight: Arc<Mutex<HashSet<usize>>>,
30    backoff_until: Arc<Mutex<HashMap<usize, Instant>>>,
31}
32
33impl EntropyPool {
34    /// Create an empty pool.
35    pub fn new(seed: Option<&[u8]>) -> Self {
36        let initial_state = {
37            let mut h = Sha256::new();
38            if let Some(s) = seed {
39                h.update(s);
40            } else {
41                // Use OS entropy for initial state
42                let mut os_random = [0u8; 32];
43                getrandom(&mut os_random);
44                h.update(os_random);
45            }
46            let digest: [u8; 32] = h.finalize().into();
47            digest
48        };
49
50        Self {
51            sources: Vec::new(),
52            buffer: Mutex::new(Vec::new()),
53            state: Mutex::new(initial_state),
54            counter: Mutex::new(0),
55            total_output: Mutex::new(0),
56            in_flight: Arc::new(Mutex::new(HashSet::new())),
57            backoff_until: Arc::new(Mutex::new(HashMap::new())),
58        }
59    }
60
61    /// Create a pool with all available sources on this machine.
62    pub fn auto() -> Self {
63        let mut pool = Self::new(None);
64        for source in crate::platform::detect_available_sources() {
65            pool.add_source(source, 1.0);
66        }
67        pool
68    }
69
70    /// Register an entropy source.
71    pub fn add_source(&mut self, source: Box<dyn EntropySource>, weight: f64) {
72        self.sources
73            .push(Arc::new(Mutex::new(SourceState::new(source, weight))));
74    }
75
76    /// Number of registered sources.
77    pub fn source_count(&self) -> usize {
78        self.sources.len()
79    }
80
81    /// Collect entropy from every registered source in parallel.
82    ///
83    /// Uses a 10s collection timeout per cycle. Slow sources are skipped and
84    /// temporarily backed off to keep callers responsive.
85    pub fn collect_all(&self) -> usize {
86        self.collect_all_parallel_n(10.0, 1000)
87    }
88
89    /// Collect entropy from all sources in parallel using detached worker threads.
90    ///
91    /// Slow or hung sources are skipped after `timeout_secs`. Timed-out sources
92    /// enter a backoff window to avoid thread buildup on repeated calls.
93    pub fn collect_all_parallel(&self, timeout_secs: f64) -> usize {
94        self.collect_all_parallel_n(timeout_secs, 1000)
95    }
96
97    /// Collect entropy from all sources in parallel using detached worker threads.
98    ///
99    /// - `timeout_secs`: max wall-clock time to wait for a collection cycle.
100    /// - `n_samples`: samples requested from each source in this cycle.
101    ///
102    /// Slow or hung sources are skipped after `timeout_secs`. Timed-out sources
103    /// enter a backoff window to avoid thread buildup on repeated calls.
104    pub fn collect_all_parallel_n(&self, timeout_secs: f64, n_samples: usize) -> usize {
105        let timeout = Duration::from_secs_f64(timeout_secs.max(0.0));
106        if timeout.is_zero() || n_samples == 0 {
107            return 0;
108        }
109
110        let (tx, rx) = std::sync::mpsc::channel::<(usize, Vec<u8>)>();
111        let now = Instant::now();
112        let mut scheduled: Vec<usize> = Vec::new();
113
114        for (idx, ss_mutex) in self.sources.iter().enumerate() {
115            // Skip sources still in backoff.
116            let in_backoff = {
117                let backoff = self.backoff_until.lock().unwrap();
118                backoff.get(&idx).is_some_and(|until| now < *until)
119            };
120            if in_backoff {
121                continue;
122            }
123
124            // Skip sources with an in-flight worker from a prior timeout.
125            {
126                let mut in_flight = self.in_flight.lock().unwrap();
127                if in_flight.contains(&idx) {
128                    continue;
129                }
130                in_flight.insert(idx);
131            }
132
133            scheduled.push(idx);
134
135            let tx = tx.clone();
136            let src = Arc::clone(ss_mutex);
137            let in_flight = Arc::clone(&self.in_flight);
138            let backoff = Arc::clone(&self.backoff_until);
139
140            std::thread::spawn(move || {
141                let data = Self::collect_one_n(&src, n_samples);
142                {
143                    let mut in_flight = in_flight.lock().unwrap();
144                    in_flight.remove(&idx);
145                }
146                let mut bo = backoff.lock().unwrap();
147                bo.remove(&idx);
148                let _ = tx.send((idx, data));
149            });
150        }
151        drop(tx);
152
153        if scheduled.is_empty() {
154            return 0;
155        }
156
157        let deadline = Instant::now() + timeout;
158        let mut received = HashSet::new();
159        let mut results = Vec::new();
160
161        while received.len() < scheduled.len() {
162            let remaining = deadline.saturating_duration_since(Instant::now());
163            if remaining.is_zero() {
164                break;
165            }
166            match rx.recv_timeout(remaining) {
167                Ok((idx, data)) => {
168                    received.insert(idx);
169                    if !data.is_empty() {
170                        results.extend_from_slice(&data);
171                    }
172                }
173                Err(std::sync::mpsc::RecvTimeoutError::Timeout) => break,
174                Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
175            }
176        }
177
178        // Back off any sources that did not respond in time.
179        let backoff_for = Duration::from_secs(30);
180        let timeout_mark = Instant::now() + backoff_for;
181        for idx in scheduled {
182            if received.contains(&idx) {
183                continue;
184            }
185
186            {
187                let mut bo = self.backoff_until.lock().unwrap();
188                bo.insert(idx, timeout_mark);
189            }
190
191            if let Ok(mut ss) = self.sources[idx].try_lock() {
192                ss.failures += 1;
193                ss.healthy = false;
194            }
195        }
196
197        let n = results.len();
198        self.buffer.lock().unwrap().extend_from_slice(&results);
199        n
200    }
201
202    /// Collect entropy only from sources whose names are in the given list.
203    /// Uses parallel threads. Collects 1000 samples per source.
204    pub fn collect_enabled(&self, enabled_names: &[String]) -> usize {
205        self.collect_enabled_n(enabled_names, 1000)
206    }
207
208    /// Collect `n_samples` of entropy from sources whose names are in the list.
209    /// Smaller `n_samples` values are faster — use this for interactive/TUI contexts.
210    pub fn collect_enabled_n(&self, enabled_names: &[String], n_samples: usize) -> usize {
211        use std::sync::Arc;
212        let results: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
213
214        std::thread::scope(|s| {
215            let handles: Vec<_> = self
216                .sources
217                .iter()
218                .filter(|ss_mutex| {
219                    let ss = ss_mutex.lock().unwrap();
220                    enabled_names.iter().any(|n| n == ss.source.info().name)
221                })
222                .map(|ss_mutex| {
223                    let results = Arc::clone(&results);
224                    s.spawn(move || {
225                        let data = Self::collect_one_n(ss_mutex, n_samples);
226                        if !data.is_empty() {
227                            results.lock().unwrap().extend_from_slice(&data);
228                        }
229                    })
230                })
231                .collect();
232
233            for handle in handles {
234                let _ = handle.join();
235            }
236        });
237
238        let results = Arc::try_unwrap(results).unwrap().into_inner().unwrap();
239        let n = results.len();
240        self.buffer.lock().unwrap().extend_from_slice(&results);
241        n
242    }
243
244    fn collect_one_n(ss_mutex: &Arc<Mutex<SourceState>>, n_samples: usize) -> Vec<u8> {
245        let mut ss = ss_mutex.lock().unwrap();
246        let t0 = Instant::now();
247        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
248            ss.source.collect(n_samples)
249        })) {
250            Ok(data) if !data.is_empty() => {
251                ss.last_collect_time = t0.elapsed();
252                ss.total_bytes += data.len() as u64;
253                ss.last_entropy = quick_shannon(&data);
254                ss.last_min_entropy = quick_min_entropy(&data);
255                ss.healthy = ss.last_entropy > 1.0;
256                data
257            }
258            Ok(_) => {
259                ss.last_collect_time = t0.elapsed();
260                ss.failures += 1;
261                ss.healthy = false;
262                Vec::new()
263            }
264            Err(_) => {
265                ss.last_collect_time = t0.elapsed();
266                ss.failures += 1;
267                ss.healthy = false;
268                Vec::new()
269            }
270        }
271    }
272
273    /// Return up to `n_bytes` of raw, unconditioned entropy (XOR-combined only).
274    ///
275    /// No SHA-256, no DRBG, no whitening. Preserves the raw hardware noise
276    /// signal for researchers studying actual device entropy characteristics.
277    ///
278    /// If sources cannot provide enough bytes after several collection rounds,
279    /// this returns the available bytes rather than blocking indefinitely.
280    pub fn get_raw_bytes(&self, n_bytes: usize) -> Vec<u8> {
281        const MAX_COLLECTION_ROUNDS: usize = 8;
282
283        let mut rounds = 0usize;
284        loop {
285            let ready = { self.buffer.lock().unwrap().len() >= n_bytes };
286            if ready || rounds >= MAX_COLLECTION_ROUNDS {
287                break;
288            }
289
290            let n = self.collect_all();
291            rounds += 1;
292            if n == 0 {
293                std::thread::sleep(Duration::from_millis(1));
294            }
295        }
296
297        let mut buf = self.buffer.lock().unwrap();
298        let take = n_bytes.min(buf.len());
299        if take == 0 {
300            return Vec::new();
301        }
302        let output: Vec<u8> = buf.drain(..take).collect();
303        drop(buf);
304        *self.total_output.lock().unwrap() += take as u64;
305        output
306    }
307
308    /// Return `n_bytes` of conditioned random output.
309    pub fn get_random_bytes(&self, n_bytes: usize) -> Vec<u8> {
310        // Auto-collect if buffer is low
311        {
312            let buf = self.buffer.lock().unwrap();
313            if buf.len() < n_bytes * 2 {
314                drop(buf);
315                self.collect_all();
316            }
317        }
318
319        let mut output = Vec::with_capacity(n_bytes);
320        while output.len() < n_bytes {
321            let mut counter = self.counter.lock().unwrap();
322            *counter += 1;
323            let cnt = *counter;
324            drop(counter);
325
326            // Take up to 256 bytes from buffer
327            let sample = {
328                let mut buf = self.buffer.lock().unwrap();
329                let take = buf.len().min(256);
330                let sample: Vec<u8> = buf.drain(..take).collect();
331                sample
332            };
333
334            // SHA-256 conditioning
335            let mut h = Sha256::new();
336            let state = self.state.lock().unwrap();
337            h.update(*state);
338            drop(state);
339            h.update(&sample);
340            h.update(cnt.to_le_bytes());
341
342            let ts = std::time::SystemTime::now()
343                .duration_since(std::time::UNIX_EPOCH)
344                .unwrap_or_default();
345            h.update(ts.as_nanos().to_le_bytes());
346
347            // Mix in OS entropy as safety net
348            let mut os_random = [0u8; 8];
349            getrandom(&mut os_random);
350            h.update(os_random);
351
352            let digest: [u8; 32] = h.finalize().into();
353            *self.state.lock().unwrap() = digest;
354            output.extend_from_slice(&digest);
355        }
356
357        *self.total_output.lock().unwrap() += n_bytes as u64;
358        output.truncate(n_bytes);
359        output
360    }
361
362    /// Return `n_bytes` of entropy with the specified conditioning mode.
363    ///
364    /// - `Raw`: XOR-combined source bytes, no whitening
365    /// - `VonNeumann`: debiased but structure-preserving
366    /// - `Sha256`: full cryptographic conditioning (default)
367    pub fn get_bytes(
368        &self,
369        n_bytes: usize,
370        mode: crate::conditioning::ConditioningMode,
371    ) -> Vec<u8> {
372        use crate::conditioning::ConditioningMode;
373        match mode {
374            ConditioningMode::Raw => self.get_raw_bytes(n_bytes),
375            ConditioningMode::VonNeumann => {
376                // VN debiasing yields ~25% of input, so collect 6x
377                let raw = self.get_raw_bytes(n_bytes * 6);
378                crate::conditioning::condition(&raw, n_bytes, ConditioningMode::VonNeumann)
379            }
380            ConditioningMode::Sha256 => self.get_random_bytes(n_bytes),
381        }
382    }
383
384    /// Health report as structured data.
385    pub fn health_report(&self) -> HealthReport {
386        let mut sources = Vec::new();
387        let mut healthy_count = 0;
388        let mut total_raw = 0u64;
389
390        for ss_mutex in &self.sources {
391            let ss = ss_mutex.lock().unwrap();
392            if ss.healthy {
393                healthy_count += 1;
394            }
395            total_raw += ss.total_bytes;
396            sources.push(SourceHealth {
397                name: ss.source.name().to_string(),
398                healthy: ss.healthy,
399                bytes: ss.total_bytes,
400                entropy: ss.last_entropy,
401                min_entropy: ss.last_min_entropy,
402                time: ss.last_collect_time.as_secs_f64(),
403                failures: ss.failures,
404            });
405        }
406
407        HealthReport {
408            healthy: healthy_count,
409            total: self.sources.len(),
410            raw_bytes: total_raw,
411            output_bytes: *self.total_output.lock().unwrap(),
412            buffer_size: self.buffer.lock().unwrap().len(),
413            sources,
414        }
415    }
416
417    /// Pretty-print health report.
418    pub fn print_health(&self) {
419        let r = self.health_report();
420        println!("\n{}", "=".repeat(60));
421        println!("ENTROPY POOL HEALTH REPORT");
422        println!("{}", "=".repeat(60));
423        println!("Sources: {}/{} healthy", r.healthy, r.total);
424        println!("Raw collected: {} bytes", r.raw_bytes);
425        println!(
426            "Output: {} bytes | Buffer: {} bytes",
427            r.output_bytes, r.buffer_size
428        );
429        println!(
430            "\n{:<25} {:>4} {:>10} {:>6} {:>6} {:>7} {:>5}",
431            "Source", "OK", "Bytes", "H", "H∞", "Time", "Fail"
432        );
433        println!("{}", "-".repeat(68));
434        for s in &r.sources {
435            let ok = if s.healthy { "✓" } else { "✗" };
436            println!(
437                "{:<25} {:>4} {:>10} {:>5.2} {:>5.2} {:>6.3}s {:>5}",
438                s.name, ok, s.bytes, s.entropy, s.min_entropy, s.time, s.failures
439            );
440        }
441    }
442
443    /// Collect entropy from a single named source and return conditioned bytes.
444    ///
445    /// Returns `None` if the source name doesn't match any registered source.
446    pub fn get_source_bytes(
447        &self,
448        source_name: &str,
449        n_bytes: usize,
450        mode: crate::conditioning::ConditioningMode,
451    ) -> Option<Vec<u8>> {
452        if n_bytes == 0 {
453            return Some(Vec::new());
454        }
455
456        let ss_mutex = self
457            .sources
458            .iter()
459            .find(|ss_mutex| {
460                let ss = ss_mutex.lock().unwrap();
461                ss.source.info().name == source_name
462            })
463            .cloned()?;
464
465        let n_samples = match mode {
466            crate::conditioning::ConditioningMode::Raw => n_bytes,
467            crate::conditioning::ConditioningMode::VonNeumann => n_bytes * 6,
468            crate::conditioning::ConditioningMode::Sha256 => n_bytes * 4 + 64,
469        };
470        let raw = Self::collect_one_n(&ss_mutex, n_samples);
471        let output = crate::conditioning::condition(&raw, n_bytes, mode);
472        Some(output)
473    }
474
475    /// Collect raw bytes from a single named source.
476    ///
477    /// Returns `None` if no source matches the name.
478    pub fn get_source_raw_bytes(&self, source_name: &str, n_samples: usize) -> Option<Vec<u8>> {
479        let ss_mutex = self.sources.iter().find(|ss_mutex| {
480            let ss = ss_mutex.lock().unwrap();
481            ss.source.info().name == source_name
482        })?;
483
484        let raw = Self::collect_one_n(ss_mutex, n_samples);
485        Some(raw)
486    }
487
488    /// List all registered source names.
489    pub fn source_names(&self) -> Vec<String> {
490        self.sources
491            .iter()
492            .map(|ss_mutex| {
493                let ss = ss_mutex.lock().unwrap();
494                ss.source.info().name.to_string()
495            })
496            .collect()
497    }
498
499    /// Get source info for each registered source.
500    pub fn source_infos(&self) -> Vec<SourceInfoSnapshot> {
501        self.sources
502            .iter()
503            .map(|ss_mutex| {
504                let ss = ss_mutex.lock().unwrap();
505                let info = ss.source.info();
506                SourceInfoSnapshot {
507                    name: info.name.to_string(),
508                    description: info.description.to_string(),
509                    physics: info.physics.to_string(),
510                    category: info.category.to_string(),
511                    platform: info.platform.to_string(),
512                    requirements: info.requirements.iter().map(|r| r.to_string()).collect(),
513                    entropy_rate_estimate: info.entropy_rate_estimate,
514                    composite: info.composite,
515                }
516            })
517            .collect()
518    }
519}
520
521/// Fill buffer with OS random bytes via the `getrandom` crate.
522/// Works cross-platform (Unix, Windows, WASM, etc.) without manual file I/O.
523///
524/// # Panics
525/// Panics if the OS CSPRNG fails — this indicates a fatal platform issue.
526fn getrandom(buf: &mut [u8]) {
527    getrandom::fill(buf).expect("OS CSPRNG failed");
528}
529
530/// Overall health report for the entropy pool.
531#[derive(Debug, Clone)]
532pub struct HealthReport {
533    /// Number of healthy sources.
534    pub healthy: usize,
535    /// Total number of registered sources.
536    pub total: usize,
537    /// Total raw bytes collected across all sources.
538    pub raw_bytes: u64,
539    /// Total conditioned output bytes produced.
540    pub output_bytes: u64,
541    /// Current internal buffer size in bytes.
542    pub buffer_size: usize,
543    /// Per-source health details.
544    pub sources: Vec<SourceHealth>,
545}
546
547/// Health status of a single entropy source.
548#[derive(Debug, Clone)]
549pub struct SourceHealth {
550    /// Source name.
551    pub name: String,
552    /// Whether the source is currently healthy (entropy > 1.0 bits/byte).
553    pub healthy: bool,
554    /// Total bytes collected from this source.
555    pub bytes: u64,
556    /// Shannon entropy of the last collection (bits per byte, max 8.0).
557    pub entropy: f64,
558    /// Min-entropy of the last collection (bits per byte, max 8.0). More conservative than Shannon.
559    pub min_entropy: f64,
560    /// Time taken for the last collection in seconds.
561    pub time: f64,
562    /// Number of collection failures.
563    pub failures: u64,
564}
565
566/// Snapshot of source metadata for external consumption.
567#[derive(Debug, Clone)]
568pub struct SourceInfoSnapshot {
569    /// Source name.
570    pub name: String,
571    /// Human-readable description.
572    pub description: String,
573    /// Physics explanation.
574    pub physics: String,
575    /// Source category.
576    pub category: String,
577    /// Target platform.
578    pub platform: String,
579    /// Hardware/software requirements.
580    pub requirements: Vec<String>,
581    /// Estimated entropy rate.
582    pub entropy_rate_estimate: f64,
583    /// Whether this is a composite source.
584    pub composite: bool,
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use crate::source::{Platform, SourceCategory, SourceInfo};
591
592    // -----------------------------------------------------------------------
593    // Mock entropy source for testing
594    // -----------------------------------------------------------------------
595
596    /// A deterministic mock entropy source that returns predictable data.
597    struct MockSource {
598        info: SourceInfo,
599        data: Vec<u8>,
600    }
601
602    impl MockSource {
603        fn new(name: &'static str, data: Vec<u8>) -> Self {
604            Self {
605                info: SourceInfo {
606                    name,
607                    description: "mock source",
608                    physics: "deterministic test data",
609                    category: SourceCategory::System,
610                    platform: Platform::Any,
611                    requirements: &[],
612                    entropy_rate_estimate: 1.0,
613                    composite: false,
614                },
615                data,
616            }
617        }
618    }
619
620    impl EntropySource for MockSource {
621        fn info(&self) -> &SourceInfo {
622            &self.info
623        }
624        fn is_available(&self) -> bool {
625            true
626        }
627        fn collect(&self, n_samples: usize) -> Vec<u8> {
628            self.data.iter().copied().cycle().take(n_samples).collect()
629        }
630    }
631
632    /// A mock source that always fails (returns empty).
633    struct FailingSource {
634        info: SourceInfo,
635    }
636
637    impl FailingSource {
638        fn new(name: &'static str) -> Self {
639            Self {
640                info: SourceInfo {
641                    name,
642                    description: "failing mock",
643                    physics: "always fails",
644                    category: SourceCategory::System,
645                    platform: Platform::Any,
646                    requirements: &[],
647                    entropy_rate_estimate: 0.0,
648                    composite: false,
649                },
650            }
651        }
652    }
653
654    impl EntropySource for FailingSource {
655        fn info(&self) -> &SourceInfo {
656            &self.info
657        }
658        fn is_available(&self) -> bool {
659            true
660        }
661        fn collect(&self, _n_samples: usize) -> Vec<u8> {
662            Vec::new()
663        }
664    }
665
666    // -----------------------------------------------------------------------
667    // Pool creation tests
668    // -----------------------------------------------------------------------
669
670    #[test]
671    fn test_pool_new_empty() {
672        let pool = EntropyPool::new(None);
673        assert_eq!(pool.source_count(), 0);
674    }
675
676    #[test]
677    fn test_pool_new_with_seed() {
678        let pool = EntropyPool::new(Some(b"test seed"));
679        assert_eq!(pool.source_count(), 0);
680    }
681
682    #[test]
683    fn test_pool_add_source() {
684        let mut pool = EntropyPool::new(Some(b"test"));
685        pool.add_source(Box::new(MockSource::new("mock1", vec![42])), 1.0);
686        assert_eq!(pool.source_count(), 1);
687    }
688
689    #[test]
690    fn test_pool_add_multiple_sources() {
691        let mut pool = EntropyPool::new(Some(b"test"));
692        pool.add_source(Box::new(MockSource::new("mock1", vec![1])), 1.0);
693        pool.add_source(Box::new(MockSource::new("mock2", vec![2])), 1.0);
694        pool.add_source(Box::new(MockSource::new("mock3", vec![3])), 0.5);
695        assert_eq!(pool.source_count(), 3);
696    }
697
698    // -----------------------------------------------------------------------
699    // Collection tests
700    // -----------------------------------------------------------------------
701
702    #[test]
703    fn test_collect_all_returns_bytes() {
704        let mut pool = EntropyPool::new(Some(b"test"));
705        pool.add_source(
706            Box::new(MockSource::new("mock1", vec![0xAA, 0xBB, 0xCC])),
707            1.0,
708        );
709        let n = pool.collect_all();
710        assert!(n > 0, "Should have collected some bytes");
711    }
712
713    #[test]
714    fn test_collect_all_parallel_with_timeout() {
715        let mut pool = EntropyPool::new(Some(b"test"));
716        pool.add_source(Box::new(MockSource::new("mock1", vec![1, 2])), 1.0);
717        pool.add_source(Box::new(MockSource::new("mock2", vec![3, 4])), 1.0);
718        let n = pool.collect_all_parallel(5.0);
719        assert!(n > 0);
720    }
721
722    #[test]
723    fn test_collect_enabled_filters_sources() {
724        let mut pool = EntropyPool::new(Some(b"test"));
725        pool.add_source(Box::new(MockSource::new("alpha", vec![1])), 1.0);
726        pool.add_source(Box::new(MockSource::new("beta", vec![2])), 1.0);
727
728        let enabled = vec!["alpha".to_string()];
729        let n = pool.collect_enabled(&enabled);
730        assert!(n > 0, "Should collect from enabled source");
731    }
732
733    #[test]
734    fn test_collect_enabled_no_match() {
735        let mut pool = EntropyPool::new(Some(b"test"));
736        pool.add_source(Box::new(MockSource::new("alpha", vec![1])), 1.0);
737
738        let enabled = vec!["nonexistent".to_string()];
739        let n = pool.collect_enabled(&enabled);
740        assert_eq!(n, 0, "No sources should match");
741    }
742
743    // -----------------------------------------------------------------------
744    // Byte output tests
745    // -----------------------------------------------------------------------
746
747    #[test]
748    fn test_get_raw_bytes_length() {
749        let mut pool = EntropyPool::new(Some(b"test"));
750        pool.add_source(Box::new(MockSource::new("mock", (0..=255).collect())), 1.0);
751        let bytes = pool.get_raw_bytes(64);
752        assert_eq!(bytes.len(), 64);
753    }
754
755    #[test]
756    fn test_get_random_bytes_length() {
757        let mut pool = EntropyPool::new(Some(b"test"));
758        pool.add_source(Box::new(MockSource::new("mock", (0..=255).collect())), 1.0);
759        let bytes = pool.get_random_bytes(64);
760        assert_eq!(bytes.len(), 64);
761    }
762
763    #[test]
764    fn test_get_random_bytes_various_sizes() {
765        let mut pool = EntropyPool::new(Some(b"test"));
766        pool.add_source(Box::new(MockSource::new("mock", (0..=255).collect())), 1.0);
767        for size in [1, 16, 32, 64, 100, 256] {
768            let bytes = pool.get_random_bytes(size);
769            assert_eq!(bytes.len(), size, "Expected {size} bytes");
770        }
771    }
772
773    #[test]
774    fn test_get_bytes_raw_mode() {
775        let mut pool = EntropyPool::new(Some(b"test"));
776        pool.add_source(Box::new(MockSource::new("mock", (0..=255).collect())), 1.0);
777        let bytes = pool.get_bytes(32, crate::conditioning::ConditioningMode::Raw);
778        assert_eq!(bytes.len(), 32);
779    }
780
781    #[test]
782    fn test_get_bytes_sha256_mode() {
783        let mut pool = EntropyPool::new(Some(b"test"));
784        pool.add_source(Box::new(MockSource::new("mock", (0..=255).collect())), 1.0);
785        let bytes = pool.get_bytes(32, crate::conditioning::ConditioningMode::Sha256);
786        assert_eq!(bytes.len(), 32);
787    }
788
789    #[test]
790    fn test_get_bytes_von_neumann_mode() {
791        let mut pool = EntropyPool::new(Some(b"test"));
792        pool.add_source(Box::new(MockSource::new("mock", (0..=255).collect())), 1.0);
793        let bytes = pool.get_bytes(16, crate::conditioning::ConditioningMode::VonNeumann);
794        // VonNeumann may produce fewer bytes due to debiasing yield
795        assert!(bytes.len() <= 16);
796    }
797
798    // -----------------------------------------------------------------------
799    // Health report tests
800    // -----------------------------------------------------------------------
801
802    #[test]
803    fn test_health_report_empty_pool() {
804        let pool = EntropyPool::new(Some(b"test"));
805        let report = pool.health_report();
806        assert_eq!(report.total, 0);
807        assert_eq!(report.healthy, 0);
808        assert_eq!(report.raw_bytes, 0);
809        assert_eq!(report.output_bytes, 0);
810        assert_eq!(report.buffer_size, 0);
811        assert!(report.sources.is_empty());
812    }
813
814    #[test]
815    fn test_health_report_after_collection() {
816        let mut pool = EntropyPool::new(Some(b"test"));
817        pool.add_source(
818            Box::new(MockSource::new("good_source", (0..=255).collect())),
819            1.0,
820        );
821        pool.collect_all();
822        let report = pool.health_report();
823        assert_eq!(report.total, 1);
824        assert!(report.raw_bytes > 0);
825        assert_eq!(report.sources.len(), 1);
826        assert_eq!(report.sources[0].name, "good_source");
827        assert!(report.sources[0].bytes > 0);
828    }
829
830    #[test]
831    fn test_health_report_failing_source() {
832        let mut pool = EntropyPool::new(Some(b"test"));
833        pool.add_source(Box::new(FailingSource::new("bad_source")), 1.0);
834        pool.collect_all();
835        let report = pool.health_report();
836        assert_eq!(report.total, 1);
837        assert_eq!(report.healthy, 0);
838        assert!(!report.sources[0].healthy);
839        assert_eq!(report.sources[0].failures, 1);
840    }
841
842    #[test]
843    fn test_health_report_mixed_sources() {
844        let mut pool = EntropyPool::new(Some(b"test"));
845        pool.add_source(Box::new(MockSource::new("good", (0..=255).collect())), 1.0);
846        pool.add_source(Box::new(FailingSource::new("bad")), 1.0);
847        pool.collect_all();
848        let report = pool.health_report();
849        assert_eq!(report.total, 2);
850        // The good source should be healthy if its entropy > 1.0
851        assert!(report.healthy >= 1);
852        assert_eq!(report.sources.len(), 2);
853    }
854
855    #[test]
856    fn test_health_report_tracks_output_bytes() {
857        let mut pool = EntropyPool::new(Some(b"test"));
858        pool.add_source(Box::new(MockSource::new("mock", (0..=255).collect())), 1.0);
859        let _ = pool.get_random_bytes(64);
860        let report = pool.health_report();
861        assert!(report.output_bytes >= 64);
862    }
863
864    // -----------------------------------------------------------------------
865    // Source info snapshot tests
866    // -----------------------------------------------------------------------
867
868    #[test]
869    fn test_source_infos_empty() {
870        let pool = EntropyPool::new(Some(b"test"));
871        let infos = pool.source_infos();
872        assert!(infos.is_empty());
873    }
874
875    #[test]
876    fn test_source_infos_populated() {
877        let mut pool = EntropyPool::new(Some(b"test"));
878        pool.add_source(Box::new(MockSource::new("test_src", vec![1])), 1.0);
879        let infos = pool.source_infos();
880        assert_eq!(infos.len(), 1);
881        assert_eq!(infos[0].name, "test_src");
882        assert_eq!(infos[0].description, "mock source");
883        assert_eq!(infos[0].category, "system");
884        assert!((infos[0].entropy_rate_estimate - 1.0).abs() < f64::EPSILON);
885    }
886
887    // -----------------------------------------------------------------------
888    // Determinism / seed tests
889    // -----------------------------------------------------------------------
890
891    #[test]
892    fn test_different_seeds_differ() {
893        let mut pool1 = EntropyPool::new(Some(b"seed_a"));
894        pool1.add_source(Box::new(MockSource::new("m", vec![42; 100])), 1.0);
895        let mut pool2 = EntropyPool::new(Some(b"seed_b"));
896        pool2.add_source(Box::new(MockSource::new("m", vec![42; 100])), 1.0);
897
898        let bytes1 = pool1.get_random_bytes(32);
899        let bytes2 = pool2.get_random_bytes(32);
900        assert_ne!(
901            bytes1, bytes2,
902            "Different seeds should produce different output"
903        );
904    }
905
906    // -----------------------------------------------------------------------
907    // Edge case tests
908    // -----------------------------------------------------------------------
909
910    #[test]
911    fn test_collect_from_empty_pool() {
912        let pool = EntropyPool::new(Some(b"test"));
913        let n = pool.collect_all();
914        assert_eq!(n, 0, "Empty pool should collect 0 bytes");
915    }
916
917    #[test]
918    fn test_collect_enabled_empty_list() {
919        let mut pool = EntropyPool::new(Some(b"test"));
920        pool.add_source(Box::new(MockSource::new("mock", vec![1])), 1.0);
921        let n = pool.collect_enabled(&[]);
922        assert_eq!(n, 0);
923    }
924}