remove non-documented per-conftest capturing option and simplify/refactor all code accordingly. Also make capturing more robust against tests closing FD1/2 and against pdb.set_trace() calls.
This commit is contained in:
		
							parent
							
								
									2e1f6c85f6
								
							
						
					
					
						commit
						ce8678e6d5
					
				|  | @ -21,9 +21,10 @@ patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'} | ||||||
| def pytest_addoption(parser): | def pytest_addoption(parser): | ||||||
|     group = parser.getgroup("general") |     group = parser.getgroup("general") | ||||||
|     group._addoption( |     group._addoption( | ||||||
|         '--capture', action="store", default=None, |         '--capture', action="store",  | ||||||
|  |         default="fd" if hasattr(os, "dup") else "sys", | ||||||
|         metavar="method", choices=['fd', 'sys', 'no'], |         metavar="method", choices=['fd', 'sys', 'no'], | ||||||
|         help="per-test capturing method: one of fd (default)|sys|no.") |         help="per-test capturing method: one of fd|sys|no.") | ||||||
|     group._addoption( |     group._addoption( | ||||||
|         '-s', action="store_const", const="no", dest="capture", |         '-s', action="store_const", const="no", dest="capture", | ||||||
|         help="shortcut for --capture=no.") |         help="shortcut for --capture=no.") | ||||||
|  | @ -32,16 +33,13 @@ def pytest_addoption(parser): | ||||||
| @pytest.mark.tryfirst | @pytest.mark.tryfirst | ||||||
| def pytest_load_initial_conftests(early_config, parser, args, __multicall__): | def pytest_load_initial_conftests(early_config, parser, args, __multicall__): | ||||||
|     ns = parser.parse_known_args(args) |     ns = parser.parse_known_args(args) | ||||||
|     method = ns.capture |  | ||||||
|     if not method: |  | ||||||
|         method = "fd" |  | ||||||
|     if method == "fd" and not hasattr(os, "dup"): |  | ||||||
|         method = "sys" |  | ||||||
|     pluginmanager = early_config.pluginmanager |     pluginmanager = early_config.pluginmanager | ||||||
|  |     method = ns.capture | ||||||
|     if method != "no": |     if method != "no": | ||||||
|         dupped_stdout = safe_text_dupfile(sys.stdout, "wb") |         dupped_stdout = safe_text_dupfile(sys.stdout, "wb") | ||||||
|         pluginmanager.register(dupped_stdout, "dupped_stdout") |         pluginmanager.register(dupped_stdout, "dupped_stdout") | ||||||
|             #pluginmanager.add_shutdown(dupped_stdout.close) |             #pluginmanager.add_shutdown(dupped_stdout.close) | ||||||
|  | 
 | ||||||
|     capman = CaptureManager(method) |     capman = CaptureManager(method) | ||||||
|     pluginmanager.register(capman, "capturemanager") |     pluginmanager.register(capman, "capturemanager") | ||||||
| 
 | 
 | ||||||
|  | @ -55,7 +53,7 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__): | ||||||
|     pluginmanager.add_shutdown(silence_logging_at_shutdown) |     pluginmanager.add_shutdown(silence_logging_at_shutdown) | ||||||
| 
 | 
 | ||||||
|     # finally trigger conftest loading but while capturing (issue93) |     # finally trigger conftest loading but while capturing (issue93) | ||||||
|     capman.resumecapture() |     capman.init_capturings() | ||||||
|     try: |     try: | ||||||
|         try: |         try: | ||||||
|             return __multicall__.execute() |             return __multicall__.execute() | ||||||
|  | @ -67,11 +65,9 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__): | ||||||
|         raise |         raise | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| class CaptureManager: | class CaptureManager: | ||||||
|     def __init__(self, defaultmethod=None): |     def __init__(self, method): | ||||||
|         self._method2capture = {} |         self._method = method | ||||||
|         self._defaultmethod = defaultmethod |  | ||||||
| 
 | 
 | ||||||
