Unbuffer so that output from packages appears when redirecting

- Python I/O would not properly interleave (or appear) with output from
  subcommands.

- Add a flusing wrapper around sys.stdout and sys.stderr when
  redirecting, so that Python output is synchronous with that of
  subcommands.
This commit is contained in:
Todd Gamblin
2017-08-20 03:29:54 -07:00
parent 79045afada
commit 10bb681b57
3 changed files with 110 additions and 2 deletions

View File

@@ -124,6 +124,26 @@ def __exit__(self, exc_type, exception, traceback):
self.stream.fileno(), termios.TCSADRAIN, self.old_cfg)
class Unbuffered(object):
"""Wrapper for Python streams that forces them to be unbuffered.
This is implemented by forcing a flush after each write.
"""
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def writelines(self, datas):
self.stream.writelines(datas)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
def _file_descriptors_work():
"""Whether we can get file descriptors for stdout and stderr.
@@ -184,18 +204,32 @@ class log_output(object):
work within test frameworks like nose and pytest.
"""
def __init__(self, filename=None, echo=False, debug=False):
def __init__(self, filename=None, echo=False, debug=False, buffer=False):
"""Create a new output log context manager.
Args:
filename (str): name of file where output should be logged
echo (bool): whether to echo output in addition to logging it
debug (bool): whether to enable tty debug mode during logging
buffer (bool): pass buffer=True to skip unbuffering output; note
this doesn't set up any *new* buffering
By default, we unbuffer sys.stdout and sys.stderr because the
logger will include output from executed programs and from python
calls. If stdout and stderr are buffered, their output won't be
printed in the right place w.r.t. output from commands.
Logger daemon is not started until ``__enter__()``.
"""
self.filename = filename
self.echo = echo
self.debug = debug
self.buffer = buffer
self._active = False # used to prevent re-entry
def __call__(self, filename=None, echo=None, debug=None):
def __call__(self, filename=None, echo=None, debug=None, buffer=None):
"""Thie behaves the same as init. It allows a logger to be reused.
With the ``__call__`` function, you can save state between uses
@@ -217,6 +251,8 @@ def __call__(self, filename=None, echo=None, debug=None):
self.echo = echo
if debug is not None:
self.debug = debug
if buffer is not None:
self.buffer = buffer
return self
def __enter__(self):
@@ -297,6 +333,11 @@ def __enter__(self):
sys.stdout = pipe_fd_out
sys.stderr = pipe_fd_out
# Unbuffer stdout and stderr at the Python level
if not self.buffer:
sys.stdout = Unbuffered(sys.stdout)
sys.stderr = Unbuffered(sys.stderr)
# Force color and debug settings now that we have redirected.
tty.color.set_color_when(forced_color)
tty._debug = self.debug
@@ -399,6 +440,7 @@ def _writer_daemon(self, stdin):
# Echo to stdout if requested or forced
if echo or force_echo:
sys.stdout.write(line)
sys.stdout.flush()
# Stripped output to log file.
log_file.write(_strip(line))

View File

@@ -23,10 +23,12 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##############################################################################
import argparse
import os
import pytest
import spack.cmd.install
from spack.spec import Spec
from spack.main import SpackCommand
install = SpackCommand('install')
@@ -86,3 +88,22 @@ def test_install_package_already_installed(
def test_install_dirty_flag(parser, arguments, expected):
args = parser.parse_args(arguments)
assert args.dirty == expected
def test_package_output(tmpdir, capsys, install_mockery, mock_fetch):
"""Ensure output printed from pkgs is captured by output redirection."""
# we can't use output capture here because it interferes with Spack's
# logging. TODO: see whether we can get multiple log_outputs to work
# when nested AND in pytest
spec = Spec('printing-package').concretized()
pkg = spec.package
pkg.do_install(verbose=True)
log_file = os.path.join(spec.prefix, '.spack', 'build.out')
with open(log_file) as f:
out = f.read()
# make sure that output from the actual package file appears in the
# right place in the build log.
assert "BEFORE INSTALL\n==> './configure'" in out
assert "'install'\nAFTER INSTALL" in out