just playing with tangled
1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::path::Path;
16
17use indoc::indoc;
18
19use crate::common::CommandOutput;
20use crate::common::TestEnvironment;
21
22#[test]
23fn test_templater_parse_error() {
24 let test_env = TestEnvironment::default();
25 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
26 let repo_path = test_env.env_root().join("repo");
27 let render = |template| get_template_output(&test_env, &repo_path, "@-", template);
28
29 insta::assert_snapshot!(render(r#"description ()"#), @r"
30 ------- stderr -------
31 Error: Failed to parse template: Syntax error
32 Caused by: --> 1:13
33 |
34 1 | description ()
35 | ^---
36 |
37 = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, or `<`
38 [EOF]
39 [exit status: 1]
40 ");
41
42 // Typo
43 test_env.add_config(
44 r###"
45 [template-aliases]
46 'conflicting' = ''
47 'shorted()' = ''
48 'socat(x)' = 'x'
49 'format_id(id)' = 'id.sort()'
50 "###,
51 );
52 insta::assert_snapshot!(render(r#"conflicts"#), @r"
53 ------- stderr -------
54 Error: Failed to parse template: Keyword `conflicts` doesn't exist
55 Caused by: --> 1:1
56 |
57 1 | conflicts
58 | ^-------^
59 |
60 = Keyword `conflicts` doesn't exist
61 Hint: Did you mean `conflict`, `conflicting`?
62 [EOF]
63 [exit status: 1]
64 ");
65 insta::assert_snapshot!(render(r#"commit_id.shorter()"#), @r"
66 ------- stderr -------
67 Error: Failed to parse template: Method `shorter` doesn't exist for type `CommitOrChangeId`
68 Caused by: --> 1:11
69 |
70 1 | commit_id.shorter()
71 | ^-----^
72 |
73 = Method `shorter` doesn't exist for type `CommitOrChangeId`
74 Hint: Did you mean `short`, `shortest`?
75 [EOF]
76 [exit status: 1]
77 ");
78 insta::assert_snapshot!(render(r#"oncat()"#), @r"
79 ------- stderr -------
80 Error: Failed to parse template: Function `oncat` doesn't exist
81 Caused by: --> 1:1
82 |
83 1 | oncat()
84 | ^---^
85 |
86 = Function `oncat` doesn't exist
87 Hint: Did you mean `concat`, `socat`?
88 [EOF]
89 [exit status: 1]
90 ");
91 insta::assert_snapshot!(render(r#""".lines().map(|s| se)"#), @r#"
92 ------- stderr -------
93 Error: Failed to parse template: Keyword `se` doesn't exist
94 Caused by: --> 1:20
95 |
96 1 | "".lines().map(|s| se)
97 | ^^
98 |
99 = Keyword `se` doesn't exist
100 Hint: Did you mean `s`, `self`?
101 [EOF]
102 [exit status: 1]
103 "#);
104 insta::assert_snapshot!(render(r#"format_id(commit_id)"#), @r"
105 ------- stderr -------
106 Error: Failed to parse template: In alias `format_id(id)`
107 Caused by:
108 1: --> 1:1
109 |
110 1 | format_id(commit_id)
111 | ^------------------^
112 |
113 = In alias `format_id(id)`
114 2: --> 1:4
115 |
116 1 | id.sort()
117 | ^--^
118 |
119 = Method `sort` doesn't exist for type `CommitOrChangeId`
120 Hint: Did you mean `short`, `shortest`?
121 [EOF]
122 [exit status: 1]
123 ");
124
125 // "at least N arguments"
126 insta::assert_snapshot!(render("separate()"), @r"
127 ------- stderr -------
128 Error: Failed to parse template: Function `separate`: Expected at least 1 arguments
129 Caused by: --> 1:10
130 |
131 1 | separate()
132 | ^
133 |
134 = Function `separate`: Expected at least 1 arguments
135 [EOF]
136 [exit status: 1]
137 ");
138
139 // -Tbuiltin shows the predefined builtin_* aliases. This isn't 100%
140 // guaranteed, but is nice.
141 insta::assert_snapshot!(render(r#"builtin"#), @r"
142 ------- stderr -------
143 Error: Failed to parse template: Keyword `builtin` doesn't exist
144 Caused by: --> 1:1
145 |
146 1 | builtin
147 | ^-----^
148 |
149 = Keyword `builtin` doesn't exist
150 Hint: Did you mean `builtin_config_list`, `builtin_config_list_detailed`, `builtin_draft_commit_description`, `builtin_log_comfortable`, `builtin_log_compact`, `builtin_log_compact_full_description`, `builtin_log_detailed`, `builtin_log_node`, `builtin_log_node_ascii`, `builtin_log_oneline`, `builtin_op_log_comfortable`, `builtin_op_log_compact`, `builtin_op_log_node`, `builtin_op_log_node_ascii`, `builtin_op_log_oneline`?
151 [EOF]
152 [exit status: 1]
153 ");
154}
155
156#[test]
157fn test_template_parse_warning() {
158 let test_env = TestEnvironment::default();
159 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
160 let repo_path = test_env.env_root().join("repo");
161
162 let template = indoc! {r#"
163 separate(' ',
164 author.username(),
165 )
166 "#};
167 let output = test_env.run_jj_in(&repo_path, ["log", "-r@", "-T", template]);
168 insta::assert_snapshot!(output, @r"
169 @ test.user
170 │
171 ~
172 [EOF]
173 ------- stderr -------
174 Warning: In template expression
175 --> 2:10
176 |
177 2 | author.username(),
178 | ^------^
179 |
180 = username() is deprecated; use email().local() instead
181 [EOF]
182 ");
183}
184
185#[test]
186fn test_templater_upper_lower() {
187 let test_env = TestEnvironment::default();
188 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
189 let repo_path = test_env.env_root().join("repo");
190 let render = |template| get_colored_template_output(&test_env, &repo_path, "@-", template);
191
192 insta::assert_snapshot!(
193 render(r#"change_id.shortest(4).upper() ++ change_id.shortest(4).upper().lower()"#),
194 @"[1m[38;5;5mZ[0m[38;5;8mZZZ[1m[38;5;5mz[0m[38;5;8mzzz[39m[EOF]");
195 insta::assert_snapshot!(
196 render(r#""Hello".upper() ++ "Hello".lower()"#), @"HELLOhello[EOF]");
197}
198
199#[test]
200fn test_templater_alias() {
201 let test_env = TestEnvironment::default();
202 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
203 let repo_path = test_env.env_root().join("repo");
204 let render = |template| get_template_output(&test_env, &repo_path, "@-", template);
205
206 test_env.add_config(
207 r###"
208 [template-aliases]
209 'my_commit_id' = 'commit_id.short()'
210 'syntax_error' = 'foo.'
211 'name_error' = 'unknown_id'
212 'recurse' = 'recurse1'
213 'recurse1' = 'recurse2()'
214 'recurse2()' = 'recurse'
215 'identity(x)' = 'x'
216 'coalesce(x, y)' = 'if(x, x, y)'
217 'deprecated()' = 'author.username()'
218 'builtin_log_node' = '"#"'
219 'builtin_op_log_node' = '"#"'
220 "###,
221 );
222
223 insta::assert_snapshot!(render("my_commit_id"), @"000000000000[EOF]");
224 insta::assert_snapshot!(render("identity(my_commit_id)"), @"000000000000[EOF]");
225
226 insta::assert_snapshot!(render("commit_id ++ syntax_error"), @r"
227 ------- stderr -------
228 Error: Failed to parse template: In alias `syntax_error`
229 Caused by:
230 1: --> 1:14
231 |
232 1 | commit_id ++ syntax_error
233 | ^----------^
234 |
235 = In alias `syntax_error`
236 2: --> 1:5
237 |
238 1 | foo.
239 | ^---
240 |
241 = expected <identifier>
242 [EOF]
243 [exit status: 1]
244 ");
245
246 insta::assert_snapshot!(render("commit_id ++ name_error"), @r"
247 ------- stderr -------
248 Error: Failed to parse template: In alias `name_error`
249 Caused by:
250 1: --> 1:14
251 |
252 1 | commit_id ++ name_error
253 | ^--------^
254 |
255 = In alias `name_error`
256 2: --> 1:1
257 |
258 1 | unknown_id
259 | ^--------^
260 |
261 = Keyword `unknown_id` doesn't exist
262 [EOF]
263 [exit status: 1]
264 ");
265
266 insta::assert_snapshot!(render(r#"identity(identity(commit_id.short("")))"#), @r#"
267 ------- stderr -------
268 Error: Failed to parse template: In alias `identity(x)`
269 Caused by:
270 1: --> 1:1
271 |
272 1 | identity(identity(commit_id.short("")))
273 | ^-------------------------------------^
274 |
275 = In alias `identity(x)`
276 2: --> 1:1
277 |
278 1 | x
279 | ^
280 |
281 = In function parameter `x`
282 3: --> 1:10
283 |
284 1 | identity(identity(commit_id.short("")))
285 | ^---------------------------^
286 |
287 = In alias `identity(x)`
288 4: --> 1:1
289 |
290 1 | x
291 | ^
292 |
293 = In function parameter `x`
294 5: --> 1:35
295 |
296 1 | identity(identity(commit_id.short("")))
297 | ^^
298 |
299 = Expected expression of type `Integer`, but actual type is `String`
300 [EOF]
301 [exit status: 1]
302 "#);
303
304 insta::assert_snapshot!(render("commit_id ++ recurse"), @r"
305 ------- stderr -------
306 Error: Failed to parse template: In alias `recurse`
307 Caused by:
308 1: --> 1:14
309 |
310 1 | commit_id ++ recurse
311 | ^-----^
312 |
313 = In alias `recurse`
314 2: --> 1:1
315 |
316 1 | recurse1
317 | ^------^
318 |
319 = In alias `recurse1`
320 3: --> 1:1
321 |
322 1 | recurse2()
323 | ^--------^
324 |
325 = In alias `recurse2()`
326 4: --> 1:1
327 |
328 1 | recurse
329 | ^-----^
330 |
331 = Alias `recurse` expanded recursively
332 [EOF]
333 [exit status: 1]
334 ");
335
336 insta::assert_snapshot!(render("identity()"), @r"
337 ------- stderr -------
338 Error: Failed to parse template: Function `identity`: Expected 1 arguments
339 Caused by: --> 1:10
340 |
341 1 | identity()
342 | ^
343 |
344 = Function `identity`: Expected 1 arguments
345 [EOF]
346 [exit status: 1]
347 ");
348 insta::assert_snapshot!(render("identity(commit_id, commit_id)"), @r"
349 ------- stderr -------
350 Error: Failed to parse template: Function `identity`: Expected 1 arguments
351 Caused by: --> 1:10
352 |
353 1 | identity(commit_id, commit_id)
354 | ^------------------^
355 |
356 = Function `identity`: Expected 1 arguments
357 [EOF]
358 [exit status: 1]
359 ");
360
361 insta::assert_snapshot!(render(r#"coalesce(label("x", "not boolean"), "")"#), @r#"
362 ------- stderr -------
363 Error: Failed to parse template: In alias `coalesce(x, y)`
364 Caused by:
365 1: --> 1:1
366 |
367 1 | coalesce(label("x", "not boolean"), "")
368 | ^-------------------------------------^
369 |
370 = In alias `coalesce(x, y)`
371 2: --> 1:4
372 |
373 1 | if(x, x, y)
374 | ^
375 |
376 = In function parameter `x`
377 3: --> 1:10
378 |
379 1 | coalesce(label("x", "not boolean"), "")
380 | ^-----------------------^
381 |
382 = Expected expression of type `Boolean`, but actual type is `Template`
383 [EOF]
384 [exit status: 1]
385 "#);
386
387 insta::assert_snapshot!(render("(-my_commit_id)"), @r"
388 ------- stderr -------
389 Error: Failed to parse template: In alias `my_commit_id`
390 Caused by:
391 1: --> 1:3
392 |
393 1 | (-my_commit_id)
394 | ^----------^
395 |
396 = In alias `my_commit_id`
397 2: --> 1:1
398 |
399 1 | commit_id.short()
400 | ^---------------^
401 |
402 = Expected expression of type `Integer`, but actual type is `String`
403 [EOF]
404 [exit status: 1]
405 ");
406
407 let output = test_env.run_jj_in(&repo_path, ["log", "-r@", "-Tdeprecated()"]);
408 insta::assert_snapshot!(output, @r"
409 # test.user
410 │
411 ~
412 [EOF]
413 ------- stderr -------
414 Warning: In template expression
415 --> 1:1
416 |
417 1 | deprecated()
418 | ^----------^
419 |
420 = In alias `deprecated()`
421 --> 1:8
422 |
423 1 | author.username()
424 | ^------^
425 |
426 = username() is deprecated; use email().local() instead
427 [EOF]
428 ");
429}
430
431#[test]
432fn test_templater_alias_override() {
433 let test_env = TestEnvironment::default();
434 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
435 let repo_path = test_env.env_root().join("repo");
436
437 test_env.add_config(
438 r#"
439 [template-aliases]
440 'f(x)' = '"user"'
441 "#,
442 );
443
444 // 'f(x)' should be overridden by --config 'f(a)'. If aliases were sorted
445 // purely by name, 'f(a)' would come first.
446 let output = test_env.run_jj_in(
447 &repo_path,
448 [
449 "log",
450 "--no-graph",
451 "-r@",
452 "-T",
453 r#"f(_)"#,
454 r#"--config=template-aliases.'f(a)'='"arg"'"#,
455 ],
456 );
457 insta::assert_snapshot!(output, @"arg[EOF]");
458}
459
460#[test]
461fn test_templater_bad_alias_decl() {
462 let test_env = TestEnvironment::default();
463 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
464 let repo_path = test_env.env_root().join("repo");
465
466 test_env.add_config(
467 r###"
468 [template-aliases]
469 'badfn(a, a)' = 'a'
470 'my_commit_id' = 'commit_id.short()'
471 "###,
472 );
473
474 // Invalid declaration should be warned and ignored.
475 let output = test_env.run_jj_in(&repo_path, ["log", "--no-graph", "-r@-", "-Tmy_commit_id"]);
476 insta::assert_snapshot!(output, @r"
477 000000000000[EOF]
478 ------- stderr -------
479 Warning: Failed to load `template-aliases.badfn(a, a)`: --> 1:7
480 |
481 1 | badfn(a, a)
482 | ^--^
483 |
484 = Redefinition of function parameter
485 [EOF]
486 ");
487}
488
489#[test]
490fn test_templater_config_function() {
491 let test_env = TestEnvironment::default();
492 test_env.run_jj_in(".", ["git", "init", "repo"]).success();
493 let repo_path = test_env.env_root().join("repo");
494 let render = |template| get_template_output(&test_env, &repo_path, "@-", template);
495
496 insta::assert_snapshot!(
497 render("config('user.name')"),
498 @r#""Test User"[EOF]"#);
499 insta::assert_snapshot!(
500 render("config('user')"),
501 @r#"{ email = "test.user@example.com", name = "Test User" }[EOF]"#);
502 insta::assert_snapshot!(render("config('invalid name')"), @r"
503 ------- stderr -------
504 Error: Failed to parse template: Failed to parse config name
505 Caused by:
506 1: --> 1:8
507 |
508 1 | config('invalid name')
509 | ^------------^
510 |
511 = Failed to parse config name
512 2: TOML parse error at line 1, column 9
513 |
514 1 | invalid name
515 | ^
516
517
518 [EOF]
519 [exit status: 1]
520 ");
521 insta::assert_snapshot!(render("config('unknown')"), @r"
522 ------- stderr -------
523 Error: Failed to parse template: Failed to get config value
524 Caused by:
525 1: --> 1:1
526 |
527 1 | config('unknown')
528 | ^----^
529 |
530 = Failed to get config value
531 2: Value not found for unknown
532 [EOF]
533 [exit status: 1]
534 ");
535}
536
537#[must_use]
538fn get_template_output(
539 test_env: &TestEnvironment,
540 repo_path: &Path,
541 rev: &str,
542 template: &str,
543) -> CommandOutput {
544 test_env.run_jj_in(repo_path, ["log", "--no-graph", "-r", rev, "-T", template])
545}
546
547#[must_use]
548fn get_colored_template_output(
549 test_env: &TestEnvironment,
550 repo_path: &Path,
551 rev: &str,
552 template: &str,
553) -> CommandOutput {
554 test_env.run_jj_in(
555 repo_path,
556 [
557 "log",
558 "--color=always",
559 "--no-graph",
560 "-r",
561 rev,
562 "-T",
563 template,
564 ],
565 )
566}