|     def _getcapture(self, method): |     def _getcapture(self, method): | ||||||
|         if method == "fd": |         if method == "fd": | ||||||
|  | @ -83,53 +79,27 @@ class CaptureManager: | ||||||
|         else: |         else: | ||||||
|             raise ValueError("unknown capturing method: %r" % method) |             raise ValueError("unknown capturing method: %r" % method) | ||||||
| 
 | 
 | ||||||
|     def _getmethod(self, config, fspath): |     def init_capturings(self): | ||||||
|         if config.option.capture: |         assert not hasattr(self, "_capturing") | ||||||
|             method = config.option.capture |         self._capturing = self._getcapture(self._method) | ||||||
|         else: |         self._capturing.start_capturing() | ||||||
|             try: |  | ||||||
|                 method = config._conftest.rget("option_capture", path=fspath) |  | ||||||
|             except KeyError: |  | ||||||
|                 method = "fd" |  | ||||||
|         if method == "fd" and not hasattr(os, 'dup'):  # e.g. jython |  | ||||||
|             method = "sys" |  | ||||||
|         return method |  | ||||||
| 
 | 
 | ||||||
|     def reset_capturings(self): |     def reset_capturings(self): | ||||||
|         for cap in self._method2capture.values(): |         cap = self.__dict__.pop("_capturing", None) | ||||||
|  |         if cap is not None: | ||||||
|             cap.pop_outerr_to_orig() |             cap.pop_outerr_to_orig() | ||||||
|             cap.stop_capturing() |             cap.stop_capturing() | ||||||
|         self._method2capture.clear() |  | ||||||
| 
 | 
 | ||||||
|     def resumecapture_item(self, item): |     def resumecapture(self): | ||||||
|         method = self._getmethod(item.config, item.fspath) |         self._capturing.resume_capturing() | ||||||
|         return self.resumecapture(method) |  | ||||||
| 
 | 
 | ||||||
|     def resumecapture(self, method=None): |     def suspendcapture(self, in_=False): | ||||||
|         if hasattr(self, '_capturing'): |  | ||||||
|             raise ValueError( |  | ||||||
|                 "cannot resume, already capturing with %r" % |  | ||||||
|                 (self._capturing,)) |  | ||||||
|         if method is None: |  | ||||||
|             method = self._defaultmethod |  | ||||||
|         cap = self._method2capture.get(method) |  | ||||||
|         self._capturing = method |  | ||||||
|         if cap is None: |  | ||||||
|             self._method2capture[method] = cap = self._getcapture(method) |  | ||||||
|             cap.start_capturing() |  | ||||||
|         else: |  | ||||||
|             cap.resume_capturing() |  | ||||||
| 
 |  | ||||||
|     def suspendcapture(self, item=None): |  | ||||||
|         self.deactivate_funcargs() |         self.deactivate_funcargs() | ||||||
|         method = self.__dict__.pop("_capturing", None) |         cap = getattr(self, "_capturing", None) | ||||||
|         outerr = "", "" |         if cap is not None: | ||||||
|         if method is not None: |             outerr = cap.readouterr() | ||||||
|             cap = self._method2capture.get(method) |             cap.suspend_capturing(in_=in_) | ||||||
|             if cap is not None: |             return outerr | ||||||
|                 outerr = cap.readouterr() |  | ||||||
|                 cap.suspend_capturing() |  | ||||||
|         return outerr |  | ||||||
| 
 | 
 | ||||||
