[coverage] Provides better tools to pause tracing

The former implementation was not restoring properly the trace function on python 2.7 at least. This cleaner implementation uses context-manager, and deprecates the pause_tracing/resume_tracing couple.

Closes #280757

authorChristophe de Vienne <christophe@unlish.com>
changeset31725b8fa3f5
branchdefault
phasepublic
hiddenno
parent revision#d7b9bf865889 [pytest] fix TestSuite.run wrapper (closes #280806)
child revision#123b72695560 [date] Fix utcdatetime, #5cdfa02c4ea5 [date] Fix utcdatetime
files modified by this revision
pytest.py
test/unittest_pytest.py
# HG changeset patch
# User Christophe de Vienne <christophe@unlish.com>
# Date 1417084944 -3600
# Thu Nov 27 11:42:24 2014 +0100
# Node ID 31725b8fa3f58b94742899a20d939a781a65be42
# Parent d7b9bf86588969721df23d9d19b2832e1a6e197d
[coverage] Provides better tools to pause tracing

The former implementation was not restoring properly the trace function on python 2.7 at least.
This cleaner implementation uses context-manager, and deprecates the pause_tracing/resume_tracing couple.

Closes #280757

diff --git a/pytest.py b/pytest.py
@@ -117,16 +117,18 @@
1  import os.path as osp
2  from time import time, clock
3  import warnings
4  import types
5  from inspect import isgeneratorfunction, isclass
6 +from contextlib import contextmanager
7 
8  from logilab.common.fileutils import abspath_listdir
9  from logilab.common import textutils
10  from logilab.common import testlib, STD_BLACKLIST
11  # use the same unittest module as testlib
12  from logilab.common.testlib import unittest, start_interactive_mode
13 +from logilab.common.deprecation import deprecated
14  import doctest
15 
16  import unittest as unittest_legacy
17  if not getattr(unittest_legacy, "__package__", None):
18      try:
@@ -143,53 +145,59 @@
19  except ImportError:
20      DJANGO_FOUND = False
21 
22  CONF_FILE = 'pytestconf.py'
23 
24 -## coverage hacks, do not read this, do not read this, do not read this
25 +## coverage pausing tools
26 
27 -# hey, but this is an aspect, right ?!!!
28 +@contextmanager
29 +def replace_trace(trace=None):
30 +    """A context manager that temporary replaces the trace function"""
31 +    oldtrace = sys.gettrace()
32 +    sys.settrace(trace)
33 +    try:
34 +        yield
35 +    finally:
36 +        sys.settrace(oldtrace)
37 +
38 +
39 +def pause_trace():
40 +    """A context manager that temporary pauses any tracing"""
41 +    return replace_trace()
42 +
43  class TraceController(object):
44 -    nesting = 0
45 +    ctx_stack = []
46 
47 +    @classmethod
48 +    @deprecated('[lgc 0.63.1] Use the pause_trace() context manager')
49      def pause_tracing(cls):
50 -        if not cls.nesting:
51 -            cls.tracefunc = staticmethod(getattr(sys, '__settrace__', sys.settrace))
52 -            cls.oldtracer = getattr(sys, '__tracer__', None)
53 -            sys.__notrace__ = True
54 -            cls.tracefunc(None)
55 -        cls.nesting += 1
56 -    pause_tracing = classmethod(pause_tracing)
57 +        cls.ctx_stack.append(pause_trace())
58 +        cls.ctx_stack[-1].__enter__()
59 
60 +    @classmethod
61 +    @deprecated('[lgc 0.63.1] Use the pause_trace() context manager')
62      def resume_tracing(cls):
63 -        cls.nesting -= 1
64 -        assert cls.nesting >= 0
65 -        if not cls.nesting:
66 -            cls.tracefunc(cls.oldtracer)
67 -            delattr(sys, '__notrace__')
68 -    resume_tracing = classmethod(resume_tracing)
69 +        cls.ctx_stack.pop().__exit__(None, None, None)
70 
71 
72  pause_tracing = TraceController.pause_tracing
73  resume_tracing = TraceController.resume_tracing
74 
75 
76  def nocoverage(func):
77 +    """Function decorator that pauses tracing functions"""
78      if hasattr(func, 'uncovered'):
79          return func
80      func.uncovered = True
81 +
82      def not_covered(*args, **kwargs):
83 -        pause_tracing()
84 -        try:
85 +        with pause_trace():
86              return func(*args, **kwargs)
87 -        finally:
88 -            resume_tracing()
89      not_covered.uncovered = True
90      return not_covered
91 
92 -
93 -## end of coverage hacks
94 +## end of coverage pausing tools
95 
96 
97  TESTFILE_RE = re.compile("^((unit)?test.*|smoketest)\.py$")
98  def this_is_a_testfile(filename):
99      """returns True if `filename` seems to be a test file"""
diff --git a/test/unittest_pytest.py b/test/unittest_pytest.py
@@ -44,7 +44,58 @@
100          self.assertFalse(this_is_a_testfile("smoketest.pl"))
101          self.assertFalse(this_is_a_testfile("unittest"))
102          self.assertTrue(this_is_a_testfile(join("coincoin", "unittest_bibi.py")))
103          self.assertFalse(this_is_a_testfile(join("unittest", "spongebob.py")))
104 
105 +    def test_replace_trace(self):
106 +        def tracefn(frame, event, arg):
107 +            pass
108 +
109 +        oldtrace = sys.gettrace()
110 +        with replace_trace(tracefn):
111 +            self.assertIs(sys.gettrace(), tracefn)
112 +
113 +        self.assertIs(sys.gettrace(), oldtrace)
114 +
115 +    def test_pause_trace(self):
116 +        def tracefn(frame, event, arg):
117 +            pass
118 +
119 +        oldtrace = sys.gettrace()
120 +        sys.settrace(tracefn)
121 +        try:
122 +            self.assertIs(sys.gettrace(), tracefn)
123 +            with pause_trace():
124 +                self.assertIs(sys.gettrace(), None)
125 +            self.assertIs(sys.gettrace(), tracefn)
126 +        finally:
127 +            sys.settrace(oldtrace)
128 +
129 +    def test_nocoverage(self):
130 +        def tracefn(frame, event, arg):
131 +            pass
132 +
133 +        @nocoverage
134 +        def myfn():
135 +            self.assertIs(sys.gettrace(), None)
136 +
137 +        with replace_trace(tracefn):
138 +            myfn()
139 +
140 +    def test_legacy_pause_resume_tracing(self):
141 +        def tracefn(frame, event, arg):
142 +            pass
143 +
144 +        with replace_trace(tracefn):
145 +            pause_tracing()
146 +            self.assertIs(sys.gettrace(), None)
147 +            with replace_trace(tracefn):
148 +                pause_tracing()
149 +                self.assertIs(sys.gettrace(), None)
150 +                resume_tracing()
151 +                self.assertIs(sys.gettrace(), tracefn)
152 +            self.assertIs(sys.gettrace(), None)
153 +            resume_tracing()
154 +            self.assertIs(sys.gettrace(), tracefn)
155 +
156  if __name__ == '__main__':
157      unittest_main()