Skip to main content

blue_lang_syntax/
test_stack.rs

1use std::num::NonZeroUsize;
2
3pub const ENV: &str = "BLUE_TEST_STACK";
4
5pub const DEFAULT_MIB: NonZeroUsize = NonZeroUsize::new(64).unwrap();
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Stack {
9    Inherit,
10    Mib(NonZeroUsize),
11}
12
13impl Stack {
14    pub fn from_setting(setting: Option<&str>) -> Result<Self, String> {
15        match setting.map(str::trim) {
16            None | Some("") => Ok(Self::Mib(DEFAULT_MIB)),
17            Some("inherit") => Ok(Self::Inherit),
18            Some(v) => v.parse::<NonZeroUsize>().map(Self::Mib).map_err(|_| {
19                format!("{ENV} is {v:?}; it takes inherit, or a whole number of MiB above 0")
20            }),
21        }
22    }
23
24    pub fn configured() -> Self {
25        let setting = match std::env::var(ENV) {
26            Ok(v) => Some(v),
27            Err(std::env::VarError::NotPresent) => None,
28            Err(std::env::VarError::NotUnicode(v)) => {
29                panic!("{ENV} is {v:?}; it takes inherit, or a whole number of MiB above 0")
30            }
31        };
32        Self::from_setting(setting.as_deref()).unwrap_or_else(|why| panic!("{why}"))
33    }
34}
35
36pub fn run<T: Send>(body: impl FnOnce() -> T + Send) -> T {
37    run_on(Stack::configured(), body)
38}
39
40pub fn run_on<T: Send>(stack: Stack, body: impl FnOnce() -> T + Send) -> T {
41    let Stack::Mib(mib) = stack else {
42        return body();
43    };
44    let name = std::thread::current()
45        .name()
46        .unwrap_or("blue-test-stack")
47        .to_string();
48    std::thread::scope(|scope| {
49        std::thread::Builder::new()
50            .name(name)
51            .stack_size(mib.get() << 20)
52            .spawn_scoped(scope, body)
53            .unwrap_or_else(|e| panic!("could not start a thread with a {mib} MiB stack: {e}"))
54            .join()
55            .unwrap_or_else(|payload| std::panic::resume_unwind(payload))
56    })
57}
58
59#[cfg(test)]
60mod tests {
61    use super::{run_on, Stack, DEFAULT_MIB};
62    use std::num::NonZeroUsize;
63
64    #[test]
65    fn the_setting_is_the_default_inherit_or_a_size_and_nothing_else() {
66        assert_eq!(Stack::from_setting(None), Ok(Stack::Mib(DEFAULT_MIB)));
67        assert_eq!(Stack::from_setting(Some("")), Ok(Stack::Mib(DEFAULT_MIB)));
68        assert_eq!(Stack::from_setting(Some("inherit")), Ok(Stack::Inherit));
69        assert_eq!(
70            Stack::from_setting(Some("16")),
71            Ok(Stack::Mib(NonZeroUsize::new(16).unwrap()))
72        );
73        for bad in ["0", "-1", "off", "16MiB", "1.5"] {
74            let why = Stack::from_setting(Some(bad)).expect_err(bad);
75            assert!(
76                why.contains("BLUE_TEST_STACK") && why.contains("inherit"),
77                "{why}"
78            );
79        }
80    }
81
82    #[test]
83    fn a_sized_stack_runs_the_body_on_its_own_thread_named_for_the_caller() {
84        let caller = std::thread::current().id();
85        let caller_name = std::thread::current().name().map(str::to_string);
86        let (id, name) = run_on(Stack::Mib(NonZeroUsize::new(4).unwrap()), || {
87            (
88                std::thread::current().id(),
89                std::thread::current().name().map(str::to_string),
90            )
91        });
92        assert_ne!(id, caller);
93        assert_eq!(name, caller_name);
94    }
95
96    #[test]
97    fn inherit_runs_the_body_on_the_calling_thread() {
98        let caller = std::thread::current().id();
99        assert_eq!(
100            run_on(Stack::Inherit, || std::thread::current().id()),
101            caller
102        );
103    }
104
105    #[test]
106    fn a_panic_in_the_body_reaches_the_caller_with_its_message() {
107        let caught = std::panic::catch_unwind(|| {
108            run_on(Stack::Mib(DEFAULT_MIB), || panic!("the body's own words"))
109        })
110        .expect_err("the panic must propagate");
111        let text = caught
112            .downcast_ref::<&str>()
113            .map(|s| s.to_string())
114            .or_else(|| caught.downcast_ref::<String>().cloned());
115        assert_eq!(text.as_deref(), Some("the body's own words"));
116    }
117
118    #[test]
119    fn a_body_deeper_than_the_harness_stack_completes_on_the_default_stack() {
120        fn depth(n: u64) -> u64 {
121            let pad = std::hint::black_box([n; 64]);
122            if n == 0 {
123                0
124            } else {
125                pad[0].min(1) + depth(n - 1)
126            }
127        }
128        assert_eq!(
129            run_on(Stack::Mib(DEFAULT_MIB), || depth(std::hint::black_box(
130                20_000
131            ))),
132            20_000
133        );
134    }
135}