geop_core_program/
watch.rs1use std::cell::RefCell;
15
16use geop_core_math::geop_error::{GeopError, GeopResult};
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct Progress<'a> {
21 pub what: &'a str,
23 pub done: usize,
24 pub total: usize,
25}
26
27type Watcher = Box<dyn FnMut(&Progress) -> bool>;
28
29thread_local! {
30 static WATCHER: RefCell<Option<Watcher>> = const { RefCell::new(None) };
31}
32
33pub fn watching<T>(watcher: impl FnMut(&Progress) -> bool + 'static, f: impl FnOnce() -> T) -> T {
35 struct Unset(Option<Watcher>);
37 impl Drop for Unset {
38 fn drop(&mut self) {
39 WATCHER.with(|w| *w.borrow_mut() = self.0.take());
40 }
41 }
42 let _unset = Unset(WATCHER.with(|w| w.borrow_mut().replace(Box::new(watcher))));
43 f()
44}
45
46pub fn go_on(what: &str, done: usize, total: usize) -> bool {
49 let progress = Progress { what, done, total };
50 WATCHER.with(|w| (w.borrow_mut().as_mut()).is_none_or(|watcher| watcher(&progress)))
51}
52
53pub fn report(what: &str, done: usize, total: usize) {
55 go_on(what, done, total);
56}
57
58pub fn checkpoint(what: &str, done: usize, total: usize) -> GeopResult<()> {
61 match go_on(what, done, total) {
62 true => Ok(()),
63 false => Err(GeopError::new(format!(
64 "stopped at {what}, {done} of {total} done: cancelled, or past its time limit"
65 ))),
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use std::{cell::Cell, rc::Rc};
72
73 use super::*;
74
75 #[test]
76 fn with_no_watcher_a_loop_goes_on() {
77 assert!(go_on("anything", 0, 1));
78 assert!(checkpoint("anything", 0, 1).is_ok());
79 }
80
81 #[test]
82 fn a_watcher_hears_how_far_it_is_and_stops_it() {
83 let heard = Rc::new(Cell::new(0));
84 let counted = heard.clone();
85 let done = watching(
86 move |progress: &Progress| {
87 counted.set(counted.get() + 1);
88 progress.done < 2
89 },
90 || (0..5).take_while(|&done| go_on("pairs", done, 5)).count(),
91 );
92 assert_eq!((done, heard.get()), (2, 3));
93 let stopped = watching(|_: &Progress| false, || checkpoint("solids", 3, 7));
94 let said = stopped.unwrap_err().to_string();
95 assert!(said.contains("solids, 3 of 7"), "{said}");
96 assert!(go_on("after it", 0, 1), "the watcher is gone afterwards");
97 }
98
99 #[test]
100 fn a_panic_takes_the_watcher_away() {
101 let panicked = std::panic::catch_unwind(|| {
102 watching(|_: &Progress| false, || panic!("the kernel crashed"))
103 });
104 assert!(panicked.is_err());
105 assert!(go_on("after it", 0, 1));
106 }
107}