feat(p5-rsh): --rsh/-e, --rsync-path, --blocking-io, --outbuf
This commit is contained in:
@@ -4604,3 +4604,53 @@ class TestExtendedAttributes:
|
||||
fields = record.split(":")
|
||||
assert len(fields) == 5
|
||||
assert fields[0] == str(uid), f"reserved uid field {fields[0]} != source uid {uid}"
|
||||
|
||||
|
||||
class TestConnectivityClientOptions:
|
||||
"""Phase 5 connectivity launch options (--outbuf, --blocking-io).
|
||||
|
||||
These are client-side launch concerns: --outbuf only restyles stdout/stderr
|
||||
buffering and --blocking-io only skips the SSH transport socket timeouts.
|
||||
Over the TCP transport both must parse cleanly and be inert -- a transfer
|
||||
must still complete and verify byte-for-byte."""
|
||||
|
||||
def _source_and_dest(self, name):
|
||||
source = os.path.join(TEST_DATA_DIR, name + "_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, name + "_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
return source, dest
|
||||
|
||||
@pytest.mark.parametrize("flag", ["--outbuf=N", "--outbuf=L", "--outbuf=B",
|
||||
"--blocking-io"])
|
||||
def test_option_does_not_break_transfer(self, shared_server, flag):
|
||||
source, dest = self._source_and_dest("connopt")
|
||||
with open(os.path.join(source, "hello.txt"), "wb") as f:
|
||||
f.write(b"connectivity options\n" * 100)
|
||||
with open(os.path.join(source, "data.bin"), "wb") as f:
|
||||
f.write(os.urandom(512 * 1024))
|
||||
|
||||
result, _ = run_client(source, dest, flags=[flag], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"{flag} failed: {(result.stderr or result.stdout)[:300]}"
|
||||
mismatches, missing = verify_transfer(source, get_dest_received_dir(dest, source))
|
||||
assert not mismatches and not missing, \
|
||||
f"{flag}: mismatches={mismatches[:3]} missing={missing[:3]}"
|
||||
|
||||
def test_rejects_invalid_outbuf(self, shared_server):
|
||||
source, dest = self._source_and_dest("connopt_bad")
|
||||
with open(os.path.join(source, "x.txt"), "wb") as f:
|
||||
f.write(b"x")
|
||||
result, _ = run_client(source, dest, flags=["--outbuf=Z"], port=shared_server.port)
|
||||
assert result.returncode != 0, "--outbuf=Z must be rejected"
|
||||
|
||||
def test_blocking_io_does_not_break_compressed_transfer(self, shared_server):
|
||||
source, dest = self._source_and_dest("connopt_zlib")
|
||||
with open(os.path.join(source, "text.txt"), "wb") as f:
|
||||
f.write(b"compress me\n" * 4096)
|
||||
result, _ = run_client(source, dest, flags=["--blocking-io", "-c"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--blocking-io -c failed: {(result.stderr or result.stdout)[:300]}"
|
||||
mismatches, missing = verify_transfer(source, get_dest_received_dir(dest, source))
|
||||
assert not mismatches and not missing
|
||||
|
||||
@@ -64,11 +64,12 @@ def setup_test_data():
|
||||
shutil.rmtree(DEST_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
def _run_ssh_test(name, flags, expected_missing=None):
|
||||
def _run_ssh_test(name, flags, expected_missing=None, path_args=None):
|
||||
ssh_dest = f"localhost:{DEST_DIR}"
|
||||
clean_dir(DEST_DIR)
|
||||
cmd = CLIENT_CMD + [SOURCE_DIR, ssh_dest, "--save-to-disk",
|
||||
"--fastsync-server-path", os.path.join(BUILD_DIR, "server")] + flags
|
||||
if not path_args:
|
||||
path_args = ["--fastsync-server-path", os.path.join(BUILD_DIR, "server")]
|
||||
cmd = CLIENT_CMD + [SOURCE_DIR, ssh_dest, "--save-to-disk"] + path_args + flags
|
||||
start = __import__("time").monotonic()
|
||||
result = subprocess.run(cmd, text=True, capture_output=True)
|
||||
duration = __import__("time").monotonic() - start
|
||||
@@ -142,3 +143,41 @@ class TestSSHFeatures:
|
||||
def test_preallocate(self):
|
||||
r = _run_ssh_test("SSH Preallocate (--preallocate)", ["--preallocate"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
class TestSSHConnectivity:
|
||||
"""Phase 5 connectivity options: -e/--rsh, --rsync-path, --blocking-io,
|
||||
--outbuf. These are client-side launch concerns, so each must parse and
|
||||
still drive a real SSH transfer to completion."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def require_ssh(self):
|
||||
if not SSH_AVAILABLE:
|
||||
pytest.skip(SSH_SKIP_REASON)
|
||||
|
||||
def test_rsh_short_form_selects_ssh(self):
|
||||
r = _run_ssh_test("SSH -e ssh", ["-e", "ssh"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_rsh_long_form_selects_ssh(self):
|
||||
r = _run_ssh_test("SSH --rsh=ssh", ["--rsh=ssh"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_rsync_path_aliases_server_path(self):
|
||||
r = _run_ssh_test("SSH --rsync-path",
|
||||
[],
|
||||
path_args=["--rsync-path", os.path.join(BUILD_DIR, "server")])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_blocking_io(self):
|
||||
r = _run_ssh_test("SSH --blocking-io", ["--blocking-io"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
@pytest.mark.parametrize("mode", ["N", "L", "B"])
|
||||
def test_outbuf_mode(self, mode):
|
||||
r = _run_ssh_test(f"SSH --outbuf={mode}", [f"--outbuf={mode}"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_blocking_io_with_compression(self):
|
||||
r = _run_ssh_test("SSH --blocking-io -c", ["--blocking-io", "-c"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
+115
-3
@@ -831,9 +831,6 @@ static void test_parse_args_rejects_unimplemented_options() {
|
||||
"--delete-excluded",
|
||||
"--max-delete",
|
||||
"--prune-empty-dirs",
|
||||
"-e",
|
||||
"--rsh",
|
||||
"--rsync-path",
|
||||
"--address",
|
||||
"--bind-address",
|
||||
"--ipv6",
|
||||
@@ -1210,6 +1207,117 @@ static void test_parse_args_old_args() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Phase 5 connectivity: -e/--rsh select the remote-shell program. Both the
|
||||
* short (space-separated value) and long (=value and space) forms parse, and
|
||||
* a multi-word command line is preserved verbatim for the transport layer. */
|
||||
static void test_parse_args_rsh() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-e", "ssh -p 2222", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->rsh_command, "ssh -p 2222");
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_eq[] = {"fastsync", "--rsh=customsh", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_eq, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->rsh_command, "customsh");
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_space[] = {"fastsync", "--rsh", "ssh -l bob", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv_space, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->rsh_command, "ssh -l bob");
|
||||
config_delete(cfg);
|
||||
|
||||
/* A missing value is a hard error. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_missing[] = {"fastsync", "-e"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 2, argv_missing, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --rsync-path is rsync's spelling for the server program path: it aliases
|
||||
* fastsync_server_path exactly like --fastsync-server-path. */
|
||||
static void test_parse_args_rsync_path_alias() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--rsync-path", "/usr/bin/fastsync-server", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->fastsync_server_path, "/usr/bin/fastsync-server");
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_eq[] = {"fastsync", "--rsync-path=/opt/bin/srv", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_eq, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_STR(cfg->fastsync_server_path, "/opt/bin/srv");
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --blocking-io is a plain boolean flag that leaves the SSH socket with no
|
||||
* timeouts; the default is off. */
|
||||
static void test_parse_args_blocking_io() {
|
||||
Config* cfg = config_create();
|
||||
EXPECT_FALSE(cfg->blocking_io);
|
||||
char* argv[] = {"fastsync", "--blocking-io", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->blocking_io);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --outbuf=N|L|B maps onto the OUTBUF_* modes (default: block). Garbage is
|
||||
* rejected, never silently coerced. */
|
||||
static void test_parse_args_outbuf() {
|
||||
Config* cfg = config_create();
|
||||
EXPECT_EQ_INT(cfg->outbuf, OUTBUF_BLOCK);
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
char* argv_n[] = {"fastsync", "--outbuf=N", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_n, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->outbuf, OUTBUF_NONE);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_l[] = {"fastsync", "--outbuf", "L", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv_l, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->outbuf, OUTBUF_LINE);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_b[] = {"fastsync", "--outbuf=b", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_b, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->outbuf, OUTBUF_BLOCK);
|
||||
config_delete(cfg);
|
||||
|
||||
static const char* const bad[] = {"G", "X", ""};
|
||||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char option[32];
|
||||
snprintf(option, sizeof(option), "--outbuf=%s", bad[i]);
|
||||
char* argv_bad[] = {"fastsync", option, "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_bad, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv_missing[] = {"fastsync", "--outbuf"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 2, argv_missing, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_parse_args_fsync() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--fsync", "/src", "/dst"};
|
||||
@@ -2556,6 +2664,10 @@ void test_client_cli() {
|
||||
test_parse_args_no_preserve_blocks_implicit_metadata();
|
||||
test_parse_args_rejects_unsafe_negation();
|
||||
test_parse_args_old_args();
|
||||
test_parse_args_rsh();
|
||||
test_parse_args_rsync_path_alias();
|
||||
test_parse_args_blocking_io();
|
||||
test_parse_args_outbuf();
|
||||
test_parse_args_fsync();
|
||||
test_parse_args_existing();
|
||||
test_parse_args_ignore_times();
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
static void test_ssh_connect_invalid_dest_no_colon() {
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL, false);
|
||||
Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL, false, NULL, false);
|
||||
EXPECT_NULL(client);
|
||||
}
|
||||
|
||||
static void test_ssh_connect_invalid_dest_empty() {
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
Client* client = client_connect_ssh("", 22, NULL, false);
|
||||
Client* client = client_connect_ssh("", 22, NULL, false, NULL, false);
|
||||
EXPECT_NULL(client);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ static void test_ssh_connect_malformed() {
|
||||
setenv("PATH", "", 1);
|
||||
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
Client* client = client_connect_ssh(":", 22, NULL, false);
|
||||
Client* client = client_connect_ssh(":", 22, NULL, false, NULL, false);
|
||||
|
||||
if (saved_path) {
|
||||
setenv("PATH", saved_path, 1);
|
||||
@@ -36,7 +36,8 @@ static void test_ssh_connect_malformed() {
|
||||
/* Test client_connect_ssh with valid format but unreachable host.
|
||||
* The function launches ssh which will fail to connect, returns a Client. */
|
||||
static void test_ssh_connect_unreachable() {
|
||||
Client* client = client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL, false);
|
||||
Client* client =
|
||||
client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL, false, NULL, false);
|
||||
if (client != NULL) {
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
@@ -58,10 +59,58 @@ static void test_ssh_remote_command_argument_modes() {
|
||||
free(command);
|
||||
}
|
||||
|
||||
/* The build for a single-word argv is [prog, six -o args, user, command]. */
|
||||
|
||||
static void test_ssh_build_client_argv_default_is_ssh() {
|
||||
char** argv = ssh_build_client_argv(NULL, 0, "u@h", "'srv' --stdio");
|
||||
EXPECT_NOT_NULL(argv);
|
||||
EXPECT_EQ_STR(argv[0], "ssh");
|
||||
EXPECT_EQ_STR(argv[1], "-o");
|
||||
EXPECT_EQ_STR(argv[7], "u@h");
|
||||
EXPECT_EQ_STR(argv[8], "'srv' --stdio");
|
||||
EXPECT_NULL(argv[9]);
|
||||
ssh_free_client_argv(argv);
|
||||
}
|
||||
|
||||
/* A configured rsh must replace "ssh" as argv[0] (and never leak the default). */
|
||||
static void test_ssh_build_client_argv_uses_custom_rsh() {
|
||||
char** argv = ssh_build_client_argv("myrsh", 0, "u@h", "rc");
|
||||
EXPECT_NOT_NULL(argv);
|
||||
EXPECT_EQ_STR(argv[0], "myrsh");
|
||||
EXPECT_NULL(argv[9]);
|
||||
ssh_free_client_argv(argv);
|
||||
}
|
||||
|
||||
/* A multi-word rsh command line (rsync -e "ssh -p 2222") is split into the
|
||||
* leading argv words; a non-default port adds a -p/value pair. */
|
||||
static void test_ssh_build_client_argv_whitespace_command_and_port() {
|
||||
char** argv = ssh_build_client_argv("ssh -p 2222", 0, "u@h", "rc");
|
||||
EXPECT_NOT_NULL(argv);
|
||||
EXPECT_EQ_STR(argv[0], "ssh");
|
||||
EXPECT_EQ_STR(argv[1], "-p");
|
||||
EXPECT_EQ_STR(argv[2], "2222");
|
||||
EXPECT_NULL(argv[11]);
|
||||
ssh_free_client_argv(argv);
|
||||
|
||||
argv = ssh_build_client_argv("ssh", 2222, "u@h", "rc");
|
||||
EXPECT_NOT_NULL(argv);
|
||||
EXPECT_EQ_STR(argv[0], "ssh");
|
||||
/* Flat [prog, -o x6, -p, port, user, command]. */
|
||||
EXPECT_EQ_STR(argv[7], "-p");
|
||||
EXPECT_EQ_STR(argv[8], "2222");
|
||||
EXPECT_EQ_STR(argv[9], "u@h");
|
||||
EXPECT_EQ_STR(argv[10], "rc");
|
||||
EXPECT_NULL(argv[11]);
|
||||
ssh_free_client_argv(argv);
|
||||
}
|
||||
|
||||
void test_transport_ssh() {
|
||||
test_ssh_connect_invalid_dest_no_colon();
|
||||
test_ssh_connect_invalid_dest_empty();
|
||||
test_ssh_connect_malformed();
|
||||
test_ssh_connect_unreachable();
|
||||
test_ssh_remote_command_argument_modes();
|
||||
test_ssh_build_client_argv_default_is_ssh();
|
||||
test_ssh_build_client_argv_uses_custom_rsh();
|
||||
test_ssh_build_client_argv_whitespace_command_and_port();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user