| 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 | //! |
| 16 | //! let actual = 2 + 2; |
| 17 | //! let expected = expect!["5" ]; // or expect![["5"]] |
| 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, the `expect!` macro uses `file!`, `line!` and `column!` to |
| 36 | //! record source position at compile time. At runtime, this position is used |
| 37 | //! to patch the 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_debug_eq` for verbose debug comparison. Note |
| 44 | //! that 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 too verbose to include inline, you can store it in |
| 69 | //! an external file using the `expect_file!` macro: |
| 70 | //! |
| 71 | //! ```no_run |
| 72 | //! use expect_test::expect_file; |
| 73 | //! |
| 74 | //! let actual = 42; |
| 75 | //! let expected = expect_file!["./the-answer.txt" ]; |
| 76 | //! expected.assert_eq(&actual.to_string()); |
| 77 | //! ``` |
| 78 | //! |
| 79 | //! File path is relative to the current file. |
| 80 | //! |
| 81 | //! # Suggested Workflows |
| 82 | //! |
| 83 | //! I like to use data-driven tests with `expect_test`. I usually define a |
| 84 | //! single 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 | //! |
| 108 | //! When writing a new test, I usually copy-paste an old one, leave the `expect` |
| 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! |
| 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) - a 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 stable, |
| 134 | //! it is planned to not release any major versions past 1.0. |
| 135 | //! |
| 136 | //! ## Minimal Supported Rust Version |
| 137 | //! |
| 138 | //! This crate's minimum supported `rustc` version is `1.60.0`. MSRV is updated |
| 139 | //! conservatively, supporting roughly 10 minor versions of `rustc`. MSRV bump |
| 140 | //! is not considered semver breaking, but will require at least minor version |
| 141 | //! bump. |
| 142 | use std::{ |
| 143 | collections::HashMap, |
| 144 | convert::TryInto, |
| 145 | env, fmt, fs, mem, |
| 146 | ops::Range, |
| 147 | panic, |
| 148 | path::{Path, PathBuf}, |
| 149 | sync::Mutex, |
| 150 | }; |
| 151 | |
| 152 | use once_cell::sync::{Lazy, OnceCell}; |
| 153 | |
| 154 | const HELP: &str = " |
| 155 | You can update all `expect!` tests by running: |
| 156 | |
| 157 | env UPDATE_EXPECT=1 cargo test |
| 158 | |
| 159 | To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer. |
| 160 | " ; |
| 161 | |
| 162 | fn update_expect() -> bool { |
| 163 | env::var(key:"UPDATE_EXPECT" ).is_ok() |
| 164 | } |
| 165 | |
| 166 | /// Creates an instance of `Expect` from string literal: |
| 167 | /// |
| 168 | /// ``` |
| 169 | /// # use expect_test::expect; |
| 170 | /// expect![[" |
| 171 | /// Foo { value: 92 } |
| 172 | /// " ]]; |
| 173 | /// expect![r#"{"Foo": 92}"# ]; |
| 174 | /// ``` |
| 175 | /// |
| 176 | /// Leading indentation is stripped. |
| 177 | #[macro_export ] |
| 178 | macro_rules! expect { |
| 179 | [$data:literal] => { $crate::expect![[$data]] }; |
| 180 | [[$data:literal]] => {$crate::Expect { |
| 181 | position: $crate::Position { |
| 182 | file: file!(), |
| 183 | line: line!(), |
| 184 | column: column!(), |
| 185 | }, |
| 186 | data: $data, |
| 187 | indent: true, |
| 188 | }}; |
| 189 | [] => { $crate::expect![["" ]] }; |
| 190 | [[]] => { $crate::expect![["" ]] }; |
| 191 | } |
| 192 | |
| 193 | /// Creates an instance of `ExpectFile` from relative or absolute path: |
| 194 | /// |
| 195 | /// ``` |
| 196 | /// # use expect_test::expect_file; |
| 197 | /// expect_file!["./test_data/bar.html" ]; |
| 198 | /// ``` |
| 199 | #[macro_export ] |
| 200 | macro_rules! expect_file { |
| 201 | [$path:expr] => {$crate::ExpectFile { |
| 202 | path: std::path::PathBuf::from($path), |
| 203 | position: file!(), |
| 204 | }}; |
| 205 | } |
| 206 | |
| 207 | /// Self-updating string literal. |
| 208 | #[derive (Debug)] |
| 209 | pub struct Expect { |
| 210 | #[doc (hidden)] |
| 211 | pub position: Position, |
| 212 | #[doc (hidden)] |
| 213 | pub data: &'static str, |
| 214 | #[doc (hidden)] |
| 215 | pub indent: bool, |
| 216 | } |
| 217 | |
| 218 | /// Self-updating file. |
| 219 | #[derive (Debug)] |
| 220 | pub struct ExpectFile { |
| 221 | #[doc (hidden)] |
| 222 | pub path: PathBuf, |
| 223 | #[doc (hidden)] |
| 224 | pub position: &'static str, |
| 225 | } |
| 226 | |
| 227 | /// Position of original `expect!` in the source file. |
| 228 | #[derive (Debug)] |
| 229 | pub struct Position { |
| 230 | #[doc (hidden)] |
| 231 | pub file: &'static str, |
| 232 | #[doc (hidden)] |
| 233 | pub line: u32, |
| 234 | #[doc (hidden)] |
| 235 | pub column: u32, |
| 236 | } |
| 237 | |
| 238 | impl fmt::Display for Position { |
| 239 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 240 | write!(f, " {}: {}: {}" , self.file, self.line, self.column) |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | #[derive (Clone, Copy)] |
| 245 | enum StrLitKind { |
| 246 | Normal, |
| 247 | Raw(usize), |
| 248 | } |
| 249 | |
| 250 | impl StrLitKind { |
| 251 | fn write_start(self, w: &mut impl std::fmt::Write) -> std::fmt::Result { |
| 252 | match self { |
| 253 | Self::Normal => write!(w, " \"" ), |
| 254 | Self::Raw(n) => { |
| 255 | write!(w, "r" )?; |
| 256 | for _ in 0..n { |
| 257 | write!(w, "#" )?; |
| 258 | } |
| 259 | write!(w, " \"" ) |
| 260 | } |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | fn write_end(self, w: &mut impl std::fmt::Write) -> std::fmt::Result { |
| 265 | match self { |
| 266 | Self::Normal => write!(w, " \"" ), |
| 267 | Self::Raw(n) => { |
| 268 | write!(w, " \"" )?; |
| 269 | for _ in 0..n { |
| 270 | write!(w, "#" )?; |
| 271 | } |
| 272 | Ok(()) |
| 273 | } |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | impl Expect { |
| 279 | /// Checks if this expect is equal to `actual`. |
| 280 | pub fn assert_eq(&self, actual: &str) { |
| 281 | let trimmed = self.trimmed(); |
| 282 | if trimmed == actual { |
| 283 | return; |
| 284 | } |
| 285 | Runtime::fail_expect(self, &trimmed, actual); |
| 286 | } |
| 287 | /// Checks if this expect 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 | /// If `true` (default), in-place update will indent the string literal. |
| 293 | pub fn indent(&mut self, yes: bool) { |
| 294 | self.indent = yes; |
| 295 | } |
| 296 | |
| 297 | /// Returns the content of this expect. |
| 298 | pub fn data(&self) -> &str { |
| 299 | self.data |
| 300 | } |
| 301 | |
| 302 | fn trimmed(&self) -> String { |
| 303 | if !self.data.contains(' \n' ) { |
| 304 | return self.data.to_string(); |
| 305 | } |
| 306 | trim_indent(self.data) |
| 307 | } |
| 308 | |
| 309 | fn locate(&self, file: &str) -> Location { |
| 310 | let mut target_line = None; |
| 311 | let mut line_start = 0; |
| 312 | for (i, line) in lines_with_ends(file).enumerate() { |
| 313 | if i == self.position.line as usize - 1 { |
| 314 | // `column` points to the first character of the macro invocation: |
| 315 | // |
| 316 | // expect![[r#""#]] expect![""] |
| 317 | // ^ ^ ^ ^ |
| 318 | // column offset offset |
| 319 | // |
| 320 | // Seek past the exclam, then skip any whitespace and |
| 321 | // the macro delimiter to get to our argument. |
| 322 | let byte_offset = line |
| 323 | .char_indices() |
| 324 | .skip((self.position.column - 1).try_into().unwrap()) |
| 325 | .skip_while(|&(_, c)| c != '!' ) |
| 326 | .skip(1) // ! |
| 327 | .skip_while(|&(_, c)| c.is_whitespace()) |
| 328 | .skip(1) // [({ |
| 329 | .skip_while(|&(_, c)| c.is_whitespace()) |
| 330 | .next() |
| 331 | .expect("Failed to parse macro invocation" ) |
| 332 | .0; |
| 333 | |
| 334 | let literal_start = line_start + byte_offset; |
| 335 | let indent = line.chars().take_while(|&it| it == ' ' ).count(); |
| 336 | target_line = Some((literal_start, indent)); |
| 337 | break; |
| 338 | } |
| 339 | line_start += line.len(); |
| 340 | } |
| 341 | let (literal_start, line_indent) = target_line.unwrap(); |
| 342 | |
| 343 | let lit_to_eof = &file[literal_start..]; |
| 344 | let lit_to_eof_trimmed = lit_to_eof.trim_start(); |
| 345 | |
| 346 | let literal_start = literal_start + (lit_to_eof.len() - lit_to_eof_trimmed.len()); |
| 347 | |
| 348 | let literal_len = |
| 349 | locate_end(lit_to_eof_trimmed).expect("Couldn't find closing delimiter for `expect!`." ); |
| 350 | let literal_range = literal_start..literal_start + literal_len; |
| 351 | Location { line_indent, literal_range } |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | fn locate_end(arg_start_to_eof: &str) -> Option<usize> { |
| 356 | match arg_start_to_eof.chars().next()? { |
| 357 | c: char if c.is_whitespace() => panic!("skip whitespace before calling `locate_end`" ), |
| 358 | |
| 359 | // expect![[]] |
| 360 | '[' => { |
| 361 | let str_start_to_eof: &str = arg_start_to_eof[1..].trim_start(); |
| 362 | let str_len: usize = find_str_lit_len(str_lit_to_eof:str_start_to_eof)?; |
| 363 | let str_end_to_eof: &str = &str_start_to_eof[str_len..]; |
| 364 | let closing_brace_offset: usize = str_end_to_eof.find(']' )?; |
| 365 | Some((arg_start_to_eof.len() - str_end_to_eof.len()) + closing_brace_offset + 1) |
| 366 | } |
| 367 | |
| 368 | // expect![] | expect!{} | expect!() |
| 369 | ']' | '}' | ')' => Some(0), |
| 370 | |
| 371 | // expect!["..."] | expect![r#"..."#] |
| 372 | _ => find_str_lit_len(str_lit_to_eof:arg_start_to_eof), |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | /// Parses a string literal, returning the byte index of its last character |
| 377 | /// (either a quote or a hash). |
| 378 | fn find_str_lit_len(str_lit_to_eof: &str) -> Option<usize> { |
| 379 | use StrLitKind::*; |
| 380 | |
| 381 | fn try_find_n_hashes( |
| 382 | s: &mut impl Iterator<Item = char>, |
| 383 | desired_hashes: usize, |
| 384 | ) -> Option<(usize, Option<char>)> { |
| 385 | let mut n = 0; |
| 386 | loop { |
| 387 | match s.next()? { |
| 388 | '#' => n += 1, |
| 389 | c => return Some((n, Some(c))), |
| 390 | } |
| 391 | |
| 392 | if n == desired_hashes { |
| 393 | return Some((n, None)); |
| 394 | } |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | let mut s = str_lit_to_eof.chars(); |
| 399 | let kind = match s.next()? { |
| 400 | '"' => Normal, |
| 401 | 'r' => { |
| 402 | let (n, c) = try_find_n_hashes(&mut s, usize::MAX)?; |
| 403 | if c != Some('"' ) { |
| 404 | return None; |
| 405 | } |
| 406 | Raw(n) |
| 407 | } |
| 408 | _ => return None, |
| 409 | }; |
| 410 | |
| 411 | let mut oldc = None; |
| 412 | loop { |
| 413 | let c = oldc.take().or_else(|| s.next())?; |
| 414 | match (c, kind) { |
| 415 | (' \\' , Normal) => { |
| 416 | let _escaped = s.next()?; |
| 417 | } |
| 418 | ('"' , Normal) => break, |
| 419 | ('"' , Raw(0)) => break, |
| 420 | ('"' , Raw(n)) => { |
| 421 | let (seen, c) = try_find_n_hashes(&mut s, n)?; |
| 422 | if seen == n { |
| 423 | break; |
| 424 | } |
| 425 | oldc = c; |
| 426 | } |
| 427 | _ => {} |
| 428 | } |
| 429 | } |
| 430 | |
| 431 | Some(str_lit_to_eof.len() - s.as_str().len()) |
| 432 | } |
| 433 | |
| 434 | impl ExpectFile { |
| 435 | /// Checks if file contents is equal to `actual`. |
| 436 | pub fn assert_eq(&self, actual: &str) { |
| 437 | let expected = self.data(); |
| 438 | if actual == expected { |
| 439 | return; |
| 440 | } |
| 441 | Runtime::fail_file(self, &expected, actual); |
| 442 | } |
| 443 | /// Checks if file contents is equal to `format!("{:#?}", actual)`. |
| 444 | pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) { |
| 445 | let actual = format!(" {:#?}\n" , actual); |
| 446 | self.assert_eq(&actual) |
| 447 | } |
| 448 | /// Returns the content of this expect. |
| 449 | pub fn data(&self) -> String { |
| 450 | fs::read_to_string(self.abs_path()).unwrap_or_default().replace(" \r\n" , " \n" ) |
| 451 | } |
| 452 | fn write(&self, contents: &str) { |
| 453 | fs::write(self.abs_path(), contents).unwrap() |
| 454 | } |
| 455 | fn abs_path(&self) -> PathBuf { |
| 456 | if self.path.is_absolute() { |
| 457 | self.path.to_owned() |
| 458 | } else { |
| 459 | let dir = Path::new(self.position).parent().unwrap(); |
| 460 | to_abs_ws_path(&dir.join(&self.path)) |
| 461 | } |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | #[derive (Default)] |
| 466 | struct Runtime { |
| 467 | help_printed: bool, |
| 468 | per_file: HashMap<&'static str, FileRuntime>, |
| 469 | } |
| 470 | static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default); |
| 471 | |
| 472 | impl Runtime { |
| 473 | fn fail_expect(expect: &Expect, expected: &str, actual: &str) { |
| 474 | let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
| 475 | if update_expect() { |
| 476 | println!(" \x1b[1m \x1b[92mupdating \x1b[0m: {}" , expect.position); |
| 477 | rt.per_file |
| 478 | .entry(expect.position.file) |
| 479 | .or_insert_with(|| FileRuntime::new(expect)) |
| 480 | .update(expect, actual); |
| 481 | return; |
| 482 | } |
| 483 | rt.panic(expect.position.to_string(), expected, actual); |
| 484 | } |
| 485 | fn fail_file(expect: &ExpectFile, expected: &str, actual: &str) { |
| 486 | let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
| 487 | if update_expect() { |
| 488 | println!(" \x1b[1m \x1b[92mupdating \x1b[0m: {}" , expect.path.display()); |
| 489 | expect.write(actual); |
| 490 | return; |
| 491 | } |
| 492 | rt.panic(expect.path.display().to_string(), expected, actual); |
| 493 | } |
| 494 | fn panic(&mut self, position: String, expected: &str, actual: &str) { |
| 495 | let print_help = !mem::replace(&mut self.help_printed, true); |
| 496 | let help = if print_help { HELP } else { "" }; |
| 497 | |
| 498 | let diff = dissimilar::diff(expected, actual); |
| 499 | |
| 500 | println!( |
| 501 | " \n |
| 502 | \x1b[1m \x1b[91merror \x1b[97m: expect test failed \x1b[0m |
| 503 | \x1b[1m \x1b[34m--> \x1b[0m {} |
| 504 | {} |
| 505 | \x1b[1mExpect \x1b[0m: |
| 506 | ---- |
| 507 | {} |
| 508 | ---- |
| 509 | |
| 510 | \x1b[1mActual \x1b[0m: |
| 511 | ---- |
| 512 | {} |
| 513 | ---- |
| 514 | |
| 515 | \x1b[1mDiff \x1b[0m: |
| 516 | ---- |
| 517 | {} |
| 518 | ---- |
| 519 | " , |
| 520 | position, |
| 521 | help, |
| 522 | expected, |
| 523 | actual, |
| 524 | format_chunks(diff) |
| 525 | ); |
| 526 | // Use resume_unwind instead of panic!() to prevent a backtrace, which is unnecessary noise. |
| 527 | panic::resume_unwind(Box::new(())); |
| 528 | } |
| 529 | } |
| 530 | |
| 531 | struct FileRuntime { |
| 532 | path: PathBuf, |
| 533 | original_text: String, |
| 534 | patchwork: Patchwork, |
| 535 | } |
| 536 | |
| 537 | impl FileRuntime { |
| 538 | fn new(expect: &Expect) -> FileRuntime { |
| 539 | let path: PathBuf = to_abs_ws_path(Path::new(expect.position.file)); |
| 540 | let original_text: String = fs::read_to_string(&path).unwrap(); |
| 541 | let patchwork: Patchwork = Patchwork::new(original_text.clone()); |
| 542 | FileRuntime { path, original_text, patchwork } |
| 543 | } |
| 544 | fn update(&mut self, expect: &Expect, actual: &str) { |
| 545 | let loc: Location = expect.locate(&self.original_text); |
| 546 | let desired_indent: Option = if expect.indent { Some(loc.line_indent) } else { None }; |
| 547 | let patch: String = format_patch(desired_indent, patch:actual); |
| 548 | self.patchwork.patch(loc.literal_range, &patch); |
| 549 | fs::write(&self.path, &self.patchwork.text).unwrap() |
| 550 | } |
| 551 | } |
| 552 | |
| 553 | #[derive (Debug)] |
| 554 | struct Location { |
| 555 | line_indent: usize, |
| 556 | |
| 557 | /// The byte range of the argument to `expect!`, including the inner `[]` if it exists. |
| 558 | literal_range: Range<usize>, |
| 559 | } |
| 560 | |
| 561 | #[derive (Debug)] |
| 562 | struct Patchwork { |
| 563 | text: String, |
| 564 | indels: Vec<(Range<usize>, usize)>, |
| 565 | } |
| 566 | |
| 567 | impl Patchwork { |
| 568 | fn new(text: String) -> Patchwork { |
| 569 | Patchwork { text, indels: Vec::new() } |
| 570 | } |
| 571 | fn patch(&mut self, mut range: Range<usize>, patch: &str) { |
| 572 | self.indels.push((range.clone(), patch.len())); |
| 573 | self.indels.sort_by_key(|(delete: &Range, _insert: &usize)| delete.start); |
| 574 | |
| 575 | let (delete: usize, insert: usize) = self |
| 576 | .indels |
| 577 | .iter() |
| 578 | .take_while(|(delete, _)| delete.start < range.start) |
| 579 | .map(|(delete, insert)| (delete.end - delete.start, insert)) |
| 580 | .fold((0usize, 0usize), |(x1: usize, y1: usize), (x2: usize, y2: &usize)| (x1 + x2, y1 + y2)); |
| 581 | |
| 582 | for pos: &mut &mut usize in &mut [&mut range.start, &mut range.end] { |
| 583 | **pos -= delete; |
| 584 | **pos += insert; |
| 585 | } |
| 586 | |
| 587 | self.text.replace_range(range, &patch); |
| 588 | } |
| 589 | } |
| 590 | |
| 591 | fn lit_kind_for_patch(patch: &str) -> StrLitKind { |
| 592 | let has_dquote: bool = patch.chars().any(|c: char| c == '"' ); |
| 593 | if !has_dquote { |
| 594 | let has_bslash_or_newline: bool = patch.chars().any(|c: char| matches!(c, ' \\' | ' \n' )); |
| 595 | return if has_bslash_or_newline { StrLitKind::Raw(1) } else { StrLitKind::Normal }; |
| 596 | } |
| 597 | |
| 598 | // Find the maximum number of hashes that follow a double quote in the string. |
| 599 | // We need to use one more than that to delimit the string. |
| 600 | let leading_hashes: impl Fn(&str) -> usize = |s: &str| s.chars().take_while(|&c: char| c == '#' ).count(); |
| 601 | let max_hashes: usize = patch.split('"' ).map(leading_hashes).max().unwrap(); |
| 602 | StrLitKind::Raw(max_hashes + 1) |
| 603 | } |
| 604 | |
| 605 | fn format_patch(desired_indent: Option<usize>, patch: &str) -> String { |
| 606 | let lit_kind = lit_kind_for_patch(patch); |
| 607 | let indent = desired_indent.map(|it| " " .repeat(it)); |
| 608 | let is_multiline = patch.contains(' \n' ); |
| 609 | |
| 610 | let mut buf = String::new(); |
| 611 | if matches!(lit_kind, StrLitKind::Raw(_)) { |
| 612 | buf.push('[' ); |
| 613 | } |
| 614 | lit_kind.write_start(&mut buf).unwrap(); |
| 615 | if is_multiline { |
| 616 | buf.push(' \n' ); |
| 617 | } |
| 618 | let mut final_newline = false; |
| 619 | for line in lines_with_ends(patch) { |
| 620 | if is_multiline && !line.trim().is_empty() { |
| 621 | if let Some(indent) = &indent { |
| 622 | buf.push_str(indent); |
| 623 | buf.push_str(" " ); |
| 624 | } |
| 625 | } |
| 626 | buf.push_str(line); |
| 627 | final_newline = line.ends_with(' \n' ); |
| 628 | } |
| 629 | if final_newline { |
| 630 | if let Some(indent) = &indent { |
| 631 | buf.push_str(indent); |
| 632 | } |
| 633 | } |
| 634 | lit_kind.write_end(&mut buf).unwrap(); |
| 635 | if matches!(lit_kind, StrLitKind::Raw(_)) { |
| 636 | buf.push(']' ); |
| 637 | } |
| 638 | buf |
| 639 | } |
| 640 | |
| 641 | fn to_abs_ws_path(path: &Path) -> PathBuf { |
| 642 | if path.is_absolute() { |
| 643 | return path.to_owned(); |
| 644 | } |
| 645 | |
| 646 | static WORKSPACE_ROOT: OnceCell<PathBuf> = OnceCell::new(); |
| 647 | WORKSPACE_ROOT |
| 648 | .get_or_try_init(|| { |
| 649 | // Until https://github.com/rust-lang/cargo/issues/3946 is resolved, this |
| 650 | // is set with a hack like https://github.com/rust-lang/cargo/issues/3946#issuecomment-973132993 |
| 651 | if let Ok(workspace_root) = env::var("CARGO_WORKSPACE_DIR" ) { |
| 652 | return Ok(workspace_root.into()); |
| 653 | } |
| 654 | |
| 655 | // If a hack isn't used, we use a heuristic to find the "top-level" workspace. |
| 656 | // This fails in some cases, see https://github.com/rust-analyzer/expect-test/issues/33 |
| 657 | let my_manifest = env::var("CARGO_MANIFEST_DIR" )?; |
| 658 | let workspace_root = Path::new(&my_manifest) |
| 659 | .ancestors() |
| 660 | .filter(|it| it.join("Cargo.toml" ).exists()) |
| 661 | .last() |
| 662 | .unwrap() |
| 663 | .to_path_buf(); |
| 664 | |
| 665 | Ok(workspace_root) |
| 666 | }) |
| 667 | .unwrap_or_else(|_: env::VarError| { |
| 668 | panic!("No CARGO_MANIFEST_DIR env var and the path is relative: {}" , path.display()) |
| 669 | }) |
| 670 | .join(path) |
| 671 | } |
| 672 | |
| 673 | fn trim_indent(mut text: &str) -> String { |
| 674 | if text.starts_with(' \n' ) { |
| 675 | text = &text[1..]; |
| 676 | } |
| 677 | let indent: usize = text |
| 678 | .lines() |
| 679 | .filter(|it| !it.trim().is_empty()) |
| 680 | .map(|it| it.len() - it.trim_start().len()) |
| 681 | .min() |
| 682 | .unwrap_or(default:0); |
| 683 | |
| 684 | lines_with_endsimpl Iterator (text) |
| 685 | .map( |
| 686 | |line: &str| { |
| 687 | if line.len() <= indent { |
| 688 | line.trim_start_matches(' ' ) |
| 689 | } else { |
| 690 | &line[indent..] |
| 691 | } |
| 692 | }, |
| 693 | ) |
| 694 | .collect() |
| 695 | } |
| 696 | |
| 697 | fn lines_with_ends(text: &str) -> LinesWithEnds { |
| 698 | LinesWithEnds { text } |
| 699 | } |
| 700 | |
| 701 | struct LinesWithEnds<'a> { |
| 702 | text: &'a str, |
| 703 | } |
| 704 | |
| 705 | impl<'a> Iterator for LinesWithEnds<'a> { |
| 706 | type Item = &'a str; |
| 707 | fn next(&mut self) -> Option<&'a str> { |
| 708 | if self.text.is_empty() { |
| 709 | return None; |
| 710 | } |
| 711 | let idx: usize = self.text.find(' \n' ).map_or(self.text.len(), |it: usize| it + 1); |
| 712 | let (res: &str, next: &str) = self.text.split_at(mid:idx); |
| 713 | self.text = next; |
| 714 | Some(res) |
| 715 | } |
| 716 | } |
| 717 | |
| 718 | fn format_chunks(chunks: Vec<dissimilar::Chunk>) -> String { |
| 719 | let mut buf: String = String::new(); |
| 720 | for chunk: Chunk<'_> in chunks { |
| 721 | let formatted: String = match chunk { |
| 722 | dissimilar::Chunk::Equal(text: &str) => text.into(), |
| 723 | dissimilar::Chunk::Delete(text: &str) => format!(" \x1b[4m \x1b[31m {}\x1b[0m" , text), |
| 724 | dissimilar::Chunk::Insert(text: &str) => format!(" \x1b[4m \x1b[32m {}\x1b[0m" , text), |
| 725 | }; |
| 726 | buf.push_str(&formatted); |
| 727 | } |
| 728 | buf |
| 729 | } |
| 730 | |
| 731 | #[cfg (test)] |
| 732 | mod tests { |
| 733 | use super::*; |
| 734 | |
| 735 | #[test ] |
| 736 | fn test_trivial_assert() { |
| 737 | expect!["5" ].assert_eq("5" ); |
| 738 | } |
| 739 | |
| 740 | #[test ] |
| 741 | fn test_format_patch() { |
| 742 | let patch = format_patch(None, "hello \nworld \n" ); |
| 743 | expect![[r##" |
| 744 | [r#" |
| 745 | hello |
| 746 | world |
| 747 | "#]"## ]] |
| 748 | .assert_eq(&patch); |
| 749 | |
| 750 | let patch = format_patch(None, r"hello\tworld" ); |
| 751 | expect![[r##"[r#"hello\tworld"#]"## ]].assert_eq(&patch); |
| 752 | |
| 753 | let patch = format_patch(None, "{ \"foo \": 42}" ); |
| 754 | expect![[r##"[r#"{"foo": 42}"#]"## ]].assert_eq(&patch); |
| 755 | |
| 756 | let patch = format_patch(Some(0), "hello \nworld \n" ); |
| 757 | expect![[r##" |
| 758 | [r#" |
| 759 | hello |
| 760 | world |
| 761 | "#]"## ]] |
| 762 | .assert_eq(&patch); |
| 763 | |
| 764 | let patch = format_patch(Some(4), "single line" ); |
| 765 | expect![[r#""single line""# ]].assert_eq(&patch); |
| 766 | } |
| 767 | |
| 768 | #[test ] |
| 769 | fn test_patchwork() { |
| 770 | let mut patchwork = Patchwork::new("one two three" .to_string()); |
| 771 | patchwork.patch(4..7, "zwei" ); |
| 772 | patchwork.patch(0..3, "один" ); |
| 773 | patchwork.patch(8..13, "3" ); |
| 774 | expect![[r#" |
| 775 | Patchwork { |
| 776 | text: "один zwei 3", |
| 777 | indels: [ |
| 778 | ( |
| 779 | 0..3, |
| 780 | 8, |
| 781 | ), |
| 782 | ( |
| 783 | 4..7, |
| 784 | 4, |
| 785 | ), |
| 786 | ( |
| 787 | 8..13, |
| 788 | 1, |
| 789 | ), |
| 790 | ], |
| 791 | } |
| 792 | "# ]] |
| 793 | .assert_debug_eq(&patchwork); |
| 794 | } |
| 795 | |
| 796 | #[test ] |
| 797 | fn test_expect_file() { |
| 798 | expect_file!["./lib.rs" ].assert_eq(include_str!("./lib.rs" )) |
| 799 | } |
| 800 | |
| 801 | #[test ] |
| 802 | fn smoke_test_indent() { |
| 803 | fn check_indented(input: &str, mut expect: Expect) { |
| 804 | expect.indent(true); |
| 805 | expect.assert_eq(input); |
| 806 | } |
| 807 | fn check_not_indented(input: &str, mut expect: Expect) { |
| 808 | expect.indent(false); |
| 809 | expect.assert_eq(input); |
| 810 | } |
| 811 | |
| 812 | check_indented( |
| 813 | "\ |
| 814 | line1 |
| 815 | line2 |
| 816 | " , |
| 817 | expect![[r#" |
| 818 | line1 |
| 819 | line2 |
| 820 | "# ]], |
| 821 | ); |
| 822 | |
| 823 | check_not_indented( |
| 824 | "\ |
| 825 | line1 |
| 826 | line2 |
| 827 | " , |
| 828 | expect![[r#" |
| 829 | line1 |
| 830 | line2 |
| 831 | "# ]], |
| 832 | ); |
| 833 | } |
| 834 | |
| 835 | #[test ] |
| 836 | fn test_locate() { |
| 837 | macro_rules! check_locate { |
| 838 | ($( [[$s:literal]] ),* $(,)?) => {$({ |
| 839 | let lit = stringify!($s); |
| 840 | let with_trailer = format!("{} \t]] \n" , lit); |
| 841 | assert_eq!(locate_end(&with_trailer), Some(lit.len())); |
| 842 | })*}; |
| 843 | } |
| 844 | |
| 845 | // Check that we handle string literals containing "]]" correctly. |
| 846 | check_locate!( |
| 847 | [[r#"{ arr: [[1, 2], [3, 4]], other: "foo" } "# ]], |
| 848 | [["]]" ]], |
| 849 | [[" \"]]" ]], |
| 850 | [[r#""]]"# ]], |
| 851 | ); |
| 852 | |
| 853 | // Check `expect![[ ]]` as well. |
| 854 | assert_eq!(locate_end("]]" ), Some(0)); |
| 855 | } |
| 856 | |
| 857 | #[test ] |
| 858 | fn test_find_str_lit_len() { |
| 859 | macro_rules! check_str_lit_len { |
| 860 | ($( $s:literal ),* $(,)?) => {$({ |
| 861 | let lit = stringify!($s); |
| 862 | assert_eq!(find_str_lit_len(lit), Some(lit.len())); |
| 863 | })*} |
| 864 | } |
| 865 | |
| 866 | check_str_lit_len![ |
| 867 | r##"foa\""#"## , |
| 868 | r##" |
| 869 | |
| 870 | asdf][]]""""# |
| 871 | "## , |
| 872 | "" , |
| 873 | " \"" , |
| 874 | " \"\"" , |
| 875 | "# \"# \"#" , |
| 876 | ]; |
| 877 | } |
| 878 | } |
| 879 | |