just 1.8.0

🤖 Just a command runner
Documentation
use super::*;

test! {
  name: alias_listing,
  justfile: "
    foo:
      echo foo

    alias f := foo
  ",
  args: ("--list"),
  stdout: "
    Available recipes:
        foo
        f   # alias for `foo`
  ",
}

test! {
  name: alias_listing_multiple_aliases,
  justfile: "foo:\n  echo foo\nalias f := foo\nalias fo := foo",
  args: ("--list"),
  stdout: "
    Available recipes:
        foo
        f   # alias for `foo`
        fo  # alias for `foo`
  ",
}

test! {
  name: alias_listing_parameters,
  justfile: "foo PARAM='foo':\n  echo {{PARAM}}\nalias f := foo",
  args: ("--list"),
  stdout: "
    Available recipes:
        foo PARAM='foo'
        f PARAM='foo'   # alias for `foo`
  ",
}

test! {
  name: alias_listing_private,
  justfile: "foo PARAM='foo':\n  echo {{PARAM}}\nalias _f := foo",
  args: ("--list"),
  stdout: "
    Available recipes:
        foo PARAM='foo'
  ",
}

test! {
  name: alias,
  justfile: "foo:\n  echo foo\nalias f := foo",
  args: ("f"),
  stdout: "foo\n",
  stderr: "echo foo\n",
}

test! {
  name: alias_with_parameters,
  justfile: "foo value='foo':\n  echo {{value}}\nalias f := foo",
  args: ("f", "bar"),
  stdout: "bar\n",
  stderr: "echo bar\n",
}

test! {
  name: bad_setting,
  justfile: "
    set foo
  ",
  stderr: "
  error: Expected ':=', but found end of line
    |
  1 | set foo
    |        ^
  ",
  status: EXIT_FAILURE,
}

test! {
  name: alias_with_dependencies,
  justfile: "foo:\n  echo foo\nbar: foo\nalias b := bar",
  args: ("b"),
  stdout: "foo\n",
  stderr: "echo foo\n",
}

test! {
  name: duplicate_alias,
  justfile: "alias foo := bar\nalias foo := baz\n",
  stderr: "
    error: Alias `foo` first defined on line 1 is redefined on line 2
      |
    2 | alias foo := baz
      |       ^^^
  ",
  status: EXIT_FAILURE,
}

test! {
  name: unknown_alias_target,
  justfile: "alias foo := bar\n",
  stderr: "
    error: Alias `foo` has an unknown target `bar`
      |
    1 | alias foo := bar
      |       ^^^
  ",
  status: EXIT_FAILURE,
}

test! {
  name: alias_shadows_recipe,
  justfile: "bar:\n  echo bar\nalias foo := bar\nfoo:\n  echo foo",
  stderr: "
    error: Alias `foo` defined on line 3 shadows recipe `foo` defined on line 4
      |
    3 | alias foo := bar
      |       ^^^
  ",
  status: EXIT_FAILURE,
}

test! {
  name:     default,
  justfile: "default:\n echo hello\nother: \n echo bar",
  stdout:   "hello\n",
  stderr:   "echo hello\n",
}

test! {
  name:     quiet,
  justfile: "default:\n @echo hello",
  stdout:   "hello\n",
}

test! {
  name:     verbose,
  justfile: "default:\n @echo hello",
  args:     ("--verbose"),
  stdout:   "hello\n",
  stderr:   "===> Running recipe `default`...\necho hello\n",
}

test! {
  name:     order,
  justfile: "
b: a
  echo b
  @mv a b

a:
  echo a
  @touch F
  @touch a

d: c
  echo d
  @rm c

c: b
  echo c
  @mv b c",
  args:     ("a", "d"),
  stdout:   "a\nb\nc\nd\n",
  stderr:   "echo a\necho b\necho c\necho d\n",
}

test! {
  name:     summary,
  justfile: "b: a
a:
d: c
c: b
_z: _y
_y:
",
  args:     ("--summary"),
  stdout:   "a b c d\n",
}

test! {
  name:     summary_sorted,
  justfile: "
b:
c:
a:
",
  args:     ("--summary"),
  stdout:   "a b c\n",
}

test! {
  name:     summary_unsorted,
  justfile: "
b:
c:
a:
",
  args:     ("--summary", "--unsorted"),
  stdout:   "b c a\n",
}

test! {
  name:     select,
  justfile: "b:
  @echo b
a:
  @echo a
d:
  @echo d
c:
  @echo c",
  args:     ("d", "c"),
  stdout:   "d\nc\n",
}

test! {
  name:     print,
  justfile: "b:
  echo b
a:
  echo a
d:
  echo d
c:
  echo c",
  args:     ("d", "c"),
  stdout:   "d\nc\n",
  stderr:   "echo d\necho c\n",
}

test! {
  name:     status_passthrough,
  justfile: "

hello:

recipe:
  @exit 100",
  args:     ("recipe"),
  stderr:   "error: Recipe `recipe` failed on line 5 with exit code 100\n",
  status:   100,
}

test! {
  name:     unknown_dependency,
  justfile: "bar:\nhello:\nfoo: bar baaaaaaaz hello",
  stderr:   "
    error: Recipe `foo` has unknown dependency `baaaaaaaz`
      |
    3 | foo: bar baaaaaaaz hello
      |          ^^^^^^^^^
  ",
  status:   EXIT_FAILURE,
}

test! {
  name:     backtick_success,
  justfile: "a := `printf Hello,`\nbar:\n printf '{{a + `printf ' world.'`}}'",
  stdout:   "Hello, world.",
  stderr:   "printf 'Hello, world.'\n",
}

