blue_lang_syntax/
test_stack.rs1use 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}