|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
| 3 | +import re |
3 | 4 | from typing import TypeVar
|
4 | 5 |
|
5 | 6 | import pytest
|
6 | 7 |
|
| 8 | +import trio |
| 9 | +from trio.lowlevel import ( |
| 10 | + add_parking_lot_breaker, |
| 11 | + current_task, |
| 12 | + remove_parking_lot_breaker, |
| 13 | +) |
| 14 | +from trio.testing import Matcher, RaisesGroup |
| 15 | + |
7 | 16 | from ... import _core
|
8 | 17 | from ...testing import wait_all_tasks_blocked
|
9 | 18 | from .._parking_lot import ParkingLot
|
@@ -215,3 +224,161 @@ async def test_parking_lot_repark_with_count() -> None:
|
215 | 224 | "wake 2",
|
216 | 225 | ]
|
217 | 226 | lot1.unpark_all()
|
| 227 | + |
| 228 | + |
| 229 | +async def dummy_task( |
| 230 | + task_status: _core.TaskStatus[_core.Task] = trio.TASK_STATUS_IGNORED, |
| 231 | +) -> None: |
| 232 | + task_status.started(_core.current_task()) |
| 233 | + await trio.sleep_forever() |
| 234 | + |
| 235 | + |
| 236 | +async def test_parking_lot_breaker_basic() -> None: |
| 237 | + """Test basic functionality for breaking lots.""" |
| 238 | + lot = ParkingLot() |
| 239 | + task = current_task() |
| 240 | + |
| 241 | + # defaults to current task |
| 242 | + lot.break_lot() |
| 243 | + assert lot.broken_by == [task] |
| 244 | + |
| 245 | + # breaking the lot again with the same task appends another copy in `broken_by` |
| 246 | + lot.break_lot() |
| 247 | + assert lot.broken_by == [task, task] |
| 248 | + |
| 249 | + # trying to park in broken lot errors |
| 250 | + broken_by_str = re.escape(str([task, task])) |
| 251 | + with pytest.raises( |
| 252 | + _core.BrokenResourceError, |
| 253 | + match=f"^Attempted to park in parking lot broken by {broken_by_str}$", |
| 254 | + ): |
| 255 | + await lot.park() |
| 256 | + |
| 257 | + |
| 258 | +async def test_parking_lot_break_parking_tasks() -> None: |
| 259 | + """Checks that tasks currently waiting to park raise an error when the breaker exits.""" |
| 260 | + |
| 261 | + async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None: |
| 262 | + add_parking_lot_breaker(current_task(), lot) |
| 263 | + with scope: |
| 264 | + await trio.sleep_forever() |
| 265 | + |
| 266 | + lot = ParkingLot() |
| 267 | + cs = _core.CancelScope() |
| 268 | + |
| 269 | + # check that parked task errors |
| 270 | + with RaisesGroup( |
| 271 | + Matcher(_core.BrokenResourceError, match="^Parking lot broken by"), |
| 272 | + ): |
| 273 | + async with _core.open_nursery() as nursery: |
| 274 | + nursery.start_soon(bad_parker, lot, cs) |
| 275 | + await wait_all_tasks_blocked() |
| 276 | + |
| 277 | + nursery.start_soon(lot.park) |
| 278 | + await wait_all_tasks_blocked() |
| 279 | + |
| 280 | + cs.cancel() |
| 281 | + |
| 282 | + |
| 283 | +async def test_parking_lot_breaker_registration() -> None: |
| 284 | + lot = ParkingLot() |
| 285 | + task = current_task() |
| 286 | + |
| 287 | + with pytest.raises( |
| 288 | + RuntimeError, |
| 289 | + match="Attempted to remove task as breaker for a lot it is not registered for", |
| 290 | + ): |
| 291 | + remove_parking_lot_breaker(task, lot) |
| 292 | + |
| 293 | + # check that a task can be registered as breaker for the same lot multiple times |
| 294 | + add_parking_lot_breaker(task, lot) |
| 295 | + add_parking_lot_breaker(task, lot) |
| 296 | + remove_parking_lot_breaker(task, lot) |
| 297 | + remove_parking_lot_breaker(task, lot) |
| 298 | + |
| 299 | + with pytest.raises( |
| 300 | + RuntimeError, |
| 301 | + match="Attempted to remove task as breaker for a lot it is not registered for", |
| 302 | + ): |
| 303 | + remove_parking_lot_breaker(task, lot) |
| 304 | + |
| 305 | + # registering a task as breaker on an already broken lot is fine |
| 306 | + lot.break_lot() |
| 307 | + child_task = None |
| 308 | + async with trio.open_nursery() as nursery: |
| 309 | + child_task = await nursery.start(dummy_task) |
| 310 | + add_parking_lot_breaker(child_task, lot) |
| 311 | + nursery.cancel_scope.cancel() |
| 312 | + assert lot.broken_by == [task, child_task] |
| 313 | + |
| 314 | + # manually breaking a lot with an already exited task is fine |
| 315 | + lot = ParkingLot() |
| 316 | + lot.break_lot(child_task) |
| 317 | + assert lot.broken_by == [child_task] |
| 318 | + |
| 319 | + |
| 320 | +async def test_parking_lot_breaker_rebreak() -> None: |
| 321 | + lot = ParkingLot() |
| 322 | + task = current_task() |
| 323 | + lot.break_lot() |
| 324 | + |
| 325 | + # breaking an already broken lot with a different task is allowed |
| 326 | + # The nursery is only to create a task we can pass to lot.break_lot |
| 327 | + async with trio.open_nursery() as nursery: |
| 328 | + child_task = await nursery.start(dummy_task) |
| 329 | + lot.break_lot(child_task) |
| 330 | + nursery.cancel_scope.cancel() |
| 331 | + |
| 332 | + assert lot.broken_by == [task, child_task] |
| 333 | + |
| 334 | + |
| 335 | +async def test_parking_lot_multiple_breakers_exit() -> None: |
| 336 | + # register multiple tasks as lot breakers, then have them all exit |
| 337 | + lot = ParkingLot() |
| 338 | + async with trio.open_nursery() as nursery: |
| 339 | + child_task1 = await nursery.start(dummy_task) |
| 340 | + child_task2 = await nursery.start(dummy_task) |
| 341 | + child_task3 = await nursery.start(dummy_task) |
| 342 | + add_parking_lot_breaker(child_task1, lot) |
| 343 | + add_parking_lot_breaker(child_task2, lot) |
| 344 | + add_parking_lot_breaker(child_task3, lot) |
| 345 | + nursery.cancel_scope.cancel() |
| 346 | + |
| 347 | + # I think the order is guaranteed currently, but doesn't hurt to be safe. |
| 348 | + assert set(lot.broken_by) == {child_task1, child_task2, child_task3} |
| 349 | + |
| 350 | + |
| 351 | +async def test_parking_lot_breaker_register_exited_task() -> None: |
| 352 | + lot = ParkingLot() |
| 353 | + child_task = None |
| 354 | + async with trio.open_nursery() as nursery: |
| 355 | + child_task = await nursery.start(dummy_task) |
| 356 | + nursery.cancel_scope.cancel() |
| 357 | + # trying to register an exited task as lot breaker errors |
| 358 | + with pytest.raises( |
| 359 | + trio.BrokenResourceError, |
| 360 | + match="^Attempted to add already exited task as lot breaker.$", |
| 361 | + ): |
| 362 | + add_parking_lot_breaker(child_task, lot) |
| 363 | + |
| 364 | + |
| 365 | +async def test_parking_lot_break_itself() -> None: |
| 366 | + """Break a parking lot, where the breakee is parked. |
| 367 | + Doing this is weird, but should probably be supported. |
| 368 | + """ |
| 369 | + |
| 370 | + async def return_me_and_park( |
| 371 | + lot: ParkingLot, |
| 372 | + *, |
| 373 | + task_status: _core.TaskStatus[_core.Task] = trio.TASK_STATUS_IGNORED, |
| 374 | + ) -> None: |
| 375 | + task_status.started(_core.current_task()) |
| 376 | + await lot.park() |
| 377 | + |
| 378 | + lot = ParkingLot() |
| 379 | + with RaisesGroup( |
| 380 | + Matcher(_core.BrokenResourceError, match="^Parking lot broken by"), |
| 381 | + ): |
| 382 | + async with _core.open_nursery() as nursery: |
| 383 | + child_task = await nursery.start(return_me_and_park, lot) |
| 384 | + lot.break_lot(child_task) |
0 commit comments