brush_core/
scripts.rs

1//! Call stack representations.
2
3use std::collections::VecDeque;
4
5/// Represents an executing script.
6#[derive(Clone, Debug)]
7pub enum CallType {
8    /// The script was sourced.
9    Sourced,
10    /// The script was executed.
11    Executed,
12}
13
14impl std::fmt::Display for CallType {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        match self {
17            Self::Sourced => write!(f, "sourced"),
18            Self::Executed => write!(f, "executed"),
19        }
20    }
21}
22
23/// Represents a single frame in a script call stack.
24#[derive(Clone, Debug)]
25pub struct CallFrame {
26    /// The type of script call that resulted in this frame.
27    pub call_type: CallType,
28    /// The source of the script (e.g., file path).
29    pub source: String,
30}
31
32/// Encapsulates a script call stack.
33#[derive(Clone, Debug, Default)]
34pub struct CallStack {
35    frames: VecDeque<CallFrame>,
36}
37
38impl std::fmt::Display for CallStack {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        if self.is_empty() {
41            return Ok(());
42        }
43
44        writeln!(f, "Script call stack (most recent first):")?;
45
46        for (index, frame) in self.iter().enumerate() {
47            writeln!(f, "  #{}| {} ({})", index, frame.source, frame.call_type)?;
48        }
49
50        Ok(())
51    }
52}
53
54impl CallStack {
55    /// Creates a new empty script call stack.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Removes the top from from the stack. If the stack is empty, does nothing and
61    /// returns `None`; otherwise, returns the removed call frame.
62    pub fn pop(&mut self) -> Option<CallFrame> {
63        self.frames.pop_front()
64    }
65
66    /// Pushes a new frame onto the stack.
67    ///
68    /// # Arguments
69    ///
70    /// * `call_type` - The type of script call (sourced or executed).
71    /// * `source` - The source of the script (e.g., file path).
72    pub fn push(&mut self, call_type: CallType, source: impl Into<String>) {
73        self.frames.push_front(CallFrame {
74            call_type,
75            source: source.into(),
76        });
77    }
78
79    /// Returns whether or not the current script stack frame is a sourced script.
80    pub fn in_sourced_script(&self) -> bool {
81        self.frames
82            .front()
83            .is_some_and(|frame| matches!(frame.call_type, CallType::Sourced))
84    }
85
86    /// Returns the current depth of the script call stack.
87    pub fn depth(&self) -> usize {
88        self.frames.len()
89    }
90
91    /// Returns whether or not the script call stack is empty.
92    pub fn is_empty(&self) -> bool {
93        self.frames.is_empty()
94    }
95
96    /// Returns an iterator over the script call frames, starting from the most
97    /// recent.
98    pub fn iter(&self) -> impl Iterator<Item = &CallFrame> {
99        self.frames.iter()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_call_type_display() {
109        assert_eq!(CallType::Sourced.to_string(), "sourced");
110        assert_eq!(CallType::Executed.to_string(), "executed");
111    }
112
113    #[test]
114    fn test_call_stack_new() {
115        let stack = CallStack::new();
116        assert!(stack.is_empty());
117        assert_eq!(stack.depth(), 0);
118    }
119
120    #[test]
121    fn test_call_stack_default() {
122        let stack = CallStack::default();
123        assert!(stack.is_empty());
124        assert_eq!(stack.depth(), 0);
125    }
126
127    #[test]
128    fn test_call_stack_push_pop() {
129        let mut stack = CallStack::new();
130
131        stack.push(CallType::Sourced, "script1.sh");
132        assert!(!stack.is_empty());
133        assert_eq!(stack.depth(), 1);
134
135        stack.push(CallType::Executed, "script2.sh");
136        assert_eq!(stack.depth(), 2);
137
138        let frame = stack.pop().unwrap();
139        assert_eq!(frame.source, "script2.sh");
140        assert!(matches!(frame.call_type, CallType::Executed));
141        assert_eq!(stack.depth(), 1);
142
143        let frame = stack.pop().unwrap();
144        assert_eq!(frame.source, "script1.sh");
145        assert!(matches!(frame.call_type, CallType::Sourced));
146        assert_eq!(stack.depth(), 0);
147        assert!(stack.is_empty());
148    }
149
150    #[test]
151    fn test_call_stack_pop_empty() {
152        let mut stack = CallStack::new();
153        assert!(stack.pop().is_none());
154    }
155
156    #[test]
157    fn test_in_sourced_script() {
158        let mut stack = CallStack::new();
159        assert!(!stack.in_sourced_script());
160
161        stack.push(CallType::Executed, "script1.sh");
162        assert!(!stack.in_sourced_script());
163
164        stack.push(CallType::Sourced, "script2.sh");
165        assert!(stack.in_sourced_script());
166
167        stack.pop();
168        assert!(!stack.in_sourced_script());
169    }
170
171    #[test]
172    fn test_call_stack_iter() {
173        let mut stack = CallStack::new();
174        stack.push(CallType::Sourced, "script1.sh");
175        stack.push(CallType::Executed, "script2.sh");
176        stack.push(CallType::Sourced, "script3.sh");
177
178        let frames: Vec<_> = stack.iter().collect();
179        assert_eq!(frames.len(), 3);
180        assert_eq!(frames[0].source, "script3.sh");
181        assert_eq!(frames[1].source, "script2.sh");
182        assert_eq!(frames[2].source, "script1.sh");
183    }
184
185    #[test]
186    fn test_call_stack_display_empty() {
187        let stack = CallStack::new();
188        assert_eq!(stack.to_string(), "");
189    }
190
191    #[test]
192    fn test_call_stack_display_with_frames() {
193        let mut stack = CallStack::new();
194        stack.push(CallType::Sourced, "script1.sh");
195        stack.push(CallType::Executed, "script2.sh");
196
197        let output = stack.to_string();
198        assert!(output.contains("Script call stack (most recent first):"));
199        assert!(output.contains("#0| script2.sh (executed)"));
200        assert!(output.contains("#1| script1.sh (sourced)"));
201    }
202
203    #[test]
204    fn test_call_frame_clone() {
205        let frame1 = CallFrame {
206            call_type: CallType::Sourced,
207            source: "test.sh".to_string(),
208        };
209        let frame2 = frame1.clone();
210
211        assert_eq!(frame1.source, frame2.source);
212        assert!(matches!(frame1.call_type, CallType::Sourced));
213        assert!(matches!(frame2.call_type, CallType::Sourced));
214    }
215
216    #[test]
217    fn test_call_stack_clone() {
218        let mut stack1 = CallStack::new();
219        stack1.push(CallType::Sourced, "script1.sh");
220        stack1.push(CallType::Executed, "script2.sh");
221
222        let stack2 = stack1.clone();
223        assert_eq!(stack1.depth(), stack2.depth());
224
225        let frames1: Vec<_> = stack1.iter().map(|f| &f.source).collect();
226        let frames2: Vec<_> = stack2.iter().map(|f| &f.source).collect();
227        assert_eq!(frames1, frames2);
228    }
229
230    #[test]
231    fn test_push_with_string_types() {
232        let mut stack = CallStack::new();
233
234        // Test with &str
235        stack.push(CallType::Sourced, "script1.sh");
236
237        // Test with String
238        stack.push(CallType::Executed, String::from("script2.sh"));
239
240        // Test with owned string reference
241        let owned = "script3.sh".to_string();
242        stack.push(CallType::Sourced, &owned);
243
244        assert_eq!(stack.depth(), 3);
245    }
246}