4 from typing
import Callable
, Optional
8 from tests
.conftest
import Dotfiles
11 def test_link_canonicalization(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
12 """Verify links to symlinked destinations are canonical.
14 "Canonical", here, means that dotbot does not create symlinks
15 that point to intermediary symlinks.
18 dotfiles
.write("f", "apple")
19 dotfiles
.write_config([{"link": {"~/.f": {"path": "f"}}}])
21 # Point to the config file in a symlinked dotfiles directory.
22 dotfiles_symlink
= os
.path
.join(home
, "dotfiles-symlink")
23 os
.symlink(dotfiles
.directory
, dotfiles_symlink
)
24 config_file
= os
.path
.join(dotfiles_symlink
, os
.path
.basename(dotfiles
.config_filename
))
25 run_dotbot("-c", config_file
, custom
=True)
27 expected
= os
.path
.join(dotfiles
.directory
, "f")
28 actual
= os
.readlink(os
.path
.abspath(os
.path
.expanduser("~/.f")))
29 if sys
.platform
== "win32" and actual
.startswith("\\\\?\\"):
31 assert expected
== actual
34 @pytest.mark
.parametrize("dst", ["~/.f", "~/f"])
35 @pytest.mark
.parametrize("include_force", [True, False])
36 def test_link_default_source(
38 include_force
: bool, # noqa: FBT001
41 run_dotbot
: Callable
[..., None],
43 """Verify that default sources are calculated correctly.
45 This test includes verifying files with and without leading periods,
46 as well as verifying handling of None dict values.
50 dotfiles
.write("f", "apple")
54 dst
: {"force": False} if include_force
else None,
58 dotfiles
.write_config(config
)
61 with
open(os
.path
.abspath(os
.path
.expanduser(dst
))) as file:
62 assert file.read() == "apple"
65 def test_link_environment_user_expansion_target(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
66 """Verify link expands user in target."""
71 with
open(os
.path
.abspath(os
.path
.expanduser(src
)), "w") as file:
73 dotfiles
.write_config([{"link": {target
: src
}}])
76 with
open(os
.path
.abspath(os
.path
.expanduser(target
))) as file:
77 assert file.read() == "apple"
80 def test_link_environment_variable_expansion_source(
81 monkeypatch
: pytest
.MonkeyPatch
, home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]
83 """Verify link expands environment variables in source."""
86 monkeypatch
.setenv("APPLE", "h")
89 dotfiles
.write("h", "grape")
90 dotfiles
.write_config([{"link": {target
: src
}}])
93 with
open(os
.path
.abspath(os
.path
.expanduser(target
))) as file:
94 assert file.read() == "grape"
97 def test_link_environment_variable_expansion_source_extended(
98 monkeypatch
: pytest
.MonkeyPatch
, home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]
100 """Verify link expands environment variables in extended config syntax."""
103 monkeypatch
.setenv("APPLE", "h")
106 dotfiles
.write("h", "grape")
107 dotfiles
.write_config([{"link": {target
: {"path": src
, "relink": True}}}])
110 with
open(os
.path
.abspath(os
.path
.expanduser(target
))) as file:
111 assert file.read() == "grape"
114 def test_link_environment_variable_expansion_target(
115 monkeypatch
: pytest
.MonkeyPatch
, home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]
117 """Verify link expands environment variables in target.
119 If the variable doesn't exist, the "variable" must not be replaced.
122 monkeypatch
.setenv("ORANGE", ".config")
123 monkeypatch
.setenv("BANANA", "g")
124 monkeypatch
.delenv("PEAR", raising
=False)
126 dotfiles
.write("f", "apple")
127 dotfiles
.write("h", "grape")
132 "~/${ORANGE}/$BANANA": {
140 dotfiles
.write_config(config
)
143 with
open(os
.path
.join(home
, ".config", "g")) as file:
144 assert file.read() == "apple"
145 with
open(os
.path
.join(home
, "$PEAR")) as file:
146 assert file.read() == "grape"
149 def test_link_environment_variable_unset(
150 monkeypatch
: pytest
.MonkeyPatch
, home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]
152 """Verify link leaves unset environment variables."""
154 monkeypatch
.delenv("ORANGE", raising
=False)
155 dotfiles
.write("$ORANGE", "apple")
156 dotfiles
.write_config([{"link": {"~/f": "$ORANGE"}}])
159 with
open(os
.path
.join(home
, "f")) as file:
160 assert file.read() == "apple"
163 def test_link_force_leaves_when_nonexistent(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
164 """Verify force doesn't erase sources when targets are nonexistent."""
166 os
.mkdir(os
.path
.join(home
, "dir"))
167 open(os
.path
.join(home
, "file"), "a").close()
171 "~/dir": {"path": "dir", "force": True},
172 "~/file": {"path": "file", "force": True},
176 dotfiles
.write_config(config
)
177 with pytest
.raises(SystemExit):
180 assert os
.path
.isdir(os
.path
.join(home
, "dir"))
181 assert os
.path
.isfile(os
.path
.join(home
, "file"))
184 def test_link_force_overwrite_symlink(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
185 """Verify force overwrites a symlinked directory."""
187 os
.mkdir(os
.path
.join(home
, "dir"))
188 dotfiles
.write("dir/f")
189 os
.symlink(home
, os
.path
.join(home
, ".dir"))
191 config
= [{"link": {"~/.dir": {"path": "dir", "force": True}}}]
192 dotfiles
.write_config(config
)
195 assert os
.path
.isfile(os
.path
.join(home
, ".dir", "f"))
198 def test_link_glob_1(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
199 """Verify globbing works."""
201 dotfiles
.write("bin/a", "apple")
202 dotfiles
.write("bin/b", "banana")
203 dotfiles
.write("bin/c", "cherry")
204 dotfiles
.write_config(
206 {"defaults": {"link": {"glob": True, "create": True}}},
207 {"link": {"~/bin": "bin/*"}},
212 with
open(os
.path
.join(home
, "bin", "a")) as file:
213 assert file.read() == "apple"
214 with
open(os
.path
.join(home
, "bin", "b")) as file:
215 assert file.read() == "banana"
216 with
open(os
.path
.join(home
, "bin", "c")) as file:
217 assert file.read() == "cherry"
220 def test_link_glob_2(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
221 """Verify globbing works with a trailing slash in the source."""
223 dotfiles
.write("bin/a", "apple")
224 dotfiles
.write("bin/b", "banana")
225 dotfiles
.write("bin/c", "cherry")
226 dotfiles
.write_config(
228 {"defaults": {"link": {"glob": True, "create": True}}},
229 {"link": {"~/bin/": "bin/*"}},
234 with
open(os
.path
.join(home
, "bin", "a")) as file:
235 assert file.read() == "apple"
236 with
open(os
.path
.join(home
, "bin", "b")) as file:
237 assert file.read() == "banana"
238 with
open(os
.path
.join(home
, "bin", "c")) as file:
239 assert file.read() == "cherry"
242 def test_link_glob_3(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
243 """Verify globbing works with hidden ("period-prefixed") files."""
245 dotfiles
.write("bin/.a", "dot-apple")
246 dotfiles
.write("bin/.b", "dot-banana")
247 dotfiles
.write("bin/.c", "dot-cherry")
248 dotfiles
.write_config(
250 {"defaults": {"link": {"glob": True, "create": True}}},
251 {"link": {"~/bin/": "bin/.*"}},
256 with
open(os
.path
.join(home
, "bin", ".a")) as file:
257 assert file.read() == "dot-apple"
258 with
open(os
.path
.join(home
, "bin", ".b")) as file:
259 assert file.read() == "dot-banana"
260 with
open(os
.path
.join(home
, "bin", ".c")) as file:
261 assert file.read() == "dot-cherry"
264 def test_link_glob_4(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
265 """Verify globbing works at the root of the home and dotfiles directories."""
267 dotfiles
.write(".a", "dot-apple")
268 dotfiles
.write(".b", "dot-banana")
269 dotfiles
.write(".c", "dot-cherry")
270 dotfiles
.write_config(
284 with
open(os
.path
.join(home
, ".a")) as file:
285 assert file.read() == "dot-apple"
286 with
open(os
.path
.join(home
, ".b")) as file:
287 assert file.read() == "dot-banana"
288 with
open(os
.path
.join(home
, ".c")) as file:
289 assert file.read() == "dot-cherry"
292 @pytest.mark
.parametrize("path", ["foo", "foo/"])
293 def test_link_glob_ignore_no_glob_chars(
294 path
: str, home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]
296 """Verify ambiguous link globbing fails."""
298 dotfiles
.makedirs("foo")
299 dotfiles
.write_config(
312 assert os
.path
.islink(os
.path
.join(home
, "foo"))
313 assert os
.path
.exists(os
.path
.join(home
, "foo"))
316 def test_link_glob_exclude_1(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
317 """Verify link globbing with an explicit exclusion."""
319 dotfiles
.write("config/foo/a", "apple")
320 dotfiles
.write("config/bar/b", "banana")
321 dotfiles
.write("config/bar/c", "cherry")
322 dotfiles
.write("config/baz/d", "donut")
323 dotfiles
.write_config(
337 "exclude": ["config/baz"],
345 assert not os
.path
.exists(os
.path
.join(home
, ".config", "baz"))
347 assert not os
.path
.islink(os
.path
.join(home
, ".config"))
348 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo"))
349 assert os
.path
.islink(os
.path
.join(home
, ".config", "bar"))
350 with
open(os
.path
.join(home
, ".config", "foo", "a")) as file:
351 assert file.read() == "apple"
352 with
open(os
.path
.join(home
, ".config", "bar", "b")) as file:
353 assert file.read() == "banana"
354 with
open(os
.path
.join(home
, ".config", "bar", "c")) as file:
355 assert file.read() == "cherry"
358 def test_link_glob_exclude_2(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
359 """Verify deep link globbing with a globbed exclusion."""
361 dotfiles
.write("config/foo/a", "apple")
362 dotfiles
.write("config/bar/b", "banana")
363 dotfiles
.write("config/bar/c", "cherry")
364 dotfiles
.write("config/baz/d", "donut")
365 dotfiles
.write("config/baz/buzz/e", "egg")
366 dotfiles
.write_config(
379 "path": "config/*/*",
380 "exclude": ["config/baz/*"],
388 assert not os
.path
.exists(os
.path
.join(home
, ".config", "baz"))
390 assert not os
.path
.islink(os
.path
.join(home
, ".config"))
391 assert not os
.path
.islink(os
.path
.join(home
, ".config", "foo"))
392 assert not os
.path
.islink(os
.path
.join(home
, ".config", "bar"))
393 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo", "a"))
394 with
open(os
.path
.join(home
, ".config", "foo", "a")) as file:
395 assert file.read() == "apple"
396 with
open(os
.path
.join(home
, ".config", "bar", "b")) as file:
397 assert file.read() == "banana"
398 with
open(os
.path
.join(home
, ".config", "bar", "c")) as file:
399 assert file.read() == "cherry"
402 def test_link_glob_exclude_3(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
403 """Verify deep link globbing with an explicit exclusion."""
405 dotfiles
.write("config/foo/a", "apple")
406 dotfiles
.write("config/bar/b", "banana")
407 dotfiles
.write("config/bar/c", "cherry")
408 dotfiles
.write("config/baz/d", "donut")
409 dotfiles
.write("config/baz/buzz/e", "egg")
410 dotfiles
.write("config/baz/bizz/g", "grape")
411 dotfiles
.write_config(
424 "path": "config/*/*",
425 "exclude": ["config/baz/buzz"],
433 assert not os
.path
.exists(os
.path
.join(home
, ".config", "baz", "buzz"))
435 assert not os
.path
.islink(os
.path
.join(home
, ".config"))
436 assert not os
.path
.islink(os
.path
.join(home
, ".config", "foo"))
437 assert not os
.path
.islink(os
.path
.join(home
, ".config", "bar"))
438 assert not os
.path
.islink(os
.path
.join(home
, ".config", "baz"))
439 assert os
.path
.islink(os
.path
.join(home
, ".config", "baz", "bizz"))
440 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo", "a"))
441 with
open(os
.path
.join(home
, ".config", "foo", "a")) as file:
442 assert file.read() == "apple"
443 with
open(os
.path
.join(home
, ".config", "bar", "b")) as file:
444 assert file.read() == "banana"
445 with
open(os
.path
.join(home
, ".config", "bar", "c")) as file:
446 assert file.read() == "cherry"
447 with
open(os
.path
.join(home
, ".config", "baz", "d")) as file:
448 assert file.read() == "donut"
449 with
open(os
.path
.join(home
, ".config", "baz", "bizz", "g")) as file:
450 assert file.read() == "grape"
453 def test_link_glob_exclude_4(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
454 """Verify deep link globbing with multiple globbed exclusions."""
456 dotfiles
.write("config/foo/a", "apple")
457 dotfiles
.write("config/bar/b", "banana")
458 dotfiles
.write("config/bar/c", "cherry")
459 dotfiles
.write("config/baz/d", "donut")
460 dotfiles
.write("config/baz/buzz/e", "egg")
461 dotfiles
.write("config/baz/bizz/g", "grape")
462 dotfiles
.write("config/fiz/f", "fig")
463 dotfiles
.write_config(
476 "path": "config/*/*",
477 "exclude": ["config/baz/*", "config/fiz/*"],
485 assert not os
.path
.exists(os
.path
.join(home
, ".config", "baz"))
486 assert not os
.path
.exists(os
.path
.join(home
, ".config", "fiz"))
488 assert not os
.path
.islink(os
.path
.join(home
, ".config"))
489 assert not os
.path
.islink(os
.path
.join(home
, ".config", "foo"))
490 assert not os
.path
.islink(os
.path
.join(home
, ".config", "bar"))
491 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo", "a"))
492 with
open(os
.path
.join(home
, ".config", "foo", "a")) as file:
493 assert file.read() == "apple"
494 with
open(os
.path
.join(home
, ".config", "bar", "b")) as file:
495 assert file.read() == "banana"
496 with
open(os
.path
.join(home
, ".config", "bar", "c")) as file:
497 assert file.read() == "cherry"
500 def test_link_glob_multi_star(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
501 """Verify link globbing with deep-nested stars."""
503 dotfiles
.write("config/foo/a", "apple")
504 dotfiles
.write("config/bar/b", "banana")
505 dotfiles
.write("config/bar/c", "cherry")
506 dotfiles
.write_config(
508 {"defaults": {"link": {"glob": True, "create": True}}},
509 {"link": {"~/.config/": "config/*/*"}},
514 assert not os
.path
.islink(os
.path
.join(home
, ".config"))
515 assert not os
.path
.islink(os
.path
.join(home
, ".config", "foo"))
516 assert not os
.path
.islink(os
.path
.join(home
, ".config", "bar"))
517 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo", "a"))
518 with
open(os
.path
.join(home
, ".config", "foo", "a")) as file:
519 assert file.read() == "apple"
520 with
open(os
.path
.join(home
, ".config", "bar", "b")) as file:
521 assert file.read() == "banana"
522 with
open(os
.path
.join(home
, ".config", "bar", "c")) as file:
523 assert file.read() == "cherry"
526 @pytest.mark
.parametrize(
527 ("pattern", "expect_file"),
529 ("conf/*", lambda fruit
: fruit
),
530 ("conf/.*", lambda fruit
: "." + fruit
),
531 ("conf/[bc]*", lambda fruit
: fruit
if fruit
[0] in "bc" else None),
532 ("conf/*e", lambda fruit
: fruit
if fruit
[-1] == "e" else None),
533 ("conf/??r*", lambda fruit
: fruit
if fruit
[2] == "r" else None),
536 def test_link_glob_patterns(
538 expect_file
: Callable
[[str], Optional
[str]],
541 run_dotbot
: Callable
[..., None],
543 """Verify link glob pattern matching."""
545 fruits
= ["apple", "apricot", "banana", "cherry", "currant", "cantalope"]
547 dotfiles
.write("conf/" + fruit
, fruit
)
548 dotfiles
.write("conf/." + fruit
, "dot-" + fruit
)
549 dotfiles
.write_config(
551 {"defaults": {"link": {"glob": True, "create": True}}},
552 {"link": {"~/globtest": pattern
}},
558 expected
= expect_file(fruit
)
560 assert not os
.path
.exists(os
.path
.join(home
, "globtest", fruit
))
561 assert not os
.path
.exists(os
.path
.join(home
, "globtest", "." + fruit
))
562 elif "." in expected
:
563 assert not os
.path
.islink(os
.path
.join(home
, "globtest", fruit
))
564 assert os
.path
.islink(os
.path
.join(home
, "globtest", "." + fruit
))
565 else: # "." not in expected
566 assert os
.path
.islink(os
.path
.join(home
, "globtest", fruit
))
567 assert not os
.path
.islink(os
.path
.join(home
, "globtest", "." + fruit
))
570 def test_link_glob_recursive(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
571 """Verify recursive link globbing and exclusions."""
573 dotfiles
.write("config/foo/bar/a", "apple")
574 dotfiles
.write("config/foo/bar/b", "banana")
575 dotfiles
.write("config/foo/bar/c", "cherry")
576 dotfiles
.write_config(
578 {"defaults": {"link": {"glob": True, "create": True}}},
579 {"link": {"~/.config/": {"path": "config/**", "exclude": ["config/**/b"]}}},
584 assert not os
.path
.islink(os
.path
.join(home
, ".config"))
585 assert not os
.path
.islink(os
.path
.join(home
, ".config", "foo"))
586 assert not os
.path
.islink(os
.path
.join(home
, ".config", "foo", "bar"))
587 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo", "bar", "a"))
588 assert not os
.path
.exists(os
.path
.join(home
, ".config", "foo", "bar", "b"))
589 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo", "bar", "c"))
590 with
open(os
.path
.join(home
, ".config", "foo", "bar", "a")) as file:
591 assert file.read() == "apple"
592 with
open(os
.path
.join(home
, ".config", "foo", "bar", "c")) as file:
593 assert file.read() == "cherry"
596 def test_link_glob_no_match(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
597 """Verify that a glob with no match doesn't raise an error."""
600 dotfiles
.makedirs("foo")
601 dotfiles
.write_config(
603 {"defaults": {"link": {"glob": True, "create": True}}},
604 {"link": {"~/.config/foo": "foo/*"}},
610 def test_link_glob_single_match(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
611 """Verify linking works even when glob matches exactly one file."""
612 # regression test for https://github.com/anishathalye/dotbot/issues/282
614 dotfiles
.write("foo/a", "apple")
615 dotfiles
.write_config(
617 {"defaults": {"link": {"glob": True, "create": True}}},
618 {"link": {"~/.config/foo": "foo/*"}},
623 assert not os
.path
.islink(os
.path
.join(home
, ".config"))
624 assert not os
.path
.islink(os
.path
.join(home
, ".config", "foo"))
625 assert os
.path
.islink(os
.path
.join(home
, ".config", "foo", "a"))
626 with
open(os
.path
.join(home
, ".config", "foo", "a")) as file:
627 assert file.read() == "apple"
631 "sys.platform == 'win32'",
632 reason
="These if commands won't run on Windows",
634 def test_link_if(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
635 """Verify 'if' directives are checked when linking."""
637 os
.mkdir(os
.path
.join(home
, "d"))
638 dotfiles
.write("f", "apple")
639 dotfiles
.write_config(
643 "~/.f": {"path": "f", "if": "true"},
644 "~/.g": {"path": "f", "if": "false"},
645 "~/.h": {"path": "f", "if": "[ -d ~/d ]"},
646 "~/.i": {"path": "f", "if": "badcommand"},
653 assert not os
.path
.exists(os
.path
.join(home
, ".g"))
654 assert not os
.path
.exists(os
.path
.join(home
, ".i"))
655 with
open(os
.path
.join(home
, ".f")) as file:
656 assert file.read() == "apple"
657 with
open(os
.path
.join(home
, ".h")) as file:
658 assert file.read() == "apple"
662 "sys.platform == 'win32'",
663 reason
="These if commands won't run on Windows.",
665 def test_link_if_defaults(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
666 """Verify 'if' directive defaults are checked when linking."""
668 os
.mkdir(os
.path
.join(home
, "d"))
669 dotfiles
.write("f", "apple")
670 dotfiles
.write_config(
681 "~/.j": {"path": "f", "if": "true"},
682 "~/.k": {"path": "f"}, # default is false
689 assert not os
.path
.exists(os
.path
.join(home
, ".k"))
690 with
open(os
.path
.join(home
, ".j")) as file:
691 assert file.read() == "apple"
695 "sys.platform != 'win32'",
696 reason
="These if commands only run on Windows.",
698 def test_link_if_windows(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
699 """Verify 'if' directives are checked when linking (Windows only)."""
701 os
.mkdir(os
.path
.join(home
, "d"))
702 dotfiles
.write("f", "apple")
703 dotfiles
.write_config(
707 "~/.f": {"path": "f", "if": 'cmd /c "exit 0"'},
708 "~/.g": {"path": "f", "if": 'cmd /c "exit 1"'},
709 "~/.h": {"path": "f", "if": 'cmd /c "dir %USERPROFILE%\\d'},
710 "~/.i": {"path": "f", "if": 'cmd /c "badcommand"'},
717 assert not os
.path
.exists(os
.path
.join(home
, ".g"))
718 assert not os
.path
.exists(os
.path
.join(home
, ".i"))
719 with
open(os
.path
.join(home
, ".f")) as file:
720 assert file.read() == "apple"
721 with
open(os
.path
.join(home
, ".h")) as file:
722 assert file.read() == "apple"
726 "sys.platform != 'win32'",
727 reason
="These if commands only run on Windows.",
729 def test_link_if_defaults_windows(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
730 """Verify 'if' directive defaults are checked when linking (Windows only)."""
732 os
.mkdir(os
.path
.join(home
, "d"))
733 dotfiles
.write("f", "apple")
734 dotfiles
.write_config(
739 "if": 'cmd /c "exit 1"',
745 "~/.j": {"path": "f", "if": 'cmd /c "exit 0"'},
746 "~/.k": {"path": "f"}, # default is false
753 assert not os
.path
.exists(os
.path
.join(home
, ".k"))
754 with
open(os
.path
.join(home
, ".j")) as file:
755 assert file.read() == "apple"
758 @pytest.mark
.parametrize("ignore_missing", [True, False])
759 def test_link_ignore_missing(
760 ignore_missing
: bool, # noqa: FBT001
763 run_dotbot
: Callable
[..., None],
765 """Verify link 'ignore_missing' is respected when the target is missing."""
767 dotfiles
.write_config(
773 "ignore-missing": ignore_missing
,
782 assert os
.path
.islink(os
.path
.join(home
, "missing_link"))
784 with pytest
.raises(SystemExit):
788 def test_link_leaves_file(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
789 """Verify relink does not overwrite file."""
791 dotfiles
.write("f", "apple")
792 with
open(os
.path
.join(home
, ".f"), "w") as file:
794 dotfiles
.write_config([{"link": {"~/.f": "f"}}])
795 with pytest
.raises(SystemExit):
798 with
open(os
.path
.join(home
, ".f")) as file:
799 assert file.read() == "grape"
802 @pytest.mark
.parametrize("key", ["canonicalize-path", "canonicalize"])
803 def test_link_no_canonicalize(key
: str, home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
804 """Verify link canonicalization can be disabled."""
806 dotfiles
.write("f", "apple")
807 dotfiles
.write_config([{"defaults": {"link": {key
: False}}}, {"link": {"~/.f": {"path": "f"}}}])
810 os
.path
.join(home
, "dotfiles-symlink"),
811 target_is_directory
=True,
815 os
.path
.join(home
, "dotfiles-symlink", os
.path
.basename(dotfiles
.config_filename
)),
818 assert "dotfiles-symlink" in os
.readlink(os
.path
.join(home
, ".f"))
821 def test_link_prefix(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
822 """Verify link prefixes are prepended."""
824 dotfiles
.write("conf/a", "apple")
825 dotfiles
.write("conf/b", "banana")
826 dotfiles
.write("conf/c", "cherry")
827 dotfiles
.write_config(
841 with
open(os
.path
.join(home
, ".a")) as file:
842 assert file.read() == "apple"
843 with
open(os
.path
.join(home
, ".b")) as file:
844 assert file.read() == "banana"
845 with
open(os
.path
.join(home
, ".c")) as file:
846 assert file.read() == "cherry"
849 def test_link_relative(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
850 """Test relative linking works."""
852 dotfiles
.write("f", "apple")
853 dotfiles
.write("d/e", "grape")
854 dotfiles
.write_config(
880 f
= os
.readlink(os
.path
.join(home
, ".f"))
881 if sys
.platform
== "win32" and f
.startswith("\\\\?\\"):
883 assert f
== os
.path
.join(dotfiles
.directory
, "f")
885 frel
= os
.readlink(os
.path
.join(home
, ".frel"))
886 if sys
.platform
== "win32" and frel
.startswith("\\\\?\\"):
888 assert frel
== os
.path
.normpath("../../dotfiles/f")
890 nested_frel
= os
.readlink(os
.path
.join(home
, "nested", ".frel"))
891 if sys
.platform
== "win32" and nested_frel
.startswith("\\\\?\\"):
892 nested_frel
= nested_frel
[4:]
893 assert nested_frel
== os
.path
.normpath("../../../dotfiles/f")
895 d
= os
.readlink(os
.path
.join(home
, ".d"))
896 if sys
.platform
== "win32" and d
.startswith("\\\\?\\"):
898 assert d
== os
.path
.normpath("../../dotfiles/d")
900 with
open(os
.path
.join(home
, ".f")) as file:
901 assert file.read() == "apple"
902 with
open(os
.path
.join(home
, ".frel")) as file:
903 assert file.read() == "apple"
904 with
open(os
.path
.join(home
, "nested", ".frel")) as file:
905 assert file.read() == "apple"
906 with
open(os
.path
.join(home
, ".d", "e")) as file:
907 assert file.read() == "grape"
910 def test_link_relink_leaves_file(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
911 """Verify relink does not overwrite file."""
913 dotfiles
.write("f", "apple")
914 with
open(os
.path
.join(home
, ".f"), "w") as file:
916 dotfiles
.write_config([{"link": {"~/.f": {"path": "f", "relink": True}}}])
917 with pytest
.raises(SystemExit):
919 with
open(os
.path
.join(home
, ".f")) as file:
920 assert file.read() == "grape"
923 def test_link_relink_overwrite_symlink(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
924 """Verify relink overwrites symlinks."""
926 dotfiles
.write("f", "apple")
927 with
open(os
.path
.join(home
, "f"), "w") as file:
929 os
.symlink(os
.path
.join(home
, "f"), os
.path
.join(home
, ".f"))
930 dotfiles
.write_config([{"link": {"~/.f": {"path": "f", "relink": True}}}])
932 with
open(os
.path
.join(home
, ".f")) as file:
933 assert file.read() == "apple"
936 def test_link_relink_relative_leaves_file(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
937 """Verify relink relative does not incorrectly relink file."""
939 dotfiles
.write("f", "apple")
940 with
open(os
.path
.join(home
, ".f"), "w") as file:
953 dotfiles
.write_config(config
)
956 mtime
= os
.stat(os
.path
.join(home
, ".folder", "f")).st_mtime
958 config
[0]["link"]["~/.folder/f"]["relink"] = True
959 dotfiles
.write_config(config
)
962 new_mtime
= os
.stat(os
.path
.join(home
, ".folder", "f")).st_mtime
963 assert mtime
== new_mtime
966 def test_source_is_not_overwritten_by_symlink_trickery(
967 capsys
: pytest
.CaptureFixture
[str], home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]
969 dotfiles_path
= pathlib
.Path(dotfiles
.directory
)
970 home_path
= pathlib
.Path(home
)
973 # * A symlink exists from `~/.ssh` to `ssh` in the dotfiles directory.
974 # * Dotbot is configured to force-recreate a symlink between two files
975 # when, in reality, it's actually the same file when resolved.
976 ssh_config
= (dotfiles_path
/ "ssh/config").absolute()
977 os
.mkdir(str(ssh_config
.parent
))
978 ssh_config
.write_text("preserve me!")
979 os
.symlink(str(ssh_config
.parent
), str(home_path
/ ".ssh"))
980 dotfiles
.write_config(
993 # When symlinks are resolved, these are actually the same file.
994 "~/.ssh/config": "ssh/config",
1001 with pytest
.raises(SystemExit):
1004 stdout
, _
= capsys
.readouterr()
1005 assert "appears to be the same file" in stdout
1006 # Verify that the file was not overwritten.
1007 assert ssh_config
.read_text() == "preserve me!"
1010 def test_link_defaults_1(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
1011 """Verify that link doesn't overwrite non-dotfiles links by default."""
1013 with
open(os
.path
.join(home
, "f"), "w") as file:
1015 os
.symlink(os
.path
.join(home
, "f"), os
.path
.join(home
, ".f"))
1016 dotfiles
.write("f", "apple")
1017 dotfiles
.write_config(
1020 "link": {"~/.f": "f"},
1024 with pytest
.raises(SystemExit):
1027 with
open(os
.path
.join(home
, ".f")) as file:
1028 assert file.read() == "grape"
1031 def test_link_defaults_2(home
: str, dotfiles
: Dotfiles
, run_dotbot
: Callable
[..., None]) -> None:
1032 """Verify that explicit link defaults override the implicit default."""
1034 with
open(os
.path
.join(home
, "f"), "w") as file:
1036 os
.symlink(os
.path
.join(home
, "f"), os
.path
.join(home
, ".f"))
1037 dotfiles
.write("f", "apple")
1038 dotfiles
.write_config(
1040 {"defaults": {"link": {"relink": True}}},
1041 {"link": {"~/.f": "f"}},
1046 with
open(os
.path
.join(home
, ".f")) as file:
1047 assert file.read() == "apple"