From 8fcdac9dd65d72511ccc37ec3792da4d6e82adf3 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 6 Aug 2009 14:34:19 +0200 Subject: [PATCH] * fix capturing and unicode printing in tests * introduce "_encoding" to py/io/terminalwriter writing * beautify a few __repr__ for better internal debugging --HG-- branch : 1.0.x --- CHANGELOG | 7 ++++ py/_com.py | 10 +++++ py/io/stdcapture.py | 4 +- py/io/terminalwriter.py | 58 +++++++-------------------- py/io/testing/test_terminalwriter.py | 20 +++++++++ py/misc/testing/test_com.py | 12 ++++++ py/test/dist/txnode.py | 6 +-- py/test/plugin/pytest_capture.py | 27 ++++++++++++- py/test/plugin/pytest_runner.py | 7 ++++ py/test/plugin/test_pytest_capture.py | 25 +++++++++++- py/test/plugin/test_pytest_runner.py | 10 +++++ 11 files changed, 133 insertions(+), 53 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b63730885..f02a85884 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +Changes between 1.0.0 and 1.0.1 +===================================== + +* various unicode fixes: capturing and prints of unicode strings now + work within tests, they are encoded as "utf8" by default, terminalwriting + was adapted and somewhat unified between windows and linux + Changes between 1.0.0b9 and 1.0.0 ===================================== diff --git a/py/_com.py b/py/_com.py index 432ee6d00..6ba7b47fb 100644 --- a/py/_com.py +++ b/py/_com.py @@ -17,6 +17,16 @@ class MultiCall: self.kwargs = kwargs self.results = [] + def __repr__(self): + args = [] + if self.args: + args.append("posargs=%r" %(self.args,)) + kw = self.kwargs + args.append(", ".join(["%s=%r" % x for x in self.kwargs.items()])) + args = " ".join(args) + status = "results: %r, rmethods: %r" % (self.results, self.methods) + return "" %(args, status) + def execute(self, firstresult=False): while self.methods: currentmethod = self.methods.pop() diff --git a/py/io/stdcapture.py b/py/io/stdcapture.py index f5f60fe82..776798df4 100644 --- a/py/io/stdcapture.py +++ b/py/io/stdcapture.py @@ -76,7 +76,7 @@ class StdCaptureFD(Capture): os.close(fd) if out: tmpfile = None - if isinstance(out, file): + if hasattr(out, 'write'): tmpfile = out self.out = py.io.FDCapture(1, tmpfile=tmpfile) if patchsys: @@ -84,7 +84,7 @@ class StdCaptureFD(Capture): if err: if mixed and out: tmpfile = self.out.tmpfile - elif isinstance(err, file): + elif hasattr(err, 'write'): tmpfile = err else: tmpfile = None diff --git a/py/io/terminalwriter.py b/py/io/terminalwriter.py index 85390e7c2..1e20bc30c 100644 --- a/py/io/terminalwriter.py +++ b/py/io/terminalwriter.py @@ -139,6 +139,7 @@ class TerminalWriter(object): Black=40, Red=41, Green=42, Yellow=43, Blue=44, Purple=45, Cyan=46, White=47, bold=1, light=2, blink=5, invert=7) + _encoding = "utf-8" def __init__(self, file=None, stringio=False): if file is None: @@ -194,58 +195,27 @@ class TerminalWriter(object): def write(self, s, **kw): if s: - s = str(s) + s = self._getbytestring(s) if self.hasmarkup and kw: s = self.markup(s, **kw) self._file.write(s) - self._file.flush() + self._file.flush() + + def _getbytestring(self, s): + if isinstance(s, unicode): + return s.encode(self._encoding) + elif not isinstance(s, str): + return str(s) + return s def line(self, s='', **kw): self.write(s, **kw) self.write('\n') -class Win32ConsoleWriter(object): - - def __init__(self, file=None, stringio=False): - if file is None: - if stringio: - self.stringio = file = py.std.cStringIO.StringIO() - else: - file = py.std.sys.stdout - elif callable(file): - file = WriteFile(file) - self._file = file - self.fullwidth = get_terminal_width() - self.hasmarkup = should_do_markup(file) - - def sep(self, sepchar, title=None, fullwidth=None, **kw): - if fullwidth is None: - fullwidth = self.fullwidth - # the goal is to have the line be as long as possible - # under the condition that len(line) <= fullwidth - if title is not None: - # we want 2 + 2*len(fill) + len(title) <= fullwidth - # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth - # 2*len(sepchar)*N <= fullwidth - len(title) - 2 - # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) - N = (fullwidth - len(title) - 2) // (2*len(sepchar)) - fill = sepchar * N - line = "%s %s %s" % (fill, title, fill) - else: - # we want len(sepchar)*N <= fullwidth - # i.e. N <= fullwidth // len(sepchar) - line = sepchar * (fullwidth // len(sepchar)) - # in some situations there is room for an extra sepchar at the right, - # in particular if we consider that with a sepchar like "_ " the - # trailing space is not important at the end of the line - if len(line) + len(sepchar.rstrip()) <= fullwidth: - line += sepchar.rstrip() - - self.line(line, **kw) - +class Win32ConsoleWriter(TerminalWriter): def write(self, s, **kw): if s: - s = str(s) + s = self._getbytestring(s) if self.hasmarkup: handle = GetStdHandle(STD_OUTPUT_HANDLE) @@ -269,8 +239,8 @@ class Win32ConsoleWriter(object): if self.hasmarkup: SetConsoleTextAttribute(handle, FOREGROUND_WHITE) - def line(self, s='', **kw): - self.write(s + '\n', **kw) + def line(self, s="", **kw): + self.write(s+"\n", **kw) if sys.platform == 'win32': TerminalWriter = Win32ConsoleWriter diff --git a/py/io/testing/test_terminalwriter.py b/py/io/testing/test_terminalwriter.py index 4bd796ab6..2eefd35f3 100644 --- a/py/io/testing/test_terminalwriter.py +++ b/py/io/testing/test_terminalwriter.py @@ -37,6 +37,16 @@ class BaseTests: assert len(l) == 1 assert l[0] == "hello\n" + def test_line_unicode(self): + tw = self.getwriter() + for encoding in 'utf8', 'latin1': + tw._encoding = encoding + msg = unicode('b\u00f6y', 'utf8') + tw.line(msg) + l = self.getlines() + assert not isinstance(l[0], unicode) + assert unicode(l[0], encoding) == msg + "\n" + def test_sep_no_title(self): tw = self.getwriter() tw.sep("-", fullwidth=60) @@ -85,6 +95,16 @@ class BaseTests: l = self.getlines() assert len(l[0]) == len(l[1]) +class TestTmpfile(BaseTests): + def getwriter(self): + self.path = py.test.config.ensuretemp("terminalwriter").ensure("tmpfile") + self.tw = py.io.TerminalWriter(self.path.open('w+')) + return self.tw + def getlines(self): + io = self.tw._file + io.flush() + return self.path.open('r').readlines() + class TestStringIO(BaseTests): def getwriter(self): self.tw = py.io.TerminalWriter(stringio=True) diff --git a/py/misc/testing/test_com.py b/py/misc/testing/test_com.py index 94b6e635a..5734420fe 100644 --- a/py/misc/testing/test_com.py +++ b/py/misc/testing/test_com.py @@ -10,6 +10,7 @@ class TestMultiCall: def test_uses_copy_of_methods(self): l = [lambda: 42] mc = MultiCall(l) + repr(mc) l[:] = [] res = mc.execute() return res == 42 @@ -33,16 +34,27 @@ class TestMultiCall: p1 = P1() p2 = P2() multicall = MultiCall([p1.m, p2.m], 23) + assert "23" in repr(multicall) reslist = multicall.execute() assert len(reslist) == 2 # ensure reversed order assert reslist == [23, 17] + def test_keyword_args(self): + def f(x): + return x + 1 + multicall = MultiCall([f], x=23) + assert "x=23" in repr(multicall) + reslist = multicall.execute() + assert reslist == [24] + assert "24" in repr(multicall) + def test_optionalcallarg(self): class P1: def m(self, x): return x call = MultiCall([P1().m], 23) + assert "23" in repr(call) assert call.execute() == [23] assert call.execute(firstresult=True) == 23 diff --git a/py/test/dist/txnode.py b/py/test/dist/txnode.py index ef42ac49f..b76f429b3 100644 --- a/py/test/dist/txnode.py +++ b/py/test/dist/txnode.py @@ -145,11 +145,9 @@ class SlaveNode(object): if call.excinfo: # likely it is not collectable here because of # platform/import-dependency induced skips - # XXX somewhat ugly shortcuts - also makes a collection - # failure into an ItemTestReport - this might confuse - # pytest_runtest_logreport hooks + # we fake a setup-error report with the obtained exception + # and do not care about capturing or non-runner hooks rep = self.runner.pytest_runtest_makereport(item=item, call=call) self.pytest_runtest_logreport(rep) return item.config.hook.pytest_runtest_protocol(item=item) - diff --git a/py/test/plugin/pytest_capture.py b/py/test/plugin/pytest_capture.py index a59075a9b..3acb8a5a3 100644 --- a/py/test/plugin/pytest_capture.py +++ b/py/test/plugin/pytest_capture.py @@ -107,11 +107,24 @@ class CaptureManager: def __init__(self): self._method2capture = {} + def _maketempfile(self): + f = py.std.tempfile.TemporaryFile() + newf = py.io.dupfile(f) + f.close() + return ustream(newf) + + def _makestringio(self): + return py.std.StringIO.StringIO() + def _startcapture(self, method): if method == "fd": - return py.io.StdCaptureFD() + return py.io.StdCaptureFD( + out=self._maketempfile(), err=self._maketempfile() + ) elif method == "sys": - return py.io.StdCapture() + return py.io.StdCapture( + out=self._makestringio(), err=self._makestringio() + ) else: raise ValueError("unknown capturing method: %r" % method) @@ -252,3 +265,13 @@ class CaptureFuncarg: def close(self): self.capture.reset() del self.capture + +def ustream(f): + import codecs + encoding = getattr(f, 'encoding', None) or "UTF-8" + reader = codecs.getreader(encoding) + writer = codecs.getwriter(encoding) + srw = codecs.StreamReaderWriter(f, reader, writer) + srw.encoding = encoding + return srw + diff --git a/py/test/plugin/pytest_runner.py b/py/test/plugin/pytest_runner.py index 571a896ef..a5ac4d362 100644 --- a/py/test/plugin/pytest_runner.py +++ b/py/test/plugin/pytest_runner.py @@ -108,6 +108,13 @@ class CallInfo: except: self.excinfo = py.code.ExceptionInfo() + def __repr__(self): + if self.excinfo: + status = "exception: %s" % str(self.excinfo.value) + else: + status = "result: %r" % (self.result,) + return "" % (self.when, status) + def forked_run_report(item): # for now, we run setup/teardown in the subprocess # XXX optionally allow sharing of setup/teardown diff --git a/py/test/plugin/test_pytest_capture.py b/py/test/plugin/test_pytest_capture.py index cc81b2664..8980e6d39 100644 --- a/py/test/plugin/test_pytest_capture.py +++ b/py/test/plugin/test_pytest_capture.py @@ -1,5 +1,5 @@ import py, os, sys -from py.__.test.plugin.pytest_capture import CaptureManager +from py.__.test.plugin.pytest_capture import CaptureManager, ustream class TestCaptureManager: @@ -54,6 +54,29 @@ class TestCaptureManager: finally: capouter.reset() +@py.test.mark.multi(method=['fd', 'sys']) +def test_capturing_unicode(testdir, method): + testdir.makepyfile(""" + # taken from issue 227 from nosests + def test_unicode(): + import sys + print sys.stdout + print u'b\\u00f6y' + """) + result = testdir.runpytest("--capture=%s" % method) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + +def test_ustream_helper(testdir): + p = testdir.makepyfile("hello") + f = p.open('w') + #f.encoding = "utf8" + x = ustream(f) + x.write(u'b\\00f6y') + x.close() + + def test_collect_capturing(testdir): p = testdir.makepyfile(""" print "collect %s failure" % 13 diff --git a/py/test/plugin/test_pytest_runner.py b/py/test/plugin/test_pytest_runner.py index f4fcc239b..e039d7e19 100644 --- a/py/test/plugin/test_pytest_runner.py +++ b/py/test/plugin/test_pytest_runner.py @@ -271,3 +271,13 @@ def test_functional_boxed(testdir): "*1 failed*" ]) +def test_callinfo(): + ci = runner.CallInfo(lambda: 0, '123') + assert ci.when == "123" + assert ci.result == 0 + assert "result" in repr(ci) + ci = runner.CallInfo(lambda: 0/0, '123') + assert ci.when == "123" + assert not hasattr(ci, 'result') + assert ci.excinfo + assert "exc" in repr(ci)