test! {
  name:     backtick_trimming,
  justfile: "a := `echo Hello,`\nbar:\n echo '{{a + `echo ' world.'`}}'",
  stdout:   "Hello, world.\n",
  stderr:   "echo 'Hello, world.'\n",
}

test! {
  name:     backtick_code_assignment,
  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
  stderr:   "
    error: Backtick failed with exit code 100
      |
    2 | a := `exit 100`
      |      ^^^^^^^^^^
  ",
  status:   100,
}

test! {
  name:     backtick_code_interpolation,
  justfile: "b := a\na := `echo hello`\nbar:\n echo '{{`exit 200`}}'",
  stderr:   "
    error: Backtick failed with exit code 200
      |
    4 |  echo '{{`exit 200`}}'
      |          ^^^^^^^^^^
  ",
  status:   200,
}

test! {
  name:     backtick_code_interpolation_mod,
  justfile: "f:\n 無{{`exit 200`}}",
  stderr:   "
    error: Backtick failed with exit code 200
      |
    2 |  無{{`exit 200`}}
      |      ^^^^^^^^^^
  ",
  status:   200,
}

test! {
  name:     backtick_code_interpolation_tab,
  justfile: "
    backtick-fail:
    \techo {{`exit 200`}}
  ",
  stderr:   "    error: Backtick failed with exit code 200
      |
    2 |     echo {{`exit 200`}}
      |            ^^^^^^^^^^
  ",
  status:   200,
}

test! {
  name:     backtick_code_interpolation_tabs,
  justfile: "
    backtick-fail:
    \techo {{\t`exit 200`}}
  ",
  stderr:   "error: Backtick failed with exit code 200
  |
2 |     echo {{    `exit 200`}}
  |                ^^^^^^^^^^
",
  status:   200,
}

test! {
  name:     backtick_code_interpolation_inner_tab,
  justfile: "
    backtick-fail:
    \techo {{\t`exit\t\t200`}}
  ",
  stderr:   "
    error: Backtick failed with exit code 200
      |
    2 |     echo {{    `exit        200`}}
      |                ^^^^^^^^^^^^^^^^^
  ",
  status:   200,
}

test! {
  name:     backtick_code_interpolation_leading_emoji,
  justfile: "
    backtick-fail:
    \techo 😬{{`exit 200`}}
  ",
  stderr: "
    error: Backtick failed with exit code 200
      |
    2 |     echo 😬{{`exit 200`}}
      |              ^^^^^^^^^^
  ",
  status:   200,
}

test! {
  name:     backtick_code_interpolation_unicode_hell,
  justfile: "
    backtick-fail:
    \techo \t\t\t😬鎌鼬{{\t\t`exit 200 # \t\t\tabc`}}\t\t\t😬鎌鼬
  ",
  stderr: "
    error: Backtick failed with exit code 200
      |
    2 |     echo             😬鎌鼬{{        `exit 200 #             abc`}}            😬鎌鼬
      |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  ",
  status:   200,
}

test! {
  name:     backtick_code_long,
  justfile: "






    b := a
    a := `echo hello`
    bar:
     echo '{{`exit 200`}}'
  ",
  stderr:   "
    error: Backtick failed with exit code 200
       |
    10 |  echo '{{`exit 200`}}'
       |          ^^^^^^^^^^
  ",
  status:   200,
}

test! {
  name:     shebang_backtick_failure,
  justfile: "foo:
 #!/bin/sh
 echo hello
 echo {{`exit 123`}}",
  stdout:   "",
  stderr:   "
    error: Backtick failed with exit code 123
      |
    4 |  echo {{`exit 123`}}
      |         ^^^^^^^^^^
  ",
  status:   123,
}

test! {
  name:     command_backtick_failure,
  justfile: "foo:
 echo hello
 echo {{`exit 123`}}",
  stdout:   "hello\n",
  stderr:   "
    echo hello
    error: Backtick failed with exit code 123
      |
    3 |  echo {{`exit 123`}}
      |         ^^^^^^^^^^
  ",
  status:   123,
}

test! {
  name:     assignment_backtick_failure,
  justfile: "foo:
 echo hello
 echo {{`exit 111`}}
a := `exit 222`",
  stdout:   "",
  stderr:   "
    error: Backtick failed with exit code 222
      |
    4 | a := `exit 222`
      |      ^^^^^^^^^^
  ",
  status:   222,
}

test! {
  name:     unknown_override_options,
  justfile: "foo:
 echo hello
 echo {{`exit 111`}}
a := `exit 222`",
  args:     ("--set", "foo", "bar", "--set", "baz", "bob", "--set", "a", "b", "a", "b"),
  stderr:   "error: Variables `baz` and `foo` overridden on the command line but not present \
    in justfile\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     unknown_override_args,
  justfile: "foo:
 echo hello
 echo {{`exit 111`}}
a := `exit 222`",
  args:     ("foo=bar", "baz=bob", "a=b", "a", "b"),
  stderr:   "error: Variables `baz` and `foo` overridden on the command line but not present \
    in justfile\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     unknown_override_arg,
  justfile: "foo:
 echo hello
 echo {{`exit 111`}}
a := `exit 222`",
  args:     ("foo=bar", "a=b", "a", "b"),
  stderr:   "error: Variable `foo` overridden on the command line but not present in justfile\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     overrides_first,
  justfile: r#"
foo := "foo"
a := "a"
baz := "baz"

recipe arg:
 echo arg={{arg}}
 echo {{foo + a + baz}}"#,
  args:     ("foo=bar", "a=b", "recipe", "baz=bar"),
  stdout:   "arg=baz=bar\nbarbbaz\n",
  stderr:   "echo arg=baz=bar\necho barbbaz\n",
}

