diff --git a/CHANGELOG b/CHANGELOG index b63730885..c4120e896 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,19 @@ +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 + +* fix issue #27: better reporting on non-collectable items given on commandline + (e.g. pyc files) + +* "Test" prefixed classes with an __init__ method are *not* collected by default anymore + +* terser reporting of collection error tracebacks + +* renaming of arguments to some special rather internal hooks + Changes between 1.0.0b9 and 1.0.0 ===================================== diff --git a/doc/test/features.txt b/doc/test/features.txt index 2cfa5b384..2de4dc178 100644 --- a/doc/test/features.txt +++ b/doc/test/features.txt @@ -21,7 +21,7 @@ on CPython 2.3 - CPython 2.6. automatically collects and executes tests =============================================== -py.test discovers tests automatically by inspect specified +py.test discovers tests automatically by inspecting specified directories or files. By default, it collects all python modules a leading ``test_`` or trailing ``_test`` filename. From each test module every function with a leading ``test_`` diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 6805d8d39..2f6a904b9 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -289,6 +289,7 @@ Funcarg Tutorial Examples .. _`application setup tutorial example`: +.. _appsetup: application specific test setup --------------------------------------------------------- @@ -494,12 +495,13 @@ example: decorating a funcarg in a test module For larger scale setups it's sometimes useful to decorare a funcarg just for a particular test module. We can -extend the `accept example`_ by putting this in our test class: +extend the `accept example`_ by putting this in our test module: .. sourcecode:: python - def pytest_funcarg__accept(self, request): - arg = request.getfuncargvalue("accept") # call the next factory + def pytest_funcarg__accept(request): + # call the next factory (living in our conftest.py) + arg = request.getfuncargvalue("accept") # create a special layout in our tempdir arg.tmpdir.mkdir("special") return arg diff --git a/doc/test/plugin/links.txt b/doc/test/plugin/links.txt index df03c6610..270e90c2b 100644 --- a/doc/test/plugin/links.txt +++ b/doc/test/plugin/links.txt @@ -1,33 +1,33 @@ .. _`terminal`: terminal.html -.. _`pytest_recwarn.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_recwarn.py +.. _`pytest_recwarn.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_recwarn.py .. _`unittest`: unittest.html -.. _`pytest_monkeypatch.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_monkeypatch.py -.. _`pytest_keyword.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_keyword.py +.. _`pytest_monkeypatch.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_monkeypatch.py +.. _`pytest_keyword.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_keyword.py .. _`pastebin`: pastebin.html .. _`plugins`: index.html -.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_capture.py -.. _`pytest_doctest.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_doctest.py +.. _`pytest_capture.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_capture.py +.. _`pytest_doctest.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_doctest.py .. _`capture`: capture.html .. _`hooklog`: hooklog.html -.. _`pytest_restdoc.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_restdoc.py -.. _`pytest_hooklog.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_hooklog.py -.. _`pytest_pastebin.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_pastebin.py -.. _`pytest_figleaf.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_figleaf.py +.. _`pytest_restdoc.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_restdoc.py +.. _`pytest_hooklog.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_hooklog.py +.. _`pytest_pastebin.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_pastebin.py +.. _`pytest_figleaf.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_figleaf.py .. _`xfail`: xfail.html .. _`contact`: ../../contact.html .. _`checkout the py.test development version`: ../../download.html#checkout .. _`oejskit`: oejskit.html -.. _`pytest_xfail.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_xfail.py +.. _`pytest_xfail.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_xfail.py .. _`figleaf`: figleaf.html .. _`extend`: ../extend.html -.. _`pytest_terminal.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_terminal.py +.. _`pytest_terminal.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_terminal.py .. _`recwarn`: recwarn.html -.. _`pytest_pdb.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_pdb.py +.. _`pytest_pdb.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_pdb.py .. _`monkeypatch`: monkeypatch.html .. _`resultlog`: resultlog.html .. _`keyword`: keyword.html .. _`restdoc`: restdoc.html -.. _`pytest_unittest.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_unittest.py +.. _`pytest_unittest.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_unittest.py .. _`doctest`: doctest.html -.. _`pytest_resultlog.py`: http://bitbucket.org/hpk42/py-trunk/raw/3b3ea41060652c47739450a590c4d71625bc05bd/py/test/plugin/pytest_resultlog.py +.. _`pytest_resultlog.py`: http://bitbucket.org/hpk42/py-trunk/raw/caf81a2cdf5083be3bfb715d7e56273330a54843/py/test/plugin/pytest_resultlog.py .. _`pdb`: pdb.html diff --git a/doc/test/talks.txt b/doc/test/talks.txt index 390065957..e6f2348e2 100644 --- a/doc/test/talks.txt +++ b/doc/test/talks.txt @@ -4,7 +4,40 @@ Talks and Tutorials .. _`funcargs`: funcargs.html -a list of the latest talk and tutorial material: +tutorial examples and blog postings +--------------------------------------------- + +function arguments: + +- `application setup in test functions with funcargs`_ (doc link) +- `monkey patching done right`_ (blog post, consult `monkeypatch + plugin`_ for actual 1.0 API) + +test parametrization: + +- `generating parametrized tests with funcargs`_ (doc link) +- `parametrizing tests, generalized`_ (blog entry) +- `putting test-hooks into local or global plugins`_ (blog entry) + +distributed testing: + +- `simultanously test your code on all platforms`_ (blog entry) + +plugins: + +- usage examples are in most of the referenced `plugins`_ docs + +.. _plugins: plugin/index.html +.. _`monkeypatch plugin`: plugin/monkeypatch.html +.. _`application setup in test functions with funcargs`: funcargs.html#appsetup +.. _`simultanously test your code on all platforms`: http://tetamap.wordpress.com/2009/03/23/new-simultanously-test-your-code-on-all-platforms/ +.. _`monkey patching done right`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ +.. _`putting test-hooks into local or global plugins`: http://tetamap.wordpress.com/2009/05/14/putting-test-hooks-into-local-and-global-plugins/ +.. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ +.. _`generating parametrized tests with funcargs`: funcargs.html#test-generators + +conference talks and tutorials +---------------------------------------- - `ep2009-rapidtesting.pdf`_ tutorial slides (July 2009): diff --git a/doc/test/test.txt b/doc/test/test.txt index 58394e752..115815f93 100644 --- a/doc/test/test.txt +++ b/doc/test/test.txt @@ -13,6 +13,8 @@ quickstart_: for getting started immediately. features_: a walk through basic features and usage. +`talks, tutorials, examples`_: tutorial examples, slides + `available plugins`_: list of py.test plugins funcargs_: powerful parametrized test function setup @@ -23,10 +25,9 @@ extend_: intro to extend and customize py.test runs config_: ``conftest.py`` files and the config object -talks_: talk and tutorial slides .. _`available plugins`: plugin/index.html -.. _talks: talks.html +.. _`talks, tutorials, examples`: talks.html .. _quickstart: quickstart.html .. _features: features.html .. _funcargs: funcargs.html diff --git a/py/__init__.py b/py/__init__.py index f2a7d53d4..ecabde8b4 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -20,7 +20,7 @@ For questions please check out http://pylib.org/contact.html from initpkg import initpkg trunk = None -version = trunk or "1.0.0" +version = trunk or "1.0.1" initpkg(__name__, description = "py.test and pylib: advanced testing tool and networking lib", 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/compat/LICENSE b/py/compat/LICENSE index e58eb97f9..881562cd3 100644 --- a/py/compat/LICENSE +++ b/py/compat/LICENSE @@ -1,7 +1,7 @@ License for modules in py/compat directory ============================================================== -The "*.py" files in py/compat/ and subdirectories are are all +The "*.py" files in py/compat/ and subdirectories are all - except when otherwise stated at the beginning of the file - copyrighted by the Python Software Foundation and licensed under the Python Software License of which you can find a copy 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/collect.py b/py/test/collect.py index eb941a712..8a2e9467b 100644 --- a/py/test/collect.py +++ b/py/test/collect.py @@ -4,6 +4,7 @@ Collectors and test Items form a tree that is usually built iteratively. """ import py +pydir = py.path.local(py.__file__).dirpath() def configproperty(name): def fget(self): @@ -166,16 +167,13 @@ class Node(object): if colitem.fspath == fspath or colitem.name == basename: l.append(colitem) if not l: - msg = ("Collector %r does not provide %r colitem " - "existing colitems are: %s" % - (cur, fspath, colitems)) - raise AssertionError(msg) + raise self.config.Error("can't collect: %s" %(fspath,)) if basenames: if len(l) > 1: msg = ("Collector %r has more than one %r colitem " "existing colitems are: %s" % (cur, fspath, colitems)) - raise AssertionError(msg) + raise self.config.Error("xxx-too many test types for: %s" % (fspath, )) cur = l[0] else: if len(l) > 1: @@ -332,6 +330,15 @@ class Collector(Node): """ return self.collect_by_name(name) + def _prunetraceback(self, traceback): + if hasattr(self, 'fspath'): + path = self.fspath + ntraceback = traceback.cut(path=self.fspath) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=pydir) + traceback = ntraceback.filter() + return traceback + class FSCollector(Collector): def __init__(self, fspath, parent=None): fspath = py.path.local(fspath) 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/hookspec.py b/py/test/plugin/hookspec.py index dd2fa8676..a82f718d1 100644 --- a/py/test/plugin/hookspec.py +++ b/py/test/plugin/hookspec.py @@ -91,7 +91,7 @@ def pytest__teardown_final(session): """ called before test session finishes. """ pytest__teardown_final.firstresult = True -def pytest__teardown_final_logerror(rep): +def pytest__teardown_final_logerror(report): """ called if runtest_teardown_final failed. """ # ------------------------------------------------------------------------- @@ -108,8 +108,8 @@ def pytest_sessionfinish(session, exitstatus): # hooks for influencing reporting (invoked from pytest_terminal) # ------------------------------------------------------------------------- -def pytest_report_teststatus(rep): - """ return shortletter and verbose word. """ +def pytest_report_teststatus(report): + """ return result-category, shortletter and verbose word for reporting.""" pytest_report_teststatus.firstresult = True def pytest_terminal_summary(terminalreporter): 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..3fb32eb4b 100644 --- a/py/test/plugin/pytest_runner.py +++ b/py/test/plugin/pytest_runner.py @@ -24,7 +24,7 @@ def pytest_sessionfinish(session, exitstatus): hook = session.config.hook rep = hook.pytest__teardown_final(session=session) if rep: - hook.pytest__teardown_final_logerror(rep=rep) + hook.pytest__teardown_final_logerror(report=rep) def pytest_make_collect_report(collector): result = excinfo = None @@ -72,12 +72,12 @@ def pytest__teardown_final(session): rep = TeardownErrorReport(call.excinfo) return rep -def pytest_report_teststatus(rep): - if rep.when in ("setup", "teardown"): - if rep.failed: +def pytest_report_teststatus(report): + if report.when in ("setup", "teardown"): + if report.failed: # category, shortletter, verbose-word return "error", "E", "ERROR" - elif rep.skipped: + elif report.skipped: return "skipped", "s", "SKIPPED" else: return "", "", "" @@ -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/pytest_terminal.py b/py/test/plugin/pytest_terminal.py index d4153d34a..158204b1e 100644 --- a/py/test/plugin/pytest_terminal.py +++ b/py/test/plugin/pytest_terminal.py @@ -82,7 +82,7 @@ class TerminalReporter: self._tw.sep(sep, title, **markup) def getcategoryletterword(self, rep): - res = self.config.hook.pytest_report_teststatus(rep=rep) + res = self.config.hook.pytest_report_teststatus(report=rep) if res: return res for cat in 'skipped failed passed ???'.split(): @@ -184,8 +184,8 @@ class TerminalReporter: fspath, lineno, msg = self._getreportinfo(item) self.write_fspath_result(fspath, "") - def pytest__teardown_final_logerror(self, rep): - self.stats.setdefault("error", []).append(rep) + def pytest__teardown_final_logerror(self, report): + self.stats.setdefault("error", []).append(report) def pytest_runtest_logreport(self, report): rep = report diff --git a/py/test/plugin/pytest_xfail.py b/py/test/plugin/pytest_xfail.py index b05292ea7..be85e8721 100644 --- a/py/test/plugin/pytest_xfail.py +++ b/py/test/plugin/pytest_xfail.py @@ -35,12 +35,11 @@ def pytest_runtest_makereport(__call__, item, call): res.failed = True return res -def pytest_report_teststatus(rep): - """ return shortletter and verbose word. """ - if 'xfail' in rep.keywords: - if rep.skipped: +def pytest_report_teststatus(report): + if 'xfail' in report.keywords: + if report.skipped: return "xfailed", "x", "xfail" - elif rep.failed: + elif report.failed: return "xpassed", "P", "xpass" # called by the terminalreporter instance/plugin 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) diff --git a/py/test/plugin/test_pytest_runner_xunit.py b/py/test/plugin/test_pytest_runner_xunit.py index 4ecf08d5c..18ff73d50 100644 --- a/py/test/plugin/test_pytest_runner_xunit.py +++ b/py/test/plugin/test_pytest_runner_xunit.py @@ -125,18 +125,12 @@ def test_func_generator_setup(testdir): def test_method_setup_uses_fresh_instances(testdir): reprec = testdir.inline_runsource(""" class TestSelfState1: - def __init__(self): - self.hello = 42 + memory = [] def test_hello(self): - self.world = 23 - def test_afterhello(self): - assert not hasattr(self, 'world') - assert self.hello == 42 - class TestSelfState2: - def test_hello(self): - self.world = 10 - def test_world(self): - assert not hasattr(self, 'world') - """) - reprec.assertoutcome(passed=4, failed=0) + self.memory.append(self) + + def test_afterhello(self): + assert self != self.memory[0] + """) + reprec.assertoutcome(passed=2, failed=0) diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 25072c373..9e7bd49df 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -123,8 +123,7 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): collector=self, name=name, obj=obj) if res is not None: return res - if (self.classnamefilter(name)) and \ - py.std.inspect.isclass(obj): + if self._istestclasscandidate(name, obj): res = self._deprecated_join(name) if res is not None: return res @@ -139,14 +138,25 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector): else: return self._genfunctions(name, obj) + def _istestclasscandidate(self, name, obj): + if self.classnamefilter(name) and \ + py.std.inspect.isclass(obj): + if hasinit(obj): + # XXX WARN + return False + return True + + def _genfunctions(self, name, funcobj): module = self.getparent(Module).obj # due to _buildname2items funcobj is the raw function, we need # to work to get at the class clscol = self.getparent(Class) cls = clscol and clscol.obj or None - metafunc = funcargs.Metafunc(funcobj, config=self.config, cls=cls, module=module) - gentesthook = self.config.hook.pytest_generate_tests.clone(extralookup=module) + metafunc = funcargs.Metafunc(funcobj, config=self.config, + cls=cls, module=module) + gentesthook = self.config.hook.pytest_generate_tests.clone( + extralookup=module) gentesthook(metafunc=metafunc) if not metafunc._calls: return self.Function(name, parent=self) @@ -371,3 +381,9 @@ class Function(FunctionMixin, py.test.collect.Item): def __ne__(self, other): return not self == other + +def hasinit(obj): + init = getattr(obj, '__init__', None) + if init: + if not isinstance(init, type(object.__init__)): + return True diff --git a/py/test/testing/acceptance_test.py b/py/test/testing/acceptance_test.py index 41d0066b6..c96c65185 100644 --- a/py/test/testing/acceptance_test.py +++ b/py/test/testing/acceptance_test.py @@ -67,3 +67,12 @@ class TestGeneralUsage: "E ImportError: No module named does_not_work", ]) assert result.ret == 1 + + def test_not_collectable_arguments(self, testdir): + p1 = testdir.makepyfile("") + p2 = testdir.makefile(".pyc", "123") + result = testdir.runpytest(p1, p2) + assert result.ret != 0 + assert result.stderr.fnmatch_lines([ + "*ERROR: can't collect: %s" %(p2,) + ]) diff --git a/py/test/testing/test_collect.py b/py/test/testing/test_collect.py index a55e7ab61..a43a4ccd3 100644 --- a/py/test/testing/test_collect.py +++ b/py/test/testing/test_collect.py @@ -179,6 +179,18 @@ class TestCollectPluginHooks: names = [rep.collector.name for rep in colreports] assert names.count("hello") == 1 +class TestPrunetraceback: + def test_collection_error(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + result = testdir.runpytest(p) + assert "__import__" not in result.stdout.str(), "too long traceback" + result.stdout.fnmatch_lines([ + "*ERROR during collection*", + ">*import not_exists*" + ]) + class TestCustomConftests: def test_non_python_files(self, testdir): testdir.makepyfile(conftest=""" diff --git a/py/test/testing/test_pycollect.py b/py/test/testing/test_pycollect.py index e2dc190a8..dcec63e98 100644 --- a/py/test/testing/test_pycollect.py +++ b/py/test/testing/test_pycollect.py @@ -39,6 +39,19 @@ class TestModule: modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") py.test.raises(ImportError, "modcol.obj") +class TestClass: + def test_class_with_init_not_collected(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass1: + def __init__(self): + pass + class TestClass2(object): + def __init__(self): + pass + """) + l = modcol.collect() + assert len(l) == 0 + class TestDisabled: def test_disabled_module(self, testdir): modcol = testdir.getmodulecol(""" diff --git a/setup.py b/setup.py index a1992123c..612037db7 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def main(): name='py', description='py.test and pylib: advanced testing tool and networking lib', long_description = long_description, - version= trunk or '1.0.0', + version= trunk or '1.0.1', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],