1use 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
21pub 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 in_flight: Arc<Mutex<HashSet<usize>>>,
30 backoff_until: Arc<Mutex<HashMap<usize, Instant>>>,
31}
32
33impl EntropyPool {
34 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 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 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 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 pub fn source_count(&self) -> usize {
78 self.sources.len()
79 }
80
81 pub fn collect_all(&self) -> usize {
86 self.collect_all_parallel_n(10.0, 1000)
87 }
88
89 pub fn collect_all_parallel(&self, timeout_secs: f64) -> usize {
94 self.collect_all_parallel_n(timeout_secs, 1000)
95 }
96
97 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 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 {
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 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 pub fn collect_enabled(&self, enabled_names: &[String]) -> usize {
205 self.collect_enabled_n(enabled_names, 1000)
206 }
207
208 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 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 pub fn get_random_bytes(&self, n_bytes: usize) -> Vec<u8> {
310 {
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 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 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 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 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 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 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 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 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 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 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 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
521fn getrandom(buf: &mut [u8]) {
527 getrandom::fill(buf).expect("OS CSPRNG failed");
528}
529
530#[derive(Debug, Clone)]
532pub struct HealthReport {
533 pub healthy: usize,
535 pub total: usize,
537 pub raw_bytes: u64,
539 pub output_bytes: u64,
541 pub buffer_size: usize,
543 pub sources: Vec<SourceHealth>,
545}
546
547#[derive(Debug, Clone)]
549pub struct SourceHealth {
550 pub name: String,
552 pub healthy: bool,
554 pub bytes: u64,
556 pub entropy: f64,
558 pub min_entropy: f64,
560 pub time: f64,
562 pub failures: u64,
564}
565
566#[derive(Debug, Clone)]
568pub struct SourceInfoSnapshot {
569 pub name: String,
571 pub description: String,
573 pub physics: String,
575 pub category: String,
577 pub platform: String,
579 pub requirements: Vec<String>,
581 pub entropy_rate_estimate: f64,
583 pub composite: bool,
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use crate::source::{Platform, SourceCategory, SourceInfo};
591
592 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 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 #[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 #[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 #[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 assert!(bytes.len() <= 16);
796 }
797
798 #[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 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 #[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 #[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 #[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}