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)
 

Reply via email to