test! {
  name:     overrides_not_evaluated,
  justfile: r#"
foo := `exit 1`
a := "a"
baz := "baz"

recipe arg:
 echo arg={{arg}}
 echo {{foo + a + baz}}"#,
  args:     ("foo=bar", "a=b", "recipe", "baz=bar"),
  stdout:   "arg=baz=bar\nbarbbaz\n",
  stderr:   "echo arg=baz=bar\necho barbbaz\n",
}

test! {
  name:     dry_run,
  justfile: r#"
var := `echo stderr 1>&2; echo backtick`

command:
  @touch /this/is/not/a/file
  {{var}}
  echo {{`echo command interpolation`}}

shebang:
  #!/bin/sh
  touch /this/is/not/a/file
  {{var}}
  echo {{`echo shebang interpolation`}}"#,
  args:     ("--dry-run", "shebang", "command"),
  stdout:   "",
  stderr:   "#!/bin/sh
touch /this/is/not/a/file
`echo stderr 1>&2; echo backtick`
echo `echo shebang interpolation`
touch /this/is/not/a/file
`echo stderr 1>&2; echo backtick`
echo `echo command interpolation`
",
}

test! {
  name:     line_error_spacing,
  justfile: r#"









???
"#,
  stdout:   "",
  stderr:   "error: Unknown start of token:
   |
10 | ???
   | ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     argument_single,
  justfile: "
foo A:
  echo {{A}}
    ",
  args:     ("foo", "ARGUMENT"),
  stdout:   "ARGUMENT\n",
  stderr:   "echo ARGUMENT\n",
}

test! {
  name:     argument_multiple,
  justfile: "
foo A B:
  echo A:{{A}} B:{{B}}
    ",
  args:     ("foo", "ONE", "TWO"),
  stdout:   "A:ONE B:TWO\n",
  stderr:   "echo A:ONE B:TWO\n",
}

test! {
  name:     argument_mismatch_more,
  justfile: "
foo A B:
  echo A:{{A}} B:{{B}}
    ",
  args:     ("foo", "ONE", "TWO", "THREE"),
  stdout:   "",
  stderr:   "error: Justfile does not contain recipe `THREE`.\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     argument_mismatch_fewer,
  justfile: "
foo A B:
  echo A:{{A}} B:{{B}}
    ",
  args:     ("foo", "ONE"),
  stdout:   "",
  stderr:   "error: Recipe `foo` got 1 argument but takes 2\nusage:\n    just foo A B\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     argument_mismatch_more_with_default,
  justfile: "
foo A B='B':
  echo A:{{A}} B:{{B}}
    ",
  args:     ("foo", "ONE", "TWO", "THREE"),
  stdout:   "",
  stderr:   "error: Justfile does not contain recipe `THREE`.\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     argument_mismatch_fewer_with_default,
  justfile: "
foo A B C='C':
  echo A:{{A}} B:{{B}} C:{{C}}
    ",
  args:     ("foo", "bar"),
  stdout:   "",
  stderr:   "
    error: Recipe `foo` got 1 argument but takes at least 2
    usage:
        just foo A B C='C'
  ",
  status:   EXIT_FAILURE,
}

test! {
  name:     unknown_recipe,
  justfile: "hello:",
  args:     ("foo"),
  stdout:   "",
  stderr:   "error: Justfile does not contain recipe `foo`.\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     unknown_recipes,
  justfile: "hello:",
  args:     ("foo", "bar"),
  stdout:   "",
  stderr:   "error: Justfile does not contain recipes `foo` or `bar`.\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     color_always,
  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
  args:     ("--color", "always"),
  stdout:   "",
  stderr:   "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100\u{1b}[0m
  |\n2 | a := `exit 100`\n  |      \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
  status:   100,
}

test! {
  name:     color_never,
  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
  args:     ("--color", "never"),
  stdout:   "",
  stderr:   "error: Backtick failed with exit code 100
  |
2 | a := `exit 100`
  |      ^^^^^^^^^^
",
  status:   100,
}

test! {
  name:     color_auto,
  justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
  args:     ("--color", "auto"),
  stdout:   "",
  stderr:   "error: Backtick failed with exit code 100
  |
2 | a := `exit 100`
  |      ^^^^^^^^^^
",
  status:   100,
}

test! {
  name:     colors_no_context,
  justfile: "
recipe:
  @exit 100",
  args:     ("--color=always"),
  stdout:   "",
  stderr:   "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1m\
Recipe `recipe` failed on line 2 with exit code 100\u{1b}[0m\n",
  status:   100,
}

test! {
  name:     dump,
  justfile: r#"
# this recipe does something
recipe a b +d:
 @exit 100"#,
  args:     ("--dump"),
  stdout:   "# this recipe does something
recipe a b +d:
    @exit 100
",
}

