Skip to content

Commit 18f7bcf

Browse files
committed
test(parallel): close coverage gaps to 100% branch
1 parent e96e1fc commit 18f7bcf

7 files changed

Lines changed: 91 additions & 11 deletions

File tree

progressbar/_parallel/_async.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,8 @@ async def completions(self) -> typing.AsyncIterator[Completion]:
141141
while self.in_flight:
142142
self._check_deadline()
143143
task = await self._next_done()
144-
if task is None:
145-
continue
146-
event: Completion | None = self._handle(task)
147-
if event is not None:
148-
yield event
144+
if task is not None:
145+
yield self._handle(task)
149146

150147
def _launch_one(self) -> bool:
151148
"""Create the next task; `False` when the input is exhausted."""
@@ -196,7 +193,7 @@ def _check_deadline(self) -> None:
196193
f'parallel execution exceeded timeout={self.timeout}'
197194
)
198195

199-
def _handle(self, task: asyncio.Task[typing.Any]) -> Completion | None:
196+
def _handle(self, task: asyncio.Task[typing.Any]) -> Completion:
200197
"""Turn one finished task into a completion event."""
201198
index, args, seq = self.in_flight.pop(task)
202199
if task.cancelled():

progressbar/_parallel/_display.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,8 @@ def tick(self) -> None:
280280

281281
def finish(self, *, success: bool = True) -> None:
282282
"""Finish the overall bar and wind down the render thread."""
283-
for key in list(self._keys.values()):
284-
if key in self.multibar:
285-
del self.multibar[key]
286-
self._keys.clear()
283+
for seq in list(self._keys):
284+
self.task_finished(seq, ok=success)
287285
self._overall.finish(dirty=not success)
288286
if self._started_thread:
289287
# One last frame so the final state is on screen even if

progressbar/_parallel/_sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def _interpreter_executor(
109109
)
110110
except AttributeError:
111111
raise ValueError('pool="interpreter" requires Python 3.14+') from None
112-
return interpreter_pool(
112+
return interpreter_pool( # pragma: no cover - reachable on 3.14+ only
113113
max_workers=workers,
114114
initializer=initializer,
115115
initargs=initargs,

tests/test_parallel_async.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,46 @@ async def _run() -> tuple[list[int], list[tuple[int, int]]]:
347347
assert pairs == [(0, 0), (1, 2), (2, 4)]
348348

349349

350+
class TestMultiBarMode:
351+
def test_async_workers_see_their_task_bar(self) -> None:
352+
from progressbar._parallel import _common
353+
354+
seen: list[bool] = []
355+
356+
async def _check(value: int) -> int:
357+
seen.append(_common.current_task_bar() is not None)
358+
return value
359+
360+
async def _run() -> list[int]:
361+
return await _async.amap(
362+
_check, range(3), bar='multi', fd=io.StringIO()
363+
)
364+
365+
assert asyncio.run(_run()) == [0, 1, 2]
366+
assert seen == [True, True, True]
367+
368+
369+
class TestExternalCancellation:
370+
def test_self_cancelling_task_surfaces(self) -> None:
371+
async def _self_cancel(value: int) -> int:
372+
if value == 1:
373+
task = asyncio.current_task()
374+
assert task is not None
375+
task.cancel()
376+
await asyncio.sleep(1)
377+
return value
378+
379+
async def _run() -> list[int]:
380+
return await _async.amap(
381+
_self_cancel, range(3), concurrency=1, bar=False
382+
)
383+
384+
# A cancellation this run did not initiate must surface, never
385+
# silently drop the item.
386+
with pytest.raises(asyncio.CancelledError):
387+
asyncio.run(_run())
388+
389+
350390
class TestCallStrategy:
351391
def test_detects_coroutine_function(self) -> None:
352392
assert _async._call_strategy(_async_double) == 'async'

tests/test_parallel_display.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,21 @@ def test_render_thread_stopped_after_finish(self) -> None:
152152
display.finish()
153153
assert display.multibar._thread is None # noqa: SLF001
154154

155+
def test_task_finished_with_unknown_seq_is_a_noop(self) -> None:
156+
display = self._multi()
157+
display.start(1)
158+
display.task_finished(99, ok=True)
159+
display.finish()
160+
161+
def test_finish_removes_live_task_bars(self) -> None:
162+
display = self._multi()
163+
display.start(3)
164+
display.task_started(1, 'still-running')
165+
display.task_started(2, 'also-running')
166+
display.finish(success=False)
167+
assert '1: still-running' not in display.multibar
168+
assert '2: also-running' not in display.multibar
169+
155170
def test_adopts_existing_multibar_without_stopping_it(self) -> None:
156171
multibar = progressbar.MultiBar(fd=io.StringIO())
157172
multibar.start()

tests/test_parallel_map.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,32 @@ def _record(value: int) -> int:
100100
assert main_thread.name not in seen
101101

102102

103+
class TestMultiBarMode:
104+
def test_workers_see_their_task_bar(self) -> None:
105+
from progressbar._parallel import _common
106+
107+
seen: list[bool] = []
108+
109+
def _check(value: int) -> int:
110+
seen.append(_common.current_task_bar() is not None)
111+
return value
112+
113+
_sync.map(_check, range(4), workers=2, bar='multi', fd=io.StringIO())
114+
assert seen == [True, True, True, True]
115+
116+
def test_plain_mode_has_no_task_bar(self) -> None:
117+
from progressbar._parallel import _common
118+
119+
seen: list[bool] = []
120+
121+
def _check(value: int) -> int:
122+
seen.append(_common.current_task_bar() is None)
123+
return value
124+
125+
_sync.map(_check, range(2), workers=2, bar=False)
126+
assert seen == [True, True]
127+
128+
103129
class TestResolveExecutor:
104130
def test_thread_pool_created_and_owned(self) -> None:
105131
executor, owned, workers = _sync.resolve_executor(

tests/test_parallel_shell.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def test_on_error_return_embeds_the_error(self) -> None:
124124
assert results[0].returncode == 0
125125
assert isinstance(results[1], subprocess.CalledProcessError)
126126

127+
def test_pool_kwarg_rejected(self) -> None:
128+
with pytest.raises(TypeError, match=r'Pool\.run'):
129+
_shell.run(_EXIT, [1], pool='process', bar=False)
130+
127131
@pytest.mark.no_freezegun
128132
def test_pool_run_method(self) -> None:
129133
with _sync.Pool(2) as pool:

0 commit comments

Comments
 (0)