]> git.proxmox.com Git - rustc.git/blame - vendor/expect-test/src/lib.rs
New upstream version 1.48.0~beta.8+dfsg1
[rustc.git] / vendor / expect-test / src / lib.rs
CommitLineData
3dfed10e
XL
1//! Minimalistic snapshot testing for Rust.
2//!
3//! # Introduction
4//!
5//! `expect_test` is a small addition over plain `assert_eq!` testing approach,
6//! which allows to automatically update tests results.
7//!
8//! The core of the library is the `expect!` macro. It can be though of as a
9//! super-charged string literal, which can update itself.
10//!
11//! Let's see an example:
12//!
13//! ```no_run
14//! use expect_test::expect;
15//!
3dfed10e 16//! let actual = 2 + 2;
1b1a35ee 17//! let expected = expect![["5"]];
3dfed10e
XL
18//! expected.assert_eq(&actual.to_string())
19//! ```
20//!
21//! Running this code will produce a test failure, as `"5"` is indeed not equal
22//! to `"4"`. Running the test with `UPDATE_EXPECT=1` env variable however would
23//! "magically" update the code to:
24//!
25//! ```no_run
26//! # use expect_test::expect;
27//! let actual = 2 + 2;
28//! let expected = expect![["4"]];
29//! expected.assert_eq(&actual.to_string())
30//! ```
31//!
32//! This becomes very useful when you have a lot of tests with verbose and
33//! potentially changing expected output.
34//!
35//! Under the hood, `expect!` macro uses `file!` and `line!` to record source
36//! position at compile time. At runtime, this position is used to patch the
37//! file in-place, if `UPDATE_EXPECT` is set.
38//!
39//! # Guide
40//!
41//! `expect!` returns an instance of `Expect` struct, which holds position
42//! information and a string literal. Use `Expect::assert_eq` for string
43//! comparison. Use `Expect::assert_eq` for verbose debug comparison. Note that
44//! leading indentation is automatically removed.
45//!
46//! ```
47//! use expect_test::expect;
48//!
49//! #[derive(Debug)]
50//! struct Foo {
51//! value: i32,
52//! }
53//!
54//! let actual = Foo { value: 92 };
55//! let expected = expect![["
56//! Foo {
57//! value: 92,
58//! }
59//! "]];
60//! expected.assert_debug_eq(&actual);
61//! ```
62//!
63//! Be careful with `assert_debug_eq` -- in general, stability of the debug
64//! representation is not guaranteed. However, even if it changes, you can
65//! quickly update all the tests by running the test suite with `UPDATE_EXPECT`
66//! environmental variable set.
67//!
68//! If the expected data is to verbose for inline test, you can store it in the
69//! external file using `expect_file!` macro:
70//!
71//! ```no_run
72//! use expect_test::expect_file;
73//!
74//! let actual = 42;
1b1a35ee 75//! let expected = expect_file!["./the-answer.txt"];
3dfed10e
XL
76//! expected.assert_eq(&actual.to_string());
77//! ```
78//!
1b1a35ee 79//! File path is relative to the current file.
3dfed10e
XL
80//!
81//! # Suggested Workflows
82//!
83//! I like to use data-driven test with `expect_test`. I usually define a single
84//! driver function `check` and then call it from individual tests:
85//!
86//! ```
87//! use expect_test::{expect, Expect};
88//!
89//! fn check(actual: i32, expect: Expect) {
90//! let actual = actual.to_string();
91//! expect.assert_eq(&actual);
92//! }
93//!
94//! #[test]
95//! fn test_addition() {
96//! check(90 + 2, expect![["92"]]);
97//! }
98//!
99//! #[test]
100//! fn test_multiplication() {
101//! check(46 * 2, expect![["92"]]);
102//! }
103//! ```
104//!
105//! Each test's body is a single call to `check`. All the variation in tests
106//! comes from the input data.
107//!
1b1a35ee 108//! When writing a new test, I usually copy-paste an old one, leave the `expect`
3dfed10e
XL
109//! blank and use `UPDATE_EXPECT` to fill the value for me:
110//!
111//! ```
112//! # use expect_test::{expect, Expect};
113//! # fn check(_: i32, _: Expect) {}
114//! #[test]
115//! fn test_division() {
116//! check(92 / 2, expect![[]])
117//! }
118//! ```
119//!
120//! See
121//! https://blog.janestreet.com/using-ascii-waveforms-to-test-hardware-designs/
122//! for a cool example of snapshot testing in the wild!
1b1a35ee
XL
123//!
124//! # Alternatives
125//!
126//! * [insta](https://crates.io/crates/insta) -- a more feature full snapshot
127//! testing library.
128//! * [k9](https://crates.io/crates/k9) -- testing library which includes
129//! support for snapshot testing among other things.
130//!
131//! # Maintenance status
132//!
133//! The main customer of this library is rust-analyzer. The library is expected
134//! to be relatively stable, but, if the need arises, it could be significantly
135//! reworked to fit rust-analyzer better.
136//!
137//! MSRV: latest stable.
3dfed10e
XL
138use std::{
139 collections::HashMap,
140 env, fmt, fs, mem,
141 ops::Range,
142 panic,
143 path::{Path, PathBuf},
144 sync::Mutex,
145};
146
147use difference::Changeset;
148use once_cell::sync::Lazy;
149
150const HELP: &str = "
151You can update all `expect![[]]` tests by running:
152
153 env UPDATE_EXPECT=1 cargo test
154
155To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer.
156";
157
158fn update_expect() -> bool {
159 env::var("UPDATE_EXPECT").is_ok()
160}
161
162/// Creates an instance of `Expect` from string literal:
163///
164/// ```
165/// # use expect_test::expect;
166/// expect![["
167/// Foo { value: 92 }
168/// "]];
169/// ```
170///
171/// Leading indentation is stripped.
172#[macro_export]
173macro_rules! expect {
174 [[$data:literal]] => {$crate::Expect {
175 position: $crate::Position {
176 file: file!(),
177 line: line!(),
178 column: column!(),
179 },
180 data: $data,
181 }};
182 [[]] => { $crate::expect![[""]] };
183}
184
1b1a35ee 185/// Creates an instance of `ExpectFile` from relative or absolute path:
3dfed10e
XL
186///
187/// ```
188/// # use expect_test::expect_file;
1b1a35ee 189/// expect_file!["./test_data/bar.html"];
3dfed10e
XL
190/// ```
191#[macro_export]
192macro_rules! expect_file {
193 [$path:expr] => {$crate::ExpectFile {
1b1a35ee
XL
194 path: std::path::PathBuf::from($path),
195 position: file!(),
3dfed10e
XL
196 }};
197}
198
199/// Self-updating string literal.
200#[derive(Debug)]
201pub struct Expect {
202 #[doc(hidden)]
203 pub position: Position,
204 #[doc(hidden)]
205 pub data: &'static str,
206}
207
208/// Self-updating file.
209#[derive(Debug)]
210pub struct ExpectFile {
211 #[doc(hidden)]
212 pub path: PathBuf,
1b1a35ee
XL
213 #[doc(hidden)]
214 pub position: &'static str,
3dfed10e
XL
215}
216
217/// Position of original `expect!` in the source file.
218#[derive(Debug)]
219pub struct Position {
220 #[doc(hidden)]
221 pub file: &'static str,
222 #[doc(hidden)]
223 pub line: u32,
224 #[doc(hidden)]
225 pub column: u32,
226}
227
228impl fmt::Display for Position {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 write!(f, "{}:{}:{}", self.file, self.line, self.column)
231 }
232}
233
234impl Expect {
235 /// Checks if this expect is equal to `actual`.
236 pub fn assert_eq(&self, actual: &str) {
237 let trimmed = self.trimmed();
238 if trimmed == actual {
239 return;
240 }
241 Runtime::fail_expect(self, &trimmed, actual);
242 }
243 /// Checks if this expect is equal to `format!("{:#?}", actual)`.
244 pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) {
245 let actual = format!("{:#?}\n", actual);
246 self.assert_eq(&actual)
247 }
248
249 fn trimmed(&self) -> String {
250 if !self.data.contains('\n') {
251 return self.data.to_string();
252 }
253 trim_indent(self.data)
254 }
255
256 fn locate(&self, file: &str) -> Location {
257 let mut target_line = None;
258 let mut line_start = 0;
259 for (i, line) in lines_with_ends(file).enumerate() {
260 if i == self.position.line as usize - 1 {
261 let pat = "expect![[";
262 let offset = line.find(pat).unwrap();
263 let literal_start = line_start + offset + pat.len();
264 let indent = line.chars().take_while(|&it| it == ' ').count();
265 target_line = Some((literal_start, indent));
266 break;
267 }
268 line_start += line.len();
269 }
270 let (literal_start, line_indent) = target_line.unwrap();
271 let literal_length =
272 file[literal_start..].find("]]").expect("Couldn't find matching `]]` for `expect![[`.");
273 let literal_range = literal_start..literal_start + literal_length;
274 Location { line_indent, literal_range }
275 }
276}
277
278impl ExpectFile {
279 /// Checks if file contents is equal to `actual`.
280 pub fn assert_eq(&self, actual: &str) {
281 let expected = self.read();
282 if actual == expected {
283 return;
284 }
285 Runtime::fail_file(self, &expected, actual);
286 }
287 /// Checks if file contents is equal to `format!("{:#?}", actual)`.
288 pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) {
289 let actual = format!("{:#?}\n", actual);
290 self.assert_eq(&actual)
291 }
292 fn read(&self) -> String {
293 fs::read_to_string(self.abs_path()).unwrap_or_default().replace("\r\n", "\n")
294 }
295 fn write(&self, contents: &str) {
296 fs::write(self.abs_path(), contents).unwrap()
297 }
298 fn abs_path(&self) -> PathBuf {
1b1a35ee
XL
299 let dir = Path::new(self.position).parent().unwrap();
300 WORKSPACE_ROOT.join(dir).join(&self.path)
3dfed10e
XL
301 }
302}
303
304#[derive(Default)]
305struct Runtime {
306 help_printed: bool,
307 per_file: HashMap<&'static str, FileRuntime>,
308}
309static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default);
310
311impl Runtime {
312 fn fail_expect(expect: &Expect, expected: &str, actual: &str) {
313 let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
314 if update_expect() {
315 println!("\x1b[1m\x1b[92mupdating\x1b[0m: {}", expect.position);
316 rt.per_file
317 .entry(expect.position.file)
318 .or_insert_with(|| FileRuntime::new(expect))
319 .update(expect, actual);
320 return;
321 }
322 rt.panic(expect.position.to_string(), expected, actual);
323 }
3dfed10e
XL
324 fn fail_file(expect: &ExpectFile, expected: &str, actual: &str) {
325 let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
326 if update_expect() {
327 println!("\x1b[1m\x1b[92mupdating\x1b[0m: {}", expect.path.display());
328 expect.write(actual);
329 return;
330 }
331 rt.panic(expect.path.display().to_string(), expected, actual);
332 }
3dfed10e
XL
333 fn panic(&mut self, position: String, expected: &str, actual: &str) {
334 let print_help = !mem::replace(&mut self.help_printed, true);
335 let help = if print_help { HELP } else { "" };
336
337 let diff = Changeset::new(actual, expected, "\n");
338
339 println!(
340 "\n
341\x1b[1m\x1b[91merror\x1b[97m: expect test failed\x1b[0m
342 \x1b[1m\x1b[34m-->\x1b[0m {}
343{}
344\x1b[1mExpect\x1b[0m:
345----
346{}
347----
348
349\x1b[1mActual\x1b[0m:
350----
351{}
352----
353
354\x1b[1mDiff\x1b[0m:
355----
356{}
357----
358",
359 position, help, expected, actual, diff
360 );
361 // Use resume_unwind instead of panic!() to prevent a backtrace, which is unnecessary noise.
362 panic::resume_unwind(Box::new(()));
363 }
364}
365
366struct FileRuntime {
367 path: PathBuf,
368 original_text: String,
369 patchwork: Patchwork,
370}
371
372impl FileRuntime {
373 fn new(expect: &Expect) -> FileRuntime {
374 let path = WORKSPACE_ROOT.join(expect.position.file);
375 let original_text = fs::read_to_string(&path).unwrap();
376 let patchwork = Patchwork::new(original_text.clone());
377 FileRuntime { path, original_text, patchwork }
378 }
379 fn update(&mut self, expect: &Expect, actual: &str) {
380 let loc = expect.locate(&self.original_text);
381 let patch = format_patch(loc.line_indent.clone(), actual);
382 self.patchwork.patch(loc.literal_range, &patch);
383 fs::write(&self.path, &self.patchwork.text).unwrap()
384 }
385}
386
387#[derive(Debug)]
388struct Location {
389 line_indent: usize,
390 literal_range: Range<usize>,
391}
392
393#[derive(Debug)]
394struct Patchwork {
395 text: String,
396 indels: Vec<(Range<usize>, usize)>,
397}
398
399impl Patchwork {
400 fn new(text: String) -> Patchwork {
401 Patchwork { text, indels: Vec::new() }
402 }
403 fn patch(&mut self, mut range: Range<usize>, patch: &str) {
404 self.indels.push((range.clone(), patch.len()));
405 self.indels.sort_by_key(|(delete, _insert)| delete.start);
406
407 let (delete, insert) = self
408 .indels
409 .iter()
410 .take_while(|(delete, _)| delete.start < range.start)
411 .map(|(delete, insert)| (delete.end - delete.start, insert))
412 .fold((0usize, 0usize), |(x1, y1), (x2, y2)| (x1 + x2, y1 + y2));
413
414 for pos in &mut [&mut range.start, &mut range.end] {
415 **pos -= delete;
416 **pos += insert;
417 }
418
419 self.text.replace_range(range, &patch);
420 }
421}
422
423fn format_patch(line_indent: usize, patch: &str) -> String {
424 let mut max_hashes = 0;
425 let mut cur_hashes = 0;
426 for byte in patch.bytes() {
427 if byte != b'#' {
428 cur_hashes = 0;
429 continue;
430 }
431 cur_hashes += 1;
432 max_hashes = max_hashes.max(cur_hashes);
433 }
434 let hashes = &"#".repeat(max_hashes + 1);
435 let indent = &" ".repeat(line_indent);
436 let is_multiline = patch.contains('\n');
437
438 let mut buf = String::new();
439 buf.push('r');
440 buf.push_str(hashes);
441 buf.push('"');
442 if is_multiline {
443 buf.push('\n');
444 }
445 let mut final_newline = false;
446 for line in lines_with_ends(patch) {
447 if is_multiline && !line.trim().is_empty() {
448 buf.push_str(indent);
449 buf.push_str(" ");
450 }
451 buf.push_str(line);
452 final_newline = line.ends_with('\n');
453 }
454 if final_newline {
455 buf.push_str(indent);
456 }
457 buf.push('"');
458 buf.push_str(hashes);
459 buf
460}
461
462static WORKSPACE_ROOT: Lazy<PathBuf> = Lazy::new(|| {
463 let my_manifest = env::var("CARGO_MANIFEST_DIR").unwrap();
464 // Heuristic, see https://github.com/rust-lang/cargo/issues/3946
465 Path::new(&my_manifest)
466 .ancestors()
467 .filter(|it| it.join("Cargo.toml").exists())
468 .last()
469 .unwrap()
470 .to_path_buf()
471});
472
473fn trim_indent(mut text: &str) -> String {
474 if text.starts_with('\n') {
475 text = &text[1..];
476 }
477 let indent = text
478 .lines()
479 .filter(|it| !it.trim().is_empty())
480 .map(|it| it.len() - it.trim_start().len())
481 .min()
482 .unwrap_or(0);
483
484 lines_with_ends(text)
485 .map(
486 |line| {
487 if line.len() <= indent {
488 line.trim_start_matches(' ')
489 } else {
490 &line[indent..]
491 }
492 },
493 )
494 .collect()
495}
496
497fn lines_with_ends(text: &str) -> LinesWithEnds {
498 LinesWithEnds { text }
499}
500
501struct LinesWithEnds<'a> {
502 text: &'a str,
503}
504
505impl<'a> Iterator for LinesWithEnds<'a> {
506 type Item = &'a str;
507 fn next(&mut self) -> Option<&'a str> {
508 if self.text.is_empty() {
509 return None;
510 }
511 let idx = self.text.find('\n').map_or(self.text.len(), |it| it + 1);
512 let (res, next) = self.text.split_at(idx);
513 self.text = next;
514 Some(res)
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn test_format_patch() {
524 let patch = format_patch(0, "hello\nworld\n");
525 expect![[r##"
526 r#"
527 hello
528 world
529 "#"##]]
530 .assert_eq(&patch);
531
532 let patch = format_patch(4, "single line");
533 expect![[r##"r#"single line"#"##]].assert_eq(&patch);
534 }
535
536 #[test]
537 fn test_patchwork() {
538 let mut patchwork = Patchwork::new("one two three".to_string());
539 patchwork.patch(4..7, "zwei");
540 patchwork.patch(0..3, "один");
541 patchwork.patch(8..13, "3");
542 expect![[r#"
543 Patchwork {
544 text: "один zwei 3",
545 indels: [
546 (
547 0..3,
548 8,
549 ),
550 (
551 4..7,
552 4,
553 ),
554 (
555 8..13,
556 1,
557 ),
558 ],
559 }
560 "#]]
561 .assert_debug_eq(&patchwork);
562 }
1b1a35ee
XL
563
564 #[test]
565 fn test_expect_file() {
566 expect_file!["./lib.rs"].assert_eq(include_str!("./lib.rs"))
567 }
3dfed10e 568}