diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 03d5e0b..434a623 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -112,15 +112,24 @@ jobs: path.write_text(text, encoding="utf-8") print("Injected daemon.api_key into base ccbt.toml for benchmark compatibility") PY - # Prefer en/ path; fall back to legacy docs/examples on older base trees. + # Resolve config relative to the *base* worktree only. + # Do not fall back to HEAD's docs/en path — that overwrote the legacy + # docs/examples path on older main and made hash_verify produce no JSON. CONFIG_FILE="${BENCH_CONFIG_FILE}" - if [ ! -f "${BENCH_BASE_DIR}/${CONFIG_FILE}" ] && [ -f "${BENCH_BASE_DIR}/docs/examples/example-config-performance.toml" ]; then - CONFIG_FILE="docs/examples/example-config-performance.toml" + if [ ! -f "${BENCH_BASE_DIR}/${CONFIG_FILE}" ]; then + if [ -f "${BENCH_BASE_DIR}/docs/examples/example-config-performance.toml" ]; then + CONFIG_FILE="docs/examples/example-config-performance.toml" + elif [ -f "${BENCH_BASE_DIR}/docs/en/examples/example-config-performance.toml" ]; then + CONFIG_FILE="docs/en/examples/example-config-performance.toml" + fi fi - if [ ! -f "${CONFIG_FILE}" ] && [ -f "docs/en/examples/example-config-performance.toml" ]; then - CONFIG_FILE="docs/en/examples/example-config-performance.toml" + if [ ! -f "${BENCH_BASE_DIR}/${CONFIG_FILE}" ]; then + echo "ERROR: No performance config found under ${BENCH_BASE_DIR} (tried ${CONFIG_FILE})" + exit 1 fi + echo "Using base benchmark config: ${CONFIG_FILE}" (cd "${BENCH_BASE_DIR}" && uv sync --dev) + # Use HEAD's runner script (has current CLI), but execute benchmarks in base workdir. uv run python dev/scripts/run_benchmark_suite.py \ --output-dir "${BENCH_BASE_DIR}" \ --workdir "${BENCH_BASE_DIR}" \ diff --git a/tests/unit/discovery/test_dht_bootstrap_readiness.py b/tests/unit/discovery/test_dht_bootstrap_readiness.py index 9747714..69efcc6 100644 --- a/tests/unit/discovery/test_dht_bootstrap_readiness.py +++ b/tests/unit/discovery/test_dht_bootstrap_readiness.py @@ -29,6 +29,7 @@ def _make_stub_client() -> AsyncDHTClient: client._max_empty_table_rebootstrap_attempts = 3 client._last_empty_table_rebootstrap_at = 0.0 client._empty_table_rebootstrap_backoff = 1.0 + client._empty_table_backoff_factor = 1.5 client._zero_node_rebootstrap_task = None return client diff --git a/tests/unit/discovery/test_tracker_ssl.py b/tests/unit/discovery/test_tracker_ssl.py index 3587d00..f27aaae 100644 --- a/tests/unit/discovery/test_tracker_ssl.py +++ b/tests/unit/discovery/test_tracker_ssl.py @@ -169,7 +169,7 @@ async def test_make_request_https_with_ssl_enabled(self): response_data = await client._make_request_async(url) assert response_data == b"response data" - mock_session.get.assert_called_once_with(url) + mock_session.get.assert_called_once_with(url, allow_redirects=False) @pytest.mark.asyncio async def test_make_request_https_with_ssl_disabled(self): diff --git a/tests/unit/security/test_mse_handshake.py b/tests/unit/security/test_mse_handshake.py index f768e6e..886d220 100644 --- a/tests/unit/security/test_mse_handshake.py +++ b/tests/unit/security/test_mse_handshake.py @@ -1316,6 +1316,7 @@ async def test_receiver_resolves_candidate_hash_from_initial_payload() -> None: chosen_info_hash = b"\x22" * 20 initial_payload = b"peer-handshake-placeholder" responder_results: asyncio.Queue[MSEHandshakeResult] = asyncio.Queue() + handshake_timeout = _MSE_INTEGRATION_TIMEOUT async def responder(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: handshake = MSEHandshake() @@ -1323,7 +1324,7 @@ async def responder(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) reader=reader, writer=writer, info_hash=ignored_info_hash, - timeout=5.0, + timeout=handshake_timeout, initial_payload_size=0, info_hash_candidates=[ignored_info_hash, chosen_info_hash], ) @@ -1334,29 +1335,40 @@ async def responder(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) server = await asyncio.start_server(responder, "127.0.0.1", 0) try: server_port = server.sockets[0].getsockname()[1] - initiator_reader, initiator_writer = await asyncio.open_connection( - "127.0.0.1", server_port - ) - try: - initiator_handshake = MSEHandshake() - initiator_result = await initiator_handshake.initiate_as_initiator( - initiator_reader, - initiator_writer, - chosen_info_hash, - timeout=5.0, - initial_payload=initial_payload, - ) - responder_result = await asyncio.wait_for( - responder_results.get(), timeout=5.0 + last_error: str | None = None + initiator_result: MSEHandshakeResult | None = None + responder_result: MSEHandshakeResult | None = None + for attempt in range(2): + initiator_reader, initiator_writer = await asyncio.open_connection( + "127.0.0.1", server_port ) - - assert initiator_result.success is True - assert responder_result.success is True - assert responder_result.resolved_info_hash == chosen_info_hash - assert responder_result.decrypted_initial_data == initial_payload - finally: - initiator_writer.close() - await initiator_writer.wait_closed() + try: + initiator_handshake = MSEHandshake() + initiator_result = await initiator_handshake.initiate_as_initiator( + initiator_reader, + initiator_writer, + chosen_info_hash, + timeout=handshake_timeout, + initial_payload=initial_payload, + ) + responder_result = await asyncio.wait_for( + responder_results.get(), timeout=handshake_timeout + ) + if initiator_result.success and responder_result.success: + break + last_error = initiator_result.error or responder_result.error + finally: + initiator_writer.close() + await initiator_writer.wait_closed() + if attempt == 0: + await asyncio.sleep(0.05) + + assert initiator_result is not None + assert responder_result is not None + assert initiator_result.success is True, last_error + assert responder_result.success is True, last_error + assert responder_result.resolved_info_hash == chosen_info_hash + assert responder_result.decrypted_initial_data == initial_payload finally: server.close() await server.wait_closed() diff --git a/tests/unit/session/test_peer_initializer.py b/tests/unit/session/test_peer_initializer.py index bdda79b..11f7854 100644 --- a/tests/unit/session/test_peer_initializer.py +++ b/tests/unit/session/test_peer_initializer.py @@ -2,6 +2,7 @@ from typing import Any +import ccbt.session.peers as peers_mod from ccbt.config.config import get_config from ccbt.session.models import SessionContext from ccbt.session.peers import PeerManagerInitializer @@ -12,16 +13,37 @@ def __init__(self, *_: Any, **__: Any) -> None: self._started = False self._security_manager = None self._is_private = False + self.connections: dict[str, Any] = {} + self.on_peer_connected = None + self.on_peer_disconnected = None + self.on_piece_received = None + self.on_bitfield_received = None + + def set_security_manager(self, _manager: Any) -> None: + self._security_manager = _manager + + def set_is_private(self, is_private: bool) -> None: + self._is_private = is_private async def start(self) -> None: self._started = True + async def stop(self) -> None: + self._started = False + + async def connect_to_peers(self, _peers: Any) -> None: + return None + + def get_connected_peers(self) -> list[Any]: + return [] + + def get_active_peers(self) -> list[Any]: + return [] + async def test_peer_initializer_binds_and_starts(monkeypatch: Any) -> None: # Monkeypatch the async peer manager used inside the initializer - import ccbt.session.peers as peers_mod - - peers_mod.AsyncPeerConnectionManager = FakePeerManager # type: ignore[attr-defined] + monkeypatch.setattr(peers_mod, "AsyncPeerConnectionManager", FakePeerManager) class DM: def __init__(self) -> None: diff --git a/tests/unit/session/test_session_manager_metrics_and_admin.py b/tests/unit/session/test_session_manager_metrics_and_admin.py index 8ed9f3c..ed661cd 100644 --- a/tests/unit/session/test_session_manager_metrics_and_admin.py +++ b/tests/unit/session/test_session_manager_metrics_and_admin.py @@ -14,6 +14,8 @@ async def test_set_rate_limits_and_stats_aggregation(monkeypatch): class _Dummy: def __init__(self): self.info = type("Info", (), {"status": "downloading"})() + self.download_manager = None + self.peer_manager = None self._status = { "status": "downloading", "download_rate": 2.5, @@ -24,6 +26,13 @@ def __init__(self): "left": 900, "peers": 3, } + self._cached_status = { + "status": self._status["status"], + "download_rate": self._status["download_rate"], + "upload_rate": self._status["upload_rate"], + "progress": self._status["progress"], + "connected_peers": self._status["peers"], + } async def get_status(self): return dict(self._status) diff --git a/tests/unit/session/test_session_remaining_coverage.py b/tests/unit/session/test_session_remaining_coverage.py index e8bc4b7..1e932c2 100644 --- a/tests/unit/session/test_session_remaining_coverage.py +++ b/tests/unit/session/test_session_remaining_coverage.py @@ -208,6 +208,12 @@ async def test_aggregate_torrent_stats_with_torrents(monkeypatch): class _Session: def __init__(self, name): self.name = name + self.download_manager = None + self.peer_manager = None + self._cached_status = { + "download_rate": 100.0, + "upload_rate": 50.0, + } @property def downloaded_bytes(self):