just/tests/misc.rs
2025-02-26 09:26:43 -08:00

2810 lines
42 KiB
Rust

use super::*;
#[test]
fn alias_listing() {
Test::new()
.arg("--list")
.justfile(
"
foo:
echo foo
alias f := foo
",
)
.stdout(
"
Available recipes:
foo # [alias: f]
",
)
.run();
}
#[test]
fn alias_listing_with_doc() {
Test::new()
.justfile(
"
# foo command
foo:
echo foo
alias f := foo
",
)
.arg("--list")
.stdout(
"
Available recipes:
foo # foo command [alias: f]
",
)
.run();
}
#[test]
fn alias_listing_multiple_aliases() {
Test::new()
.arg("--list")
.justfile("foo:\n echo foo\nalias f := foo\nalias fo := foo")
.stdout(
"
Available recipes:
foo # [aliases: f, fo]
",
)
.run();
}
#[test]
fn alias_listing_parameters() {
Test::new()
.args(["--list"])
.justfile("foo PARAM='foo':\n echo {{PARAM}}\nalias f := foo")
.stdout(
"
Available recipes:
foo PARAM='foo' # [alias: f]
",
)
.run();
}
#[test]
fn alias_listing_private() {
Test::new()
.arg("--list")
.justfile("foo PARAM='foo':\n echo {{PARAM}}\nalias _f := foo")
.stdout(
"
Available recipes:
foo PARAM='foo'
",
)
.run();
}
#[test]
fn alias() {
Test::new()
.arg("f")
.justfile("foo:\n echo foo\nalias f := foo")
.stdout("foo\n")
.stderr("echo foo\n")
.run();
}
#[test]
fn alias_with_parameters() {
Test::new()
.arg("f")
.arg("bar")
.justfile("foo value='foo':\n echo {{value}}\nalias f := foo")
.stdout("bar\n")
.stderr("echo bar\n")
.run();
}
#[test]
fn bad_setting() {
Test::new()
.justfile(
"
set foo
",
)
.stderr(
"
error: Unknown setting `foo`
——▶ justfile:1:5
1 │ set foo
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn bad_setting_with_keyword_name() {
Test::new()
.justfile(
"
set if := 'foo'
",
)
.stderr(
"
error: Unknown setting `if`
——▶ justfile:1:5
1 │ set if := 'foo'
│ ^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn alias_with_dependencies() {
Test::new()
.arg("b")
.justfile("foo:\n echo foo\nbar: foo\nalias b := bar")
.stdout("foo\n")
.stderr("echo foo\n")
.run();
}
#[test]
fn duplicate_alias() {
Test::new()
.justfile("alias foo := bar\nalias foo := baz\n")
.stderr(
"
error: Alias `foo` first defined on line 1 is redefined on line 2
——▶ justfile:2:7
2 │ alias foo := baz
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_alias_target() {
Test::new()
.justfile("alias foo := bar\n")
.stderr(
"
error: Alias `foo` has an unknown target `bar`
——▶ justfile:1:7
1 │ alias foo := bar
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn alias_shadows_recipe() {
Test::new()
.justfile("bar:\n echo bar\nalias foo := bar\nfoo:\n echo foo")
.stderr(
"
error: Alias `foo` defined on line 3 is redefined as a recipe on line 4
——▶ justfile:4:1
4 │ foo:
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn default() {
Test::new()
.justfile("default:\n echo hello\nother: \n echo bar")
.stdout("hello\n")
.stderr("echo hello\n")
.run();
}
#[test]
fn quiet() {
Test::new()
.justfile("default:\n @echo hello")
.stdout("hello\n")
.run();
}
#[test]
fn verbose() {
Test::new()
.arg("--verbose")
.justfile("default:\n @echo hello")
.stdout("hello\n")
.stderr("===> Running recipe `default`...\necho hello\n")
.run();
}
#[test]
fn order() {
Test::new()
.arg("a")
.arg("d")
.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",
)
.stdout("a\nb\nc\nd\n")
.stderr("echo a\necho b\necho c\necho d\n")
.run();
}
#[test]
fn select() {
Test::new()
.arg("d")
.arg("c")
.justfile("b:\n @echo b\na:\n @echo a\nd:\n @echo d\nc:\n @echo c")
.stdout("d\nc\n")
.run();
}
#[test]
fn print() {
Test::new()
.arg("d")
.arg("c")
.justfile("b:\n echo b\na:\n echo a\nd:\n echo d\nc:\n echo c")
.stdout("d\nc\n")
.stderr("echo d\necho c\n")
.run();
}
#[test]
fn status_passthrough() {
Test::new()
.arg("recipe")
.justfile(
"
hello:
recipe:
@exit 100",
)
.stderr("error: Recipe `recipe` failed on line 5 with exit code 100\n")
.status(100)
.run();
}
#[test]
fn unknown_dependency() {
Test::new()
.justfile("bar:\nhello:\nfoo: bar baaaaaaaz hello")
.stderr(
"
error: Recipe `foo` has unknown dependency `baaaaaaaz`
——▶ justfile:3:10
3 │ foo: bar baaaaaaaz hello
│ ^^^^^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn backtick_success() {
Test::new()
.justfile("a := `printf Hello,`\nbar:\n printf '{{a + `printf ' world.'`}}'")
.stdout("Hello, world.")
.stderr("printf 'Hello, world.'\n")
.run();
}
#[test]
fn backtick_trimming() {
Test::new()
.justfile("a := `echo Hello,`\nbar:\n echo '{{a + `echo ' world.'`}}'")
.stdout("Hello, world.\n")
.stderr("echo 'Hello, world.'\n")
.run();
}
#[test]
fn backtick_code_assignment() {
Test::new()
.justfile("b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'")
.stderr(
"
error: Backtick failed with exit code 100
——▶ justfile:2:6
2 │ a := `exit 100`
│ ^^^^^^^^^^
",
)
.status(100)
.run();
}
#[test]
fn backtick_code_interpolation() {
Test::new()
.justfile("b := a\na := `echo hello`\nbar:\n echo '{{`exit 200`}}'")
.stderr(
"
error: Backtick failed with exit code 200
——▶ justfile:4:10
4 │ echo '{{`exit 200`}}'
│ ^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn backtick_code_interpolation_mod() {
Test::new()
.justfile("f:\n 無{{`exit 200`}}")
.stderr(
"
error: Backtick failed with exit code 200
——▶ justfile:2:7
2 │ 無{{`exit 200`}}
│ ^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn backtick_code_interpolation_tab() {
Test::new()
.justfile(
"
backtick-fail:
\techo {{`exit 200`}}
",
)
.stderr(
" error: Backtick failed with exit code 200
——▶ justfile:2:9
2 │ echo {{`exit 200`}}
│ ^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn backtick_code_interpolation_tabs() {
Test::new()
.justfile(
"
backtick-fail:
\techo {{\t`exit 200`}}
",
)
.stderr(
"error: Backtick failed with exit code 200
——▶ justfile:2:10
2 │ echo {{ `exit 200`}}
│ ^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn backtick_code_interpolation_inner_tab() {
Test::new()
.justfile(
"
backtick-fail:
\techo {{\t`exit\t\t200`}}
",
)
.stderr(
"
error: Backtick failed with exit code 200
——▶ justfile:2:10
2 │ echo {{ `exit 200`}}
│ ^^^^^^^^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn backtick_code_interpolation_leading_emoji() {
Test::new()
.justfile(
"
backtick-fail:
\techo 😬{{`exit 200`}}
",
)
.stderr(
"
error: Backtick failed with exit code 200
——▶ justfile:2:13
2 │ echo 😬{{`exit 200`}}
│ ^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn backtick_code_interpolation_unicode_hell() {
Test::new()
.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
——▶ justfile:2:24
2 │ echo 😬鎌鼬{{ `exit 200 # abc`}} 😬鎌鼬
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn backtick_code_long() {
Test::new()
.justfile(
"
b := a
a := `echo hello`
bar:
echo '{{`exit 200`}}'
",
)
.stderr(
"
error: Backtick failed with exit code 200
——▶ justfile:10:10
10 │ echo '{{`exit 200`}}'
│ ^^^^^^^^^^
",
)
.status(200)
.run();
}
#[test]
fn shebang_backtick_failure() {
Test::new()
.justfile(
"foo:
#!/bin/sh
echo hello
echo {{`exit 123`}}",
)
.stderr(
"
error: Backtick failed with exit code 123
——▶ justfile:4:9
4 │ echo {{`exit 123`}}
│ ^^^^^^^^^^
",
)
.status(123)
.run();
}
#[test]
fn command_backtick_failure() {
Test::new()
.justfile(
"foo:
echo hello
echo {{`exit 123`}}",
)
.stdout("hello\n")
.stderr(
"
echo hello
error: Backtick failed with exit code 123
——▶ justfile:3:9
3 │ echo {{`exit 123`}}
│ ^^^^^^^^^^
",
)
.status(123)
.run();
}
#[test]
fn assignment_backtick_failure() {
Test::new()
.justfile(
"foo:
echo hello
echo {{`exit 111`}}
a := `exit 222`",
)
.stderr(
"
error: Backtick failed with exit code 222
——▶ justfile:4:6
4 │ a := `exit 222`
│ ^^^^^^^^^^
",
)
.status(222)
.run();
}
#[test]
fn unknown_override_options() {
Test::new()
.arg("--set")
.arg("foo")
.arg("bar")
.arg("--set")
.arg("baz")
.arg("bob")
.arg("--set")
.arg("a")
.arg("b")
.arg("a")
.arg("b")
.justfile(
"foo:
echo hello
echo {{`exit 111`}}
a := `exit 222`",
)
.status(EXIT_FAILURE)
.stderr(
"error: Variables `baz` and `foo` overridden on the command line but not present \
in justfile\n",
)
.run();
}
#[test]
fn unknown_override_args() {
Test::new()
.arg("foo=bar")
.arg("baz=bob")
.arg("a=b")
.arg("a")
.arg("b")
.justfile(
"foo:
echo hello
echo {{`exit 111`}}
a := `exit 222`",
)
.stderr(
"error: Variables `baz` and `foo` overridden on the command line but not present \
in justfile\n",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_override_arg() {
Test::new()
.arg("foo=bar")
.arg("a=b")
.arg("a")
.arg("b")
.justfile(
"foo:
echo hello
echo {{`exit 111`}}
a := `exit 222`",
)
.stderr("error: Variable `foo` overridden on the command line but not present in justfile\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn overrides_first() {
Test::new()
.arg("foo=bar")
.arg("a=b")
.arg("recipe")
.arg("baz=bar")
.justfile(
r#"
foo := "foo"
a := "a"
baz := "baz"
recipe arg:
echo arg={{arg}}
echo {{foo + a + baz}}"#,
)
.stdout("arg=baz=bar\nbarbbaz\n")
.stderr("echo arg=baz=bar\necho barbbaz\n")
.run();
}
#[test]
fn overrides_not_evaluated() {
Test::new()
.arg("foo=bar")
.arg("a=b")
.arg("recipe")
.arg("baz=bar")
.justfile(
r#"
foo := `exit 1`
a := "a"
baz := "baz"
recipe arg:
echo arg={{arg}}
echo {{foo + a + baz}}"#,
)
.stdout("arg=baz=bar\nbarbbaz\n")
.stderr("echo arg=baz=bar\necho barbbaz\n")
.run();
}
#[test]
fn dry_run() {
Test::new()
.arg("--dry-run")
.arg("shebang")
.arg("command")
.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`}}",
)
.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`
",
)
.run();
}
#[test]
fn line_error_spacing() {
Test::new()
.justfile(
r"
^^^
",
)
.stderr(
"error: Unknown start of token '^'
——▶ justfile:10:1
10 │ ^^^
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn argument_single() {
Test::new()
.arg("foo")
.arg("ARGUMENT")
.justfile(
"
foo A:
echo {{A}}
",
)
.stdout("ARGUMENT\n")
.stderr("echo ARGUMENT\n")
.run();
}
#[test]
fn argument_multiple() {
Test::new()
.arg("foo")
.arg("ONE")
.arg("TWO")
.justfile(
"
foo A B:
echo A:{{A}} B:{{B}}
",
)
.stdout("A:ONE B:TWO\n")
.stderr("echo A:ONE B:TWO\n")
.run();
}
#[test]
fn argument_mismatch_more() {
Test::new()
.arg("foo")
.arg("ONE")
.arg("TWO")
.arg("THREE")
.stderr("error: Justfile does not contain recipe `THREE`\n")
.status(EXIT_FAILURE)
.justfile(
"
foo A B:
echo A:{{A}} B:{{B}}
",
)
.run();
}
#[test]
fn argument_mismatch_fewer() {
Test::new()
.arg("foo")
.arg("ONE")
.justfile(
"
foo A B:
echo A:{{A}} B:{{B}}
",
)
.stderr("error: Recipe `foo` got 1 argument but takes 2\nusage:\n just foo A B\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn argument_mismatch_more_with_default() {
Test::new()
.arg("foo")
.arg("ONE")
.arg("TWO")
.arg("THREE")
.justfile(
"
foo A B='B':
echo A:{{A}} B:{{B}}
",
)
.stderr("error: Justfile does not contain recipe `THREE`\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn argument_mismatch_fewer_with_default() {
Test::new()
.arg("foo")
.arg("bar")
.justfile(
"
foo A B C='C':
echo A:{{A}} B:{{B}} C:{{C}}
",
)
.stderr(
"
error: Recipe `foo` got 1 argument but takes at least 2
usage:
just foo A B C='C'
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_recipe() {
Test::new()
.arg("foo")
.justfile("hello:")
.stderr("error: Justfile does not contain recipe `foo`\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_recipes() {
Test::new()
.arg("foo")
.arg("bar")
.justfile("hello:")
.stderr("error: Justfile does not contain recipe `foo`\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn color_always() {
Test::new()
.arg("--color")
.arg("always")
.justfile("b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'")
.status(100)
.stderr("\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100\u{1b}[0m\n \u{1b}[1;34m——▶\u{1b}[0m justfile:2:6\n \u{1b}[1;34m│\u{1b}[0m\n\u{1b}[1;34m2 │\u{1b}[0m a := `exit 100`\n \u{1b}[1;34m│\u{1b}[0m \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n")
.run();
}
#[test]
fn color_never() {
Test::new()
.arg("--color")
.arg("never")
.justfile("b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'")
.stderr(
"error: Backtick failed with exit code 100
——▶ justfile:2:6
2 │ a := `exit 100`
│ ^^^^^^^^^^
",
)
.status(100)
.run();
}
#[test]
fn color_auto() {
Test::new()
.arg("--color")
.arg("auto")
.justfile("b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'")
.stderr(
"error: Backtick failed with exit code 100
——▶ justfile:2:6
2 │ a := `exit 100`
│ ^^^^^^^^^^
",
)
.status(100)
.run();
}
#[test]
fn colors_no_context() {
Test::new()
.arg("--color=always")
.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)
.justfile(
"
recipe:
@exit 100",
)
.run();
}
#[test]
fn dump() {
Test::new()
.arg("--dump")
.justfile(
r"
# this recipe does something
recipe a b +d:
@exit 100",
)
.stdout(
"# this recipe does something
recipe a b +d:
@exit 100
",
)
.run();
}
#[test]
fn mixed_whitespace() {
Test::new()
.justfile("bar:\n\t echo hello")
.stderr(
"error: Found a mix of tabs and spaces in leading whitespace: `␉␠`
Leading whitespace may consist of tabs or spaces, but not both
——▶ justfile:2:1
2 │ echo hello
│ ^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn extra_leading_whitespace() {
Test::new()
.justfile("bar:\n\t\techo hello\n\t\t\techo goodbye")
.stderr(
"error: Recipe line has extra leading whitespace
——▶ justfile:3:3
3 │ echo goodbye
│ ^^^^^^^^^^^^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn inconsistent_leading_whitespace() {
Test::new()
.justfile("bar:\n\t\techo hello\n\t echo goodbye")
.stderr(
"error: Recipe line has inconsistent leading whitespace. \
Recipe started with `␉␉` but found line with `␉␠`
——▶ justfile:3:1
3 │ echo goodbye
│ ^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn required_after_default() {
Test::new()
.justfile("bar:\nhello baz arg='foo' bar:")
.stderr(
"error: Non-default parameter `bar` follows default parameter
——▶ justfile:2:21
2 │ hello baz arg='foo' bar:
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn required_after_plus_variadic() {
Test::new()
.justfile("bar:\nhello baz +arg bar:")
.stderr(
"error: Parameter `bar` follows variadic parameter
——▶ justfile:2:16
2 │ hello baz +arg bar:
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn required_after_star_variadic() {
Test::new()
.justfile("bar:\nhello baz *arg bar:")
.stderr(
"error: Parameter `bar` follows variadic parameter
——▶ justfile:2:16
2 │ hello baz *arg bar:
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn use_string_default() {
Test::new()
.arg("hello")
.arg("ABC")
.justfile(
r#"
bar:
hello baz arg="XYZ\t\" ":
echo '{{baz}}...{{arg}}'
"#,
)
.stdout("ABC...XYZ\t\"\t\n")
.stderr("echo 'ABC...XYZ\t\"\t'\n")
.run();
}
#[test]
fn use_raw_string_default() {
Test::new()
.arg("hello")
.arg("ABC")
.justfile(
r#"
bar:
hello baz arg='XYZ" ':
printf '{{baz}}...{{arg}}'
"#,
)
.stdout("ABC...XYZ\"\t")
.stderr("printf 'ABC...XYZ\"\t'\n")
.run();
}
#[test]
fn supply_use_default() {
Test::new()
.arg("hello")
.arg("0")
.arg("1")
.justfile(
r"
hello a b='B' c='C':
echo {{a}} {{b}} {{c}}
",
)
.stdout("0 1 C\n")
.stderr("echo 0 1 C\n")
.run();
}
#[test]
fn supply_defaults() {
Test::new()
.arg("hello")
.arg("0")
.arg("1")
.arg("2")
.justfile(
r"
hello a b='B' c='C':
echo {{a}} {{b}} {{c}}
",
)
.stdout("0 1 2\n")
.stderr("echo 0 1 2\n")
.run();
}
#[test]
fn list() {
Test::new()
.arg("--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:
"#,
)
.stdout(
r#"
Available recipes:
a Z="\t z"
hello a b='B ' c='C' # this does a thing
"#,
)
.run();
}
#[test]
fn list_alignment() {
Test::new()
.arg("--list")
.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:
"#,
)
.stdout(
r#"
Available recipes:
a Z="\t z" # something else
hello a b='B ' c='C' # this does a thing
"#,
)
.run();
}
#[test]
fn list_alignment_long() {
Test::new()
.arg("--list")
.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-very-very-very-very-very-important Z="\t z":
# this recipe will not appear
_private-recipe:
"#,
)
.stdout(
r#"
Available recipes:
hello a b='B ' c='C' # this does a thing
this-recipe-is-very-very-very-very-very-very-very-very-important Z="\t z" # something else
x a b='B ' c='C' # this does another thing
"#,
)
.run();
}
#[test]
fn list_sorted() {
Test::new()
.arg("--list")
.justfile(
r"
alias c := b
b:
a:
",
)
.stdout(
r"
Available recipes:
a
b # [alias: c]
",
)
.run();
}
#[test]
fn list_unsorted() {
Test::new()
.arg("--list")
.arg("--unsorted")
.justfile(
r"
alias c := b
b:
a:
",
)
.stdout(
r"
Available recipes:
b # [alias: c]
a
",
)
.run();
}
#[test]
fn list_heading() {
Test::new()
.arg("--list")
.arg("--list-heading")
.arg("Cool stuff…\n")
.justfile(
r"
a:
b:
",
)
.stdout(
r"
Cool stuff…
a
b
",
)
.run();
}
#[test]
fn list_prefix() {
Test::new()
.arg("--list")
.arg("--list-prefix")
.arg("····")
.justfile(
r"
a:
b:
",
)
.stdout(
r"
Available recipes:
····a
····b
",
)
.run();
}
#[test]
fn list_empty_prefix_and_heading() {
Test::new()
.arg("--list")
.arg("--list-heading")
.arg("")
.arg("--list-prefix")
.arg("")
.justfile(
r"
a:
b:
",
)
.stdout(
r"
a
b
",
)
.run();
}
#[test]
fn run_suggestion() {
Test::new()
.arg("hell")
.justfile(
r#"
hello a b='B ' c='C':
echo {{a}} {{b}} {{c}}
a Z="\t z":
"#,
)
.stderr("error: Justfile does not contain recipe `hell`\nDid you mean `hello`?\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn line_continuation_with_space() {
Test::new()
.justfile(
r"
foo:
echo a\
b \
c
",
)
.stdout("ab c\n")
.stderr("echo ab c\n")
.run();
}
#[test]
fn line_continuation_with_quoted_space() {
Test::new()
.justfile(
r"
foo:
echo 'a\
b \
c'
",
)
.stdout("ab c\n")
.stderr("echo 'ab c'\n")
.run();
}
#[test]
fn line_continuation_no_space() {
Test::new()
.justfile(
r"
foo:
echo a\
b\
c
",
)
.stdout("abc\n")
.stderr("echo abc\n")
.run();
}
#[test]
fn infallible_command() {
Test::new()
.justfile(
r"
infallible:
-exit 101
",
)
.stderr("exit 101\n")
.status(EXIT_SUCCESS)
.run();
}
#[test]
fn infallible_with_failing() {
Test::new()
.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)
.run();
}
#[test]
fn quiet_recipe() {
Test::new()
.justfile(
r"
@quiet:
# a
# b
@echo c
",
)
.stdout("c\n")
.stderr("echo c\n")
.run();
}
#[test]
fn quiet_shebang_recipe() {
Test::new()
.justfile(
r"
@quiet:
#!/bin/sh
echo hello
",
)
.stdout("hello\n")
.stderr("#!/bin/sh\necho hello\n")
.run();
}
#[test]
fn complex_dependencies() {
Test::new()
.arg("b")
.justfile(
r"
a: b
b:
c: b a
",
)
.run();
}
#[test]
fn unknown_function_in_assignment() {
Test::new()
.arg("bar")
.justfile(
r#"foo := foo() + "hello"
bar:"#,
)
.stderr(
r#"error: Call to unknown function `foo`
——▶ justfile:1:8
1 │ foo := foo() + "hello"
│ ^^^
"#,
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn dependency_takes_arguments_exact() {
Test::new()
.arg("b")
.justfile(
"
a FOO:
b: a
",
)
.stderr(
"error: Dependency `a` got 0 arguments but takes 1 argument
——▶ justfile:2:4
2 │ b: a
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn dependency_takes_arguments_at_least() {
Test::new()
.arg("b")
.justfile(
"
a FOO LUZ='hello':
b: a
",
)
.stderr(
"error: Dependency `a` got 0 arguments but takes at least 1 argument
——▶ justfile:2:4
2 │ b: a
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn dependency_takes_arguments_at_most() {
Test::new()
.arg("b")
.justfile(
"
a FOO LUZ='hello':
b: (a '0' '1' '2')
",
)
.stderr(
"error: Dependency `a` got 3 arguments but takes at most 2 arguments
——▶ justfile:2:5
2 │ b: (a '0' '1' '2')
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn duplicate_parameter() {
Test::new()
.arg("a")
.justfile("a foo foo:")
.stderr(
"error: Recipe `a` has duplicate parameter `foo`
——▶ justfile:1:7
1 │ a foo foo:
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn duplicate_recipe() {
Test::new()
.arg("b")
.justfile("b:\nb:")
.stderr(
"error: Recipe `b` first defined on line 1 is redefined on line 2
——▶ justfile:2:1
2 │ b:
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn duplicate_variable() {
Test::new()
.arg("foo")
.justfile("a := 'hello'\na := 'hello'\nfoo:")
.status(EXIT_FAILURE)
.stderr(
"error: Variable `a` has multiple definitions
——▶ justfile:2:1
2 │ a := 'hello'
│ ^
",
)
.run();
}
#[test]
fn unexpected_token_in_dependency_position() {
Test::new()
.arg("foo")
.justfile("foo: 'bar'")
.stderr(
"error: Expected '&&', comment, end of file, end of line, \
identifier, or '(', but found string
——▶ justfile:1:6
1 │ foo: 'bar'
│ ^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unexpected_token_after_name() {
Test::new()
.arg("foo")
.justfile("foo 'bar'")
.stderr(
"error: Expected '*', ':', '$', identifier, or '+', but found string
——▶ justfile:1:5
1 │ foo 'bar'
│ ^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn self_dependency() {
Test::new()
.arg("a")
.justfile("a: a")
.stderr(
"error: Recipe `a` depends on itself
——▶ justfile:1:4
1 │ a: a
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn long_circular_recipe_dependency() {
Test::new()
.arg("a")
.justfile("a: b\nb: c\nc: d\nd: a")
.stderr(
"error: Recipe `d` has circular dependency `a -> b -> c -> d -> a`
——▶ justfile:4:4
4 │ d: a
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn variable_self_dependency() {
Test::new()
.arg("a")
.justfile("z := z\na:")
.stderr(
"error: Variable `z` is defined in terms of itself
——▶ justfile:1:1
1 │ z := z
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn variable_circular_dependency() {
Test::new()
.arg("a")
.justfile("x := y\ny := z\nz := x\na:")
.status(EXIT_FAILURE)
.stderr(
"error: Variable `x` depends on its own value: `x -> y -> z -> x`
——▶ justfile:1:1
1 │ x := y
│ ^
",
)
.run();
}
#[test]
fn variable_circular_dependency_with_additional_variable() {
Test::new()
.arg("a")
.justfile(
"
a := ''
x := y
y := x
a:
",
)
.stderr(
"error: Variable `x` depends on its own value: `x -> y -> x`
——▶ justfile:2:1
2 │ x := y
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn plus_variadic_recipe() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.arg("2")
.arg("3")
.arg(" 4 ")
.justfile(
"
a x y +z:
echo {{x}} {{y}} {{z}}
",
)
.stdout("0 1 2 3 4\n")
.stderr("echo 0 1 2 3 4 \n")
.run();
}
#[test]
fn plus_variadic_ignore_default() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.arg("2")
.arg("3")
.arg(" 4 ")
.justfile(
"
a x y +z='HELLO':
echo {{x}} {{y}} {{z}}
",
)
.stdout("0 1 2 3 4\n")
.stderr("echo 0 1 2 3 4 \n")
.run();
}
#[test]
fn plus_variadic_use_default() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.justfile(
"
a x y +z='HELLO':
echo {{x}} {{y}} {{z}}
",
)
.stdout("0 1 HELLO\n")
.stderr("echo 0 1 HELLO\n")
.run();
}
#[test]
fn plus_variadic_too_few() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.justfile(
"
a x y +z:
echo {{x}} {{y}} {{z}}
",
)
.stderr("error: Recipe `a` got 2 arguments but takes at least 3\nusage:\n just a x y +z\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn star_variadic_recipe() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.arg("2")
.arg("3")
.arg(" 4 ")
.justfile(
"
a x y *z:
echo {{x}} {{y}} {{z}}
",
)
.stdout("0 1 2 3 4\n")
.stderr("echo 0 1 2 3 4 \n")
.run();
}
#[test]
fn star_variadic_none() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.justfile(
"
a x y *z:
echo {{x}} {{y}} {{z}}
",
)
.stdout("0 1\n")
.stderr("echo 0 1 \n")
.run();
}
#[test]
fn star_variadic_ignore_default() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.arg("2")
.arg("3")
.arg(" 4 ")
.justfile(
"
a x y *z='HELLO':
echo {{x}} {{y}} {{z}}
",
)
.stdout("0 1 2 3 4\n")
.stderr("echo 0 1 2 3 4 \n")
.run();
}
#[test]
fn star_variadic_use_default() {
Test::new()
.arg("a")
.arg("0")
.arg("1")
.justfile(
"
a x y *z='HELLO':
echo {{x}} {{y}} {{z}}
",
)
.stdout("0 1 HELLO\n")
.stderr("echo 0 1 HELLO\n")
.run();
}
#[test]
fn star_then_plus_variadic() {
Test::new()
.justfile(
"
foo *a +b:
echo {{a}} {{b}}
",
)
.stderr(
"error: Expected \':\' or \'=\', but found \'+\'
——▶ justfile:1:8
1 │ foo *a +b:
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn plus_then_star_variadic() {
Test::new()
.justfile(
"
foo +a *b:
echo {{a}} {{b}}
",
)
.stderr(
"error: Expected \':\' or \'=\', but found \'*\'
——▶ justfile:1:8
1 │ foo +a *b:
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn argument_grouping() {
Test::new()
.arg("BAR")
.arg("0")
.arg("FOO")
.arg("1")
.arg("2")
.arg("BAZ")
.arg("3")
.arg("4")
.arg("5")
.justfile(
"
FOO A B='blarg':
echo foo: {{A}} {{B}}
BAR X:
echo bar: {{X}}
BAZ +Z:
echo baz: {{Z}}
",
)
.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")
.run();
}
#[test]
fn missing_second_dependency() {
Test::new()
.justfile(
"
x:
a: x y
",
)
.stderr(
"error: Recipe `a` has unknown dependency `y`
——▶ justfile:3:6
3 │ a: x y
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn list_colors() {
Test::new()
.arg("--color")
.arg("always")
.arg("--list")
.justfile(
"
# comment
a B C +D='hello':
echo {{B}} {{C}} {{D}}
",
)
.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
",
)
.run();
}
#[test]
fn run_colors() {
Test::new()
.arg("--color")
.arg("always")
.arg("--highlight")
.arg("--verbose")
.justfile(
"
# comment
a:
echo hi
",
)
.stdout("hi\n")
.stderr("\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\n\u{1b}[1mecho hi\u{1b}[0m\n")
.run();
}
#[test]
fn no_highlight() {
Test::new()
.arg("--color")
.arg("always")
.arg("--highlight")
.arg("--no-highlight")
.arg("--verbose")
.justfile(
"
# comment
a:
echo hi
",
)
.stdout("hi\n")
.stderr("\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\necho hi\n")
.run();
}
#[test]
fn trailing_flags() {
Test::new()
.arg("echo")
.arg("--some")
.arg("--awesome")
.arg("--flags")
.justfile(
"
echo A B C:
echo {{A}} {{B}} {{C}}
",
)
.stdout("--some --awesome --flags\n")
.stderr("echo --some --awesome --flags\n")
.run();
}
#[test]
fn comment_before_variable() {
Test::new()
.arg("echo")
.justfile(
"
#
A:='1'
echo:
echo {{A}}
",
)
.stdout("1\n")
.stderr("echo 1\n")
.run();
}
#[test]
fn invalid_escape_sequence_message() {
Test::new()
.justfile(
r#"
X := "\'"
"#,
)
.stderr(
r#"error: `\'` is not a valid escape sequence
——▶ justfile:1:6
1 │ X := "\'"
│ ^^^^
"#,
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_variable_in_default() {
Test::new()
.justfile(
"
foo x=bar:
",
)
.stderr(
r"error: Variable `bar` not defined
——▶ justfile:1:7
1 │ foo x=bar:
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_function_in_default() {
Test::new()
.justfile(
"
foo x=bar():
",
)
.stderr(
r"error: Call to unknown function `bar`
——▶ justfile:1:7
1 │ foo x=bar():
│ ^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn default_string() {
Test::new()
.justfile(
"
foo x='bar':
echo {{x}}
",
)
.stdout("bar\n")
.stderr("echo bar\n")
.run();
}
#[test]
fn default_concatenation() {
Test::new()
.justfile(
"
foo x=(`echo foo` + 'bar'):
echo {{x}}
",
)
.stdout("foobar\n")
.stderr("echo foobar\n")
.run();
}
#[test]
fn default_backtick() {
Test::new()
.justfile(
"
foo x=`echo foo`:
echo {{x}}
",
)
.stdout("foo\n")
.stderr("echo foo\n")
.run();
}
#[test]
fn default_variable() {
Test::new()
.justfile(
"
y := 'foo'
foo x=y:
echo {{x}}
",
)
.stdout("foo\n")
.stderr("echo foo\n")
.run();
}
#[test]
fn unterminated_interpolation_eol() {
Test::new()
.justfile(
"
foo:
echo {{
",
)
.stderr(
r"
error: Unterminated interpolation
——▶ justfile:2:8
2 │ echo {{
│ ^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unterminated_interpolation_eof() {
Test::new()
.justfile(
"
foo:
echo {{
",
)
.stderr(
r"
error: Unterminated interpolation
——▶ justfile:2:8
2 │ echo {{
│ ^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_start_of_token() {
Test::new()
.justfile(
"
assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
",
)
.stderr(
r"
error: Unknown start of token '%'
——▶ justfile:1:25
1 │ assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_start_of_token_invisible_unicode() {
Test::new()
.justfile(
"
\u{200b}foo := 'bar'
",
)
.stderr(
"
error: Unknown start of token '\u{200b}' (U+200B)
——▶ justfile:1:1
1 │ \u{200b}foo := 'bar'
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn unknown_start_of_token_ascii_control_char() {
Test::new()
.justfile(
"
\0foo := 'bar'
",
)
.stderr(
"
error: Unknown start of token '\0' (U+0000)
——▶ justfile:1:1
1 │ \0foo := 'bar'
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn backtick_variable_cat() {
Test::new()
.justfile(
"
stdin := `cat`
default:
echo {{stdin}}
",
)
.stdin("STDIN")
.stdout("STDIN\n")
.stderr("echo STDIN\n")
.run();
}
#[test]
fn backtick_default_cat_stdin() {
Test::new()
.justfile(
"
default stdin = `cat`:
echo {{stdin}}
",
)
.stdin("STDIN")
.stdout("STDIN\n")
.stderr("echo STDIN\n")
.run();
}
#[test]
fn backtick_default_cat_justfile() {
Test::new()
.justfile(
"
default stdin = `cat justfile`:
echo '{{stdin}}'
",
)
.stdout(
"
default stdin = `cat justfile`:
echo {{stdin}}
",
)
.stderr(
"
echo 'default stdin = `cat justfile`:
echo '{{stdin}}''
",
)
.run();
}
#[test]
fn backtick_variable_read_single() {
Test::new()
.justfile(
"
password := `read PW && echo $PW`
default:
echo {{password}}
",
)
.stdin("foobar\n")
.stdout("foobar\n")
.stderr("echo foobar\n")
.run();
}
#[test]
fn backtick_variable_read_multiple() {
Test::new()
.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")
.run();
}
#[test]
fn backtick_default_read_multiple() {
Test::new()
.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")
.run();
}
#[test]
fn old_equals_assignment_syntax_produces_error() {
Test::new()
.justfile(
"
foo = 'bar'
default:
echo {{foo}}
",
)
.stderr(
"
error: Expected '*', ':', '$', identifier, or '+', but found '='
——▶ justfile:1:5
1 │ foo = 'bar'
│ ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn dependency_argument_string() {
Test::new()
.justfile(
"
release: (build 'foo') (build 'bar')
build target:
echo 'Building {{target}}...'
",
)
.stdout("Building foo...\nBuilding bar...\n")
.stderr("echo 'Building foo...'\necho 'Building bar...'\n")
.shell(false)
.run();
}
#[test]
fn dependency_argument_parameter() {
Test::new()
.justfile(
"
default: (release '1.0')
release version: (build 'foo' version) (build 'bar' version)
build target version:
echo 'Building {{target}}@{{version}}...'
",
)
.stdout("Building foo@1.0...\nBuilding bar@1.0...\n")
.stderr("echo 'Building foo@1.0...'\necho 'Building bar@1.0...'\n")
.shell(false)
.run();
}
#[test]
fn dependency_argument_function() {
Test::new()
.justfile(
"
foo: (bar env_var_or_default('x', 'y'))
bar arg:
echo {{arg}}
",
)
.stdout("y\n")
.stderr("echo y\n")
.shell(false)
.run();
}
#[test]
fn env_function_as_env_var() {
Test::new()
.env("x", "z")
.justfile(
"
foo: (bar env('x'))
bar arg:
echo {{arg}}
",
)
.stdout("z\n")
.stderr("echo z\n")
.shell(false)
.run();
}
#[test]
fn env_function_as_env_var_or_default() {
Test::new()
.env("x", "z")
.justfile(
"
foo: (bar env('x', 'y'))
bar arg:
echo {{arg}}
",
)
.stdout("z\n")
.stderr("echo z\n")
.shell(false)
.run();
}
#[test]
fn env_function_as_env_var_with_existing_env_var() {
Test::new()
.env("x", "z")
.justfile(
"
foo: (bar env('x'))
bar arg:
echo {{arg}}
",
)
.stdout("z\n")
.stderr("echo z\n")
.shell(false)
.run();
}
#[test]
fn env_function_as_env_var_or_default_with_existing_env_var() {
Test::new()
.env("x", "z")
.justfile(
"
foo: (bar env('x', 'y'))
bar arg:
echo {{arg}}
",
)
.stdout("z\n")
.stderr("echo z\n")
.shell(false)
.run();
}
#[test]
fn dependency_argument_backtick() {
Test::new()
.justfile(
"
export X := 'X'
foo: (bar `echo $X`)
bar arg:
echo {{arg}}
echo $X
",
)
.stdout("X\nX\n")
.stderr("echo X\necho $X\n")
.shell(false)
.run();
}
#[test]
fn dependency_argument_assignment() {
Test::new()
.justfile(
"
v := '1.0'
default: (release v)
release version:
echo Release {{version}}...
",
)
.stdout("Release 1.0...\n")
.stderr("echo Release 1.0...\n")
.shell(false)
.run();
}
#[test]
fn dependency_argument_plus_variadic() {
Test::new()
.justfile(
"
foo: (bar 'A' 'B' 'C')
bar +args:
echo {{args}}
",
)
.stdout("A B C\n")
.stderr("echo A B C\n")
.shell(false)
.run();
}
#[test]
fn duplicate_dependency_no_args() {
Test::new()
.justfile(
"
foo: bar bar bar bar
bar:
echo BAR
",
)
.stdout("BAR\n")
.stderr("echo BAR\n")
.shell(false)
.run();
}
#[test]
fn duplicate_dependency_argument() {
Test::new()
.justfile(
"
foo: (bar 'BAR') (bar `echo BAR`)
bar bar:
echo {{bar}}
",
)
.stdout("BAR\n")
.stderr("echo BAR\n")
.shell(false)
.run();
}
#[cfg(windows)]
#[test]
fn pwsh_invocation_directory() {
Test::new()
.justfile(
r#"
set shell := ["pwsh", "-NoProfile", "-c"]
pwd:
@Test-Path {{invocation_directory()}} > result.txt
"#,
)
.status(EXIT_SUCCESS)
.shell(false)
.run();
}
#[test]
fn variables() {
Test::new()
.arg("--variables")
.justfile(
"
z := 'a'
a := 'z'
",
)
.stdout("a z\n")
.shell(false)
.run();
}
#[test]
fn interpolation_evaluation_ignore_quiet() {
Test::new()
.justfile(
r#"
foo:
{{"@echo foo 2>/dev/null"}}
"#,
)
.stderr(
"
@echo foo 2>/dev/null
error: Recipe `foo` failed on line 2 with exit code 127
",
)
.status(127)
.shell(false)
.run();
}
#[test]
fn interpolation_evaluation_ignore_quiet_continuation() {
Test::new()
.justfile(
r#"
foo:
{{""}}\
@echo foo 2>/dev/null
"#,
)
.stderr(
"
@echo foo 2>/dev/null
error: Recipe `foo` failed on line 3 with exit code 127
",
)
.status(127)
.shell(false)
.run();
}
#[test]
fn brace_escape() {
Test::new()
.justfile(
"
foo:
echo '{{{{'
",
)
.stdout("{{\n")
.stderr(
"
echo '{{'
",
)
.run();
}
#[test]
fn brace_escape_extra() {
Test::new()
.justfile(
"
foo:
echo '{{{{{'
",
)
.stdout("{{{\n")
.stderr(
"
echo '{{{'
",
)
.run();
}
#[test]
fn multi_line_string_in_interpolation() {
Test::new()
.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")
.run();
}
#[cfg(windows)]
#[test]
fn windows_interpreter_path_no_base() {
Test::new()
.justfile(
r#"
foo:
#!powershell
exit 0
"#,
)
.run();
}