Fix an issue when running tests on Windows with Python 3.6 and later. Since Python 3.6 on Windows, io.IOBase object overlaid tty is incompatible with other file like objects. This makes it impossible to redirect input or output between file and tty without changing file descriptor number by using os.dup2() without changing the overlaid object [1]. This prevents our test suite from running correctly because it attempts to redirect stdout to a file using os.dup2() [2]. This is a workaround for it. [1] _io._WindowsConsoleIO breaks in the face of fd redirection https://bugs.python.org/issue30555 [2] https://lists.apache.org/thread.html/r87a33f0046949fe59188e844832297d8a00e675c4e59a1ab01005e0e%40%3Cdev.subversion.apache.org%3E * build/run_tests.py (rebuild_textio): New. (TestHarness._run_py_test): Rebuild IO object after os.dup2() if it is needed. Found by: jcorvel Index: build/run_tests.py =================================================================== --- build/run_tests.py (revision 1876712) +++ build/run_tests.py (working copy) @@ -48,6 +48,7 @@ ''' import os, sys, shutil, codecs +import io import re import logging import optparse, subprocess, threading, traceback @@ -141,6 +142,10 @@ else: return s.decode("latin-1") +def rebuild_textio(fd, mode): + '''Rebuild an io.BaseIO object from file descriptor for sys.std*''' + return open(fd, mode, encoding='utf-8', errors='replace', closefd=False) + class TestHarness: '''Test harness for Subversion tests. ''' @@ -844,6 +849,16 @@ # setup the output pipes if self.log: + # Since Python 3.6 on Windows, redirecting tty to file or file to tty + # by using os.dup2 breaks overlaid IO object. + # (See https://bugs.python.org/issue30555). + # To avoid this, we rebuild IO object after os.dup2 if it is needed. + need_rebuild_stdout = ( isinstance(sys.stdout, io.IOBase) + and not isinstance(sys.stdout.buffer.raw, + io.FileIO)) + need_rebuild_stderr = ( isinstance(sys.stderr, io.IOBase) + and not isinstance(sys.stderr.buffer.raw, + io.FileIO)) sys.stdout.flush() sys.stderr.flush() self.log.flush() @@ -851,6 +866,10 @@ old_stderr = os.dup(sys.stderr.fileno()) os.dup2(self.log.fileno(), sys.stdout.fileno()) os.dup2(self.log.fileno(), sys.stderr.fileno()) + if need_rebuild_stdout: + sys.stdout = rebuild_textio(sys.stdout.fileno(), 'w') + if need_rebuild_stderr: + sys.stderr = rebuild_textio(sys.stderr.fileno(), 'w') # These have to be class-scoped for use in the progress_func() self.dots_written = 0 @@ -897,6 +916,10 @@ os.dup2(old_stderr, sys.stderr.fileno()) os.close(old_stdout) os.close(old_stderr) + if need_rebuild_stdout: + sys.stdout = rebuild_textio(sys.stdout.fileno(), 'w') + if need_rebuild_stderr: + sys.stderr = rebuild_textio(sys.stderr.fileno(), 'w') return failed