use super::*; use std::{ fs, path::PathBuf, sync::atomic::{AtomicBool, AtomicU64, Ordering}, }; #[test] fn percentages_reject_invalid_data_and_include_both_endpoints() { assert_eq!(percentage(0, 100), Some(0)); assert_eq!(percentage(100, 100), Some(100)); assert_eq!(percentage(1, 3), Some(33)); assert_eq!(percentage(u64::MAX, u64::MAX), Some(100)); assert_eq!(percentage(0, 0), None); assert_eq!(percentage(101, 100), None); assert_eq!(windows_charge(0, 4), Some(0)); assert_eq!(windows_charge(100, 8), Some(100)); assert_eq!(windows_charge(101, 0), None); assert_eq!(windows_charge(255, 0), None); assert_eq!(windows_charge(50, 128), None); assert_eq!(windows_charge(50, 255), None); } fn worker(samples: Vec>) -> (Battery, Receiver<()>, Arc) { struct ReaderDropped(Arc); impl Drop for ReaderDropped { fn drop(&mut self) { self.0.store(true, Ordering::Release); } } let dropped = Arc::new(AtomicBool::new(false)); let reader = ReaderDropped(Arc::clone(&dropped)); let mut samples = samples.into_iter(); let (ready, published) = mpsc::channel(); let battery = Battery::start_with( move || { let _ = &reader; samples .next() .expect("test provides every requested sample") }, move |wake| { ready.send(()).unwrap(); // Explicit channel messages replace the production clock entirely. wake.recv().is_ok() }, ) .unwrap(); (battery, published, dropped) } #[test] fn worker_reads_immediately_replaces_stale_samples_and_clears_invalid_data() { let (mut battery, published, dropped) = worker(vec![Some(0), Some(100), None, Some(101)]); published.recv().unwrap(); assert_eq!(battery.poll(), Some(Some(0))); assert_eq!(battery.poll(), None); battery.wake.as_ref().unwrap().send(()).unwrap(); published.recv().unwrap(); battery.wake.as_ref().unwrap().send(()).unwrap(); published.recv().unwrap(); assert_eq!(battery.poll(), Some(None)); assert_eq!(battery.poll(), None); battery.wake.as_ref().unwrap().send(()).unwrap(); published.recv().unwrap(); assert_eq!(battery.poll(), Some(None)); drop(battery); assert!(dropped.load(Ordering::Acquire)); } #[test] fn dropping_interrupts_wait_and_joins_even_with_an_unread_sample() { let (battery, published, dropped) = worker(vec![Some(50)]); published.recv().unwrap(); drop(battery); assert!(dropped.load(Ordering::Acquire)); } static FIXTURE_ID: AtomicU64 = AtomicU64::new(0); struct Fixture(PathBuf); impl Fixture { fn new() -> Self { let path = std::env::temp_dir().join(format!( "vamp-battery-{}-{}", std::process::id(), FIXTURE_ID.fetch_add(1, Ordering::Relaxed) )); fs::create_dir(&path).unwrap(); Self(path) } fn source(&self, name: &str, fields: &[(&str, &str)]) { let path = self.0.join(name); fs::create_dir_all(&path).unwrap(); for (key, value) in fields { fs::write(path.join(key), value).unwrap(); } } fn read(&self) -> Option { linux::read_charge(&self.0) } } impl Drop for Fixture { fn drop(&mut self) { fs::remove_dir_all(&self.0).unwrap(); } } #[test] fn sysfs_reads_present_system_batteries_but_not_external_or_absent_sources() { let fixture = Fixture::new(); assert_eq!(fixture.read(), None); fixture.source("AC", &[("type", "Mains"), ("capacity", "100")]); fixture.source( "mouse", &[ ("type", "Battery"), ("scope", "Device"), ("present", "1"), ("capacity", "100"), ], ); fixture.source( "absent", &[("type", "Battery"), ("present", "0"), ("capacity", "100")], ); assert_eq!(fixture.read(), None); fixture.source( "BAT0", &[ ("type", "Battery\n"), ("scope", "System\n"), ("present", "1\n"), ("capacity", "27\n"), ], ); assert_eq!(fixture.read(), Some(27)); fixture.source("BAT0", &[("capacity", "0")]); assert_eq!(fixture.read(), Some(0)); fixture.source("BAT0", &[("capacity", "100")]); assert_eq!(fixture.read(), Some(100)); for invalid in ["101", "-1", "255", "garbage", ""] { fixture.source("BAT0", &[("capacity", invalid)]); assert_eq!(fixture.read(), None); } fs::remove_file(fixture.0.join("BAT0/capacity")).unwrap(); assert_eq!(fixture.read(), None); } #[test] fn sysfs_uses_matching_actual_energy_or_charge_totals_only() { let energy = Fixture::new(); energy.source( "BAT0", &[ ("type", "Battery"), ("present", "1"), ("energy_now", "27"), ("energy_full", "100"), ], ); assert_eq!(energy.read(), Some(27)); energy.source("BAT0", &[("energy_full", "0")]); assert_eq!(energy.read(), None); energy.source("BAT0", &[("energy_full", "26")]); assert_eq!(energy.read(), None); energy.source("BAT0", &[("energy_full", "garbage")]); assert_eq!(energy.read(), None); let charge = Fixture::new(); charge.source( "BAT0", &[ ("type", "Battery"), ("present", "1"), ("charge_now", "15"), ("charge_full", "20"), ], ); assert_eq!(charge.read(), Some(75)); charge.source("BAT0", &[("energy_now", "5")]); assert_eq!(charge.read(), None); let design = Fixture::new(); design.source( "BAT0", &[ ("type", "Battery"), ("present", "1"), ("energy_now", "20"), ("energy_full_design", "100"), ], ); assert_eq!(design.read(), None); }