diff --git a/tests/conftest.py b/tests/conftest.py index 7843584..51d2ded 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,17 @@ """Shared pytest configuration for integration tests.""" import os import sys +import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "integration")) + +from common import ServerManager + + +@pytest.fixture(scope="session") +def shared_server(): + """One server for the entire test session. Avoids 27+ server start/stop cycles.""" + server = ServerManager() + server.start() + yield server + server.stop() diff --git a/tests/integration/common.py b/tests/integration/common.py index 6d306dc..08cc9c9 100644 --- a/tests/integration/common.py +++ b/tests/integration/common.py @@ -28,7 +28,7 @@ class ServerManager: cmd = SERVER_CMD + ["-p", str(self._port)] if extra_args: cmd += extra_args - self._proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=None) + self._proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) _wait_for_port(self._port, timeout=5) def stop(self): diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index e97123b..63a5bf6 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -1,7 +1,6 @@ """Feature tests: incremental sync, bandwidth limiting, dry run, metadata, filters.""" import os import shutil -import subprocess import sys import time import pytest @@ -9,7 +8,7 @@ import pytest sys.path.insert(0, os.path.dirname(__file__)) from common import ( PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR, - ServerManager, run_client, + run_client, generate_test_files, verify_transfer, clean_dir, make_result, get_dest_received_dir, CLIENT_CMD, ) @@ -38,14 +37,13 @@ class TestDryRun: class TestArchiveMode: - def test_archive_mode(self): + def test_archive_mode(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["-a"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-a"], + port=shared_server.port, + ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) @@ -55,65 +53,57 @@ class TestArchiveMode: class TestExclude: - def test_exclude_single(self): + def test_exclude_single(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--exclude", "small.txt"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--exclude", "small.txt"], + port=shared_server.port, + ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) mismatches, missing = verify_transfer(SOURCE_DIR, received) - # small.txt should be missing (excluded) - assert "small.txt" in missing, f"small.txt should be excluded but was transferred" - # all other files should be present + assert "small.txt" in missing, "small.txt should be excluded but was transferred" other_missing = [m for m in missing if m != "small.txt"] assert not other_missing, f"Other files missing: {other_missing}" assert not mismatches, f"Mismatch: {mismatches}" - def test_exclude_glob(self): + def test_exclude_glob(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--exclude", "*.txt"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--exclude", "*.txt"], + port=shared_server.port, + ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) - # Only binary.bin and bulk files should be present assert not os.path.exists(os.path.join(received, "small.txt")), "small.txt should be excluded" assert os.path.exists(os.path.join(received, "binary.bin")), "binary.bin should be present" class TestInclude: - def test_include_single(self): + def test_include_single(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--include", "binary.bin"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--include", "binary.bin"], + port=shared_server.port, + ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) - # Only binary.bin should be present assert os.path.exists(os.path.join(received, "binary.bin")), "binary.bin should be included" assert not os.path.exists(os.path.join(received, "small.txt")), "small.txt should not be included" - def test_include_glob(self): + def test_include_glob(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--include", "*.bin"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--include", "*.bin"], + port=shared_server.port, + ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) @@ -121,101 +111,81 @@ class TestInclude: class TestSizeFilters: - def test_max_size(self): - """Files larger than --max-size should be skipped.""" + def test_max_size(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--max-size", "100"], # 100 bytes - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--max-size", "100"], + port=shared_server.port, + ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) - # small.txt (12 bytes) should be present, medium.txt (220k+) should be skipped assert os.path.exists(os.path.join(received, "small.txt")), "small.txt should be present" assert not os.path.exists(os.path.join(received, "medium.txt")), "medium.txt should be skipped" - def test_min_size(self): - """Files smaller than --min-size should be skipped.""" + def test_min_size(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--min-size", "1000"], # 1 KB - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--min-size", "1000"], + port=shared_server.port, + ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) - # small.txt (12 bytes) should be skipped, medium.txt should be present assert not os.path.exists(os.path.join(received, "small.txt")), "small.txt should be skipped" assert os.path.exists(os.path.join(received, "medium.txt")), "medium.txt should be present" class TestIncremental: - def test_incremental_skips_unchanged(self): - """Second sync with --incremental should be fast (skips unchanged files).""" - # First sync: populate dest + def test_incremental_skips_unchanged(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["-M"], - port=server.port, - ) - assert result.returncode == 0, f"First sync failed: {result.stderr[:100]}" + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-M"], + port=shared_server.port, + ) + assert result.returncode == 0, f"First sync failed: {result.stderr[:100]}" - # Second sync with --incremental (should be near-instant) - with ServerManager() as server: - start = time.monotonic() - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["-M", "--incremental"], - port=server.port, - ) - incremental_time = time.monotonic() - start + start = time.monotonic() + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-M", "--incremental"], + port=shared_server.port, + ) + incremental_time = time.monotonic() - start assert result.returncode == 0, f"Incremental sync failed: {(result.stderr or result.stdout)[:200]}" - # Verify files are still correct received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) mismatches, missing = verify_transfer(SOURCE_DIR, received) assert not missing, f"Missing: {missing}" assert not mismatches, f"Mismatch: {mismatches}" - def test_incremental_detects_changes(self): - """Incremental sync should transfer modified files.""" - # First sync + def test_incremental_detects_changes(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, _ = run_client( - SOURCE_DIR, DEST_DIR, - flags=["-M"], - port=server.port, - ) - assert result.returncode == 0 + result, _ = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-M"], + port=shared_server.port, + ) + assert result.returncode == 0 - # Modify a file modified_file = os.path.join(SOURCE_DIR, "small.txt") with open(modified_file, "wb") as f: f.write(b"modified content for incremental test\n") - # Second sync with --incremental - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["-M", "--incremental"], - port=server.port, - ) - assert result.returncode == 0 + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-M", "--incremental"], + port=shared_server.port, + ) + assert result.returncode == 0 - # Restore original content with open(modified_file, "wb") as f: f.write(b"hello world\n") - # Verify the modified content was transferred received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) received_file = os.path.join(received, "small.txt") assert os.path.exists(received_file), "Modified file should be present" @@ -225,19 +195,15 @@ class TestIncremental: class TestDelete: - def test_delete_removes_extra_files(self): - """--delete should remove files on dest that aren't in source.""" - # First sync: populate dest + def test_delete_removes_extra_files(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, _ = run_client( - SOURCE_DIR, DEST_DIR, - flags=["-M"], - port=server.port, - ) - assert result.returncode == 0 + result, _ = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-M"], + port=shared_server.port, + ) + assert result.returncode == 0 - # Add extra files to destination received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) extra_file = os.path.join(received, "extra_file.txt") extra_dir = os.path.join(received, "extra_dir") @@ -247,51 +213,42 @@ class TestDelete: with open(os.path.join(extra_dir, "nested.txt"), "w") as f: f.write("nested extra") - # Second sync with --delete - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["-M", "--delete"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-M", "--delete"], + port=shared_server.port, + ) assert result.returncode == 0, f"Delete sync failed: {(result.stderr or result.stdout)[:200]}" assert not os.path.exists(extra_file), "extra_file.txt should be deleted" assert not os.path.exists(extra_dir), "extra_dir should be deleted" - # Verify remaining files are correct mismatches, missing = verify_transfer(SOURCE_DIR, received) assert not missing, f"Missing: {missing}" assert not mismatches, f"Mismatch: {mismatches}" class TestProgress: - def test_progress_output(self): - """--progress should produce some output.""" + def test_progress_output(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--progress"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--progress"], + port=shared_server.port, + ) assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}" - # Progress output goes to stderr or stdout output = result.stdout + result.stderr - # Just verify it ran successfully; progress output format may vary - assert len(output) >= 0 # No assertion on specific output format + assert len(output) >= 0 class TestBandwidthLimit: - def test_bwlimit_runs(self): - """--bwlimit should run without error.""" + def test_bwlimit_runs(self, shared_server): clean_dir(DEST_DIR) - with ServerManager() as server: - result, dur = run_client( - SOURCE_DIR, DEST_DIR, - flags=["--bwlimit", "10240"], - port=server.port, - ) + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--bwlimit", "10240"], + port=shared_server.port, + ) assert result.returncode == 0, f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}" received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) mismatches, missing = verify_transfer(SOURCE_DIR, received) diff --git a/tests/integration/test_tcp.py b/tests/integration/test_tcp.py index 04f64a4..18cc07c 100644 --- a/tests/integration/test_tcp.py +++ b/tests/integration/test_tcp.py @@ -1,12 +1,13 @@ """TCP transport correctness tests.""" import os +import shutil import sys import pytest sys.path.insert(0, os.path.dirname(__file__)) from common import ( PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR, - ServerManager, run_client, run_client_posix, + run_client, run_client_posix, generate_test_files, verify_transfer, clean_dir, make_result, get_dest_received_dir, CLIENT_CMD, ) @@ -20,22 +21,20 @@ def setup_test_data(): generate_test_files(SOURCE_DIR, full=False) clean_dir(DEST_DIR) yield - import shutil shutil.rmtree(TEST_DATA_DIR, ignore_errors=True) -def _run_tcp_test(name, flags, use_metadata=True, posix=False): - """Run a single TCP test case with a fresh server.""" +def _run_tcp_test(name, port, flags, use_metadata=True, posix=False): + """Run a single TCP test case against a shared server.""" clean_dir(DEST_DIR) - with ServerManager() as server: - if posix: - result, dur = run_client_posix(SOURCE_DIR, DEST_DIR, - flags=(["-M"] if use_metadata else []) + flags, - port=server.port) - else: - result, dur = run_client(SOURCE_DIR, DEST_DIR, - flags=(["-M"] if use_metadata else []) + flags, - port=server.port) + if posix: + result, dur = run_client_posix(SOURCE_DIR, DEST_DIR, + flags=(["-M"] if use_metadata else []) + flags, + port=port) + else: + result, dur = run_client(SOURCE_DIR, DEST_DIR, + flags=(["-M"] if use_metadata else []) + flags, + port=port) if result.returncode != 0: return make_result(name, False, dur, f"Exit {result.returncode}: {(result.stderr or result.stdout)[:100]}") @@ -50,62 +49,62 @@ def _run_tcp_test(name, flags, use_metadata=True, posix=False): class TestTCPStandard: - def test_standard(self): - r = _run_tcp_test("Standard", []) + def test_standard(self, shared_server): + r = _run_tcp_test("Standard", shared_server.port, []) assert r["status"] == "Success", r["error"] - def test_posix_args(self): - r = _run_tcp_test("Posix Args", [], posix=True) + def test_posix_args(self, shared_server): + r = _run_tcp_test("Posix Args", shared_server.port, [], posix=True) assert r["status"] == "Success", r["error"] - def test_no_metadata(self): - r = _run_tcp_test("Standard (no metadata)", [], use_metadata=False) + def test_no_metadata(self, shared_server): + r = _run_tcp_test("Standard (no metadata)", shared_server.port, [], use_metadata=False) assert r["status"] == "Success", r["error"] class TestTCPFlags: - def test_multithreading(self): - r = _run_tcp_test("Multithreading (-m)", ["-m"]) + def test_multithreading(self, shared_server): + r = _run_tcp_test("Multithreading (-m)", shared_server.port, ["-m"]) assert r["status"] == "Success", r["error"] - def test_compression(self): - r = _run_tcp_test("Compression (-c)", ["-c"]) + def test_compression(self, shared_server): + r = _run_tcp_test("Compression (-c)", shared_server.port, ["-c"]) assert r["status"] == "Success", r["error"] - def test_chunk_serialization(self): - r = _run_tcp_test("Chunk Serialization (-s)", ["-s"]) + def test_chunk_serialization(self, shared_server): + r = _run_tcp_test("Chunk Serialization (-s)", shared_server.port, ["-s"]) assert r["status"] == "Success", r["error"] - def test_compression_chunk(self): - r = _run_tcp_test("Compression + Chunk (-c -s)", ["-c", "-s"]) + def test_compression_chunk(self, shared_server): + r = _run_tcp_test("Compression + Chunk (-c -s)", shared_server.port, ["-c", "-s"]) assert r["status"] == "Success", r["error"] - def test_multithread_compression(self): - r = _run_tcp_test("Multithreading + Compression (-m -c)", ["-m", "-c"]) + def test_multithread_compression(self, shared_server): + r = _run_tcp_test("Multithreading + Compression (-m -c)", shared_server.port, ["-m", "-c"]) assert r["status"] == "Success", r["error"] - def test_multithread_chunk(self): - r = _run_tcp_test("Multithreading + Chunk (-m -s)", ["-m", "-s"]) + def test_multithread_chunk(self, shared_server): + r = _run_tcp_test("Multithreading + Chunk (-m -s)", shared_server.port, ["-m", "-s"]) assert r["status"] == "Success", r["error"] - def test_all_flags(self): - r = _run_tcp_test("Multithread + Compression + Chunk (-m -c -s)", ["-m", "-c", "-s"]) + def test_all_flags(self, shared_server): + r = _run_tcp_test("Multithread + Compression + Chunk (-m -c -s)", shared_server.port, ["-m", "-c", "-s"]) assert r["status"] == "Success", r["error"] - def test_sendfile(self): - r = _run_tcp_test("Sendfile (-f)", ["-f"]) + def test_sendfile(self, shared_server): + r = _run_tcp_test("Sendfile (-f)", shared_server.port, ["-f"]) assert r["status"] == "Success", r["error"] - def test_sendfile_multithread(self): - r = _run_tcp_test("Sendfile + Multithreading (-f -m)", ["-f", "-m"]) + def test_sendfile_multithread(self, shared_server): + r = _run_tcp_test("Sendfile + Multithreading (-f -m)", shared_server.port, ["-f", "-m"]) assert r["status"] == "Success", r["error"] class TestTCPChunkSize: - def test_custom_chunk_size(self): - r = _run_tcp_test("Chunk size 5MB", ["--chunk-size", "5242880"]) + def test_custom_chunk_size(self, shared_server): + r = _run_tcp_test("Chunk size 5MB", shared_server.port, ["--chunk-size", "5242880"]) assert r["status"] == "Success", r["error"] - def test_small_chunk_size(self): - r = _run_tcp_test("Chunk size 1KB", ["--chunk-size", "1024"]) + def test_small_chunk_size(self, shared_server): + r = _run_tcp_test("Chunk size 1KB", shared_server.port, ["--chunk-size", "1024"]) assert r["status"] == "Success", r["error"]