Skip to content

Commit 425416d

Browse files
committed
test framework: threadlocalize nesting level
1 parent ef900ba commit 425416d

1 file changed

Lines changed: 24 additions & 11 deletions

File tree

unpythonic/test/fixtures.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,8 @@ class TestConfig:
324324
errors. `TestConfig.postproc` sets the default that is used when no other
325325
(more local) `postproc` is in effect.
326326
327-
It receives one argument, which is a `TestFailure` or `TestError` instance
328-
that was signaled by a failed or errored test (respectively).
327+
It receives one argument, which is a `TestFailure`, `TestError` or `TestWarning`
328+
instance that was signaled by a failed, errored or warned test (respectively).
329329
330330
`postproc` is called after sending the error to `printer`, just before
331331
resuming with the remaining tests. To continue processing, the `postproc`
@@ -334,6 +334,21 @@ class TestConfig:
334334
If you want a failure in a particular testset to abort the whole unit, you
335335
can use `terminate` as your `postproc`.
336336
"""
337+
# It is overwhelmingly common that tests are invoked from a single thread,
338+
# so by default, all threads share the same printer. (It is not worth
339+
# complicating the common use case here to cater for the rare use case.)
340+
#
341+
# However, if you want different printers in different threads, that can
342+
# be done. As `printer`, use a `Shim` that contains a `ThreadLocalBox`.
343+
# In each thread, place in that box a custom object that has a `__call__`
344+
# method that takes the same args `print` does. Because `Shim` redirects
345+
# all attribute accesses, it will redirect the lookup of `__call__`
346+
# (it doesn't have its own `__call__`, so it assumes the client wants to
347+
# call the thing that is inside the box), and hence that method will then
348+
# be used for printing.
349+
#
350+
# TODO: This is subject to change later if I figure out a better design
351+
# TODO: that conveniently caters for *both* the common and rare use cases.
337352
printer = partial(print, file=sys.stderr)
338353
use_color = True
339354
postproc = None
@@ -511,8 +526,7 @@ def catch_signals(state):
511526
yield
512527
_threadlocals.catch_uncaught_signals.popleft()
513528

514-
# TODO: how to handle nesting level across multiple threads? Fully independent doesn't make sense.
515-
_nesting_level = 0
529+
_threadlocals.nesting_level = 0
516530
@contextmanager
517531
def session(name=None):
518532
"""Context manager representing a test session.
@@ -523,7 +537,7 @@ def session(name=None):
523537
To terminate the session by the first failure in a particular testset,
524538
use `terminate` as `postproc` for that testset.
525539
"""
526-
if _nesting_level > 0:
540+
if _threadlocals.nesting_level > 0:
527541
raise RuntimeError("A test `session` cannot be nested inside a `testset`.")
528542

529543
title = maybe_colorize("SESSION", TC.BRIGHT, TestConfig.CS.HEADING)
@@ -575,10 +589,9 @@ def makeindent(level):
575589
indent += " "
576590
return indent
577591

578-
global _nesting_level
579-
indent = makeindent(_nesting_level)
580-
errmsg_indent = makeindent(_nesting_level + 1)
581-
_nesting_level += 1
592+
indent = makeindent(_threadlocals.nesting_level)
593+
errmsg_indent = makeindent(_threadlocals.nesting_level + 1)
594+
_threadlocals.nesting_level += 1
582595

583596
title = "{}Testset".format(indent)
584597
if name is not None:
@@ -653,8 +666,8 @@ def print_and_proceed(condition):
653666
finally:
654667
if postproc is not None:
655668
_threadlocals.postproc_stack.popleft()
656-
_nesting_level -= 1
657-
assert _nesting_level >= 0
669+
_threadlocals.nesting_level -= 1
670+
assert _threadlocals.nesting_level >= 0
658671

659672
r2, f2, e2, w2 = counters()
660673
runs = r2 - r1

0 commit comments

Comments
 (0)