diffutilslib/
unified_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    Expected(Vec<u8>),
17    Actual(Vec<u8>),
18    MissingNL,
19}
20
21#[derive(Debug, PartialEq)]
22struct Mismatch {
23    pub line_number_expected: u32,
24    pub line_number_actual: u32,
25    pub lines: Vec<DiffLine>,
26}
27
28impl Mismatch {
29    fn new(line_number_expected: u32, line_number_actual: u32) -> Mismatch {
30        Mismatch {
31            line_number_expected,
32            line_number_actual,
33            lines: Vec::new(),
34        }
35    }
36}
37
38// Produces a diff between the expected output and actual output.
39fn make_diff(
40    expected: &[u8],
41    actual: &[u8],
42    context_size: usize,
43    stop_early: bool,
44) -> Vec<Mismatch> {
45    let mut line_number_expected = 1;
46    let mut line_number_actual = 1;
47    let mut context_queue: VecDeque<&[u8]> = VecDeque::with_capacity(context_size);
48    let mut lines_since_mismatch = context_size + 1;
49    let mut results = Vec::new();
50    let mut mismatch = Mismatch::new(0, 0);
51
52    let mut expected_lines: Vec<&[u8]> = expected.split(|&c| c == b'\n').collect();
53    let mut actual_lines: Vec<&[u8]> = actual.split(|&c| c == b'\n').collect();
54
55    debug_assert_eq!(b"".split(|&c| c == b'\n').count(), 1);
56    // ^ means that underflow here is impossible
57    let expected_lines_count = expected_lines.len() as u32 - 1;
58    let actual_lines_count = actual_lines.len() as u32 - 1;
59
60    if expected_lines.last() == Some(&&b""[..]) {
61        expected_lines.pop();
62    }
63
64    if actual_lines.last() == Some(&&b""[..]) {
65        actual_lines.pop();
66    }
67
68    for result in diff::slice(&expected_lines, &actual_lines) {
69        match result {
70            diff::Result::Left(str) => {
71                if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
72                    results.push(mismatch);
73                    mismatch = Mismatch::new(
74                        line_number_expected - context_queue.len() as u32,
75                        line_number_actual - context_queue.len() as u32,
76                    );
77                }
78
79                while let Some(line) = context_queue.pop_front() {
80                    mismatch.lines.push(DiffLine::Context(line.to_vec()));
81                }
82
83                if mismatch.lines.last() == Some(&DiffLine::MissingNL) {
84                    mismatch.lines.pop();
85                    match mismatch.lines.pop() {
86                        Some(DiffLine::Actual(res)) => {
87                            // We have to make sure that Actual (the + lines)
88                            // always come after Expected (the - lines)
89                            mismatch.lines.push(DiffLine::Expected(str.to_vec()));
90                            if line_number_expected > expected_lines_count {
91                                mismatch.lines.push(DiffLine::MissingNL);
92                            }
93                            mismatch.lines.push(DiffLine::Actual(res));
94                            mismatch.lines.push(DiffLine::MissingNL);
95                        }
96                        _ => unreachable!("unterminated Left and Common lines shouldn't be followed by more Left lines"),
97                    }
98                } else {
99                    mismatch.lines.push(DiffLine::Expected(str.to_vec()));
100                    if line_number_expected > expected_lines_count {
101                        mismatch.lines.push(DiffLine::MissingNL);
102                    }
103                }
104                line_number_expected += 1;
105                lines_since_mismatch = 0;
106            }
107            diff::Result::Right(str) => {
108                if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
109                    results.push(mismatch);
110                    mismatch = Mismatch::new(
111                        line_number_expected - context_queue.len() as u32,
112                        line_number_actual - context_queue.len() as u32,
113                    );
114                }
115
116                while let Some(line) = context_queue.pop_front() {
117                    debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
118                    mismatch.lines.push(DiffLine::Context(line.to_vec()));
119                }
120
121                mismatch.lines.push(DiffLine::Actual(str.to_vec()));
122                if line_number_actual > actual_lines_count {
123                    mismatch.lines.push(DiffLine::MissingNL);
124                }
125                line_number_actual += 1;
126                lines_since_mismatch = 0;
127            }
128            diff::Result::Both(str, _) => {
129                // if one of them is missing a newline and the other isn't, then they don't actually match
130                if (line_number_actual > actual_lines_count)
131                    && (line_number_expected > expected_lines_count)
132                {
133                    if context_queue.len() < context_size {
134                        while let Some(line) = context_queue.pop_front() {
135                            debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
136                            mismatch.lines.push(DiffLine::Context(line.to_vec()));
137                        }
138                        if lines_since_mismatch < context_size {
139                            mismatch.lines.push(DiffLine::Context(str.to_vec()));
140                            mismatch.lines.push(DiffLine::MissingNL);
141                        }
142                    }
143                    lines_since_mismatch = 0;
144                } else if line_number_actual > actual_lines_count {
145                    if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
146                        results.push(mismatch);
147                        mismatch = Mismatch::new(
148                            line_number_expected - context_queue.len() as u32,
149                            line_number_actual - context_queue.len() as u32,
150                        );
151                    }
152                    while let Some(line) = context_queue.pop_front() {
153                        debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
154                        mismatch.lines.push(DiffLine::Context(line.to_vec()));
155                    }
156                    mismatch.lines.push(DiffLine::Expected(str.to_vec()));
157                    mismatch.lines.push(DiffLine::Actual(str.to_vec()));
158                    mismatch.lines.push(DiffLine::MissingNL);
159                    lines_since_mismatch = 0;
160                } else if line_number_expected > expected_lines_count {
161                    if lines_since_mismatch >= context_size && lines_since_mismatch > 0 {
162                        results.push(mismatch);
163                        mismatch = Mismatch::new(
164                            line_number_expected - context_queue.len() as u32,
165                            line_number_actual - context_queue.len() as u32,
166                        );
167                    }
168                    while let Some(line) = context_queue.pop_front() {
169                        debug_assert!(mismatch.lines.last() != Some(&DiffLine::MissingNL));
170                        mismatch.lines.push(DiffLine::Context(line.to_vec()));
171                    }
172                    mismatch.lines.push(DiffLine::Expected(str.to_vec()));
173                    mismatch.lines.push(DiffLine::MissingNL);
174                    mismatch.lines.push(DiffLine::Actual(str.to_vec()));
175                    lines_since_mismatch = 0;
176                } else {
177                    debug_assert!(context_queue.len() <= context_size);
178                    if context_queue.len() >= context_size {
179                        let _ = context_queue.pop_front();
180                    }
181                    if lines_since_mismatch < context_size {
182                        mismatch.lines.push(DiffLine::Context(str.to_vec()));
183                    } else if context_size > 0 {
184                        context_queue.push_back(str);
185                    }
186                    lines_since_mismatch += 1;
187                }
188                line_number_expected += 1;
189                line_number_actual += 1;
190            }
191        }
192        if stop_early && !results.is_empty() {
193            // Optimization: stop analyzing the files as soon as there are any differences
194            return results;
195        }
196    }
197
198    results.push(mismatch);
199    results.remove(0);
200
201    if results.is_empty() && expected_lines_count != actual_lines_count {
202        let mut mismatch = Mismatch::new(expected_lines.len() as u32, actual_lines.len() as u32);
203        // empty diff and only expected lines has a missing line at end
204        if expected_lines_count != expected_lines.len() as u32 {
205            mismatch.lines.push(DiffLine::Expected(
206                expected_lines
207                    .pop()
208                    .expect("can't be empty; produced by split()")
209                    .to_vec(),
210            ));
211            mismatch.lines.push(DiffLine::MissingNL);
212            mismatch.lines.push(DiffLine::Actual(
213                actual_lines
214                    .pop()
215                    .expect("can't be empty; produced by split()")
216                    .to_vec(),
217            ));
218            results.push(mismatch);
219        } else if actual_lines_count != actual_lines.len() as u32 {
220            mismatch.lines.push(DiffLine::Expected(
221                expected_lines
222                    .pop()
223                    .expect("can't be empty; produced by split()")
224                    .to_vec(),
225            ));
226            mismatch.lines.push(DiffLine::Actual(
227                actual_lines
228                    .pop()
229                    .expect("can't be empty; produced by split()")
230                    .to_vec(),
231            ));
232            mismatch.lines.push(DiffLine::MissingNL);
233            results.push(mismatch);
234        }
235    }
236
237    results
238}
239
240#[must_use]
241pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec<u8> {
242    let from_modified_time = get_modification_time(&params.from.to_string_lossy());
243    let to_modified_time = get_modification_time(&params.to.to_string_lossy());
244    let mut output = format!(
245        "--- {0}\t{1}\n+++ {2}\t{3}\n",
246        params.from.to_string_lossy(),
247        from_modified_time,
248        params.to.to_string_lossy(),
249        to_modified_time
250    )
251    .into_bytes();
252    let diff_results = make_diff(expected, actual, params.context_count, params.brief);
253    if diff_results.is_empty() {
254        return Vec::new();
255    }
256    if params.brief {
257        return output;
258    }
259    for result in diff_results {
260        let mut line_number_expected = result.line_number_expected;
261        let mut line_number_actual = result.line_number_actual;
262        let mut expected_count = 0;
263        let mut actual_count = 0;
264        for line in &result.lines {
265            match line {
266                DiffLine::Expected(_) => {
267                    expected_count += 1;
268                }
269                DiffLine::Context(_) => {
270                    expected_count += 1;
271                    actual_count += 1;
272                }
273                DiffLine::Actual(_) => {
274                    actual_count += 1;
275                }
276                DiffLine::MissingNL => {}
277            }
278        }
279        // Let's imagine this diff file
280        //
281        // --- a/something
282        // +++ b/something
283        // @@ -2,0 +3,1 @@
284        // + x
285        //
286        // In the unified diff format as implemented by GNU diff and patch,
287        // this is an instruction to insert the x *after* the preexisting line 2,
288        // not before. You can demonstrate it this way:
289        //
290        // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +3,1 @@\n+ x\n' > diff
291        // $ echo -ne 'a\nb\nc\nd\n' > something
292        // $ patch -p1 < diff
293        // patching file something
294        // $ cat something
295        // a
296        // b
297        //  x
298        // c
299        // d
300        //
301        // Notice how the x winds up at line 3, not line 2. This requires contortions to
302        // work with our diffing algorithm, which keeps track of the "intended destination line",
303        // not a line that things are supposed to be placed after. It's changing the first number,
304        // not the second, that actually affects where the x goes.
305        //
306        // # change the first number from 2 to 3, and now the x is on line 4 (it's placed after line 3)
307        // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,0 +3,1 @@\n+ x\n' > diff
308        // $ echo -ne 'a\nb\nc\nd\n' > something
309        // $ patch -p1 < diff
310        // patching file something
311        // $ cat something
312        // a
313        // b
314        // c
315        //  x
316        // d
317        // # change the third number from 3 to 1000, and it's obvious that it's the first number that's
318        // # actually being read
319        // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,0 +1000,1 @@\n+ x\n' > diff
320        // $ echo -ne 'a\nb\nc\nd\n' > something
321        // $ patch -p1 < diff
322        // patching file something
323        // $ cat something
324        // a
325        // b
326        //  x
327        // c
328        // d
329        //
330        // Now watch what happens if I add a context line:
331        //
332        // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -2,1 +3,2 @@\n+ x\n c\n' > diff
333        // $ echo -ne 'a\nb\nc\nd\n' > something
334        // $ patch -p1 < diff
335        // patching file something
336        // Hunk #1 succeeded at 3 (offset 1 line).
337        //
338        // It technically "succeeded", but this is a warning. We want to produce clean diffs.
339        // Now that I have a context line, I'm supposed to say what line it's actually on, which is the
340        // line that the x will wind up on, and not the line immediately before.
341        //
342        // $ echo -ne '--- a/something\t\n+++ b/something\t\n@@ -3,1 +3,2 @@\n+ x\n c\n' > diff
343        // $ echo -ne 'a\nb\nc\nd\n' > something
344        // $ patch -p1 < diff
345        // patching file something
346        // $ cat something
347        // a
348        // b
349        //  x
350        // c
351        // d
352        //
353        // I made this comment because this stuff is not obvious from GNU's
354        // documentation on the format at all.
355        if expected_count == 0 {
356            line_number_expected -= 1;
357        }
358        if actual_count == 0 {
359            line_number_actual -= 1;
360        }
361        let exp_ct = if expected_count == 1 {
362            String::new()
363        } else {
364            format!(",{expected_count}")
365        };
366        let act_ct = if actual_count == 1 {
367            String::new()
368        } else {
369            format!(",{actual_count}")
370        };
371        writeln!(
372            output,
373            "@@ -{line_number_expected}{exp_ct} +{line_number_actual}{act_ct} @@"
374        )
375        .expect("write to Vec is infallible");
376        for line in result.lines {
377            match line {
378                DiffLine::Expected(e) => {
379                    write!(output, "-").expect("write to Vec is infallible");
380                    do_write_line(&mut output, &e, params.expand_tabs, params.tabsize)
381                        .expect("write to Vec is infallible");
382                    writeln!(output).unwrap();
383                }
384                DiffLine::Context(c) => {
385                    write!(output, " ").expect("write to Vec is infallible");
386                    do_write_line(&mut output, &c, params.expand_tabs, params.tabsize)
387                        .expect("write to Vec is infallible");
388                    writeln!(output).unwrap();
389                }
390                DiffLine::Actual(r) => {
391                    write!(output, "+",).expect("write to Vec is infallible");
392                    do_write_line(&mut output, &r, params.expand_tabs, params.tabsize)
393                        .expect("write to Vec is infallible");
394                    writeln!(output).unwrap();
395                }
396                DiffLine::MissingNL => {
397                    writeln!(output, r"\ No newline at end of file")
398                        .expect("write to Vec is infallible");
399                }
400            }
401        }
402    }
403    output
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use pretty_assertions::assert_eq;
410
411    #[test]
412    fn test_permutations() {
413        let target = "target/unified-diff/";
414        // test all possible six-line files.
415        let _ = std::fs::create_dir(target);
416        for &a in &[0, 1, 2] {
417            for &b in &[0, 1, 2] {
418                for &c in &[0, 1, 2] {
419                    for &d in &[0, 1, 2] {
420                        for &e in &[0, 1, 2] {
421                            for &f in &[0, 1, 2] {
422                                use std::fs::{self, File};
423                                use std::io::Write;
424                                use std::process::Command;
425                                let mut alef = Vec::new();
426                                let mut bet = Vec::new();
427                                alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
428                                    .unwrap();
429                                if a != 2 {
430                                    bet.write_all(b"b\n").unwrap();
431                                }
432                                alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
433                                    .unwrap();
434                                if b != 2 {
435                                    bet.write_all(b"d\n").unwrap();
436                                }
437                                alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
438                                    .unwrap();
439                                if c != 2 {
440                                    bet.write_all(b"f\n").unwrap();
441                                }
442                                alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
443                                    .unwrap();
444                                if d != 2 {
445                                    bet.write_all(b"h\n").unwrap();
446                                }
447                                alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
448                                    .unwrap();
449                                if e != 2 {
450                                    bet.write_all(b"j\n").unwrap();
451                                }
452                                alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
453                                    .unwrap();
454                                if f != 2 {
455                                    bet.write_all(b"l\n").unwrap();
456                                }
457                                // This test diff is intentionally reversed.
458                                // We want it to turn the alef into bet.
459                                let diff = diff(
460                                    &alef,
461                                    &bet,
462                                    &Params {
463                                        from: "a/alef".into(),
464                                        to: (&format!("{target}/alef")).into(),
465                                        context_count: 2,
466                                        ..Default::default()
467                                    },
468                                );
469                                File::create(format!("{target}/ab.diff"))
470                                    .unwrap()
471                                    .write_all(&diff)
472                                    .unwrap();
473                                let mut fa = File::create(format!("{target}/alef")).unwrap();
474                                fa.write_all(&alef[..]).unwrap();
475                                let mut fb = File::create(format!("{target}/bet")).unwrap();
476                                fb.write_all(&bet[..]).unwrap();
477                                let _ = fa;
478                                let _ = fb;
479                                println!(
480                                    "diff: {:?}",
481                                    String::from_utf8(diff.clone())
482                                        .unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
483                                );
484                                println!(
485                                    "alef: {:?}",
486                                    String::from_utf8(alef.clone())
487                                        .unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
488                                );
489                                println!(
490                                    "bet: {:?}",
491                                    String::from_utf8(bet.clone())
492                                        .unwrap_or_else(|_| String::from("[Invalid UTF-8]"))
493                                );
494
495                                let output = Command::new("patch")
496                                    .arg("-p0")
497                                    .stdin(File::open(format!("{target}/ab.diff")).unwrap())
498                                    .output()
499                                    .unwrap();
500                                println!("{}", String::from_utf8_lossy(&output.stdout));
501                                println!("{}", String::from_utf8_lossy(&output.stderr));
502                                assert!(output.status.success(), "{output:?}");
503                                let alef = fs::read(format!("{target}/alef")).unwrap();
504                                assert_eq!(alef, bet);
505                            }
506                        }
507                    }
508                }
509            }
510        }
511    }
512
513    #[test]
514    fn test_permutations_missing_line_ending() {
515        let target = "target/unified-diff/";
516        // test all possible six-line files with missing newlines.
517        let _ = std::fs::create_dir(target);
518        for &a in &[0, 1, 2] {
519            for &b in &[0, 1, 2] {
520                for &c in &[0, 1, 2] {
521                    for &d in &[0, 1, 2] {
522                        for &e in &[0, 1, 2] {
523                            for &f in &[0, 1, 2] {
524                                for &g in &[0, 1, 2] {
525                                    use std::fs::{self, File};
526                                    use std::io::Write;
527                                    use std::process::Command;
528                                    let mut alef = Vec::new();
529                                    let mut bet = Vec::new();
530                                    alef.write_all(if a == 0 { b"a\n" } else { b"b\n" })
531                                        .unwrap();
532                                    if a != 2 {
533                                        bet.write_all(b"b\n").unwrap();
534                                    }
535                                    alef.write_all(if b == 0 { b"c\n" } else { b"d\n" })
536                                        .unwrap();
537                                    if b != 2 {
538                                        bet.write_all(b"d\n").unwrap();
539                                    }
540                                    alef.write_all(if c == 0 { b"e\n" } else { b"f\n" })
541                                        .unwrap();
542                                    if c != 2 {
543                                        bet.write_all(b"f\n").unwrap();
544                                    }
545                                    alef.write_all(if d == 0 { b"g\n" } else { b"h\n" })
546                                        .unwrap();
547                                    if d != 2 {
548                                        bet.write_all(b"h\n").unwrap();
549                                    }
550                                    alef.write_all(if e == 0 { b"i\n" } else { b"j\n" })
551                                        .unwrap();
552                                    if e != 2 {
553                                        bet.write_all(b"j\n").unwrap();
554                                    }
555                                    alef.write_all(if f == 0 { b"k\n" } else { b"l\n" })
556                                        .unwrap();
557                                    if f != 2 {
558                                        bet.write_all(b"l\n").unwrap();
559                                    }
560                                    match g {
561                                        0 => {
562                                            alef.pop();
563                                        }
564                                        1 => {
565                                            bet.pop();
566                                        }
567                                        2 => {
568                                            alef.pop();
569                                            bet.pop();
570                                        }
571                                        _ => unreachable!(),
572                                    }
573                                    // This test diff is intentionally reversed.
574                                    // We want it to turn the alef into bet.
575                                    let diff = diff(
576                                        &alef,
577                                        &bet,
578                                        &Params {
579                                            from: "a/alefn".into(),
580                                            to: (&format!("{target}/alefn")).into(),
581                                            context_count: 2,
582                                            ..Default::default()
583                                        },
584                                    );
585                                    File::create(format!("{target}/abn.diff"))
586                                        .unwrap()
587                                        .write_all(&diff)
588                                        .unwrap();
589                                    let mut fa = File::create(format!("{target}/alefn")).unwrap();
590                                    fa.write_all(&alef[..]).unwrap();
591                                    let mut fb = File::create(format!("{target}/betn")).unwrap();
592                                    fb.write_all(&bet[..]).unwrap();
593                                    let _ = fa;
594                                    let _ = fb;
595                                    let output = Command::new("patch")
596                                        .arg("-p0")
597                                        .stdin(File::open(format!("{target}/abn.diff")).unwrap())
598                                        .output()
599                                        .unwrap();
600                                    assert!(output.status.success(), "{output:?}");
601                                    //println!("{}", String::from_utf8_lossy(&output.stdout));
602                                    //println!("{}", String::from_utf8_lossy(&output.stderr));
603                                    let alef = fs::read(format!("{target}/alefn")).unwrap();
604                                    assert_eq!(alef, bet);
605                                }
606                            }
607                        }
608                    }
609                }
610            }
611        }
612    }
613
614    #[test]
615    fn test_permutations_empty_lines() {
616        let target = "target/unified-diff/";
617        // test all possible six-line files with missing newlines.
618        let _ = std::fs::create_dir(target);
619        for &a in &[0, 1, 2] {
620            for &b in &[0, 1, 2] {
621                for &c in &[0, 1, 2] {
622                    for &d in &[0, 1, 2] {
623                        for &e in &[0, 1, 2] {
624                            for &f in &[0, 1, 2] {
625                                for &g in &[0, 1, 2, 3] {
626                                    use std::fs::{self, File};
627                                    use std::io::Write;
628                                    use std::process::Command;
629                                    let mut alef = Vec::new();
630                                    let mut bet = Vec::new();
631                                    alef.write_all(if a == 0 { b"\n" } else { b"b\n" }).unwrap();
632                                    if a != 2 {
633                                        bet.write_all(b"b\n").unwrap();
634                                    }
635                                    alef.write_all(if b == 0 { b"\n" } else { b"d\n" }).unwrap();
636                                    if b != 2 {
637                                        bet.write_all(b"d\n").unwrap();
638                                    }
639                                    alef.write_all(if c == 0 { b"\n" } else { b"f\n" }).unwrap();
640                                    if c != 2 {
641                                        bet.write_all(b"f\n").unwrap();
642                                    }
643                                    alef.write_all(if d == 0 { b"\n" } else { b"h\n" }).unwrap();
644                                    if d != 2 {
645                                        bet.write_all(b"h\n").unwrap();
646                                    }
647                                    alef.write_all(if e == 0 { b"\n" } else { b"j\n" }).unwrap();
648                                    if e != 2 {
649                                        bet.write_all(b"j\n").unwrap();
650                                    }
651                                    alef.write_all(if f == 0 { b"\n" } else { b"l\n" }).unwrap();
652                                    if f != 2 {
653                                        bet.write_all(b"l\n").unwrap();
654                                    }
655                                    match g {
656                                        0 => {
657                                            alef.pop();
658                                        }
659                                        1 => {
660                                            bet.pop();
661                                        }
662                                        2 => {
663                                            alef.pop();
664                                            bet.pop();
665                                        }
666                                        3 => {}
667                                        _ => unreachable!(),
668                                    }
669                                    // This test diff is intentionally reversed.
670                                    // We want it to turn the alef into bet.
671                                    let diff = diff(
672                                        &alef,
673                                        &bet,
674                                        &Params {
675                                            from: "a/alef_".into(),
676                                            to: (&format!("{target}/alef_")).into(),
677                                            context_count: 2,
678                                            ..Default::default()
679                                        },
680                                    );
681                                    File::create(format!("{target}/ab_.diff"))
682                                        .unwrap()
683                                        .write_all(&diff)
684                                        .unwrap();
685                                    let mut fa = File::create(format!("{target}/alef_")).unwrap();
686                                    fa.write_all(&alef[..]).unwrap();
687                                    let mut fb = File::create(format!("{target}/bet_")).unwrap();
688                                    fb.write_all(&bet[..]).unwrap();
689                                    let _ = fa;
690                                    let _ = fb;
691                                    let output = Command::new("patch")
692                                        .arg("-p0")
693                                        .stdin(File::open(format!("{target}/ab_.diff")).unwrap())
694                                        .output()
695                                        .unwrap();
696                                    assert!(output.status.success(), "{output:?}");
697                                    //println!("{}", String::from_utf8_lossy(&output.stdout));
698                                    //println!("{}", String::from_utf8_lossy(&output.stderr));
699                                    let alef = fs::read(format!("{target}/alef_")).unwrap();
700                                    assert_eq!(alef, bet);
701                                }
702                            }
703                        }
704                    }
705                }
706            }
707        }
708    }
709
710    #[test]
711    fn test_permutations_missing_lines() {
712        let target = "target/unified-diff/";
713        // test all possible six-line files.
714        let _ = std::fs::create_dir(target);
715        for &a in &[0, 1, 2] {
716            for &b in &[0, 1, 2] {
717                for &c in &[0, 1, 2] {
718                    for &d in &[0, 1, 2] {
719                        for &e in &[0, 1, 2] {
720                            for &f in &[0, 1, 2] {
721                                use std::fs::{self, File};
722                                use std::io::Write;
723                                use std::process::Command;
724                                let mut alef = Vec::new();
725                                let mut bet = Vec::new();
726                                alef.write_all(if a == 0 { b"a\n" } else { b"" }).unwrap();
727                                if a != 2 {
728                                    bet.write_all(b"b\n").unwrap();
729                                }
730                                alef.write_all(if b == 0 { b"c\n" } else { b"" }).unwrap();
731                                if b != 2 {
732                                    bet.write_all(b"d\n").unwrap();
733                                }
734                                alef.write_all(if c == 0 { b"e\n" } else { b"" }).unwrap();
735                                if c != 2 {
736                                    bet.write_all(b"f\n").unwrap();
737                                }
738                                alef.write_all(if d == 0 { b"g\n" } else { b"" }).unwrap();
739                                if d != 2 {
740                                    bet.write_all(b"h\n").unwrap();
741                                }
742                                alef.write_all(if e == 0 { b"i\n" } else { b"" }).unwrap();
743                                if e != 2 {
744                                    bet.write_all(b"j\n").unwrap();
745                                }
746                                alef.write_all(if f == 0 { b"k\n" } else { b"" }).unwrap();
747                                if f != 2 {
748                                    bet.write_all(b"l\n").unwrap();
749                                }
750                                // This test diff is intentionally reversed.
751                                // We want it to turn the alef into bet.
752                                let diff = diff(
753                                    &alef,
754                                    &bet,
755                                    &Params {
756                                        from: "a/alefx".into(),
757                                        to: (&format!("{target}/alefx")).into(),
758                                        context_count: 2,
759                                        ..Default::default()
760                                    },
761                                );
762                                File::create(format!("{target}/abx.diff"))
763                                    .unwrap()
764                                    .write_all(&diff)
765                                    .unwrap();
766                                let mut fa = File::create(format!("{target}/alefx")).unwrap();
767                                fa.write_all(&alef[..]).unwrap();
768                                let mut fb = File::create(format!("{target}/betx")).unwrap();
769                                fb.write_all(&bet[..]).unwrap();
770                                let _ = fa;
771                                let _ = fb;
772                                let output = Command::new("patch")
773                                    .arg("-p0")
774                                    .stdin(File::open(format!("{target}/abx.diff")).unwrap())
775                                    .output()
776                                    .unwrap();
777                                assert!(output.status.success(), "{output:?}");
778                                //println!("{}", String::from_utf8_lossy(&output.stdout));
779                                //println!("{}", String::from_utf8_lossy(&output.stderr));
780                                let alef = fs::read(format!("{target}/alefx")).unwrap();
781                                assert_eq!(alef, bet);
782                            }
783                        }
784                    }
785                }
786            }
787        }
788    }
789
790    #[test]
791    fn test_permutations_reverse() {
792        let target = "target/unified-diff/";
793        // test all possible six-line files.
794        let _ = std::fs::create_dir(target);
795        for &a in &[0, 1, 2] {
796            for &b in &[0, 1, 2] {
797                for &c in &[0, 1, 2] {
798                    for &d in &[0, 1, 2] {
799                        for &e in &[0, 1, 2] {
800                            for &f in &[0, 1, 2] {
801                                use std::fs::{self, File};
802                                use std::io::Write;
803                                use std::process::Command;
804                                let mut alef = Vec::new();
805                                let mut bet = Vec::new();
806                                alef.write_all(if a == 0 { b"a\n" } else { b"f\n" })
807                                    .unwrap();
808                                if a != 2 {
809                                    bet.write_all(b"a\n").unwrap();
810                                }
811                                alef.write_all(if b == 0 { b"b\n" } else { b"e\n" })
812                                    .unwrap();
813                                if b != 2 {
814                                    bet.write_all(b"b\n").unwrap();
815                                }
816                                alef.write_all(if c == 0 { b"c\n" } else { b"d\n" })
817                                    .unwrap();
818                                if c != 2 {
819                                    bet.write_all(b"c\n").unwrap();
820                                }
821                                alef.write_all(if d == 0 { b"d\n" } else { b"c\n" })
822                                    .unwrap();
823                                if d != 2 {
824                                    bet.write_all(b"d\n").unwrap();
825                                }
826                                alef.write_all(if e == 0 { b"e\n" } else { b"b\n" })
827                                    .unwrap();
828                                if e != 2 {
829                                    bet.write_all(b"e\n").unwrap();
830                                }
831                                alef.write_all(if f == 0 { b"f\n" } else { b"a\n" })
832                                    .unwrap();
833                                if f != 2 {
834                                    bet.write_all(b"f\n").unwrap();
835                                }
836                                // This test diff is intentionally reversed.
837                                // We want it to turn the alef into bet.
838                                let diff = diff(
839                                    &alef,
840                                    &bet,
841                                    &Params {
842                                        from: "a/alefr".into(),
843                                        to: (&format!("{target}/alefr")).into(),
844                                        context_count: 2,
845                                        ..Default::default()
846                                    },
847                                );
848                                File::create(format!("{target}/abr.diff"))
849                                    .unwrap()
850                                    .write_all(&diff)
851                                    .unwrap();
852                                let mut fa = File::create(format!("{target}/alefr")).unwrap();
853                                fa.write_all(&alef[..]).unwrap();
854                                let mut fb = File::create(format!("{target}/betr")).unwrap();
855                                fb.write_all(&bet[..]).unwrap();
856                                let _ = fa;
857                                let _ = fb;
858                                let output = Command::new("patch")
859                                    .arg("-p0")
860                                    .stdin(File::open(format!("{target}/abr.diff")).unwrap())
861                                    .output()
862                                    .unwrap();
863                                assert!(output.status.success(), "{output:?}");
864                                //println!("{}", String::from_utf8_lossy(&output.stdout));
865                                //println!("{}", String::from_utf8_lossy(&output.stderr));
866                                let alef = fs::read(format!("{target}/alefr")).unwrap();
867                                assert_eq!(alef, bet);
868                            }
869                        }
870                    }
871                }
872            }
873        }
874    }
875
876    #[test]
877    fn test_stop_early() {
878        use crate::assert_diff_eq;
879
880        let from_filename = "foo";
881        let from = ["a", "b", "c", ""].join("\n");
882        let to_filename = "bar";
883        let to = ["a", "d", "c", ""].join("\n");
884
885        let diff_full = diff(
886            from.as_bytes(),
887            to.as_bytes(),
888            &Params {
889                from: from_filename.into(),
890                to: to_filename.into(),
891                ..Default::default()
892            },
893        );
894
895        let expected_full = [
896            "--- foo\tTIMESTAMP",
897            "+++ bar\tTIMESTAMP",
898            "@@ -1,3 +1,3 @@",
899            " a",
900            "-b",
901            "+d",
902            " c",
903            "",
904        ]
905        .join("\n");
906        assert_diff_eq!(diff_full, expected_full);
907
908        let diff_brief = diff(
909            from.as_bytes(),
910            to.as_bytes(),
911            &Params {
912                from: from_filename.into(),
913                to: to_filename.into(),
914                brief: true,
915                ..Default::default()
916            },
917        );
918
919        let expected_brief = ["--- foo\tTIMESTAMP", "+++ bar\tTIMESTAMP", ""].join("\n");
920        assert_diff_eq!(diff_brief, expected_brief);
921
922        let nodiff_full = diff(
923            from.as_bytes(),
924            from.as_bytes(),
925            &Params {
926                from: from_filename.into(),
927                to: to_filename.into(),
928                ..Default::default()
929            },
930        );
931        assert!(nodiff_full.is_empty());
932
933        let nodiff_brief = diff(
934            from.as_bytes(),
935            from.as_bytes(),
936            &Params {
937                from: from_filename.into(),
938                to: to_filename.into(),
939                brief: true,
940                ..Default::default()
941            },
942        );
943        assert!(nodiff_brief.is_empty());
944    }
945}