From 2874a69faf16923df3f04b5cacda3758f2a8bbdf Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Thu, 26 Oct 2023 16:41:29 +0900 Subject: [PATCH] tests: migrate most tests from test_templater.rs test_templater_alias(), test_templater_alias_override(), and test_templater_bad_alias_decl() aren't moved since they also test config loading and error formatting. The first test in test_templater_parse_error() is left for the same reason. test_templater_upper_lower() depends on the commit templater. --- cli/src/template_builder.rs | 660 ++++++++++++++++++++++++++++++++++++ cli/tests/test_templater.rs | 619 --------------------------------- 2 files changed, 660 insertions(+), 619 deletions(-) diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index e2b624ec5..663c72a86 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -940,6 +940,14 @@ mod tests { } } + fn new_signature(name: &str, email: &str) -> Signature { + Signature { + name: name.to_owned(), + email: email.to_owned(), + timestamp: new_timestamp(0, 0), + } + } + fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp { Timestamp { timestamp: MillisSinceEpoch(msec), @@ -947,6 +955,436 @@ mod tests { } } + #[test] + fn test_parsed_tree() { + let mut env = TestTemplateEnv::default(); + env.add_keyword("divergent", |language| { + language.wrap_boolean(Literal(false)) + }); + env.add_keyword("empty", |language| language.wrap_boolean(Literal(true))); + env.add_keyword("hello", |language| { + language.wrap_string(Literal("Hello".to_owned())) + }); + + // Empty + insta::assert_snapshot!(env.render_ok(r#" "#), @""); + + // Single term with whitespace + insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO"); + + // Multiple terms + insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue"); + + // Parenthesized single term + insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO"); + + // Parenthesized multiple terms and concatenation + insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true"); + + // Parenthesized "if" condition + insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f"); + + // Parenthesized method chaining + insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO"); + } + + #[test] + fn test_parse_error() { + let mut env = TestTemplateEnv::default(); + env.add_keyword("description", |language| { + language.wrap_string(Literal("".to_owned())) + }); + env.add_keyword("empty", |language| language.wrap_boolean(Literal(true))); + + insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r###" + --> 1:13 + | + 1 | description () + | ^--- + | + = expected EOI + "###); + + insta::assert_snapshot!(env.parse_err(r#"foo"#), @r###" + --> 1:1 + | + 1 | foo + | ^-^ + | + = Keyword "foo" doesn't exist + "###); + + insta::assert_snapshot!(env.parse_err(r#"foo()"#), @r###" + --> 1:1 + | + 1 | foo() + | ^-^ + | + = Function "foo" doesn't exist + "###); + insta::assert_snapshot!(env.parse_err(r#"false()"#), @r###" + --> 1:1 + | + 1 | false() + | ^---^ + | + = Expected identifier + "###); + + insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @r###" + --> 1:26 + | + 1 | description.first_line().foo() + | ^-^ + | + = Method "foo" doesn't exist for type "String" + "###); + + insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @r###" + --> 1:1 + | + 1 | 10000000000000000000 + | ^------------------^ + | + = Invalid integer literal: number too large to fit in target type + "###); + insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @r###" + --> 1:4 + | + 1 | 42.foo() + | ^-^ + | + = Method "foo" doesn't exist for type "Integer" + "###); + insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @r###" + --> 1:2 + | + 1 | (-empty) + | ^--- + | + = expected template + "###); + + insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r###" + --> 1:18 + | + 1 | ("foo" ++ "bar").baz() + | ^-^ + | + = Method "baz" doesn't exist for type "Template" + "###); + + insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @r###" + --> 1:22 + | + 1 | description.contains() + | ^ + | + = Function "contains": Expected 1 arguments + "###); + + insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r###" + --> 1:24 + | + 1 | description.first_line("foo") + | ^---^ + | + = Function "first_line": Expected 0 arguments + "###); + + insta::assert_snapshot!(env.parse_err(r#"label()"#), @r###" + --> 1:7 + | + 1 | label() + | ^ + | + = Function "label": Expected 2 arguments + "###); + insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r###" + --> 1:7 + | + 1 | label("foo", "bar", "baz") + | ^-----------------^ + | + = Function "label": Expected 2 arguments + "###); + + insta::assert_snapshot!(env.parse_err(r#"if()"#), @r###" + --> 1:4 + | + 1 | if() + | ^ + | + = Function "if": Expected 2 to 3 arguments + "###); + insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r###" + --> 1:4 + | + 1 | if("foo", "bar", "baz", "quux") + | ^-------------------------^ + | + = Function "if": Expected 2 to 3 arguments + "###); + + insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r###" + --> 1:4 + | + 1 | if(label("foo", "bar"), "baz") + | ^-----------------^ + | + = Expected expression of type "Boolean" + "###); + + insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @r###" + --> 1:1 + | + 1 | |x| description + | ^-------------^ + | + = Lambda cannot be defined here + "###); + } + + #[test] + fn test_list_method() { + let mut env = TestTemplateEnv::default(); + env.add_keyword("empty", |language| language.wrap_boolean(Literal(true))); + env.add_keyword("sep", |language| { + language.wrap_string(Literal("sep".to_owned())) + }); + + insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @""); + insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c"); + // Null separator + insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c"); + // Keyword as separator + insta::assert_snapshot!( + env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#), + @"aSEPbSEPc"); + + insta::assert_snapshot!( + env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#), + @"aa bb cc"); + // Global keyword in item template + insta::assert_snapshot!( + env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#), + @"atrue btrue ctrue"); + // Override global keyword 'empty' + insta::assert_snapshot!( + env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#), + @"a b c"); + // Nested map operations + insta::assert_snapshot!( + env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#), + @"ax ay bx by cx cy"); + // Nested map/join operations + insta::assert_snapshot!( + env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#), + @"ax,ay;bx,by;cx,cy"); + // Nested string operations + insta::assert_snapshot!( + env.render_ok(r#""!a\n!b\nc\nend".remove_suffix("end").lines().map(|s| s.remove_prefix("!"))"#), + @"a b c"); + + // Lambda expression in alias + env.add_alias("identity", "|x| x"); + insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c"); + + // Not a lambda expression + insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r###" + --> 1:17 + | + 1 | "a".lines().map(empty) + | ^---^ + | + = Expected lambda expression + "###); + // Bad lambda parameter count + insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r###" + --> 1:18 + | + 1 | "a".lines().map(|| "") + | ^ + | + = Expected 1 lambda parameters + "###); + insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r###" + --> 1:18 + | + 1 | "a".lines().map(|a, b| "") + | ^--^ + | + = Expected 1 lambda parameters + "###); + // Error in lambda expression + insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r###" + --> 1:23 + | + 1 | "a".lines().map(|s| s.unknown()) + | ^-----^ + | + = Method "unknown" doesn't exist for type "String" + "###); + // Error in lambda alias + env.add_alias("too_many_params", "|x, y| x"); + insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r###" + --> 1:17 + | + 1 | "a".lines().map(too_many_params) + | ^-------------^ + | + = Alias "too_many_params" cannot be expanded + --> 1:2 + | + 1 | |x, y| x + | ^--^ + | + = Expected 1 lambda parameters + "###); + } + + #[test] + fn test_string_method() { + let mut env = TestTemplateEnv::default(); + env.add_keyword("description", |language| { + language.wrap_string(Literal("description 1".to_owned())) + }); + + insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false"); + insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true"); + insta::assert_snapshot!( + env.render_ok(r#""description 123".contains(description.first_line())"#), + @"true"); + + insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @""); + insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo"); + + insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @""); + insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c"); + + insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false"); + insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false"); + + insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false"); + insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false"); + insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true"); + + insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @""); + insta::assert_snapshot!( + env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#), + @"testing"); + + insta::assert_snapshot!( + env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#), + @"bar@my.example.com"); + insta::assert_snapshot!( + env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#), + @"bar"); + + insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @""); + insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f"); + insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 99)"#), @"foo"); + insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde"); + insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def"); + insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c"); + insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 99)"#), @"💩"); + + // ranges with end > start are empty + insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @""); + insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @""); + } + + #[test] + fn test_signature() { + let mut env = TestTemplateEnv::default(); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature("Test User", "test.user@example.com"))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User "); + insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User"); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com"); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user"); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature( + "Another Test User", + "test.user@example.com", + ))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User "); + insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User"); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com"); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user"); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature( + "Test User", + "test.user@invalid@example.com", + ))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User "); + insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User"); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com"); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user"); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature("Test User", "test.user"))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User "); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user"); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user"); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature( + "Test User", + "test.user+tag@example.com", + ))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User "); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com"); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user+tag"); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature("Test User", "x@y"))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User "); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y"); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"x"); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature("", "test.user@example.com"))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @""); + insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @""); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com"); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"test.user"); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature("Test User", ""))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User"); + insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User"); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @""); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @""); + + env.add_keyword("author", |language| { + language.wrap_signature(Literal(new_signature("", ""))) + }); + insta::assert_snapshot!(env.render_ok(r#"author"#), @""); + insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @""); + insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @""); + insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @""); + } + #[test] fn test_timestamp_method() { let mut env = TestTemplateEnv::default(); @@ -1008,6 +1446,152 @@ mod tests { "###); } + #[test] + fn test_fill_function() { + let mut env = TestTemplateEnv::default(); + env.add_color("error", crossterm::style::Color::DarkRed); + + insta::assert_snapshot!( + env.render_ok(r#"fill(20, "The quick fox jumps over the " ++ + label("error", "lazy") ++ " dog\n")"#), + @r###" + The quick fox jumps + over the lazy dog + "###); + + // A low value will not chop words, but can chop a label by words + insta::assert_snapshot!( + env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++ + label("error", "longlonglongword and short words") ++ + " back out\n")"#), + @r###" + Longlonglongword + an some + short + words + longlonglongword + and short + words + back out + "###); + + // Filling to 0 means breaking at every word + insta::assert_snapshot!( + env.render_ok(r#"fill(0, "The quick fox jumps over the " ++ + label("error", "lazy") ++ " dog\n")"#), + @r###" + The + quick + fox + jumps + over + the + lazy + dog + "###); + + // Filling to -0 is the same as 0 + insta::assert_snapshot!( + env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++ + label("error", "lazy") ++ " dog\n")"#), + @r###" + The + quick + fox + jumps + over + the + lazy + dog + "###); + + // Filling to negatives are clamped to the same as zero + insta::assert_snapshot!( + env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++ + label("error", "lazy") ++ " dog\n")"#), + @r###" + The + quick + fox + jumps + over + the + lazy + dog + "###); + + // Word-wrap, then indent + insta::assert_snapshot!( + env.render_ok(r#""START marker to help insta\n" ++ + indent(" ", fill(20, "The quick fox jumps over the " ++ + label("error", "lazy") ++ " dog\n"))"#), + @r###" + START marker to help insta + The quick fox jumps + over the lazy dog + "###); + + // Word-wrap indented (no special handling for leading spaces) + insta::assert_snapshot!( + env.render_ok(r#""START marker to help insta\n" ++ + fill(20, indent(" ", "The quick fox jumps over the " ++ + label("error", "lazy") ++ " dog\n"))"#), + @r###" + START marker to help insta + The quick fox + jumps over the lazy + dog + "###); + } + + #[test] + fn test_indent_function() { + let mut env = TestTemplateEnv::default(); + env.add_color("error", crossterm::style::Color::DarkRed); + env.add_color("warning", crossterm::style::Color::DarkYellow); + env.add_color("hint", crossterm::style::Color::DarkCyan); + + // Empty line shouldn't be indented. Not using insta here because we test + // whitespace existence. + assert_eq!(env.render_ok(r#"indent("__", "")"#), ""); + assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n"); + assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b"); + + // "\n" at end of labeled text + insta::assert_snapshot!( + env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#), + @r###" + __a + __b + "###); + + // "\n" in labeled text + insta::assert_snapshot!( + env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#), + @r###" + __ab + __c + "###); + + // Labeled prefix + unlabeled content + insta::assert_snapshot!( + env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#), + @r###" + XXa + XXb + "###); + + // Nested indent, silly but works + insta::assert_snapshot!( + env.render_ok(r#"indent(label("hint", "A"), + label("warning", indent(label("hint", "B"), + label("error", "x\n") ++ "y")))"#), + @r###" + ABx + ABy + "###); + } + #[test] fn test_label_function() { let mut env = TestTemplateEnv::default(); @@ -1030,4 +1614,80 @@ mod tests { env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#), @"text"); } + + #[test] + fn test_concat_function() { + let mut env = TestTemplateEnv::default(); + env.add_keyword("empty", |language| language.wrap_boolean(Literal(true))); + env.add_keyword("hidden", |language| language.wrap_boolean(Literal(false))); + env.add_color("empty", crossterm::style::Color::DarkGreen); + env.add_color("error", crossterm::style::Color::DarkRed); + env.add_color("warning", crossterm::style::Color::DarkYellow); + + insta::assert_snapshot!(env.render_ok(r#"concat()"#), @""); + insta::assert_snapshot!( + env.render_ok(r#"concat(hidden, empty)"#), + @"falsetrue"); + insta::assert_snapshot!( + env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#), + @"ab"); + } + + #[test] + fn test_separate_function() { + let mut env = TestTemplateEnv::default(); + env.add_keyword("description", |language| { + language.wrap_string(Literal("".to_owned())) + }); + env.add_keyword("empty", |language| language.wrap_boolean(Literal(true))); + env.add_keyword("hidden", |language| language.wrap_boolean(Literal(false))); + env.add_color("empty", crossterm::style::Color::DarkGreen); + env.add_color("error", crossterm::style::Color::DarkRed); + env.add_color("warning", crossterm::style::Color::DarkYellow); + + insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @""); + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @""); + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a"); + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b"); + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b"); + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b"); + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b"); + + // Labeled + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#), + @"a b"); + + // List template + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a"); + insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b"); + + // Nested separate + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a"); + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b"); + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c"); + + // Conditional template + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a"); + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a"); + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a"); + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t"); + + // Separate keywords + insta::assert_snapshot!( + env.render_ok(r#"separate(" ", hidden, description, empty)"#), + @"false true"); + + // Keyword as separator + insta::assert_snapshot!( + env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#), + @"XfalseYfalseZ"); + } } diff --git a/cli/tests/test_templater.rs b/cli/tests/test_templater.rs index 3a161ab77..23916039e 100644 --- a/cli/tests/test_templater.rs +++ b/cli/tests/test_templater.rs @@ -18,35 +18,6 @@ use crate::common::TestEnvironment; pub mod common; -#[test] -fn test_templater_parsed_tree() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let render = |template| get_template_output(&test_env, &repo_path, "@-", template); - - // Empty - insta::assert_snapshot!(render(r#" "#), @""); - - // Single term with whitespace - insta::assert_snapshot!(render(r#" commit_id.short() "#), @"000000000000"); - - // Multiple terms - insta::assert_snapshot!(render(r#" commit_id.short() ++ empty "#), @"000000000000true"); - - // Parenthesized single term - insta::assert_snapshot!(render(r#"(commit_id.short())"#), @"000000000000"); - - // Parenthesized multiple terms and concatenation - insta::assert_snapshot!(render(r#"(commit_id.short() ++ " ") ++ empty"#), @"000000000000 true"); - - // Parenthesized "if" condition - insta::assert_snapshot!(render(r#"if((divergent), "t", "f")"#), @"f"); - - // Parenthesized method chaining - insta::assert_snapshot!(render(r#"(commit_id).short()"#), @"000000000000"); -} - #[test] fn test_templater_parse_error() { let test_env = TestEnvironment::default(); @@ -62,596 +33,6 @@ fn test_templater_parse_error() { | = expected EOI "###); - - insta::assert_snapshot!(render_err(r#"foo"#), @r###" - Error: Failed to parse template: --> 1:1 - | - 1 | foo - | ^-^ - | - = Keyword "foo" doesn't exist - "###); - - insta::assert_snapshot!(render_err(r#"foo()"#), @r###" - Error: Failed to parse template: --> 1:1 - | - 1 | foo() - | ^-^ - | - = Function "foo" doesn't exist - "###); - insta::assert_snapshot!(render_err(r#"false()"#), @r###" - Error: Failed to parse template: --> 1:1 - | - 1 | false() - | ^---^ - | - = Expected identifier - "###); - - insta::assert_snapshot!(render_err(r#"description.first_line().foo()"#), @r###" - Error: Failed to parse template: --> 1:26 - | - 1 | description.first_line().foo() - | ^-^ - | - = Method "foo" doesn't exist for type "String" - "###); - - insta::assert_snapshot!(render_err(r#"10000000000000000000"#), @r###" - Error: Failed to parse template: --> 1:1 - | - 1 | 10000000000000000000 - | ^------------------^ - | - = Invalid integer literal: number too large to fit in target type - "###); - insta::assert_snapshot!(render_err(r#"42.foo()"#), @r###" - Error: Failed to parse template: --> 1:4 - | - 1 | 42.foo() - | ^-^ - | - = Method "foo" doesn't exist for type "Integer" - "###); - insta::assert_snapshot!(render_err(r#"(-empty)"#), @r###" - Error: Failed to parse template: --> 1:2 - | - 1 | (-empty) - | ^--- - | - = expected template - "###); - - insta::assert_snapshot!(render_err(r#"("foo" ++ "bar").baz()"#), @r###" - Error: Failed to parse template: --> 1:18 - | - 1 | ("foo" ++ "bar").baz() - | ^-^ - | - = Method "baz" doesn't exist for type "Template" - "###); - - insta::assert_snapshot!(render_err(r#"description.contains()"#), @r###" - Error: Failed to parse template: --> 1:22 - | - 1 | description.contains() - | ^ - | - = Function "contains": Expected 1 arguments - "###); - - insta::assert_snapshot!(render_err(r#"description.first_line("foo")"#), @r###" - Error: Failed to parse template: --> 1:24 - | - 1 | description.first_line("foo") - | ^---^ - | - = Function "first_line": Expected 0 arguments - "###); - - insta::assert_snapshot!(render_err(r#"label()"#), @r###" - Error: Failed to parse template: --> 1:7 - | - 1 | label() - | ^ - | - = Function "label": Expected 2 arguments - "###); - insta::assert_snapshot!(render_err(r#"label("foo", "bar", "baz")"#), @r###" - Error: Failed to parse template: --> 1:7 - | - 1 | label("foo", "bar", "baz") - | ^-----------------^ - | - = Function "label": Expected 2 arguments - "###); - - insta::assert_snapshot!(render_err(r#"if()"#), @r###" - Error: Failed to parse template: --> 1:4 - | - 1 | if() - | ^ - | - = Function "if": Expected 2 to 3 arguments - "###); - insta::assert_snapshot!(render_err(r#"if("foo", "bar", "baz", "quux")"#), @r###" - Error: Failed to parse template: --> 1:4 - | - 1 | if("foo", "bar", "baz", "quux") - | ^-------------------------^ - | - = Function "if": Expected 2 to 3 arguments - "###); - - insta::assert_snapshot!(render_err(r#"if(label("foo", "bar"), "baz")"#), @r###" - Error: Failed to parse template: --> 1:4 - | - 1 | if(label("foo", "bar"), "baz") - | ^-----------------^ - | - = Expected expression of type "Boolean" - "###); - - insta::assert_snapshot!(render_err(r#"|x| description"#), @r###" - Error: Failed to parse template: --> 1:1 - | - 1 | |x| description - | ^-------------^ - | - = Lambda cannot be defined here - "###); -} - -#[test] -fn test_templater_list_method() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let render = |template| get_template_output(&test_env, &repo_path, "@-", template); - let render_err = |template| test_env.jj_cmd_failure(&repo_path, &["log", "-T", template]); - - test_env.add_config( - r###" - [template-aliases] - 'identity' = '|x| x' - 'too_many_params' = '|x, y| x' - "###, - ); - - insta::assert_snapshot!(render(r#""".lines().join("|")"#), @""); - insta::assert_snapshot!(render(r#""a\nb\nc".lines().join("|")"#), @"a|b|c"); - // Null separator - insta::assert_snapshot!(render(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c"); - // Keyword as separator - insta::assert_snapshot!(render(r#""a\nb\nc".lines().join(commit_id.short(2))"#), @"a00b00c"); - - insta::assert_snapshot!(render(r#""a\nb\nc".lines().map(|s| s ++ s)"#), @"aa bb cc"); - // Global keyword in item template - insta::assert_snapshot!( - render(r#""a\nb\nc".lines().map(|s| s ++ empty)"#), @"atrue btrue ctrue"); - // Override global keyword 'empty' - insta::assert_snapshot!( - render(r#""a\nb\nc".lines().map(|empty| empty)"#), @"a b c"); - // Nested map operations - insta::assert_snapshot!( - render(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#), - @"ax ay bx by cx cy"); - // Nested map/join operations - insta::assert_snapshot!( - render(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#), - @"ax,ay;bx,by;cx,cy"); - // Nested string operations - insta::assert_snapshot!(render(r#""!a\n!b\nc\nend".remove_suffix("end").lines().map(|s| s.remove_prefix("!"))"#), @"a b c"); - - // Lambda expression in alias - insta::assert_snapshot!(render(r#""a\nb\nc".lines().map(identity)"#), @"a b c"); - - // Not a lambda expression - insta::assert_snapshot!(render_err(r#""a".lines().map(empty)"#), @r###" - Error: Failed to parse template: --> 1:17 - | - 1 | "a".lines().map(empty) - | ^---^ - | - = Expected lambda expression - "###); - // Bad lambda parameter count - insta::assert_snapshot!(render_err(r#""a".lines().map(|| "")"#), @r###" - Error: Failed to parse template: --> 1:18 - | - 1 | "a".lines().map(|| "") - | ^ - | - = Expected 1 lambda parameters - "###); - insta::assert_snapshot!(render_err(r#""a".lines().map(|a, b| "")"#), @r###" - Error: Failed to parse template: --> 1:18 - | - 1 | "a".lines().map(|a, b| "") - | ^--^ - | - = Expected 1 lambda parameters - "###); - // Error in lambda expression - insta::assert_snapshot!(render_err(r#""a".lines().map(|s| s.unknown())"#), @r###" - Error: Failed to parse template: --> 1:23 - | - 1 | "a".lines().map(|s| s.unknown()) - | ^-----^ - | - = Method "unknown" doesn't exist for type "String" - "###); - // Error in lambda alias - insta::assert_snapshot!(render_err(r#""a".lines().map(too_many_params)"#), @r###" - Error: Failed to parse template: --> 1:17 - | - 1 | "a".lines().map(too_many_params) - | ^-------------^ - | - = Alias "too_many_params" cannot be expanded - --> 1:2 - | - 1 | |x, y| x - | ^--^ - | - = Expected 1 lambda parameters - "###); -} - -#[test] -fn test_templater_string_method() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - test_env.jj_cmd_ok(&repo_path, &["commit", "-m=description 1"]); - let render = |template| get_template_output(&test_env, &repo_path, "@-", template); - - insta::assert_snapshot!(render(r#""fooo".contains("foo")"#), @"true"); - insta::assert_snapshot!(render(r#""foo".contains("fooo")"#), @"false"); - insta::assert_snapshot!(render(r#"description.contains("description")"#), @"true"); - insta::assert_snapshot!( - render(r#""description 123".contains(description.first_line())"#), @"true"); - - insta::assert_snapshot!(render(r#""".first_line()"#), @""); - insta::assert_snapshot!(render(r#""foo\nbar".first_line()"#), @"foo"); - - insta::assert_snapshot!(render(r#""".lines()"#), @""); - insta::assert_snapshot!(render(r#""a\nb\nc\n".lines()"#), @"a b c"); - - insta::assert_snapshot!(render(r#""".starts_with("")"#), @"true"); - insta::assert_snapshot!(render(r#""everything".starts_with("")"#), @"true"); - insta::assert_snapshot!(render(r#""".starts_with("foo")"#), @"false"); - insta::assert_snapshot!(render(r#""foo".starts_with("foo")"#), @"true"); - insta::assert_snapshot!(render(r#""foobar".starts_with("foo")"#), @"true"); - insta::assert_snapshot!(render(r#""foobar".starts_with("bar")"#), @"false"); - - insta::assert_snapshot!(render(r#""".ends_with("")"#), @"true"); - insta::assert_snapshot!(render(r#""everything".ends_with("")"#), @"true"); - insta::assert_snapshot!(render(r#""".ends_with("foo")"#), @"false"); - insta::assert_snapshot!(render(r#""foo".ends_with("foo")"#), @"true"); - insta::assert_snapshot!(render(r#""foobar".ends_with("foo")"#), @"false"); - insta::assert_snapshot!(render(r#""foobar".ends_with("bar")"#), @"true"); - - insta::assert_snapshot!(render(r#""".remove_prefix("wip: ")"#), @""); - insta::assert_snapshot!(render(r#""wip: testing".remove_prefix("wip: ")"#), @"testing"); - - insta::assert_snapshot!(render(r#""bar@my.example.com".remove_suffix("@other.example.com")"#), @"bar@my.example.com"); - insta::assert_snapshot!(render(r#""bar@other.example.com".remove_suffix("@other.example.com")"#), @"bar"); - - insta::assert_snapshot!(render(r#""foo".substr(0, 0)"#), @""); - insta::assert_snapshot!(render(r#""foo".substr(0, 1)"#), @"f"); - insta::assert_snapshot!(render(r#""foo".substr(0, 99)"#), @"foo"); - insta::assert_snapshot!(render(r#""abcdef".substr(2, -1)"#), @"cde"); - insta::assert_snapshot!(render(r#""abcdef".substr(-3, 99)"#), @"def"); - insta::assert_snapshot!(render(r#""abc💩".substr(2, -1)"#), @"c"); - insta::assert_snapshot!(render(r#""abc💩".substr(-1, 99)"#), @"💩"); - - // ranges with end > start are empty - insta::assert_snapshot!(render(r#""abcdef".substr(4, 2)"#), @""); - insta::assert_snapshot!(render(r#""abcdef".substr(-2, -4)"#), @""); -} - -#[test] -fn test_templater_signature() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let render = |template| get_template_output(&test_env, &repo_path, "@", template); - - test_env.jj_cmd_ok(&repo_path, &["new"]); - - insta::assert_snapshot!(render(r#"author"#), @"Test User "); - insta::assert_snapshot!(render(r#"author.name()"#), @"Test User"); - insta::assert_snapshot!(render(r#"author.email()"#), @"test.user@example.com"); - insta::assert_snapshot!(render(r#"author.username()"#), @"test.user"); - - test_env.jj_cmd_ok( - &repo_path, - &["--config-toml=user.name='Another Test User'", "new"], - ); - - insta::assert_snapshot!(render(r#"author"#), @"Another Test User "); - insta::assert_snapshot!(render(r#"author.name()"#), @"Another Test User"); - insta::assert_snapshot!(render(r#"author.email()"#), @"test.user@example.com"); - insta::assert_snapshot!(render(r#"author.username()"#), @"test.user"); - - test_env.jj_cmd_ok( - &repo_path, - &[ - "--config-toml=user.email='test.user@invalid@example.com'", - "new", - ], - ); - - insta::assert_snapshot!(render(r#"author"#), @"Test User "); - insta::assert_snapshot!(render(r#"author.name()"#), @"Test User"); - insta::assert_snapshot!(render(r#"author.email()"#), @"test.user@invalid@example.com"); - insta::assert_snapshot!(render(r#"author.username()"#), @"test.user"); - - test_env.jj_cmd_ok(&repo_path, &["--config-toml=user.email='test.user'", "new"]); - - insta::assert_snapshot!(render(r#"author"#), @"Test User "); - insta::assert_snapshot!(render(r#"author.email()"#), @"test.user"); - insta::assert_snapshot!(render(r#"author.username()"#), @"test.user"); - - test_env.jj_cmd_ok( - &repo_path, - &[ - "--config-toml=user.email='test.user+tag@example.com'", - "new", - ], - ); - - insta::assert_snapshot!(render(r#"author"#), @"Test User "); - insta::assert_snapshot!(render(r#"author.email()"#), @"test.user+tag@example.com"); - insta::assert_snapshot!(render(r#"author.username()"#), @"test.user+tag"); - - test_env.jj_cmd_ok(&repo_path, &["--config-toml=user.email='x@y'", "new"]); - - insta::assert_snapshot!(render(r#"author"#), @"Test User "); - insta::assert_snapshot!(render(r#"author.email()"#), @"x@y"); - insta::assert_snapshot!(render(r#"author.username()"#), @"x"); - - test_env.jj_cmd_ok(&repo_path, &["--config-toml=user.name=''", "new"]); - - insta::assert_snapshot!(render(r#"author"#), @""); - insta::assert_snapshot!(render(r#"author.name()"#), @""); - insta::assert_snapshot!(render(r#"author.email()"#), @"test.user@example.com"); - insta::assert_snapshot!(render(r#"author.username()"#), @"test.user"); - - test_env.jj_cmd_ok(&repo_path, &["--config-toml=user.email=''", "new"]); - - insta::assert_snapshot!(render(r#"author"#), @"Test User"); - insta::assert_snapshot!(render(r#"author.name()"#), @"Test User"); - insta::assert_snapshot!(render(r#"author.email()"#), @""); - insta::assert_snapshot!(render(r#"author.username()"#), @""); - - test_env.jj_cmd_ok( - &repo_path, - &[ - "--config-toml=user.email=''", - "--config-toml=user.name=''", - "new", - ], - ); - - insta::assert_snapshot!(render(r#"author"#), @""); - insta::assert_snapshot!(render(r#"author.name()"#), @""); - insta::assert_snapshot!(render(r#"author.email()"#), @""); - insta::assert_snapshot!(render(r#"author.username()"#), @""); -} - -#[test] -fn test_templater_fill_function() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let render = |template| get_colored_template_output(&test_env, &repo_path, "@-", template); - - insta::assert_snapshot!( - render(r#"fill(20, "The quick fox jumps over the " ++ - label("error", "lazy") ++ " dog\n")"#), - @r###" - The quick fox jumps - over the lazy dog - "###); - - // A low value will not chop words, but can chop a label by words - insta::assert_snapshot!( - render(r#"fill(9, "Longlonglongword an some short words " ++ - label("error", "longlonglongword and short words") ++ " back out\n")"#), - @r###" - Longlonglongword - an some - short - words - longlonglongword - and short - words - back out - "###); - - // Filling to 0 means breaking at every word - insta::assert_snapshot!( - render(r#"fill(0, "The quick fox jumps over the " ++ - label("error", "lazy") ++ " dog\n")"#), - @r###" - The - quick - fox - jumps - over - the - lazy - dog - "###); - - // Filling to -0 is the same as 0 - insta::assert_snapshot!( - render(r#"fill(-0, "The quick fox jumps over the " ++ - label("error", "lazy") ++ " dog\n")"#), - @r###" - The - quick - fox - jumps - over - the - lazy - dog - "###); - - // Filling to negatives are clamped to the same as zero - insta::assert_snapshot!( - render(r#"fill(-10, "The quick fox jumps over the " ++ - label("error", "lazy") ++ " dog\n")"#), - @r###" - The - quick - fox - jumps - over - the - lazy - dog - "###); - - // Word-wrap, then indent - insta::assert_snapshot!( - render(r#""START marker to help insta\n" ++ - indent(" ", fill(20, "The quick fox jumps over the " ++ - label("error", "lazy") ++ " dog\n"))"#), - @r###" - START marker to help insta - The quick fox jumps - over the lazy dog - "###); - - // Word-wrap indented (no special handling for leading spaces) - insta::assert_snapshot!( - render(r#""START marker to help insta\n" ++ - fill(20, indent(" ", "The quick fox jumps over the " ++ - label("error", "lazy") ++ " dog\n"))"#), - @r###" - START marker to help insta - The quick fox - jumps over the lazy - dog - "###); -} - -#[test] -fn test_templater_indent_function() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let render = |template| get_colored_template_output(&test_env, &repo_path, "@-", template); - - // Empty line shouldn't be indented. Not using insta here because we test - // whitespace existence. - assert_eq!(render(r#"indent("__", "")"#), ""); - assert_eq!(render(r#"indent("__", "\n")"#), "\n"); - assert_eq!(render(r#"indent("__", "a\n\nb")"#), "__a\n\n__b"); - - // "\n" at end of labeled text - insta::assert_snapshot!( - render(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#), - @r###" - __a - __b - "###); - - // "\n" in labeled text - insta::assert_snapshot!( - render(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#), - @r###" - __ab - __c - "###); - - // Labeled prefix + unlabeled content - insta::assert_snapshot!( - render(r#"indent(label("error", "XX"), "a\nb\n")"#), - @r###" - XXa - XXb - "###); - - // Nested indent, silly but works - insta::assert_snapshot!( - render(r#"indent(label("hint", "A"), - label("warning", indent(label("hint", "B"), - label("error", "x\n") ++ "y")))"#), - @r###" - ABx - ABy - "###); -} - -#[test] -fn test_templater_concat_function() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let render = |template| get_colored_template_output(&test_env, &repo_path, "@-", template); - - insta::assert_snapshot!(render(r#"concat()"#), @""); - insta::assert_snapshot!(render(r#"concat(hidden, empty)"#), @"falsetrue"); - insta::assert_snapshot!( - render(r#"concat(label("error", ""), label("warning", "a"), "b")"#), - @"ab"); -} - -#[test] -fn test_templater_separate_function() { - let test_env = TestEnvironment::default(); - test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); - let repo_path = test_env.env_root().join("repo"); - let render = |template| get_colored_template_output(&test_env, &repo_path, "@-", template); - - insta::assert_snapshot!(render(r#"separate(" ")"#), @""); - insta::assert_snapshot!(render(r#"separate(" ", "")"#), @""); - insta::assert_snapshot!(render(r#"separate(" ", "a")"#), @"a"); - insta::assert_snapshot!(render(r#"separate(" ", "a", "b")"#), @"a b"); - insta::assert_snapshot!(render(r#"separate(" ", "a", "", "b")"#), @"a b"); - insta::assert_snapshot!(render(r#"separate(" ", "a", "b", "")"#), @"a b"); - insta::assert_snapshot!(render(r#"separate(" ", "", "a", "b")"#), @"a b"); - - // Labeled - insta::assert_snapshot!( - render(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#), - @"a b"); - - // List template - insta::assert_snapshot!(render(r#"separate(" ", "a", ("" ++ ""))"#), @"a"); - insta::assert_snapshot!(render(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b"); - - // Nested separate - insta::assert_snapshot!( - render(r#"separate(" ", "a", separate("|", "", ""))"#), @"a"); - insta::assert_snapshot!( - render(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b"); - insta::assert_snapshot!( - render(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c"); - - // Conditional template - insta::assert_snapshot!( - render(r#"separate(" ", "a", if(true, ""))"#), @"a"); - insta::assert_snapshot!( - render(r#"separate(" ", "a", if(true, "", "f"))"#), @"a"); - insta::assert_snapshot!( - render(r#"separate(" ", "a", if(false, "t", ""))"#), @"a"); - insta::assert_snapshot!( - render(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t"); - - // Separate keywords - insta::assert_snapshot!( - render(r#"separate(" ", hidden, description, empty)"#), @"false true"); - - // Keyword as separator - insta::assert_snapshot!( - render(r#"separate(hidden, "X", "Y", "Z")"#), @"XfalseYfalseZ"); } #[test]