项目文件夹

文件
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

255 行
8.3 KiB
Python

"""Unit tests for ``tests/integration/concurrency/helpers.py``.
Each helper has happy-path + at least one edge case. Helpers are
covered here (not via dogfooding in downstream fix tests) so a
helper-level regression surfaces before any consumer PR depends on it.
"""
from __future__ import annotations
import asyncio
import pytest
from .helpers import (
assert_no_concurrent_overlap,
assert_unique_outputs,
with_simulated_cancel,
)
# pure helper-logic tests for tests/integration/concurrency/helpers.py;
# no HTTP, no cassette. Opt out of the tier-enforcement hook in
# tests/integration/conftest.py.
pytestmark = pytest.mark.allow_no_vcr
# ---------------------------------------------------------------------------
# assert_no_concurrent_overlap
# ---------------------------------------------------------------------------
def test_no_concurrent_overlap_serialized_passes() -> None:
"""A serialized stream (each enter followed by exit) passes."""
events = [
(0.0, "enter"),
(0.1, "exit"),
(0.2, "enter"),
(0.3, "exit"),
]
assert_no_concurrent_overlap(events) # type: ignore[arg-type]
def test_no_concurrent_overlap_detects_overlap() -> None:
"""Two enters without an intervening exit raises with index/timestamp."""
events = [
(0.0, "enter"),
(0.1, "enter"), # overlap at index 1
(0.2, "exit"),
(0.3, "exit"),
]
with pytest.raises(AssertionError, match="index 1.*timestamp=0.1"):
assert_no_concurrent_overlap(events) # type: ignore[arg-type]
def test_no_concurrent_overlap_unmatched_exit_raises() -> None:
"""An exit before any enter is a caller bug; raise with a clear msg."""
events = [
(0.0, "exit"),
]
with pytest.raises(AssertionError, match="unmatched exit.*index 0"):
assert_no_concurrent_overlap(events) # type: ignore[arg-type]
def test_no_concurrent_overlap_dangling_enter_raises() -> None:
"""A stream that ends with inflight > 0 raises (caller bug)."""
events = [
(0.0, "enter"),
# no matching exit
]
with pytest.raises(AssertionError, match="ended with inflight=1"):
assert_no_concurrent_overlap(events) # type: ignore[arg-type]
def test_no_concurrent_overlap_unknown_kind_raises() -> None:
"""An unknown event kind is a caller bug."""
events = [(0.0, "wat")]
with pytest.raises(AssertionError, match="unknown event kind"):
assert_no_concurrent_overlap(events) # type: ignore[arg-type]
def test_no_concurrent_overlap_empty_stream_passes() -> None:
"""No events is vacuously serialized."""
assert_no_concurrent_overlap([])
# ---------------------------------------------------------------------------
# assert_unique_outputs
# ---------------------------------------------------------------------------
async def test_unique_outputs_distinct_passes() -> None:
"""Each invocation returns a fresh value -> no duplicates."""
counter = 0
async def _next() -> int:
nonlocal counter
counter += 1
return counter
outputs = await assert_unique_outputs(_next, 50)
assert len(outputs) == 50
assert sorted(outputs) == list(range(1, 51))
async def test_unique_outputs_duplicate_raises_with_indices() -> None:
"""A constant-returning callable trips the duplicate check."""
async def _const() -> str:
return "same"
with pytest.raises(AssertionError, match=r"duplicate output 'same'"):
await assert_unique_outputs(_const, 3)
async def test_unique_outputs_zero_raises_value_error() -> None:
"""``n_concurrent < 1`` is a misuse and raises early."""
async def _noop() -> None:
return None
with pytest.raises(ValueError, match="n_concurrent must be >= 1"):
await assert_unique_outputs(_noop, 0)
async def test_unique_outputs_returns_outputs_for_caller_assertions() -> None:
"""Callers may add their own assertions on the returned list."""
seq = iter(range(5))
async def _seq() -> int:
return next(seq)
outputs = await assert_unique_outputs(_seq, 5)
# Caller can check shape beyond uniqueness. Use ``sorted`` /
# ``set`` rather than positional equality so the test stays
# stable across asyncio event-loop scheduling implementations
# (CPython runs zero-await coroutines FIFO; uvloop/others may not).
assert sorted(outputs) == [0, 1, 2, 3, 4]
assert set(outputs) == set(range(5))
# ---------------------------------------------------------------------------
# with_simulated_cancel
# ---------------------------------------------------------------------------
async def test_with_simulated_cancel_returns_result_when_fast_enough() -> None:
"""A coro that finishes before the cancel fires returns its result."""
async def _quick() -> str:
await asyncio.sleep(0.01)
return "done"
result = await with_simulated_cancel(_quick(), delay=1.0)
assert result == "done"
async def test_with_simulated_cancel_returns_cancelled_error_on_timeout() -> None:
"""A coro slower than the cancel returns ``CancelledError``."""
async def _slow() -> str:
await asyncio.sleep(10.0)
return "never"
result = await with_simulated_cancel(_slow(), delay=0.05, label="slow")
assert isinstance(result, asyncio.CancelledError)
async def test_with_simulated_cancel_returns_application_exception() -> None:
"""If the coro raises before the cancel, that exception is captured."""
async def _raises() -> str:
await asyncio.sleep(0.01)
raise RuntimeError("boom")
result = await with_simulated_cancel(_raises(), delay=1.0)
assert isinstance(result, RuntimeError)
assert str(result) == "boom"
async def test_with_simulated_cancel_negative_delay_raises() -> None:
"""``delay < 0`` is a misuse and raises early."""
async def _noop() -> None:
return None
with pytest.raises(ValueError, match="delay must be >= 0"):
await with_simulated_cancel(_noop(), delay=-1.0)
async def test_with_simulated_cancel_propagates_outer_cancellation_to_inner() -> None:
"""Outer cancel of the helper itself must cancel the wrapped task too.
Otherwise the inner task becomes a stray background task and the
cancellation is silently consumed. Regression coverage for the
claude[bot] iter-2 finding.
"""
inner_started = asyncio.Event()
inner_finished = False
async def _slow() -> str:
nonlocal inner_finished
inner_started.set()
try:
await asyncio.sleep(10.0)
inner_finished = True
return "never"
except asyncio.CancelledError:
# Re-raise so the cancellation actually takes effect on
# the task (the helper expects to receive CancelledError
# from awaiting the task).
raise
helper_task = asyncio.create_task(
with_simulated_cancel(_slow(), delay=10.0, label="outer-cancel-test")
)
# Wait until the inner coro has started (so the task is real).
await inner_started.wait()
# Give the canceller a tick to schedule, then cancel the OUTER helper.
await asyncio.sleep(0)
helper_task.cancel()
# The helper catches CancelledError and returns it as a value
# (per its contract); we just verify the outer cancel did NOT
# leave the inner running.
try:
result = await helper_task
assert isinstance(result, asyncio.CancelledError)
except asyncio.CancelledError:
# Acceptable: depending on timing, the outer cancel may
# propagate before the helper's except block runs.
pass
# Yield to let the inner task observe its own cancellation.
await asyncio.sleep(0)
assert not inner_finished, (
"Inner task should have been cancelled when the outer helper was cancelled."
)
async def test_with_simulated_cancel_cleans_up_canceller_task() -> None:
"""The canceller task should not leak after the helper returns.
No direct API to inspect; we rely on no warnings about
"Task was destroyed but it is pending" emerging at teardown. This
test exists so a future regression that drops the cleanup `cancel()`
will surface as a noisy test run.
"""
async def _quick() -> int:
return 42
result = await with_simulated_cancel(_quick(), delay=0.5)
assert result == 42
# Yield once so any pending cancellation can complete cleanly.
await asyncio.sleep(0)