Skip to content

Commit d1599e5

Browse files
committed
Add tests.
1 parent bf74de8 commit d1599e5

2 files changed

Lines changed: 475 additions & 16 deletions

File tree

nox/popen.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ def __init__(
272272
self.terminate_timeout = terminate_timeout
273273
self.collectors: list[asyncio.Task[None] | asyncio.Task[int]] = []
274274
self.main_task: asyncio.Task[bool] | None = None
275+
self.is_terminating = False
276+
self.do_terminate = False
275277

276278
async def _shutdown_process(self, proc: Process) -> None:
277279
"""Gracefully shutdown a child process."""
@@ -295,6 +297,8 @@ async def _main_task(self, proc: Process, wait_task: asyncio.Task[int]) -> bool:
295297
Return ``True`` if the process had a non-zero exit code during shutdown.
296298
"""
297299
try:
300+
if self.do_terminate:
301+
self._handle_sigint()
298302
await proc.wait()
299303
except asyncio.CancelledError:
300304
# SIGINT causes the task to be cancelled. We first need to uncancel it
@@ -352,7 +356,8 @@ async def _read_stream(
352356
# should be data and assume that the chunk is complete and EOF has been reached.
353357
if chunk_part:
354358
continue
355-
is_eof = True
359+
# We mark the following line as 'no cover' since it should never happen.
360+
is_eof = True # pragma: no cover
356361

357362
# Re-combine the line (including separator, if EOF wasn't found).
358363
chunk = b"".join(chunk_parts)
@@ -370,6 +375,7 @@ async def _stream_subprocess(
370375
args: Sequence[str] | str,
371376
*,
372377
env: Mapping[str, str] | None,
378+
extra_tasks: Sequence[asyncio.Task[None]],
373379
) -> tuple[bool, int]:
374380
"""
375381
Start the process, all tasks, and wait for them to finish.
@@ -418,14 +424,33 @@ async def _stream_subprocess(
418424
self.main_task = self.loop.create_task(self._main_task(proc, wait_task))
419425

420426
# We wait for all tasks to finish and extract the return code.
421-
await asyncio.wait([*self.collectors, self.main_task])
427+
await asyncio.wait([*self.collectors, self.main_task, *extra_tasks])
422428
return self.main_task.result(), wait_task.result()
423429

430+
def _handle_sigint(self) -> None:
431+
"""
432+
SIGINT handler for event loop.
433+
"""
434+
# In case this is called before the main task is created,
435+
# set a flag and that's it.
436+
if not self.main_task:
437+
self.do_terminate = True
438+
return
439+
440+
# Don't handle it twice.
441+
if self.is_terminating:
442+
return
443+
self.is_terminating = True
444+
445+
# Cancel the main task. This triggers graceful shutdown.
446+
self.main_task.cancel()
447+
424448
def run(
425449
self,
426450
args: Sequence[str] | str,
427451
*,
428452
env: Mapping[str, str] | None = None,
453+
extra_tasks: Sequence[asyncio.Task[None]] | None = None,
429454
) -> tuple[int, bytes, bytes]:
430455
"""
431456
Run the command with the given environment.
@@ -434,22 +459,10 @@ def run(
434459
Note that this function is **NOT** thread-safe, since we have to
435460
add a SIGINT handler to the event loop and later remove it.
436461
"""
437-
is_terminating = [False]
438-
439-
def handle_sigint() -> None:
440-
# Don't handle it twice.
441-
if is_terminating[0]:
442-
return
443-
is_terminating[0] = True
444-
445-
# Cancel the main task. This triggers graceful shutdown.
446-
if self.main_task:
447-
self.main_task.cancel()
448-
449-
self.loop.add_signal_handler(signal.SIGINT, handle_sigint)
462+
self.loop.add_signal_handler(signal.SIGINT, self._handle_sigint)
450463
try:
451464
is_canceled, return_code = self.loop.run_until_complete(
452-
self._stream_subprocess(args, env=env)
465+
self._stream_subprocess(args, env=env, extra_tasks=extra_tasks or [])
453466
)
454467
finally:
455468
self.loop.remove_signal_handler(signal.SIGINT)

0 commit comments

Comments
 (0)