Repository: dandavison/delta
Pull request #876 (2022-01-02 00:11:42)

DeltaTest improvements
- Added .expect_contains(), .expect_raw_contains(), and .expect_contains_once() member functions to DeltaTestOutput.  These functions also output some helpful debug info when the assert fails.
- Separated .with_config_and_input() into .with_config() and (the tweaked) .with_input().
- Made .with_config() create a DeltaTest object (like .with() does).
- Renamed .with() as .with_args().
- Moved .explain_ansi() from DeltaTestOutput to DeltaTest, which must now be called prior to .with_input() since the latter stashes off both the raw_output & the processed output in the object.
- Changed .expect() and .expect_skip() to return Self so that they can continue a chain of multiple expect calls (e.g. a series of partial matches with different skip values).
- The processed output text can be accessed via `test_obj.output` (see also `test_obj.raw_output`).
- Renamed .expect_skip() to .expect_after_skip().
- Changed .expect() to start at the first line.
- Added .expect_after_header() that works like the old .expect().
- Renamed lines_match() to assert_lines_match() and made it match all lines (no skip number).
- Added assert_lines_match_after_skip() to work like the old lines_match() function, but with the .expect_after_skip() arg order. 
- Converted some old-style tests into DeltaTest style tests.

dandavison: cc @th1000s who's the creator of `DeltaTest`.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 51135de..63df776 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1053,9 +1053,9 @@ dependencies = [
 
 [[package]]
 name = "sysinfo"