test! {
  name:     mixed_whitespace,
  justfile: "bar:\n\t echo hello",
  stdout:   "",
  stderr:   "error: Found a mix of tabs and spaces in leading whitespace: `␉␠`
Leading whitespace may consist of tabs or spaces, but not both
  |
2 |      echo hello
  | ^^^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     extra_leading_whitespace,
  justfile: "bar:\n\t\techo hello\n\t\t\techo goodbye",
  stdout:   "",
  stderr:   "error: Recipe line has extra leading whitespace
  |
3 |             echo goodbye
  |         ^^^^^^^^^^^^^^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     inconsistent_leading_whitespace,
  justfile: "bar:\n\t\techo hello\n\t echo goodbye",
  stdout:   "",
  stderr:   "error: Recipe line has inconsistent leading whitespace. \
            Recipe started with `␉␉` but found line with `␉␠`
  |
3 |      echo goodbye
  | ^^^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     required_after_default,
  justfile: "bar:\nhello baz arg='foo' bar:",
  stdout:   "",
  stderr:   "error: Non-default parameter `bar` follows default parameter
  |
2 | hello baz arg='foo' bar:
  |                     ^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     required_after_plus_variadic,
  justfile: "bar:\nhello baz +arg bar:",
  stdout:   "",
  stderr:   "error: Parameter `bar` follows variadic parameter
  |
2 | hello baz +arg bar:
  |                ^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     required_after_star_variadic,
  justfile: "bar:\nhello baz *arg bar:",
  stdout:   "",
  stderr:   "error: Parameter `bar` follows variadic parameter
  |
2 | hello baz *arg bar:
  |                ^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     use_string_default,
  justfile: r#"
bar:
hello baz arg="XYZ\t\"	":
  echo '{{baz}}...{{arg}}'
"#,
  args:     ("hello", "ABC"),
  stdout:   "ABC...XYZ\t\"\t\n",
  stderr:   "echo 'ABC...XYZ\t\"\t'\n",
}

test! {
  name:     use_raw_string_default,
  justfile: r#"
bar:
hello baz arg='XYZ"	':
  printf '{{baz}}...{{arg}}'
"#,
  args:     ("hello", "ABC"),
  stdout:   "ABC...XYZ\"\t",
  stderr:   "printf 'ABC...XYZ\"\t'\n",
}

test! {
  name:     supply_use_default,
  justfile: r#"
hello a b='B' c='C':
  echo {{a}} {{b}} {{c}}
"#,
  args:     ("hello", "0", "1"),
  stdout:   "0 1 C\n",
  stderr:   "echo 0 1 C\n",
}

test! {
  name:     supply_defaults,
  justfile: r#"
hello a b='B' c='C':
  echo {{a}} {{b}} {{c}}
"#,
  args:     ("hello", "0", "1", "2"),
  stdout:   "0 1 2\n",
  stderr:   "echo 0 1 2\n",
}

test! {
  name:     list,
  justfile: r#"

# this does a thing
hello a b='B	' c='C':
  echo {{a}} {{b}} {{c}}

# this comment will be ignored

a Z="\t z":

# this recipe will not appear
_private-recipe:
"#,
  args:     ("--list"),
  stdout:   r#"
    Available recipes:
        a Z="\t z"
        hello a b='B	' c='C' # this does a thing
  "#,
}

test! {
  name:     list_alignment,
  justfile: r#"

# this does a thing
hello a b='B	' c='C':
  echo {{a}} {{b}} {{c}}

# something else
a Z="\t z":

# this recipe will not appear
_private-recipe:
"#,
  args:     ("--list"),
  stdout:   r#"
    Available recipes:
        a Z="\t z"          # something else
        hello a b='B	' c='C' # this does a thing
  "#,
}

test! {
  name:     list_alignment_long,
  justfile: r#"

# this does a thing
hello a b='B	' c='C':
  echo {{a}} {{b}} {{c}}

# this does another thing
x a b='B	' c='C':
  echo {{a}} {{b}} {{c}}

# something else
this-recipe-is-very-very-very-important Z="\t z":

# this recipe will not appear
_private-recipe:
"#,
  args:     ("--list"),
  stdout:   r#"
    Available recipes:
        hello a b='B	' c='C' # this does a thing
        this-recipe-is-very-very-very-important Z="\t z" # something else
        x a b='B	' c='C'     # this does another thing
  "#,
}

test! {
  name:     list_sorted,
  justfile: r#"
alias c := b
b:
a:
"#,
  args:     ("--list"),
  stdout:   r#"
    Available recipes:
        a
        b
        c # alias for `b`
  "#,
}

test! {
  name:     list_unsorted,
  justfile: r#"
alias c := b
b:
a:
"#,
  args:     ("--list", "--unsorted"),
  stdout:   r#"
    Available recipes:
        b
        c # alias for `b`
        a
  "#,
}

test! {
  name:     list_heading,
  justfile: r#"
a:
b:
"#,
  args:     ("--list", "--list-heading", "Cool stuff…\n"),
  stdout:   r#"
    Cool stuff…
        a
        b
  "#,
}

test! {
  name:     list_prefix,
  justfile: r#"
a:
b:
"#,
  args:     ("--list", "--list-prefix", "····"),
  stdout:   r#"
    Available recipes:
    ····a
    ····b
  "#,
}

test! {
  name:     list_empty_prefix_and_heading,
  justfile: r#"
a:
b:
"#,
  args:     ("--list", "--list-heading", "", "--list-prefix", ""),
  stdout:   r#"
    a
    b
  "#,
}

test! {
  name:     run_suggestion,
  justfile: r#"
hello a b='B	' c='C':
  echo {{a}} {{b}} {{c}}

a Z="\t z":
"#,
  args:     ("hell"),
  stdout:   "",
  stderr:   "error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     line_continuation_with_space,
  justfile: r#"
foo:
  echo a\
         b  \
             c
"#,
  stdout:   "ab c\n",
  stderr:   "echo ab  c\n",
}

