Show More
@@ -0,0 +1,238 b'' | |||
|
1 | import re | |
|
2 | import new | |
|
3 | import inspect | |
|
4 | import logging | |
|
5 | import logging.handlers | |
|
6 | from functools import wraps | |
|
7 | ||
|
8 | from nose.tools import nottest | |
|
9 | from unittest import TestCase | |
|
10 | ||
|
11 | ||
|
12 | def _terrible_magic_get_defining_classes(): | |
|
13 | """ Returns the set of parent classes of the class currently being defined. | |
|
14 | Will likely only work if called from the ``parameterized`` decorator. | |
|
15 | This function is entirely @brandon_rhodes's fault, as he suggested | |
|
16 | the implementation: http://stackoverflow.com/a/8793684/71522 | |
|
17 | """ | |
|
18 | stack = inspect.stack() | |
|
19 | if len(stack) <= 4: | |
|
20 | return [] | |
|
21 | frame = stack[3] | |
|
22 | code_context = frame[4][0].strip() | |
|
23 | if not code_context.startswith("class "): | |
|
24 | return [] | |
|
25 | _, parents = code_context.split("(", 1) | |
|
26 | parents, _ = parents.rsplit(")", 1) | |
|
27 | return eval("[" + parents + "]", frame[0].f_globals, frame[0].f_locals) | |
|
28 | ||
|
29 | ||
|
30 | def parameterized(input): | |
|
31 | """ Parameterize a test case: | |
|
32 | >>> add1_tests = [(1, 2), (2, 3)] | |
|
33 | >>> class TestFoo(object): | |
|
34 | ... @parameterized(add1_tests) | |
|
35 | ... def test_add1(self, input, expected): | |
|
36 | ... assert_equal(add1(input), expected) | |
|
37 | >>> @parameterized(add1_tests) | |
|
38 | ... def test_add1(input, expected): | |
|
39 | ... assert_equal(add1(input), expected) | |
|
40 | >>> | |
|
41 | """ | |
|
42 | ||
|
43 | if not hasattr(input, "__iter__"): | |
|
44 | raise ValueError("expected iterable input; got %r" % (input,)) | |
|
45 | ||
|
46 | def parameterized_helper(f): | |
|
47 | attached_instance_method = [False] | |
|
48 | ||
|
49 | parent_classes = _terrible_magic_get_defining_classes() | |
|
50 | if any(issubclass(cls, TestCase) for cls in parent_classes): | |
|
51 | raise Exception("Warning: '@parameterized' tests won't work " | |
|
52 | "inside subclasses of 'TestCase' - use " | |
|
53 | "'@parameterized.expand' instead") | |
|
54 | ||
|
55 | @wraps(f) | |
|
56 | def parameterized_helper_method(self=None): | |
|
57 | if self is not None and not attached_instance_method[0]: | |
|
58 | # confusingly, we need to create a named instance method and | |
|
59 | # attach that to the class... | |
|
60 | cls = self.__class__ | |
|
61 | im_f = new.instancemethod(f, None, cls) | |
|
62 | setattr(cls, f.__name__, im_f) | |
|
63 | attached_instance_method[0] = True | |
|
64 | for args in input: | |
|
65 | if isinstance(args, basestring): | |
|
66 | args = [args] | |
|
67 | # ... then pull that named instance method off, turning it into | |
|
68 | # a bound method ... | |
|
69 | if self is not None: | |
|
70 | args = [getattr(self, f.__name__)] + list(args) | |
|
71 | else: | |
|
72 | args = [f] + list(args) | |
|
73 | # ... then yield that as a tuple. If those steps aren't | |
|
74 | # followed precicely, Nose gets upset and doesn't run the test | |
|
75 | # or doesn't run setup methods. | |
|
76 | yield tuple(args) | |
|
77 | ||
|
78 | f.__name__ = "_helper_for_%s" % (f.__name__,) | |
|
79 | parameterized_helper_method.parameterized_input = input | |
|
80 | parameterized_helper_method.parameterized_func = f | |
|
81 | return parameterized_helper_method | |
|
82 | ||
|
83 | return parameterized_helper | |
|
84 | ||
|
85 | ||
|
86 | def to_safe_name(s): | |
|
87 | return re.sub("[^a-zA-Z0-9_]", "", s) | |
|
88 | ||
|
89 | ||
|
90 | def parameterized_expand_helper(func_name, func, args): | |
|
91 | def parameterized_expand_helper_helper(self=()): | |
|
92 | if self != (): | |
|
93 | self = (self,) | |
|
94 | return func(*(self + args)) | |
|
95 | parameterized_expand_helper_helper.__name__ = func_name | |
|
96 | return parameterized_expand_helper_helper | |
|
97 | ||
|
98 | ||
|
99 | def parameterized_expand(input): | |
|
100 | """ A "brute force" method of parameterizing test cases. Creates new test | |
|
101 | cases and injects them into the namespace that the wrapped function | |
|
102 | is being defined in. Useful for parameterizing tests in subclasses | |
|
103 | of 'UnitTest', where Nose test generators don't work. | |
|
104 | ||
|
105 | >>> @parameterized.expand([("foo", 1, 2)]) | |
|
106 | ... def test_add1(name, input, expected): | |
|
107 | ... actual = add1(input) | |
|
108 | ... assert_equal(actual, expected) | |
|
109 | ... | |
|
110 | >>> locals() | |
|
111 | ... 'test_add1_foo_0': <function ...> ... | |
|
112 | >>> | |
|
113 | """ | |
|
114 | ||
|
115 | def parameterized_expand_wrapper(f): | |
|
116 | stack = inspect.stack() | |
|
117 | frame = stack[1] | |
|
118 | frame_locals = frame[0].f_locals | |
|
119 | ||
|
120 | base_name = f.__name__ | |
|
121 | for num, args in enumerate(input): | |
|
122 | name_suffix = "_%s" % (num,) | |
|
123 | if len(args) > 0 and isinstance(args[0], basestring): | |
|
124 | name_suffix += "_" + to_safe_name(args[0]) | |
|
125 | name = base_name + name_suffix | |
|
126 | new_func = parameterized_expand_helper(name, f, args) | |
|
127 | frame_locals[name] = new_func | |
|
128 | return nottest(f) | |
|
129 | return parameterized_expand_wrapper | |
|
130 | ||
|
131 | parameterized.expand = parameterized_expand | |
|
132 | ||
|
133 | ||
|
134 | def assert_contains(haystack, needle): | |
|
135 | if needle not in haystack: | |
|
136 | raise AssertionError("%r not in %r" % (needle, haystack)) | |
|
137 | ||
|
138 | ||
|
139 | def assert_not_contains(haystack, needle): | |
|
140 | if needle in haystack: | |
|
141 | raise AssertionError("%r in %r" % (needle, haystack)) | |
|
142 | ||
|
143 | ||
|
144 | def imported_from_test(): | |
|
145 | """ Returns true if it looks like this module is being imported by unittest | |
|
146 | or nose. """ | |
|
147 | import re | |
|
148 | import inspect | |
|
149 | nose_re = re.compile(r"\bnose\b") | |
|
150 | unittest_re = re.compile(r"\bunittest2?\b") | |
|
151 | for frame in inspect.stack(): | |
|
152 | file = frame[1] | |
|
153 | if nose_re.search(file) or unittest_re.search(file): | |
|
154 | return True | |
|
155 | return False | |
|
156 | ||
|
157 | ||
|
158 | def assert_raises(func, exc_type, str_contains=None, repr_contains=None): | |
|
159 | try: | |
|
160 | func() | |
|
161 | except exc_type as e: | |
|
162 | if str_contains is not None and str_contains not in str(e): | |
|
163 | raise AssertionError("%s raised, but %r does not contain %r" | |
|
164 | % (exc_type, str(e), str_contains)) | |
|
165 | if repr_contains is not None and repr_contains not in repr(e): | |
|
166 | raise AssertionError("%s raised, but %r does not contain %r" | |
|
167 | % (exc_type, repr(e), repr_contains)) | |
|
168 | return e | |
|
169 | else: | |
|
170 | raise AssertionError("%s not raised" % (exc_type,)) | |
|
171 | ||
|
172 | ||
|
173 | log_handler = None | |
|
174 | ||
|
175 | ||
|
176 | def setup_logging(): | |
|
177 | """ Configures a log handler which will capure log messages during a test. | |
|
178 | The ``logged_messages`` and ``assert_no_errors_logged`` functions can be | |
|
179 | used to make assertions about these logged messages. | |
|
180 | ||
|
181 | For example:: | |
|
182 | ||
|
183 | from ensi_common.testing import ( | |
|
184 | setup_logging, teardown_logging, assert_no_errors_logged, | |
|
185 | assert_logged, | |
|
186 | ) | |
|
187 | ||
|
188 | class TestWidget(object): | |
|
189 | def setup(self): | |
|
190 | setup_logging() | |
|
191 | ||
|
192 | def teardown(self): | |
|
193 | assert_no_errors_logged() | |
|
194 | teardown_logging() | |
|
195 | ||
|
196 | def test_that_will_fail(self): | |
|
197 | log.warning("this warning message will trigger a failure") | |
|
198 | ||
|
199 | def test_that_will_pass(self): | |
|
200 | log.info("but info messages are ok") | |
|
201 | assert_logged("info messages are ok") | |
|
202 | """ | |
|
203 | ||
|
204 | global log_handler | |
|
205 | if log_handler is not None: | |
|
206 | logging.getLogger().removeHandler(log_handler) | |
|
207 | log_handler = logging.handlers.BufferingHandler(1000) | |
|
208 | formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s") | |
|
209 | log_handler.setFormatter(formatter) | |
|
210 | logging.getLogger().addHandler(log_handler) | |
|
211 | ||
|
212 | ||
|
213 | def teardown_logging(): | |
|
214 | global log_handler | |
|
215 | if log_handler is not None: | |
|
216 | logging.getLogger().removeHandler(log_handler) | |
|
217 | log_handler = None | |
|
218 | ||
|
219 | ||
|
220 | def logged_messages(): | |
|
221 | assert log_handler, "setup_logging not called" | |
|
222 | return [(log_handler.format(record), record) for record in log_handler.buffer] | |
|
223 | ||
|
224 | ||
|
225 | def assert_no_errors_logged(): | |
|
226 | for _, record in logged_messages(): | |
|
227 | if record.levelno >= logging.WARNING: | |
|
228 | # Assume that the nose log capture plugin is being used, so it will | |
|
229 | # show the exception. | |
|
230 | raise AssertionError("an unexpected error was logged") | |
|
231 | ||
|
232 | ||
|
233 | def assert_logged(expected_msg_contents): | |
|
234 | for msg, _ in logged_messages(): | |
|
235 | if expected_msg_contents in msg: | |
|
236 | return | |
|
237 | raise AssertionError("no logged message contains %r" | |
|
238 | % (expected_msg_contents,)) |
General Comments 0
You need to be logged in to leave comments.
Login now