test_better_matchers/
soft.rs1use std::borrow::Cow;
20
21use test_better_core::{ContextFrame, ErrorKind, Payload, TestError, TestResult};
22
23use crate::matcher::Matcher;
24
25#[track_caller]
49pub fn soft<F>(f: F) -> TestResult
50where
51 F: FnOnce(&mut SoftAsserter),
52{
53 let mut asserter = SoftAsserter::new();
54
55 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&mut asserter)));
62
63 let result = asserter.into_result();
64
65 match outcome {
66 Ok(()) => result,
67 Err(panic) => {
68 if let Err(ref soft_failures) = result {
72 eprintln!("soft assertions recorded before the panic:\n{soft_failures}");
73 }
74 std::panic::resume_unwind(panic);
75 }
76 }
77}
78
79#[derive(Default)]
86pub struct SoftAsserter {
87 errors: Vec<TestError>,
88 context: Vec<ContextFrame>,
92}
93
94impl SoftAsserter {
95 #[must_use]
98 pub fn new() -> Self {
99 Self::default()
100 }
101
102 #[track_caller]
108 pub fn expect<T, M>(&mut self, actual: &T, matcher: M)
109 where
110 T: ?Sized,
111 M: Matcher<T>,
112 {
113 if let Some(mismatch) = matcher.check(actual).failure {
114 self.record(TestError::new(ErrorKind::Assertion).with_payload(
115 Payload::ExpectedActual {
116 expected: mismatch.expected.to_string(),
117 actual: mismatch.actual,
118 diff: mismatch.diff,
119 },
120 ));
121 }
122 }
123
124 #[track_caller]
127 pub fn check(&mut self, result: TestResult) {
128 if let Err(error) = result {
129 self.record(error);
130 }
131 }
132
133 #[track_caller]
138 pub fn context(&mut self, message: impl Into<Cow<'static, str>>) -> SoftScope<'_> {
139 self.context.push(ContextFrame::new(message));
140 SoftScope { asserter: self }
141 }
142
143 fn record(&mut self, mut error: TestError) {
147 if !self.context.is_empty() {
148 let mut frames = self.context.clone();
149 frames.append(&mut error.context);
150 error.context = frames;
151 }
152 self.errors.push(error);
153 }
154
155 #[track_caller]
159 fn into_result(self) -> TestResult {
160 if self.errors.is_empty() {
161 return Ok(());
162 }
163 let count = self.errors.len();
164 let noun = if count == 1 {
165 "soft assertion"
166 } else {
167 "soft assertions"
168 };
169 Err(TestError::new(ErrorKind::Assertion)
170 .with_message(format!("{count} {noun} failed"))
171 .with_payload(Payload::Multiple(self.errors)))
172 }
173}
174
175pub struct SoftScope<'a> {
184 asserter: &'a mut SoftAsserter,
185}
186
187impl SoftScope<'_> {
188 #[track_caller]
191 pub fn expect<T, M>(&mut self, actual: &T, matcher: M)
192 where
193 T: ?Sized,
194 M: Matcher<T>,
195 {
196 self.asserter.expect(actual, matcher);
197 }
198
199 #[track_caller]
202 pub fn check(&mut self, result: TestResult) {
203 self.asserter.check(result);
204 }
205
206 #[track_caller]
210 pub fn context(&mut self, message: impl Into<Cow<'static, str>>) -> SoftScope<'_> {
211 self.asserter.context(message)
212 }
213}
214
215impl Drop for SoftScope<'_> {
216 fn drop(&mut self) {
217 self.asserter.context.pop();
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use test_better_core::{Payload, TestError, TestResult};
224
225 use super::*;
226 use crate::{contains_str, eq, expect, is_true};
227
228 #[test]
229 fn soft_with_no_failures_returns_ok() -> TestResult {
230 let result = soft(|s| {
231 s.expect(&2, eq(2));
232 s.check(Ok(()));
233 });
234 expect!(result.is_ok()).to(is_true())?;
235 Ok(())
236 }
237
238 #[test]
239 fn soft_collects_every_failure_each_with_its_own_location() -> TestResult {
240 let result = soft(|s| {
241 s.expect(&1, eq(2));
242 s.expect(&3, eq(4));
243 s.expect(&5, eq(6));
244 });
245 let error = result.expect_err("three soft assertions failed");
246
247 let rendered = error.to_string();
248 expect!(rendered.contains("3 soft assertions failed")).to(is_true())?;
249 expect!(rendered.contains("3 failures")).to(is_true())?;
250
251 match error.payload.as_deref() {
252 Some(Payload::Multiple(errors)) => {
253 expect!(errors.len()).to(eq(3))?;
254 let lines: Vec<u32> = errors.iter().map(|e| e.location.line()).collect();
257 expect!(lines[0] != lines[1] && lines[1] != lines[2] && lines[0] != lines[2])
258 .to(is_true())?;
259 }
260 _ => return Err(TestError::assertion("expected a Multiple payload")),
261 }
262 Ok(())
263 }
264
265 #[test]
266 fn soft_check_records_an_err_and_ignores_ok() -> TestResult {
267 let result = soft(|s| {
268 s.check(Ok(()));
269 s.check(Err(TestError::assertion("a recorded failure")));
270 });
271 let error = result.expect_err("one recorded check failed");
272 expect!(error.to_string().contains("a recorded failure")).to(is_true())?;
273 Ok(())
274 }
275
276 #[test]
277 fn soft_check_preserves_the_recorded_error_location() -> TestResult {
278 let recorded = TestError::assertion("from elsewhere");
279 let recorded_line = recorded.location.line();
280 let result = soft(|s| s.check(Err(recorded)));
281 let error = result.expect_err("one recorded check failed");
282 match error.payload.as_deref() {
283 Some(Payload::Multiple(errors)) => {
284 expect!(errors[0].location.line()).to(eq(recorded_line))?;
285 }
286 _ => return Err(TestError::assertion("expected a Multiple payload")),
287 }
288 Ok(())
289 }
290
291 #[test]
292 fn context_scope_attaches_a_frame_to_recorded_failures() -> TestResult {
293 let result = soft(|s| {
294 let mut scope = s.context("while validating the user");
295 scope.expect(&1, eq(2));
296 });
297 let error = result.expect_err("one soft assertion failed");
298 match error.payload.as_deref() {
299 Some(Payload::Multiple(errors)) => {
300 let frames: Vec<&str> = errors[0]
301 .context
302 .iter()
303 .map(|frame| frame.message.as_ref())
304 .collect();
305 expect!(frames).to(eq(vec!["while validating the user"]))?;
306 }
307 _ => return Err(TestError::assertion("expected a Multiple payload")),
308 }
309 Ok(())
310 }
311
312 #[test]
313 fn context_scope_ends_when_the_scope_is_dropped() -> TestResult {
314 let result = soft(|s| {
315 {
316 let mut scope = s.context("inside the scope");
317 scope.expect(&1, eq(2));
318 }
319 s.expect(&3, eq(4));
321 });
322 let error = result.expect_err("two soft assertions failed");
323 match error.payload.as_deref() {
324 Some(Payload::Multiple(errors)) => {
325 expect!(errors[0].context.len()).to(eq(1usize))?;
326 expect!(errors[1].context.len()).to(eq(0usize))?;
327 }
328 _ => return Err(TestError::assertion("expected a Multiple payload")),
329 }
330 Ok(())
331 }
332
333 #[test]
334 fn nested_context_scopes_stack_outermost_first() -> TestResult {
335 let result = soft(|s| {
336 let mut outer = s.context("while validating the user");
337 outer.expect(&1, eq(2));
338 let mut inner = outer.context("while checking the email");
339 inner.expect(&"bad", contains_str("@"));
340 });
341 let error = result.expect_err("two soft assertions failed");
342 let rendered = error.to_string();
343 expect!(rendered.contains("while validating the user")).to(is_true())?;
344 expect!(rendered.contains("while checking the email")).to(is_true())?;
345
346 match error.payload.as_deref() {
347 Some(Payload::Multiple(errors)) => {
348 let outer_frames: Vec<&str> = errors[0]
349 .context
350 .iter()
351 .map(|frame| frame.message.as_ref())
352 .collect();
353 let inner_frames: Vec<&str> = errors[1]
354 .context
355 .iter()
356 .map(|frame| frame.message.as_ref())
357 .collect();
358 expect!(outer_frames).to(eq(vec!["while validating the user"]))?;
359 expect!(inner_frames).to(eq(vec![
360 "while validating the user",
361 "while checking the email",
362 ]))?;
363 }
364 _ => return Err(TestError::assertion("expected a Multiple payload")),
365 }
366 Ok(())
367 }
368}