Skip to main content

memscope_rs/capture/platform/
stack_walker.rs

1use std::time::{Duration, Instant};
2
3/// Platform-specific stack walking implementation
4pub struct PlatformStackWalker {
5    /// Walker configuration
6    config: StackWalkConfig,
7    /// Performance statistics
8    stats: WalkStats,
9    /// Platform-specific context
10    platform_context: PlatformContext,
11}
12
13/// Configuration for stack walking
14#[derive(Debug, Clone)]
15pub struct StackWalkConfig {
16    /// Maximum depth to walk
17    pub max_depth: usize,
18    /// Number of frames to skip at top
19    pub skip_frames: usize,
20    /// Whether to use fast unwinding
21    pub fast_unwind: bool,
22    /// Whether to collect frame info
23    pub collect_frame_info: bool,
24    /// Maximum time to spend walking
25    pub max_walk_time: Duration,
26}
27
28/// Platform-specific context for stack walking
29#[derive(Debug)]
30struct PlatformContext {
31    /// Whether unwinder is initialized
32    initialized: bool,
33    /// Platform-specific data
34    #[cfg(target_os = "linux")]
35    linux_context: LinuxContext,
36    #[cfg(target_os = "windows")]
37    windows_context: WindowsContext,
38    #[cfg(target_os = "macos")]
39    macos_context: MacOSContext,
40}
41
42/// Linux-specific context
43#[cfg(target_os = "linux")]
44#[derive(Debug)]
45struct LinuxContext {
46    /// Whether libunwind is available
47    libunwind_available: bool,
48    /// Whether DWARF info is available
49    dwarf_available: bool,
50}
51
52/// Windows-specific context
53#[cfg(target_os = "windows")]
54#[derive(Debug)]
55struct WindowsContext {
56    /// Whether RtlCaptureStackBackTrace is available
57    capture_available: bool,
58    /// Whether symbol APIs are initialized
59    symbols_initialized: bool,
60}
61
62/// macOS-specific context
63#[cfg(target_os = "macos")]
64#[derive(Debug)]
65struct MacOSContext {
66    /// Whether backtrace() is available
67    backtrace_available: bool,
68    /// Whether dSYM files are accessible
69    dsym_available: bool,
70}
71
72/// Statistics for stack walking performance
73#[derive(Debug)]
74struct WalkStats {
75    /// Total walks performed
76    total_walks: std::sync::atomic::AtomicUsize,
77    /// Total frames collected
78    total_frames: std::sync::atomic::AtomicUsize,
79    /// Total time spent walking
80    total_walk_time: std::sync::atomic::AtomicU64,
81    /// Failed walks
82    failed_walks: std::sync::atomic::AtomicUsize,
83}
84
85/// Result of stack walk operation
86#[derive(Debug, Clone)]
87pub struct WalkResult {
88    /// Success status
89    pub success: bool,
90    /// Collected stack frames
91    pub frames: Vec<StackFrame>,
92    /// Time taken for walk
93    pub walk_time: Duration,
94    /// Error if walk failed
95    pub error: Option<WalkError>,
96}
97
98/// Individual stack frame information
99#[derive(Debug, Clone)]
100pub struct StackFrame {
101    /// Instruction pointer
102    pub ip: usize,
103    /// Frame pointer
104    pub fp: Option<usize>,
105    /// Stack pointer
106    pub sp: Option<usize>,
107    /// Module base address
108    pub module_base: Option<usize>,
109    /// Offset within module
110    pub module_offset: Option<usize>,
111    /// Symbol information if available
112    pub symbol_info: Option<FrameSymbolInfo>,
113}
114
115/// Symbol information for a frame
116#[derive(Debug, Clone)]
117pub struct FrameSymbolInfo {
118    /// Symbol name
119    pub name: String,
120    /// Demangled name
121    pub demangled_name: Option<String>,
122    /// File name
123    pub file_name: Option<String>,
124    /// Line number
125    pub line_number: Option<u32>,
126}
127
128/// Errors that can occur during stack walking
129#[derive(Debug, Clone, PartialEq)]
130pub enum WalkError {
131    /// Platform not supported
132    UnsupportedPlatform,
133    /// Unwind library not available
134    UnwindUnavailable,
135    /// Insufficient permissions
136    InsufficientPermissions,
137    /// Corrupted stack
138    CorruptedStack,
139    /// Timeout during walk
140    Timeout,
141    /// Memory access error
142    MemoryError,
143    /// Unknown error
144    Unknown(String),
145}
146
147impl PlatformStackWalker {
148    /// Create new platform stack walker
149    pub fn new() -> Self {
150        Self {
151            config: StackWalkConfig::default(),
152            stats: WalkStats::new(),
153            platform_context: PlatformContext::new(),
154        }
155    }
156
157    /// Initialize stack walker for current platform
158    pub fn initialize(&mut self) -> Result<(), WalkError> {
159        #[cfg(target_os = "linux")]
160        {
161            self.initialize_linux()
162        }
163
164        #[cfg(target_os = "windows")]
165        {
166            self.initialize_windows()
167        }
168
169        #[cfg(target_os = "macos")]
170        {
171            self.initialize_macos()
172        }
173
174        #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
175        {
176            Err(WalkError::UnsupportedPlatform)
177        }
178    }
179
180    /// Walk current thread's stack
181    pub fn walk_current_thread(&mut self) -> WalkResult {
182        let start_time = Instant::now();
183
184        if !self.platform_context.initialized {
185            return WalkResult {
186                success: false,
187                frames: Vec::new(),
188                walk_time: start_time.elapsed(),
189                error: Some(WalkError::UnwindUnavailable),
190            };
191        }
192
193        let result = self.perform_stack_walk();
194        let walk_time = start_time.elapsed();
195
196        // Update statistics
197        self.stats
198            .total_walks
199            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
200        if result.success {
201            self.stats
202                .total_frames
203                .fetch_add(result.frames.len(), std::sync::atomic::Ordering::Relaxed);
204        } else {
205            self.stats
206                .failed_walks
207                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
208        }
209        self.stats.total_walk_time.fetch_add(
210            walk_time.as_nanos() as u64,
211            std::sync::atomic::Ordering::Relaxed,
212        );
213
214        WalkResult {
215            success: result.success,
216            frames: result.frames,
217            walk_time,
218            error: result.error,
219        }
220    }
221
222    /// Walk specific thread's stack
223    pub fn walk_thread(&mut self, thread_id: u32) -> WalkResult {
224        // Platform-specific thread stack walking
225        #[cfg(target_os = "linux")]
226        {
227            self.walk_linux_thread(thread_id)
228        }
229
230        #[cfg(target_os = "windows")]
231        {
232            self.walk_windows_thread(thread_id)
233        }
234
235        #[cfg(target_os = "macos")]
236        {
237            self.walk_macos_thread(thread_id)
238        }
239
240        #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
241        {
242            WalkResult {
243                success: false,
244                frames: Vec::new(),
245                walk_time: Duration::ZERO,
246                error: Some(WalkError::UnsupportedPlatform),
247            }
248        }
249    }
250
251    /// Get walking statistics
252    pub fn get_statistics(&self) -> WalkStatistics {
253        let total_walks = self
254            .stats
255            .total_walks
256            .load(std::sync::atomic::Ordering::Relaxed);
257        let total_frames = self
258            .stats
259            .total_frames
260            .load(std::sync::atomic::Ordering::Relaxed);
261        let total_time_ns = self
262            .stats
263            .total_walk_time
264            .load(std::sync::atomic::Ordering::Relaxed);
265        let failed_walks = self
266            .stats
267            .failed_walks
268            .load(std::sync::atomic::Ordering::Relaxed);
269
270        WalkStatistics {
271            total_walks,
272            successful_walks: total_walks.saturating_sub(failed_walks),
273            failed_walks,
274            total_frames_collected: total_frames,
275            average_frames_per_walk: if total_walks > 0 {
276                total_frames as f64 / total_walks as f64
277            } else {
278                0.0
279            },
280            average_walk_time: if total_walks > 0 {
281                Duration::from_nanos(total_time_ns / total_walks as u64)
282            } else {
283                Duration::ZERO
284            },
285            success_rate: if total_walks > 0 {
286                (total_walks - failed_walks) as f64 / total_walks as f64
287            } else {
288                0.0
289            },
290        }
291    }
292
293    /// Update walker configuration
294    pub fn update_config(&mut self, config: StackWalkConfig) {
295        self.config = config;
296    }
297
298    fn perform_stack_walk(&self) -> WalkResult {
299        let mut frames = Vec::with_capacity(self.config.max_depth);
300        let start_time = Instant::now();
301
302        #[cfg(target_os = "linux")]
303        let result = self.walk_linux_stack(&mut frames);
304
305        #[cfg(target_os = "windows")]
306        let result = self.walk_windows_stack(&mut frames);
307
308        #[cfg(target_os = "macos")]
309        let result = self.walk_macos_stack(&mut frames);
310
311        #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
312        let result = Err(WalkError::UnsupportedPlatform);
313
314        match result {
315            Ok(()) => WalkResult {
316                success: true,
317                frames,
318                walk_time: start_time.elapsed(),
319                error: None,
320            },
321            Err(error) => WalkResult {
322                success: false,
323                frames,
324                walk_time: start_time.elapsed(),
325                error: Some(error),
326            },
327        }
328    }
329
330    #[cfg(target_os = "linux")]
331    fn initialize_linux(&mut self) -> Result<(), WalkError> {
332        // Check for libunwind availability
333        self.platform_context.linux_context.libunwind_available = true; // Simplified
334        self.platform_context.linux_context.dwarf_available = true; // Simplified
335        self.platform_context.initialized = true;
336        Ok(())
337    }
338
339    #[cfg(target_os = "windows")]
340    fn initialize_windows(&mut self) -> Result<(), WalkError> {
341        // Initialize Windows stack walking APIs
342        self.platform_context.windows_context.capture_available = true; // Simplified
343        self.platform_context.windows_context.symbols_initialized = true; // Simplified
344        self.platform_context.initialized = true;
345        Ok(())
346    }
347
348    #[cfg(target_os = "macos")]
349    fn initialize_macos(&mut self) -> Result<(), WalkError> {
350        // Initialize macOS stack walking
351        self.platform_context.macos_context.backtrace_available = true; // Simplified
352        self.platform_context.macos_context.dsym_available = true; // Simplified
353        self.platform_context.initialized = true;
354        Ok(())
355    }
356
357    #[cfg(target_os = "linux")]
358    fn walk_linux_stack(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
359        #[cfg(feature = "backtrace")]
360        {
361            use backtrace::Backtrace;
362            let bt = Backtrace::new();
363
364            for (i, frame) in bt.frames().iter().enumerate() {
365                if frames.len() >= self.config.max_depth {
366                    break;
367                }
368
369                if i < self.config.skip_frames {
370                    continue;
371                }
372
373                let ip = frame.ip() as usize;
374
375                frames.push(StackFrame {
376                    ip,
377                    fp: None,
378                    sp: None,
379                    module_base: None,
380                    module_offset: None,
381                    symbol_info: frame.symbols().first().map(|sym| FrameSymbolInfo {
382                        name: sym
383                            .name()
384                            .map(|n| format!("{:?}", n))
385                            .unwrap_or_else(|| format!("unknown_symbol_{i}")),
386                        demangled_name: sym.name().map(|n| format!("{:?}", n)),
387                        file_name: sym.filename().map(|f| f.display().to_string()),
388                        line_number: sym.lineno(),
389                    }),
390                });
391            }
392
393            if !frames.is_empty() {
394                return Ok(());
395            }
396        }
397
398        #[cfg(not(feature = "backtrace"))]
399        {
400            self.walk_linux_stack_fallback(frames)?;
401        }
402
403        Ok(())
404    }
405
406    #[cfg(target_os = "linux")]
407    #[allow(dead_code)] // Fallback for when backtrace feature is disabled
408    fn walk_linux_stack_fallback(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
409        use libc::{backtrace, c_void};
410
411        struct FrameData {
412            frames: *mut Vec<StackFrame>,
413            max_depth: usize,
414            skip_frames: usize,
415            count: usize,
416        }
417
418        extern "C" fn callback(
419            data: *mut c_void,
420            ip: libc::uintptr_t,
421            _sp: libc::uintptr_t,
422            _fp: libc::uintptr_t,
423        ) -> libc::c_int {
424            let frame_data = match unsafe { (data as *mut FrameData).as_mut() } {
425                Some(fd) => fd,
426                None => return -1,
427            };
428
429            if frame_data.count < frame_data.skip_frames {
430                frame_data.count += 1;
431                return 0;
432            }
433
434            if frame_data.frames.is_null() {
435                return -1;
436            }
437
438            let frames = unsafe { &mut *frame_data.frames };
439            if frames.len() < frame_data.max_depth {
440                frames.push(StackFrame {
441                    ip,
442                    fp: None,
443                    sp: None,
444                    module_base: None,
445                    module_offset: None,
446                    symbol_info: None,
447                });
448            }
449
450            0
451        }
452
453        let mut frame_data = FrameData {
454            frames: frames as *mut Vec<StackFrame>,
455            max_depth: self.config.max_depth,
456            skip_frames: self.config.skip_frames,
457            count: 0,
458        };
459
460        unsafe {
461            let mut buffer: [*mut libc::c_void; 64] = [std::ptr::null_mut(); 64];
462            let result = backtrace(buffer.as_mut_ptr(), buffer.len() as libc::c_int);
463            if result > 0 {
464                for i in 0..result {
465                    let ip = buffer[i as usize] as libc::uintptr_t;
466                    let _ = callback(&mut frame_data as *mut FrameData as *mut c_void, ip, 0, 0);
467                }
468            }
469        }
470
471        Ok(())
472    }
473
474    #[cfg(target_os = "windows")]
475    #[cfg(target_os = "windows")]
476    fn walk_windows_stack(&self, _frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
477        // Windows-specific stack walking using StackWalk64 or RtlCaptureStackBackTrace
478        //
479        // NOTE: This is a simplified implementation for demonstration.
480        // A full implementation would:
481        // 1. Use StackWalk64 API from DbgHelp.dll for accurate stack walking
482        // 2. Use SymFromAddr and SymGetLineFromAddr64 for symbol resolution
483        // 3. Handle both 32-bit and 64-bit architectures
484        // 4. Support exception handling contexts
485        //
486        // Current implementation: Uses backtrace crate if available, otherwise simulated
487
488        // Try to use backtrace() if available
489        #[cfg(feature = "backtrace")]
490        {
491            use backtrace::Backtrace;
492            let bt = Backtrace::new();
493
494            for (i, frame) in bt.frames().iter().enumerate() {
495                if _frames.len() >= self.config.max_depth {
496                    break;
497                }
498
499                if i < self.config.skip_frames {
500                    continue;
501                }
502
503                let ip = frame.ip() as usize;
504
505                _frames.push(StackFrame {
506                    ip,
507                    fp: None,
508                    sp: None,
509                    module_base: None,
510                    module_offset: None,
511                    symbol_info: frame.symbols().first().map(|sym| FrameSymbolInfo {
512                        name: sym
513                            .name()
514                            .map(|n| format!("{:?}", n))
515                            .unwrap_or_else(|| format!("unknown_symbol_{i}")),
516                        demangled_name: sym.name().map(|n| format!("{:?}", n)),
517                        file_name: sym.filename().map(|f| f.display().to_string()),
518                        line_number: sym.lineno(),
519                    }),
520                });
521            }
522
523            if !_frames.is_empty() {
524                return Ok(());
525            }
526        }
527
528        // Fallback: Simulated implementation
529        tracing::warn!("Using simulated stack walking on Windows. Consider enabling 'backtrace' feature for accurate results.");
530
531        // Fallback: Unable to get real stack frames
532        tracing::warn!("Unable to capture stack frames on Windows. Backtrace feature may not be enabled or failed.");
533        Ok(())
534    }
535
536    #[cfg(target_os = "macos")]
537    fn walk_macos_stack(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
538        use libc::{backtrace, c_void};
539
540        const MAX_FRAMES: usize = 64;
541        let mut buffer: Vec<*mut c_void> = vec![std::ptr::null_mut(); MAX_FRAMES];
542
543        let num_frames: i32 = unsafe { backtrace(buffer.as_mut_ptr(), MAX_FRAMES as i32) };
544
545        if num_frames <= 0 {
546            return Ok(());
547        }
548
549        let num_frames_usize = num_frames as usize;
550
551        for (i, &frame) in buffer.iter().take(num_frames_usize).enumerate() {
552            if frames.len() >= self.config.max_depth {
553                break;
554            }
555
556            if i < self.config.skip_frames {
557                continue;
558            }
559
560            let ip = frame as usize;
561
562            frames.push(StackFrame {
563                ip,
564                fp: None,
565                sp: None,
566                module_base: None,
567                module_offset: None,
568                symbol_info: None,
569            });
570        }
571
572        Ok(())
573    }
574
575    #[cfg(target_os = "linux")]
576    fn walk_linux_thread(&self, _thread_id: u32) -> WalkResult {
577        // Linux thread-specific walking would use ptrace or /proc/[pid]/task/[tid]/stack
578        // This feature is not yet implemented
579        WalkResult {
580            success: false,
581            frames: Vec::new(),
582            walk_time: Duration::ZERO,
583            error: Some(WalkError::Unknown(
584                "Thread-specific stack walking is not yet implemented on Linux. Would require ptrace or reading /proc/[pid]/task/[tid]/stack.".to_string(),
585            )),
586        }
587    }
588
589    #[cfg(target_os = "windows")]
590    fn walk_windows_thread(&self, _thread_id: u32) -> WalkResult {
591        // Windows thread-specific walking would use OpenThread + StackWalk64 API
592        // This feature is not yet implemented
593        WalkResult {
594            success: false,
595            frames: Vec::new(),
596            walk_time: Duration::ZERO,
597            error: Some(WalkError::Unknown(
598                "Thread-specific stack walking is not yet implemented on Windows. Would require OpenThread and StackWalk64 API integration.".to_string(),
599            )),
600        }
601    }
602
603    #[cfg(target_os = "macos")]
604    fn walk_macos_thread(&self, _thread_id: u32) -> WalkResult {
605        // macOS thread-specific walking would use thread_get_state API
606        // This feature is not yet implemented
607        WalkResult {
608            success: false,
609            frames: Vec::new(),
610            walk_time: Duration::ZERO,
611            error: Some(WalkError::Unknown(
612                "Thread-specific stack walking is not yet implemented on macOS. Would require thread_get_state API integration.".to_string(),
613            )),
614        }
615    }
616}
617
618impl PlatformContext {
619    fn new() -> Self {
620        Self {
621            initialized: false,
622            #[cfg(target_os = "linux")]
623            linux_context: LinuxContext {
624                libunwind_available: false,
625                dwarf_available: false,
626            },
627            #[cfg(target_os = "windows")]
628            windows_context: WindowsContext {
629                capture_available: false,
630                symbols_initialized: false,
631            },
632            #[cfg(target_os = "macos")]
633            macos_context: MacOSContext {
634                backtrace_available: false,
635                dsym_available: false,
636            },
637        }
638    }
639}
640
641impl WalkStats {
642    fn new() -> Self {
643        Self {
644            total_walks: std::sync::atomic::AtomicUsize::new(0),
645            total_frames: std::sync::atomic::AtomicUsize::new(0),
646            total_walk_time: std::sync::atomic::AtomicU64::new(0),
647            failed_walks: std::sync::atomic::AtomicUsize::new(0),
648        }
649    }
650}
651
652/// Statistics about stack walking performance
653#[derive(Debug, Clone)]
654pub struct WalkStatistics {
655    /// Total number of walks performed
656    pub total_walks: usize,
657    /// Number of successful walks
658    pub successful_walks: usize,
659    /// Number of failed walks
660    pub failed_walks: usize,
661    /// Total frames collected across all walks
662    pub total_frames_collected: usize,
663    /// Average frames per successful walk
664    pub average_frames_per_walk: f64,
665    /// Average time per walk
666    pub average_walk_time: Duration,
667    /// Success rate (0.0 to 1.0)
668    pub success_rate: f64,
669}
670
671impl Default for StackWalkConfig {
672    fn default() -> Self {
673        Self {
674            max_depth: 32,
675            skip_frames: 2,
676            fast_unwind: true,
677            collect_frame_info: true,
678            max_walk_time: Duration::from_millis(10),
679        }
680    }
681}
682
683impl Default for PlatformStackWalker {
684    fn default() -> Self {
685        Self::new()
686    }
687}
688
689impl std::fmt::Display for WalkError {
690    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691        match self {
692            WalkError::UnsupportedPlatform => write!(f, "Platform not supported for stack walking"),
693            WalkError::UnwindUnavailable => write!(f, "Stack unwinding library not available"),
694            WalkError::InsufficientPermissions => {
695                write!(f, "Insufficient permissions for stack walking")
696            }
697            WalkError::CorruptedStack => write!(f, "Stack appears to be corrupted"),
698            WalkError::Timeout => write!(f, "Stack walk timed out"),
699            WalkError::MemoryError => write!(f, "Memory access error during stack walk"),
700            WalkError::Unknown(msg) => write!(f, "Unknown error: {}", msg),
701        }
702    }
703}
704
705impl std::error::Error for WalkError {}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710
711    #[test]
712    fn test_platform_stack_walker_creation() {
713        let walker = PlatformStackWalker::new();
714        assert!(!walker.platform_context.initialized);
715
716        let stats = walker.get_statistics();
717        assert_eq!(stats.total_walks, 0);
718        assert_eq!(stats.success_rate, 0.0);
719    }
720
721    #[test]
722    fn test_stack_walk_config() {
723        let config = StackWalkConfig::default();
724        assert_eq!(config.max_depth, 32);
725        assert_eq!(config.skip_frames, 2);
726        assert!(config.fast_unwind);
727        assert!(config.collect_frame_info);
728    }
729
730    #[test]
731    fn test_initialization() {
732        let mut walker = PlatformStackWalker::new();
733        let result = walker.initialize();
734
735        // Should succeed on supported platforms
736        #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
737        assert!(result.is_ok());
738
739        #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
740        assert_eq!(result, Err(WalkError::UnsupportedPlatform));
741    }
742
743    #[test]
744    fn test_current_thread_walk() {
745        let mut walker = PlatformStackWalker::new();
746        let _ = walker.initialize();
747
748        let result = walker.walk_current_thread();
749
750        #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
751        {
752            if walker.platform_context.initialized {
753                assert!(result.success);
754                #[cfg(any(target_os = "linux", target_os = "macos"))]
755                {
756                    assert!(
757                        !result.frames.is_empty(),
758                        "Native backtrace should produce frames on Linux/macOS"
759                    );
760                    assert!(result.walk_time > Duration::ZERO);
761                }
762            }
763        }
764    }
765
766    #[cfg(target_os = "macos")]
767    #[test]
768    fn test_macos_native_backtrace() {
769        let mut walker = PlatformStackWalker::new();
770        let _ = walker.initialize();
771
772        let mut frames = Vec::new();
773        let result = walker.walk_macos_stack(&mut frames);
774
775        assert!(result.is_ok());
776        assert!(
777            !frames.is_empty(),
778            "macOS native backtrace should produce at least the test frame"
779        );
780
781        for frame in &frames {
782            assert!(
783                frame.ip > 0,
784                "Each frame should have a valid instruction pointer"
785            );
786        }
787    }
788
789    #[cfg(target_os = "macos")]
790    #[test]
791    fn test_macos_backtrace_respects_max_depth() {
792        let mut walker = PlatformStackWalker::new();
793        walker.config.max_depth = 3;
794
795        let mut frames = Vec::new();
796        let _ = walker.walk_macos_stack(&mut frames);
797
798        assert!(
799            frames.len() <= 3,
800            "Should respect max_depth limit, got {} frames",
801            frames.len()
802        );
803    }
804
805    #[cfg(target_os = "macos")]
806    #[test]
807    fn test_macos_backtrace_respects_skip_frames() {
808        let mut walker = PlatformStackWalker::new();
809        walker.config.skip_frames = 2;
810
811        let mut frames = Vec::new();
812        let _ = walker.walk_macos_stack(&mut frames);
813
814        if frames.len() > 2 {
815            assert!(
816                frames[0].ip != frames[1].ip || frames.len() < 3,
817                "First frames should be skipped"
818            );
819        }
820    }
821
822    #[test]
823    fn test_frame_information() {
824        let frame = StackFrame {
825            ip: 0x12345678,
826            fp: Some(0x7fff0000),
827            sp: Some(0x7fff0008),
828            module_base: Some(0x12340000),
829            module_offset: Some(0x5678),
830            symbol_info: Some(FrameSymbolInfo {
831                name: "test_function".to_string(),
832                demangled_name: Some("namespace::test_function".to_string()),
833                file_name: Some("test.rs".to_string()),
834                line_number: Some(42),
835            }),
836        };
837
838        assert_eq!(frame.ip, 0x12345678);
839        assert_eq!(frame.fp, Some(0x7fff0000));
840        assert!(frame.symbol_info.is_some());
841
842        let symbol = frame
843            .symbol_info
844            .as_ref()
845            .expect("Symbol info should exist");
846        assert_eq!(symbol.name, "test_function");
847        assert_eq!(symbol.line_number, Some(42));
848    }
849}