| ... |
... |
@@ -88,6 +88,61 @@ def get_all_ways() -> Set[WayName]: |
|
88
|
88
|
global testopts_ctx_var
|
|
89
|
89
|
testopts_ctx_var = contextvars.ContextVar('testopts_ctx_var') # type: ignore
|
|
90
|
90
|
|
|
|
91
|
+# Pipe each test's output into a per-test buffer (set up by runTestAtomically)
|
|
|
92
|
+# to avoid interleaving the output of concurrent tests. Writes from contexts
|
|
|
93
|
+# with no active buffer pass straight through to the real streams.
|
|
|
94
|
+
|
|
|
95
|
+output_buffer_ctx_var = contextvars.ContextVar('output_buffer_ctx_var', default=None) # type: contextvars.ContextVar[Optional[io.StringIO]]
|
|
|
96
|
+
|
|
|
97
|
+class _OutputProxyBuffer:
|
|
|
98
|
+ """The .buffer of an _OutputProxy; takes bytes."""
|
|
|
99
|
+ def __init__(self, real) -> None:
|
|
|
100
|
+ self._real = real
|
|
|
101
|
+
|
|
|
102
|
+ def write(self, b: bytes) -> None:
|
|
|
103
|
+ buf = output_buffer_ctx_var.get()
|
|
|
104
|
+ if buf is None:
|
|
|
105
|
+ self._real.buffer.write(b)
|
|
|
106
|
+ else:
|
|
|
107
|
+ buf.write(b.decode('utf-8', errors='backslashreplace'))
|
|
|
108
|
+
|
|
|
109
|
+ def flush(self) -> None:
|
|
|
110
|
+ if output_buffer_ctx_var.get() is None:
|
|
|
111
|
+ self._real.buffer.flush()
|
|
|
112
|
+
|
|
|
113
|
+class _OutputProxy:
|
|
|
114
|
+ def __init__(self, real) -> None:
|
|
|
115
|
+ self._real = real
|
|
|
116
|
+ self.buffer = _OutputProxyBuffer(real)
|
|
|
117
|
+
|
|
|
118
|
+ @property
|
|
|
119
|
+ def encoding(self) -> str:
|
|
|
120
|
+ return self._real.encoding
|
|
|
121
|
+
|
|
|
122
|
+ def write(self, s: str) -> int:
|
|
|
123
|
+ buf = output_buffer_ctx_var.get()
|
|
|
124
|
+ if buf is None:
|
|
|
125
|
+ return self._real.write(s)
|
|
|
126
|
+ return buf.write(s)
|
|
|
127
|
+
|
|
|
128
|
+ def flush(self) -> None:
|
|
|
129
|
+ if output_buffer_ctx_var.get() is None:
|
|
|
130
|
+ self._real.flush()
|
|
|
131
|
+
|
|
|
132
|
+ def isatty(self) -> bool:
|
|
|
133
|
+ return self._real.isatty()
|
|
|
134
|
+
|
|
|
135
|
+ def fileno(self) -> int:
|
|
|
136
|
+ return self._real.fileno()
|
|
|
137
|
+
|
|
|
138
|
+def install_output_proxies() -> None:
|
|
|
139
|
+ # Both streams feed the same per-task buffer, so a test's stdout and
|
|
|
140
|
+ # stderr stay in print order.
|
|
|
141
|
+ if not isinstance(sys.stdout, _OutputProxy):
|
|
|
142
|
+ sys.stdout = _OutputProxy(sys.stdout)
|
|
|
143
|
+ if not isinstance(sys.stderr, _OutputProxy):
|
|
|
144
|
+ sys.stderr = _OutputProxy(sys.stderr)
|
|
|
145
|
+
|
|
91
|
146
|
def getTestOpts() -> TestOptions:
|
|
92
|
147
|
return testopts_ctx_var.get()
|
|
93
|
148
|
|
| ... |
... |
@@ -1502,9 +1557,25 @@ allTestNames = set([]) # type: Set[TestName] |
|
1502
|
1557
|
|
|
1503
|
1558
|
async def runTest(sem, opts, name: TestName, func, args):
|
|
1504
|
1559
|
if sem is None:
|
|
1505
|
|
- return await test_common_work(name, opts, func, args)
|
|
|
1560
|
+ return await runTestAtomically(opts, name, func, args)
|
|
1506
|
1561
|
async with sem:
|
|
|
1562
|
+ return await runTestAtomically(opts, name, func, args)
|
|
|
1563
|
+
|
|
|
1564
|
+async def runTestAtomically(opts, name: TestName, func, args):
|
|
|
1565
|
+ # Buffer this test's output and emit it as one block at the end, so that
|
|
|
1566
|
+ # concurrent tests' output does not interleave.
|
|
|
1567
|
+ buf = io.StringIO()
|
|
|
1568
|
+ token = output_buffer_ctx_var.set(buf)
|
|
|
1569
|
+ try:
|
|
1507
|
1570
|
return await test_common_work(name, opts, func, args)
|
|
|
1571
|
+ finally:
|
|
|
1572
|
+ output_buffer_ctx_var.reset(token)
|
|
|
1573
|
+ s = buf.getvalue()
|
|
|
1574
|
+ if s:
|
|
|
1575
|
+ # The event loop is single-threaded and there is no await between
|
|
|
1576
|
+ # these calls, so the block is written out atomically.
|
|
|
1577
|
+ sys.stdout.write(s)
|
|
|
1578
|
+ sys.stdout.flush()
|
|
1508
|
1579
|
|
|
1509
|
1580
|
# name :: String
|
|
1510
|
1581
|
# setup :: [TestOpt] -> IO ()
|