1 new commit in pytest: https://bitbucket.org/pytest-dev/pytest/commits/7d4a0b78d19b/ Changeset: 7d4a0b78d19b User: hpk42 Date: 2015-04-29 14:32:28+00:00 Summary: Merged in hpk42/pytest-patches/testrefactor (pull request #284)
majorly refactor pytester and speed/streamline tests Affected #: 25 files diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,14 @@ implementations. Use the ``hookwrapper`` mechanism instead already introduced with pytest-2.7. +- speed up pytest's own test suite considerably by using inprocess + tests by default (testrun can be modified with --runpytest=subprocess + to create subprocesses in many places instead). The main + APIs to run pytest in a test is "runpytest()" or "runpytest_subprocess" + and "runpytest_inprocess" if you need a particular way of running + the test. In all cases you get back a RunResult but the inprocess + one will also have a "reprec" attribute with the recorded events/reports. + 2.7.1.dev (compared to 2.7.0) ----------------------------- diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -29,17 +29,24 @@ initialization. """ try: - config = _prepareconfig(args, plugins) - except ConftestImportFailure: - e = sys.exc_info()[1] - tw = py.io.TerminalWriter(sys.stderr) - for line in traceback.format_exception(*e.excinfo): - tw.line(line.rstrip(), red=True) - tw.line("ERROR: could not load %s\n" % (e.path), red=True) + try: + config = _prepareconfig(args, plugins) + except ConftestImportFailure as e: + tw = py.io.TerminalWriter(sys.stderr) + for line in traceback.format_exception(*e.excinfo): + tw.line(line.rstrip(), red=True) + tw.line("ERROR: could not load %s\n" % (e.path), red=True) + return 4 + else: + try: + config.pluginmanager.check_pending() + return config.hook.pytest_cmdline_main(config=config) + finally: + config._ensure_unconfigure() + except UsageError as e: + for msg in e.args: + sys.stderr.write("ERROR: %s\n" %(msg,)) return 4 - else: - config.pluginmanager.check_pending() - return config.hook.pytest_cmdline_main(config=config) class cmdline: # compatibility namespace main = staticmethod(main) @@ -81,12 +88,18 @@ if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = shlex.split(args) - pluginmanager = get_config().pluginmanager - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) + config = get_config() + pluginmanager = config.pluginmanager + try: + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + except BaseException: + config._ensure_unconfigure() + raise + def exclude_pytest_names(name): return not name.startswith(name) or name == "pytest_plugins" or \ @@ -259,7 +272,10 @@ def consider_pluginarg(self, arg): if arg.startswith("no:"): - self.set_blocked(arg[3:]) + name = arg[3:] + self.set_blocked(name) + if not name.startswith("pytest_"): + self.set_blocked("pytest_" + name) else: self.import_plugin(arg) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -83,10 +83,7 @@ initstate = 2 doit(config, session) except pytest.UsageError: - args = sys.exc_info()[1].args - for msg in args: - sys.stderr.write("ERROR: %s\n" %(msg,)) - session.exitstatus = EXIT_USAGEERROR + raise except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() config.hook.pytest_keyboard_interrupt(excinfo=excinfo) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1,5 +1,7 @@ """ (disabled by default) support for testing pytest and pytest plugins. """ +import gc import sys +import traceback import os import codecs import re @@ -15,6 +17,136 @@ from _pytest.main import Session, EXIT_OK + +def pytest_addoption(parser): + # group = parser.getgroup("pytester", "pytester (self-tests) options") + parser.addoption('--lsof', + action="store_true", dest="lsof", default=False, + help=("run FD checks if lsof is available")) + + parser.addoption('--runpytest', default="inprocess", dest="runpytest", + choices=("inprocess", "subprocess", ), + help=("run pytest sub runs in tests using an 'inprocess' " + "or 'subprocess' (python -m main) method")) + + +def pytest_configure(config): + # This might be called multiple times. Only take the first. + global _pytest_fullpath + try: + _pytest_fullpath + except NameError: + _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) + _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") + + if config.getvalue("lsof"): + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) + + +class LsofFdLeakChecker(object): + def get_open_files(self): + out = self._exec_lsof() + open_files = self._parse_lsof_output(out) + return open_files + + def _exec_lsof(self): + pid = os.getpid() + return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) + + def _parse_lsof_output(self, out): + def isopen(line): + return line.startswith('f') and ("deleted" not in line and + 'mem' not in line and "txt" not in line and 'cwd' not in line) + + open_files = [] + + for line in out.split("\n"): + if isopen(line): + fields = line.split('\0') + fd = fields[0][1:] + filename = fields[1][1:] + if filename.startswith('/'): + open_files.append((fd, filename)) + + return open_files + + def matching_platform(self): + try: + py.process.cmdexec("lsof -v") + except py.process.cmdexec.Error: + return False + else: + return True + + @pytest.hookimpl_opts(hookwrapper=True, tryfirst=True) + def pytest_runtest_item(self, item): + lines1 = self.get_open_files() + yield + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = set([t[0] for t in lines2]) - set([t[0] for t in lines1]) + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [] + error.append("***** %s FD leakage detected" % len(leaked_files)) + error.extend([str(f) for f in leaked_files]) + error.append("*** Before:") + error.extend([str(f) for f in lines1]) + error.append("*** After:") + error.extend([str(f) for f in lines2]) + error.append(error[0]) + error.append("*** function %s:%s: %s " % item.location) + pytest.fail("\n".join(error), pytrace=False) + + +# XXX copied from execnet's conftest.py - needs to be merged +winpymap = { + 'python2.7': r'C:\Python27\python.exe', + 'python2.6': r'C:\Python26\python.exe', + 'python3.1': r'C:\Python31\python.exe', + 'python3.2': r'C:\Python32\python.exe', + 'python3.3': r'C:\Python33\python.exe', + 'python3.4': r'C:\Python34\python.exe', + 'python3.5': r'C:\Python35\python.exe', +} + +def getexecutable(name, cache={}): + try: + return cache[name] + except KeyError: + executable = py.path.local.sysfind(name) + if executable: + if name == "jython": + import subprocess + popen = subprocess.Popen([str(executable), "--version"], + universal_newlines=True, stderr=subprocess.PIPE) + out, err = popen.communicate() + if not err or "2.5" not in err: + executable = None + if "2.5.2" in err: + executable = None # http://bugs.jython.org/issue1790 + cache[name] = executable + return executable + +@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", + 'pypy', 'pypy3']) +def anypython(request): + name = request.param + executable = getexecutable(name) + if executable is None: + if sys.platform == "win32": + executable = winpymap.get(name, None) + if executable: + executable = py.path.local(executable) + if executable.check(): + return executable + pytest.skip("no suitable %s found" % (name,)) + return executable + # used at least by pytest-xdist plugin @pytest.fixture def _pytest(request): @@ -39,23 +171,6 @@ return [x for x in l if x[0] != "_"] -def pytest_addoption(parser): - group = parser.getgroup("pylib") - group.addoption('--no-tools-on-path', - action="store_true", dest="notoolsonpath", default=False, - help=("discover tools on PATH instead of going through py.cmdline.") - ) - -def pytest_configure(config): - # This might be called multiple times. Only take the first. - global _pytest_fullpath - try: - _pytest_fullpath - except NameError: - _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) - _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") - - class ParsedCall: def __init__(self, name, kwargs): self.__dict__.update(kwargs) @@ -201,9 +316,11 @@ return LineMatcher def pytest_funcarg__testdir(request): - tmptestdir = TmpTestdir(request) + tmptestdir = Testdir(request) return tmptestdir + + rex_outcome = re.compile("(\d+) (\w+)") class RunResult: """The result of running a command. @@ -213,10 +330,10 @@ :ret: The return value. :outlines: List of lines captured from stdout. :errlines: List of lines captures from stderr. - :stdout: LineMatcher of stdout, use ``stdout.str()`` to + :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to reconstruct stdout or the commonly used ``stdout.fnmatch_lines()`` method. - :stderrr: LineMatcher of stderr. + :stderrr: :py:class:`LineMatcher` of stderr. :duration: Duration in seconds. """ @@ -229,6 +346,8 @@ self.duration = duration def parseoutcomes(self): + """ Return a dictionary of outcomestring->num from parsing + the terminal output that the test process produced.""" for line in reversed(self.outlines): if 'seconds' in line: outcomes = rex_outcome.findall(line) @@ -238,14 +357,17 @@ d[cat] = int(num) return d - def assertoutcome(self, passed=0, skipped=0, failed=0): + def assert_outcomes(self, passed=0, skipped=0, failed=0): + """ assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" d = self.parseoutcomes() assert passed == d.get("passed", 0) assert skipped == d.get("skipped", 0) assert failed == d.get("failed", 0) -class TmpTestdir: + +class Testdir: """Temporary test directory with tools to test/run py.test itself. This is based on the ``tmpdir`` fixture but provides a number of @@ -268,7 +390,6 @@ def __init__(self, request): self.request = request - self.Config = request.config.__class__ # XXX remove duplication with tmpdir plugin basetmp = request.config._tmpdirhandler.ensuretemp("testdir") name = request.function.__name__ @@ -280,12 +401,18 @@ break self.tmpdir = tmpdir self.plugins = [] - self._savesyspath = list(sys.path) + self._savesyspath = (list(sys.path), list(sys.meta_path)) + self._savemodulekeys = set(sys.modules) self.chdir() # always chdir self.request.addfinalizer(self.finalize) + method = self.request.config.getoption("--runpytest") + if method == "inprocess": + self._runpytest_method = self.runpytest_inprocess + elif method == "subprocess": + self._runpytest_method = self.runpytest_subprocess def __repr__(self): - return "<TmpTestdir %r>" % (self.tmpdir,) + return "<Testdir %r>" % (self.tmpdir,) def finalize(self): """Clean up global state artifacts. @@ -296,23 +423,22 @@ has finished. """ - sys.path[:] = self._savesyspath + sys.path[:], sys.meta_path[:] = self._savesyspath if hasattr(self, '_olddir'): self._olddir.chdir() self.delete_loaded_modules() def delete_loaded_modules(self): - """Delete modules that have been loaded from tmpdir. + """Delete modules that have been loaded during a test. This allows the interpreter to catch module changes in case the module is re-imported. - """ - for name, mod in list(sys.modules.items()): - if mod: - fn = getattr(mod, '__file__', None) - if fn and fn.startswith(str(self.tmpdir)): - del sys.modules[name] + for name in set(sys.modules).difference(self._savemodulekeys): + # it seems zope.interfaces is keeping some state + # (used by twisted related tests) + if name != "zope.interface": + del sys.modules[name] def make_hook_recorder(self, pluginmanager): """Create a new :py:class:`HookRecorder` for a PluginManager.""" @@ -503,43 +629,19 @@ l = list(cmdlineargs) + [p] return self.inline_run(*l) - def inline_runsource1(self, *args): - """Run a test module in process using ``pytest.main()``. - - This behaves exactly like :py:meth:`inline_runsource` and - takes identical arguments. However the return value is a list - of the reports created by the pytest_runtest_logreport hook - during the run. - - """ - args = list(args) - source = args.pop() - p = self.makepyfile(source) - l = list(args) + [p] - reprec = self.inline_run(*l) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 3, reports # setup/call/teardown - return reports[1] - def inline_genitems(self, *args): """Run ``pytest.main(['--collectonly'])`` in-process. Retuns a tuple of the collected items and a :py:class:`HookRecorder` instance. - """ - return self.inprocess_run(list(args) + ['--collectonly']) - - def inprocess_run(self, args, plugins=()): - """Run ``pytest.main()`` in-process, return Items and a HookRecorder. - This runs the :py:func:`pytest.main` function to run all of py.test inside the test process itself like :py:meth:`inline_run`. However the return value is a tuple of the collection items and a :py:class:`HookRecorder` instance. """ - rec = self.inline_run(*args, plugins=plugins) + rec = self.inline_run("--collect-only", *args) items = [x.item for x in rec.getcalls("pytest_itemcollected")] return items, rec @@ -568,12 +670,50 @@ plugins = kwargs.get("plugins") or [] plugins.append(Collect()) ret = pytest.main(list(args), plugins=plugins) - assert len(rec) == 1 - reprec = rec[0] + self.delete_loaded_modules() + if len(rec) == 1: + reprec = rec.pop() + else: + class reprec: + pass reprec.ret = ret - self.delete_loaded_modules() return reprec + def runpytest_inprocess(self, *args, **kwargs): + """ Return result of running pytest in-process, providing a similar + interface to what self.runpytest() provides. """ + if kwargs.get("syspathinsert"): + self.syspathinsert() + now = time.time() + capture = py.io.StdCapture() + try: + try: + reprec = self.inline_run(*args) + except SystemExit as e: + class reprec: + ret = e.args[0] + except Exception: + traceback.print_exc() + class reprec: + ret = 3 + finally: + out, err = capture.reset() + sys.stdout.write(out) + sys.stderr.write(err) + + res = RunResult(reprec.ret, + out.split("\n"), err.split("\n"), + time.time()-now) + res.reprec = reprec + return res + + def runpytest(self, *args, **kwargs): + """ Run pytest inline or in a subprocess, depending on the command line + option "--runpytest" and return a :py:class:`RunResult`. + + """ + return self._runpytest_method(*args, **kwargs) + def parseconfig(self, *args): """Return a new py.test Config instance from given commandline args. @@ -745,57 +885,23 @@ except UnicodeEncodeError: print("couldn't print to %s because of encoding" % (fp,)) - def runpybin(self, scriptname, *args): - """Run a py.* tool with arguments. + def _getpytestargs(self): + # we cannot use "(sys.executable,script)" + # because on windows the script is e.g. a py.test.exe + return (sys.executable, _pytest_fullpath,) # noqa - This can realy only be used to run py.test, you probably want - :py:meth:`runpytest` instead. + def runpython(self, script): + """Run a python script using sys.executable as interpreter. Returns a :py:class:`RunResult`. - """ - fullargs = self._getpybinargs(scriptname) + args - return self.run(*fullargs) - - def _getpybinargs(self, scriptname): - if not self.request.config.getvalue("notoolsonpath"): - # XXX we rely on script referring to the correct environment - # we cannot use "(sys.executable,script)" - # because on windows the script is e.g. a py.test.exe - return (sys.executable, _pytest_fullpath,) # noqa - else: - pytest.skip("cannot run %r with --no-tools-on-path" % scriptname) - - def runpython(self, script, prepend=True): - """Run a python script. - - If ``prepend`` is True then the directory from which the py - package has been imported will be prepended to sys.path. - - Returns a :py:class:`RunResult`. - - """ - # XXX The prepend feature is probably not very useful since the - # split of py and pytest. - if prepend: - s = self._getsysprepend() - if s: - script.write(s + "\n" + script.read()) return self.run(sys.executable, script) - def _getsysprepend(self): - if self.request.config.getvalue("notoolsonpath"): - s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) - else: - s = "" - return s - def runpython_c(self, command): """Run python -c "command", return a :py:class:`RunResult`.""" - command = self._getsysprepend() + command return self.run(sys.executable, "-c", command) - def runpytest(self, *args): + def runpytest_subprocess(self, *args, **kwargs): """Run py.test as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will added @@ -820,7 +926,8 @@ plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ('-p', plugins[0]) + args - return self.runpybin("py.test", *args) + args = self._getpytestargs() + args + return self.run(*args) def spawn_pytest(self, string, expect_timeout=10.0): """Run py.test using pexpect. @@ -831,10 +938,8 @@ The pexpect child is returned. """ - if self.request.config.getvalue("notoolsonpath"): - pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests") basetemp = self.tmpdir.mkdir("pexpect") - invoke = " ".join(map(str, self._getpybinargs("py.test"))) + invoke = " ".join(map(str, self._getpytestargs())) cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) return self.spawn(cmd, expect_timeout=expect_timeout) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c doc/en/example/assertion/test_failures.py --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -7,7 +7,7 @@ target = testdir.tmpdir.join(failure_demo.basename) failure_demo.copy(target) failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) - result = testdir.runpytest(target) + result = testdir.runpytest(target, syspathinsert=True) result.stdout.fnmatch_lines([ "*42 failed*" ]) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c doc/en/writing_plugins.txt --- a/doc/en/writing_plugins.txt +++ b/doc/en/writing_plugins.txt @@ -186,12 +186,44 @@ If you want to look at the names of existing plugins, use the ``--traceconfig`` option. +Testing plugins +--------------- + +pytest comes with some facilities that you can enable for testing your +plugin. Given that you have an installed plugin you can enable the +:py:class:`testdir <_pytest.pytester.Testdir>` fixture via specifying a +command line option to include the pytester plugin (``-p pytester``) or +by putting ``pytest_plugins = pytester`` into your test or +``conftest.py`` file. You then will have a ``testdir`` fixure which you +can use like this:: + + # content of test_myplugin.py + + pytest_plugins = pytester # to get testdir fixture + + def test_myplugin(testdir): + testdir.makepyfile(""" + def test_example(): + pass + """) + result = testdir.runpytest("--verbose") + result.fnmatch_lines(""" + test_example* + """) + +Note that by default ``testdir.runpytest()`` will perform a pytest +in-process. You can pass the command line option ``--runpytest=subprocess`` +to have it happen in a subprocess. + +Also see the :py:class:`RunResult <_pytest.pytester.RunResult>` for more +methods of the result object that you get from a call to ``runpytest``. .. _`writinghooks`: Writing hook functions ====================== + .. _validation: hook function validation and execution @@ -493,3 +525,13 @@ .. autoclass:: _pytest.core.CallOutcome() :members: +.. currentmodule:: _pytest.pytester + +.. autoclass:: Testdir() + :members: runpytest,runpytest_subprocess,runpytest_inprocess,makeconftest,makepyfile + +.. autoclass:: RunResult() + :members: + +.. autoclass:: LineMatcher() + :members: diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/acceptance_test.py --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -82,7 +82,7 @@ def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) - result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123") + result = testdir.runpytest("-p", "pytest_xyz", "--xyz=123", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ '*1 passed*', @@ -203,7 +203,7 @@ os.chdir(os.path.dirname(os.getcwd())) print (py.log) """)) - result = testdir.runpython(p, prepend=False) + result = testdir.runpython(p) assert not result.ret def test_issue109_sibling_conftests_not_loaded(self, testdir): @@ -353,7 +353,8 @@ *unrecognized* """) - def test_getsourcelines_error_issue553(self, testdir): + def test_getsourcelines_error_issue553(self, testdir, monkeypatch): + monkeypatch.setattr("inspect.getsourcelines", None) p = testdir.makepyfile(""" def raise_error(obj): raise IOError('source code not available') diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/conftest.py --- a/testing/conftest.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -import sys - -pytest_plugins = "pytester", - -import os, py - -class LsofFdLeakChecker(object): - def get_open_files(self): - out = self._exec_lsof() - open_files = self._parse_lsof_output(out) - return open_files - - def _exec_lsof(self): - pid = os.getpid() - return py.process.cmdexec("lsof -Ffn0 -p %d" % pid) - - def _parse_lsof_output(self, out): - def isopen(line): - return line.startswith('f') and ( - "deleted" not in line and 'mem' not in line and "txt" not in line and 'cwd' not in line) - - open_files = [] - - for line in out.split("\n"): - if isopen(line): - fields = line.split('\0') - fd = fields[0][1:] - filename = fields[1][1:] - if filename.startswith('/'): - open_files.append((fd, filename)) - - return open_files - - -def pytest_addoption(parser): - parser.addoption('--lsof', - action="store_true", dest="lsof", default=False, - help=("run FD checks if lsof is available")) - -def pytest_runtest_setup(item): - config = item.config - config._basedir = py.path.local() - if config.getvalue("lsof"): - try: - config._fd_leak_checker = LsofFdLeakChecker() - config._openfiles = config._fd_leak_checker.get_open_files() - except py.process.cmdexec.Error: - pass - -#def pytest_report_header(): -# return "pid: %s" % os.getpid() - -def check_open_files(config): - lines2 = config._fd_leak_checker.get_open_files() - new_fds = set([t[0] for t in lines2]) - set([t[0] for t in config._openfiles]) - open_files = [t for t in lines2 if t[0] in new_fds] - if open_files: - error = [] - error.append("***** %s FD leakage detected" % len(open_files)) - error.extend([str(f) for f in open_files]) - error.append("*** Before:") - error.extend([str(f) for f in config._openfiles]) - error.append("*** After:") - error.extend([str(f) for f in lines2]) - error.append(error[0]) - raise AssertionError("\n".join(error)) - -@pytest.hookimpl_opts(hookwrapper=True, trylast=True) -def pytest_runtest_teardown(item): - yield - item.config._basedir.chdir() - if hasattr(item.config, '_openfiles'): - check_open_files(item.config) - -# XXX copied from execnet's conftest.py - needs to be merged -winpymap = { - 'python2.7': r'C:\Python27\python.exe', - 'python2.6': r'C:\Python26\python.exe', - 'python3.1': r'C:\Python31\python.exe', - 'python3.2': r'C:\Python32\python.exe', - 'python3.3': r'C:\Python33\python.exe', - 'python3.4': r'C:\Python34\python.exe', - 'python3.5': r'C:\Python35\python.exe', -} - -def getexecutable(name, cache={}): - try: - return cache[name] - except KeyError: - executable = py.path.local.sysfind(name) - if executable: - if name == "jython": - import subprocess - popen = subprocess.Popen([str(executable), "--version"], - universal_newlines=True, stderr=subprocess.PIPE) - out, err = popen.communicate() - if not err or "2.5" not in err: - executable = None - if "2.5.2" in err: - executable = None # http://bugs.jython.org/issue1790 - cache[name] = executable - return executable - -@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", - 'pypy', 'pypy3']) -def anypython(request): - name = request.param - executable = getexecutable(name) - if executable is None: - if sys.platform == "win32": - executable = winpymap.get(name, None) - if executable: - executable = py.path.local(executable) - if executable.check(): - return executable - pytest.skip("no suitable %s found" % (name,)) - return executable diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/collect.py --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -627,9 +627,7 @@ sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") result = testdir.runpytest("-v", "-s") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/fixture.py --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -100,9 +100,7 @@ sub1.join("test_in_sub1.py").write("def test_1(arg1): pass") sub2.join("test_in_sub2.py").write("def test_2(arg2): pass") result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_extend_fixture_module_class(self, testdir): testfile = testdir.makepyfile(""" diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/python/metafunc.py --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -292,9 +292,7 @@ """) result = testdir.runpytest() assert result.ret == 1 - result.stdout.fnmatch_lines([ - "*6 fail*", - ]) + result.assert_outcomes(failed=6) def test_parametrize_CSV(self, testdir): testdir.makepyfile(""" @@ -375,7 +373,7 @@ assert metafunc.cls == TestClass """) result = testdir.runpytest(p, "-v") - result.assertoutcome(passed=2) + result.assert_outcomes(passed=2) def test_addcall_with_two_funcargs_generators(self, testdir): testdir.makeconftest(""" @@ -430,9 +428,7 @@ pass """) result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 pass*", - ]) + result.assert_outcomes(passed=1) def test_generate_plugin_and_module(self, testdir): @@ -506,9 +502,7 @@ self.val = 1 """) result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 pass*", - ]) + result.assert_outcomes(passed=1) def test_parametrize_functional2(self, testdir): testdir.makepyfile(""" @@ -653,8 +647,8 @@ def test_function(): pass """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=1) def test_generate_tests_only_done_in_subdir(self, testdir): sub1 = testdir.mkpydir("sub1") @@ -670,9 +664,7 @@ sub1.join("test_in_sub1.py").write("def test_1(): pass") sub2.join("test_in_sub2.py").write("def test_2(): pass") result = testdir.runpytest("-v", "-s", sub1, sub2, sub1) - result.stdout.fnmatch_lines([ - "*3 passed*" - ]) + result.assert_outcomes(passed=3) def test_generate_same_function_names_issue403(self, testdir): testdir.makepyfile(""" @@ -687,8 +679,8 @@ test_x = make_tests() test_y = make_tests() """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=4) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=4) @pytest.mark.issue463 def test_parameterize_misspelling(self, testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_assertion.py --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -461,7 +461,7 @@ ("--assert=plain", "--nomagic"), ("--assert=plain", "--no-assert", "--nomagic")) for opt in off_options: - result = testdir.runpytest(*opt) + result = testdir.runpytest_subprocess(*opt) assert "3 == 4" not in result.stdout.str() def test_old_assert_mode(testdir): @@ -469,7 +469,7 @@ def test_in_old_mode(): assert "@py_builtins" not in globals() """) - result = testdir.runpytest("--assert=reinterp") + result = testdir.runpytest_subprocess("--assert=reinterp") assert result.ret == 0 def test_triple_quoted_string_issue113(testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -453,7 +453,7 @@ assert not os.path.exists(__cached__) assert not os.path.exists(os.path.dirname(__cached__))""") monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") - assert testdir.runpytest().ret == 0 + assert testdir.runpytest_subprocess().ret == 0 @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): @@ -468,12 +468,12 @@ tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) - assert testdir.runpybin("py.test", tmp).ret == 0 + assert testdir.runpytest_subprocess(tmp).ret == 0 tagged = "test_pyc_vs_pyo." + PYTEST_TAG assert tagged + ".pyo" in os.listdir("__pycache__") monkeypatch.undo() monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) - assert testdir.runpybin("py.test", tmp).ret == 1 + assert testdir.runpytest_subprocess(tmp).ret == 1 assert tagged + ".pyc" in os.listdir("__pycache__") def test_package(self, testdir): @@ -615,10 +615,8 @@ testdir.makepyfile(**contents) testdir.maketxtfile(**{'testpkg/resource': "Load me please."}) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '* 1 passed*', - ]) + result = testdir.runpytest_subprocess() + result.assert_outcomes(passed=1) def test_read_pyc(self, tmpdir): """ diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_capture.py --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -282,7 +282,7 @@ logging.basicConfig(stream=stream) stream.close() # to free memory/release resources """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stderr.str().find("atexit") == -1 def test_logging_and_immediate_setupteardown(self, testdir): @@ -301,7 +301,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors show first! @@ -327,7 +327,7 @@ """) for optargs in (('--capture=sys',), ('--capture=fd',)): print (optargs) - result = testdir.runpytest(p, *optargs) + result = testdir.runpytest_subprocess(p, *optargs) s = result.stdout.str() result.stdout.fnmatch_lines([ "*WARN*hello3", # errors come first @@ -348,7 +348,7 @@ logging.warn("hello432") assert 0 """) - result = testdir.runpytest( + result = testdir.runpytest_subprocess( p, "--traceconfig", "-p", "no:capturelog") assert result.ret != 0 @@ -364,7 +364,7 @@ logging.warn("hello435") """) # make sure that logging is still captured in tests - result = testdir.runpytest("-s", "-p", "no:capturelog") + result = testdir.runpytest_subprocess("-s", "-p", "no:capturelog") assert result.ret == 0 result.stderr.fnmatch_lines([ "WARNING*hello435*", @@ -383,7 +383,7 @@ logging.warn("hello433") assert 0 """) - result = testdir.runpytest(p, "-p", "no:capturelog") + result = testdir.runpytest_subprocess(p, "-p", "no:capturelog") assert result.ret != 0 result.stdout.fnmatch_lines([ "WARNING*hello433*", @@ -461,7 +461,7 @@ os.write(1, str(42).encode('ascii')) raise KeyboardInterrupt() """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) result.stdout.fnmatch_lines([ "*KeyboardInterrupt*" ]) @@ -474,7 +474,7 @@ def test_log(capsys): logging.error('x') """) - result = testdir.runpytest(p) + result = testdir.runpytest_subprocess(p) assert 'closed' not in result.stderr.str() @@ -500,7 +500,7 @@ def test_hello(capfd): pass """) - result = testdir.runpytest("--capture=no") + result = testdir.runpytest_subprocess("--capture=no") result.stdout.fnmatch_lines([ "*1 skipped*" ]) @@ -563,9 +563,7 @@ test_foo() """) result = testdir.runpytest('--assert=plain') - result.stdout.fnmatch_lines([ - '*2 passed*', - ]) + result.assert_outcomes(passed=2) class TestTextIO: @@ -885,7 +883,7 @@ os.write(1, "hello\\n".encode("ascii")) assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_x* *assert 0* @@ -936,7 +934,7 @@ cap = StdCaptureFD(out=False, err=False, in_=True) cap.stop_capturing() """) - result = testdir.runpytest("--capture=fd") + result = testdir.runpytest_subprocess("--capture=fd") assert result.ret == 0 assert result.parseoutcomes()['passed'] == 3 @@ -971,7 +969,7 @@ os.write(1, b"hello\\n") assert 0 """) - result = testdir.runpytest() + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(""" *test_capture_again* *assert 0* diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_collection.py --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -296,7 +296,6 @@ subdir.ensure("__init__.py") target = subdir.join(p.basename) p.move(target) - testdir.chdir() subdir.chdir() config = testdir.parseconfig(p.basename) rcol = Session(config=config) @@ -313,7 +312,7 @@ def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) - # XXX migrate to inline_genitems? (see below) + # XXX migrate to collectonly? (see below) config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Session(config) @@ -470,7 +469,6 @@ assert col.config is config def test_pkgfile(self, testdir): - testdir.chdir() tmpdir = testdir.tmpdir subdir = tmpdir.join("subdir") x = subdir.ensure("x.py") diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -75,7 +75,7 @@ [pytest] addopts = --qwe """) - result = testdir.runpytest("--confcutdir=.") + result = testdir.inline_run("--confcutdir=.") assert result.ret == 0 class TestConfigCmdlineParsing: diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -961,7 +961,7 @@ """) p.copy(p.dirpath("skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + result = testdir.runpytest("-rw", "-p", "skipping1", syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines([ "WI1*skipped plugin*skipping1*hello*", @@ -990,7 +990,7 @@ assert plugin is not None """) monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) + result = testdir.runpytest(p, syspathinsert=True) assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_doctest.py --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,5 +1,5 @@ from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile -import py, pytest +import py class TestDoctests: @@ -75,8 +75,6 @@ assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - @pytest.mark.xfail('hasattr(sys, "pypy_version_info")', reason= - "pypy leaks one FD") def test_simple_doctestfile(self, testdir): p = testdir.maketxtfile(test_doc=""" >>> x = 1 diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_genscript.py --- a/testing/test_genscript.py +++ b/testing/test_genscript.py @@ -16,7 +16,6 @@ assert self.script.check() def run(self, anypython, testdir, *args): - testdir.chdir() return testdir._run(anypython, self.script, *args) def test_gen(testdir, anypython, standalone): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_helpconfig.py --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -53,14 +53,14 @@ ]) def test_debug(testdir, monkeypatch): - result = testdir.runpytest("--debug") + result = testdir.runpytest_subprocess("--debug") assert result.ret == 0 p = testdir.tmpdir.join("pytestdebug.log") assert "pytest_sessionstart" in p.read() def test_PYTEST_DEBUG(testdir, monkeypatch): monkeypatch.setenv("PYTEST_DEBUG", "1") - result = testdir.runpytest() + result = testdir.runpytest_subprocess() assert result.ret == 0 result.stderr.fnmatch_lines([ "*pytest_plugin_registered*", diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_nose.py --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -19,9 +19,7 @@ test_hello.teardown = lambda: l.append(2) """) result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_setup_func_with_setup_decorator(): @@ -66,9 +64,7 @@ """) result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) + result.assert_outcomes(passed=2) def test_nose_setup_func_failure(testdir): @@ -302,7 +298,7 @@ pass """) result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + result.assert_outcomes(passed=1) @pytest.mark.skipif("sys.version_info < (2,6)") def test_setup_teardown_linking_issue265(testdir): @@ -327,8 +323,8 @@ """Undoes the setup.""" raise Exception("should not call teardown for skipped tests") ''') - reprec = testdir.inline_run() - reprec.assertoutcome(passed=1, skipped=1) + reprec = testdir.runpytest() + reprec.assert_outcomes(passed=1, skipped=1) def test_SkipTest_during_collection(testdir): @@ -339,7 +335,7 @@ assert False """) result = testdir.runpytest(p) - result.assertoutcome(skipped=1) + result.assert_outcomes(skipped=1) def test_SkipTest_in_test(testdir): diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_pdb.py --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -2,6 +2,13 @@ import py import sys +def runpdb_and_get_report(testdir, source): + p = testdir.makepyfile(source) + result = testdir.runpytest_inprocess("--pdb", p) + reports = result.reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3, reports # setup/call/teardown + return reports[1] + class TestPDB: def pytest_funcarg__pdblist(self, request): @@ -14,7 +21,7 @@ return pdblist def test_pdb_on_fail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ def test_func(): assert 0 """) @@ -24,7 +31,7 @@ assert tb[-1].name == "test_func" def test_pdb_on_xfail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest @pytest.mark.xfail def test_func(): @@ -34,7 +41,7 @@ assert not pdblist def test_pdb_on_skip(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import pytest def test_func(): pytest.skip("hello") @@ -43,7 +50,7 @@ assert len(pdblist) == 0 def test_pdb_on_BdbQuit(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ + rep = runpdb_and_get_report(testdir, """ import bdb def test_func(): raise bdb.BdbQuit @@ -260,7 +267,7 @@ def test_pdb_collection_failure_is_shown(self, testdir): p1 = testdir.makepyfile("""xxx """) - result = testdir.runpytest("--pdb", p1) + result = testdir.runpytest_subprocess("--pdb", p1) result.stdout.fnmatch_lines([ "*NameError*xxx*", "*1 error*", diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_pytester.py --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -69,9 +69,7 @@ assert 1 """) result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) + result.assert_outcomes(passed=1) def make_holder(): @@ -114,16 +112,6 @@ unichr = chr testdir.makepyfile(unichr(0xfffd)) -def test_inprocess_plugins(testdir): - class Plugin(object): - configured = False - def pytest_configure(self, config): - self.configured = True - plugin = Plugin() - testdir.inprocess_run([], [plugin]) - - assert plugin.configured - def test_inline_run_clean_modules(testdir): test_mod = testdir.makepyfile("def test_foo(): assert True") result = testdir.inline_run(str(test_mod)) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -203,7 +203,6 @@ def test_plugin_specify(testdir): - testdir.chdir() pytest.raises(ImportError, """ testdir.parseconfig("-p", "nqweotexistent") """) diff -r a2dfd7c1fb40818cf8b61e17ebf30b0ed287918e -r 7d4a0b78d19b985ccca88827129d825151ad494c tox.ini --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,py27-trial,py33-trial,doctesting,py27-cxfreeze +envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py33,py27-xdist,py33-xdist,{py27,py33}-trial,py27-subprocess,doctesting,py27-cxfreeze [testenv] changedir=testing @@ -9,6 +9,15 @@ nose mock +[testenv:py27-subprocess] +changedir=. +basepython=python2.7 +deps=pytest-xdist + mock + nose +commands= + py.test -n3 -rfsxX --runpytest=subprocess {posargs:testing} + [testenv:genscript] changedir=. commands= py.test --genscript=pytest1 @@ -136,7 +145,7 @@ minversion=2.0 plugins=pytester #--pyargs --doctest-modules --ignore=.tox -addopts= -rxsX +addopts= -rxsX -p pytester rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance Repository URL: https://bitbucket.org/pytest-dev/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org https://mail.python.org/mailman/listinfo/pytest-commit