test! {
  name:     line_continuation_with_quoted_space,
  justfile: r#"
foo:
  echo 'a\
         b  \
             c'
"#,
  stdout:   "ab  c\n",
  stderr:   "echo 'ab  c'\n",
}

test! {
  name:     line_continuation_no_space,
  justfile: r#"
foo:
  echo a\
  b\
  c
"#,
  stdout:   "abc\n",
  stderr:   "echo abc\n",
}

test! {
  name: infallible_command,
  justfile: r#"
infallible:
  -exit 101
"#,
  stderr: "exit 101\n",
  status: EXIT_SUCCESS,
}

test! {
  name: infallible_with_failing,
  justfile: r#"
infallible:
  -exit 101
  exit 202
"#,
  stderr: r#"exit 101
exit 202
error: Recipe `infallible` failed on line 3 with exit code 202
"#,
  status: 202,
}

test! {
  name:     quiet_recipe,
  justfile: r#"
@quiet:
  # a
  # b
  @echo c
"#,
  stdout:   "c\n",
  stderr:   "echo c\n",
}

test! {
  name:     quiet_shebang_recipe,
  justfile: r#"
@quiet:
  #!/bin/sh
  echo hello
"#,
  stdout:   "hello\n",
  stderr:   "#!/bin/sh\necho hello\n",
}

test! {
  name:     shebang_line_numbers,
  justfile: r#"
    quiet:
      #!/usr/bin/env cat

      a

      b


      c


  "#,
  stdout:   "
    #!/usr/bin/env cat


    a

    b


    c
  ",
}

test! {
  name:     complex_dependencies,
  justfile: r#"
a: b
b:
c: b a
"#,
  args:     ("b"),
  stdout:   "",
}

test! {
  name:     parameter_shadows_variable,
  justfile: "FOO := 'hello'\na FOO:",
  args:     ("a"),
  stdout:   "",
  stderr:   "error: Parameter `FOO` shadows variable of the same name
  |
2 | a FOO:
  |   ^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     unknown_function_in_assignment,
  justfile: r#"foo := foo() + "hello"
bar:"#,
  args:     ("bar"),
  stdout:   "",
  stderr:   r#"error: Call to unknown function `foo`
  |
1 | foo := foo() + "hello"
  |        ^^^
"#,
  status:   EXIT_FAILURE,
}

test! {
  name:     dependency_takes_arguments_exact,
  justfile: "
    a FOO:
    b: a
  ",
  args:     ("b"),
  stdout:   "",
  stderr:   "error: Dependency `a` got 0 arguments but takes 1 argument
  |
2 | b: a
  |    ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     dependency_takes_arguments_at_least,
  justfile: "
    a FOO LUZ='hello':
    b: a
  ",
  args:     ("b"),
  stdout:   "",
  stderr:   "error: Dependency `a` got 0 arguments but takes at least 1 argument
  |
2 | b: a
  |    ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     dependency_takes_arguments_at_most,
  justfile: "
    a FOO LUZ='hello':
    b: (a '0' '1' '2')
  ",
  args:     ("b"),
  stdout:   "",
  stderr:   "error: Dependency `a` got 3 arguments but takes at most 2 arguments
  |
2 | b: (a '0' '1' '2')
  |     ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     duplicate_parameter,
  justfile: "a foo foo:",
  args:     ("a"),
  stdout:   "",
  stderr:   "error: Recipe `a` has duplicate parameter `foo`
  |
1 | a foo foo:
  |       ^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     duplicate_recipe,
  justfile: "b:\nb:",
  args:     ("b"),
  stdout:   "",
  stderr:   "error: Recipe `b` first defined on line 1 is redefined on line 2
  |
2 | b:
  | ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     duplicate_variable,
  justfile: "a := 'hello'\na := 'hello'\nfoo:",
  args:     ("foo"),
  stdout:   "",
  stderr:   "error: Variable `a` has multiple definitions
  |
2 | a := 'hello'
  | ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     unexpected_token_in_dependency_position,
  justfile: "foo: 'bar'",
  args:     ("foo"),
  stdout:   "",
  stderr:   "error: Expected '&&', comment, end of file, end of line, \
    identifier, or '(', but found string
  |
1 | foo: 'bar'
  |      ^^^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     unexpected_token_after_name,
  justfile: "foo 'bar'",
  args:     ("foo"),
  stdout:   "",
  stderr:   "error: Expected '*', ':', '$', identifier, or '+', but found string
  |
1 | foo 'bar'
  |     ^^^^^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     self_dependency,
  justfile: "a: a",
  args:     ("a"),
  stdout:   "",
  stderr:   "error: Recipe `a` depends on itself
  |
1 | a: a
  |    ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     long_circular_recipe_dependency,
  justfile: "a: b\nb: c\nc: d\nd: a",
  args:     ("a"),
  stdout:   "",
  stderr:   "error: Recipe `d` has circular dependency `a -> b -> c -> d -> a`
  |
4 | d: a
  |    ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     variable_self_dependency,
  justfile: "z := z\na:",
  args:     ("a"),
  stdout:   "",
  stderr:   "error: Variable `z` is defined in terms of itself
  |
1 | z := z
  | ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     variable_circular_dependency,
  justfile: "x := y\ny := z\nz := x\na:",
  args:     ("a"),
  stdout:   "",
  stderr:   "error: Variable `x` depends on its own value: `x -> y -> z -> x`
  |
1 | x := y
  | ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     variable_circular_dependency_with_additional_variable,
  justfile: "
    a := ''
    x := y
    y := x

    a:
  ",
  args:     ("a"),
  stdout:   "",
  stderr:   "error: Variable `x` depends on its own value: `x -> y -> x`
  |
2 | x := y
  | ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     plus_variadic_recipe,
  justfile: "
a x y +z:
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1", "2", "3", " 4 "),
  stdout:   "0 1 2 3 4\n",
  stderr:   "echo 0 1 2 3  4 \n",
}

