1
0
Fork 0
mirror of synced 2024-11-03 00:48:57 -04:00
dotbot/tests/test_link.py
2023-07-28 20:15:19 +03:00

1196 lines
39 KiB
Python

import os
import sys
import pytest
def test_link_canonicalization(home, dotfiles, run_dotbot):
"""Verify links to symlinked destinations are canonical.
"Canonical", here, means that dotbot does not create symlinks
that point to intermediary symlinks.
"""
dotfiles.write("f", "apple")
dotfiles.write_config([{"link": {"~/.f": {"path": "f"}}}])
# Point to the config file in a symlinked dotfiles directory.
dotfiles_symlink = os.path.join(home, "dotfiles-symlink")
os.symlink(dotfiles.directory, dotfiles_symlink)
config_file = os.path.join(dotfiles_symlink, os.path.basename(dotfiles.config_filename))
run_dotbot("-c", config_file, custom=True)
expected = os.path.join(dotfiles.directory, "f")
actual = os.readlink(os.path.abspath(os.path.expanduser("~/.f")))
if sys.platform[:5] == "win32" and actual.startswith("\\\\?\\"):
actual = actual[4:]
assert expected == actual
@pytest.mark.parametrize("dst", ("~/.f", "~/f"))
@pytest.mark.parametrize("include_force", (True, False))
def test_link_default_source(root, home, dst, include_force, dotfiles, run_dotbot):
"""Verify that default sources are calculated correctly.
This test includes verifying files with and without leading periods,
as well as verifying handling of None dict values.
"""
dotfiles.write("f", "apple")
config = [
{
"link": {
dst: {"force": False} if include_force else None,
}
}
]
dotfiles.write_config(config)
run_dotbot()
with open(os.path.abspath(os.path.expanduser(dst)), "r") as file:
assert file.read() == "apple"
def test_link_environment_user_expansion_target(home, dotfiles, run_dotbot):
"""Verify link expands user in target."""
src = "~/f"
target = "~/g"
with open(os.path.abspath(os.path.expanduser(src)), "w") as file:
file.write("apple")
dotfiles.write_config([{"link": {target: src}}])
run_dotbot()
with open(os.path.abspath(os.path.expanduser(target)), "r") as file:
assert file.read() == "apple"
def test_link_environment_variable_expansion_source(monkeypatch, root, home, dotfiles, run_dotbot):
"""Verify link expands environment variables in source."""
monkeypatch.setenv("APPLE", "h")
target = "~/.i"
src = "$APPLE"
dotfiles.write("h", "grape")
dotfiles.write_config([{"link": {target: src}}])
run_dotbot()
with open(os.path.abspath(os.path.expanduser(target)), "r") as file:
assert file.read() == "grape"
def test_link_environment_variable_expansion_source_extended(
monkeypatch, root, home, dotfiles, run_dotbot
):
"""Verify link expands environment variables in extended config syntax."""
monkeypatch.setenv("APPLE", "h")
target = "~/.i"
src = "$APPLE"
dotfiles.write("h", "grape")
dotfiles.write_config([{"link": {target: {"path": src, "relink": True}}}])
run_dotbot()
with open(os.path.abspath(os.path.expanduser(target)), "r") as file:
assert file.read() == "grape"
def test_link_environment_variable_expansion_target(monkeypatch, root, home, dotfiles, run_dotbot):
"""Verify link expands environment variables in target.
If the variable doesn't exist, the "variable" must not be replaced.
"""
monkeypatch.setenv("ORANGE", ".config")
monkeypatch.setenv("BANANA", "g")
monkeypatch.delenv("PEAR", raising=False)
dotfiles.write("f", "apple")
dotfiles.write("h", "grape")
config = [
{
"link": {
"~/${ORANGE}/$BANANA": {
"path": "f",
"create": True,
},
"~/$PEAR": "h",
}
}
]
dotfiles.write_config(config)
run_dotbot()
with open(os.path.join(home, ".config", "g"), "r") as file:
assert file.read() == "apple"
with open(os.path.join(home, "$PEAR"), "r") as file:
assert file.read() == "grape"
def test_link_environment_variable_unset(monkeypatch, root, home, dotfiles, run_dotbot):
"""Verify link leaves unset environment variables."""
monkeypatch.delenv("ORANGE", raising=False)
dotfiles.write("$ORANGE", "apple")
dotfiles.write_config([{"link": {"~/f": "$ORANGE"}}])
run_dotbot()
with open(os.path.join(home, "f"), "r") as file:
assert file.read() == "apple"
def test_link_force_leaves_when_nonexistent(root, home, dotfiles, run_dotbot):
"""Verify force doesn't erase sources when targets are nonexistent."""
os.mkdir(os.path.join(home, "dir"))
open(os.path.join(home, "file"), "a").close()
config = [
{
"link": {
"~/dir": {"path": "dir", "force": True},
"~/file": {"path": "file", "force": True},
}
}
]
dotfiles.write_config(config)
with pytest.raises(SystemExit):
run_dotbot()
assert os.path.isdir(os.path.join(home, "dir"))
assert os.path.isfile(os.path.join(home, "file"))
def test_link_force_overwrite_symlink(home, dotfiles, run_dotbot):
"""Verify force overwrites a symlinked directory."""
os.mkdir(os.path.join(home, "dir"))
dotfiles.write("dir/f")
os.symlink(home, os.path.join(home, ".dir"))
config = [{"link": {"~/.dir": {"path": "dir", "force": True}}}]
dotfiles.write_config(config)
run_dotbot()
assert os.path.isfile(os.path.join(home, ".dir", "f"))
def test_link_force_overwrite_source(home, dotfiles, run_dotbot):
"""Verify force overwrites a symlinked directory."""
os.symlink(home, os.path.join(home, ".link"))
with open(os.path.join(home, "file"), "w") as file:
file.write("")
os.mkdir(os.path.join(home, "dir"))
dotfiles.write("dir/f")
config = [
{
"link": {
"~/.link": {"path": "dir", "force": True},
"~/file": {"path": "dir", "force": True},
"~/dir": {"path": "dir", "force": True},
}
}
]
dotfiles.write_config(config)
run_dotbot()
assert os.path.isfile(os.path.join(home, ".link", "f"))
assert os.path.isfile(os.path.join(home, "file", "f"))
assert os.path.isfile(os.path.join(home, "dir", "f"))
def test_backup_mirrors_absolute_path(home, dotfiles, run_dotbot):
"""Verify backup path from backup dir is same as original path from root."""
os.symlink(home, os.path.join(home, ".f"))
dotfiles.write("f")
backup_root = os.path.join(dotfiles.directory, "backup")
config = [{"link": {"~/.f": {"path": "f", "force": True, "backup-root": backup_root}}}]
dotfiles.write_config(config)
run_dotbot()
assert os.path.isdir(os.path.join(backup_root, home[1:]))
def test_link_force_backups_source(home, dotfiles, run_dotbot):
"""Verify force backups a symlinked directory."""
os.symlink(home, os.path.join(home, ".link"))
with open(os.path.join(home, "file"), "w") as file:
file.write("apple")
os.mkdir(os.path.join(home, "dir"))
dotfiles.write("dir/f", "peach")
backup_root = os.path.join(dotfiles.directory, "backup")
config = [
{
"link": {
"~/.link": {"path": "dir", "force": True, "backup-root": backup_root},
"~/file": {"path": "dir", "force": True, "backup-root": backup_root},
"~/dir": {"path": "dir", "force": True, "backup-root": backup_root},
}
}
]
dotfiles.write_config(config)
run_dotbot()
backup_home = os.path.join(backup_root, home[1:])
dir_items = os.listdir(backup_home)
test_items = (".link", "file", "dir")
backuped = {
test_item: dir_item
for dir_item in dir_items
for test_item in test_items
if test_item in dir_item
}
assert set(backuped.keys()) == set(test_items)
assert os.path.islink(os.path.join(backup_home, backuped[".link"]))
assert home in os.readlink(os.path.join(backup_home, backuped[".link"]))
assert os.path.isfile(os.path.join(backup_home, backuped["file"]))
with open(os.path.join(backup_home, backuped["file"])) as file:
assert file.read() == "apple"
assert os.path.isdir(os.path.join(backup_home, backuped["dir"]))
for link in test_items:
with open(os.path.join(home, link, "f")) as file:
assert file.read() == "peach"
def test_link_force_aborts_on_failed_backup(home, dotfiles, run_dotbot):
"""Verify force is aborted if backup fails."""
os.symlink(home, os.path.join(home, ".link"))
with open(os.path.join(home, "file"), "w") as file:
file.write("apple")
os.mkdir(os.path.join(home, "dir"))
dotfiles.write("backup")
dotfiles.write("dir/f")
backup_root = os.path.join(dotfiles.directory, "backup")
config = [
{
"link": {
"~/.link": {"path": "dir", "force": True, "backup-root": backup_root},
"~/file": {"path": "dir", "force": True, "backup-root": backup_root},
"~/dir": {"path": "dir", "force": True, "backup-root": backup_root},
}
}
]
dotfiles.write_config(config)
with pytest.raises(SystemExit):
run_dotbot()
assert os.path.islink(os.path.join(home, ".link"))
assert home in os.readlink(os.path.join(home, ".link"))
assert os.path.isfile(os.path.join(home, "file"))
with open(os.path.join(home, "file")) as file:
assert file.read() == "apple"
assert os.path.isdir(os.path.join(home, "dir"))
def test_no_backup_when_no_force_relink(home, dotfiles, run_dotbot):
"""Verify backup is a no-op without an accompanying force or relink."""
os.symlink(home, os.path.join(home, ".link"))
with open(os.path.join(home, "file"), "w") as file:
file.write("apple")
os.mkdir(os.path.join(home, "dir"))
dotfiles.write("dir/f")
backup_root = os.path.join(dotfiles.directory, "backup")
config = [
{
"link": {
"~/.link": {"path": "dir", "backup-root": backup_root},
"~/file": {"path": "dir", "backup-root": backup_root},
"~/dir": {"path": "dir", "backup-root": backup_root},
"~/.new": {"path": "dir", "backup-root": backup_root},
}
}
]
dotfiles.write_config(config)
with pytest.raises(SystemExit):
run_dotbot()
assert not os.path.exists(backup_root)
def test_link_glob_1(home, dotfiles, run_dotbot):
"""Verify globbing works."""
dotfiles.write("bin/a", "apple")
dotfiles.write("bin/b", "banana")
dotfiles.write("bin/c", "cherry")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/bin": "bin/*"}},
]
)
run_dotbot()
with open(os.path.join(home, "bin", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, "bin", "b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, "bin", "c")) as file:
assert file.read() == "cherry"
def test_link_glob_2(home, dotfiles, run_dotbot):
"""Verify globbing works with a trailing slash in the source."""
dotfiles.write("bin/a", "apple")
dotfiles.write("bin/b", "banana")
dotfiles.write("bin/c", "cherry")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/bin/": "bin/*"}},
]
)
run_dotbot()
with open(os.path.join(home, "bin", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, "bin", "b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, "bin", "c")) as file:
assert file.read() == "cherry"
def test_link_glob_3(home, dotfiles, run_dotbot):
"""Verify globbing works with hidden ("period-prefixed") files."""
dotfiles.write("bin/.a", "dot-apple")
dotfiles.write("bin/.b", "dot-banana")
dotfiles.write("bin/.c", "dot-cherry")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/bin/": "bin/.*"}},
]
)
run_dotbot()
with open(os.path.join(home, "bin", ".a")) as file:
assert file.read() == "dot-apple"
with open(os.path.join(home, "bin", ".b")) as file:
assert file.read() == "dot-banana"
with open(os.path.join(home, "bin", ".c")) as file:
assert file.read() == "dot-cherry"
def test_link_glob_4(home, dotfiles, run_dotbot):
"""Verify globbing works at the root of the home and dotfiles directories."""
dotfiles.write(".a", "dot-apple")
dotfiles.write(".b", "dot-banana")
dotfiles.write(".c", "dot-cherry")
dotfiles.write_config(
[
{
"link": {
"~": {
"path": ".*",
"glob": True,
},
},
}
]
)
run_dotbot()
with open(os.path.join(home, ".a")) as file:
assert file.read() == "dot-apple"
with open(os.path.join(home, ".b")) as file:
assert file.read() == "dot-banana"
with open(os.path.join(home, ".c")) as file:
assert file.read() == "dot-cherry"
@pytest.mark.parametrize("path", ("foo", "foo/"))
def test_link_glob_ignore_no_glob_chars(path, home, dotfiles, run_dotbot):
"""Verify ambiguous link globbing fails."""
dotfiles.makedirs("foo")
dotfiles.write_config(
[
{
"link": {
"~/foo/": {
"path": path,
"glob": True,
}
}
}
]
)
run_dotbot()
assert os.path.islink(os.path.join(home, "foo"))
assert os.path.exists(os.path.join(home, "foo"))
def test_link_glob_exclude_1(home, dotfiles, run_dotbot):
"""Verify link globbing with an explicit exclusion."""
dotfiles.write("config/foo/a", "apple")
dotfiles.write("config/bar/b", "banana")
dotfiles.write("config/bar/c", "cherry")
dotfiles.write("config/baz/d", "donut")
dotfiles.write_config(
[
{
"defaults": {
"link": {
"glob": True,
"create": True,
},
},
},
{
"link": {
"~/.config/": {
"path": "config/*",
"exclude": ["config/baz"],
},
},
},
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".config", "baz"))
assert not os.path.islink(os.path.join(home, ".config"))
assert os.path.islink(os.path.join(home, ".config", "foo"))
assert os.path.islink(os.path.join(home, ".config", "bar"))
with open(os.path.join(home, ".config", "foo", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".config", "bar", "b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, ".config", "bar", "c")) as file:
assert file.read() == "cherry"
def test_link_glob_exclude_2(home, dotfiles, run_dotbot):
"""Verify deep link globbing with a globbed exclusion."""
dotfiles.write("config/foo/a", "apple")
dotfiles.write("config/bar/b", "banana")
dotfiles.write("config/bar/c", "cherry")
dotfiles.write("config/baz/d", "donut")
dotfiles.write("config/baz/buzz/e", "egg")
dotfiles.write_config(
[
{
"defaults": {
"link": {
"glob": True,
"create": True,
},
},
},
{
"link": {
"~/.config/": {
"path": "config/*/*",
"exclude": ["config/baz/*"],
},
},
},
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".config", "baz"))
assert not os.path.islink(os.path.join(home, ".config"))
assert not os.path.islink(os.path.join(home, ".config", "foo"))
assert not os.path.islink(os.path.join(home, ".config", "bar"))
assert os.path.islink(os.path.join(home, ".config", "foo", "a"))
with open(os.path.join(home, ".config", "foo", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".config", "bar", "b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, ".config", "bar", "c")) as file:
assert file.read() == "cherry"
def test_link_glob_exclude_3(home, dotfiles, run_dotbot):
"""Verify deep link globbing with an explicit exclusion."""
dotfiles.write("config/foo/a", "apple")
dotfiles.write("config/bar/b", "banana")
dotfiles.write("config/bar/c", "cherry")
dotfiles.write("config/baz/d", "donut")
dotfiles.write("config/baz/buzz/e", "egg")
dotfiles.write("config/baz/bizz/g", "grape")
dotfiles.write_config(
[
{
"defaults": {
"link": {
"glob": True,
"create": True,
},
},
},
{
"link": {
"~/.config/": {
"path": "config/*/*",
"exclude": ["config/baz/buzz"],
},
},
},
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".config", "baz", "buzz"))
assert not os.path.islink(os.path.join(home, ".config"))
assert not os.path.islink(os.path.join(home, ".config", "foo"))
assert not os.path.islink(os.path.join(home, ".config", "bar"))
assert not os.path.islink(os.path.join(home, ".config", "baz"))
assert os.path.islink(os.path.join(home, ".config", "baz", "bizz"))
assert os.path.islink(os.path.join(home, ".config", "foo", "a"))
with open(os.path.join(home, ".config", "foo", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".config", "bar", "b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, ".config", "bar", "c")) as file:
assert file.read() == "cherry"
with open(os.path.join(home, ".config", "baz", "d")) as file:
assert file.read() == "donut"
with open(os.path.join(home, ".config", "baz", "bizz", "g")) as file:
assert file.read() == "grape"
def test_link_glob_exclude_4(home, dotfiles, run_dotbot):
"""Verify deep link globbing with multiple globbed exclusions."""
dotfiles.write("config/foo/a", "apple")
dotfiles.write("config/bar/b", "banana")
dotfiles.write("config/bar/c", "cherry")
dotfiles.write("config/baz/d", "donut")
dotfiles.write("config/baz/buzz/e", "egg")
dotfiles.write("config/baz/bizz/g", "grape")
dotfiles.write("config/fiz/f", "fig")
dotfiles.write_config(
[
{
"defaults": {
"link": {
"glob": True,
"create": True,
},
},
},
{
"link": {
"~/.config/": {
"path": "config/*/*",
"exclude": ["config/baz/*", "config/fiz/*"],
},
},
},
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".config", "baz"))
assert not os.path.exists(os.path.join(home, ".config", "fiz"))
assert not os.path.islink(os.path.join(home, ".config"))
assert not os.path.islink(os.path.join(home, ".config", "foo"))
assert not os.path.islink(os.path.join(home, ".config", "bar"))
assert os.path.islink(os.path.join(home, ".config", "foo", "a"))
with open(os.path.join(home, ".config", "foo", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".config", "bar", "b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, ".config", "bar", "c")) as file:
assert file.read() == "cherry"
def test_link_glob_multi_star(home, dotfiles, run_dotbot):
"""Verify link globbing with deep-nested stars."""
dotfiles.write("config/foo/a", "apple")
dotfiles.write("config/bar/b", "banana")
dotfiles.write("config/bar/c", "cherry")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/.config/": "config/*/*"}},
]
)
run_dotbot()
assert not os.path.islink(os.path.join(home, ".config"))
assert not os.path.islink(os.path.join(home, ".config", "foo"))
assert not os.path.islink(os.path.join(home, ".config", "bar"))
assert os.path.islink(os.path.join(home, ".config", "foo", "a"))
with open(os.path.join(home, ".config", "foo", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".config", "bar", "b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, ".config", "bar", "c")) as file:
assert file.read() == "cherry"
@pytest.mark.parametrize(
"pattern, expect_file",
(
("conf/*", lambda fruit: fruit),
("conf/.*", lambda fruit: "." + fruit),
("conf/[bc]*", lambda fruit: fruit if fruit[0] in "bc" else None),
("conf/*e", lambda fruit: fruit if fruit[-1] == "e" else None),
("conf/??r*", lambda fruit: fruit if fruit[2] == "r" else None),
),
)
def test_link_glob_patterns(pattern, expect_file, home, dotfiles, run_dotbot):
"""Verify link glob pattern matching."""
fruits = ["apple", "apricot", "banana", "cherry", "currant", "cantalope"]
[dotfiles.write("conf/" + fruit, fruit) for fruit in fruits]
[dotfiles.write("conf/." + fruit, "dot-" + fruit) for fruit in fruits]
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/globtest": pattern}},
]
)
run_dotbot()
for fruit in fruits:
if expect_file(fruit) is None:
assert not os.path.exists(os.path.join(home, "globtest", fruit))
assert not os.path.exists(os.path.join(home, "globtest", "." + fruit))
elif "." in expect_file(fruit):
assert not os.path.islink(os.path.join(home, "globtest", fruit))
assert os.path.islink(os.path.join(home, "globtest", "." + fruit))
else: # "." not in expect_file(fruit)
assert os.path.islink(os.path.join(home, "globtest", fruit))
assert not os.path.islink(os.path.join(home, "globtest", "." + fruit))
@pytest.mark.skipif(
"sys.version_info < (3, 5)",
reason="Python 3.5 required for ** globbing",
)
def test_link_glob_recursive(home, dotfiles, run_dotbot):
"""Verify recursive link globbing and exclusions."""
dotfiles.write("config/foo/bar/a", "apple")
dotfiles.write("config/foo/bar/b", "banana")
dotfiles.write("config/foo/bar/c", "cherry")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/.config/": {"path": "config/**", "exclude": ["config/**/b"]}}},
]
)
run_dotbot()
assert not os.path.islink(os.path.join(home, ".config"))
assert not os.path.islink(os.path.join(home, ".config", "foo"))
assert not os.path.islink(os.path.join(home, ".config", "foo", "bar"))
assert os.path.islink(os.path.join(home, ".config", "foo", "bar", "a"))
assert not os.path.exists(os.path.join(home, ".config", "foo", "bar", "b"))
assert os.path.islink(os.path.join(home, ".config", "foo", "bar", "c"))
with open(os.path.join(home, ".config", "foo", "bar", "a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".config", "foo", "bar", "c")) as file:
assert file.read() == "cherry"
def test_link_glob_no_match(home, dotfiles, run_dotbot):
"""Verify that a glob with no match doesn't raise an error."""
dotfiles.makedirs("foo")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/.config/foo": "foo/*"}},
]
)
run_dotbot()
def test_link_glob_single_match(home, dotfiles, run_dotbot):
"""Verify linking works even when glob matches exactly one file."""
# regression test for https://github.com/anishathalye/dotbot/issues/282
dotfiles.write("foo/a", "apple")
dotfiles.write_config(
[
{"defaults": {"link": {"glob": True, "create": True}}},
{"link": {"~/.config/foo": "foo/*"}},
]
)
run_dotbot()
assert not os.path.islink(os.path.join(home, ".config"))
assert not os.path.islink(os.path.join(home, ".config", "foo"))
assert os.path.islink(os.path.join(home, ".config", "foo", "a"))
with open(os.path.join(home, ".config", "foo", "a")) as file:
assert file.read() == "apple"
@pytest.mark.skipif(
"sys.platform[:5] == 'win32'",
reason="These if commands won't run on Windows",
)
def test_link_if(home, dotfiles, run_dotbot):
"""Verify 'if' directives are checked when linking."""
os.mkdir(os.path.join(home, "d"))
dotfiles.write("f", "apple")
dotfiles.write_config(
[
{
"link": {
"~/.f": {"path": "f", "if": "true"},
"~/.g": {"path": "f", "if": "false"},
"~/.h": {"path": "f", "if": "[ -d ~/d ]"},
"~/.i": {"path": "f", "if": "badcommand"},
},
}
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".g"))
assert not os.path.exists(os.path.join(home, ".i"))
with open(os.path.join(home, ".f")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".h")) as file:
assert file.read() == "apple"
@pytest.mark.skipif(
"sys.platform[:5] == 'win32'",
reason="These if commands won't run on Windows.",
)
def test_link_if_defaults(home, dotfiles, run_dotbot):
"""Verify 'if' directive defaults are checked when linking."""
os.mkdir(os.path.join(home, "d"))
dotfiles.write("f", "apple")
dotfiles.write_config(
[
{
"defaults": {
"link": {
"if": "false",
},
},
},
{
"link": {
"~/.j": {"path": "f", "if": "true"},
"~/.k": {"path": "f"}, # default is false
},
},
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".k"))
with open(os.path.join(home, ".j")) as file:
assert file.read() == "apple"
@pytest.mark.skipif(
"sys.platform[:5] != 'win32'",
reason="These if commands only run on Windows.",
)
def test_link_if_windows(home, dotfiles, run_dotbot):
"""Verify 'if' directives are checked when linking (Windows only)."""
os.mkdir(os.path.join(home, "d"))
dotfiles.write("f", "apple")
dotfiles.write_config(
[
{
"link": {
"~/.f": {"path": "f", "if": 'cmd /c "exit 0"'},
"~/.g": {"path": "f", "if": 'cmd /c "exit 1"'},
"~/.h": {"path": "f", "if": 'cmd /c "dir %USERPROFILE%\\d'},
"~/.i": {"path": "f", "if": 'cmd /c "badcommand"'},
},
}
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".g"))
assert not os.path.exists(os.path.join(home, ".i"))
with open(os.path.join(home, ".f")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".h")) as file:
assert file.read() == "apple"
@pytest.mark.skipif(
"sys.platform[:5] != 'win32'",
reason="These if commands only run on Windows",
)
def test_link_if_defaults_windows(home, dotfiles, run_dotbot):
"""Verify 'if' directive defaults are checked when linking (Windows only)."""
os.mkdir(os.path.join(home, "d"))
dotfiles.write("f", "apple")
dotfiles.write_config(
[
{
"defaults": {
"link": {
"if": 'cmd /c "exit 1"',
},
},
},
{
"link": {
"~/.j": {"path": "f", "if": 'cmd /c "exit 0"'},
"~/.k": {"path": "f"}, # default is false
},
},
]
)
run_dotbot()
assert not os.path.exists(os.path.join(home, ".k"))
with open(os.path.join(home, ".j")) as file:
assert file.read() == "apple"
@pytest.mark.parametrize("ignore_missing", (True, False))
def test_link_ignore_missing(ignore_missing, home, dotfiles, run_dotbot):
"""Verify link 'ignore_missing' is respected when the target is missing."""
dotfiles.write_config(
[
{
"link": {
"~/missing_link": {
"path": "missing",
"ignore-missing": ignore_missing,
},
},
}
]
)
if ignore_missing:
run_dotbot()
assert os.path.islink(os.path.join(home, "missing_link"))
else:
with pytest.raises(SystemExit):
run_dotbot()
def test_link_leaves_file(home, dotfiles, run_dotbot):
"""Verify relink does not overwrite file."""
dotfiles.write("f", "apple")
with open(os.path.join(home, ".f"), "w") as file:
file.write("grape")
dotfiles.write_config([{"link": {"~/.f": "f"}}])
with pytest.raises(SystemExit):
run_dotbot()
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "grape"
@pytest.mark.parametrize("key", ("canonicalize-path", "canonicalize"))
def test_link_no_canonicalize(key, home, dotfiles, run_dotbot):
"""Verify link canonicalization can be disabled."""
dotfiles.write("f", "apple")
dotfiles.write_config([{"defaults": {"link": {key: False}}}, {"link": {"~/.f": {"path": "f"}}}])
try:
os.symlink(
dotfiles.directory,
os.path.join(home, "dotfiles-symlink"),
target_is_directory=True,
)
except TypeError:
# Python 2 compatibility:
# target_is_directory is only consistently available after Python 3.3.
os.symlink(
dotfiles.directory,
os.path.join(home, "dotfiles-symlink"),
)
run_dotbot(
"-c",
os.path.join(home, "dotfiles-symlink", os.path.basename(dotfiles.config_filename)),
custom=True,
)
assert "dotfiles-symlink" in os.readlink(os.path.join(home, ".f"))
def test_link_prefix(home, dotfiles, run_dotbot):
"""Verify link prefixes are prepended."""
dotfiles.write("conf/a", "apple")
dotfiles.write("conf/b", "banana")
dotfiles.write("conf/c", "cherry")
dotfiles.write_config(
[
{
"link": {
"~/": {
"glob": True,
"path": "conf/*",
"prefix": ".",
},
},
}
]
)
run_dotbot()
with open(os.path.join(home, ".a")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".b")) as file:
assert file.read() == "banana"
with open(os.path.join(home, ".c")) as file:
assert file.read() == "cherry"
def test_link_relative(home, dotfiles, run_dotbot):
"""Test relative linking works."""
dotfiles.write("f", "apple")
dotfiles.write("d/e", "grape")
dotfiles.write_config(
[
{
"link": {
"~/.f": {
"path": "f",
},
"~/.frel": {
"path": "f",
"relative": True,
},
"~/nested/.frel": {
"path": "f",
"relative": True,
"create": True,
},
"~/.d": {
"path": "d",
"relative": True,
},
},
}
]
)
run_dotbot()
f = os.readlink(os.path.join(home, ".f"))
if sys.platform[:5] == "win32" and f.startswith("\\\\?\\"):
f = f[4:]
assert f == os.path.join(dotfiles.directory, "f")
frel = os.readlink(os.path.join(home, ".frel"))
if sys.platform[:5] == "win32" and frel.startswith("\\\\?\\"):
frel = frel[4:]
assert frel == os.path.normpath("../../dotfiles/f")
nested_frel = os.readlink(os.path.join(home, "nested", ".frel"))
if sys.platform[:5] == "win32" and nested_frel.startswith("\\\\?\\"):
nested_frel = nested_frel[4:]
assert nested_frel == os.path.normpath("../../../dotfiles/f")
d = os.readlink(os.path.join(home, ".d"))
if sys.platform[:5] == "win32" and d.startswith("\\\\?\\"):
d = d[4:]
assert d == os.path.normpath("../../dotfiles/d")
with open(os.path.join(home, ".f")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".frel")) as file:
assert file.read() == "apple"
with open(os.path.join(home, "nested", ".frel")) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".d", "e")) as file:
assert file.read() == "grape"
def test_link_relink_leaves_file(home, dotfiles, run_dotbot):
"""Verify relink does not overwrite file."""
dotfiles.write("f", "apple")
with open(os.path.join(home, ".f"), "w") as file:
file.write("grape")
dotfiles.write_config([{"link": {"~/.f": {"path": "f", "relink": True}}}])
with pytest.raises(SystemExit):
run_dotbot()
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "grape"
def test_link_relink_overwrite_symlink(home, dotfiles, run_dotbot):
"""Verify relink overwrites symlinks."""
dotfiles.write("f", "apple")
with open(os.path.join(home, "f"), "w") as file:
file.write("grape")
os.symlink(os.path.join(home, "f"), os.path.join(home, ".f"))
dotfiles.write_config([{"link": {"~/.f": {"path": "f", "relink": True}}}])
run_dotbot()
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "apple"
def test_link_relink_backups_symlink(home, dotfiles, run_dotbot):
"""Verify relink backups symlinks."""
dotfiles.write("f", "apple")
with open(os.path.join(home, "f"), "w") as file:
file.write("grape")
os.symlink(os.path.join(home, "f"), os.path.join(home, ".f"))
backup_root = os.path.join(dotfiles.directory, "backup")
dotfiles.write_config(
[{"link": {"~/.f": {"path": "f", "relink": True, "backup-root": backup_root}}}]
)
run_dotbot()
backup_home = os.path.join(backup_root, home[1:])
assert os.path.isdir(backup_home)
dir_items = os.listdir(backup_home)
assert len(dir_items) == 1
backuped = os.path.join(backup_home, dir_items[0])
assert os.path.islink(backuped)
with open(backuped, "r") as file:
assert file.read() == "grape"
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "apple"
def test_link_relink_aborts_on_failed_backup(home, dotfiles, run_dotbot):
"""Verify relink is aborted if backup fails."""
dotfiles.write("f", "apple")
with open(os.path.join(home, "f"), "w") as file:
file.write("grape")
os.symlink(os.path.join(home, "f"), os.path.join(home, ".f"))
dotfiles.write("backup")
backup_root = os.path.join(dotfiles.directory, "backup")
dotfiles.write_config(
[{"link": {"~/.f": {"path": "f", "relink": True, "backup-root": backup_root}}}]
)
with pytest.raises(SystemExit):
run_dotbot()
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "grape"
def test_backup_is_sequentially_differetiated(home, dotfiles, run_dotbot):
"""Verify that backups do not overwrite each other and get sequentially different names."""
with open(os.path.join(home, "f"), "w") as file:
file.write("grape")
os.symlink(os.path.join(home, "f"), os.path.join(home, ".f"))
dotfiles.write("f", "apple")
dotfiles.write("dir/f", "peach")
backup_root = os.path.join(dotfiles.directory, "backup")
dotfiles.write_config(
[
{"link": {"~/.f": {"path": "f", "relink": True, "backup-root": backup_root}}},
{"link": {"~/.f": {"path": "dir/f", "relink": True, "backup-root": backup_root}}},
]
)
run_dotbot()
backup_home = os.path.join(backup_root, home[1:])
dir_items = sorted(os.listdir(backup_home))
with open(os.path.join(backup_home, dir_items[0])) as file:
assert file.read() == "grape"
with open(os.path.join(backup_home, dir_items[1])) as file:
assert file.read() == "apple"
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "peach"
def test_link_relink_relative_leaves_file(home, dotfiles, run_dotbot):
"""Verify relink relative does not incorrectly relink file."""
dotfiles.write("f", "apple")
with open(os.path.join(home, ".f"), "w") as file:
file.write("grape")
config = [
{
"link": {
"~/.folder/f": {
"path": "f",
"create": True,
"relative": True,
},
},
}
]
dotfiles.write_config(config)
run_dotbot()
mtime = os.stat(os.path.join(home, ".folder", "f")).st_mtime
config[0]["link"]["~/.folder/f"]["relink"] = True
dotfiles.write_config(config)
run_dotbot()
new_mtime = os.stat(os.path.join(home, ".folder", "f")).st_mtime
assert mtime == new_mtime
def test_link_defaults_1(home, dotfiles, run_dotbot):
"""Verify that link doesn't overwrite non-dotfiles links by default."""
with open(os.path.join(home, "f"), "w") as file:
file.write("grape")
os.symlink(os.path.join(home, "f"), os.path.join(home, ".f"))
dotfiles.write("f", "apple")
dotfiles.write_config(
[
{
"link": {"~/.f": "f"},
}
]
)
with pytest.raises(SystemExit):
run_dotbot()
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "grape"
def test_link_defaults_2(home, dotfiles, run_dotbot):
"""Verify that explicit link defaults override the implicit default."""
with open(os.path.join(home, "f"), "w") as file:
file.write("grape")
os.symlink(os.path.join(home, "f"), os.path.join(home, ".f"))
dotfiles.write("f", "apple")
dotfiles.write_config(
[
{"defaults": {"link": {"relink": True}}},
{"link": {"~/.f": "f"}},
]
)
run_dotbot()
with open(os.path.join(home, ".f"), "r") as file:
assert file.read() == "apple"