Repository: casey/just
Pull request #415 (2019-04-19 08:47:13)

Clean up and rename test macros
Collapse `error_test!` and `compilation_error_test!` down to a single implementation, since they were basically the same.

Rename the two `summary_test!` macros to `parse_test!` and `lex_test!`, which is a bit clearer.

Add colored diff and pretty assertions in unit and integration tests.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index b8a1d6cb..db29a280 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -96,6 +96,25 @@ dependencies = [
  "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "colored-diff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 0.15.31 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "ctrlc"
 version = "3.1.1"
@@ -105,6 +124,11 @@ dependencies = [
  "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "difference"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "dotenv"
 version = "0.13.0"
@@ -180,6 +204,14 @@ dependencies = [
  "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "itertools"
 version = "0.8.0"
@@ -197,6 +229,7 @@ dependencies = [
  "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "brev 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "colored-diff 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "edit-distance 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -206,6 +239,7 @@ dependencies = [
  "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -246,6 +280,25 @@ dependencies = [
  "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "output_vt100"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "pretty_assertions"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ctor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "0.4.27"
@@ -499,7 +552,10 @@ dependencies = [
 "checksum cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)" = "5e5f3fee5eeb60324c2781f1e41286bdee933850fff9b3c672587fed5ec58c83"
 "checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"
 "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
+"checksum colored-diff 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fcbd90537dc19162289fbce7e95c882dded30f4ed41044cf1470612528d7163"
+"checksum ctor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3b4c17619643c1252b5f690084b82639dd7fac141c57c8e77a00e0148132092c"
 "checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e"
+"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
 "checksum dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d0a1279c96732bc6800ce6337b6a614697b0e74ae058dc03c62ebeb78b4d86"
 "checksum edit-distance 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b"
 "checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
@@ -510,12 +566,15 @@ dependencies = [
 "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
 "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
+"checksum itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0d47946d458e94a1b7bcabbf6521ea7c037062c81f534615abcad76e84d4970d"
 "checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358"
 "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
 "checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
 "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
 "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
 "checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17"
+"checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
+"checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
 "checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
 "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
 "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
diff --git a/Cargo.toml b/Cargo.toml
index 63114eb9..e8263d59 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,4 +30,6 @@ version  = "3.1"
 features = ["termination"]
 
 [dev-dependencies]
-executable-path = "1.0.0"
+executable-path   = "1.0.0"
+pretty_assertions = "0.6.1"
+colored-diff = "0.2.1"
diff --git a/src/assignment_evaluator.rs b/src/assignment_evaluator.rs
index 5b6a2049..70a5b5cc 100644
--- a/src/assignment_evaluator.rs
+++ b/src/assignment_evaluator.rs
@@ -165,7 +165,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
 #[cfg(test)]
 mod test {
   use super::*;
-  use crate::testing::parse_success;
+  use crate::testing::parse;
   use brev::OutputError;
 
   fn no_cwd_err() -> Result<PathBuf, String> {
@@ -174,7 +174,7 @@ mod test {
 
   #[test]
   fn backtick_code() {
-    match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
+    match parse("a:\n echo {{`f() { return 100; }; f`}}")
       .run(&no_cwd_err(), &["a"], &Default::default())
       .unwrap_err()
     {
@@ -203,7 +203,7 @@ recipe:
       ..Default::default()
     };
 
-    match parse_success(text)
+    match parse(text)
       .run(&no_cwd_err(), &["recipe"], &configuration)
       .unwrap_err()
     {
diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs
index d103fcda..ea10aab8 100644
--- a/src/assignment_resolver.rs
+++ b/src/assignment_resolver.rs
@@ -93,7 +93,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
 mod test {
   use super::*;
 
-  compilation_error_test! {
+  error_test! {
     name:   circular_variable_dependency,
     input:   "a = b\nb = a",
     offset:  0,
@@ -103,7 +103,7 @@ mod test {
     kind:   CircularVariableDependency{variable: "a", circle: vec!["a", "b", "a"]},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   self_variable_dependency,
     input:  "a = a",
     offset:  0,
@@ -113,7 +113,7 @@ mod test {
     kind:   CircularVariableDependency{variable: "a", circle: vec!["a", "a"]},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_expression_variable,
     input:  "x = yy",
     offset:  4,
@@ -123,7 +123,7 @@ mod test {
     kind:   UndefinedVariable{variable: "yy"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_function,
     input:  "a = foo()",
     offset:  4,
diff --git a/src/common.rs b/src/common.rs
index 52dd032b..6d434944 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -64,3 +64,6 @@ pub(crate) use crate::command_ext::CommandExt;
 
 #[allow(unused_imports)]
 pub(crate) use crate::range_ext::RangeExt;
+
+#[allow(unused_imports)]
+pub(crate) use crate::ordinal::Ordinal;
diff --git a/src/compilation_error.rs b/src/compilation_error.rs
index 1d62b2f3..576a4845 100644
--- a/src/compilation_error.rs
+++ b/src/compilation_error.rs
@@ -118,8 +118,8 @@ impl<'a> Display for CompilationError<'a> {
           f,
           "Alias `{}` defined on `{}` shadows recipe defined on `{}`",
           alias,
-          self.line + 1,
-          recipe_line + 1,
+          self.line.ordinal(),
+          recipe_line.ordinal(),
         )?;
       }
       CircularRecipeDependency { recipe, ref circle } => {
@@ -181,8 +181,8 @@ impl<'a> Display for CompilationError<'a> {
           f,
           "Alias `{}` first defined on line `{}` is redefined on line `{}`",
           alias,
-          first + 1,
-          self.line + 1,
+          first.ordinal(),
+          self.line.ordinal(),
         )?;
       }
       DuplicateDependency { recipe, dependency } => {
@@ -197,8 +197,8 @@ impl<'a> Display for CompilationError<'a> {
           f,
           "Recipe `{}` first defined on line {} is redefined on line {}",
           recipe,
-          first + 1,
-          self.line + 1
+          first.ordinal(),
+          self.line.ordinal()
         )?;
       }
       DependencyHasParameters { recipe, dependency } => {
diff --git a/src/justfile.rs b/src/justfile.rs
index f86bde36..8dddad74 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -205,7 +205,7 @@ mod test {
   use super::*;
 
   use crate::runtime_error::RuntimeError::*;
-  use crate::testing::parse_success;
+  use crate::testing::parse;
 
   fn no_cwd_err() -> Result<PathBuf, String> {
     Err(String::from("no cwd in tests"))
@@ -213,7 +213,7 @@ mod test {
 
   #[test]
   fn unknown_recipes() {
-    match parse_success("a:\nb:\nc:")
+    match parse("a:\nb:\nc:")
       .run(&no_cwd_err(), &["a", "x", "y", "z"], &Default::default())
       .unwrap_err()
     {
@@ -246,7 +246,7 @@ a:
       x
 ";
 
-    match parse_success(text)
+    match parse(text)
       .run(&no_cwd_err(), &["a"], &Default::default())
       .unwrap_err()
     {
@@ -265,7 +265,7 @@ a:
 
   #[test]
   fn code_error() {
-    match parse_success("fail:\n @exit 100")
+    match parse("fail:\n @exit 100")
       .run(&no_cwd_err(), &["fail"], &Default::default())
       .unwrap_err()
     {
@@ -288,7 +288,7 @@ a:
 a return code:
  @x() { {{return}} {{code + "0"}}; }; x"#;
 
-    match parse_success(text)
+    match parse(text)
       .run(&no_cwd_err(), &["a", "return", "15"], &Default::default())
       .unwrap_err()
     {
@@ -307,7 +307,7 @@ a return code:
 
   #[test]
   fn missing_some_arguments() {
-    match parse_success("a b c d:")
+    match parse("a b c d:")
       .run(&no_cwd_err(), &["a", "b", "c"], &Default::default())
       .unwrap_err()
     {
@@ -331,7 +331,7 @@ a return code:
 
   #[test]
   fn missing_some_arguments_variadic() {
-    match parse_success("a b c +d:")
+    match parse("a b c +d:")
       .run(&no_cwd_err(), &["a", "B", "C"], &Default::default())
       .unwrap_err()
     {
@@ -355,7 +355,7 @@ a return code:
 
   #[test]
   fn missing_all_arguments() {
-    match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
+    match parse("a b c d:\n echo {{b}}{{c}}{{d}}")
       .run(&no_cwd_err(), &["a"], &Default::default())
       .unwrap_err()
     {
@@ -379,7 +379,7 @@ a return code:
 
   #[test]
   fn missing_some_defaults() {
-    match parse_success("a b c d='hello':")
+    match parse("a b c d='hello':")
       .run(&no_cwd_err(), &["a", "b"], &Default::default())
       .unwrap_err()
     {
@@ -403,7 +403,7 @@ a return code:
 
   #[test]
   fn missing_all_defaults() {
-    match parse_success("a b c='r' d='h':")
+    match parse("a b c='r' d='h':")
       .run(&no_cwd_err(), &["a"], &Default::default())
       .unwrap_err()
     {
@@ -430,7 +430,7 @@ a return code:
     let mut configuration: Configuration = Default::default();
     configuration.overrides.insert("foo", "bar");
     configuration.overrides.insert("baz", "bob");
-    match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
+    match parse("a:\n echo {{`f() { return 100; }; f`}}")
       .run(&no_cwd_err(), &["a"], &configuration)
       .unwrap_err()
     {
@@ -458,7 +458,7 @@ wut:
       ..Default::default()
     };
 
-    match parse_success(text)
+    match parse(text)
       .run(&no_cwd_err(), &["wut"], &configuration)
       .unwrap_err()
     {
diff --git a/src/lexer.rs b/src/lexer.rs
index 04de1b74..1fb05b49 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -626,9 +626,40 @@ impl<'a> Lexer<'a> {
 mod tests {
   use super::*;
 
-  use crate::testing::token_summary;
-
-  macro_rules! summary_test {
+  fn summary(tokens: &[Token]) -> String {
+    use TokenKind::*;
+
+    tokens
+      .iter()
+      .map(|t| match t.kind {
+        At => "@",
+        Backtick => "`",
+        Colon => ":",
+        ColonEquals => ":=",
+        Comma => ",",
+        Comment => "#",
+        Dedent => "<",
+        Eof => ".",
+        Eol => "$",
+        Equals => "=",
+        Indent => ">",
+        InterpolationEnd => "}",
+        InterpolationStart => "{",
+        Line => "^",
+        Name => "N",
+        ParenL => "(",
+        ParenR => ")",
+        Plus => "+",
+        StringRaw => "'",
+        StringCooked => "\"",
+        Text => "_",
+        Whitespace => " ",
+      })
+      .collect::<Vec<&str>>()
+      .join("")
+  }
+
+  macro_rules! lex_test {
     ($name:ident, $input:expr, $expected:expr $(,)*) => {
       #[test]
       fn $name() {
@@ -640,116 +671,78 @@ mod tests {
           .map(Token::lexeme)
           .collect::<Vec<&str>>()
           .join("");
-        let actual = token_summary(&tokens);
-        if actual != expected {
-          panic!(
-            "token summary mismatch:\nexpected: {}\ngot:      {}\n",
-            expected, actual
-          );
-        }
-        assert_eq!(input, roundtrip);
-      }
-    };
-  }
+        let actual = summary(&tokens);
 
-  macro_rules! error_test {
-    (
-      name:     $name:ident,
-      input:    $input:expr,
-      offset:   $offset:expr,
-      line:     $line:expr,
-      column:   $column:expr,
-      width:    $width:expr,
-      kind:     $kind:expr,
-    ) => {
-      #[test]
-      fn $name() {
-        let input = $input;
+        use pretty_assertions::assert_eq;
 
-        let expected = CompilationError {
-          text: input,
-          offset: $offset,
-          line: $line,
-          column: $column,
-          width: $width,
-          kind: $kind,
-        };
-
-        if let Err(error) = Lexer::lex(input) {
-          assert_eq!(error.text, expected.text);
-          assert_eq!(error.offset, expected.offset);
-          assert_eq!(error.line, expected.line);
-          assert_eq!(error.column, expected.column);
-          assert_eq!(error.kind, expected.kind);
-          assert_eq!(error, expected);
-        } else {
-          panic!("tokenize succeeded but expected: {}\n{}", expected, input);
-        }
+        assert_eq!(actual, expected, "token summary mismatch");
+
+        assert_eq!(input, roundtrip, "token round-trip not equal");
       }
     };
   }
 
-  summary_test! {
+  lex_test! {
     name,
     "foo",
     "N.",
   }
 
-  summary_test! {
+  lex_test! {
     comment,
     "# hello",
     "#.",
   }
 
-  summary_test! {
+  lex_test! {
     backtick,
     "`echo`",
     "`.",
   }
 
-  summary_test! {
+  lex_test! {
     raw_string,
     "'hello'",
     "'.",
   }
 
-  summary_test! {
+  lex_test! {
     cooked_string,
     r#""hello""#,
     r#""."#,
   }
 
-  summary_test! {
+  lex_test! {
     export_concatination,
     "export foo = 'foo' + 'bar'",
     "N N = ' + '.",
   }
 
-  summary_test! {
+  lex_test! {
     export_complex,
     "export foo = ('foo' + 'bar') + `baz`",
     "N N = (' + ') + `.",
   }
 
-  summary_test! {
+  lex_test! {
     eol_linefeed,
     "\n",
     "$.",
   }
 
-  summary_test! {
+  lex_test! {
     eol_carriage_return_linefeed,
     "\r\n",
     "$.",
   }
 
-  summary_test! {
+  lex_test! {
     indented_line,
     "foo:\n a",
     "N:$>^_<.",
   }
 
-  summary_test! {
+  lex_test! {
     indented_block,
     r##"foo:
   a
@@ -759,7 +752,7 @@ mod tests {
     "N:$>^_$ ^_$ ^_$<.",
   }
 
-  summary_test! {
+  lex_test! {
     indented_block_followed_by_item,
     "foo:
   a
@@ -767,7 +760,7 @@ b:",
     "N:$>^_$<N:.",
   }
 
-  summary_test! {
+  lex_test! {
     indented_block_followed_by_blank,
     "foo:
     a
@@ -776,13 +769,13 @@ b:",
       "N:$>^_$^$<N:.",
   }
 
-  summary_test! {
+  lex_test! {
     indented_line_containing_unpaired_carriage_return,
     "foo:\n \r \n",
     "N:$>^_$<.",
   }
 
-  summary_test! {
+  lex_test! {
     indented_blocks,
     "
 b: a
@@ -800,19 +793,19 @@ c: b
     "$N: N$>^_$^$<N:$>^_$ ^_$^$<N: N$>^_$^$<N: N$>^_<.",
   }
 
-  summary_test! {
+  lex_test! {
     interpolation_empty,
     "hello:\n echo {{}}",
     "N:$>^_{}<.",
   }
 
-  summary_test! {
+  lex_test! {
     interpolation_expression,
     "hello:\n echo {{`echo hello` + `echo goodbye`}}",
     "N:$>^_{` + `}<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_names,
     "\
 foo
@@ -822,13 +815,13 @@ test123",
     "N$N$N$N.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_indented_line,
     "foo:\n a",
     "N:$>^_<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_indented_block,
     r##"foo:
   a
@@ -838,13 +831,13 @@ test123",
     "N:$>^_$ ^_$ ^_$<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_strings,
     r#"a = "'a'" + '"b"' + "'c'" + '"d"'#echo hello"#,
     r#"N = " + ' + " + '#."#,
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_recipe_interpolation_eol,
     "foo: # some comment
  {{hello}}
@@ -852,7 +845,7 @@ test123",
     "N: #$>^{N}$<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_recipe_interpolation_eof,
     "foo: # more comments
  {{hello}}
@@ -861,19 +854,19 @@ test123",
     "N: #$>^{N}$<#$.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_recipe_complex_interpolation_expression,
     "foo: #lol\n {{a + b + \"z\" + blarg}}",
     "N: #$>^{N + N + \" + N}<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_recipe_multiple_interpolations,
     "foo:,#ok\n {{a}}0{{b}}1{{c}}",
     "N:,#$>^{N}_{N}_{N}<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_junk,
     "bob
 
@@ -882,7 +875,7 @@ hello blah blah blah : a b c #whatever
     "N$$N N N N : N N N #$ .",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_empty_lines,
     "
 # this does something
@@ -899,7 +892,7 @@ hello:
     "$#$N:$>^_$ ^_$^$ ^_$^$ ^_$^$<#$ .",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_comment_before_variable,
     "
 #
@@ -910,25 +903,25 @@ echo:
     "$#$N='$N:$>^_{N}$ <.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_interpolation_backticks,
     "hello:\n echo {{`echo hello` + `echo goodbye`}}",
     "N:$>^_{` + `}<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_empty_interpolation,
     "hello:\n echo {{}}",
     "N:$>^_{}<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_assignment_backticks,
     "a = `echo hello` + `echo goodbye`",
     "N = ` + `.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_multiple,
     "
 hello:
@@ -947,19 +940,19 @@ bob:
     "$N:$>^_$ ^_$^$ ^_$^$ ^_$^$<#$N:$>^_$ <.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_comment,
     "a:=#",
     "N:=#."
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_comment_with_bang,
     "a:=#foo!",
     "N:=#."
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_order,
     r"
 b: a
@@ -977,19 +970,19 @@ c: b
     "$N: N$>^_$^$<N:$>^_$ ^_$^$<N: N$>^_$^$<N: N$>^_<.",
   }
 
-  summary_test! {
+  lex_test! {
     tokenize_parens,
     r"((())) )abc(+",
     "((())) )N(+.",
   }
 
-  summary_test! {
+  lex_test! {
     crlf_newline,
     "#\r\n#asdf\r\n",
     "#$#$.",
   }
 
-  summary_test! {
+  lex_test! {
     multiple_recipes,
     "a:\n  foo\nb:",
     "N:$>^_$<N:.",
diff --git a/src/lib.rs b/src/lib.rs
index 12afb095..c93b53ab 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -31,6 +31,7 @@ mod justfile;
 mod lexer;
 mod load_dotenv;
 mod misc;
+mod ordinal;
 mod parameter;
 mod parser;
 mod platform;
diff --git a/src/misc.rs b/src/misc.rs
index dcad3d9b..50f4efcc 100644
--- a/src/misc.rs
+++ b/src/misc.rs
@@ -65,7 +65,7 @@ pub fn write_error_context(
 ) -> Result<(), fmt::Error> {
   let width = if width == 0 { 1 } else { width };
 
-  let line_number = line + 1;
+  let line_number = line.ordinal();
   let red = Color::fmt(f).error();
   match text.lines().nth(line) {
     Some(line) => {
diff --git a/src/ordinal.rs b/src/ordinal.rs
new file mode 100644
index 00000000..7c53dbdb
--- /dev/null
+++ b/src/ordinal.rs
@@ -0,0 +1,10 @@
+pub trait Ordinal {
+  /// Convert an index starting at 0 to an ordinal starting at 1
+  fn ordinal(self) -> Self;
+}
+
+impl Ordinal for usize {
+  fn ordinal(self) -> Self {
+    self + 1
+  }
+}
diff --git a/src/parser.rs b/src/parser.rs
index 0b6e7b68..b45d5ebf 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -523,34 +523,27 @@ impl<'a> Parser<'a> {
 #[cfg(test)]
 mod test {
   use super::*;
-  use crate::testing::parse_success;
+  use crate::testing::parse;
 
-  macro_rules! summary_test {
+  macro_rules! parse_test {
     ($name:ident, $input:expr, $expected:expr $(,)*) => {
       #[test]
       fn $name() {
         let input = $input;
         let expected = $expected;
-        let justfile = parse_success(input);
+        let justfile = parse(input);
         let actual = format!("{:#}", justfile);
-        if actual != expected {
-          println!("got:\n\"{}\"\n", actual);
-          println!("expected:\n\"{}\"", expected);
-          assert_eq!(actual, expected);
-        }
+        use pretty_assertions::assert_eq;
+        assert_eq!(actual, expected);
         println!("Re-parsing...");
-        let reparsed = parse_success(&actual);
+        let reparsed = parse(&actual);
         let redumped = format!("{:#}", reparsed);
-        if redumped != actual {
-          println!("reparsed:\n\"{}\"\n", redumped);
-          println!("expected:\n\"{}\"", actual);
-          assert_eq!(redumped, actual);
-        }
+        assert_eq!(redumped, actual);
       }
     };
   }
 
-  summary_test! {
+  parse_test! {
     parse_empty,
     "
 
@@ -561,7 +554,7 @@ mod test {
     "",
   }
 
-  summary_test! {
+  parse_test! {
     parse_string_default,
     r#"
 
@@ -572,7 +565,7 @@ foo a="b\t":
     r#"foo a="b\t":"#,
   }
 
-  summary_test! {
+  parse_test! {
   parse_multiple,
     r#"
 a:
@@ -583,7 +576,7 @@ b:
 b:"#,
   }
 
-  summary_test! {
+  parse_test! {
     parse_variadic,
     r#"
 
@@ -594,7 +587,7 @@ foo +a:
     r#"foo +a:"#,
   }
 
-  summary_test! {
+  parse_test! {
     parse_variadic_string_default,
     r#"
 
@@ -605,7 +598,7 @@ foo +a="Hello":
     r#"foo +a="Hello":"#,
   }
 
-  summary_test! {
+  parse_test! {
     parse_raw_string_default,
     r#"
 
@@ -616,7 +609,7 @@ foo a='b\t':
     r#"foo a='b\t':"#,
   }
 
-  summary_test! {
+  parse_test! {
     parse_export,
     r#"
 export a := "hello"
@@ -625,7 +618,7 @@ export a := "hello"
     r#"export a := "hello""#,
   }
 
-  summary_test! {
+  parse_test! {
   parse_alias_after_target,
     r#"
 foo:
@@ -638,7 +631,7 @@ foo:
     echo a"#
   }
 
-  summary_test! {
+  parse_test! {
   parse_alias_before_target,
     r#"
 alias f := foo
@@ -651,7 +644,7 @@ foo:
     echo a"#
   }
 
-  summary_test! {
+  parse_test! {
   parse_alias_with_comment,
     r#"
 alias f := foo #comment
@@ -664,7 +657,7 @@ foo:
     echo a"#
   }
 
-  summary_test! {
+  parse_test! {
   parse_complex,
     "
 x:
@@ -702,7 +695,7 @@ y:
 z:"
   }
 
-  summary_test! {
+  parse_test! {
   parse_shebang,
     "
 practicum := 'hello'
@@ -721,13 +714,13 @@ install:
     fi",
   }
 
-  summary_test! {
+  parse_test! {
     parse_simple_shebang,
     "a:\n #!\n  print(1)",
     "a:\n    #!\n     print(1)",
   }
 
-  summary_test! {
+  parse_test! {
   parse_assignments,
     r#"a := "0"
 c := a + b + a + b
@@ -740,7 +733,7 @@ b := "1"
 c := a + b + a + b"#,
   }
 
-  summary_test! {
+  parse_test! {
   parse_assignment_backticks,
     "a := `echo hello`
 c := a + b + a + b
@@ -752,7 +745,7 @@ b := `echo goodbye`
 c := a + b + a + b",
   }
 
-  summary_test! {
+  parse_test! {
   parse_interpolation_backticks,
     r#"a:
   echo {{  `echo hello` + "blarg"   }} {{   `echo bob`   }}"#,
@@ -760,25 +753,25 @@ c := a + b + a + b",
     echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#,
   }
 
-  summary_test! {
+  parse_test! {
     eof_test,
     "x:\ny:\nz:\na b c: x y z",
     "a b c: x y z\n\nx:\n\ny:\n\nz:",
   }
 
-  summary_test! {
+  parse_test! {
     string_quote_escape,
     r#"a := "hello\"""#,
     r#"a := "hello\"""#,
   }
 
-  summary_test! {
+  parse_test! {
     string_escapes,
     r#"a := "\n\t\r\"\\""#,
     r#"a := "\n\t\r\"\\""#,
   }
 
-  summary_test! {
+  parse_test! {
   parameters,
     "a b c:
   {{b}} {{c}}",
@@ -786,7 +779,7 @@ c := a + b + a + b",
     {{b}} {{c}}",
   }
 
-  summary_test! {
+  parse_test! {
   unary_functions,
     "
 x := arch()
@@ -799,7 +792,7 @@ a:
     {{os()}} {{os_family()}}",
   }
 
-  summary_test! {
+  parse_test! {
   env_functions,
     r#"
 x := env_var('foo',)
@@ -812,7 +805,7 @@ a:
     {{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
   }
 
-  summary_test! {
+  parse_test! {
     parameter_default_string,
     r#"
 f x="abc":
@@ -820,7 +813,7 @@ f x="abc":
     r#"f x="abc":"#,
   }
 
-  summary_test! {
+  parse_test! {
     parameter_default_raw_string,
     r#"
 f x='abc':
@@ -828,7 +821,7 @@ f x='abc':
     r#"f x='abc':"#,
   }
 
-  summary_test! {
+  parse_test! {
     parameter_default_backtick,
     r#"
 f x=`echo hello`:
@@ -836,7 +829,7 @@ f x=`echo hello`:
     r#"f x=`echo hello`:"#,
   }
 
-  summary_test! {
+  parse_test! {
     parameter_default_concatination_string,
     r#"
 f x=(`echo hello` + "foo"):
@@ -844,7 +837,7 @@ f x=(`echo hello` + "foo"):
     r#"f x=(`echo hello` + "foo"):"#,
   }
 
-  summary_test! {
+  parse_test! {
     parameter_default_concatination_variable,
     r#"
 x := "10"
@@ -855,7 +848,7 @@ f y=(`echo hello` + x) +z="foo":
 f y=(`echo hello` + x) +z="foo":"#,
   }
 
-  summary_test! {
+  parse_test! {
     parameter_default_multiple,
     r#"
 x := "10"
@@ -866,20 +859,20 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):
 f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
   }
 
-  summary_test! {
+  parse_test! {
     concatination_in_group,
     "x := ('0' + '1')",
     "x := ('0' + '1')",
   }
 
-  summary_test! {
+  parse_test! {
     string_in_group,
     "x := ('0'   )",
     "x := ('0')",
   }
 
   #[rustfmt::skip]
-  summary_test! {
+  parse_test! {
     escaped_dos_newlines,
     "@spam:\r
 \t{ \\\r
@@ -896,7 +889,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     } | less",
   }
 
-  compilation_error_test! {
+  error_test! {
     name: duplicate_alias,
     input: "alias foo = bar\nalias foo = baz",
     offset: 22,
@@ -906,7 +899,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind: DuplicateAlias { alias: "foo", first: 0 },
   }
 
-  compilation_error_test! {
+  error_test! {
     name: alias_syntax_multiple_rhs,
     input: "alias foo = bar baz",
     offset: 16,
@@ -916,7 +909,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind: UnexpectedToken { expected: vec![Eol, Eof], found: Name },
   }
 
-  compilation_error_test! {
+  error_test! {
     name: alias_syntax_no_rhs,
     input: "alias foo = \n",
     offset: 12,
@@ -926,7 +919,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind: UnexpectedToken {expected: vec![Name], found:Eol},
   }
 
-  compilation_error_test! {
+  error_test! {
     name: unknown_alias_target,
     input: "alias foo = bar\n",
     offset: 6,
@@ -936,7 +929,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind: UnknownAliasTarget {alias: "foo", target: "bar"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name: alias_shadows_recipe_before,
     input: "bar: \n  echo bar\nalias foo = bar\nfoo:\n  echo foo",
     offset: 23,
@@ -946,7 +939,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind: AliasShadowsRecipe {alias: "foo", recipe_line: 3},
   }
 
-  compilation_error_test! {
+  error_test! {
     name: alias_shadows_recipe_after,
     input: "foo:\n  echo foo\nalias foo = bar\nbar:\n  echo bar",
     offset: 22,
@@ -956,7 +949,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind: AliasShadowsRecipe { alias: "foo", recipe_line: 0 },
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   missing_colon,
     input:  "a b c\nd e f",
     offset:  5,
@@ -966,7 +959,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eol},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   missing_default_eol,
     input:  "hello arg=\n",
     offset:  10,
@@ -976,7 +969,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name, StringCooked], found: Eol},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   missing_default_eof,
     input:  "hello arg=",
     offset:  10,
@@ -986,7 +979,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name, StringCooked], found: Eof},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   parameter_after_variadic,
     input:  "foo +a bbb:",
     offset:  7,
@@ -996,7 +989,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   ParameterFollowsVariadicParameter{parameter: "bbb"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   required_after_default,
     input:  "hello arg='foo' bar:",
     offset:  16,
@@ -1006,7 +999,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   RequiredParameterFollowsDefaultParameter{parameter: "bar"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   missing_eol,
     input:  "a b c: z =",
     offset:  9,
@@ -1016,7 +1009,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name, Eol, Eof], found: Equals},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   duplicate_parameter,
     input:  "a b b:",
     offset:  4,
@@ -1026,7 +1019,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   DuplicateParameter{recipe: "a", parameter: "b"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   parameter_shadows_varible,
     input:  "foo = \"h\"\na foo:",
     offset:  12,
@@ -1036,7 +1029,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   ParameterShadowsVariable{parameter: "foo"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   dependency_has_parameters,
     input:  "foo arg:\nb: foo",
     offset:  12,
@@ -1046,7 +1039,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   DependencyHasParameters{recipe: "b", dependency: "foo"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   duplicate_dependency,
     input:  "a b c: b c z z",
     offset:  13,
@@ -1056,7 +1049,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   DuplicateDependency{recipe: "a", dependency: "z"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   duplicate_recipe,
     input:  "a:\nb:\na:",
     offset:  6,
@@ -1066,7 +1059,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   DuplicateRecipe{recipe: "a", first: 0},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   duplicate_variable,
     input:  "a = \"0\"\na = \"0\"",
     offset:  8,
@@ -1076,7 +1069,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   DuplicateVariable{variable: "a"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   extra_whitespace,
     input:  "a:\n blah\n  blarg",
     offset:  10,
@@ -1086,7 +1079,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   ExtraLeadingWhitespace,
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   interpolation_outside_of_recipe,
     input:  "{{",
     offset:  0,
@@ -1096,7 +1089,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name, At], found: InterpolationStart},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unclosed_parenthesis_in_expression,
     input:  "x = foo(",
     offset:  8,
@@ -1106,7 +1099,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name, StringCooked, ParenR], found: Eof},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unclosed_parenthesis_in_interpolation,
     input:  "a:\n echo {{foo(}}",
     offset:  15,
@@ -1116,7 +1109,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name, StringCooked, ParenR], found: InterpolationEnd},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   plus_following_parameter,
     input:  "a b c+:",
     offset:  5,
@@ -1126,7 +1119,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     kind:   UnexpectedToken{expected: vec![Name], found: Plus},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   bad_export,
     input:  "export a",
     offset:  8,
@@ -1157,14 +1150,14 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
     }
 
     for justfile in justfiles {
-      parse_success(&justfile);
+      parse(&justfile);
     }
   }
 
   #[test]
   fn empty_recipe_lines() {
     let text = "a:";
-    let justfile = parse_success(&text);
+    let justfile = parse(&text);
 
     assert_eq!(justfile.recipes["a"].lines.len(), 0);
   }
@@ -1172,7 +1165,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
   #[test]
   fn simple_recipe_lines() {
     let text = "a:\n foo";
-    let justfile = parse_success(&text);
+    let justfile = parse(&text);
 
     assert_eq!(justfile.recipes["a"].lines.len(), 1);
   }
@@ -1185,7 +1178,7 @@ f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
 b:
 ";
 
-    let justfile = parse_success(&text);
+    let justfile = parse(&text);
 
     assert_eq!(justfile.recipes["a"].lines.len(), 1);
   }
diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs
index 5eafb47e..7b9b6d11 100644
--- a/src/recipe_resolver.rs
+++ b/src/recipe_resolver.rs
@@ -154,7 +154,7 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
 mod test {
   use super::*;
 
-  compilation_error_test! {
+  error_test! {
     name:   circular_recipe_dependency,
     input:  "a: b\nb: a",
     offset: 8,
@@ -164,7 +164,7 @@ mod test {
     kind:   CircularRecipeDependency{recipe: "b", circle: vec!["a", "b", "a"]},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   self_recipe_dependency,
     input:  "a: a",
     offset: 3,
@@ -174,7 +174,7 @@ mod test {
     kind:   CircularRecipeDependency{recipe: "a", circle: vec!["a", "a"]},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_dependency,
     input:  "a: b",
     offset: 3,
@@ -184,7 +184,7 @@ mod test {
     kind:   UnknownDependency{recipe: "a", unknown: "b"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_interpolation_variable,
     input:  "x:\n {{   hello}}",
     offset: 9,
@@ -194,7 +194,7 @@ mod test {
     kind:   UndefinedVariable{variable: "hello"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_second_interpolation_variable,
     input:  "wtf=\"x\"\nx:\n echo\n foo {{wtf}} {{ lol }}",
     offset: 33,
@@ -204,7 +204,7 @@ mod test {
     kind:   UndefinedVariable{variable: "lol"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_function_in_interpolation,
     input:  "a:\n echo {{bar()}}",
     offset: 11,
@@ -214,7 +214,7 @@ mod test {
     kind:   UnknownFunction{function: "bar"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_function_in_default,
     input:  "a f=baz():",
     offset: 4,
@@ -224,7 +224,7 @@ mod test {
     kind:   UnknownFunction{function: "baz"},
   }
 
-  compilation_error_test! {
+  error_test! {
     name:   unknown_variable_in_default,
     input:  "a f=foo:",
     offset: 4,


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index 773e2ee2..e2b675ce 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -1,46 +1,13 @@
 use crate::common::*;
 
-pub fn parse_success(text: &str) -> Justfile {
+pub fn parse(text: &str) -> Justfile {
   match Parser::parse(text) {
     Ok(justfile) => justfile,
-    Err(error) => panic!("Expected successful parse but got error:\n{}", error),
+    Err(error) => panic!("Expected successful parse but got error:\n {}", error),
   }
 }
 
-pub fn token_summary(tokens: &[Token]) -> String {
-  use TokenKind::*;
-
-  tokens
-    .iter()
-    .map(|t| match t.kind {
-      At => "@",
-      Backtick => "`",
-      Colon => ":",
-      ColonEquals => ":=",
-      Comma => ",",
-      Comment => "#",
-      Dedent => "<",
-      Eof => ".",
-      Eol => "$",
-      Equals => "=",
-      Indent => ">",
-      InterpolationEnd => "}",
-      InterpolationStart => "{",
-      Line => "^",
-      Name => "N",
-      ParenL => "(",
-      ParenR => ")",
-      Plus => "+",
-      StringRaw => "'",
-      StringCooked => "\"",
-      Text => "_",
-      Whitespace => " ",
-    })
-    .collect::<Vec<&str>>()
-    .join("")
-}
-
-macro_rules! compilation_error_test {
+macro_rules! error_test {
   (
     name:   $name:ident,
     input:  $input:expr,
@@ -52,33 +19,28 @@ macro_rules! compilation_error_test {
   ) => {
     #[test]
     fn $name() {
-      let input = $input;
-
-      let expected = crate::compilation_error::CompilationError {
-        text: input,
-        offset: $offset,
-        line: $line,
-        column: $column,
-        width: $width,
-        kind: $kind,
+      let text: &str = $input;
+      let offset: usize = $offset;
+      let column: usize = $column;
+      let width: usize = $width;
+      let line: usize = $line;
+      let kind: CompilationErrorKind = $kind;
+
+      let expected = CompilationError {
+        text,
+        offset,
+        line,
+        column,
+        width,
+        kind,
       };
 
-      let mut tokens = Lexer::lex(input).unwrap();
-
-      tokens.retain(|token| token.kind != TokenKind::Whitespace);
-
-      let parser = crate::parser::Parser::new(input, tokens);
-
-      if let Err(error) = parser.justfile() {
-        assert_eq!(error.text, expected.text);
-        assert_eq!(error.offset, expected.offset);
-        assert_eq!(error.line, expected.line);
-        assert_eq!(error.column, expected.column);
-        assert_eq!(error.width, expected.width);
-        assert_eq!(error.kind, expected.kind);
-        assert_eq!(error, expected);
-      } else {
-        panic!("parse succeeded but expected: {}\n{}", expected, input);
+      match Parser::parse(text) {
+        Ok(_) => panic!("Compilation succeeded but expected: {}\n{}", expected, text),
+        Err(actual) => {
+          use pretty_assertions::assert_eq;
+          assert_eq!(actual, expected);
+        }
       }
     }
   };
diff --git a/tests/integration.rs b/tests/integration.rs
index 8d152682..07b54ad4 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -1,3 +1,4 @@
+use colored_diff::PrettyDifference;
 use executable_path::executable_path;
 use libc::{EXIT_FAILURE, EXIT_SUCCESS};
 use std::{
@@ -93,10 +94,14 @@ fn integration_test(
   }
 
   let stdout = str::from_utf8(&output.stdout).unwrap();
+
   if stdout != expected_stdout {
     println!(
-      "bad stdout:\ngot:\n{}\n\nexpected:\n{}",
-      stdout, expected_stdout
+      "bad stdout:\n {}",
+      PrettyDifference {
+        expected: expected_stdout,
+        actual: stdout
+      },
     );
     failure = true;
   }
@@ -104,8 +109,11 @@ fn integration_test(
   let stderr = str::from_utf8(&output.stderr).unwrap();
   if stderr != expected_stderr {
     println!(
-      "bad stderr:\ngot:\n{}\n\nexpected:\n{}",
-      stderr, expected_stderr
+      "bad stderr: {}",
+      PrettyDifference {
+        expected: expected_stderr,
+        actual: stderr
+      },
     );
     failure = true;
   }
@@ -148,9 +156,13 @@ fn integration_test(
     let reparsed = String::from_utf8(output.stdout).unwrap();
 
     if reparsed != dumped {
-      print!("expected:\n{}", reparsed);
-      print!("got:\n{}", dumped);
-      assert_eq!(reparsed, dumped);
+      println!(
+        "reparse mismatch:\n {}",
+        PrettyDifference {
+          expected: &dumped,
+          actual: &reparsed
+        },
+      );
     }
   }
 }