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 insta::assert_snapshot;
16use itertools::Itertools;
17use regex::Regex;
18
19use crate::common::TestEnvironment;
20
21#[test]
22fn test_config_list_single() {
23 let test_env = TestEnvironment::default();
24 test_env.add_config(
25 r#"
26 [test-table]
27 somekey = "some value"
28 "#,
29 );
30
31 let stdout = test_env.jj_cmd_success(
32 test_env.env_root(),
33 &["config", "list", "test-table.somekey"],
34 );
35 insta::assert_snapshot!(stdout, @r###"
36 test-table.somekey="some value"
37 "###);
38
39 let stdout = test_env.jj_cmd_success(
40 test_env.env_root(),
41 &["config", "list", r#"-Tname ++ "\n""#, "test-table.somekey"],
42 );
43 insta::assert_snapshot!(stdout, @r###"
44 test-table.somekey
45 "###);
46}
47
48#[test]
49fn test_config_list_nonexistent() {
50 let test_env = TestEnvironment::default();
51 let (stdout, stderr) = test_env.jj_cmd_ok(
52 test_env.env_root(),
53 &["config", "list", "nonexistent-test-key"],
54 );
55 insta::assert_snapshot!(stdout, @"");
56 insta::assert_snapshot!(stderr, @r###"
57 Warning: No matching config key for nonexistent-test-key
58 "###);
59}
60
61#[test]
62fn test_config_list_table() {
63 let test_env = TestEnvironment::default();
64 test_env.add_config(
65 r#"
66 [test-table]
67 x = true
68 y.foo = "abc"
69 y.bar = 123
70 "#,
71 );
72 let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-table"]);
73 insta::assert_snapshot!(
74 stdout,
75 @r###"
76 test-table.x=true
77 test-table.y.bar=123
78 test-table.y.foo="abc"
79 "###);
80}
81
82#[test]
83fn test_config_list_array() {
84 let test_env = TestEnvironment::default();
85 test_env.add_config(
86 r#"
87 test-array = [1, "b", 3.4]
88 "#,
89 );
90 let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-array"]);
91 insta::assert_snapshot!(stdout, @r###"
92 test-array=[1, "b", 3.4]
93 "###);
94}
95
96#[test]
97fn test_config_list_inline_table() {
98 let test_env = TestEnvironment::default();
99 test_env.add_config(
100 r#"
101 [[test-table]]
102 x = 1
103 [[test-table]]
104 y = ["z"]
105 "#,
106 );
107 let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list", "test-table"]);
108 insta::assert_snapshot!(stdout, @r###"
109 test-table=[{x=1}, {y=["z"]}]
110 "###);
111}
112
113#[test]
114fn test_config_list_all() {
115 let test_env = TestEnvironment::default();
116 test_env.add_config(
117 r#"
118 test-val = [1, 2, 3]
119 [test-table]
120 x = true
121 y.foo = "abc"
122 y.bar = 123
123 "#,
124 );
125
126 let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "list"]);
127 insta::assert_snapshot!(
128 find_stdout_lines(r"(test-val|test-table\b[^=]*)", &stdout),
129 @r###"
130 test-table.x=true
131 test-table.y.bar=123
132 test-table.y.foo="abc"
133 test-val=[1, 2, 3]
134 "###);
135}
136
137#[test]
138fn test_config_list_layer() {
139 let mut test_env = TestEnvironment::default();
140 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
141 let user_config_path = test_env.config_path().join("config.toml");
142 test_env.set_config_path(user_config_path.to_owned());
143 let repo_path = test_env.env_root().join("repo");
144
145 // User
146 test_env.jj_cmd_ok(
147 &repo_path,
148 &["config", "set", "--user", "test-key", "test-val"],
149 );
150
151 test_env.jj_cmd_ok(
152 &repo_path,
153 &[
154 "config",
155 "set",
156 "--user",
157 "test-layered-key",
158 "test-original-val",
159 ],
160 );
161
162 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", "--user"]);
163 insta::assert_snapshot!(stdout, @r###"
164 test-key="test-val"
165 test-layered-key="test-original-val"
166 "###);
167
168 // Repo
169 test_env.jj_cmd_ok(
170 &repo_path,
171 &[
172 "config",
173 "set",
174 "--repo",
175 "test-layered-key",
176 "test-layered-val",
177 ],
178 );
179
180 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", "--user"]);
181 insta::assert_snapshot!(stdout, @r###"
182 test-key="test-val"
183 "###);
184
185 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", "--repo"]);
186 insta::assert_snapshot!(stdout, @r###"
187 test-layered-key="test-layered-val"
188 "###);
189}
190
191#[test]
192fn test_config_layer_override_default() {
193 let test_env = TestEnvironment::default();
194 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
195 let repo_path = test_env.env_root().join("repo");
196 let config_key = "merge-tools.vimdiff.program";
197
198 // Default
199 let stdout = test_env.jj_cmd_success(
200 &repo_path,
201 &["config", "list", config_key, "--include-defaults"],
202 );
203 insta::assert_snapshot!(stdout, @r###"
204 merge-tools.vimdiff.program="vim"
205 "###);
206
207 // User
208 test_env.add_config(&format!("{config_key} = {value:?}\n", value = "user"));
209 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", config_key]);
210 insta::assert_snapshot!(stdout, @r###"
211 merge-tools.vimdiff.program="user"
212 "###);
213
214 // Repo
215 std::fs::write(
216 repo_path.join(".jj/repo/config.toml"),
217 format!("{config_key} = {value:?}\n", value = "repo"),
218 )
219 .unwrap();
220 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", config_key]);
221 insta::assert_snapshot!(stdout, @r###"
222 merge-tools.vimdiff.program="repo"
223 "###);
224
225 // Command argument
226 let stdout = test_env.jj_cmd_success(
227 &repo_path,
228 &[
229 "config",
230 "list",
231 config_key,
232 "--config-toml",
233 &format!("{config_key}={value:?}", value = "command-arg"),
234 ],
235 );
236 insta::assert_snapshot!(stdout, @r###"
237 merge-tools.vimdiff.program="command-arg"
238 "###);
239
240 // Allow printing overridden values
241 let stdout = test_env.jj_cmd_success(
242 &repo_path,
243 &[
244 "config",
245 "list",
246 config_key,
247 "--include-overridden",
248 "--config-toml",
249 &format!("{config_key}={value:?}", value = "command-arg"),
250 ],
251 );
252 insta::assert_snapshot!(stdout, @r###"
253 # merge-tools.vimdiff.program="user"
254 # merge-tools.vimdiff.program="repo"
255 merge-tools.vimdiff.program="command-arg"
256 "###);
257
258 let stdout = test_env.jj_cmd_success(
259 &repo_path,
260 &[
261 "config",
262 "list",
263 "--color=always",
264 config_key,
265 "--include-overridden",
266 ],
267 );
268 insta::assert_snapshot!(stdout, @r###"
269 [38;5;8m# merge-tools.vimdiff.program="user"[39m
270 [38;5;2mmerge-tools.vimdiff.program[39m=[38;5;3m"repo"[39m
271 "###);
272}
273
274#[test]
275fn test_config_layer_override_env() {
276 let mut test_env = TestEnvironment::default();
277 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
278 let repo_path = test_env.env_root().join("repo");
279 let config_key = "ui.editor";
280
281 // Environment base
282 test_env.add_env_var("EDITOR", "env-base");
283 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", config_key]);
284 insta::assert_snapshot!(stdout, @r###"
285 ui.editor="env-base"
286 "###);
287
288 // User
289 test_env.add_config(&format!("{config_key} = {value:?}\n", value = "user"));
290 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", config_key]);
291 insta::assert_snapshot!(stdout, @r###"
292 ui.editor="user"
293 "###);
294
295 // Repo
296 std::fs::write(
297 repo_path.join(".jj/repo/config.toml"),
298 format!("{config_key} = {value:?}\n", value = "repo"),
299 )
300 .unwrap();
301 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", config_key]);
302 insta::assert_snapshot!(stdout, @r###"
303 ui.editor="repo"
304 "###);
305
306 // Environment override
307 test_env.add_env_var("JJ_EDITOR", "env-override");
308 let stdout = test_env.jj_cmd_success(&repo_path, &["config", "list", config_key]);
309 insta::assert_snapshot!(stdout, @r###"
310 ui.editor="env-override"
311 "###);
312
313 // Command argument
314 let stdout = test_env.jj_cmd_success(
315 &repo_path,
316 &[
317 "config",
318 "list",
319 config_key,
320 "--config-toml",
321 &format!("{config_key}={value:?}", value = "command-arg"),
322 ],
323 );
324 insta::assert_snapshot!(stdout, @r###"
325 ui.editor="command-arg"
326 "###);
327
328 // Allow printing overridden values
329 let stdout = test_env.jj_cmd_success(
330 &repo_path,
331 &[
332 "config",
333 "list",
334 config_key,
335 "--include-overridden",
336 "--config-toml",
337 &format!("{config_key}={value:?}", value = "command-arg"),
338 ],
339 );
340 insta::assert_snapshot!(stdout, @r###"
341 # ui.editor="env-base"
342 # ui.editor="user"
343 # ui.editor="repo"
344 # ui.editor="env-override"
345 ui.editor="command-arg"
346 "###);
347}
348
349#[test]
350fn test_config_layer_workspace() {
351 let test_env = TestEnvironment::default();
352 test_env.jj_cmd_ok(test_env.env_root(), &["init", "--git", "main"]);
353 let main_path = test_env.env_root().join("main");
354 let secondary_path = test_env.env_root().join("secondary");
355 let config_key = "ui.editor";
356
357 std::fs::write(main_path.join("file"), "contents").unwrap();
358 test_env.jj_cmd_ok(&main_path, &["new"]);
359 test_env.jj_cmd_ok(
360 &main_path,
361 &["workspace", "add", "--name", "second", "../secondary"],
362 );
363
364 // Repo
365 std::fs::write(
366 main_path.join(".jj/repo/config.toml"),
367 format!("{config_key} = {value:?}\n", value = "main-repo"),
368 )
369 .unwrap();
370 let stdout = test_env.jj_cmd_success(&main_path, &["config", "list", config_key]);
371 insta::assert_snapshot!(stdout, @r###"
372 ui.editor="main-repo"
373 "###);
374 let stdout = test_env.jj_cmd_success(&secondary_path, &["config", "list", config_key]);
375 insta::assert_snapshot!(stdout, @r###"
376 ui.editor="main-repo"
377 "###);
378}
379
380#[test]
381fn test_config_set_missing_opts() {
382 let test_env = TestEnvironment::default();
383 let stderr = test_env.jj_cmd_cli_error(test_env.env_root(), &["config", "set"]);
384 insta::assert_snapshot!(stderr, @r###"
385 error: the following required arguments were not provided:
386 <--user|--repo>
387 <NAME>
388 <VALUE>
389
390 Usage: jj config set <--user|--repo> <NAME> <VALUE>
391
392 For more information, try '--help'.
393 "###);
394}
395
396#[test]
397fn test_config_set_for_user() {
398 let mut test_env = TestEnvironment::default();
399 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
400 // Point to a config file since `config set` can't handle directories.
401 let user_config_path = test_env.config_path().join("config.toml");
402 test_env.set_config_path(user_config_path.to_owned());
403 let repo_path = test_env.env_root().join("repo");
404
405 test_env.jj_cmd_ok(
406 &repo_path,
407 &["config", "set", "--user", "test-key", "test-val"],
408 );
409 test_env.jj_cmd_ok(
410 &repo_path,
411 &["config", "set", "--user", "test-table.foo", "true"],
412 );
413
414 // Ensure test-key successfully written to user config.
415 let user_config_toml = std::fs::read_to_string(&user_config_path)
416 .unwrap_or_else(|_| panic!("Failed to read file {}", user_config_path.display()));
417 insta::assert_snapshot!(user_config_toml, @r###"
418 test-key = "test-val"
419
420 [test-table]
421 foo = true
422 "###);
423}
424
425#[test]
426fn test_config_set_for_repo() {
427 let test_env = TestEnvironment::default();
428 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
429 let repo_path = test_env.env_root().join("repo");
430 test_env.jj_cmd_ok(
431 &repo_path,
432 &["config", "set", "--repo", "test-key", "test-val"],
433 );
434 test_env.jj_cmd_ok(
435 &repo_path,
436 &["config", "set", "--repo", "test-table.foo", "true"],
437 );
438 // Ensure test-key successfully written to user config.
439 let expected_repo_config_path = repo_path.join(".jj/repo/config.toml");
440 let repo_config_toml =
441 std::fs::read_to_string(&expected_repo_config_path).unwrap_or_else(|_| {
442 panic!(
443 "Failed to read file {}",
444 expected_repo_config_path.display()
445 )
446 });
447 insta::assert_snapshot!(repo_config_toml, @r###"
448 test-key = "test-val"
449
450 [test-table]
451 foo = true
452 "###);
453}
454
455#[test]
456fn test_config_set_toml_types() {
457 let mut test_env = TestEnvironment::default();
458 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
459 let user_config_path = test_env.config_path().join("config.toml");
460 test_env.set_config_path(user_config_path.clone());
461 let repo_path = test_env.env_root().join("repo");
462
463 let set_value = |key, value| {
464 test_env.jj_cmd_success(&repo_path, &["config", "set", "--user", key, value]);
465 };
466 set_value("test-table.integer", "42");
467 set_value("test-table.float", "3.14");
468 set_value("test-table.array", r#"["one", "two"]"#);
469 set_value("test-table.boolean", "true");
470 set_value("test-table.string", r#""foo""#);
471 set_value("test-table.invalid", r"a + b");
472 insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r###"
473 [test-table]
474 integer = 42
475 float = 3.14
476 array = ["one", "two"]
477 boolean = true
478 string = "foo"
479 invalid = "a + b"
480 "###);
481}
482
483#[test]
484fn test_config_set_type_mismatch() {
485 let mut test_env = TestEnvironment::default();
486 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
487 let user_config_path = test_env.config_path().join("config.toml");
488 test_env.set_config_path(user_config_path);
489 let repo_path = test_env.env_root().join("repo");
490
491 test_env.jj_cmd_ok(
492 &repo_path,
493 &["config", "set", "--user", "test-table.foo", "test-val"],
494 );
495 let stderr = test_env.jj_cmd_failure(
496 &repo_path,
497 &["config", "set", "--user", "test-table", "not-a-table"],
498 );
499 insta::assert_snapshot!(stderr, @r###"
500 Error: Failed to set test-table: would overwrite entire table
501 "###);
502
503 // But it's fine to overwrite arrays and inline tables
504 test_env.jj_cmd_success(
505 &repo_path,
506 &["config", "set", "--user", "test-table.array", "[1,2,3]"],
507 );
508 test_env.jj_cmd_success(
509 &repo_path,
510 &["config", "set", "--user", "test-table.array", "[4,5,6]"],
511 );
512 test_env.jj_cmd_success(
513 &repo_path,
514 &["config", "set", "--user", "test-table.inline", "{ x = 42}"],
515 );
516 test_env.jj_cmd_success(
517 &repo_path,
518 &["config", "set", "--user", "test-table.inline", "42"],
519 );
520}
521
522#[test]
523fn test_config_set_nontable_parent() {
524 let mut test_env = TestEnvironment::default();
525 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
526 let user_config_path = test_env.config_path().join("config.toml");
527 test_env.set_config_path(user_config_path);
528 let repo_path = test_env.env_root().join("repo");
529
530 test_env.jj_cmd_ok(
531 &repo_path,
532 &["config", "set", "--user", "test-nontable", "test-val"],
533 );
534 let stderr = test_env.jj_cmd_failure(
535 &repo_path,
536 &["config", "set", "--user", "test-nontable.foo", "test-val"],
537 );
538 insta::assert_snapshot!(stderr, @r###"
539 Error: Failed to set test-nontable.foo: would overwrite non-table value with parent table
540 "###);
541}
542
543#[test]
544fn test_config_edit_missing_opt() {
545 let test_env = TestEnvironment::default();
546 let stderr = test_env.jj_cmd_cli_error(test_env.env_root(), &["config", "edit"]);
547 insta::assert_snapshot!(stderr, @r###"
548 error: the following required arguments were not provided:
549 <--user|--repo>
550
551 Usage: jj config edit <--user|--repo>
552
553 For more information, try '--help'.
554 "###);
555}
556
557#[test]
558fn test_config_edit_user() {
559 let mut test_env = TestEnvironment::default();
560 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
561 let repo_path = test_env.env_root().join("repo");
562 let edit_script = test_env.set_up_fake_editor();
563
564 std::fs::write(
565 edit_script,
566 format!("expectpath\n{}", test_env.config_path().to_str().unwrap()),
567 )
568 .unwrap();
569 test_env.jj_cmd_ok(&repo_path, &["config", "edit", "--user"]);
570}
571
572#[test]
573fn test_config_edit_repo() {
574 let mut test_env = TestEnvironment::default();
575 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
576 let repo_path = test_env.env_root().join("repo");
577 let edit_script = test_env.set_up_fake_editor();
578
579 std::fs::write(
580 edit_script,
581 format!(
582 "expectpath\n{}",
583 repo_path.join(".jj/repo/config.toml").to_str().unwrap()
584 ),
585 )
586 .unwrap();
587 test_env.jj_cmd_ok(&repo_path, &["config", "edit", "--repo"]);
588}
589
590#[test]
591fn test_config_path() {
592 let test_env = TestEnvironment::default();
593 test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
594 let repo_path = test_env.env_root().join("repo");
595
596 assert_snapshot!(
597 test_env.jj_cmd_success(&repo_path, &["config", "path", "--user"]),
598 @r###"
599 $TEST_ENV/config
600 "###
601 );
602 assert_snapshot!(
603 test_env.jj_cmd_success(&repo_path, &["config", "path", "--repo"]),
604 @r###"
605 $TEST_ENV/repo/.jj/repo/config.toml
606 "###
607 );
608}
609
610#[test]
611fn test_config_edit_repo_outside_repo() {
612 let test_env = TestEnvironment::default();
613 let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["config", "edit", "--repo"]);
614 insta::assert_snapshot!(stderr, @r###"
615 Error: There is no jj repo in "."
616 "###);
617}
618
619#[test]
620fn test_config_get() {
621 let test_env = TestEnvironment::default();
622 test_env.add_config(
623 r#"
624 [table]
625 string = "some value 1"
626 int = 123
627 list = ["list", "value"]
628 overridden = "foo"
629 "#,
630 );
631 test_env.add_config(
632 r#"
633 [table]
634 overridden = "bar"
635 "#,
636 );
637
638 let stdout = test_env.jj_cmd_failure(test_env.env_root(), &["config", "get", "nonexistent"]);
639 insta::assert_snapshot!(stdout, @r###"
640 Config error: configuration property "nonexistent" not found
641 For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md.
642 "###);
643
644 let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "table.string"]);
645 insta::assert_snapshot!(stdout, @r###"
646 some value 1
647 "###);
648
649 let stdout = test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "table.int"]);
650 insta::assert_snapshot!(stdout, @r###"
651 123
652 "###);
653
654 let stdout = test_env.jj_cmd_failure(test_env.env_root(), &["config", "get", "table.list"]);
655 insta::assert_snapshot!(stdout, @r###"
656 Config error: invalid type: sequence, expected a value convertible to a string
657 For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md.
658 "###);
659
660 let stdout = test_env.jj_cmd_failure(test_env.env_root(), &["config", "get", "table"]);
661 insta::assert_snapshot!(stdout, @r###"
662 Config error: invalid type: map, expected a value convertible to a string
663 For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md.
664 "###);
665
666 let stdout =
667 test_env.jj_cmd_success(test_env.env_root(), &["config", "get", "table.overridden"]);
668 insta::assert_snapshot!(stdout, @"bar");
669}
670
671fn find_stdout_lines(keyname_pattern: &str, stdout: &str) -> String {
672 let key_line_re = Regex::new(&format!(r"(?m)^{keyname_pattern}=.*$")).unwrap();
673 key_line_re
674 .find_iter(stdout)
675 .map(|m| m.as_str())
676 .collect_vec()
677 .join("\n")
678}