|     def activate_funcargs(self, pyfuncitem): |     def activate_funcargs(self, pyfuncitem): | ||||||
|         capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None) |         capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None) | ||||||
|  | @ -142,28 +112,20 @@ class CaptureManager: | ||||||
|         if capfuncarg is not None: |         if capfuncarg is not None: | ||||||
|             capfuncarg.close() |             capfuncarg.close() | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.hookwrapper |     @pytest.mark.tryfirst | ||||||
|     def pytest_make_collect_report(self, __multicall__, collector): |     def pytest_make_collect_report(self, __multicall__, collector): | ||||||
|         method = self._getmethod(collector.config, collector.fspath) |         if not isinstance(collector, pytest.File): | ||||||
|         try: |  | ||||||
|             self.resumecapture(method) |  | ||||||
|         except ValueError: |  | ||||||
|             yield |  | ||||||
|             # recursive collect, XXX refactor capturing |  | ||||||
|             # to allow for more lightweight recursive capturing |  | ||||||
|             return |             return | ||||||
|         yield |         self.resumecapture() | ||||||
|         out, err = self.suspendcapture() |         try: | ||||||
|         # XXX getting the report from the ongoing hook call is a bit |             rep = __multicall__.execute() | ||||||
|         # of a hack.  We need to think about capturing during collection |         finally: | ||||||
|         # and find out if it's really needed fine-grained (per |             out, err = self.suspendcapture() | ||||||
|         # collector). |         if out: | ||||||
|         if __multicall__.results: |             rep.sections.append(("Captured stdout", out)) | ||||||
|             rep = __multicall__.results[0] |         if err: | ||||||
|             if out: |             rep.sections.append(("Captured stderr", err)) | ||||||
|                 rep.sections.append(("Captured stdout", out)) |         return rep | ||||||
|             if err: |  | ||||||
|                 rep.sections.append(("Captured stderr", err)) |  | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.hookwrapper |     @pytest.mark.hookwrapper | ||||||
|     def pytest_runtest_setup(self, item): |     def pytest_runtest_setup(self, item): | ||||||
|  | @ -192,9 +154,9 @@ class CaptureManager: | ||||||
| 
 | 
 | ||||||
|     @contextlib.contextmanager |     @contextlib.contextmanager | ||||||
|     def item_capture_wrapper(self, item, when): |     def item_capture_wrapper(self, item, when): | ||||||
|         self.resumecapture_item(item) |         self.resumecapture() | ||||||
|         yield |         yield | ||||||
|         out, err = self.suspendcapture(item) |         out, err = self.suspendcapture() | ||||||
|         item.add_report_section(when, "out", out) |         item.add_report_section(when, "out", out) | ||||||
|         item.add_report_section(when, "err", err) |         item.add_report_section(when, "err", err) | ||||||
| 
 | 
 | ||||||
|  | @ -238,14 +200,14 @@ class CaptureFixture: | ||||||
|     def close(self): |     def close(self): | ||||||
|         cap = self.__dict__.pop("_capture", None) |         cap = self.__dict__.pop("_capture", None) | ||||||
|         if cap is not None: |         if cap is not None: | ||||||
|             cap.pop_outerr_to_orig() |             self._outerr = cap.pop_outerr_to_orig() | ||||||
|             cap.stop_capturing() |             cap.stop_capturing() | ||||||
| 
 | 
 | ||||||
|     def readouterr(self): |     def readouterr(self): | ||||||
|         try: |         try: | ||||||
|             return self._capture.readouterr() |             return self._capture.readouterr() | ||||||
|         except AttributeError: |         except AttributeError: | ||||||
|             return "", "" |             return self._outerr | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def safe_text_dupfile(f, mode, default_encoding="UTF8"): | def safe_text_dupfile(f, mode, default_encoding="UTF8"): | ||||||
|  | @ -311,18 +273,25 @@ class MultiCapture(object): | ||||||
|             self.out.writeorg(out) |             self.out.writeorg(out) | ||||||
|         if err: |         if err: | ||||||
|             self.err.writeorg(err) |             self.err.writeorg(err) | ||||||
|  |         return out, err | ||||||
| 
 | 
 | ||||||
