Skip to content

Commit

Permalink
twisted#11707 fix trial -j / ctrl-c UnboundLocalError bug (twisted#…
Browse files Browse the repository at this point in the history
  • Loading branch information
glyph committed Jul 23, 2023
2 parents 949f4b6 + 8d74ac9 commit a9ee8e5
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 8 deletions.
7 changes: 7 additions & 0 deletions src/twisted/newsfragments/11707.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
When interrupted with control-C, `trial -j` no longer obscures tracebacks for
any errors caused by that interruption with an `UnboundLocalError` due to a bug
in its own implementation. Note that there are still several internal
tracebacks that will be emitted upon exiting, because tearing down the test
runner mid-suite is still not an entirely clean operation, but it should at
least be possible to see errors reported from, for example, a test that is
hanging more clearly.
44 changes: 37 additions & 7 deletions src/twisted/trial/_dist/disttrial.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,17 @@
import sys
from functools import partial
from os.path import isabs
from typing import Awaitable, Callable, Iterable, List, Sequence, TextIO, Union, cast
from typing import (
Awaitable,
Callable,
Iterable,
List,
Optional,
Sequence,
TextIO,
Union,
cast,
)
from unittest import TestCase, TestSuite

from attrs import define, field, frozen
Expand Down Expand Up @@ -441,15 +451,35 @@ async def runAndReport(n: int) -> DistReporter:
await startedPool.join()

def _run(self, test: Union[TestCase, TestSuite], untilFailure: bool) -> IReporter:
result: Union[Failure, DistReporter]
result: Union[Failure, DistReporter, None] = None
reactorStopping: bool = False
testsInProgress: Deferred[object]

def capture(r):
def capture(r: Union[Failure, DistReporter]) -> None:
nonlocal result
result = r

d = Deferred.fromCoroutine(self.runAsync(test, untilFailure))
d.addBoth(capture)
d.addBoth(lambda ignored: self._reactor.stop())
def maybeStopTests() -> Optional[Deferred[object]]:
nonlocal reactorStopping
reactorStopping = True
if result is None:
testsInProgress.cancel()
return testsInProgress
return None

def maybeStopReactor(result: object) -> object:
if not reactorStopping:
self._reactor.stop()
return result

self._reactor.addSystemEventTrigger("before", "shutdown", maybeStopTests)

testsInProgress = (
Deferred.fromCoroutine(self.runAsync(test, untilFailure))
.addBoth(capture)
.addBoth(maybeStopReactor)
)

self._reactor.run()

if isinstance(result, Failure):
Expand All @@ -458,7 +488,7 @@ def capture(r):
# mypy can't see that raiseException raises an exception so we can
# only get here if result is not a Failure, so tell mypy result is
# certainly a DistReporter at this point.
assert isinstance(result, DistReporter)
assert isinstance(result, DistReporter), f"{result} is not DistReporter"

# Unwrap the DistReporter to give the caller some regular IReporter
# object. DistReporter isn't type annotated correctly so fix it here.
Expand Down
19 changes: 18 additions & 1 deletion src/twisted/trial/_dist/test/test_disttrial.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

from twisted.internet import interfaces
from twisted.internet.base import ReactorBase
from twisted.internet.defer import Deferred, succeed
from twisted.internet.defer import CancelledError, Deferred, succeed
from twisted.internet.error import ProcessDone
from twisted.internet.protocol import ProcessProtocol, Protocol
from twisted.internet.test.modulehelpers import AlternateReactor
Expand Down Expand Up @@ -125,6 +125,11 @@ def stop(self):
See L{IReactorCore.stop}.
"""
MemoryReactorClock.stop(self)
# TODO: implementing this more comprehensively in MemoryReactor would
# be nice, this is rather hard-coded to disttrial's current
# implementation.
if "before" in self.triggers:
self.triggers["before"]["shutdown"][0][0]()
self.stopCount += 1

def run(self):
Expand All @@ -139,6 +144,9 @@ def run(self):

for f, args, kwargs in self.whenRunningHooks:
f(*args, **kwargs)
self.stop()
# do not count internal 'stop' against trial-initiated .stop() count
self.stopCount -= 1


class CountingReactorTests(SynchronousTestCase):
Expand Down Expand Up @@ -461,6 +469,15 @@ def test_runUnexpectedError(self) -> None:
assert_that(errors, has_length(1))
assert_that(errors[0][1].type, equal_to(WorkerPoolBroken))

def test_runUnexpectedErrorCtrlC(self) -> None:
"""
If the reactor is stopped by C-c (i.e. `run` returns before the test
case's Deferred has been fired) we should cancel the pending test run.
"""
runner = self.getRunner(workerPoolFactory=LocalWorkerPool)
with self.assertRaises(CancelledError):
runner.run(self.suite)

def test_runUnexpectedWorkerError(self) -> None:
"""
If for some reason the worker process cannot run a test, the error is
Expand Down

0 comments on commit a9ee8e5

Please sign in to comment.