elif_testing/
assertions.rs

1//! Test assertion utilities and helpers
2//!
3//! Provides comprehensive assertion helpers specifically designed
4//! for elif.rs applications, including custom assertion macros
5//! and helper functions.
6
7use serde_json::{Value as JsonValue};
8use crate::{TestError, TestResult};
9
10/// Collection of test assertions
11pub struct TestAssertions;
12
13impl TestAssertions {
14    /// Assert that two JSON values are equal
15    pub fn assert_json_eq(actual: &JsonValue, expected: &JsonValue) -> TestResult<()> {
16        if actual != expected {
17            return Err(TestError::Assertion {
18                message: format!("JSON assertion failed:\nExpected: {}\nActual: {}", 
19                    serde_json::to_string_pretty(expected).unwrap_or_default(),
20                    serde_json::to_string_pretty(actual).unwrap_or_default()
21                ),
22            });
23        }
24        Ok(())
25    }
26    
27    /// Assert that JSON contains expected fields/values
28    pub fn assert_json_contains(actual: &JsonValue, expected: &JsonValue) -> TestResult<()> {
29        if !json_contains(actual, expected) {
30            return Err(TestError::Assertion {
31                message: format!("JSON does not contain expected values:\nExpected to contain: {}\nActual: {}", 
32                    serde_json::to_string_pretty(expected).unwrap_or_default(),
33                    serde_json::to_string_pretty(actual).unwrap_or_default()
34                ),
35            });
36        }
37        Ok(())
38    }
39    
40    /// Assert that a value is within a certain range
41    pub fn assert_in_range<T>(value: T, min: T, max: T) -> TestResult<()> 
42    where 
43        T: PartialOrd + std::fmt::Display,
44    {
45        if value < min || value > max {
46            return Err(TestError::Assertion {
47                message: format!("Value {} is not in range [{}, {}]", value, min, max),
48            });
49        }
50        Ok(())
51    }
52    
53    /// Assert that a string matches a pattern
54    pub fn assert_matches_pattern(text: &str, pattern: &str) -> TestResult<()> {
55        use regex::Regex;
56        
57        let regex = Regex::new(pattern).map_err(|e| TestError::Assertion {
58            message: format!("Invalid regex pattern '{}': {}", pattern, e),
59        })?;
60        
61        if !regex.is_match(text) {
62            return Err(TestError::Assertion {
63                message: format!("Text '{}' does not match pattern '{}'", text, pattern),
64            });
65        }
66        Ok(())
67    }
68    
69    /// Assert that a collection contains an item
70    pub fn assert_contains<T, I>(collection: &[T], item: &I) -> TestResult<()>
71    where
72        T: PartialEq<I>,
73        T: std::fmt::Debug,
74        I: std::fmt::Debug,
75    {
76        if !collection.iter().any(|x| x == item) {
77            return Err(TestError::Assertion {
78                message: format!("Collection {:?} does not contain item {:?}", collection, item),
79            });
80        }
81        Ok(())
82    }
83    
84    /// Assert that a collection has a specific length
85    pub fn assert_length<T>(collection: &[T], expected_length: usize) -> TestResult<()> 
86    where
87        T: std::fmt::Debug,
88    {
89        if collection.len() != expected_length {
90            return Err(TestError::Assertion {
91                message: format!("Expected collection length {}, got {}: {:?}", 
92                    expected_length, collection.len(), collection),
93            });
94        }
95        Ok(())
96    }
97    
98    /// Assert that a collection is empty
99    pub fn assert_empty<T>(collection: &[T]) -> TestResult<()> 
100    where
101        T: std::fmt::Debug,
102    {
103        if !collection.is_empty() {
104            return Err(TestError::Assertion {
105                message: format!("Expected empty collection, got {:?}", collection),
106            });
107        }
108        Ok(())
109    }
110    
111    /// Assert that a collection is not empty
112    pub fn assert_not_empty<T>(collection: &[T]) -> TestResult<()> 
113    where
114        T: std::fmt::Debug,
115    {
116        if collection.is_empty() {
117            return Err(TestError::Assertion {
118                message: "Expected non-empty collection, got empty collection".to_string(),
119            });
120        }
121        Ok(())
122    }
123    
124    /// Assert that all items in a collection satisfy a predicate
125    pub fn assert_all<T, F>(collection: &[T], predicate: F, message: &str) -> TestResult<()>
126    where
127        T: std::fmt::Debug,
128        F: Fn(&T) -> bool,
129    {
130        let failing_items: Vec<&T> = collection.iter().filter(|item| !predicate(item)).collect();
131        
132        if !failing_items.is_empty() {
133            return Err(TestError::Assertion {
134                message: format!("{}: failing items: {:?}", message, failing_items),
135            });
136        }
137        Ok(())
138    }
139    
140    /// Assert that any item in a collection satisfies a predicate
141    pub fn assert_any<T, F>(collection: &[T], predicate: F, message: &str) -> TestResult<()>
142    where
143        T: std::fmt::Debug,
144        F: Fn(&T) -> bool,
145    {
146        if !collection.iter().any(predicate) {
147            return Err(TestError::Assertion {
148                message: format!("{}: no items match condition in {:?}", message, collection),
149            });
150        }
151        Ok(())
152    }
153    
154    /// Assert that two time values are close (within tolerance)
155    pub fn assert_time_close(
156        actual: chrono::DateTime<chrono::Utc>, 
157        expected: chrono::DateTime<chrono::Utc>,
158        tolerance_seconds: i64,
159    ) -> TestResult<()> {
160        let diff = (actual - expected).num_seconds().abs();
161        if diff > tolerance_seconds {
162            return Err(TestError::Assertion {
163                message: format!("Time difference too large: {} seconds (tolerance: {} seconds)", 
164                    diff, tolerance_seconds),
165            });
166        }
167        Ok(())
168    }
169}
170
171/// Helper function for JSON containment checking
172fn json_contains(actual: &JsonValue, expected: &JsonValue) -> bool {
173    match (actual, expected) {
174        (JsonValue::Object(actual_map), JsonValue::Object(expected_map)) => {
175            for (key, expected_value) in expected_map {
176                if let Some(actual_value) = actual_map.get(key) {
177                    if !json_contains(actual_value, expected_value) {
178                        return false;
179                    }
180                } else {
181                    return false;
182                }
183            }
184            true
185        },
186        (JsonValue::Array(actual_arr), JsonValue::Array(expected_arr)) => {
187            // For arrays, check if all expected items exist in actual array
188            expected_arr.iter().all(|expected_item| {
189                actual_arr.iter().any(|actual_item| json_contains(actual_item, expected_item))
190            })
191        },
192        _ => actual == expected,
193    }
194}
195
196/// Macros for common assertions
197#[macro_export]
198macro_rules! assert_json_eq {
199    ($actual:expr, $expected:expr) => {
200        $crate::assertions::TestAssertions::assert_json_eq($actual, $expected)?
201    };
202}
203
204#[macro_export]
205macro_rules! assert_json_contains {
206    ($actual:expr, $expected:expr) => {
207        $crate::assertions::TestAssertions::assert_json_contains($actual, $expected)?
208    };
209}
210
211#[macro_export]
212macro_rules! assert_in_range {
213    ($value:expr, $min:expr, $max:expr) => {
214        $crate::assertions::TestAssertions::assert_in_range($value, $min, $max)?
215    };
216}
217
218#[macro_export]
219macro_rules! assert_matches {
220    ($text:expr, $pattern:expr) => {
221        $crate::assertions::TestAssertions::assert_matches_pattern($text, $pattern)?
222    };
223}
224
225#[macro_export]
226macro_rules! assert_contains {
227    ($collection:expr, $item:expr) => {
228        $crate::assertions::TestAssertions::assert_contains($collection, $item)?
229    };
230}
231
232#[macro_export]
233macro_rules! assert_length {
234    ($collection:expr, $length:expr) => {
235        $crate::assertions::TestAssertions::assert_length($collection, $length)?
236    };
237}
238
239#[macro_export]
240macro_rules! assert_empty {
241    ($collection:expr) => {
242        $crate::assertions::TestAssertions::assert_empty($collection)?
243    };
244}
245
246#[macro_export]
247macro_rules! assert_not_empty {
248    ($collection:expr) => {
249        $crate::assertions::TestAssertions::assert_not_empty($collection)?
250    };
251}
252
253#[macro_export]
254macro_rules! assert_all {
255    ($collection:expr, $predicate:expr, $message:expr) => {
256        $crate::assertions::TestAssertions::assert_all($collection, $predicate, $message)?
257    };
258}
259
260#[macro_export]
261macro_rules! assert_any {
262    ($collection:expr, $predicate:expr, $message:expr) => {
263        $crate::assertions::TestAssertions::assert_any($collection, $predicate, $message)?
264    };
265}
266
267#[macro_export]
268macro_rules! assert_time_close {
269    ($actual:expr, $expected:expr, $tolerance:expr) => {
270        $crate::assertions::TestAssertions::assert_time_close($actual, $expected, $tolerance)?
271    };
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use serde_json::json;
278    use chrono::{Utc, Duration};
279    
280    #[test]
281    fn test_json_equality() -> TestResult<()> {
282        let json1 = json!({"name": "John", "age": 30});
283        let json2 = json!({"name": "John", "age": 30});
284        let json3 = json!({"name": "Jane", "age": 25});
285        
286        TestAssertions::assert_json_eq(&json1, &json2)?;
287        
288        let result = TestAssertions::assert_json_eq(&json1, &json3);
289        assert!(result.is_err());
290        
291        Ok(())
292    }
293    
294    #[test]
295    fn test_json_contains() -> TestResult<()> {
296        let actual = json!({
297            "user": {
298                "name": "John",
299                "age": 30,
300                "active": true
301            },
302            "posts": [
303                {"title": "Post 1"},
304                {"title": "Post 2"}
305            ]
306        });
307        
308        let expected = json!({
309            "user": {
310                "name": "John"
311            }
312        });
313        
314        TestAssertions::assert_json_contains(&actual, &expected)?;
315        
316        let expected_fail = json!({
317            "user": {
318                "name": "Jane"
319            }
320        });
321        
322        let result = TestAssertions::assert_json_contains(&actual, &expected_fail);
323        assert!(result.is_err());
324        
325        Ok(())
326    }
327    
328    #[test]
329    fn test_range_assertion() -> TestResult<()> {
330        TestAssertions::assert_in_range(5, 1, 10)?;
331        
332        let result = TestAssertions::assert_in_range(15, 1, 10);
333        assert!(result.is_err());
334        
335        Ok(())
336    }
337    
338    #[test]
339    fn test_pattern_matching() -> TestResult<()> {
340        TestAssertions::assert_matches_pattern("test@example.com", r"^[^@]+@[^@]+\.[^@]+$")?;
341        
342        let result = TestAssertions::assert_matches_pattern("invalid-email", r"^[^@]+@[^@]+\.[^@]+$");
343        assert!(result.is_err());
344        
345        Ok(())
346    }
347    
348    #[test]
349    fn test_collection_assertions() -> TestResult<()> {
350        let collection = vec![1, 2, 3, 4, 5];
351        
352        TestAssertions::assert_contains(&collection, &3)?;
353        TestAssertions::assert_length(&collection, 5)?;
354        TestAssertions::assert_not_empty(&collection)?;
355        
356        TestAssertions::assert_all(&collection, |x| *x > 0, "All items should be positive")?;
357        TestAssertions::assert_any(&collection, |x| *x > 4, "Some items should be greater than 4")?;
358        
359        let empty_collection: Vec<i32> = vec![];
360        TestAssertions::assert_empty(&empty_collection)?;
361        
362        Ok(())
363    }
364    
365    #[test]
366    fn test_time_assertion() -> TestResult<()> {
367        let now = Utc::now();
368        let close_time = now + Duration::seconds(2);
369        let far_time = now + Duration::seconds(60);
370        
371        TestAssertions::assert_time_close(close_time, now, 10)?;
372        
373        let result = TestAssertions::assert_time_close(far_time, now, 10);
374        assert!(result.is_err());
375        
376        Ok(())
377    }
378    
379    #[test]
380    fn test_macro_usage() -> TestResult<()> {
381        let json1 = json!({"test": "value"});
382        let json2 = json!({"test": "value"});
383        
384        assert_json_eq!(&json1, &json2);
385        assert_in_range!(5, 1, 10);
386        
387        let collection = vec![1, 2, 3];
388        assert_contains!(&collection, &2);
389        assert_length!(&collection, 3);
390        
391        Ok(())
392    }
393}