test! {
  name:     plus_variadic_ignore_default,
  justfile: "
a x y +z='HELLO':
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1", "2", "3", " 4 "),
  stdout:   "0 1 2 3 4\n",
  stderr:   "echo 0 1 2 3  4 \n",
}

test! {
  name:     plus_variadic_use_default,
  justfile: "
a x y +z='HELLO':
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1"),
  stdout:   "0 1 HELLO\n",
  stderr:   "echo 0 1 HELLO\n",
}

test! {
  name:     plus_variadic_too_few,
  justfile: "
a x y +z:
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1"),
  stdout:   "",
  stderr:   "error: Recipe `a` got 2 arguments but takes at least 3\nusage:\n    just a x y +z\n",
  status:   EXIT_FAILURE,
}

test! {
  name:     star_variadic_recipe,
  justfile: "
a x y *z:
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1", "2", "3", " 4 "),
  stdout:   "0 1 2 3 4\n",
  stderr:   "echo 0 1 2 3  4 \n",
}

test! {
  name:     star_variadic_none,
  justfile: "
a x y *z:
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1"),
  stdout:   "0 1\n",
  stderr:   "echo 0 1 \n",
}

test! {
  name:     star_variadic_ignore_default,
  justfile: "
a x y *z='HELLO':
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1", "2", "3", " 4 "),
  stdout:   "0 1 2 3 4\n",
  stderr:   "echo 0 1 2 3  4 \n",
}

test! {
  name:     star_variadic_use_default,
  justfile: "
a x y *z='HELLO':
  echo {{x}} {{y}} {{z}}
",
  args:     ("a", "0", "1"),
  stdout:   "0 1 HELLO\n",
  stderr:   "echo 0 1 HELLO\n",
}

test! {
  name:     star_then_plus_variadic,
  justfile: "
foo *a +b:
  echo {{a}} {{b}}
",
  stdout:   "",
  stderr:   "error: Expected \':\' or \'=\', but found \'+\'
  |
1 | foo *a +b:
  |        ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     plus_then_star_variadic,
  justfile: "
foo +a *b:
  echo {{a}} {{b}}
",
  stdout:   "",
  stderr:   "error: Expected \':\' or \'=\', but found \'*\'
  |
1 | foo +a *b:
  |        ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     argument_grouping,
  justfile: "
FOO A B='blarg':
  echo foo: {{A}} {{B}}

BAR X:
  echo bar: {{X}}

BAZ +Z:
  echo baz: {{Z}}
",
  args:     ("BAR", "0", "FOO", "1", "2", "BAZ", "3", "4", "5"),
  stdout:   "bar: 0\nfoo: 1 2\nbaz: 3 4 5\n",
  stderr:   "echo bar: 0\necho foo: 1 2\necho baz: 3 4 5\n",
}

test! {
  name:     missing_second_dependency,
  justfile: "
x:

a: x y
",
  stdout:   "",
  stderr:   "error: Recipe `a` has unknown dependency `y`
  |
3 | a: x y
  |      ^
",
  status:   EXIT_FAILURE,
}

