Hi, On 2020/05/07 16:17, Johan Corveleyn wrote:
There is still the issue of PYTHONLEGACYWINDOWSSTDIO that is very much blocking for anyone running the testsuite on Windows with Python 3. Worst case, we might have to simply document it, and perhaps emit a warning if it's not set? Or can we simply set that envvar ourselves, automatically, from within some central place in the testsuite (as soon as redirection to a logfile has been requested or something)? Unless you still have some more magic up your sleeve to handle this, Yasuhito :-).
I created patch to resolve the issue of PYTHONLEGACYWINDOWSSTDIO and remove uses of os.dup2() in run_tests.py. After attached patch, PYTHONLEGACYWINDOWSSTDIO is not needed. [[[ * build/run_tests.py (open_logfile): New function returning file-like object which is reassignable sys.stdout and sys.stderr. (TestHarness.run): Use open_logfile() instead of codecs.open(). (TestHarness._open_log): Ditto. (TestHarness._run_py_test): Reassign sys.stdout and sys.stderr instead of uses of os.dup2(). * subversion/tests/cmdline/svntest/main.py (LoggingStdoutHandler): New function to use the value of sys.stdout at call time. (parse_options): Use LoggingStdoutHandler() instead of StreamHandler(sys.stdout). ]]] Tested with the following environments: * Python 3.8.2 and Python 2.7.18 on Windows 10 * Python 3.8.2 on Linux -- Jun Omae <jun6...@gmail.com> (大前 潤)
* build/run_tests.py (open_logfile): New function returning file-like object which is reassignable sys.stdout and sys.stderr. (TestHarness.run): Use open_logfile() instead of codecs.open(). (TestHarness._open_log): Ditto. (TestHarness._run_py_test): Reassign sys.stdout and sys.stderr instead of uses of os.dup2(). * subversion/tests/cmdline/svntest/main.py (LoggingStdoutHandler): New function to use the value of sys.stdout at call time. (parse_options): Use LoggingStdoutHandler() instead of StreamHandler(sys.stdout). Index: build/run_tests.py =================================================================== --- build/run_tests.py (revision 1877480) +++ build/run_tests.py (working copy) @@ -47,7 +47,7 @@ separated list of test numbers; the default is to run all the tests in it. ''' -import os, sys, shutil, codecs +import os, sys, shutil import re import logging import optparse, subprocess, threading, traceback @@ -141,6 +141,18 @@ else: return s.decode("latin-1") +def open_logfile(filename, mode, encoding='utf-8'): + if sys.version_info[0] != 2: + return open(filename, mode, encoding=encoding, errors='surrogateescape') + else: + class Wrapper(object): + def __init__(self, stream, encoding): + self._stream = stream + self.encoding = encoding + def __getattr__(self, name): + return getattr(self._stream, name) + return Wrapper(open(filename, mode), encoding) + class TestHarness: '''Test harness for Subversion tests. ''' @@ -700,7 +712,7 @@ # Copy the truly interesting verbose logs to a separate file, for easier # viewing. if xpassed or failed_list: - faillog = codecs.open(self.faillogfile, 'w', encoding="latin-1") + faillog = open_logfile(self.faillogfile, 'w') last_start_lineno = None last_start_re = re.compile('^(FAIL|SKIP|XFAIL|PASS|START|CLEANUP|END):') for lineno, line in enumerate(log_lines): @@ -733,7 +745,7 @@ 'Open the log file with the required MODE.' if self.logfile: self._close_log() - self.log = codecs.open(self.logfile, mode, encoding="latin-1") + self.log = open_logfile(self.logfile, mode) def _close_log(self): 'Close the log file.' @@ -843,14 +855,13 @@ sys.exit(1) # setup the output pipes + old_stdout = sys.stdout.fileno() if self.log: sys.stdout.flush() sys.stderr.flush() self.log.flush() - old_stdout = os.dup(sys.stdout.fileno()) - old_stderr = os.dup(sys.stderr.fileno()) - os.dup2(self.log.fileno(), sys.stdout.fileno()) - os.dup2(self.log.fileno(), sys.stderr.fileno()) + saved_stds = sys.stdout, sys.stderr + sys.stdout = sys.stderr = self.log # These have to be class-scoped for use in the progress_func() self.dots_written = 0 @@ -891,12 +902,8 @@ # restore some values if self.log: - sys.stdout.flush() - sys.stderr.flush() - os.dup2(old_stdout, sys.stdout.fileno()) - os.dup2(old_stderr, sys.stderr.fileno()) - os.close(old_stdout) - os.close(old_stderr) + self.log.flush() + sys.stdout, sys.stderr = saved_stds return failed Index: subversion/tests/cmdline/svntest/main.py =================================================================== --- subversion/tests/cmdline/svntest/main.py (revision 1877480) +++ subversion/tests/cmdline/svntest/main.py (working copy) @@ -2084,6 +2084,23 @@ record.levelshort = self._level_short[record.levelno] return logging.Formatter.format(self, record) + +class LoggingStdoutHandler(logging.StreamHandler): + """ + The handler is always writing using sys.stdout at call time rather than the + value of sys.stdout at construction time. + + Inspired by logging._StderrHandler on Python 3. + """ + + def __init__(self, level=logging.NOTSET): + logging.Handler.__init__(self, level) + + @property + def stream(self): + return sys.stdout + + def _create_parser(usage=None): """Return a parser for our test suite.""" @@ -2276,7 +2293,7 @@ datefmt='%Y-%m-%d %H:%M:%S') else: formatter = AbbreviatedFormatter('%(levelshort)s: %(message)s') - handler = logging.StreamHandler(sys.stdout) + handler = LoggingStdoutHandler() handler.setFormatter(formatter) logger.addHandler(handler)