diffutilslib/
context_diff.rs

1// This file is part of the uutils diffutils package.
2//
3// For the full copyright and license information, please view the LICENSE-*
4// files that was distributed with this source code.
5
6use std::collections::VecDeque;
7use std::io::Write;
8
9use crate::params::Params;
10use crate::utils::do_write_line;
11use crate::utils::get_modification_time;
12
13#[derive(Debug, PartialEq)]
14pub enum DiffLine {
15    Context(Vec<u8>),
16    Change(Vec<u8>),
17    Add(Vec<u8>),
18}
19
20#[derive(Debug, PartialEq)]
21struct Mismatch {
22    pub line_number_expected: usize,
23    pub line_number_actual: usize,
24    pub expected: Vec<DiffLine>,
25    pub actual: Vec<DiffLine>,
26    pub expected_missing_nl: bool,
27    pub actual_missing_nl: bool,
28    pub expected_all_context: bool,
29    pub actual_all_context: bool,
30}
31
32impl Mismatch {
33    fn new(line_number_expected: usize, line_number_actual: usize) -> Mismatch {
34        Mismatch {
35            line_number_expected,
36            line_number_actual,
37            expected: Vec::new(),
38            actual: Vec::new(),
39            expected_missing_nl: false,
40            actual_missing_nl: false,
41            expected_all_context: false,
42            actual_all_context: false,
43        }
44    }
45}
46
47// Produces a diff between the expected output and actual output.
48fn make_diff(
49    expected: &[u8],
50    actual: &[u8],
51    context_size: usize,
52    stop_early: bool,
53) -> Vec<Mismatch> {
54    let mut line_number_expected = 1;
55    let mut line_number_actual = 1;
56    let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
57    let mut lines_since_mismatch = context_size + 1;
58    let mut results = Vec::new();
59    let mut mismatch = Mismatch::new(0, 0);
60
61    let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
62    let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
63
64    debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
65    // ^ means that underflow here is impossible
66    let expected_lines_count = expected_lines.len() - 1;
67    let actual_lines_count = actual_lines.len() - 1;
68
69    if expected_lines.last() == Some(&&b""[..]) {
70        expected_lines.pop();
71    }
72
73    if actual_lines.last() == Some(&&b""[..]) {
74        actual_lines.pop();
75    }
76
77    // Rust only allows allocations to grow to isize::MAX, and this is bigger than that.
78    let mut expected_lines_change_idx: usize = !0;
79
80    for result in diff::slice(&expected_lines, &actual_lines) {
81        match result {
82            diff::Result::Left(str) => {
83                if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
84                    results.push(mismatch);
85                    mismatch = Mismatch::new(
86                        line_number_expected - context_queue.len(),
87                        line_number_actual - context_queue.len(),
88                    );
89                }
90
91                while let Some(line) = context_queue.pop_front() {
92                    mismatch.expected.push(DiffLine::Context(line.to_vec()));
93                    mismatch.actual.push(DiffLine::Context(line.to_vec()));
94                }
95
96                expected_lines_change_idx = mismatch.expected.len();
97                mismatch.expected.push(DiffLine::Add(str.to_vec()));
98                if line_number_expected > expected_lines_count {
99                    mismatch.expected_missing_nl = true;
100                }
101                line_number_expected += 1;
102                lines_since_mismatch = 0;
103            }
104            diff::Result::Right(str) => {
105                if lines_since_mismatch > context_size && lines_since_mismatch > 0 {
106                    results.push(mismatch);
107                    mismatch = Mismatch::new(
108                        line_number_expected - context_queue.len(),
109                        line_number_actual - context_queue.len(),
110                    );
111                    expected_lines_change_idx = !0;
112                }
113
114                while let Some(line) = context_queue.pop_front() {
115                    mismatch.expected.push(DiffLine::Context(line.to_vec()));
116                    mismatch.actual.push(DiffLine::Context(line.to_vec()));
117                }
118
119                if let Some(DiffLine::Add(content)) =
120                    mismatch.expected.get_mut(expected_lines_change_idx)
121                {
122                    let content = std::mem::take(content);
123                    mismatch.expected[expected_lines_change_idx] = DiffLine::Change(content);
124                    expected_lines_change_idx = expected_lines_change_idx.wrapping_sub(1); // if 0, becomes !0
125                    mismatch.actual.push(DiffLine::Change(str.to_vec()));
126                } else {
127                    mismatch.actual.push(DiffLine::Add(str.to_vec()));
128                }
129                if line_number_actual > actual_lines_count {
130                    mismatch.actual_missing_nl = true;
131                }
132                line_number_actual += 1;
133                lines_since_mismatch = 0;
134            }
135            diff::Result::Both(str, _) => {
136                expected_lines_change_idx = !0;
137                // if one of them is missing a newline and the other isn't, then they don't actually match
138                if (line_number_actual > actual_lines_count)
139                    && (line_number_expected > expected_lines_count)
140                {
141                    if context_queue.len() < context_size {
142                        while let Some(line) = context_queue.pop_front() {
143                            mismatch.expected.push(DiffLine::Context(line.to_vec()));
144                            mismatch.actual.push(DiffLine::Context(line.to_vec()));
145                        }
146                        if lines_since_mismatch < context_size {
147                            mismatch.expected.push(DiffLine::Context(str.to_vec()));
148                            mismatch.actual.push(DiffLine::Context(str.to_vec()));
149                            mismatch.expected_missing_nl = true;
150                            mismatch.actual_missing_nl = true;
151                        }
152                    }
153                    lines_since_mismatch = 0;
154                } else if line_number_actual > actual_lines_count {
155                    if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
156                        results.push(mismatch);
157                        mismatch = Mismatch::new(
158                            line_number_expected - context_queue.len(),
159                            line_number_actual - context_queue.len(),
160                        );
161                    }
162                    while let Some(line) = context_queue.pop_front() {
163                        mismatch.expected.push(DiffLine::Context(line.to_vec()));
164                        mismatch.actual.push(DiffLine::Context(line.to_vec()));
165                    }
166                    mismatch.expected.push(DiffLine::Change(str.to_vec()));
167                    mismatch.actual.push(DiffLine::Change(str.to_vec()));
168                    mismatch.actual_missing_nl = true;
169                    lines_since_mismatch = 0;
170                } else if line_number_expected > expected_lines_count {
171                    if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
172                        results.push(mismatch);
173                        mismatch = Mismatch::new(
174                            line_number_expected - context_queue.len(),
175                            line_number_actual - context_queue.len(),
176                        );
177                    }
178                    while let Some(line) = context_queue.pop_front() {
179                        mismatch.expected.push(DiffLine::Context(line.to_vec()));
180                        mismatch.actual.push(DiffLine::Context(line.to_vec()));
181                    }
182                    mismatch.expected.push(DiffLine::Change(str.to_vec()));
183                    mismatch.expected_missing_nl = true;
184                    mismatch.actual.push(DiffLine::Change(str.to_vec()));
185                    lines_since_mismatch = 0;
186                } else {
187                    debug_assert!(context_queue.len() <= context_size);
188                    if context_queue.len() >= context_size {
189                        let _ = context_queue.pop_front();
190                    }
191                    if lines_since_mismatch < context_size {
192                        mismatch.expected.push(DiffLine::Context(str.to_vec()));
193                        mismatch.actual.push(DiffLine::Context(str.to_vec()));
194                    } else if context_size > 0 {
195                        context_queue.push_back(str);
196                    }
197                    lines_since_mismatch += 1;
198                }
199                line_number_expected += 1;
200                line_number_actual += 1;
201            }
202        }
203        if stop_early && !results.is_empty() {
204            // Optimization: stop analyzing the files as soon as there are any differences
205            return results;
206        }
207    }
208
209    results.push(mismatch);
210    results.remove(0);
211
212    if results.is_empty() && expected_lines_count != actual_lines_count {
213        let mut mismatch = Mismatch::new(expected_lines.len(), actual_lines.len());
214        // empty diff and only expected lines has a missing line at end
215        if expected_lines_count != expected_lines.len() {
216            mismatch.expected.push(DiffLine::Change(
217                expected_lines
218                    .pop()
219                    .expect("can't be empty; produced by split()")
220                    .to_vec(),
221            ));
222            mismatch.expected_missing_nl = true;
223            mismatch.actual.push(DiffLine::Change(
224                actual_lines
225                    .pop()
226                    .expect("can't be empty; produced by split()")
227                    .to_vec(),
228            ));
229            results.push(mismatch);
230        } else if actual_lines_count != actual_lines.len() {
231            mismatch.expected.push(DiffLine::Change(
232                expected_lines
233                    .pop()
234                    .expect("can't be empty; produced by split()")
235                    .to_vec(),
236            ));
237            mismatch.actual.push(DiffLine::Change(
238                actual_lines
239                    .pop()
240                    .expect("can't be empty; produced by split()")
241                    .to_vec(),
242            ));
243            mismatch.actual_missing_nl = true;
244            results.push(mismatch);
245        }
246    }
247
248    // hunks with pure context lines get truncated to empty
249    for mismatch in &mut results {
250        if !mismatch
251            .expected
252            .iter()
253            .any(|x| !matches!(&x, DiffLine::Context(_)))
254        {
255            mismatch.expected_all_context = true;
256        }
257        if !mismatch
258            .actual
259            .iter()
260            .any(|x| !matches!(&x, DiffLine::Context(_)))
261        {
262            mismatch.actual_all_context = true;
263        }
264    }
265
266    results
267}
268
269#[must_use]
270pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
271    let from_modified_time = get_modification_time(&params.from.to_string_lossy());
272    let to_modified_time = get_modification_time(&params.to.to_string_lossy());
273    let mut output = format!(
274        "*** {0}\t{1}\n--- {2}\t{3}\n",
275        params.from.to_string_lossy(),
276        from_modified_time,
277        params.to.to_string_lossy(),
278        to_modified_time
279    )
280    .into_bytes();
281    let diff_results = make_diff(expected, actual, params.context_count, params.brief);
282    if diff_results.is_empty() {
283        return Vec::new();
284    }
285    if params.brief {
286        return output;
287    }
288    for result in diff_results {
289        let mut line_number_expected = result.line_number_expected;
290        let mut line_number_actual = result.line_number_actual;
291        let mut expected_count = result.expected.len();
292        let mut actual_count = result.actual.len();
293        if expected_count == 0 {
294            line_number_expected -= 1;
295            expected_count = 1;
296        }
297        if actual_count == 0 {
298            line_number_actual -= 1;
299            actual_count = 1;
300        }
301        let end_line_number_expected = expected_count + line_number_expected - 1;
302        let end_line_number_actual = actual_count + line_number_actual - 1;
303        let exp_start = if end_line_number_expected == line_number_expected {
304            String::new()
305        } else {
306            format!("{line_number_expected},")
307        };
308        let act_start = if end_line_number_actual == line_number_actual {
309            String::new()
310        } else {
311            format!("{line_number_actual},")
312        };
313        writeln!(
314            output,
315            "***************\n*** {exp_start}{end_line_number_expected} ****"
316        )
317        .expect("write to Vec is infallible");
318        if !result.expected_all_context {
319            for line in result.expected {
320                match line {
321                    DiffLine::Context(e) => {
322                        write!(output, "  ").expect("write to Vec is infallible");
323                        do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
324                            .expect("write to Vec is infallible");
325                        writeln!(output).unwrap();
326                    }
327                    DiffLine::Change(e) => {
328                        write!(output, "! ").expect("write to Vec is infallible");
329                        do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
330                            .expect("write to Vec is infallible");
331                        writeln!(output).unwrap();
332                    }
333                    DiffLine::Add(e) => {
334                        write!(output, "- ").expect("write to Vec is infallible");
335                        do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
336                            .expect("write to Vec is infallible");
337                        writeln!(output).unwrap();
338                    }
339                }
340            }
341            if result.expected_missing_nl {
342                writeln!(output, r"\ No newline at end of file")
343                    .expect("write to Vec is infallible");
344            }
345        }
346        writeln!(output, "--- {act_start}{end_line_number_actual} ----")
347            .expect("write to Vec is infallible");
348        if !result.actual_all_context {
349            for line in result.actual {
350                match line {
351                    DiffLine::Context(e) => {
352                        write!(output, "  ").expect("write to Vec is infallible");
353                        do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
354                            .expect("write to Vec is infallible");
355                        writeln!(output).unwrap();
356                    }
357                    DiffLine::Change(e) => {
358                        write!(output, "! ").expect("write to Vec is infallible");
359                        do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
360                            .expect("write to Vec is infallible");
361                        writeln!(output).unwrap();
362                    }
363                    DiffLine::Add(e) => {
364                        write!(output, "+ ").expect("write to Vec is infallible");
365                        do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
366                            .expect("write to Vec is infallible");
367                        writeln!(output).unwrap();
368                    }
369                }
370            }
371            if result.actual_missing_nl {
372                writeln!(output, r"\ No newline at end of file")
373                    .expect("write to Vec is infallible");
374            }
375        }
376    }
377    output
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use pretty_assertions::assert_eq;
384    #[test]
385    fn test_permutations() {
386        // test all possible six-line files.
387        let target = "target/context-diff/";
388        let _ = std::fs::create_dir(target);
389        for &a in &[0, 1, 2] {
390            for &b in &[0, 1, 2] {
391                for &c in &[0, 1, 2] {
392                    for &d in &[0, 1, 2] {
393                        for &e in &[0, 1, 2] {
394                            for &f in &[0, 1, 2] {
395                                use std::fs::{self, File};
396                                use std::io::Write;
397                                use std::process::Command;
398                                let mut alef = Vec::new();
399                                let mut bet = Vec::new();
400                                alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
401                                    .unwrap();
402                                if a != 2 {
403                                    bet.write_all(b"b\n").unwrap();
404                                }
405                                alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
406                                    .unwrap();
407                                if b != 2 {
408                                    bet.write_all(b"d\n").unwrap();
409                                }
410                                alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
411                                    .unwrap();
412                                if c != 2 {
413                                    bet.write_all(b"f\n").unwrap();
414                                }
415                                alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
416                                    .unwrap();
417                                if d != 2 {
418                                    bet.write_all(b"h\n").unwrap();
419                                }
420                                alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
421                                    .unwrap();
422                                if e != 2 {
423                                    bet.write_all(b"j\n").unwrap();
424                                }
425                                alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
426                                    .unwrap();
427                                if f != 2 {
428                                    bet.write_all(b"l\n").unwrap();
429                                }
430                                // This test diff is intentionally reversed.
431                                // We want it to turn the alef into bet.
432                                let diff = diff(
433                                    &alef,
434                                    &bet,
435                                    &Params {
436                                        from: "a/alef".into(),
437                                        to: (&format!("{target}/alef")).into(),
438                                        context_count: 2,
439                                        ..Default::default()
440                                    },
441                                );
442                                File::create(format!("{target}/ab.diff"))
443                                    .unwrap()
444                                    .write_all(&diff)
445                                    .unwrap();
446                                let mut fa = File::create(format!("{target}/alef")).unwrap();
447                                fa.write_all(&alef[..]).unwrap();
448                                let mut fb = File::create(format!("{target}/bet")).unwrap();
449                                fb.write_all(&bet[..]).unwrap();
450                                let _ = fa;
451                                let _ = fb;
452                                let output = Command::new("patch")
453                                    .arg("-p0")
454                                    .arg("--context")
455                                    .stdin(File::open(format!("{target}/ab.diff")).unwrap())
456                                    .output()
457                                    .unwrap();
458                                assert!(output.status.success(), "{output:?}");
459                                //println!("{}", String::from_utf8_lossy(&output.stdout));
460                                //println!("{}", String::from_utf8_lossy(&output.stderr));
461                                let alef = fs::read(format!("{target}/alef")).unwrap();
462                                assert_eq!(alef, bet);
463                            }
464                        }
465                    }
466                }
467            }
468        }
469    }
470
471    #[test]
472    fn test_permutations_empty_lines() {
473        let target = "target/context-diff/";
474        // test all possible six-line files with missing newlines.
475        let _ = std::fs::create_dir(target);
476        for &a in &[0, 1, 2] {
477            for &b in &[0, 1, 2] {
478                for &c in &[0, 1, 2] {
479                    for &d in &[0, 1, 2] {
480                        for &e in &[0, 1, 2] {
481                            for &f in &[0, 1, 2] {
482                                use std::fs::{self, File};
483                                use std::io::Write;
484                                use std::process::Command;
485                                let mut alef = Vec::new();
486                                let mut bet = Vec::new();
487                                alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
488                                if a != 2 {
489                                    bet.write_all(b"b\n").unwrap();
490                                }
491                                alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
492                                if b != 2 {
493                                    bet.write_all(b"d\n").unwrap();
494                                }
495                                alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
496                                if c != 2 {
497                                    bet.write_all(b"f\n").unwrap();
498                                }
499                                alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
500                                if d != 2 {
501                                    bet.write_all(b"h\n").unwrap();
502                                }
503                                alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
504                                if e != 2 {
505                                    bet.write_all(b"j\n").unwrap();
506                                }
507                                alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
508                                if f != 2 {
509                                    bet.write_all(b"l\n").unwrap();
510                                }
511                                // This test diff is intentionally reversed.
512                                // We want it to turn the alef into bet.
513                                let diff = diff(
514                                    &alef,
515                                    &bet,
516                                    &Params {
517                                        from: "a/alef_".into(),
518                                        to: (&format!("{target}/alef_")).into(),
519                                        context_count: 2,
520                                        ..Default::default()
521                                    },
522                                );
523                                File::create(format!("{target}/ab_.diff"))
524                                    .unwrap()
525                                    .write_all(&diff)
526                                    .unwrap();
527                                let mut fa = File::create(format!("{target}/alef_")).unwrap();
528                                fa.write_all(&alef[..]).unwrap();
529                                let mut fb = File::create(format!("{target}/bet_")).unwrap();
530                                fb.write_all(&bet[..]).unwrap();
531                                let _ = fa;
532                                let _ = fb;
533                                let output = Command::new("patch")
534                                    .arg("-p0")
535                                    .arg("--context")
536                                    .stdin(File::open(format!("{target}/ab_.diff")).unwrap())
537                                    .output()
538                                    .unwrap();
539                                assert!(output.status.success(), "{output:?}");
540                                //println!("{}", String::from_utf8_lossy(&output.stdout));
541                                //println!("{}", String::from_utf8_lossy(&output.stderr));
542                                let alef = fs::read(format!("{target}/alef_")).unwrap();
543                                assert_eq!(alef, bet);
544                            }
545                        }
546                    }
547                }
548            }
549        }
550    }
551
552    #[test]
553    fn test_permutations_missing_lines() {
554        let target = "target/context-diff/";
555        // test all possible six-line files.
556        let _ = std::fs::create_dir(target);
557        for &a in &[0, 1, 2] {
558            for &b in &[0, 1, 2] {
559                for &c in &[0, 1, 2] {
560                    for &d in &[0, 1, 2] {
561                        for &e in &[0, 1, 2] {
562                            for &f in &[0, 1, 2] {
563                                use std::fs::{self, File};
564                                use std::io::Write;
565                                use std::process::Command;
566                                let mut alef = Vec::new();
567                                let mut bet = Vec::new();
568                                alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
569                                if a != 2 {
570                                    bet.write_all(b"b\n").unwrap();
571                                }
572                                alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
573                                if b != 2 {
574                                    bet.write_all(b"d\n").unwrap();
575                                }
576                                alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
577                                if c != 2 {
578                                    bet.write_all(b"f\n").unwrap();
579                                }
580                                alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
581                                if d != 2 {
582                                    bet.write_all(b"h\n").unwrap();
583                                }
584                                alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
585                                if e != 2 {
586                                    bet.write_all(b"j\n").unwrap();
587                                }
588                                alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
589                                if f != 2 {
590                                    bet.write_all(b"l\n").unwrap();
591                                }
592                                if alef.is_empty() && bet.is_empty() {
593                                    continue;
594                                };
595                                // This test diff is intentionally reversed.
596                                // We want it to turn the alef into bet.
597                                let diff = diff(
598                                    &alef,
599                                    &bet,
600                                    &Params {
601                                        from: "a/alefx".into(),
602                                        to: (&format!("{target}/alefx")).into(),
603                                        context_count: 2,
604                                        ..Default::default()
605                                    },
606                                );
607                                File::create(format!("{target}/abx.diff"))
608                                    .unwrap()
609                                    .write_all(&diff)
610                                    .unwrap();
611                                let mut fa = File::create(format!("{target}/alefx")).unwrap();
612                                fa.write_all(&alef[..]).unwrap();
613                                let mut fb = File::create(format!("{target}/betx")).unwrap();
614                                fb.write_all(&bet[..]).unwrap();
615                                let _ = fa;
616                                let _ = fb;
617                                let output = Command::new("patch")
618                                    .arg("-p0")
619                                    .arg("--context")
620                                    .stdin(File::open(format!("{target}/abx.diff")).unwrap())
621                                    .output()
622                                    .unwrap();
623                                assert!(output.status.success(), "{output:?}");
624                                //println!("{}", String::from_utf8_lossy(&output.stdout));
625                                //println!("{}", String::from_utf8_lossy(&output.stderr));
626                                let alef = fs::read(format!("{target}/alefx")).unwrap();
627                                assert_eq!(alef, bet);
628                            }
629                        }
630                    }
631                }
632            }
633        }
634    }
635
636    #[test]
637    fn test_permutations_reverse() {
638        let target = "target/context-diff/";
639        // test all possible six-line files.
640        let _ = std::fs::create_dir(target);
641        for &a in &[0, 1, 2] {
642            for &b in &[0, 1, 2] {
643                for &c in &[0, 1, 2] {
644                    for &d in &[0, 1, 2] {
645                        for &e in &[0, 1, 2] {
646                            for &f in &[0, 1, 2] {
647                                use std::fs::{self, File};
648                                use std::io::Write;
649                                use std::process::Command;
650                                let mut alef = Vec::new();
651                                let mut bet = Vec::new();
652                                alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
653                                    .unwrap();
654                                if a != 2 {
655                                    bet.write_all(b"a\n").unwrap();
656                                }
657                                alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
658                                    .unwrap();
659                                if b != 2 {
660                                    bet.write_all(b"b\n").unwrap();
661                                }
662                                alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
663                                    .unwrap();
664                                if c != 2 {
665                                    bet.write_all(b"c\n").unwrap();
666                                }
667                                alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
668                                    .unwrap();
669                                if d != 2 {
670                                    bet.write_all(b"d\n").unwrap();
671                                }
672                                alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
673                                    .unwrap();
674                                if e != 2 {
675                                    bet.write_all(b"e\n").unwrap();
676                                }
677                                alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
678                                    .unwrap();
679                                if f != 2 {
680                                    bet.write_all(b"f\n").unwrap();
681                                }
682                                // This test diff is intentionally reversed.
683                                // We want it to turn the alef into bet.
684                                let diff = diff(
685                                    &alef,
686                                    &bet,
687                                    &Params {
688                                        from: "a/alefr".into(),
689                                        to: (&format!("{target}/alefr")).into(),
690                                        context_count: 2,
691                                        ..Default::default()
692                                    },
693                                );
694                                File::create(format!("{target}/abr.diff"))
695                                    .unwrap()
696                                    .write_all(&diff)
697                                    .unwrap();
698                                let mut fa = File::create(format!("{target}/alefr")).unwrap();
699                                fa.write_all(&alef[..]).unwrap();
700                                let mut fb = File::create(format!("{target}/betr")).unwrap();
701                                fb.write_all(&bet[..]).unwrap();
702                                let _ = fa;
703                                let _ = fb;
704                                let output = Command::new("patch")
705                                    .arg("-p0")
706                                    .arg("--context")
707                                    .stdin(File::open(format!("{target}/abr.diff")).unwrap())
708                                    .output()
709                                    .unwrap();
710                                assert!(output.status.success(), "{output:?}");
711                                //println!("{}", String::from_utf8_lossy(&output.stdout));
712                                //println!("{}", String::from_utf8_lossy(&output.stderr));
713                                let alef = fs::read(format!("{target}/alefr")).unwrap();
714                                assert_eq!(alef, bet);
715                            }
716                        }
717                    }
718                }
719            }
720        }
721    }
722
723    #[test]
724    fn test_stop_early() {
725        use crate::assert_diff_eq;
726
727        let from_filename = "foo";
728        let from = ["a", "b", "c", ""].join("\n");
729        let to_filename = "bar";
730        let to = ["a", "d", "c", ""].join("\n");
731
732        let diff_full = diff(
733            from.as_bytes(),
734            to.as_bytes(),
735            &Params {
736                from: from_filename.into(),
737                to: to_filename.into(),
738                ..Default::default()
739            },
740        );
741
742        let expected_full = [
743            "*** foo\tTIMESTAMP",
744            "--- bar\tTIMESTAMP",
745            "***************",
746            "*** 1,3 ****",
747            "  a",
748            "! b",
749            "  c",
750            "--- 1,3 ----",
751            "  a",
752            "! d",
753            "  c",
754            "",
755        ]
756        .join("\n");
757        assert_diff_eq!(diff_full, expected_full);
758
759        let diff_brief = diff(
760            from.as_bytes(),
761            to.as_bytes(),
762            &Params {
763                from: from_filename.into(),
764                to: to_filename.into(),
765                brief: true,
766                ..Default::default()
767            },
768        );
769
770        let expected_brief = ["*** foo\tTIMESTAMP", "--- bar\tTIMESTAMP", ""].join("\n");
771        assert_diff_eq!(diff_brief, expected_brief);
772
773        let nodiff_full = diff(
774            from.as_bytes(),
775            from.as_bytes(),
776            &Params {
777                from: from_filename.into(),
778                to: to_filename.into(),
779                ..Default::default()
780            },
781        );
782        assert!(nodiff_full.is_empty());
783
784        let nodiff_brief = diff(
785            from.as_bytes(),
786            from.as_bytes(),
787            &Params {
788                from: from_filename.into(),
789                to: to_filename.into(),
790                brief: true,
791                ..Default::default()
792            },
793        );
794        assert!(nodiff_brief.is_empty());
795    }
796}