test! {
  name:     list_colors,
  justfile: "
# comment
a B C +D='hello':
  echo {{B}} {{C}} {{D}}
",
  args:     ("--color", "always", "--list"),
  stdout:   "
    Available recipes:
        a \
    \u{1b}[36mB\u{1b}[0m \u{1b}[36mC\u{1b}[0m \u{1b}[35m+\
    \u{1b}[0m\u{1b}[36mD\u{1b}[0m=\u{1b}[32m'hello'\u{1b}[0m \
     \u{1b}[34m#\u{1b}[0m \u{1b}[34mcomment\u{1b}[0m
  ",
}

test! {
  name:     run_colors,
  justfile: "
# comment
a:
  echo hi
",
  args:     ("--color", "always", "--highlight", "--verbose"),
  stdout:   "hi\n",
  stderr:   "\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\n\u{1b}[1mecho hi\u{1b}[0m\n",
}

test! {
  name:     no_highlight,
  justfile: "
# comment
a:
  echo hi
",
  args:     ("--color", "always", "--highlight", "--no-highlight", "--verbose"),
  stdout:   "hi\n",
  stderr:   "\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\necho hi\n",
}

test! {
  name:     trailing_flags,
  justfile: "
echo A B C:
  echo {{A}} {{B}} {{C}}
",
  args:     ("echo", "--some", "--awesome", "--flags"),
  stdout:   "--some --awesome --flags\n",
  stderr:   "echo --some --awesome --flags\n",
}

test! {
   name:     comment_before_variable,
   justfile: "
#
A:='1'
echo:
  echo {{A}}
 ",
   args:     ("echo"),
   stdout:   "1\n",
   stderr:   "echo 1\n",
}

test! {
   name:     dotenv_variable_in_recipe,
   justfile: "
#
set dotenv-load

echo:
  echo $DOTENV_KEY
 ",
   stdout:   "dotenv-value\n",
   stderr:   "echo $DOTENV_KEY\n",
}

test! {
   name:     dotenv_variable_in_backtick,
   justfile: "
#
set dotenv-load
X:=`echo $DOTENV_KEY`
echo:
  echo {{X}}
 ",
   stdout:   "dotenv-value\n",
   stderr:   "echo dotenv-value\n",
}
test! {
   name:     dotenv_variable_in_function_in_recipe,
   justfile: "
#
set dotenv-load
echo:
  echo {{env_var_or_default('DOTENV_KEY', 'foo')}}
  echo {{env_var('DOTENV_KEY')}}
 ",
   stdout:   "dotenv-value\ndotenv-value\n",
   stderr:   "echo dotenv-value\necho dotenv-value\n",
}

test! {
   name:     dotenv_variable_in_function_in_backtick,
   justfile: "
#
set dotenv-load
X:=env_var_or_default('DOTENV_KEY', 'foo')
Y:=env_var('DOTENV_KEY')
echo:
  echo {{X}}
  echo {{Y}}
 ",
   stdout:   "dotenv-value\ndotenv-value\n",
   stderr:   "echo dotenv-value\necho dotenv-value\n",
}

test! {
   name:     no_dotenv,
   justfile: "
#
X:=env_var_or_default('DOTENV_KEY', 'DEFAULT')
echo:
  echo {{X}}
 ",
   args:     ("--no-dotenv"),
   stdout:   "DEFAULT\n",
   stderr:   "echo DEFAULT\n",
}

test! {
   name:     dotenv_env_var_override,
   justfile: "
#
echo:
  echo $DOTENV_KEY
 ",
   env:      {"DOTENV_KEY": "not-the-dotenv-value",},
   stdout:   "not-the-dotenv-value\n",
   stderr:   "echo $DOTENV_KEY\n",
}

test! {
   name:     invalid_escape_sequence_message,
   justfile: r#"
X := "\'"
"#,
   stdout:   "",
   stderr:   r#"error: `\'` is not a valid escape sequence
  |
1 | X := "\'"
  |      ^^^^
"#,
   status:   EXIT_FAILURE,
}

test! {
   name:     unknown_variable_in_default,
   justfile: "
     foo x=bar:
   ",
   stdout:   "",
   stderr:   r#"error: Variable `bar` not defined
  |
1 | foo x=bar:
  |       ^^^
"#,
   status:   EXIT_FAILURE,
}

test! {
   name:     unknown_function_in_default,
   justfile: "
foo x=bar():
",
   stdout:   "",
   stderr:   r#"error: Call to unknown function `bar`
  |
1 | foo x=bar():
  |       ^^^
"#,
   status:   EXIT_FAILURE,
}

test! {
   name:     default_string,
   justfile: "
foo x='bar':
  echo {{x}}
",
   stdout:   "bar\n",
   stderr:   "echo bar\n",
}

test! {
   name:     default_concatenation,
   justfile: "
foo x=(`echo foo` + 'bar'):
  echo {{x}}
",
   stdout:   "foobar\n",
   stderr:   "echo foobar\n",
}

test! {
   name:     default_backtick,
   justfile: "
foo x=`echo foo`:
  echo {{x}}
",
   stdout:   "foo\n",
   stderr:   "echo foo\n",
}

test! {
   name:     default_variable,
   justfile: "
y := 'foo'
foo x=y:
  echo {{x}}
",
   stdout:   "foo\n",
   stderr:   "echo foo\n",
}

test! {
  name:     unterminated_interpolation_eol,
  justfile: "
    foo:
      echo {{
  ",
  stderr:   r#"
    error: Unterminated interpolation
      |
    2 |   echo {{
      |        ^^
  "#,
  status:   EXIT_FAILURE,
}

test! {
  name:     unterminated_interpolation_eof,
  justfile: "
    foo:
      echo {{
  ",
  stderr:   r#"
    error: Unterminated interpolation
      |
    2 |   echo {{
      |        ^^
  "#,
  status:   EXIT_FAILURE,
}

test! {
  name:     unknown_start_of_token,
  justfile: "
assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
",
  stderr:   r#"
    error: Unknown start of token:
      |
    1 | assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
      |                         ^
  "#,
   status:   EXIT_FAILURE,
}

test! {
  name:     backtick_variable_cat,
  justfile: "
stdin := `cat`

default:
  echo {{stdin}}
",
  stdin:    "STDIN",
  stdout:   "STDIN\n",
  stderr:   "echo STDIN\n",
}

test! {
   name:     backtick_default_cat_stdin,
   justfile: "
default stdin = `cat`:
  echo {{stdin}}
",
   stdin:    "STDIN",
   stdout:   "STDIN\n",
   stderr:   "echo STDIN\n",
}

test! {
  name:     backtick_default_cat_justfile,
  justfile: "
    default stdin = `cat justfile`:
      echo '{{stdin}}'
  ",
  stdout:   "
    default stdin = `cat justfile`:
      echo {{stdin}}
  ",
  stderr:   "
    echo 'default stdin = `cat justfile`:
      echo '{{stdin}}''
  ",
}

test! {
   name:     backtick_variable_read_single,
   justfile: "
password := `read PW && echo $PW`

default:
  echo {{password}}
",
   stdin:    "foobar\n",
   stdout:   "foobar\n",
   stderr:   "echo foobar\n",
}

test! {
   name:     backtick_variable_read_multiple,
   justfile: "
a := `read A && echo $A`
b := `read B && echo $B`

default:
  echo {{a}}
  echo {{b}}
",
   stdin:    "foo\nbar\n",
   stdout:   "foo\nbar\n",
   stderr:   "echo foo\necho bar\n",
}

test! {
   name:     backtick_default_read_multiple,
   justfile: "

default a=`read A && echo $A` b=`read B && echo $B`:
  echo {{a}}
  echo {{b}}
",
   stdin:    "foo\nbar\n",
   stdout:   "foo\nbar\n",
   stderr:   "echo foo\necho bar\n",
}

test! {
  name: old_equals_assignment_syntax_produces_error,
  justfile: "
    foo = 'bar'

    default:
      echo {{foo}}
  ",
  stderr: "
    error: Expected '*', ':', '$', identifier, or '+', but found '='
      |
    1 | foo = 'bar'
      |     ^
    ",
  status: EXIT_FAILURE,
}

test! {
  name: dependency_argument_string,
  justfile: "
    release: (build 'foo') (build 'bar')

    build target:
      echo 'Building {{target}}...'
  ",
  args: (),
  stdout: "Building foo...\nBuilding bar...\n",
  stderr: "echo 'Building foo...'\necho 'Building bar...'\n",
  shell: false,
}

test! {
  name: dependency_argument_parameter,
  justfile: "
    default: (release '1.0')

    release version: (build 'foo' version) (build 'bar' version)

    build target version:
      echo 'Building {{target}}@{{version}}...'
  ",
  args: (),
  stdout: "Building foo@1.0...\nBuilding bar@1.0...\n",
  stderr: "echo 'Building foo@1.0...'\necho 'Building bar@1.0...'\n",
  shell: false,
}

test! {
  name: dependency_argument_function,
  justfile: "
    foo: (bar env_var_or_default('x', 'y'))

    bar arg:
      echo {{arg}}
  ",
  args: (),
  stdout: "y\n",
  stderr: "echo y\n",
  shell: false,
}

test! {
  name: dependency_argument_backtick,
  justfile: "
    export X := 'X'

    foo: (bar `echo $X`)

    bar arg:
      echo {{arg}}
      echo $X
  ",
  args: (),
  stdout: "X\nX\n",
  stderr: "echo X\necho $X\n",
  shell: false,
}

test! {
  name: dependency_argument_assignment,
  justfile: "
    v := '1.0'

    default: (release v)

    release version:
      echo Release {{version}}...
  ",
  args: (),
  stdout: "Release 1.0...\n",
  stderr: "echo Release 1.0...\n",
  shell: false,
}

test! {
  name: dependency_argument_plus_variadic,
  justfile: "
    foo: (bar 'A' 'B' 'C')

    bar +args:
      echo {{args}}
  ",
  args: (),
  stdout: "A B C\n",
  stderr: "echo A B C\n",
  shell: false,
}

test! {
  name: duplicate_dependency_no_args,
  justfile: "
    foo: bar bar bar bar

    bar:
      echo BAR
  ",
  args: (),
  stdout: "BAR\n",
  stderr: "echo BAR\n",
  shell: false,
}

test! {
  name: duplicate_dependency_argument,
  justfile: "
    foo: (bar 'BAR') (bar `echo BAR`)

    bar bar:
      echo {{bar}}
  ",
  args: (),
  stdout: "BAR\n",
  stderr: "echo BAR\n",
  shell: false,
}

test! {
  name: parameter_cross_reference_error,
  justfile: "
    foo:

    bar a b=a:
  ",
  args: (),
  stdout: "",
  stderr: "
    error: Variable `a` not defined
      |
    3 | bar a b=a:
      |         ^
  ",
  status: EXIT_FAILURE,
  shell: false,
}

#[cfg(windows)]
test! {
  name: pwsh_invocation_directory,
  justfile: r#"
    set shell := ["pwsh", "-NoProfile", "-c"]

    pwd:
      @Test-Path {{invocation_directory()}} > result.txt
  "#,
  args: (),
  stdout: "",
  stderr: "",
  status: EXIT_SUCCESS,
  shell: false,
}

test! {
  name: variables,
  justfile: "
    z := 'a'
    a := 'z'
  ",
  args: ("--variables"),
  stdout: "a z\n",
  stderr: "",
  shell: false,
}

test! {
  name: interpolation_evaluation_ignore_quiet,
  justfile: r#"
    foo:
      {{"@echo foo 2>/dev/null"}}
  "#,
  args: (),
  stdout: "",
  stderr: "
    @echo foo 2>/dev/null
    error: Recipe `foo` failed on line 2 with exit code 127
  ",
  status: 127,
  shell: false,
}

test! {
  name: interpolation_evaluation_ignore_quiet_continuation,
  justfile: r#"
    foo:
      {{""}}\
      @echo foo 2>/dev/null
  "#,
  args: (),
  stdout: "",
  stderr: "
    @echo foo 2>/dev/null
    error: Recipe `foo` failed on line 3 with exit code 127
  ",
  status: 127,
  shell: false,
}

test! {
  name: brace_escape,
  justfile: "
    foo:
      echo '{{{{'
  ",
  stdout: "{{\n",
  stderr: "
    echo '{{'
  ",
}

test! {
  name: brace_escape_extra,
  justfile: "
    foo:
      echo '{{{{{'
  ",
  stdout: "{{{\n",
  stderr: "
    echo '{{{'
  ",
}

test! {
  name: multi_line_string_in_interpolation,
  justfile: "
    foo:
      echo {{'a
      echo b
      echo c'}}z
      echo baz
  ",
  stdout: "a\nb\ncz\nbaz\n",
  stderr: "echo a\n  echo b\n  echo cz\necho baz\n",
}

#[cfg(windows)]
test! {
  name: windows_interpreter_path_no_base,
  justfile: r#"
    foo:
      #!powershell

      exit 0
  "#,
  args: (),
}