diff options
Diffstat (limited to 'vendor/ui_test/src/diff.rs')
-rw-r--r-- | vendor/ui_test/src/diff.rs | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/vendor/ui_test/src/diff.rs b/vendor/ui_test/src/diff.rs new file mode 100644 index 000000000..916645dd1 --- /dev/null +++ b/vendor/ui_test/src/diff.rs @@ -0,0 +1,175 @@ +use colored::*; +use diff::{chars, lines, Result, Result::*}; + +#[derive(Default)] +struct DiffState<'a> { + /// Whether we've already printed something, so we should print starting context, too. + print_start_context: bool, + /// When we skip lines, remember the last `CONTEXT` ones to + /// display after the "skipped N lines" message + skipped_lines: Vec<&'a str>, + /// When we see a removed line, we don't print it, we + /// keep it around to compare it with the next added line. + prev_left: Option<&'a str>, +} + +/// How many lines of context are displayed around the actual diffs +const CONTEXT: usize = 2; + +impl<'a> DiffState<'a> { + /// Print `... n lines skipped ...` followed by the last `CONTEXT` lines. + fn print_end_skip(&self, skipped: usize) { + self.print_skipped_msg(skipped); + for line in self.skipped_lines.iter().rev().take(CONTEXT).rev() { + eprintln!(" {line}"); + } + } + + fn print_skipped_msg(&self, skipped: usize) { + match skipped { + // When the amount of skipped lines is exactly `CONTEXT * 2`, we already + // print all the context and don't actually skip anything. + 0 => {} + // Instead of writing a line saying we skipped one line, print that one line + 1 => eprintln!(" {}", self.skipped_lines[CONTEXT]), + _ => eprintln!("... {skipped} lines skipped ..."), + } + } + + /// Print an initial `CONTEXT` amount of lines. + fn print_start_skip(&self) { + for line in self.skipped_lines.iter().take(CONTEXT) { + eprintln!(" {line}"); + } + } + + fn print_skip(&mut self) { + let half = self.skipped_lines.len() / 2; + if !self.print_start_context { + self.print_start_context = true; + self.print_end_skip(self.skipped_lines.len().saturating_sub(CONTEXT)); + } else if half < CONTEXT { + // Print all the skipped lines if the amount of context desired is less than the amount of lines + for line in self.skipped_lines.drain(..) { + eprintln!(" {line}"); + } + } else { + self.print_start_skip(); + let skipped = self.skipped_lines.len() - CONTEXT * 2; + self.print_end_skip(skipped); + } + self.skipped_lines.clear(); + } + + fn skip(&mut self, line: &'a str) { + self.skipped_lines.push(line); + } + + fn print_prev(&mut self) { + if let Some(l) = self.prev_left.take() { + self.print_left(l); + } + } + + fn print_left(&self, l: &str) { + eprintln!("{}{}", "-".red(), l.red()); + } + + fn print_right(&self, r: &str) { + eprintln!("{}{}", "+".green(), r.green()); + } + + fn row(&mut self, row: Result<&'a str>) { + match row { + Left(l) => { + self.print_skip(); + self.print_prev(); + self.prev_left = Some(l); + } + Both(l, _) => { + self.print_prev(); + self.skip(l); + } + Right(r) => { + // When there's an added line after a removed line, we'll want to special case some print cases. + // FIXME(oli-obk): also do special printing modes when there are multiple lines that only have minor changes. + if let Some(l) = self.prev_left.take() { + let diff = chars(l, r); + let mut seen_l = false; + let mut seen_r = false; + for char in &diff { + match char { + Left(l) if !l.is_whitespace() => seen_l = true, + Right(r) if !r.is_whitespace() => seen_r = true, + _ => {} + } + } + if seen_l && seen_r { + // The line both adds and removes chars, print both lines, but highlight their differences instead of + // drawing the entire line in red/green. + eprint!("{}", "-".red()); + for char in &diff { + match *char { + Left(l) => eprint!("{}", l.to_string().red()), + Right(_) => {} + Both(l, _) => eprint!("{l}"), + } + } + eprintln!(); + eprint!("{}", "+".green()); + for char in diff { + match char { + Left(_) => {} + Right(r) => eprint!("{}", r.to_string().green()), + Both(l, _) => eprint!("{l}"), + } + } + eprintln!(); + } else { + // The line only adds or only removes chars, print a single line highlighting their differences. + eprint!("{}", "~".yellow()); + for char in diff { + match char { + Left(l) => eprint!("{}", l.to_string().red()), + Both(l, _) => eprint!("{l}"), + Right(r) => eprint!("{}", r.to_string().green()), + } + } + eprintln!(); + } + } else { + self.print_skip(); + self.print_right(r); + } + } + } + } + + fn finish(self) { + self.print_start_skip(); + self.print_skipped_msg(self.skipped_lines.len().saturating_sub(CONTEXT)); + eprintln!() + } +} + +pub fn print_diff(expected: &[u8], actual: &[u8]) { + let expected_str = String::from_utf8_lossy(expected); + let actual_str = String::from_utf8_lossy(actual); + + if expected_str.as_bytes() != expected || actual_str.as_bytes() != actual { + eprintln!( + "{}", + "Non-UTF8 characters in output, diff may be imprecise.".red() + ); + } + + let pat = |c: char| c.is_whitespace() && c != ' ' && c != '\n' && c != '\r'; + let expected_str = expected_str.replace(pat, "░"); + let actual_str = actual_str.replace(pat, "░"); + + let mut state = DiffState::default(); + for row in lines(&expected_str, &actual_str) { + state.row(row); + } + state.finish(); +} |