Skip to main content

edtest/
lib.rs

1#![doc = include_str!("../README.md")]
2
3/// Generate a test function using `rstest`. If used on a `async` function
4/// the test will use the `tokio` runtime.
5/// See the [rstest documentation](https://docs.rs/rstest/latest/rstest).
6pub use edtest_macros::rstest;
7
8/// Creation of test-fixtures. see the
9/// [fixture documentation](https://docs.rs/rstest/latest/rstest/attr.fixture.html).
10pub use rstest::fixture;
11
12pub use serial_test::serial;
13
14pub use static_assertions::*;
15
16/// Helper macro to set an `insta` snapshot suffix for the current scope.
17///
18/// Example:
19///
20/// set_snapshot_suffix!("{}", input);
21///
22/// Expands to code that clones the current `insta::Settings`, sets the
23/// snapshot suffix and binds the settings to the current scope so snapshots
24/// get the provided suffix for the duration of the scope.
25#[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    use std::collections::hash_map::DefaultHasher;
41    use std::hash::{Hash, Hasher};
42
43    static COUNTER: AtomicUsize = AtomicUsize::new(0);
44
45    /// Convert any provided snapshot suffix into a short, filesystem-safe hash.
46    ///
47    /// Strategy:
48    /// - Use the standard library's default hasher (currently SipHash-based) over the input bytes.
49    /// - Format the resulting u64 as 16 lowercase hex characters.
50    /// - Deterministic within a given Rust version/implementation; ASCII-only and cross-platform safe.
51    #[doc(hidden)]
52    pub fn clean_snapshot_suffix(input: &str) -> String {
53        let mut hasher = DefaultHasher::new();
54        input.hash(&mut hasher);
55        let h: u64 = hasher.finish();
56        format!("{:016x}", h)
57    }
58
59    // Tiny side effects to ensure coverage sees executed regions and prevent full optimization.
60    pub fn on_test_enter(name: &str) {
61        black_box(name.len());
62        COUNTER.fetch_add(1, Ordering::Relaxed);
63    }
64
65    pub fn on_test_exit() {
66        COUNTER.fetch_add(1, Ordering::Relaxed);
67    }
68}
69
70/// A guard that calls `internal::on_test_exit()` when dropped.
71/// Used by the macro expansion to ensure an exit hook runs even if the test panics.
72#[doc(hidden)]
73pub struct TestGuard;
74
75#[allow(clippy::new_without_default)]
76impl TestGuard {
77    #[inline(always)]
78    pub fn new() -> Self {
79        Self
80    }
81}
82
83impl Drop for TestGuard {
84    fn drop(&mut self) {
85        internal::on_test_exit();
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    #[test]
92    fn internal_hooks_execute() {
93        crate::internal::on_test_enter("sample");
94        let _g = crate::TestGuard::new();
95        crate::internal::on_test_exit();
96    }
97
98    #[test]
99    fn hashes_suffix_general() {
100        use crate::internal::clean_snapshot_suffix as clean;
101        let s = "a/b\\c:d*e?f|g<h>i\"j k.";
102        let h = clean(s);
103        assert_eq!(h.len(), 16);
104        assert!(h
105            .chars()
106            .all(|c| c.is_ascii_hexdigit() && (c.is_ascii_lowercase() || c.is_ascii_digit())));
107        // Deterministic for the same input
108        assert_eq!(h, clean(s));
109        // Likely different for a different input
110        assert_ne!(h, clean("different"));
111    }
112
113    #[test]
114    fn hash_properties_reserved_and_empty() {
115        use crate::internal::clean_snapshot_suffix as clean;
116        // Case sensitivity preserved via hashing
117        assert_ne!(clean("CON"), clean("con"));
118        // Empty still yields a deterministic hex string (for a given implementation)
119        let e = clean("");
120        assert_eq!(e.len(), 16);
121        assert!(e
122            .chars()
123            .all(|c| c.is_ascii_hexdigit() && (c.is_ascii_lowercase() || c.is_ascii_digit())));
124    }
125}