Files
FastSync/tests/integration/test_preflight.py
T
TapTap f1a447bb4a feat(p7-times): real directory/symlink time preservation; -O/-J meaningful
Wave D of Phase 7. Make -O/--omit-dir-times and -J/--omit-link-times real by
preserving directory and symlink times, and mark --secluded-args as an explicit
Impossible/Divergence no-op.

Wire: PROTOCOL_VERSION 2.16.0 -> 2.17.0. Adds a terminal STATUS_DIR_TIMES frame
(int count + (wire path, metadata) pairs) sent after all file data and the
optional delete manifest. STATUS_MKDIR also carries metadata for --dirs entries.
Config-frame layout is unchanged.

Sender: the recursive scanner captures every traversed source directory (both
DirectoryScanner and the parallel scanner root + workers, appends mutex-guarded)
into a shared list; the single-threaded and -m paths transmit it last.

Receiver: a DirTimeList accumulates received directory metadata and applies it
with fd-relative no-follow utimensat only at the very end -- after all children,
after the commit-style --delete, and after --delay-updates publication -- in the
single-threaded success frame and in server.c after the -m threads join. -O skips
the application. Symlink metadata is applied at link creation with
utimensat/fchownat/fchmodat AT_SYMLINK_NOFOLLOW; -J suppresses only link times.
identity_apply_ownership_link shares the identity resolver with the fd path.

Docs: -O/-J rows -> Implemented; --secluded-args -> Impossible/Divergence;
--protocol accepted/rejected values and Phase-6/7 notes updated.

Tests: unit (scanner dir capture, DirTimeList apply, symlink metadata, protocol
version values) and integration (dir mtime round-trip + -O, symlink mtime
round-trip + -J, independent suppression), parameterized over single/multithread.
2026-09-12 10:31:20 +02:00

138 lines
5.3 KiB
Python

"""CLI validation and preflight checks."""
import subprocess
import sys
import os
import shutil
import pytest
sys.path.insert(0, os.path.dirname(__file__))
from common import BUILD_DIR, CLIENT_CMD, SERVER_CMD, TEST_DATA_DIR, run_client, verify_transfer
class TestHelp:
def test_client_help(self):
r = subprocess.run(CLIENT_CMD + ["--help"], capture_output=True, text=True)
assert r.returncode == 0
assert "Usage:" in r.stdout
assert "SSH transport" in r.stdout
def test_server_help(self):
r = subprocess.run(SERVER_CMD + ["--help"], capture_output=True, text=True)
assert r.returncode == 0
assert "Usage:" in r.stdout
class TestSSHDetection:
def test_remote_dest_detected(self):
"""Posix-style SSH dest should be detected and fail gracefully."""
r = subprocess.run(
CLIENT_CMD + ["/x", "somehost:/y"],
capture_output=True, text=True, timeout=5,
)
assert r.returncode != 0
stderr = (r.stderr or "").lower()
assert "ssh" in stderr or "error" in stderr or "could not" in stderr
def test_local_dest_not_ssh(self):
"""Local path should not be detected as SSH."""
r = subprocess.run(
CLIENT_CMD + ["/tmp/x", "/tmp/y"],
capture_output=True, text=True, timeout=5,
)
# Should fail with connection error (no server), not SSH error
assert r.returncode != 0
class TestServerStdio:
def test_stdio_mode_starts(self):
"""Server --stdio should start and wait for stdin."""
try:
r = subprocess.run(
SERVER_CMD + ["--stdio"],
capture_output=True, text=True, timeout=3,
)
# Should exit with error (no data on stdin) or timeout
except subprocess.TimeoutExpired:
pass # Expected: server waiting for stdin
class TestServerPort:
def test_invalid_port(self):
"""Server should reject invalid port numbers."""
r = subprocess.run(
SERVER_CMD + ["-p", "99999"],
capture_output=True, text=True, timeout=5,
)
assert r.returncode != 0
def test_default_port(self):
"""Server should start on default port 8080."""
proc = subprocess.Popen(
SERVER_CMD, stdout=subprocess.DEVNULL, stderr=None,
)
try:
import socket, time
time.sleep(0.5)
with socket.create_connection(("127.0.0.1", 8080), timeout=2):
pass # Port is listening
except (ConnectionRefusedError, OSError):
pytest.fail("Server not listening on default port 8080")
finally:
proc.terminate()
proc.wait(timeout=5)
def _seed_protocol_source(source):
os.makedirs(source, exist_ok=True)
with open(os.path.join(source, "p.txt"), "w") as fh:
fh.write("protocol test\n")
os.makedirs(os.path.join(source, "nested"), exist_ok=True)
with open(os.path.join(source, "nested", "deep.txt"), "w") as fh:
fh.write("deep file\n")
class TestProtocol:
@pytest.mark.ci
def test_protocol_current_version_accepted(self, shared_server):
"""--protocol=2.17.0 (the current PROTOCOL_VERSION) is accepted and the
transfer completes normally."""
source = os.path.join(TEST_DATA_DIR, "proto_ok_src")
dest = os.path.join(TEST_DATA_DIR, "proto_ok_dst")
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
_seed_protocol_source(source)
result, _ = run_client(source, dest, flags=["--protocol=2.17.0"],
port=shared_server.port)
assert result.returncode == 0, \
f"--protocol current run failed: {(result.stderr or result.stdout)[:400]}"
received = os.path.join(dest, os.path.abspath(source).lstrip(os.sep))
mismatches, missing = verify_transfer(source, received)
assert not mismatches and not missing, \
f"transfer mismatch: missing={missing} mismatches={mismatches}"
@pytest.mark.ci
def test_protocol_rejects_other_versions(self, shared_server):
"""Other versions are rejected up front, before connecting."""
source = os.path.join(TEST_DATA_DIR, "proto_reject_src")
dest = os.path.join(TEST_DATA_DIR, "proto_reject_dst")
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
_seed_protocol_source(source)
for bad in ("2.15.0", "2.16.0", "216", "31"):
result, _ = run_client(source, dest, flags=[f"--protocol={bad}"],
port=shared_server.port)
assert result.returncode != 0, f"--protocol={bad} should be rejected"
@pytest.mark.ci
def test_protocol_rejects_garbage(self, shared_server):
"""Garbage/empty --protocol values are rejected up front."""
source = os.path.join(TEST_DATA_DIR, "proto_garbage_src")
dest = os.path.join(TEST_DATA_DIR, "proto_garbage_dst")
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
_seed_protocol_source(source)
for bad in ("abc", ""):
result, _ = run_client(source, dest, flags=[f"--protocol={bad}"],
port=shared_server.port)
assert result.returncode != 0, f"--protocol={bad} should be rejected"