Skip to content

Commit 739b2b2

Browse files
committed
Simplify fake RSH
With the addition of `dest_prefix` we can simplify the tests to just monkeypatch `RSH` which simply executes the command provided, skipping over the first argument, which is the destination hostname.
1 parent 961a0b0 commit 739b2b2

File tree

3 files changed

+67
-52
lines changed

3 files changed

+67
-52
lines changed

src/deploy/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def check() -> None:
6868
def sync(no_async: bool, dry_run: bool, extra_scripts: str) -> None:
6969
config = load_config(Args.config_dir)
7070
extra_scripts_path = (
71-
Path(extra_scripts).expanduser().resolve() if len(extra_scripts) > 0 else None
71+
Path(extra_scripts).expanduser().resolve() if extra_scripts else None
7272
)
7373
do_sync(
7474
Args.config_dir,

src/deploy/sync.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@
1313
from deploy.utils import redirect_output
1414

1515

16-
RSH: list[str] = [
17-
"ssh",
18-
"-q",
19-
"-oBatchMode=yes",
20-
"-oPasswordAuthentication=no",
21-
"-oStrictHostKeyChecking=no",
22-
"-oConnectTimeout=20",
23-
]
16+
def change_prefix(path: Path, old_prefix: Path, new_prefix: Path) -> Path:
17+
return new_prefix / path.relative_to(old_prefix)
2418

2519

2620
class Sync:
21+
RSH: list[str] = [
22+
"ssh",
23+
"-q",
24+
"-oBatchMode=yes",
25+
"-oPasswordAuthentication=no",
26+
"-oStrictHostKeyChecking=no",
27+
"-oConnectTimeout=20",
28+
]
29+
2730
def __init__(
2831
self,
2932
storepath: Path,
@@ -35,7 +38,7 @@ def __init__(
3538
self._storepath: Path = storepath
3639
self._dry_run: bool = dry_run
3740
self._prefix = plist.prefix
38-
dest_prefix = dest_prefix or plist.prefix
41+
self._dest_prefix = dest_prefix or plist.prefix
3942

4043
self._store_paths: list[Path] = [pkg.out for pkg in plist.packages.values()]
4144

@@ -53,9 +56,9 @@ def __init__(
5356
self._post_script = io.StringIO()
5457
self._post_script.write("set -euxo pipefail\n")
5558
for _, dest in plist.envs:
56-
self._post_script.write(f"mkdir -p {dest_prefix / dest}\n")
59+
self._post_script.write(f"mkdir -p {self._dest_prefix / dest}\n")
5760
self._post_script.writelines(
58-
f"ln -sfn {os.readlink(path)} {dest_prefix/path.relative_to(plist.prefix)} \n"
61+
f"ln -sfn {os.readlink(path)} {change_prefix(path, plist.prefix, self._dest_prefix)} \n"
5962
for path in (plist.prefix / dest).glob("*")
6063
if path.is_symlink()
6164
if (path / "manifest").is_file()
@@ -77,11 +80,9 @@ async def _bash(
7780
) -> None:
7881
await self._check_call(
7982
area,
80-
*RSH,
83+
*self.RSH,
8184
area.host,
82-
"--",
8385
"bash",
84-
"-",
8586
input=script,
8687
context=context,
8788
)
@@ -99,10 +100,10 @@ async def _rsync(
99100
"rsync",
100101
"-a",
101102
"--rsh",
102-
shlex.join(RSH),
103+
shlex.join(self.RSH),
103104
"--progress",
104105
*paths,
105-
self._format_dest_path(area, parent),
106+
f"{area.host}:{change_prefix(parent, self._prefix, self._dest_prefix)}",
106107
context=context,
107108
)
108109

@@ -151,10 +152,6 @@ async def _check_call(
151152
returncode, (program, *args), stdout.getvalue(), stderr.getvalue()
152153
)
153154

154-
@staticmethod
155-
def _format_dest_path(area: AreaConfig, path: Path) -> str:
156-
return f"{area.host}:{path}"
157-
158155

159156
async def _sync(
160157
configpath: Path,

tests/test_sync.py

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from deploy.build import Build
88
from deploy.config import Config
99
from deploy.links import make_links
10-
from deploy.sync import do_sync
10+
from deploy.sync import Sync, do_sync, change_prefix
1111

1212
BUILD_SCRIPT = """\
1313
#!/usr/bin/env bash
@@ -16,32 +16,20 @@
1616
echo "hello world">>$out/bin/a_file
1717
"""
1818

19-
RSH = [
20-
"python",
21-
"-c",
22-
"import sys, os; args = sys.argv[sys.argv.index('--')+1:]; os.execvp(args[0], args)",
23-
]
2419

20+
@pytest.fixture(autouse=True)
21+
def fake_ssh(monkeypatch: pytest.MonkeyPatch) -> None:
22+
"""Override Sync's SSH command so that it doesn't use SSH, and instead
23+
executes the command locally
2524
26-
def mocked_format_dest_path(area, path, dest):
27-
# Original function defines this as area.host:path, where area will be
28-
# the same in both source and destination. We mock this setup and
29-
# disregard the host (i.e. no ssh) and allow a different destination
30-
# instead of source
31-
# We wrap this function in a lambda that passes on dest (as that is
32-
# not included in the original one. Similar behavoir as partial,
33-
# without including functools
34-
return dest
25+
"""
3526

36-
37-
def setup_local_sync(monkeypatch: pytest.MonkeyPatch, destination: str) -> None:
38-
import deploy.sync
39-
40-
def format_dest_path(*_) -> str:
41-
return destination
42-
43-
monkeypatch.setattr(deploy.sync.Sync, "_format_dest_path", format_dest_path)
44-
monkeypatch.setattr(deploy.sync, "RSH", RSH)
27+
# We replace RSH with an inline sh script. Both rsync and our `Sync._bash`
28+
# set the first argument to be the destination hostname. The remainder is
29+
# the command to execute on the "remote server". We simply execute it
30+
# locally. Then, sh sets the next argument ("fake_ssh") to be the program
31+
# name ($0), which is why we specify it.
32+
monkeypatch.setattr(Sync, "RSH", ["/bin/sh", "-c", 'shift; exec "$@"', "fake_ssh"])
4533

4634

4735
@pytest.fixture
@@ -56,7 +44,7 @@ def base_config(tmp_path):
5644
},
5745
],
5846
"envs": [{"name": "A", "dest": "location"}],
59-
"areas": [{"name": "destination", "host": "localhost"}],
47+
"areas": [{"name": "destination", "host": "example.com"}],
6048
"links": {"location": {"latest": "^"}},
6149
}
6250

@@ -74,9 +62,40 @@ def _deploy_config(config, configpath, prefix=None):
7462
return builder
7563

7664

77-
def test_successful_sync(tmp_path, monkeypatch, base_config):
65+
@pytest.mark.parametrize(
66+
"old_prefix, new_prefix, path, expectation",
67+
[
68+
pytest.param(
69+
"/foo",
70+
"/bar",
71+
"/foo/file",
72+
"/bar/file",
73+
id="Simple",
74+
),
75+
pytest.param(
76+
"/some/prefix",
77+
"/yet/another/new/prefix",
78+
"/some/prefix/path/in/prefix",
79+
"/yet/another/new/prefix/path/in/prefix",
80+
id="Longer path",
81+
),
82+
pytest.param("/foo", "/bar", "/bar/file", ValueError, id="Invalid prefix"),
83+
],
84+
)
85+
def test_change_prefix(old_prefix, new_prefix, path, expectation):
86+
path = Path(path)
87+
old_prefix = Path(old_prefix)
88+
new_prefix = Path(new_prefix)
89+
90+
if isinstance(expectation, type) and issubclass(expectation, BaseException):
91+
with pytest.raises(expectation):
92+
change_prefix(path, old_prefix, new_prefix)
93+
else:
94+
assert change_prefix(path, old_prefix, new_prefix) == Path(expectation)
95+
96+
97+
def test_successful_sync(tmp_path, base_config):
7898
destination = tmp_path / "destination"
79-
setup_local_sync(monkeypatch, str(destination))
8099

81100
builder = _deploy_config(base_config, tmp_path)
82101

@@ -99,18 +118,17 @@ def test_successful_sync(tmp_path, monkeypatch, base_config):
99118
assert os.path.islink(destination / "location/latest")
100119

101120

102-
def test_failing_sync(tmp_path, monkeypatch, base_config):
121+
def test_failing_sync(tmp_path, base_config):
103122
"""Try to sync to non existent area"""
104-
setup_local_sync(monkeypatch, "/non-existent/destination")
105-
106123
_deploy_config(base_config, tmp_path)
107124
with pytest.raises(
108125
CalledProcessError,
109-
match="'/non-existent/destination'\\)' returned non-zero exit status 11.",
126+
match=r"'example.com:/non-existent/destination'\)' returned non-zero exit status 11.",
110127
):
111128
do_sync(
112129
configpath=tmp_path,
113130
config=base_config,
114131
extra_scripts=tmp_path,
115132
prefix=tmp_path,
133+
dest_prefix=Path("/non-existent/destination"),
116134
)

0 commit comments

Comments
 (0)