Simon Jakobi pushed to branch wip/sjakobi/testsuite-atomic-output at Glasgow Haskell Compiler / GHC

Commits:

2 changed files:

Changes:

  • testsuite/driver/runtests.py
    ... ... @@ -487,6 +487,7 @@ if config.list_broken:
    487 487
             print('')
    
    488 488
     else:
    
    489 489
         # Now run all the tests
    
    490
    +    install_output_proxies() # avoid interleaved output from concurrent tests
    
    490 491
         try:
    
    491 492
             async def run_parallelTests():
    
    492 493
                 sem = asyncio.Semaphore(config.threads)
    

  • testsuite/driver/testlib.py
    ... ... @@ -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 ()