|     def suspend_capturing(self): |     def suspend_capturing(self, in_=False): | ||||||
|         if self.out: |         if self.out: | ||||||
|             self.out.suspend() |             self.out.suspend() | ||||||
|         if self.err: |         if self.err: | ||||||
|             self.err.suspend() |             self.err.suspend() | ||||||
|  |         if in_ and self.in_: | ||||||
|  |             self.in_.suspend() | ||||||
|  |             self._in_suspended = True | ||||||
| 
 | 
 | ||||||
|     def resume_capturing(self): |     def resume_capturing(self): | ||||||
|         if self.out: |         if self.out: | ||||||
|             self.out.resume() |             self.out.resume() | ||||||
|         if self.err: |         if self.err: | ||||||
|             self.err.resume() |             self.err.resume() | ||||||
|  |         if hasattr(self, "_in_suspended"): | ||||||
|  |             self.in_.resume() | ||||||
|  |             del self._in_suspended | ||||||
| 
 | 
 | ||||||
|     def stop_capturing(self): |     def stop_capturing(self): | ||||||
|         """ stop capturing and reset capturing streams """ |         """ stop capturing and reset capturing streams """ | ||||||
|  | @ -393,7 +362,8 @@ class FDCapture: | ||||||
|                 res = py.builtin._totext(res, enc, "replace") |                 res = py.builtin._totext(res, enc, "replace") | ||||||
|             f.truncate(0) |             f.truncate(0) | ||||||
|             f.seek(0) |             f.seek(0) | ||||||
|         return res |             return res | ||||||
|  |         return '' | ||||||
| 
 | 
 | ||||||
|     def done(self): |     def done(self): | ||||||
|         """ stop capturing, restore streams, return original capture file, |         """ stop capturing, restore streams, return original capture file, | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ class pytestPDB: | ||||||
|         if self._pluginmanager is not None: |         if self._pluginmanager is not None: | ||||||
|             capman = self._pluginmanager.getplugin("capturemanager") |             capman = self._pluginmanager.getplugin("capturemanager") | ||||||
|             if capman: |             if capman: | ||||||
|                 capman.reset_capturings() |                 capman.suspendcapture(in_=True) | ||||||
|             tw = py.io.TerminalWriter() |             tw = py.io.TerminalWriter() | ||||||
|             tw.line() |             tw.line() | ||||||
|             tw.sep(">", "PDB set_trace (IO-capturing turned off)") |             tw.sep(">", "PDB set_trace (IO-capturing turned off)") | ||||||
|  | @ -45,8 +45,8 @@ class PdbInvoke: | ||||||
|     def pytest_exception_interact(self, node, call, report): |     def pytest_exception_interact(self, node, call, report): | ||||||
|         capman = node.config.pluginmanager.getplugin("capturemanager") |         capman = node.config.pluginmanager.getplugin("capturemanager") | ||||||
|         if capman: |         if capman: | ||||||
|             capman.reset_capturings() |             capman.suspendcapture(in_=True) | ||||||
|         return _enter_pdb(node, call.excinfo, report) |         _enter_pdb(node, call.excinfo, report) | ||||||
| 
 | 
 | ||||||
|     def pytest_internalerror(self, excrepr, excinfo): |     def pytest_internalerror(self, excrepr, excinfo): | ||||||
|         for line in str(excrepr).split("\n"): |         for line in str(excrepr).split("\n"): | ||||||
|  |  | ||||||
|  | @ -53,79 +53,54 @@ def StdCapture(out=True, err=True, in_=True): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestCaptureManager: | class TestCaptureManager: | ||||||
|     def test_getmethod_default_no_fd(self, testdir, monkeypatch): |     def test_getmethod_default_no_fd(self, monkeypatch): | ||||||
|         config = testdir.parseconfig(testdir.tmpdir) |         from _pytest.capture import pytest_addoption | ||||||
|         assert config.getvalue("capture") is None |         from _pytest.config import Parser | ||||||
|         capman = CaptureManager() |         parser = Parser() | ||||||
|  |         pytest_addoption(parser) | ||||||
|  |         default = parser._groups[0].options[0].default | ||||||
|  |         assert default == "fd" if hasattr(os, "dup") else "sys" | ||||||
|  |         parser = Parser() | ||||||
|         monkeypatch.delattr(os, 'dup', raising=False) |         monkeypatch.delattr(os, 'dup', raising=False) | ||||||
|         try: |         pytest_addoption(parser) | ||||||
|             assert capman._getmethod(config, None) == "sys" |         assert parser._groups[0].options[0].default == "sys" | ||||||
|         finally: |  | ||||||
|             monkeypatch.undo() |  | ||||||
| 
 |  | ||||||
