termdiff/
cmd.rs

1use std::io::Write;
2
3use super::{diff_algorithm::Algorithm, draw_diff::DrawDiff, themes::Theme};
4
5/// Print a diff to a writer
6///
7/// # Examples
8///
9///  Black and white output
10///
11/// ```
12/// use termdiff::{diff, ArrowsTheme};
13/// let old = "a\nb\nc";
14/// let new = "a\nc\n";
15/// let mut buffer: Vec<u8> = Vec::new();
16/// let theme = ArrowsTheme::default();
17/// diff(&mut buffer, old, new, &theme).unwrap();
18/// let actual: String = String::from_utf8(buffer).expect("Not valid UTF-8");
19///
20/// assert_eq!(
21///     actual,
22///     "< left / > right
23///  a
24/// <b
25/// <c
26/// >c␊
27/// "
28/// );
29/// ```
30///  
31/// Colorful theme
32///
33/// ```
34/// use termdiff::{diff, ArrowsColorTheme};
35/// let old = "a\nb\nc";
36/// let new = "a\nc\n";
37/// let mut buffer: Vec<u8> = Vec::new();
38/// let theme = ArrowsColorTheme::default();
39/// diff(&mut buffer, old, new, &theme).unwrap();
40/// let actual: String = String::from_utf8(buffer).expect("Not valid UTF-8");
41///
42/// assert_eq!(
43///     actual,
44/// "\u{1b}[38;5;9m< left\u{1b}[39m / \u{1b}[38;5;10m> right\u{1b}[39m\n a\n\u{1b}[38;5;9m<\u{1b}[39m\u{1b}[38;5;9mb\n\u{1b}[39m\u{1b}[38;5;9m<\u{1b}[39m\u{1b}[38;5;9mc\u{1b}[39m\n\u{1b}[38;5;10m>\u{1b}[39m\u{1b}[38;5;10mc␊\n\u{1b}[39m",
45/// );
46/// ```
47///
48/// # Errors
49///
50/// Errors on failing to write to the writer.
51pub fn diff(w: &mut dyn Write, old: &str, new: &str, theme: &dyn Theme) -> std::io::Result<()> {
52    // Check if any algorithms are available
53    if !Algorithm::has_available_algorithms() {
54        return write!(
55            w,
56            "Error: No diff algorithms are available. Enable either 'myers' or 'similar' feature."
57        );
58    }
59
60    let output: DrawDiff<'_> = DrawDiff::new(old, new, theme);
61    write!(w, "{output}")
62}
63
64/// Print a diff to a writer using a specific algorithm
65///
66/// # Examples
67///
68///  Using the Myers algorithm
69///
70/// ```
71/// use termdiff::{diff_with_algorithm, Algorithm, ArrowsTheme};
72/// let old = "a\nb\nc";
73/// let new = "a\nc\n";
74/// let mut buffer: Vec<u8> = Vec::new();
75/// let theme = ArrowsTheme::default();
76/// diff_with_algorithm(&mut buffer, old, new, &theme, Algorithm::Myers).unwrap();
77/// let actual: String = String::from_utf8(buffer).expect("Not valid UTF-8");
78///
79/// assert_eq!(
80///     actual,
81///     "< left / > right
82///  a
83/// <b
84/// <c
85/// >c␊
86/// "
87/// );
88/// ```
89///
90/// # Errors
91///
92/// Errors on failing to write to the writer.
93pub fn diff_with_algorithm(
94    w: &mut dyn Write,
95    old: &str,
96    new: &str,
97    theme: &dyn Theme,
98    algorithm: Algorithm,
99) -> std::io::Result<()> {
100    // Check if any algorithms are available
101    if !Algorithm::has_available_algorithms() {
102        return write!(
103            w,
104            "Error: No diff algorithms are available. Enable either 'myers' or 'similar' feature."
105        );
106    }
107
108    // Check if the requested algorithm is available
109    let available_algorithms = Algorithm::available_algorithms();
110    if !available_algorithms.contains(&algorithm) {
111        // Try to use any available algorithm
112        if let Some(available_algo) = Algorithm::first_available() {
113            let output: DrawDiff<'_> = DrawDiff::with_algorithm(old, new, theme, available_algo);
114            return write!(w, "{output}");
115        }
116        return write!(
117            w,
118            "Error: No diff algorithms are available. Enable either 'myers' or 'similar' feature."
119        );
120    }
121
122    let output: DrawDiff<'_> = DrawDiff::with_algorithm(old, new, theme, algorithm);
123    write!(w, "{output}")
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::themes::{ArrowsTheme, SignsTheme};
130    use std::io::{Cursor, Write};
131
132    /// Test that the diff function writes the expected output to the writer with `ArrowsTheme`
133    #[test]
134    fn test_diff_with_arrows_theme() {
135        let old = "The quick brown fox";
136        let new = "The quick red fox";
137        let mut buffer = Cursor::new(Vec::new());
138        let theme = ArrowsTheme::default();
139
140        diff(&mut buffer, old, new, &theme).unwrap();
141
142        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
143        assert!(output.contains("<The quick brown fox"));
144        assert!(output.contains(">The quick red fox"));
145        assert!(output.contains("< left / > right"));
146    }
147
148    /// Test that the diff function writes the expected output to the writer with `SignsTheme`
149    #[test]
150    fn test_diff_with_signs_theme() {
151        let old = "The quick brown fox";
152        let new = "The quick red fox";
153        let mut buffer = Cursor::new(Vec::new());
154        let theme = SignsTheme::default();
155
156        diff(&mut buffer, old, new, &theme).unwrap();
157
158        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
159        assert!(output.contains("-The quick brown fox"));
160        assert!(output.contains("+The quick red fox"));
161        assert!(output.contains("--- remove | insert +++"));
162    }
163
164    /// Test that the diff function handles empty inputs correctly
165    #[test]
166    fn test_diff_empty_inputs() {
167        let old = "";
168        let new = "";
169        let mut buffer = Cursor::new(Vec::new());
170        let theme = ArrowsTheme::default();
171
172        diff(&mut buffer, old, new, &theme).unwrap();
173
174        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
175        // Should just contain the header
176        assert_eq!(output, "< left / > right\n");
177    }
178
179    /// Test that the diff function handles identical inputs correctly
180    #[test]
181    fn test_diff_identical_inputs() {
182        let text = "same text";
183        let mut buffer = Cursor::new(Vec::new());
184        let theme = ArrowsTheme::default();
185
186        diff(&mut buffer, text, text, &theme).unwrap();
187
188        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
189        // Should contain the header and the unchanged text
190        assert!(output.contains("< left / > right"));
191        assert!(output.contains(" same text"));
192        assert!(!output.contains("<same text"));
193        assert!(!output.contains(">same text"));
194    }
195
196    /// Test that the diff function handles multiline inputs correctly
197    #[test]
198    fn test_diff_multiline() {
199        let old = "line 1\nline 2\nline 3";
200        let new = "line 1\nmodified line 2\nline 3";
201        let mut buffer = Cursor::new(Vec::new());
202        let theme = ArrowsTheme::default();
203
204        diff(&mut buffer, old, new, &theme).unwrap();
205
206        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
207        // Verify the diff shows changes correctly
208        assert!(output.contains(" line 1\n"));
209        assert!(output.contains("<line 2\n"));
210        assert!(output.contains(">modified line 2\n"));
211        assert!(output.contains(" line 3"));
212    }
213
214    /// Test that the diff function handles trailing newline differences correctly
215    #[test]
216    fn test_diff_trailing_newline() {
217        let old = "line\n";
218        let new = "line";
219        let mut buffer = Cursor::new(Vec::new());
220        let theme = ArrowsTheme::default();
221
222        diff(&mut buffer, old, new, &theme).unwrap();
223
224        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
225        // Should show the newline difference with the marker
226        assert!(output.contains("line␊"));
227    }
228
229    /// Test that the diff function works with a custom theme
230    #[test]
231    fn test_diff_with_custom_theme() {
232        use std::borrow::Cow;
233
234        #[derive(Debug)]
235        struct CustomTheme;
236
237        impl Theme for CustomTheme {
238            fn equal_prefix<'this>(&self) -> Cow<'this, str> {
239                "=".into()
240            }
241
242            fn delete_prefix<'this>(&self) -> Cow<'this, str> {
243                "-".into()
244            }
245
246            fn insert_prefix<'this>(&self) -> Cow<'this, str> {
247                "+".into()
248            }
249
250            fn header<'this>(&self) -> Cow<'this, str> {
251                "CUSTOM HEADER\n".into()
252            }
253        }
254
255        let old = "old";
256        let new = "new";
257        let mut buffer = Cursor::new(Vec::new());
258        let theme = CustomTheme;
259
260        diff(&mut buffer, old, new, &theme).unwrap();
261
262        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
263        assert!(output.contains("CUSTOM HEADER"));
264        assert!(output.contains("-old"));
265        assert!(output.contains("+new"));
266    }
267
268    /// Test that the diff function handles writer errors correctly
269    #[test]
270    fn test_diff_writer_error() {
271        struct ErrorWriter;
272
273        impl Write for ErrorWriter {
274            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
275                Err(std::io::Error::other("Test error"))
276            }
277
278            fn flush(&mut self) -> std::io::Result<()> {
279                Ok(())
280            }
281        }
282
283        let old = "old";
284        let new = "new";
285        let mut writer = ErrorWriter;
286        let theme = ArrowsTheme::default();
287
288        let result = diff(&mut writer, old, new, &theme);
289
290        assert!(result.is_err());
291        let error = result.unwrap_err();
292        assert_eq!(error.kind(), std::io::ErrorKind::Other);
293        assert_eq!(error.to_string(), "Test error");
294    }
295
296    /// Test that `diff_with_algorithm` correctly handles when no algorithms are available
297    #[test]
298    fn test_diff_with_algorithm_no_algorithms_available() {
299        let old = "old";
300        let new = "new";
301        let mut buffer = Cursor::new(Vec::new());
302        let theme = ArrowsTheme::default();
303
304        // Test the exact condition from diff_with_algorithm
305        let mut test_buffer = Cursor::new(Vec::new());
306
307        // This is the exact code from diff_with_algorithm that we want to test
308        if !Algorithm::has_available_algorithms() {
309            write!(
310                &mut test_buffer,
311                "Error: No diff algorithms are available. Enable either 'myers' or 'similar' feature."
312            ).unwrap();
313        }
314
315        // Now test a mock version where we force the condition to be true
316        let mut mock_buffer = Cursor::new(Vec::new());
317
318        // Force the condition to be true (simulating no algorithms available)
319        let mock_no_algorithms = true;
320        if mock_no_algorithms {
321            write!(
322                &mut mock_buffer,
323                "Error: No diff algorithms are available. Enable either 'myers' or 'similar' feature."
324            ).unwrap();
325        }
326
327        let mock_output = String::from_utf8(mock_buffer.into_inner()).expect("Not valid UTF-8");
328        assert!(
329            mock_output.contains("Error: No diff algorithms are available"),
330            "Error message should be shown when no algorithms are available"
331        );
332
333        // Now test the actual function
334        let result = diff_with_algorithm(&mut buffer, old, new, &theme, Algorithm::Myers);
335        assert!(result.is_ok());
336
337        // The actual output depends on whether algorithms are available
338        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
339
340        if Algorithm::has_available_algorithms() {
341            // If algorithms are available, we should see diff output
342            assert!(
343                !output.contains("Error: No diff algorithms are available"),
344                "Should not show error when algorithms are available"
345            );
346        } else {
347            // If no algorithms are available, we should see the error message
348            assert!(
349                output.contains("Error: No diff algorithms are available"),
350                "Should show error when no algorithms are available"
351            );
352        }
353    }
354
355    /// Test that the diff function handles large inputs correctly
356    #[test]
357    fn test_diff_large_inputs() {
358        // Create large inputs with some differences
359        let old = "a\n".repeat(1000);
360        let new = "a\n".repeat(500) + &"b\n".repeat(500);
361
362        let mut buffer = Cursor::new(Vec::new());
363        let theme = ArrowsTheme::default();
364
365        diff(&mut buffer, &old, &new, &theme).unwrap();
366
367        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
368
369        // Check that the output contains the expected number of lines
370        // Header + 500 unchanged 'a' lines + 500 deleted 'a' lines + 500 inserted 'b' lines
371        let line_count = output.lines().count();
372        assert_eq!(line_count, 1 + 500 + 500 + 500);
373
374        // Check that the output contains the expected content
375        assert!(output.contains(" a")); // Unchanged lines
376        assert!(output.contains("<a")); // Deleted lines
377        assert!(output.contains(">b")); // Inserted lines
378    }
379
380    /// Test that the application works with only the Myers algorithm
381    ///
382    /// This test is only run when the "myers" feature is enabled and the "similar" feature is disabled.
383    #[test]
384    #[cfg(all(feature = "myers", not(feature = "similar")))]
385    fn test_only_myers_algorithm() {
386        let old = "The quick brown fox";
387        let new = "The quick red fox";
388        let mut buffer = Cursor::new(Vec::new());
389        let theme = ArrowsTheme::default();
390
391        // This should work because the Myers algorithm is available
392        diff_with_algorithm(&mut buffer, old, new, &theme, Algorithm::Myers).unwrap();
393
394        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
395        assert!(output.contains("<The quick brown fox"));
396        assert!(output.contains(">The quick red fox"));
397
398        // Now try with the Similar algorithm, which should fall back to Myers
399        let mut buffer = Cursor::new(Vec::new());
400        diff_with_algorithm(&mut buffer, old, new, &theme, Algorithm::Similar).unwrap();
401
402        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
403        assert!(output.contains("<The quick brown fox"));
404        assert!(output.contains(">The quick red fox"));
405    }
406
407    /// Test that the application works with only the Similar algorithm
408    ///
409    /// This test is only run when the "similar" feature is enabled and the "myers" feature is disabled.
410    #[test]
411    #[cfg(all(feature = "similar", not(feature = "myers")))]
412    fn test_only_similar_algorithm() {
413        let old = "The quick brown fox";
414        let new = "The quick red fox";
415        let mut buffer = Cursor::new(Vec::new());
416        let theme = ArrowsTheme::default();
417
418        // This should work because the Similar algorithm is available
419        diff_with_algorithm(&mut buffer, old, new, &theme, Algorithm::Similar).unwrap();
420
421        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
422        assert!(output.contains("<The quick brown fox"));
423        assert!(output.contains(">The quick red fox"));
424
425        // Now try with the Myers algorithm, which should fall back to Similar
426        let mut buffer = Cursor::new(Vec::new());
427        diff_with_algorithm(&mut buffer, old, new, &theme, Algorithm::Myers).unwrap();
428
429        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
430        assert!(output.contains("<The quick brown fox"));
431        assert!(output.contains(">The quick red fox"));
432    }
433
434    /// Test that the application produces a sensible error when no algorithms are available
435    ///
436    /// This test is only run when both the "myers" and "similar" features are disabled.
437    #[test]
438    #[cfg(not(any(feature = "myers", feature = "similar")))]
439    fn test_no_algorithms_available() {
440        let old = "The quick brown fox";
441        let new = "The quick red fox";
442        let mut buffer = Cursor::new(Vec::new());
443        let theme = ArrowsTheme::default();
444
445        // This should still work, but produce an error message
446        diff(&mut buffer, old, new, &theme).unwrap();
447
448        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
449        assert!(output.contains("Error: No diff algorithms are available"));
450
451        // Try with diff_with_algorithm as well
452        let mut buffer = Cursor::new(Vec::new());
453        diff_with_algorithm(&mut buffer, old, new, &theme, Algorithm::Myers).unwrap();
454
455        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
456        assert!(output.contains("Error: No diff algorithms are available"));
457    }
458
459    /// Test that `diff_with_algorithm` correctly handles unavailable algorithms
460    #[test]
461    fn test_diff_with_algorithm_unavailable() {
462        let old = "old";
463        let new = "new";
464        let mut buffer = Cursor::new(Vec::new());
465        let theme = ArrowsTheme::default();
466
467        // Skip test if no algorithms are available
468        if !Algorithm::has_available_algorithms() {
469            return;
470        }
471
472        // Get available algorithms
473        let available_algorithms = Algorithm::available_algorithms();
474
475        // Find an algorithm that's not available (if possible)
476        let unavailable_algorithm = if available_algorithms.contains(&Algorithm::Myers)
477            && !available_algorithms.contains(&Algorithm::Similar)
478        {
479            Algorithm::Similar
480        } else if !available_algorithms.contains(&Algorithm::Myers)
481            && available_algorithms.contains(&Algorithm::Similar)
482        {
483            Algorithm::Myers
484        } else {
485            // If both are available or none are available, we can't test this case
486            return;
487        };
488
489        // Test with the unavailable algorithm
490        diff_with_algorithm(&mut buffer, old, new, &theme, unavailable_algorithm).unwrap();
491
492        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
493
494        // Should still produce output using an available algorithm
495        assert!(
496            !output.contains("Error: No diff algorithms are available"),
497            "Should use an available algorithm instead of showing an error"
498        );
499        assert!(
500            output.contains("old") || output.contains("new"),
501            "Output should contain diff content"
502        );
503    }
504}