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