Skip to main content

kobo_core/device/
traits.rs

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