|     @pytest.mark.parametrize("mode", "no fd sys".split()) |  | ||||||
|     def test_configure_per_fspath(self, testdir, mode): |  | ||||||
|         config = testdir.parseconfig(testdir.tmpdir) |  | ||||||
|         capman = CaptureManager() |  | ||||||
|         hasfd = hasattr(os, 'dup') |  | ||||||
|         if hasfd: |  | ||||||
|             assert capman._getmethod(config, None) == "fd" |  | ||||||
|         else: |  | ||||||
|             assert capman._getmethod(config, None) == "sys" |  | ||||||
| 
 |  | ||||||
|         if not hasfd and mode == 'fd': |  | ||||||
|             return |  | ||||||
|         sub = testdir.tmpdir.mkdir("dir" + mode) |  | ||||||
|         sub.ensure("__init__.py") |  | ||||||
|         sub.join("conftest.py").write('option_capture = %r' % mode) |  | ||||||
|         assert capman._getmethod(config, sub.join("test_hello.py")) == mode |  | ||||||
| 
 | 
 | ||||||
|     @needsosdup |     @needsosdup | ||||||
|     @pytest.mark.parametrize("method", ['no', 'fd', 'sys']) |     @pytest.mark.parametrize("method",  | ||||||
|  |         ['no', 'sys', pytest.mark.skipif('not hasattr(os, "dup")', 'fd')]) | ||||||
|     def test_capturing_basic_api(self, method): |     def test_capturing_basic_api(self, method): | ||||||
|         capouter = StdCaptureFD() |         capouter = StdCaptureFD() | ||||||
|         old = sys.stdout, sys.stderr, sys.stdin |         old = sys.stdout, sys.stderr, sys.stdin | ||||||
|         try: |         try: | ||||||
|             capman = CaptureManager() |             capman = CaptureManager(method) | ||||||
|             # call suspend without resume or start |             capman.init_capturings() | ||||||
|             outerr = capman.suspendcapture() |             outerr = capman.suspendcapture() | ||||||
|  |             assert outerr == ("", "") | ||||||
|             outerr = capman.suspendcapture() |             outerr = capman.suspendcapture() | ||||||
|             assert outerr == ("", "") |             assert outerr == ("", "") | ||||||
|             capman.resumecapture(method) |  | ||||||
|             print ("hello") |             print ("hello") | ||||||
|             out, err = capman.suspendcapture() |             out, err = capman.suspendcapture() | ||||||
|             if method == "no": |             if method == "no": | ||||||
|                 assert old == (sys.stdout, sys.stderr, sys.stdin) |                 assert old == (sys.stdout, sys.stderr, sys.stdin) | ||||||
|             else: |             else: | ||||||
|                 assert out == "hello\n" |                 assert not out | ||||||
|             capman.resumecapture(method) |             capman.resumecapture() | ||||||
|  |             print ("hello") | ||||||
|             out, err = capman.suspendcapture() |             out, err = capman.suspendcapture() | ||||||
|             assert not out and not err |             if method != "no": | ||||||
|  |                 assert out == "hello\n" | ||||||
|             capman.reset_capturings() |             capman.reset_capturings() | ||||||
|         finally: |         finally: | ||||||
|             capouter.stop_capturing() |             capouter.stop_capturing() | ||||||
| 
 | 
 | ||||||
