1#![doc = include_str!("../README.md")]
2
3pub use edtest_macros::rstest;
7
8pub use rstest::fixture;
11
12pub use serial_test::serial;
13
14pub use static_assertions::*;
15
16#[macro_export]
26macro_rules! set_snapshot_suffix {
27 ($($expr:expr),*) => {
28 let mut settings = insta::Settings::clone_current();
29 let raw_suffix = format!($($expr,)*);
30 let cleaned = $crate::internal::clean_snapshot_suffix(&raw_suffix);
31 settings.set_snapshot_suffix(cleaned);
32 let _guard = settings.bind_to_scope();
33 }
34}
35
36#[doc(hidden)]
37pub mod internal {
38 use core::hint::black_box;
39 use core::sync::atomic::{AtomicUsize, Ordering};
40
41 static COUNTER: AtomicUsize = AtomicUsize::new(0);
42
43 #[doc(hidden)]
55 pub fn clean_snapshot_suffix(input: &str) -> String {
56 let mut out = String::with_capacity(input.len());
57 for ch in input.chars() {
58 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '@' | '+') {
59 out.push(ch);
60 } else if ch.is_whitespace() || ch == '/' || ch == '\\' {
61 out.push('-');
62 } else {
63 out.push('_');
64 }
65 }
66
67 while out.ends_with(['.', ' ']) {
69 out.pop();
70 }
71
72 fn is_windows_reserved(name: &str) -> bool {
75 const RESERVED: &[&str] = &[
76 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
77 "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
78 "LPT9",
79 ];
80 RESERVED.iter().any(|&r| r.eq_ignore_ascii_case(name))
81 }
82
83 if out.is_empty() || !out.chars().any(|c| c.is_ascii_alphanumeric()) {
84 return "snapshot".to_string();
85 }
86
87 if is_windows_reserved(&out) {
88 return format!("_{}", out);
89 }
90
91 out
92 }
93
94 pub fn on_test_enter(name: &str) {
96 black_box(name.len());
97 COUNTER.fetch_add(1, Ordering::Relaxed);
98 }
99
100 pub fn on_test_exit() {
101 COUNTER.fetch_add(1, Ordering::Relaxed);
102 }
103}
104
105#[doc(hidden)]
108pub struct TestGuard;
109
110#[allow(clippy::new_without_default)]
111impl TestGuard {
112 #[inline(always)]
113 pub fn new() -> Self {
114 Self
115 }
116}
117
118impl Drop for TestGuard {
119 fn drop(&mut self) {
120 internal::on_test_exit();
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 #[test]
127 fn internal_hooks_execute() {
128 crate::internal::on_test_enter("sample");
129 let _g = crate::TestGuard::new();
130 crate::internal::on_test_exit();
131 }
132
133 #[test]
134 fn cleans_suffix_general() {
135 use crate::internal::clean_snapshot_suffix as clean;
136 let s = "a/b\\c:d*e?f|g<h>i\"j k.";
138 assert_eq!(clean(s), "a-b-c_d_e_f_g_h_i_j-k");
139 }
140
141 #[test]
142 fn cleans_suffix_reserved_and_empty() {
143 use crate::internal::clean_snapshot_suffix as clean;
144 assert_eq!(clean("CON"), "_CON");
145 assert_eq!(clean("con"), "_con");
146 assert_eq!(clean(" \t\n"), "snapshot");
147 }
148}