1use std::collections::VecDeque;
4
5#[derive(Clone, Debug)]
7pub enum CallType {
8 Sourced,
10 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#[derive(Clone, Debug)]
25pub struct CallFrame {
26 pub call_type: CallType,
28 pub source: String,
30}
31
32#[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 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn pop(&mut self) -> Option<CallFrame> {
63 self.frames.pop_front()
64 }
65
66 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 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 pub fn depth(&self) -> usize {
88 self.frames.len()
89 }
90
91 pub fn is_empty(&self) -> bool {
93 self.frames.is_empty()
94 }
95
96 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 stack.push(CallType::Sourced, "script1.sh");
236
237 stack.push(CallType::Executed, String::from("script2.sh"));
239
240 let owned = "script3.sh".to_string();
242 stack.push(CallType::Sourced, &owned);
243
244 assert_eq!(stack.depth(), 3);
245 }
246}