|     @needsosdup |     @needsosdup | ||||||
|     def test_juggle_capturings(self, testdir): |     def test_init_capturing(self): | ||||||
|         capouter = StdCaptureFD() |         capouter = StdCaptureFD() | ||||||
|         try: |         try: | ||||||
|             #config = testdir.parseconfig(testdir.tmpdir) |             capman = CaptureManager("fd") | ||||||
|             capman = CaptureManager() |             capman.init_capturings() | ||||||
|             try: |             pytest.raises(AssertionError, "capman.init_capturings()") | ||||||
|                 capman.resumecapture("fd") |             capman.reset_capturings() | ||||||
|                 pytest.raises(ValueError, 'capman.resumecapture("fd")') |  | ||||||
|                 pytest.raises(ValueError, 'capman.resumecapture("sys")') |  | ||||||
|                 os.write(1, "hello\n".encode('ascii')) |  | ||||||
|                 out, err = capman.suspendcapture() |  | ||||||
|                 assert out == "hello\n" |  | ||||||
|                 capman.resumecapture("sys") |  | ||||||
|                 os.write(1, "hello\n".encode('ascii')) |  | ||||||
|                 py.builtin.print_("world", file=sys.stderr) |  | ||||||
|                 out, err = capman.suspendcapture() |  | ||||||
|                 assert not out |  | ||||||
|                 assert err == "world\n" |  | ||||||
|             finally: |  | ||||||
|                 capman.reset_capturings() |  | ||||||
|         finally: |         finally: | ||||||
|             capouter.stop_capturing() |             capouter.stop_capturing() | ||||||
| 
 | 
 | ||||||
|  | @ -991,7 +966,7 @@ def test_close_and_capture_again(testdir): | ||||||
|         def test_close(): |         def test_close(): | ||||||
|             os.close(1) |             os.close(1) | ||||||
|         def test_capture_again(): |         def test_capture_again(): | ||||||
|             os.write(1, "hello\\n") |             os.write(1, b"hello\\n") | ||||||
|             assert 0 |             assert 0 | ||||||
|     """) |     """) | ||||||
|     result = testdir.runpytest() |     result = testdir.runpytest() | ||||||
|  |  | ||||||
|  | @ -167,6 +167,26 @@ class TestPDB: | ||||||
|         if child.isalive(): |         if child.isalive(): | ||||||
|             child.wait() |             child.wait() | ||||||
| 
 | 
 | ||||||
|  |     def test_set_trace_capturing_afterwards(self, testdir): | ||||||
|  |         p1 = testdir.makepyfile(""" | ||||||
|  |             import pdb | ||||||
|  |             def test_1(): | ||||||
|  |                 pdb.set_trace() | ||||||
|  |             def test_2(): | ||||||
|  |                 print ("hello") | ||||||
|  |                 assert 0 | ||||||
|  |         """) | ||||||
|  |         child = testdir.spawn_pytest(str(p1)) | ||||||
|  |         child.expect("test_1") | ||||||
|  |         child.send("c\n") | ||||||
|  |         child.expect("test_2") | ||||||
|  |         child.expect("Captured") | ||||||
|  |         child.expect("hello") | ||||||
|  |         child.sendeof() | ||||||
|  |         child.read() | ||||||
|  |         if child.isalive(): | ||||||
|  |             child.wait() | ||||||
|  | 
 | ||||||
|     @xfail_if_pdbpp_installed |     @xfail_if_pdbpp_installed | ||||||
|     def test_pdb_interaction_doctest(self, testdir): |     def test_pdb_interaction_doctest(self, testdir): | ||||||
|         p1 = testdir.makepyfile(""" |         p1 = testdir.makepyfile(""" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue