Repository: brendanzab/codespan (rust)

Issue:
Add smoke tests for colored output
We added a non-colored smoke test in #76, but we don't currently test the colors in our output. We should figure out how to do that nicely!

Pull request description:
Add color snapshot tests
This adds some color snapshot tests, using @wycats' [groovey color tester thing from language-reporting](https://github.com/wycats/language-reporting/blob/b021c87e0d4916b5f32756151bf215c220eee52d/crates/render-tree/src/stylesheet/accumulator.rs). I noticed that the output was kinda wonky so I did some cleaning up in the process.

It's a tad hard to eyeball how everything lines up in these, due to the formatting tags, but I figure for that you can check out the no_color tests.

Closes #81.

Patch:
diff --git a/codespan-reporting/src/emitter/views/source_snippet.rs b/codespan-reporting/src/emitter/views/source_snippet.rs
index 1978fb6..25390c7 100644
--- a/codespan-reporting/src/emitter/views/source_snippet.rs
+++ b/codespan-reporting/src/emitter/views/source_snippet.rs
@@ -171,7 +171,9 @@ impl<'a> SourceSnippet<'a> {
                 .source_slice(marked_span, &tab)
                 .expect("marked_source_1");
             writer.set_color(&label_spec)?;
-            write!(writer, "{}", marked_source)?;
+            write!(writer, "{}", marked_source.trim_end_matches(line_trimmer))?;
+            writer.reset()?;
+            NewLine::new().emit(writer, config)?;
 
             for line_index in ((start.line.to_usize() + 1)..end.line.to_usize())
                 .map(|i| LineIndex::from(i as u32))
@@ -187,6 +189,7 @@ impl<'a> SourceSnippet<'a> {
                     .expect("marked_source_2");
                 writer.set_color(&label_spec)?;
                 write!(writer, "{}", marked_source.trim_end_matches(line_trimmer))?;
+                writer.reset()?;
                 NewLine::new().emit(writer, config)?;
             }
 
@@ -216,13 +219,13 @@ impl<'a> SourceSnippet<'a> {
         BorderLeft::new().emit(writer, config)?;
 
         // Write underline and label
-        writer.set_color(&label_spec)?;
         write!(
             writer,
             "{space: >width$}",
             space = "",
             width = config.width(&source_prefix),
         )?;
+        writer.set_color(&label_spec)?;
         // We use `usize::max` here to ensure that we print at least one
         // underline character - even when we have a zero-length span.
         for _ in 0..usize::max(mark_len, 1) {
@@ -231,8 +234,8 @@ impl<'a> SourceSnippet<'a> {
         if !self.label.message.is_empty() {
             write!(writer, " {}", self.label.message)?;
         }
-        NewLine::new().emit(writer, config)?;
         writer.reset()?;
+        NewLine::new().emit(writer, config)?;
 
         // Write final border
         Gutter::new(None, gutter_padding).emit(writer, config)?;


Test patch:
diff --git a/codespan-reporting/tests/emit.rs b/codespan-reporting/tests/emit.rs
index ecdb786..e97494a 100644
--- a/codespan-reporting/tests/emit.rs
+++ b/codespan-reporting/tests/emit.rs
@@ -2,6 +2,10 @@ use codespan::Files;
 use codespan_reporting::termcolor::{Buffer, WriteColor};
 use codespan_reporting::{emit, Config, Diagnostic, DisplayStyle, Label};
 
+mod support;
+
+use self::support::ColorBuffer;
+
 mod empty_spans {
     use super::*;
 
@@ -22,6 +26,19 @@ mod empty_spans {
         }
     }
 
+    #[test]
+    fn rich_color() {
+        let config = Config {
+            display_style: DisplayStyle::Rich,
+            ..Config::default()
+        };
+
+        let mut buffer = ColorBuffer::new();
+        emit_test(&mut buffer, &config);
+        let result = buffer.into_string();
+        insta::assert_snapshot_matches!("rich_color", result);
+    }
+
     #[test]
     fn rich_no_color() {
         let config = Config {
@@ -113,6 +130,32 @@ mod multifile {
         }
     }
 
+    #[test]
+    fn rich_color() {
+        let config = Config {
+            display_style: DisplayStyle::Rich,
+            ..Config::default()
+        };
+
+        let mut buffer = ColorBuffer::new();
+        emit_test(&mut buffer, &config);
+        let result = buffer.into_string();
+        insta::assert_snapshot_matches!("rich_color", result);
+    }
+
+    #[test]
+    fn short_color() {
+        let config = Config {
+            display_style: DisplayStyle::Short,
+            ..Config::default()
+        };
+
+        let mut buffer = ColorBuffer::new();
+        emit_test(&mut buffer, &config);
+        let result = buffer.into_string();
+        insta::assert_snapshot_matches!("short_color", result);
+    }
+
     #[test]
     fn rich_no_color() {
         let config = Config {
@@ -127,7 +170,7 @@ mod multifile {
     }
 
     #[test]
-    fn simple_no_color() {
+    fn short_no_color() {
         let config = Config {
             display_style: DisplayStyle::Short,
             ..Config::default()
@@ -211,6 +254,32 @@ mod fizz_buzz {
         }
     }
 
+    #[test]
+    fn rich_color() {
+        let config = Config {
+            display_style: DisplayStyle::Rich,
+            ..Config::default()
+        };
+
+        let mut buffer = ColorBuffer::new();
+        emit_test(&mut buffer, &config);
+        let result = buffer.into_string();
+        insta::assert_snapshot_matches!("rich_color", result);
+    }
+
+    #[test]
+    fn short_color() {
+        let config = Config {
+            display_style: DisplayStyle::Short,
+            ..Config::default()
+        };
+
+        let mut buffer = ColorBuffer::new();
+        emit_test(&mut buffer, &config);
+        let result = buffer.into_string();
+        insta::assert_snapshot_matches!("short_color", result);
+    }
+
     #[test]
     fn rich_no_color() {
         let config = Config {
@@ -225,7 +294,7 @@ mod fizz_buzz {
     }
 
     #[test]
-    fn simple_no_color() {
+    fn short_no_color() {
         let config = Config {
             display_style: DisplayStyle::Short,
             ..Config::default()
diff --git a/codespan-reporting/tests/snapshots/empty_spans__rich_color.snap b/codespan-reporting/tests/snapshots/empty_spans__rich_color.snap
new file mode 100644
index 0000000..9c8da1b
--- /dev/null
+++ b/codespan-reporting/tests/snapshots/empty_spans__rich_color.snap
@@ -0,0 +1,31 @@
+---
+created: "2019-06-17T13:57:18.684239Z"
+creator: insta@0.8.1
+source: codespan-reporting/tests/emit.rs
+expression: result
+---
+{fg:Green bold bright}note{bold bright}: middle{/}
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} hello:1:7 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}1{/} {fg:Blue}│ {/}Hello {fg:Green}{/}world!
+   {fg:Blue}│ {/}      {fg:Green}^ middle{/}
+   {fg:Blue}│ {/}
+
+{fg:Green bold bright}note{bold bright}: end of line{/}
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} hello:1:13 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}1{/} {fg:Blue}│ {/}Hello world!{fg:Green}{/}
+   {fg:Blue}│ {/}            {fg:Green}^ end of line{/}
+   {fg:Blue}│ {/}
+
+{fg:Green bold bright}note{bold bright}: end of file{/}
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} hello:2:11 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}2{/} {fg:Blue}│ {/}Bye world!{fg:Green}{/}
+   {fg:Blue}│ {/}          {fg:Green}^ end of file{/}
+   {fg:Blue}│ {/}
+
+
diff --git a/codespan-reporting/tests/snapshots/fizz_buzz__rich_color.snap b/codespan-reporting/tests/snapshots/fizz_buzz__rich_color.snap
new file mode 100644
index 0000000..cf33245
--- /dev/null
+++ b/codespan-reporting/tests/snapshots/fizz_buzz__rich_color.snap
@@ -0,0 +1,71 @@
+---
+created: "2019-06-17T13:57:18.683891Z"
+creator: insta@0.8.1
+source: codespan-reporting/tests/emit.rs
+expression: result
+---
+{fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:8:12 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}8{/} {fg:Blue}│ {/}    _ _ => {fg:Red}num{/}
+   {fg:Blue}│ {/}           {fg:Red}^^^ expected `String`, found `Nat`{/}
+   {fg:Blue}│ {/}
+   {fg:Blue}={/} expected type `String`
+        found type `Nat`
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:4:13 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}4{/} {fg:Blue}│ {/}fizz₁ num = {fg:Blue}case (mod num 5) (mod num 3) of{/}
+ {fg:Blue}5{/} {fg:Blue}│ {/}{fg:Blue}    0 0 => "FizzBuzz"{/}
+ {fg:Blue}6{/} {fg:Blue}│ {/}{fg:Blue}    0 _ => "Fizz"{/}
+ {fg:Blue}7{/} {fg:Blue}│ {/}{fg:Blue}    _ 0 => "Buzz"{/}
+ {fg:Blue}8{/} {fg:Blue}│ {/}{fg:Blue}    _ _ => num{/}
+   {fg:Blue}│ {/}            {fg:Blue}-------------- `case` clauses have incompatible types{/}
+   {fg:Blue}│ {/}
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:3:15 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}3{/} {fg:Blue}│ {/}fizz₁ : Nat → {fg:Blue}String{/}
+   {fg:Blue}│ {/}              {fg:Blue}------ expected type `String` found here{/}
+   {fg:Blue}│ {/}
+
+{fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+
+    {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:15:16 {fg:Blue}───{/}
+    {fg:Blue}│ {/}
+ {fg:Blue}15{/} {fg:Blue}│ {/}        _ _ => {fg:Red}num{/}
+    {fg:Blue}│ {/}               {fg:Red}^^^ expected `String`, found `Nat`{/}
+    {fg:Blue}│ {/}
+    {fg:Blue}={/} expected type `String`
+         found type `Nat`
+
+    {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:11:5 {fg:Blue}───{/}
+    {fg:Blue}│ {/}
+ {fg:Blue}11{/} {fg:Blue}│ {/}    {fg:Blue}case (mod num 5) (mod num 3) of{/}
+ {fg:Blue}12{/} {fg:Blue}│ {/}{fg:Blue}        0 0 => "FizzBuzz"{/}
+ {fg:Blue}13{/} {fg:Blue}│ {/}{fg:Blue}        0 _ => "Fizz"{/}
+ {fg:Blue}14{/} {fg:Blue}│ {/}{fg:Blue}        _ 0 => "Buzz"{/}
+ {fg:Blue}15{/} {fg:Blue}│ {/}{fg:Blue}        _ _ => num{/}
+    {fg:Blue}│ {/}    {fg:Blue}------------------ `case` clauses have incompatible types{/}
+    {fg:Blue}│ {/}
+
+    {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:12:16 {fg:Blue}───{/}
+    {fg:Blue}│ {/}
+ {fg:Blue}12{/} {fg:Blue}│ {/}        0 0 => {fg:Blue}"FizzBuzz"{/}
+    {fg:Blue}│ {/}               {fg:Blue}---------- this is found to be of type `String`{/}
+    {fg:Blue}│ {/}
+
+    {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:13:16 {fg:Blue}───{/}
+    {fg:Blue}│ {/}
+ {fg:Blue}13{/} {fg:Blue}│ {/}        0 _ => {fg:Blue}"Fizz"{/}
+    {fg:Blue}│ {/}               {fg:Blue}------ this is found to be of type `String`{/}
+    {fg:Blue}│ {/}
+
+    {fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:14:16 {fg:Blue}───{/}
+    {fg:Blue}│ {/}
+ {fg:Blue}14{/} {fg:Blue}│ {/}        _ 0 => {fg:Blue}"Buzz"{/}
+    {fg:Blue}│ {/}               {fg:Blue}------ this is found to be of type `String`{/}
+    {fg:Blue}│ {/}
+
+
diff --git a/codespan-reporting/tests/snapshots/fizz_buzz__short_color.snap b/codespan-reporting/tests/snapshots/fizz_buzz__short_color.snap
new file mode 100644
index 0000000..89966a6
--- /dev/null
+++ b/codespan-reporting/tests/snapshots/fizz_buzz__short_color.snap
@@ -0,0 +1,9 @@
+---
+created: "2019-06-17T13:42:02.846649Z"
+creator: insta@0.8.1
+source: codespan-reporting/tests/emit.rs
+expression: result
+---
+FizzBuzz.fun:8:12: {fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+FizzBuzz.fun:15:16: {fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}
+
diff --git a/codespan-reporting/tests/snapshots/multifile__rich_color.snap b/codespan-reporting/tests/snapshots/multifile__rich_color.snap
new file mode 100644
index 0000000..47a7ddd
--- /dev/null
+++ b/codespan-reporting/tests/snapshots/multifile__rich_color.snap
@@ -0,0 +1,39 @@
+---
+created: "2019-06-17T13:57:18.683834Z"
+creator: insta@0.8.1
+source: codespan-reporting/tests/emit.rs
+expression: result
+---
+{fg:Red bold bright}error{bold bright}: unknown builtin: `NATRAL`{/}
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} Data/Nat.fun:7:13 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}7{/} {fg:Blue}│ {/}{-# BUILTIN {fg:Red}NATRAL{/} Nat #-}
+   {fg:Blue}│ {/}            {fg:Red}^^^^^^ unknown builtin{/}
+   {fg:Blue}│ {/}
+   {fg:Blue}={/} there is a builtin with a similar name: `NATURAL`
+
+{fg:Yellow bold bright}warning{bold bright}: unused parameter pattern: `n₂`{/}
+
+    {fg:Blue}┌{/}{fg:Blue}──{/} Data/Nat.fun:17:16 {fg:Blue}───{/}
+    {fg:Blue}│ {/}
+ {fg:Blue}17{/} {fg:Blue}│ {/}zero    - succ {fg:Yellow}n₂{/} = zero
+    {fg:Blue}│ {/}               {fg:Yellow}^^ unused parameter{/}
+    {fg:Blue}│ {/}
+    {fg:Blue}={/} consider using a wildcard pattern: `_`
+
+{fg:Red bold bright}error[E0001]{bold bright}: unexpected type in application of `_+_`{/}
+
+   {fg:Blue}┌{/}{fg:Blue}──{/} Test.fun:4:11 {fg:Blue}───{/}
+   {fg:Blue}│ {/}
+ {fg:Blue}4{/} {fg:Blue}│ {/}_ = 123 + {fg:Red}"hello"{/}
+   {fg:Blue}│ {/}          {fg:Red}^^^^^^^ expected `Nat`, found `String`{/}
+   {fg:Blue}│ {/}
+
+    {fg:Blue}┌{/}{fg:Blue}──{/} Data/Nat.fun:11:1 {fg:Blue}───{/}
+    {fg:Blue}│ {/}
+ {fg:Blue}11{/} {fg:Blue}│ {/}{fg:Blue}_+_ : Nat → Nat → Nat{/}
+    {fg:Blue}│ {/}{fg:Blue}--------------------- based on the definition of `_+_`{/}
+    {fg:Blue}│ {/}
+
+
diff --git a/codespan-reporting/tests/snapshots/multifile__short_color.snap b/codespan-reporting/tests/snapshots/multifile__short_color.snap
new file mode 100644
index 0000000..09ae9ab
--- /dev/null
+++ b/codespan-reporting/tests/snapshots/multifile__short_color.snap
@@ -0,0 +1,10 @@
+---
+created: "2019-06-17T13:42:02.846651Z"
+creator: insta@0.8.1
+source: codespan-reporting/tests/emit.rs
+expression: result
+---
+Data/Nat.fun:7:13: {fg:Red bold bright}error{bold bright}: unknown builtin: `NATRAL`{/}
+Data/Nat.fun:17:16: {fg:Yellow bold bright}warning{bold bright}: unused parameter pattern: `n₂`{/}
+Test.fun:4:11: {fg:Red bold bright}error[E0001]{bold bright}: unexpected type in application of `_+_`{/}
+
diff --git a/codespan-reporting/tests/support/mod.rs b/codespan-reporting/tests/support/mod.rs
new file mode 100644
index 0000000..bad4060
--- /dev/null
+++ b/codespan-reporting/tests/support/mod.rs
@@ -0,0 +1,137 @@
+use codespan_reporting::termcolor::{ColorSpec, WriteColor};
+use std::io;
+use std::io::prelude::*;
+
+// Color tester from:
+// https://github.com/wycats/language-reporting/blob/b021c87e0d4916b5f32756151bf215c220eee52d/crates/render-tree/src/stylesheet/accumulator.rs
+
+/// A facility for creating visually inspectable representations of colored output
+/// so they can be easily tested.
+///
+/// A new color is represented as `{style}` and a reset is represented by `{/}`.
+///
+/// Attributes are printed in this order:
+///
+/// - Foreground color as `fg:Color`
+/// - Background color as `bg:Color`
+/// - Bold as `bold`
+/// - Underline as `underline`
+/// - Intense as `bright`
+///
+/// For example, the style "intense, bold red foreground" would be printed as:
+///
+/// ```text
+/// {fg:Red bold intense}
+/// ```
+///
+/// Since this implementation attempts to make it possible to faithfully
+/// understand what real WriteColor implementations would do, it tries
+/// to approximate the contract in the WriteColor trait: "Subsequent
+/// writes to this write will use these settings until either reset is
+/// called or new color settings are set.")
+///
+/// - If set_color is called with a style, `{...}` is emitted containing the
+///   color attributes.
+/// - If set_color is called with no style, `{/}` is emitted
+/// - If reset is called, `{/}` is emitted.
+pub struct ColorBuffer {
+    buf: Vec<u8>,
+    color: ColorSpec,
+}
+
+impl ColorBuffer {
+    pub fn new() -> ColorBuffer {
+        ColorBuffer {
+            buf: Vec::new(),
+            color: ColorSpec::new(),
+        }
+    }
+
+    pub fn into_string(self) -> String {
+        String::from_utf8(self.buf).unwrap()
+    }
+}
+
+impl io::Write for ColorBuffer {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.buf.extend(buf);
+        Ok(buf.len())
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}
+
+impl WriteColor for ColorBuffer {
+    fn supports_color(&self) -> bool {
+        true
+    }
+
+    fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> {
+        #![allow(unused_assignments)]
+
+        if self.color == *spec {
+            return Ok(());
+        } else {
+            self.color = spec.clone();
+        }
+
+        if spec.is_none() {
+            write!(self, "{{/}}")?;
+            return Ok(());
+        } else {
+            write!(self, "{{")?;
+        }
+
+        let mut first = true;
+
+        fn write_first(first: bool, write: &mut ColorBuffer) -> io::Result<bool> {
+            if !first {
+                write!(write, " ")?;
+            }
+
+            Ok(false)
+        };
+
+        if let Some(fg) = spec.fg() {
+            first = write_first(first, self)?;
+            write!(self, "fg:{:?}", fg)?;
+        }
+
+        if let Some(bg) = spec.bg() {
+            first = write_first(first, self)?;
+            write!(self, "bg:{:?}", bg)?;
+        }
+
+        if spec.bold() {
+            first = write_first(first, self)?;
+            write!(self, "bold")?;
+        }
+
+        if spec.underline() {
+            first = write_first(first, self)?;
+            write!(self, "underline")?;
+        }
+
+        if spec.intense() {
+            first = write_first(first, self)?;
+            write!(self, "bright")?;
+        }
+
+        write!(self, "}}")?;
+
+        Ok(())
+    }
+
+    fn reset(&mut self) -> io::Result<()> {
+        let color = self.color.clone();
+
+        if color != ColorSpec::new() {
+            write!(self, "{{/}}")?;
+            self.color = ColorSpec::new();
+        }
+
+        Ok(())
+    }
+}