Skip to main content

forge_runtime/testing/
assertions.rs

1//! Test assertion macros and helpers.
2
3use forge_core::error::ForgeError;
4use forge_core::job::JobStatus;
5use forge_core::workflow::WorkflowStatus;
6
7/// Assert that a result is Ok.
8#[macro_export]
9macro_rules! assert_ok {
10    ($expr:expr) => {
11        match &$expr {
12            Ok(_) => (),
13            Err(e) => panic!("assertion failed: expected Ok, got Err({:?})", e),
14        }
15    };
16    ($expr:expr, $($arg:tt)+) => {
17        match &$expr {
18            Ok(_) => (),
19            Err(e) => panic!("assertion failed: {}: expected Ok, got Err({:?})", format_args!($($arg)+), e),
20        }
21    };
22}
23
24/// Assert that a result is Err.
25#[macro_export]
26macro_rules! assert_err {
27    ($expr:expr) => {
28        match &$expr {
29            Err(_) => (),
30            Ok(v) => panic!("assertion failed: expected Err, got Ok({:?})", v),
31        }
32    };
33    ($expr:expr, $($arg:tt)+) => {
34        match &$expr {
35            Err(_) => (),
36            Ok(v) => panic!("assertion failed: {}: expected Err, got Ok({:?})", format_args!($($arg)+), v),
37        }
38    };
39}
40
41/// Assert that an error matches a specific variant.
42#[macro_export]
43macro_rules! assert_err_variant {
44    ($expr:expr, $variant:pat) => {
45        match &$expr {
46            Err($variant) => (),
47            Err(e) => panic!(
48                "assertion failed: expected {}, got {:?}",
49                stringify!($variant),
50                e
51            ),
52            Ok(v) => panic!(
53                "assertion failed: expected Err({}), got Ok({:?})",
54                stringify!($variant),
55                v
56            ),
57        }
58    };
59}
60
61/// Assert that a job was dispatched.
62#[macro_export]
63macro_rules! assert_job_dispatched {
64    ($ctx:expr, $job_type:expr) => {
65        assert!(
66            $ctx.job_dispatched($job_type),
67            "assertion failed: job '{}' was not dispatched",
68            $job_type
69        );
70    };
71    ($ctx:expr, $job_type:expr, $predicate:expr) => {
72        let jobs = $ctx
73            .dispatched_jobs()
74            .iter()
75            .filter(|j| j.job_type == $job_type)
76            .collect::<Vec<_>>();
77        assert!(
78            jobs.iter().any(|j| $predicate(&j.input)),
79            "assertion failed: no job '{}' matching predicate was dispatched",
80            $job_type
81        );
82    };
83}
84
85/// Assert that a workflow was started.
86#[macro_export]
87macro_rules! assert_workflow_started {
88    ($ctx:expr, $workflow_name:expr) => {
89        assert!(
90            $ctx.started_workflows()
91                .iter()
92                .any(|w| w.workflow_name == $workflow_name),
93            "assertion failed: workflow '{}' was not started",
94            $workflow_name
95        );
96    };
97}
98
99/// Check if an error message contains a substring.
100pub fn error_contains(error: &ForgeError, substring: &str) -> bool {
101    error.to_string().contains(substring)
102}
103
104/// Check if a validation error contains specific field.
105pub fn validation_error_for_field(error: &ForgeError, field: &str) -> bool {
106    match error {
107        ForgeError::Validation(msg) => msg.contains(field),
108        _ => false,
109    }
110}
111
112/// Assert helper for job status.
113#[allow(clippy::panic)]
114pub fn assert_job_status(actual: Option<JobStatus>, expected: JobStatus) {
115    match actual {
116        Some(status) => assert_eq!(
117            status, expected,
118            "expected job status {:?}, got {:?}",
119            expected, status
120        ),
121        None => panic!("expected job status {:?}, but job not found", expected),
122    }
123}
124
125/// Assert helper for workflow status.
126#[allow(clippy::panic)]
127pub fn assert_workflow_status(actual: Option<WorkflowStatus>, expected: WorkflowStatus) {
128    match actual {
129        Some(status) => assert_eq!(
130            status, expected,
131            "expected workflow status {:?}, got {:?}",
132            expected, status
133        ),
134        None => panic!(
135            "expected workflow status {:?}, but workflow not found",
136            expected
137        ),
138    }
139}
140
141/// Assert that a value matches a JSON pattern.
142pub fn assert_json_matches(actual: &serde_json::Value, pattern: &serde_json::Value) -> bool {
143    match (actual, pattern) {
144        (serde_json::Value::Object(a), serde_json::Value::Object(p)) => {
145            for (key, expected_value) in p {
146                match a.get(key) {
147                    Some(actual_value) => {
148                        if !assert_json_matches(actual_value, expected_value) {
149                            return false;
150                        }
151                    }
152                    None => return false,
153                }
154            }
155            true
156        }
157        (serde_json::Value::Array(a), serde_json::Value::Array(p)) => {
158            if a.len() != p.len() {
159                return false;
160            }
161            a.iter()
162                .zip(p.iter())
163                .all(|(a, p)| assert_json_matches(a, p))
164        }
165        (a, p) => a == p,
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_assert_ok_macro() {
175        let result: Result<i32, String> = Ok(42);
176        assert_ok!(result);
177    }
178
179    #[test]
180    #[should_panic(expected = "expected Ok")]
181    fn test_assert_ok_macro_fails() {
182        let result: Result<i32, String> = Err("error".to_string());
183        assert_ok!(result);
184    }
185
186    #[test]
187    fn test_assert_err_macro() {
188        let result: Result<i32, String> = Err("error".to_string());
189        assert_err!(result);
190    }
191
192    #[test]
193    #[should_panic(expected = "expected Err")]
194    fn test_assert_err_macro_fails() {
195        let result: Result<i32, String> = Ok(42);
196        assert_err!(result);
197    }
198
199    #[test]
200    fn test_error_contains() {
201        let error = ForgeError::Validation("email is required".to_string());
202        assert!(error_contains(&error, "email"));
203        assert!(error_contains(&error, "required"));
204        assert!(!error_contains(&error, "password"));
205    }
206
207    #[test]
208    fn test_validation_error_for_field() {
209        let error = ForgeError::Validation("email: is invalid".to_string());
210        assert!(validation_error_for_field(&error, "email"));
211        assert!(!validation_error_for_field(&error, "password"));
212
213        let other_error = ForgeError::Internal("internal error".to_string());
214        assert!(!validation_error_for_field(&other_error, "email"));
215    }
216
217    #[test]
218    fn test_assert_job_status() {
219        assert_job_status(Some(JobStatus::Completed), JobStatus::Completed);
220    }
221
222    #[test]
223    #[should_panic(expected = "expected job status")]
224    fn test_assert_job_status_mismatch() {
225        assert_job_status(Some(JobStatus::Pending), JobStatus::Completed);
226    }
227
228    #[test]
229    #[should_panic(expected = "job not found")]
230    fn test_assert_job_status_not_found() {
231        assert_job_status(None, JobStatus::Completed);
232    }
233
234    #[test]
235    fn test_assert_job_status_cancelled() {
236        assert_job_status(Some(JobStatus::Cancelled), JobStatus::Cancelled);
237    }
238
239    #[test]
240    fn test_assert_json_matches() {
241        let actual = serde_json::json!({
242            "id": 123,
243            "name": "Test",
244            "nested": {
245                "foo": "bar"
246            }
247        });
248
249        // Partial match
250        assert!(assert_json_matches(
251            &actual,
252            &serde_json::json!({"id": 123})
253        ));
254        assert!(assert_json_matches(
255            &actual,
256            &serde_json::json!({"name": "Test"})
257        ));
258        assert!(assert_json_matches(
259            &actual,
260            &serde_json::json!({"nested": {"foo": "bar"}})
261        ));
262
263        // Non-match
264        assert!(!assert_json_matches(
265            &actual,
266            &serde_json::json!({"id": 456})
267        ));
268        assert!(!assert_json_matches(
269            &actual,
270            &serde_json::json!({"missing": true})
271        ));
272    }
273
274    #[test]
275    fn test_assert_json_matches_arrays() {
276        let actual = serde_json::json!([1, 2, 3]);
277        assert!(assert_json_matches(&actual, &serde_json::json!([1, 2, 3])));
278        assert!(!assert_json_matches(&actual, &serde_json::json!([1, 2])));
279        assert!(!assert_json_matches(&actual, &serde_json::json!([1, 2, 4])));
280    }
281}