-version = "0.22.3"
+version = "0.22.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b645b59c59114c25d3d554f781b0a1f1f01545d1d02f271bfb1c897bdfdfdcf3"
+checksum = "ccb37aa4af23791c584202d286ed9420e023e9d27e49d5a76215623f4bcc2502"
 dependencies = [
  "cfg-if 1.0.0",
  "core-foundation-sys",
diff --git a/Cargo.toml b/Cargo.toml
index cdcf455..6f46bcf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,7 +53,7 @@ default-features = false
 features = ["parsing", "assets", "yaml-load", "dump-load", "regex-onig"]
 
 [dependencies.sysinfo]
-version = "0.22.3"
+version = "0.22.4"
 # no default features to disable the use of threads
 default-features = false
 features = []
diff --git a/README.md b/README.md
index a85022d..bd26b3b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 <p align="center">
-  <img width=400px src="https://user-images.githubusercontent.com/52205/102416950-ae124300-3ff2-11eb-9d66-40d2aef4888f.png" alt="image" />
+  <img width=400px src="https://user-images.githubusercontent.com/52205/147996902-9829bd3f-cd33-466e-833e-49a6f3ebd623.png" alt="image" />
 </p>
 <p align="center">
   <a href="https://github.com/dandavison/delta/actions">
diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs
index fd2232f..65cf402 100644
--- a/src/features/line_numbers.rs
+++ b/src/features/line_numbers.rs
@@ -630,7 +630,7 @@ pub mod tests {
 
     #[test]
     fn test_two_minus_lines() {
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--line-numbers",
             "--line-numbers-left-format",
             "{nm:^4}⋮",
@@ -646,7 +646,7 @@ pub mod tests {
             "0 4",
         ])
         .with_input(TWO_MINUS_LINES_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
              #indent_mark
               1  ⋮    │a = 1
@@ -656,7 +656,7 @@ pub mod tests {
 
     #[test]
     fn test_two_plus_lines() {
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--line-numbers",
             "--line-numbers-left-format",
             "{nm:^4}⋮",
@@ -672,7 +672,7 @@ pub mod tests {
             "0 4",
         ])
         .with_input(TWO_PLUS_LINES_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
              #indent_mark
                  ⋮ 1  │a = 1
@@ -779,9 +779,9 @@ pub mod tests {
 
     #[test]
     fn test_line_numbers_continue_correctly() {
-        DeltaTest::with(&["--side-by-side", "--width", "44", "--line-fill-method=ansi"])
+        DeltaTest::with_args(&["--side-by-side", "--width", "44", "--line-fill-method=ansi"])
             .with_input(DIFF_PLUS_MINUS_WITH_1_CONTEXT_DIFF)
-            .expect(
+            .expect_after_header(
                 r#"
                 │ 1  │abc             │ 1  │abc
                 │ 2  │a = left side   │ 2  │a = right side
@@ -791,7 +791,7 @@ pub mod tests {
 
     #[test]
     fn test_line_numbers_continue_correctly_after_wrapping() {
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--side-by-side",
             "--width",
             "32",
@@ -802,7 +802,7 @@ pub mod tests {
             "@",
         ])
         .with_input(DIFF_PLUS_MINUS_WITH_1_CONTEXT_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
             │ 1  │abc       │ 1  │abc
             │ 2  │a = left @│ 2  │a = right@
@@ -821,9 +821,9 @@ pub mod tests {
             "@",
         ];
 
-        DeltaTest::with(cfg)
+        DeltaTest::with_args(cfg)
             .with_input(DIFF_WITH_LONGER_MINUS_1_CONTEXT)
-            .expect(
+            .expect_after_header(
                 r#"
                 │ 1  │abc            │ 1  │abc
                 │ 2  │a = one side   │ 2  │a = one longer@
@@ -831,9 +831,9 @@ pub mod tests {
                 │ 3  │xyz            │ 3  │xyz"#,
             );
 
-        DeltaTest::with(cfg)
+        DeltaTest::with_args(cfg)
             .with_input(DIFF_WITH_LONGER_PLUS_1_CONTEXT)
-            .expect(
+            .expect_after_header(
                 r#"
                 │ 1  │abc            │ 1  │abc
                 │ 2  │a = one longer@│ 2  │a = one side
@@ -841,9 +841,9 @@ pub mod tests {
                 │ 3  │xyz            │ 3  │xyz"#,
             );
 
-        DeltaTest::with(cfg)
+        DeltaTest::with_args(cfg)
             .with_input(DIFF_MISMATCH_LONGER_MINUS_1_CONTEXT)
-            .expect(
+            .expect_after_header(
                 r#"
                 │ 1  │abc            │ 1  │abc
                 │ 2  │a = left side @│    │
@@ -852,9 +852,9 @@ pub mod tests {
                 │ 3  │xyz            │ 3  │xyz"#,
             );
 
-        DeltaTest::with(cfg)
+        DeltaTest::with_args(cfg)
             .with_input(DIFF_MISMATCH_LONGER_PLUS_1_CONTEXT)
-            .expect(
+            .expect_after_header(
                 r#"
                 │ 1  │abc            │ 1  │abc
                 │ 2  │a = other one  │    │
diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs
index 9643169..fb0a819 100644
--- a/src/features/side_by_side.rs
+++ b/src/features/side_by_side.rs
@@ -597,9 +597,9 @@ pub mod tests {
 
     #[test]
     fn test_two_minus_lines() {
-        DeltaTest::with(&["--side-by-side", "--width", "40"])
+        DeltaTest::with_args(&["--side-by-side", "--width", "40"])
             .with_input(TWO_MINUS_LINES_DIFF)
-            .expect(
+            .expect_after_header(
                 r#"
                 │ 1  │a = 1         │    │
                 │ 2  │b = 23456     │    │"#,
@@ -608,7 +608,7 @@ pub mod tests {
 
     #[test]
     fn test_two_minus_lines_truncated() {
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--side-by-side",
             "--wrap-max-lines",
             "0",
@@ -616,9 +616,9 @@ pub mod tests {
             "28",
             "--line-fill-method=spaces",
         ])
-        .set_cfg(|cfg| cfg.truncation_symbol = ">".into())
+        .set_config(|cfg| cfg.truncation_symbol = ">".into())
         .with_input(TWO_MINUS_LINES_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
             │ 1  │a = 1   │    │
             │ 2  │b = 234>│    │"#,
@@ -627,14 +627,14 @@ pub mod tests {
 
     #[test]
     fn test_two_plus_lines() {
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--side-by-side",
             "--width",
             "41",
             "--line-fill-method=spaces",
         ])
         .with_input(TWO_PLUS_LINES_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
             │    │              │ 1  │a = 1         
             │    │              │ 2  │b = 234567    "#,
@@ -643,27 +643,27 @@ pub mod tests {
 
     #[test]
     fn test_two_plus_lines_spaces_and_ansi() {
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--side-by-side",
             "--width",
             "41",
             "--line-fill-method=spaces",
         ])
-        .with_input(TWO_PLUS_LINES_DIFF)
         .explain_ansi()
-        .expect(r#"
+        .with_input(TWO_PLUS_LINES_DIFF)
+        .expect_after_header(r#"
         (blue)│(88)    (blue)│(normal)              (blue)│(28) 1  (blue)│(231 22)a (203)=(231) (141)1(normal 22)         (normal)
         (blue)│(88)    (blue)│(normal)              (blue)│(28) 2  (blue)│(231 22)b (203)=(231) (141)234567(normal 22)    (normal)"#);
 
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--side-by-side",
             "--width",
             "41",
             "--line-fill-method=ansi",
         ])
-        .with_input(TWO_PLUS_LINES_DIFF)
         .explain_ansi()
-        .expect(r#"
+        .with_input(TWO_PLUS_LINES_DIFF)
+        .expect_after_header(r#"
         (blue)│(88)    (blue)│(normal)              (blue) │(28) 1  (blue)│(231 22)a (203)=(231) (141)1(normal)
         (blue)│(88)    (blue)│(normal)              (blue) │(28) 2  (blue)│(231 22)b (203)=(231) (141)234567(normal)"#);
     }
@@ -701,14 +701,14 @@ pub mod tests {
 
     #[test]
     fn test_one_minus_one_plus_line() {
-        DeltaTest::with(&[
+        DeltaTest::with_args(&[
             "--side-by-side",
             "--width",
             "40",
             "--line-fill-method=spaces",
         ])
         .with_input(ONE_MINUS_ONE_PLUS_LINE_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
             │ 1  │a = 1         │ 1  │a = 1
             │ 2  │b = 2         │ 2  │bb = 2        "#,
diff --git a/src/handlers/git_show_file.rs b/src/handlers/git_show_file.rs
index d5eeb51..eb26120 100644
--- a/src/handlers/git_show_file.rs
+++ b/src/handlers/git_show_file.rs
@@ -9,9 +9,7 @@ impl<'a> StateMachine<'a> {
         self.painter.emit()?;
         let mut handled_line = false;
         if matches!(self.state, State::Unknown) {
-            if let Some(process::CallingProcess::GitShow(_, extension)) =
-                process::calling_process().as_deref()
-            {
+            if let process::CallingProcess::GitShow(_, extension) = &*process::calling_process() {
                 self.state = State::GitShowFile;
                 self.painter.set_syntax(extension.as_deref());
                 self.painter.set_highlighter();
diff --git a/src/handlers/grep.rs b/src/handlers/grep.rs
index 32e1463..c3d6da6 100644
--- a/src/handlers/grep.rs
+++ b/src/handlers/grep.rs
@@ -246,8 +246,8 @@ fn get_code_style_sections<'b>(
 }
 
 fn make_output_config() -> GrepOutputConfig {
-    match process::calling_process().as_deref() {
-        Some(process::CallingProcess::GitGrep(command_line))
+    match &*process::calling_process() {
+        process::CallingProcess::GitGrep(command_line)
             if command_line.short_options.contains("-W")
                 || command_line.long_options.contains("--function-context") =>
         {
@@ -265,7 +265,7 @@ fn make_output_config() -> GrepOutputConfig {
                 pad_line_number: true,
             }
         }
-        Some(process::CallingProcess::GitGrep(command_line))
+        process::CallingProcess::GitGrep(command_line)
             if command_line.short_options.contains("-p")
                 || command_line.long_options.contains("--show-function") =>
         {
@@ -380,9 +380,8 @@ pub fn parse_grep_line(line: &str) -> Option<GrepLine> {
     if line.starts_with('{') {
         ripgrep_json::parse_line(line)
     } else {
-        match process::calling_process().as_deref() {
-            Some(process::CallingProcess::GitGrep(_))
-            | Some(process::CallingProcess::OtherGrep) => [
+        match &*process::calling_process() {
+            process::CallingProcess::GitGrep(_) | process::CallingProcess::OtherGrep => [
                 &*GREP_LINE_REGEX_ASSUMING_FILE_EXTENSION,
                 &*GREP_LINE_REGEX_ASSUMING_NO_INTERNAL_SEPARATOR_CHARS,
             ]
diff --git a/src/handlers/hunk.rs b/src/handlers/hunk.rs
index 2acf2ea..2abd418 100644
--- a/src/handlers/hunk.rs
+++ b/src/handlers/hunk.rs
@@ -27,13 +27,11 @@ lazy_static! {
 }
 
 fn compute_is_word_diff() -> bool {
-    match process::calling_process().as_deref() {
-        Some(
-            CallingProcess::GitDiff(cmd_line)
-            | CallingProcess::GitShow(cmd_line, _)
-            | CallingProcess::GitLog(cmd_line)
-            | CallingProcess::GitReflog(cmd_line),
-        ) => {
+    match &*process::calling_process() {
+        CallingProcess::GitDiff(cmd_line)
+        | CallingProcess::GitShow(cmd_line, _)
+        | CallingProcess::GitLog(cmd_line)
+        | CallingProcess::GitReflog(cmd_line) => {
             cmd_line.long_options.contains("--word-diff")
                 || cmd_line.long_options.contains("--word-diff-regex")
                 || cmd_line.long_options.contains("--color-words")
@@ -283,11 +281,11 @@ mod tests {
 
         #[test]
         fn test_word_diff() {
-            DeltaTest::with(&[])
+            DeltaTest::with_args(&[])
                 .with_calling_process("git diff --word-diff")
-                .with_input(GIT_DIFF_WORD_DIFF)
                 .explain_ansi()
-                .expect_skip(
+                .with_input(GIT_DIFF_WORD_DIFF)
+                .expect_after_skip(
                     11,
                     "
 #indent_mark
@@ -301,11 +299,11 @@ mod tests {
 
         #[test]
         fn test_color_words() {
-            DeltaTest::with(&[])
+            DeltaTest::with_args(&[])
                 .with_calling_process("git diff --color-words")
-                .with_input(GIT_DIFF_COLOR_WORDS)
                 .explain_ansi()
-                .expect_skip(
+                .with_input(GIT_DIFF_COLOR_WORDS)
+                .expect_after_skip(
                     11,
                     "
 #indent_mark
@@ -320,15 +318,15 @@ mod tests {
         #[test]
         #[ignore] // FIXME
         fn test_color_words_map_styles() {
-            DeltaTest::with(&[
+            DeltaTest::with_args(&[
                 "--map-styles",
                 "red => bold yellow #dddddd, green => bold blue #dddddd",
             ])
             .with_calling_process("git diff --color-words")
-            .with_input(GIT_DIFF_COLOR_WORDS)
             .explain_ansi()
+            .with_input(GIT_DIFF_COLOR_WORDS)
             .inspect()
-            .expect_skip(
+            .expect_after_skip(
                 11,
                 r##"
 #indent_mark
@@ -342,10 +340,10 @@ mod tests {
 
         #[test]
         fn test_hunk_line_style_raw() {
-            DeltaTest::with(&["--minus-style", "raw", "--plus-style", "raw"])
-                .with_input(GIT_DIFF_WITH_COLOR)
+            DeltaTest::with_args(&["--minus-style", "raw", "--plus-style", "raw"])
                 .explain_ansi()
-                .expect_skip(
+                .with_input(GIT_DIFF_WITH_COLOR)
+                .expect_after_skip(
                     14,
                     "
 (red)aaa(normal)
@@ -356,7 +354,7 @@ mod tests {
 
         #[test]
         fn test_hunk_line_style_raw_map_styles() {
-            DeltaTest::with(&[
+            DeltaTest::with_args(&[
                 "--minus-style",
                 "raw",
                 "--plus-style",
@@ -364,9 +362,9 @@ mod tests {
                 "--map-styles",
                 "red => bold blue, green => dim yellow",
             ])
-            .with_input(GIT_DIFF_WITH_COLOR)
             .explain_ansi()
-            .expect_skip(
+            .with_input(GIT_DIFF_WITH_COLOR)
+            .expect_after_skip(
                 14,
                 "
 (bold blue)aaa(normal)
diff --git a/src/main.rs b/src/main.rs
index 5e6eab5..1de9375 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -65,6 +65,12 @@ pub mod errors {
 
 #[cfg(not(tarpaulin_include))]
 fn main() -> std::io::Result<()> {
+    // Do this first because both parsing all the input in `run_app()` and
+    // listing all processes takes about 50ms on Linux.
+    // It also improves the chance that the calling process is still around when
+    // input is piped into delta (e.g. `git show  --word-diff=color | delta`).
+    utils::process::start_determining_calling_process_in_thread();
+
     // Ignore ctrl-c (SIGINT) to avoid leaving an orphaned pager process.
     // See https://github.com/dandavison/delta/issues/681
     ctrlc::set_handler(|| {})
diff --git a/src/utils/process.rs b/src/utils/process.rs
index 68b57a6..f111e9d 100644
--- a/src/utils/process.rs
+++ b/src/utils/process.rs
@@ -1,9 +1,9 @@
-use std::borrow::Cow;
 use std::collections::{HashMap, HashSet};
 use std::path::Path;
-use sysinfo::{Pid, Process, ProcessExt, ProcessRefreshKind, SystemExt};
+use std::sync::{Arc, Condvar, Mutex, MutexGuard};
 
 use lazy_static::lazy_static;
+use sysinfo::{Pid, Process, ProcessExt, ProcessRefreshKind, SystemExt};
 
 #[derive(Clone, Debug, PartialEq)]
 pub enum CallingProcess {
@@ -13,6 +13,8 @@ pub enum CallingProcess {
     GitReflog(CommandLine),
     GitGrep(CommandLine),
     OtherGrep, // rg, grep, ag, ack, etc
+    None,      // no matching process could be found
+    Pending,   // calling process is currently being determined
 }
 // TODO: Git blame is currently handled differently
 
@@ -23,23 +25,63 @@ pub struct CommandLine {
     last_arg: Option<String>,
 }
 
-pub fn calling_process() -> Option<Cow<'static, CallingProcess>> {
-    #[cfg(not(test))]
-    {
-        CACHED_CALLING_PROCESS.as_ref().map(Cow::Borrowed)
-    }
-    #[cfg(test)]
-    {
-        determine_calling_process().map(Cow::Owned)
-    }
+lazy_static! {
+    static ref CALLER: Arc<(Mutex<CallingProcess>, Condvar)> =
+        Arc::new((Mutex::new(CallingProcess::Pending), Condvar::new()));
 }
 
-lazy_static! {
-    static ref CACHED_CALLING_PROCESS: Option<CallingProcess> = determine_calling_process();
+pub fn start_determining_calling_process_in_thread() {
+    // The handle is neither kept nor returned nor joined but dropped, so the main
+    // thread can exit early if it does not need to know its parent process.
+    std::thread::Builder::new()
+        .name("find_calling_process".into())
+        .spawn(move || {
+            let calling_process = determine_calling_process();
+
+            let (caller_mutex, determine_done) = &**CALLER;
+
+            let mut caller = caller_mutex.lock().unwrap();
+            *caller = calling_process;
+            determine_done.notify_all();
+        })
+        .unwrap();
+}
+
+#[cfg(not(test))]
+pub fn calling_process() -> MutexGuard<'static, CallingProcess> {
+    let (caller_mutex, determine_done) = &**CALLER;
+
+    determine_done
+        .wait_while(caller_mutex.lock().unwrap(), |caller| {
+            *caller == CallingProcess::Pending
+        })
+        .unwrap()
+}
+
+// The return value is duck-typed to work in place of a MutexGuard when testing.
+#[cfg(test)]
+pub fn calling_process() -> Box<CallingProcess> {
+    type _UnusedImport = MutexGuard<'static, i8>;
+
+    if crate::utils::process::tests::FakeParentArgs::are_set() {
+        // If the (thread-local) FakeParentArgs are set, then the following command returns
+        // these, so the cached global real ones can not be used.
+        Box::new(determine_calling_process())
+    } else {
+        let (caller_mutex, _) = &**CALLER;
+
+        let mut caller = caller_mutex.lock().unwrap();
+        if *caller == CallingProcess::Pending {
+            *caller = determine_calling_process();
+        }
+
+        Box::new(caller.clone())
+    }
 }
 
-fn determine_calling_process() -> Option<CallingProcess> {
+fn determine_calling_process() -> CallingProcess {
     calling_process_cmdline(ProcInfo::new(), describe_calling_process)
+        .unwrap_or(CallingProcess::None)
 }
 
 // Return value of `extract_args(args: &[String]) -> ProcessArgs<T>` function which is
@@ -226,6 +268,13 @@ struct ProcInfo {
 }
 impl ProcInfo {
     fn new() -> Self {
+        // On Linux sysinfo optimizes for repeated process queries and keeps per-process
+        // /proc file descriptors open. This caching is not needed here, so
+        // set this to zero (this does nothing on other platforms).
+        // Also, there is currently a kernel bug which slows down syscalls when threads are
+        // involved (here: the ctrlc handler) and a lot of files are kept open.
+        sysinfo::set_open_files_limit(0);
+
         ProcInfo {
             info: sysinfo::System::new(),
         }
@@ -329,7 +378,11 @@ trait ProcessInterface {
                     };
                     iter_parents(self, pid, &mut sum_distance);
 
-                    Some((length_of_process_chain, args))
+                    if length_of_process_chain == usize::MAX {
+                        None
+                    } else {
+                        Some((length_of_process_chain, args))
+                    }
                 }
                 _ => None,
             })
@@ -437,13 +490,8 @@ where
 
     match info.find_sibling_in_refreshed_processes(my_pid, &extract_args) {
         None => {
-            #[cfg(test)]
-            {
-                info.refresh_processes();
-                info.find_sibling_in_refreshed_processes(my_pid, &extract_args)
-            }
-            #[cfg(not(test))]
-            None
+            info.refresh_processes();
+            info.find_sibling_in_refreshed_processes(my_pid, &extract_args)
         }
         some => some,
     }
@@ -555,6 +603,9 @@ pub mod tests {
                 }
             })
         }
+        pub fn are_set() -> bool {
+            FAKE_ARGS.with(|a| *a.borrow() != TlsState::None)
+        }
         fn error(where_: &str) -> ! {
             panic!(
                 "test logic error (in {}): wrong FakeParentArgs scope?",
@@ -818,6 +869,20 @@ pub mod tests {
         calling_process_cmdline(ProcInfo::new(), guess_git_blame_filename_extension);
     }
 
+    #[test]
+    fn test_process_blame_no_parent_found() {
+        let two_trees = MockProcInfo::with(&[
+            (2, 100, "-shell", None),
+            (3, 100, "git blame src/main.rs", Some(2)),
+            (4, 100, "call_delta.sh", None),
+            (5, 100, "delta", Some(4)),
+        ]);
+        assert_eq!(
+            calling_process_cmdline(two_trees, guess_git_blame_filename_extension),
+            None
+        );
+    }
+
     #[test]
     fn test_process_blame_info_with_parent() {
         let no_processes = MockProcInfo::with(&[]);
diff --git a/src/wrapping.rs b/src/wrapping.rs
index 05fa431..f1078de 100644
--- a/src/wrapping.rs
+++ b/src/wrapping.rs
@@ -903,7 +903,7 @@ index 223ca50..e69de29 100644
 
     #[test]
     fn test_wrap_with_unequal_hunk_zero_width() {
-        DeltaTest::with(&default_wrap_cfg_plus(&[
+        DeltaTest::with_args(&default_wrap_cfg_plus(&[
             "--side-by-side",
             "--line-numbers-left-format",
             "│L│",
@@ -914,9 +914,9 @@ index 223ca50..e69de29 100644
             "--line-fill-method",
             "spaces",
         ]))
-        .set_cfg(|cfg| cfg.truncation_symbol = ">".into())
+        .set_config(|cfg| cfg.truncation_symbol = ">".into())
         .with_input(HUNK_ZERO_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
             │L│abcdefghijklm+   │RRRR│abcdefghijklm+
             │L│nopqrstuvwxzy+   │RRRR│nopqrstuvwxzy+
@@ -929,7 +929,7 @@ index 223ca50..e69de29 100644
 
     #[test]
     fn test_wrap_with_large_hunk_zero_line_numbers() {
-        DeltaTest::with(&default_wrap_cfg_plus(&[
+        DeltaTest::with_args(&default_wrap_cfg_plus(&[
             "--side-by-side",
             "--line-numbers-left-format",
             "│LLL│",
@@ -940,9 +940,9 @@ index 223ca50..e69de29 100644
             "--line-fill-method",
             "ansi",
         ]))
-        .set_cfg(|cfg| cfg.truncation_symbol = ">".into())
+        .set_config(|cfg| cfg.truncation_symbol = ">".into())
         .with_input(HUNK_ZERO_LARGE_LINENUMBERS_DIFF)
-        .expect(
+        .expect_after_header(
             r#"
             │LLL│abcde+                   │WW   10   +- 101999 WW│abcde+
             │LLL│fghij+                   │WW        +-        WW│fghij+
@@ -956,15 +956,15 @@ index 223ca50..e69de29 100644
     #[test]
     fn test_wrap_with_keep_markers() {
         use crate::features::side_by_side::ansifill::ODD_PAD_CHAR;
-        let t = DeltaTest::with(&default_wrap_cfg_plus(&[
+        let t = DeltaTest::with_args(&default_wrap_cfg_plus(&[
             "--side-by-side",
             "--keep-plus-minus-markers",
             "--width",
             "45",
         ]))
-        .set_cfg(|cfg| cfg.truncation_symbol = ">".into())
-        .with_input(HUNK_MP_DIFF);
-        let output = t.expect(
+        .set_config(|cfg| cfg.truncation_symbol = ">".into())
+        .with_input(HUNK_MP_DIFF)
+        .expect_after_header(
             r#"
             │ 4  │ abcdefghijklmn+ │ 15 │ abcdefghijklmn+
             │    │ opqrstuvwxzy 0+ │    │ opqrstuvwxzy 0+
@@ -979,138 +979,133 @@ index 223ca50..e69de29 100644
             // this column here is^ where ODD_PAD_CHAR is inserted due to the odd 45 width
         );
 
-        assert!(!output.is_empty());
+        assert!(!t.output.is_empty());
 
-        for line in output.lines().skip(crate::config::HEADER_LEN) {
+        for line in t.output.lines().skip(crate::config::HEADER_LEN) {
             assert_eq!(line.chars().nth(22), Some(ODD_PAD_CHAR));
         }
     }
 
     #[test]
     fn test_alignment_2_lines_vs_3_lines() {
-        let config =
-            make_config_from_args(&default_wrap_cfg_plus(&["--side-by-side", "--width", "55"]));
+        let config_1 =
+            || make_config_from_args(&default_wrap_cfg_plus(&["--side-by-side", "--width", "55"]));
+
         {
-            DeltaTest::with_config_and_input(
-                &config,
-                &format!(
+            DeltaTest::with_config(config_1())
+                .with_input(&format!(
                     "{}-{}+{}",
                     HUNK_ALIGN_DIFF_HEADER, HUNK_ALIGN_DIFF_SHORT, HUNK_ALIGN_DIFF_LONG
-                ),
-            )
-            .expect(
-                r#"
-                │ 1  │.........1.........2< │ 1  │.........1.........2+
-                │    │                >.... │    │.........3.........4+
-                │    │                      │    │.........5.........6"#,
-            );
+                ))
+                .expect_after_header(
+                    r#"
+                    │ 1  │.........1.........2< │ 1  │.........1.........2+
+                    │    │                >.... │    │.........3.........4+
+                    │    │                      │    │.........5.........6"#,
+                );
             // the place where ODD_PAD_CHAR^ is inserted due to the odd 55 width
         }
 
         {
-            DeltaTest::with_config_and_input(
-                &config,
-                &format!(
+            DeltaTest::with_config(config_1())
+                .with_input(&format!(
                     "{}-{}+{}",
                     HUNK_ALIGN_DIFF_HEADER, HUNK_ALIGN_DIFF_LONG, HUNK_ALIGN_DIFF_SHORT
-                ),
-            )
-            .expect(
-                r#"
-                │ 1  │.........1.........2+ │ 1  │.........1.........2<
-                │    │.........3.........4+ │    │                >....
-                │    │.........5.........6  │    │"#,
-            );
+                ))
+                .expect_after_header(
+                    r#"
+                    │ 1  │.........1.........2+ │ 1  │.........1.........2<
+                    │    │.........3.........4+ │    │                >....
+                    │    │.........5.........6  │    │"#,
+                );
         }
     }
 
     #[test]
     fn test_alignment_1_line_vs_3_lines() {
-        let config = make_config_from_args(&default_wrap_cfg_plus(&[
-            "--side-by-side",
-            "--width",
-            "61",
-            "--line-fill-method",
-            "spaces",
-        ]));
+        let config_2 = || {
+            make_config_from_args(&default_wrap_cfg_plus(&[
+                "--side-by-side",
+                "--width",
+                "61",
+                "--line-fill-method",
+                "spaces",
+            ]))
+        };
 
         {
-            DeltaTest::with_config_and_input(
-                &config,
-                &format!(
+            DeltaTest::with_config(config_2())
+                .with_input(&format!(
                     "{}-{}+{}",
                     HUNK_ALIGN_DIFF_HEADER, HUNK_ALIGN_DIFF_SHORT, HUNK_ALIGN_DIFF_LONG
-                ),
-            )
-            .expect(
-                r#"
-                │ 1  │.........1.........2....│ 1  │.........1.........2...+
-                │    │                        │    │......3.........4......+
-                │    │                        │    │...5.........6          "#,
-            );
+                ))
+                .expect_after_header(
+                    r#"
+                    │ 1  │.........1.........2....│ 1  │.........1.........2...+
+                    │    │                        │    │......3.........4......+
+                    │    │                        │    │...5.........6          "#,
+                );
         }
 
         {
-            DeltaTest::with_config_and_input(
-                &config,
-                &format!(
+            DeltaTest::with_config(config_2())
+                .with_input(&format!(
                     "{}-{}+{}",
                     HUNK_ALIGN_DIFF_HEADER, HUNK_ALIGN_DIFF_LONG, HUNK_ALIGN_DIFF_SHORT
-                ),
-            )
-            .expect(
-                r#"
-                │ 1  │.........1.........2...+│ 1  │.........1.........2....
-                │    │......3.........4......+│    │
-                │    │...5.........6          │    │"#,
-            );
+                ))
+                .expect_after_header(
+                    r#"
+                    │ 1  │.........1.........2...+│ 1  │.........1.........2....
+                    │    │......3.........4......+│    │
+                    │    │...5.........6          │    │"#,
+                );
         }
     }
 
     #[test]
     fn test_wrap_max_lines_2() {
         // TODO overriding is not possible, need to change config directly
-        let mut config = make_config_from_args(&default_wrap_cfg_plus(&[
-            // "--wrap-max-lines",
-            // "2",
-            "--side-by-side",
-            "--width",
-            "72",
-            "--line-fill-method",
-            "spaces",
-        ]));
-        config.truncation_symbol = ">".into();
+        let config_3 = || {
+            let mut config = make_config_from_args(&default_wrap_cfg_plus(&[
+                // "--wrap-max-lines",
+                // "2",
+                "--side-by-side",
+                "--width",
+                "72",
+                "--line-fill-method",
+                "spaces",
+            ]));
+            config.truncation_symbol = ">".into();
+            config
+        };
 
         {
-            DeltaTest::with_config_and_input(
-                &config,
-                &format!(
+            DeltaTest::with_config(config_3())
+                .with_input(&format!(
                     "{}-{}+{}",
                     HUNK_ALIGN_DIFF_HEADER, HUNK_ALIGN_DIFF_SHORT, HUNK_ALIGN_DIFF_LONG
-                ),
-            )
-            .expect(
-                r#"
-                │ 1  │.........1.........2....      │ 1  │.........1.........2.........+
-                │    │                              │    │3.........4.........5........+
-                │    │                              │    │.6                            "#,
-            );
+                ))
+                .expect_after_header(
+                    r#"
+                    │ 1  │.........1.........2....      │ 1  │.........1.........2.........+
+                    │    │                              │    │3.........4.........5........+
+                    │    │                              │    │.6                            "#,
+                );
         }
 
         {
+            let mut config = config_3();
             config.wrap_config.max_lines = 2;
-            DeltaTest::with_config_and_input(
-                &config,
-                &format!(
+            DeltaTest::with_config(config)
+                .with_input(&format!(
                     "{}-{}+{}",
                     HUNK_ALIGN_DIFF_HEADER, HUNK_ALIGN_DIFF_SHORT, HUNK_ALIGN_DIFF_LONG
-                ),
-            )
-            .expect(
-                r#"
-                │ 1  │.........1.........2....      │ 1  │.........1.........2.........+
-                │    │                              │    │3.........4.........5........>"#,
-            );
+                ))
+                .expect_after_header(
+                    r#"
+                    │ 1  │.........1.........2....      │ 1  │.........1.........2.........+
+                    │    │                              │    │3.........4.........5........>"#,
+                );
         }
     }
 }


Test patch:
diff --git a/src/tests/integration_test_utils.rs b/src/tests/integration_test_utils.rs
index a1ec62e..2e7e24c 100644
--- a/src/tests/integration_test_utils.rs
+++ b/src/tests/integration_test_utils.rs
@@ -12,6 +12,7 @@ use crate::cli;
 use crate::config;
 use crate::delta::delta;
 use crate::git_config::GitConfig;
+use crate::tests::test_utils;
 use crate::utils::process::tests::FakeParentArgs;
 
 pub fn make_options_from_args_and_git_config(
@@ -94,19 +95,25 @@ pub fn get_line_of_code_from_delta(
 //     line1"#;`  // line 2 etc.
 // ignore the first newline and compare the following `lines()` to those produced
 // by `have`, `skip`-ping the first few. The leading spaces of the first line
+// to indicate the last line in the list). The leading spaces of the first line
 // are stripped from every following line (and verified), unless the first line
 // marks the indentation level with `#indent_mark`.
-pub fn lines_match(expected: &str, have: &str, skip: Option<usize>) {
+pub fn assert_lines_match_after_skip(skip: usize, expected: &str, have: &str) {
     let mut exp = expected.lines().peekable();
-    assert!(exp.next() == Some(""), "first line must be empty");
-    let line1 = exp.peek().unwrap();
+    let mut line1 = exp.next().unwrap();
+    let allow_partial = line1 == "#partial";
+    assert!(
+        allow_partial || line1.is_empty(),
+        "first line must be empty or \"#partial\""
+    );
+    line1 = exp.peek().unwrap();
     let indentation = line1.find(|c| c != ' ').unwrap_or(0);
     let ignore_indent = &line1[indentation..] == "#indent_mark";
     if ignore_indent {
         let _indent_mark = exp.next();
     }
 
-    let mut it = have.lines().skip(skip.unwrap_or(0));
+    let mut it = have.lines().skip(skip);
 
     for (i, expected) in exp.enumerate() {
         if !ignore_indent {
@@ -119,27 +126,51 @@ pub fn lines_match(expected: &str, have: &str, skip: Option<usize>) {
         assert_eq!(
             &expected[indentation..],
             it.next().unwrap(),
-            "on line {} of input",
-            i + 1
+            "on line {} of input:\n{}",
+            i + 1,
+            delineated_string(have),
         );
     }
-    assert_eq!(it.next(), None, "more input than expected");
+    if !allow_partial {
+        assert_eq!(it.next(), None, "more input than expected");
+    }
+}
+
+pub fn assert_lines_match(expected: &str, have: &str) {
+    assert_lines_match_after_skip(0, expected, have)
+}
+
+pub fn delineated_string(txt: &str) -> String {
+    let top = "▼".repeat(100);
+    let btm = "▲".repeat(100);
+    let nl = "\n";
+    top + &nl + txt + &nl + &btm
 }
 
 pub struct DeltaTest {
     config: config::Config,
     calling_process: Option<String>,
+    explain_ansi_: bool,
 }
 
 impl DeltaTest {
-    pub fn with(args: &[&str]) -> Self {
+    pub fn with_args(args: &[&str]) -> Self {
         Self {
             config: make_config_from_args(args),
             calling_process: None,
+            explain_ansi_: false,
+        }
+    }
+
+    pub fn with_config(config: config::Config) -> Self {
+        Self {
+            config: config,
+            calling_process: None,
+            explain_ansi_: false,
         }
     }
 
-    pub fn set_cfg<F>(mut self, f: F) -> Self
+    pub fn set_config<F>(mut self, f: F) -> Self
     where
         F: Fn(&mut config::Config),
     {
@@ -152,22 +183,30 @@ impl DeltaTest {
         self
     }
 
-    pub fn with_config_and_input(config: &config::Config, input: &str) -> DeltaTestOutput {
-        DeltaTestOutput {
-            output: run_delta(input, &config),
-            explain_ansi_: false,
-        }
+    pub fn explain_ansi(mut self) -> Self {
+        self.explain_ansi_ = true;
+        self
     }
 
     pub fn with_input(&self, input: &str) -> DeltaTestOutput {
         let _args = FakeParentArgs::for_scope(self.calling_process.as_deref().unwrap_or(""));
-        DeltaTest::with_config_and_input(&self.config, input)
+        let raw = run_delta(input, &self.config);
+        let cooked = if self.explain_ansi_ {
+            ansi::explain_ansi(&raw, false)
+        } else {
+            ansi::strip_ansi_codes(&raw)
+        };
+
+        DeltaTestOutput {
+            raw_output: raw,
+            output: cooked,
+        }
     }
 }
 
 pub struct DeltaTestOutput {
-    output: String,
-    explain_ansi_: bool,
+    pub raw_output: String,
+    pub output: String,
 }
 
 impl DeltaTestOutput {
@@ -175,42 +214,58 @@ impl DeltaTestOutput {
     /// with ASCII explanation of ANSI escape sequences.
     #[allow(unused)]
     pub fn inspect(self) -> Self {
-        eprintln!("{}", "▼".repeat(100));
-        eprintln!("{}", self.format_output());
-        eprintln!("{}", "▲".repeat(100));
+        eprintln!("{}", delineated_string(&self.output.as_str()));
         self
     }
 
     /// Print raw output, with any ANSI escape sequences.
     #[allow(unused)]
     pub fn inspect_raw(self) -> Self {
-        eprintln!("{}", "▼".repeat(100));
-        eprintln!("{}", self.output);
-        eprintln!("{}", "▲".repeat(100));
+        eprintln!("{}", delineated_string(&self.raw_output));
         self
     }
 
-    pub fn explain_ansi(mut self) -> Self {
-        self.explain_ansi_ = true;
+    pub fn expect_after_skip(self, skip: usize, expected: &str) -> Self {
+        assert_lines_match_after_skip(skip, expected, &self.output);
         self
     }
 
-    pub fn expect_skip(self, skip: usize, expected: &str) -> String {
-        let processed = self.format_output();
-        lines_match(expected, &processed, Some(skip));
-        processed
+    pub fn expect(self, expected: &str) -> Self {
+        self.expect_after_skip(0, expected)
     }
 
-    pub fn expect(self, expected: &str) -> String {
-        self.expect_skip(crate::config::HEADER_LEN, expected)
+    pub fn expect_after_header(self, expected: &str) -> Self {
+        self.expect_after_skip(crate::config::HEADER_LEN, expected)
     }
 
-    fn format_output(&self) -> String {
-        if self.explain_ansi_ {
-            ansi::explain_ansi(&self.output, false)
-        } else {
-            ansi::strip_ansi_codes(&self.output)
-        }
+    pub fn expect_contains(self, expected: &str) -> Self {
+        assert!(
+            self.output.contains(expected),
+            "Output does not contain \"{}\":\n{}",
+            expected,
+            delineated_string(&self.output.as_str())
+        );
+        self
+    }
+
+    pub fn expect_raw_contains(self, expected: &str) -> Self {
+        assert!(
+            self.raw_output.contains(expected),
+            "Raw output does not contain \"{}\":\n{}",
+            expected,
+            delineated_string(&self.raw_output.as_str())
+        );
+        self
+    }
+
+    pub fn expect_contains_once(self, expected: &str) -> Self {
+        assert!(
+            test_utils::contains_once(&self.output, expected),
+            "Output does not contain \"{}\" exactly once:\n{}",
+            expected,
+            delineated_string(&self.output.as_str())
+        );
+        self
     }
 }
 
@@ -235,30 +290,30 @@ pub mod tests {
         one
         two
         three"#;
-        lines_match(expected, "one\ntwo\nthree", None);
+        assert_lines_match(expected, "one\ntwo\nthree");
 
         let expected = r#"
         #indent_mark
         one
           2
         three"#;
-        lines_match(expected, "one\n  2\nthree", None);
+        assert_lines_match(expected, "one\n  2\nthree");
 
         let expected = r#"
             #indent_mark
              1 
               2  
                3"#;
-        lines_match(expected, " 1 \n  2  \n   3", None);
+        assert_lines_match(expected, " 1 \n  2  \n   3");
 
         let expected = r#"
         #indent_mark
          1 
 ignored!  2  
            3"#;
-        lines_match(expected, " 1 \n  2  \n   3", None);
+        assert_lines_match(expected, " 1 \n  2  \n   3");
         let expected = "\none\ntwo\nthree";
-        lines_match(expected, "one\ntwo\nthree", None);
+        assert_lines_match(expected, "one\ntwo\nthree");
     }
 
     #[test]
@@ -266,7 +321,7 @@ ignored!  2
     fn test_lines_match_no_nl() {
         let expected = r#"bad
         lines"#;
-        lines_match(expected, "bad\nlines", None);
+        assert_lines_match(expected, "bad\nlines");
     }
 
     #[test]
@@ -276,7 +331,7 @@ ignored!  2
         one
         two
         three"#;
-        lines_match(expected, "one\ntwo\nthree\nFOUR", None);
+        assert_lines_match(expected, "one\ntwo\nthree\nFOUR");
     }
 
     #[test]
@@ -286,7 +341,7 @@ ignored!  2
         ok
           wrong_indent
         "#;
-        lines_match(expected, "ok", None);
+        assert_lines_match(expected, "ok");
     }
 
     #[test]
@@ -296,18 +351,17 @@ ignored!  2
         ok
        wrong_indent
         "#;
-        lines_match(expected, "ok", None);
+        assert_lines_match(expected, "ok");
     }
 
     #[test]
     fn test_delta_test() {
         let input = "@@ -1,1 +1,1 @@ fn foo() {\n-1\n+2\n";
-        DeltaTest::with(&["--raw"])
-            .set_cfg(|c| c.pager = None)
-            .set_cfg(|c| c.line_numbers = true)
+        DeltaTest::with_args(&["--raw"])
+            .set_config(|c| c.pager = None)
+            .set_config(|c| c.line_numbers = true)
             .with_input(input)
-            .expect_skip(
-                0,
+            .expect(
                 r#"
                  #indent_mark
                  @@ -1,1 +1,1 @@ fn foo() {
@@ -315,18 +369,19 @@ ignored!  2
                      ⋮ 1  │+2"#,
             );
 
-        DeltaTest::with(&[]).with_input(input).expect_skip(
-            4,
-            r#"
+        DeltaTest::with_args(&[])
+            .with_input(input)
+            .expect_after_skip(
+                4,
+                r#"
                 1
                 2"#,
-        );
+            );
 
-        DeltaTest::with(&["--raw"])
-            .with_input(input)
+        DeltaTest::with_args(&["--raw"])
             .explain_ansi()
-            .expect_skip(
-                0,
+            .with_input(input)
+            .expect(
                 "\n\
                 (normal)@@ -1,1 +1,1 @@ fn foo() {\n\
                 (red)-1(normal)\n\
diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs
index a135a9b..6e0c542 100644
--- a/src/tests/test_example_diffs.rs
+++ b/src/tests/test_example_diffs.rs
@@ -7,67 +7,53 @@ mod tests {
     use crate::style;
     use crate::tests::ansi_test_utils::ansi_test_utils;
     use crate::tests::integration_test_utils;
-    use crate::tests::test_utils;
-    use regex::Regex;
+    use crate::tests::integration_test_utils::DeltaTest;
 
     #[test]
     fn test_added_file() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output = integration_test_utils::run_delta(ADDED_FILE_INPUT, &config);
-        let output = strip_ansi_codes(&output);
-        assert!(output.contains("\nadded: a.py\n"));
+        DeltaTest::with_args(&[])
+            .with_input(ADDED_FILE_INPUT)
+            .expect_contains("\nadded: a.py\n");
     }
 
     #[test]
     #[ignore] // #128
     fn test_added_empty_file() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output = integration_test_utils::run_delta(ADDED_EMPTY_FILE, &config);
-        let output = strip_ansi_codes(&output);
-        assert!(output.contains("\nadded: file\n"));
+        DeltaTest::with_args(&[])
+            .with_input(ADDED_EMPTY_FILE)
+            .expect_contains("\nadded: file\n");
     }
 
     #[test]
     fn test_added_file_directory_path_containing_space() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output =
-            integration_test_utils::run_delta(ADDED_FILES_DIRECTORY_PATH_CONTAINING_SPACE, &config);
-        let output = strip_ansi_codes(&output);
-        assert!(output.contains("\nadded: with space/file1\n"));
-        assert!(output.contains("\nadded: nospace/file2\n"));
+        DeltaTest::with_args(&[])
+            .with_input(ADDED_FILES_DIRECTORY_PATH_CONTAINING_SPACE)
+            .expect_contains("\nadded: with space/file1\n")
+            .expect_contains("\nadded: nospace/file2\n");
     }
 
     #[test]
     fn test_renamed_file() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output = integration_test_utils::run_delta(RENAMED_FILE_INPUT, &config);
-        let output = strip_ansi_codes(&output);
-        assert!(test_utils::contains_once(
-            &output,
-            "\nrenamed: a.py ⟶   b.py\n"
-        ));
+        DeltaTest::with_args(&[])
+            .with_input(RENAMED_FILE_INPUT)
+            .expect_contains_once("\nrenamed: a.py ⟶   b.py\n");
     }
 
     #[test]
     fn test_copied_file() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output = integration_test_utils::run_delta(GIT_DIFF_WITH_COPIED_FILE, &config);
-        let output = strip_ansi_codes(&output);
-        assert!(test_utils::contains_once(
-            &output,
-            "\ncopied: first_file ⟶   copied_file\n"
-        ));
+        DeltaTest::with_args(&[])
+            .with_input(GIT_DIFF_WITH_COPIED_FILE)
+            .expect_contains_once("\ncopied: first_file ⟶   copied_file\n");
     }
 
     #[test]
     fn test_renamed_file_with_changes() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output = integration_test_utils::run_delta(RENAMED_FILE_WITH_CHANGES_INPUT, &config);
-        let output = strip_ansi_codes(&output);
-        println!("{}", output);
-        assert!(test_utils::contains_once(
-            &output,
-            "\nrenamed: Casks/font-dejavusansmono-nerd-font.rb ⟶   Casks/font-dejavu-sans-mono-nerd-font.rb\n"));
+        let t = DeltaTest::with_args(&[])
+            .with_input(RENAMED_FILE_WITH_CHANGES_INPUT)
+            .expect_contains_once(
+            "\nrenamed: Casks/font-dejavusansmono-nerd-font.rb ⟶   Casks/font-dejavu-sans-mono-nerd-font.rb\n"
+            );
+        println!("{}", t.output);
     }
 
     #[test]
@@ -1587,84 +1573,65 @@ src/align.rs:71: impl<'a> Alignment<'a> { │
 
     #[test]
     fn test_file_mode_change_gain_executable_bit() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output = integration_test_utils::run_delta(
-            GIT_DIFF_FILE_MODE_CHANGE_GAIN_EXECUTABLE_BIT,
-            &config,
-        );
-        let output = strip_ansi_codes(&output);
-        assert!(output.contains(r"src/delta.rs: mode +x"));
+        DeltaTest::with_args(&[])
+            .with_input(GIT_DIFF_FILE_MODE_CHANGE_GAIN_EXECUTABLE_BIT)
+            .expect_contains(r"src/delta.rs: mode +x");
     }
 
     #[test]
     fn test_file_mode_change_lose_executable_bit() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output = integration_test_utils::run_delta(
-            GIT_DIFF_FILE_MODE_CHANGE_LOSE_EXECUTABLE_BIT,
-            &config,
-        );
-        let output = strip_ansi_codes(&output);
-        assert!(output.contains(r"src/delta.rs: mode -x"));
+        DeltaTest::with_args(&[])
+            .with_input(GIT_DIFF_FILE_MODE_CHANGE_LOSE_EXECUTABLE_BIT)
+            .expect_contains(r"src/delta.rs: mode -x");
     }
 
     #[test]
     fn test_file_mode_change_unexpected_bits() {
-        let config =
-            integration_test_utils::make_config_from_args(&["--navigate", "--right-arrow=->"]);
-        let output =
-            integration_test_utils::run_delta(GIT_DIFF_FILE_MODE_CHANGE_UNEXPECTED_BITS, &config);
-        let output = strip_ansi_codes(&output);
-        assert!(output.contains(r"Δ src/delta.rs: 100700 -> 100644"));
+        DeltaTest::with_args(&["--navigate", "--right-arrow=->"])
+            .with_input(GIT_DIFF_FILE_MODE_CHANGE_UNEXPECTED_BITS)
+            .expect_contains(r"Δ src/delta.rs: 100700 -> 100644");
     }
 
     #[test]
     fn test_file_mode_change_with_diff() {
-        let config = integration_test_utils::make_config_from_args(&[
-            "--navigate",
-            "--keep-plus-minus-markers",
-        ]);
-        let output =
-            integration_test_utils::run_delta(GIT_DIFF_FILE_MODE_CHANGE_WITH_DIFF, &config);
-        let output = strip_ansi_codes(&output);
-        let re = Regex::new(r"\n─+\n").unwrap();
-        let output = re.replace(&output, "\n-----\n");
-        assert!(output.contains(
-            "Δ src/script: mode +x
------
-
-─────┐
-• 1: │
-─────┘
--#!/bin/sh
-+#!/bin/bash
-"
-        ));
+        DeltaTest::with_args(&["--navigate", "--keep-plus-minus-markers"])
+            .with_input(GIT_DIFF_FILE_MODE_CHANGE_WITH_DIFF)
+            .expect_contains("Δ src/script: mode +x")
+            .expect_after_skip(
+                5,
+                "
+                ─────┐
+                • 1: │
+                ─────┘
+                -#!/bin/sh
+                +#!/bin/bash",
+            );
     }
 
     #[test]
     fn test_hyperlinks_commit_link_format() {
-        let config = integration_test_utils::make_config_from_args(&[
-            // If commit-style is not set then the commit line is handled in raw
-            // mode, in which case we only format hyperlinks if output is a tty;
-            // this causes the test to fail on Github Actions, but pass locally
-            // if output is left going to the screen.
-            "--commit-style",
-            "blue",
-            "--hyperlinks",
-            "--hyperlinks-commit-link-format",
-            "https://invent.kde.org/utilities/konsole/-/commit/{commit}",
-        ]);
-        let output = integration_test_utils::run_delta(GIT_DIFF_SINGLE_HUNK, &config);
-        assert!(output.contains(r"https://invent.kde.org/utilities/konsole/-/commit/94907c0f136f46dc46ffae2dc92dca9af7eb7c2e"));
+        // If commit-style is not set then the commit line is handled in raw
+        // mode, in which case we only format hyperlinks if output is a tty;
+        // this causes the test to fail on Github Actions, but pass locally
+        // if output is left going to the screen.
+        DeltaTest::with_args(&[
+                "--commit-style",
+                "blue",
+                "--hyperlinks",
+                "--hyperlinks-commit-link-format",
+                "https://invent.kde.org/utilities/konsole/-/commit/{commit}",
+            ])
+            .with_input(GIT_DIFF_SINGLE_HUNK)
+            .expect_raw_contains(
+                r"https://invent.kde.org/utilities/konsole/-/commit/94907c0f136f46dc46ffae2dc92dca9af7eb7c2e"
+                );
     }
 
     #[test]
     fn test_filenames_with_spaces() {
-        let config = integration_test_utils::make_config_from_args(&[]);
-        let output =
-            integration_test_utils::run_delta(GIT_DIFF_NO_INDEX_FILENAMES_WITH_SPACES, &config);
-        let output = strip_ansi_codes(&output);
-        assert!(output.contains("a b ⟶   c d\n"));
+        DeltaTest::with_args(&[])
+            .with_input(GIT_DIFF_NO_INDEX_FILENAMES_WITH_SPACES)
+            .expect_contains("a b ⟶   c d\n");
     }
 
     const GIT_DIFF_SINGLE_HUNK: &str = "\