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(¶ms.from.to_string_lossy());
243 let to_modified_time = get_modification_time(¶ms.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}