Skip to main content

kobo_core/device/
traits.rs

1//! Device trait surface for testability.
2//!
3//! Each device subsystem gets a narrow trait so that consumer apps can inject
4//! Mock impls for desktop unit tests instead of hitting real sysfs/ioctl.
5//!
6//! - [`Frontlight`] - brightness get/set/restore
7//! - [`Battery`] - capacity percentage
8//! - [`Wifi`] - link status, SSID, toggle
9//! - [`Bluetooth`] - connection status, name, toggle
10//! - [`SystemControl`] - suspend, wall-clock
11//! - [`Framebuffer`] - resolution, present
12//!
13//! Sys impls (production wrappers around the free functions) live in
14//! [`crate::device::impls`].
15
16use std::cell::RefCell;
17use std::path::PathBuf;
18
19// ---------------------------------------------------------------------------
20// Frontlight
21// ---------------------------------------------------------------------------
22
23pub trait Frontlight {
24    fn get(&self) -> u32;
25    fn set(&self, brightness: u32);
26    fn restore(&self, brightness: u32);
27}
28
29/// Scripted frontlight mock: records every `set` call and returns scripted
30/// `get` values (repeating the last once exhausted). Use for deterministic
31/// retry-loop testing.
32pub struct MockFrontlight {
33    pub sets: RefCell<Vec<u32>>,
34    pub scripted_gets: Vec<u32>,
35    get_idx: std::cell::Cell<usize>,
36}
37
38impl MockFrontlight {
39    pub fn new(gets: Vec<u32>) -> Self {
40        MockFrontlight {
41            sets: RefCell::new(Vec::new()),
42            scripted_gets: gets,
43            get_idx: std::cell::Cell::new(0),
44        }
45    }
46
47    pub fn last_set(&self) -> Option<u32> {
48        self.sets.borrow().last().copied()
49    }
50
51    pub fn set_count(&self) -> usize {
52        self.sets.borrow().len()
53    }
54}
55
56impl Frontlight for MockFrontlight {
57    fn get(&self) -> u32 {
58        let i = self.get_idx.get();
59        let v = self
60            .scripted_gets
61            .get(i)
62            .copied()
63            .unwrap_or_else(|| self.scripted_gets.last().copied().unwrap_or(0));
64        self.get_idx.set(i + 1);
65        v
66    }
67
68    fn set(&self, brightness: u32) {
69        self.sets.borrow_mut().push(brightness);
70    }
71
72    fn restore(&self, brightness: u32) {
73        self.set(brightness);
74    }
75}
76
77// ---------------------------------------------------------------------------
78// Battery
79// ---------------------------------------------------------------------------
80
81pub trait Battery {
82    fn pct(&self) -> i32;
83    fn is_charging(&self) -> bool {
84        false
85    }
86}
87
88#[derive(Debug, Clone, Default)]
89pub struct MockBattery {
90    pub pct: i32,
91    pub charging: bool,
92}
93
94impl Battery for MockBattery {
95    fn pct(&self) -> i32 {
96        self.pct
97    }
98    fn is_charging(&self) -> bool {
99        self.charging
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Wifi
105// ---------------------------------------------------------------------------
106
107pub trait Wifi {
108    fn connected(&self) -> bool;
109    fn ssid(&self) -> Option<String>;
110    fn toggle(&self, on: bool);
111}
112
113#[derive(Debug, Clone, Default)]
114pub struct MockWifi {
115    pub connected: bool,
116    pub ssid: Option<String>,
117}
118
119impl Wifi for MockWifi {
120    fn connected(&self) -> bool {
121        self.connected
122    }
123    fn ssid(&self) -> Option<String> {
124        self.ssid.clone()
125    }
126    fn toggle(&self, on: bool) {
127        let _ = on;
128    }
129}
130
131// ---------------------------------------------------------------------------
132// Bluetooth
133// ---------------------------------------------------------------------------
134
135pub trait Bluetooth {
136    fn connected(&self) -> bool;
137    fn name(&self) -> Option<String>;
138    fn toggle(&self, on: bool);
139}
140
141#[derive(Debug, Clone, Default)]
142pub struct MockBluetooth {
143    pub connected: bool,
144    pub name: Option<String>,
145}
146
147impl Bluetooth for MockBluetooth {
148    fn connected(&self) -> bool {
149        self.connected
150    }
151    fn name(&self) -> Option<String> {
152        self.name.clone()
153    }
154    fn toggle(&self, on: bool) {
155        let _ = on;
156    }
157}
158
159// ---------------------------------------------------------------------------
160// SystemControl
161// ---------------------------------------------------------------------------
162
163pub trait SystemControl {
164    fn suspend(&self, state: &str);
165    fn clock(&self) -> String;
166}
167
168#[derive(Debug, Clone, Default)]
169pub struct MockSystemControl {
170    pub clock: String,
171}
172
173impl SystemControl for MockSystemControl {
174    fn suspend(&self, _state: &str) {}
175    fn clock(&self) -> String {
176        if self.clock.is_empty() {
177            "--:--".to_string()
178        } else {
179            self.clock.clone()
180        }
181    }
182}
183
184// ---------------------------------------------------------------------------
185// Framebuffer
186// ---------------------------------------------------------------------------
187
188pub trait Framebuffer {
189    fn resolution(&self) -> (usize, usize);
190    fn present(
191        &self,
192        buf: &[u8],
193        w: usize,
194        h: usize,
195        full: bool,
196        top: usize,
197        rh: usize,
198        waveform: u32,
199    );
200}
201
202pub struct MockFramebuffer {
203    pub width: usize,
204    pub height: usize,
205    pub present_count: std::cell::Cell<usize>,
206}
207
208impl MockFramebuffer {
209    pub fn new(width: usize, height: usize) -> Self {
210        MockFramebuffer {
211            width,
212            height,
213            present_count: std::cell::Cell::new(0),
214        }
215    }
216}
217
218impl Framebuffer for MockFramebuffer {
219    fn resolution(&self) -> (usize, usize) {
220        (self.width, self.height)
221    }
222    fn present(
223        &self,
224        _buf: &[u8],
225        _w: usize,
226        _h: usize,
227        _full: bool,
228        _top: usize,
229        _rh: usize,
230        _waveform: u32,
231    ) {
232        self.present_count.set(self.present_count.get() + 1);
233    }
234}
235
236// ---------------------------------------------------------------------------
237// Re-export the brightness path type for Sys impl constructors
238// ---------------------------------------------------------------------------
239
240/// Frontlight sysfs paths resolved from a [`crate::device::config::FrontlightConfig`].
241pub struct FrontlightPaths {
242    pub brightness: PathBuf,
243    pub mixer: Option<PathBuf>,
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn mock_battery_round_trip() {
252        let b = MockBattery {
253            pct: 73,
254            charging: true,
255        };
256        assert_eq!(b.pct(), 73);
257        assert!(b.is_charging());
258    }
259
260    #[test]
261    fn mock_frontlight_scripts_gets() {
262        let fl = MockFrontlight::new(vec![0, 0, 60]);
263        assert_eq!(fl.get(), 0);
264        assert_eq!(fl.get(), 0);
265        fl.set(60);
266        assert_eq!(fl.get(), 60);
267        assert_eq!(fl.set_count(), 1);
268        assert_eq!(fl.last_set(), Some(60));
269    }
270
271    #[test]
272    fn mock_frontlight_repeats_last_get() {
273        let fl = MockFrontlight::new(vec![42]);
274        assert_eq!(fl.get(), 42);
275        assert_eq!(fl.get(), 42);
276    }
277
278    #[test]
279    fn mock_wifi_and_bt() {
280        let w = MockWifi {
281            connected: true,
282            ssid: Some("HomeNet".into()),
283        };
284        assert!(w.connected());
285        assert_eq!(w.ssid().as_deref(), Some("HomeNet"));
286
287        let bt = MockBluetooth {
288            connected: false,
289            name: None,
290        };
291        assert!(!bt.connected());
292        assert!(bt.name().is_none());
293    }
294
295    #[test]
296    fn mock_system_clock() {
297        let sc = MockSystemControl {
298            clock: "9:41 PM".into(),
299        };
300        assert_eq!(sc.clock(), "9:41 PM");
301        assert_eq!(MockSystemControl::default().clock(), "--:--");
302    }
303
304    #[test]
305    fn mock_framebuffer_counts_presents() {
306        let fb = MockFramebuffer::new(100, 200);
307        assert_eq!(fb.resolution(), (100, 200));
308        fb.present(&[], 100, 200, true, 0, 200, 2);
309        fb.present(&[], 100, 200, false, 0, 100, 2);
310        assert_eq!(fb.present_count.get(), 2);
311    }
312}