use super::*; #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct Alias<'a> { attributes: Vec<&'a str>, name: &'a str, target: &'a str, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct Assignment<'a> { export: bool, name: &'a str, private: bool, value: &'a str, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct Dependency<'a> { arguments: Vec, recipe: &'a str, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct Interpreter<'a> { arguments: Vec<&'a str>, command: &'a str, } #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct Module<'a> { aliases: BTreeMap<&'a str, Alias<'a>>, assignments: BTreeMap<&'a str, Assignment<'a>>, doc: Option<&'a str>, first: Option<&'a str>, groups: Vec<&'a str>, modules: BTreeMap<&'a str, Module<'a>>, recipes: BTreeMap<&'a str, Recipe<'a>>, settings: Settings<'a>, source: PathBuf, unexports: Vec<&'a str>, warnings: Vec<&'a str>, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct Parameter<'a> { default: Option<&'a str>, export: bool, kind: &'a str, name: &'a str, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct Recipe<'a> { attributes: Vec, body: Vec, dependencies: Vec>, doc: Option<&'a str>, name: &'a str, namepath: &'a str, parameters: Vec>, priors: u32, private: bool, quiet: bool, shebang: bool, } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct Settings<'a> { allow_duplicate_recipes: bool, allow_duplicate_variables: bool, dotenv_filename: Option<&'a str>, dotenv_load: bool, dotenv_override: bool, dotenv_path: Option<&'a str>, dotenv_required: bool, export: bool, fallback: bool, ignore_comments: bool, no_exit_message: bool, positional_arguments: bool, quiet: bool, shell: Option>, tempdir: Option<&'a str>, unstable: bool, windows_powershell: bool, windows_shell: Option<&'a str>, working_directory: Option<&'a str>, } #[track_caller] fn case(justfile: &str, expected: Module) { case_with_submodule(justfile, None, expected); } fn fix_source(dir: &Path, module: &mut Module) { let filename = if module.source.as_os_str().is_empty() { Path::new("justfile") } else { &module.source }; module.source = if cfg!(target_os = "macos") { dir.canonicalize().unwrap().join(filename) } else { dir.join(filename) }; for module in module.modules.values_mut() { fix_source(dir, module); } } #[track_caller] fn case_with_submodule(justfile: &str, submodule: Option<(&str, &str)>, mut expected: Module) { let mut test = Test::new() .justfile(justfile) .args(["--dump", "--dump-format", "json"]) .stdout_regex(".*"); if let Some((path, source)) = submodule { test = test.write(path, source); } fix_source(test.tempdir.path(), &mut expected); let actual = test.run().stdout; let actual: Module = serde_json::from_str(actual.as_str()).unwrap(); pretty_assertions::assert_eq!(actual, expected); } #[test] fn alias() { case( " alias f := foo foo: ", Module { aliases: [( "f", Alias { name: "f", target: "foo", ..default() }, )] .into(), first: Some("foo"), recipes: [( "foo", Recipe { name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); } #[test] fn assignment() { case( "foo := 'bar'", Module { assignments: [( "foo", Assignment { name: "foo", value: "bar", ..default() }, )] .into(), ..default() }, ); } #[test] fn private_assignment() { case( " _foo := 'foo' [private] bar := 'bar' ", Module { assignments: [ ( "_foo", Assignment { name: "_foo", value: "foo", private: true, ..default() }, ), ( "bar", Assignment { name: "bar", value: "bar", private: true, ..default() }, ), ] .into(), ..default() }, ); } #[test] fn body() { case( " foo: bar abc{{ 'xyz' }}def ", Module { first: Some("foo"), recipes: [( "foo", Recipe { name: "foo", namepath: "foo", body: [json!(["bar"]), json!(["abc", ["xyz"], "def"])].into(), ..default() }, )] .into(), ..default() }, ); } #[test] fn dependencies() { case( " foo: bar: foo ", Module { first: Some("foo"), recipes: [ ( "foo", Recipe { name: "foo", namepath: "foo", ..default() }, ), ( "bar", Recipe { name: "bar", namepath: "bar", dependencies: [Dependency { recipe: "foo", ..default() }] .into(), priors: 1, ..default() }, ), ] .into(), ..default() }, ); } #[test] fn dependency_argument() { case( " x := 'foo' foo *args: bar: ( foo 'baz' ('baz') ('a' + 'b') `echo` x if 'a' == 'b' { 'c' } else { 'd' } arch() env_var('foo') join('a', 'b') replace('a', 'b', 'c') ) ", Module { assignments: [( "x", Assignment { name: "x", value: "foo", ..default() }, )] .into(), first: Some("foo"), recipes: [ ( "foo", Recipe { name: "foo", namepath: "foo", parameters: [Parameter { kind: "star", name: "args", ..default() }] .into(), ..default() }, ), ( "bar", Recipe { name: "bar", namepath: "bar", dependencies: [Dependency { recipe: "foo", arguments: [ json!("baz"), json!("baz"), json!(["concatenate", "a", "b"]), json!(["evaluate", "echo"]), json!(["variable", "x"]), json!(["if", ["==", "a", "b"], "c", "d"]), json!(["call", "arch"]), json!(["call", "env_var", "foo"]), json!(["call", "join", "a", "b"]), json!(["call", "replace", "a", "b", "c"]), ] .into(), }] .into(), priors: 1, ..default() }, ), ] .into(), ..default() }, ); } #[test] fn duplicate_recipes() { case( " set allow-duplicate-recipes alias f := foo foo: foo bar: ", Module { aliases: [( "f", Alias { name: "f", target: "foo", ..default() }, )] .into(), first: Some("foo"), recipes: [( "foo", Recipe { name: "foo", namepath: "foo", parameters: [Parameter { kind: "singular", name: "bar", ..default() }] .into(), ..default() }, )] .into(), settings: Settings { allow_duplicate_recipes: true, ..default() }, ..default() }, ); } #[test] fn duplicate_variables() { case( " set allow-duplicate-variables x := 'foo' x := 'bar' ", Module { assignments: [( "x", Assignment { name: "x", value: "bar", ..default() }, )] .into(), settings: Settings { allow_duplicate_variables: true, ..default() }, ..default() }, ); } #[test] fn doc_comment() { case( "# hello\nfoo:", Module { first: Some("foo"), recipes: [( "foo", Recipe { doc: Some("hello"), name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); } #[test] fn empty_justfile() { case("", Module::default()); } #[test] fn parameters() { case( " a: b x: c x='y': d +x: e *x: f $x: ", Module { first: Some("a"), recipes: [ ( "a", Recipe { name: "a", namepath: "a", ..default() }, ), ( "b", Recipe { name: "b", namepath: "b", parameters: [Parameter { kind: "singular", name: "x", ..default() }] .into(), ..default() }, ), ( "c", Recipe { name: "c", namepath: "c", parameters: [Parameter { default: Some("y"), kind: "singular", name: "x", ..default() }] .into(), ..default() }, ), ( "d", Recipe { name: "d", namepath: "d", parameters: [Parameter { kind: "plus", name: "x", ..default() }] .into(), ..default() }, ), ( "e", Recipe { name: "e", namepath: "e", parameters: [Parameter { kind: "star", name: "x", ..default() }] .into(), ..default() }, ), ( "f", Recipe { name: "f", namepath: "f", parameters: [Parameter { export: true, kind: "singular", name: "x", ..default() }] .into(), ..default() }, ), ] .into(), ..default() }, ); } #[test] fn priors() { case( " a: b: a && c c: ", Module { first: Some("a"), recipes: [ ( "a", Recipe { name: "a", namepath: "a", ..default() }, ), ( "b", Recipe { dependencies: [ Dependency { recipe: "a", ..default() }, Dependency { recipe: "c", ..default() }, ] .into(), name: "b", namepath: "b", priors: 1, ..default() }, ), ( "c", Recipe { name: "c", namepath: "c", ..default() }, ), ] .into(), ..default() }, ); } #[test] fn private() { case( "_foo:", Module { first: Some("_foo"), recipes: [( "_foo", Recipe { name: "_foo", namepath: "_foo", private: true, ..default() }, )] .into(), ..default() }, ); } #[test] fn quiet() { case( "@foo:", Module { first: Some("foo"), recipes: [( "foo", Recipe { name: "foo", namepath: "foo", quiet: true, ..default() }, )] .into(), ..default() }, ); } #[test] fn settings() { case( " set allow-duplicate-recipes set dotenv-filename := \"filename\" set dotenv-load set dotenv-path := \"path\" set export set fallback set ignore-comments set positional-arguments set quiet set shell := ['a', 'b', 'c'] foo: #!bar ", Module { first: Some("foo"), recipes: [( "foo", Recipe { name: "foo", namepath: "foo", shebang: true, body: [json!(["#!bar"])].into(), ..default() }, )] .into(), settings: Settings { allow_duplicate_recipes: true, dotenv_filename: Some("filename"), dotenv_path: Some("path"), dotenv_load: true, export: true, fallback: true, ignore_comments: true, positional_arguments: true, quiet: true, shell: Some(Interpreter { arguments: ["b", "c"].into(), command: "a", }), ..default() }, ..default() }, ); } #[test] fn shebang() { case( " foo: #!bar ", Module { first: Some("foo"), recipes: [( "foo", Recipe { name: "foo", namepath: "foo", shebang: true, body: [json!(["#!bar"])].into(), ..default() }, )] .into(), ..default() }, ); } #[test] fn simple() { case( "foo:", Module { first: Some("foo"), recipes: [( "foo", Recipe { name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); } #[test] fn attribute() { case( " [no-exit-message] foo: ", Module { first: Some("foo"), recipes: [( "foo", Recipe { attributes: [json!("no-exit-message")].into(), name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); } #[test] fn single_metadata_attribute() { case( " [metadata('example')] foo: ", Module { first: Some("foo"), recipes: [( "foo", Recipe { attributes: [json!({"metadata": ["example"]})].into(), name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); } #[test] fn multiple_metadata_attributes() { case( " [metadata('example')] [metadata('sample')] foo: ", Module { first: Some("foo"), recipes: [( "foo", Recipe { attributes: [ json!({"metadata": ["example"]}), json!({"metadata": ["sample"]}), ] .into(), name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); } #[test] fn multiple_metadata_attributes_with_multiple_arguments() { case( " [metadata('example', 'arg1')] [metadata('sample', 'argument')] foo: ", Module { first: Some("foo"), recipes: [( "foo", Recipe { attributes: [ json!({"metadata": ["example", "arg1"]}), json!({"metadata": ["sample", "argument"]}), ] .into(), name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); } #[test] fn module() { case_with_submodule( " # hello mod foo ", Some(("foo.just", "bar:")), Module { modules: [( "foo", Module { doc: Some("hello"), first: Some("bar"), source: "foo.just".into(), recipes: [( "bar", Recipe { name: "bar", namepath: "foo::bar", ..default() }, )] .into(), ..default() }, )] .into(), ..default() }, ); } #[test] fn module_group() { case_with_submodule( " [group('alpha')] mod foo ", Some(("foo.just", "bar:")), Module { modules: [( "foo", Module { first: Some("bar"), groups: ["alpha"].into(), source: "foo.just".into(), recipes: [( "bar", Recipe { name: "bar", namepath: "foo::bar", ..default() }, )] .into(), ..default() }, )] .into(), ..default() }, ); } #[test] fn recipes_with_private_attribute_are_private() { case( " [private] foo: ", Module { first: Some("foo"), recipes: [( "foo", Recipe { attributes: [json!("private")].into(), name: "foo", namepath: "foo", private: true, ..default() }, )] .into(), ..default() }, ); } #[test] fn doc_attribute_overrides_comment() { case( " # COMMENT [doc('ATTRIBUTE')] foo: ", Module { first: Some("foo"), recipes: [( "foo", Recipe { attributes: [json!({"doc": "ATTRIBUTE"})].into(), doc: Some("ATTRIBUTE"), name: "foo", namepath: "foo", ..default() }, )] .into(), ..default() }, ); }