Merge lp:~mbp/bzr/597791-http-tests into lp:bzr
- 597791-http-tests
- Merge into bzr.dev
Status: | Merged |
---|---|
Approved by: | Vincent Ladeuil |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5483 |
Proposed branch: | lp:~mbp/bzr/597791-http-tests |
Merge into: | lp:bzr |
Diff against target: |
689 lines (+326/-100) 6 files modified
NEWS (+6/-0) bzrlib/tests/__init__.py (+14/-1) bzrlib/tests/scenarios.py (+61/-0) bzrlib/tests/test_http.py (+115/-89) bzrlib/tests/test_scenarios.py (+110/-0) doc/developers/testing.txt (+20/-10) |
To merge this branch: | bzr merge lp:~mbp/bzr/597791-http-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins (community) | Approve | ||
Vincent Ladeuil | Approve | ||
Review via email: mp+37941@code.launchpad.net |
Commit message
cleanup test_http to use scenarios; add load_tests_
Description of the change
TestCase classes can now just have a `variations` attribute to describe how they should be multiplied. This lets us entirely delete the load_tests function complained of in bug 597791 and to my mind it makes the code _much_ nicer by eliminating spooky action at a distance. Now each class says what kind of variation it wants, without needing to know all the specific values, which may be dynamically determined.
I tested this doesn't change the parameterization by running 'selftest --list' on this and trunk, and ignoring ordering they're the same.
It could be nice to move or copy this into testscenarios.
I haven't yet rolled this out to other modules but I expect in many cases we could delete their custom load_tests methods in favour of just using load_tests_
Martin Pool (mbp) wrote : | # |
Vincent Ladeuil (vila) wrote : | # |
Lovely result :)
I had a look at that strange assertionError and... it looks like an uncaught regression *long* ago.
Or even worse, this looks like a test that has *never* failed :-(
I'll look at it again tomorrow.
One question: why is it required to do:
load_tests = load_tests_
Can't that be triggered automatically if a 'scenarios' attribute is defined on the test class ?
It's true that load_tests is generally required when you want to parametrize but... is it still possible to define a load_tests function and call load_tests_
I can't put my finger on it as I write this but I somehow feel there is a hole somewhere... imbw, you may just have fill the hole instead...
Martin Pool (mbp) wrote : | # |
I added (obviously) the raise AssertionError() just to make it really obvious that the thing does indeed fail. My guess is that an error here is sent back to the client as an http 500 error, and it accepts that as a reasonable response.
Is load_tests needed? I think Robert added that as a semi-standard extension point where the test loader calls back in to our code. If we want the test suite to work with other runners, we can't hardcode a check for scenarios. Perhaps we can simplify it more. otoh explicit is better than implicit, and now it's only one line (plus an import...)
Robert Collins (lifeless) wrote : | # |
load_tests is in python 2.7 (with a slightly different signature); it would be nice to adapt to that.
Doing load_tests = load_tests_
Doing a global is fine to - you could simply use generate_scenarios in bzrlib.test_suite()
381 + # XXX: this is strange; the 'random' name below seems undefined and
382 + # yet the tests pass -- mbp 2010-10-11
383 + raise AssertionError()
Looks like you can't land it like this?
This looks lovely. I would love to see a patch with the missing bits put into testscenarios.
Martin Pool (mbp) wrote : | # |
> load_tests is in python 2.7 (with a slightly different signature); it would be
> nice to adapt to that.
Right, bug 607412.
> Doing load_tests = load_tests_
> say 'load_tests_
> not terrible.
>
> Doing a global is fine to - you could simply use generate_scenarios in
> bzrlib.test_suite()
>
>
> 381 + # XXX: this is strange; the 'random' name below seems
> undefined and
> 382 + # yet the tests pass -- mbp 2010-10-11
> 383 + raise AssertionError()
>
> Looks like you can't land it like this?
I can; the bug (bug 658773) was latent before I came here, and I just noticed it by running pyflakes.
>
> This looks lovely. I would love to see a patch with the missing bits put into
> testscenarios.
Right, I'll do that under bug 658044.
Martin Pool (mbp) wrote : | # |
sent to pqm by email
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2010-10-11 15:18:38 +0000 |
3 | +++ NEWS 2010-10-11 22:43:41 +0000 |
4 | @@ -158,6 +158,12 @@ |
5 | Testing |
6 | ******* |
7 | |
8 | +* Add a new simpler way to generate multiple test variations, by setting |
9 | + the `scenarios` attribute of a test class to a list of scenarios |
10 | + descriptions, then using `load_tests_apply_scenarios`. (See the testing |
11 | + guide and `bzrlib.tests.scenarios`.) Simplify `test_http` using this. |
12 | + (Martin Pool, #597791) |
13 | + |
14 | * Add ``tests/ssl_certs/ca.crt`` to the required test files list. Test |
15 | involving the pycurl https test server fail otherwise when running |
16 | selftest from an installed version. (Vincent Ladeuil, #651706) |
17 | |
18 | === modified file 'bzrlib/tests/__init__.py' |
19 | --- bzrlib/tests/__init__.py 2010-10-08 10:16:12 +0000 |
20 | +++ bzrlib/tests/__init__.py 2010-10-11 22:43:41 +0000 |
21 | @@ -3809,6 +3809,7 @@ |
22 | 'bzrlib.tests.test_rio', |
23 | 'bzrlib.tests.test_rules', |
24 | 'bzrlib.tests.test_sampler', |
25 | + 'bzrlib.tests.test_scenarios', |
26 | 'bzrlib.tests.test_script', |
27 | 'bzrlib.tests.test_selftest', |
28 | 'bzrlib.tests.test_serializer', |
29 | @@ -3991,7 +3992,19 @@ |
30 | return suite |
31 | |
32 | |
33 | -def multiply_scenarios(scenarios_left, scenarios_right): |
34 | +def multiply_scenarios(*scenarios): |
35 | + """Multiply two or more iterables of scenarios. |
36 | + |
37 | + It is safe to pass scenario generators or iterators. |
38 | + |
39 | + :returns: A list of compound scenarios: the cross-product of all |
40 | + scenarios, with the names concatenated and the parameters |
41 | + merged together. |
42 | + """ |
43 | + return reduce(_multiply_two_scenarios, map(list, scenarios)) |
44 | + |
45 | + |
46 | +def _multiply_two_scenarios(scenarios_left, scenarios_right): |
47 | """Multiply two sets of scenarios. |
48 | |
49 | :returns: the cartesian product of the two sets of scenarios, that is |
50 | |
51 | === added file 'bzrlib/tests/scenarios.py' |
52 | --- bzrlib/tests/scenarios.py 1970-01-01 00:00:00 +0000 |
53 | +++ bzrlib/tests/scenarios.py 2010-10-11 22:43:41 +0000 |
54 | @@ -0,0 +1,61 @@ |
55 | +# Copyright (C) 2005-2010 Canonical Ltd |
56 | +# |
57 | +# This program is free software; you can redistribute it and/or modify |
58 | +# it under the terms of the GNU General Public License as published by |
59 | +# the Free Software Foundation; either version 2 of the License, or |
60 | +# (at your option) any later version. |
61 | +# |
62 | +# This program is distributed in the hope that it will be useful, |
63 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
64 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
65 | +# GNU General Public License for more details. |
66 | +# |
67 | +# You should have received a copy of the GNU General Public License |
68 | +# along with this program; if not, write to the Free Software |
69 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
70 | + |
71 | + |
72 | +"""Generate multiple variations in different scenarios. |
73 | + |
74 | +For a class whose tests should be repeated in varying scenarios, set a |
75 | +`scenarios` member to a list of scenarios where it should be repeated. |
76 | + |
77 | +This is similar to the interface provided by |
78 | +<http://launchpad.net/testscenarios/>. |
79 | +""" |
80 | + |
81 | + |
82 | +from bzrlib.tests import ( |
83 | + iter_suite_tests, |
84 | + multiply_scenarios, |
85 | + multiply_tests, |
86 | + ) |
87 | + |
88 | + |
89 | +def load_tests_apply_scenarios(standard_tests, module, loader): |
90 | + """Multiply tests depending on their 'scenarios' attribute. |
91 | + |
92 | + This can be assigned to 'load_tests' in any test module to make this |
93 | + automatically work across tests in the module. |
94 | + """ |
95 | + result = loader.suiteClass() |
96 | + multiply_tests_by_their_scenarios(standard_tests, result) |
97 | + return result |
98 | + |
99 | + |
100 | +def multiply_tests_by_their_scenarios(some_tests, into_suite): |
101 | + """Multiply the tests in the given suite by their declared scenarios. |
102 | + |
103 | + Each test must have a 'scenarios' attribute which is a list of |
104 | + (name, params) pairs. |
105 | + |
106 | + :param some_tests: TestSuite or Test. |
107 | + :param into_suite: A TestSuite into which the resulting tests will be |
108 | + inserted. |
109 | + """ |
110 | + for test in iter_suite_tests(some_tests): |
111 | + scenarios = getattr(test, 'scenarios', None) |
112 | + if scenarios is None: |
113 | + into_suite.addTest(test) |
114 | + else: |
115 | + multiply_tests(test, test.scenarios, into_suite) |
116 | |
117 | === modified file 'bzrlib/tests/test_http.py' |
118 | --- bzrlib/tests/test_http.py 2010-08-24 13:03:18 +0000 |
119 | +++ bzrlib/tests/test_http.py 2010-10-11 22:43:41 +0000 |
120 | @@ -23,10 +23,7 @@ |
121 | # TODO: Should be renamed to bzrlib.transport.http.tests? |
122 | # TODO: What about renaming to bzrlib.tests.transport.http ? |
123 | |
124 | -from cStringIO import StringIO |
125 | import httplib |
126 | -import os |
127 | -import select |
128 | import SimpleHTTPServer |
129 | import socket |
130 | import sys |
131 | @@ -42,7 +39,6 @@ |
132 | tests, |
133 | transport, |
134 | ui, |
135 | - urlutils, |
136 | ) |
137 | from bzrlib.tests import ( |
138 | features, |
139 | @@ -50,6 +46,10 @@ |
140 | http_utils, |
141 | test_server, |
142 | ) |
143 | +from bzrlib.tests.scenarios import ( |
144 | + load_tests_apply_scenarios, |
145 | + multiply_scenarios, |
146 | + ) |
147 | from bzrlib.transport import ( |
148 | http, |
149 | remote, |
150 | @@ -64,17 +64,11 @@ |
151 | from bzrlib.transport.http._pycurl import PyCurlTransport |
152 | |
153 | |
154 | -def load_tests(standard_tests, module, loader): |
155 | - """Multiply tests for http clients and protocol versions.""" |
156 | - result = loader.suiteClass() |
157 | - |
158 | - # one for each transport implementation |
159 | - t_tests, remaining_tests = tests.split_suite_by_condition( |
160 | - standard_tests, tests.condition_isinstance(( |
161 | - TestHttpTransportRegistration, |
162 | - TestHttpTransportUrls, |
163 | - Test_redirected_to, |
164 | - ))) |
165 | +load_tests = load_tests_apply_scenarios |
166 | + |
167 | + |
168 | +def vary_by_http_client_implementation(): |
169 | + """Test the two libraries we can use, pycurl and urllib.""" |
170 | transport_scenarios = [ |
171 | ('urllib', dict(_transport=_urllib.HttpTransport_urllib, |
172 | _server=http_server.HttpServer_urllib, |
173 | @@ -85,85 +79,48 @@ |
174 | ('pycurl', dict(_transport=PyCurlTransport, |
175 | _server=http_server.HttpServer_PyCurl, |
176 | _url_protocol='http+pycurl',))) |
177 | - tests.multiply_tests(t_tests, transport_scenarios, result) |
178 | - |
179 | - protocol_scenarios = [ |
180 | - ('HTTP/1.0', dict(_protocol_version='HTTP/1.0')), |
181 | - ('HTTP/1.1', dict(_protocol_version='HTTP/1.1')), |
182 | - ] |
183 | - |
184 | - # some tests are parametrized by the protocol version only |
185 | - p_tests, remaining_tests = tests.split_suite_by_condition( |
186 | - remaining_tests, tests.condition_isinstance(( |
187 | - TestAuthOnRedirected, |
188 | - ))) |
189 | - tests.multiply_tests(p_tests, protocol_scenarios, result) |
190 | - |
191 | - # each implementation tested with each HTTP version |
192 | - tp_tests, remaining_tests = tests.split_suite_by_condition( |
193 | - remaining_tests, tests.condition_isinstance(( |
194 | - SmartHTTPTunnellingTest, |
195 | - TestDoCatchRedirections, |
196 | - TestHTTPConnections, |
197 | - TestHTTPRedirections, |
198 | - TestHTTPSilentRedirections, |
199 | - TestLimitedRangeRequestServer, |
200 | - TestPost, |
201 | - TestProxyHttpServer, |
202 | - TestRanges, |
203 | - TestSpecificRequestHandler, |
204 | - ))) |
205 | - tp_scenarios = tests.multiply_scenarios(transport_scenarios, |
206 | - protocol_scenarios) |
207 | - tests.multiply_tests(tp_tests, tp_scenarios, result) |
208 | - |
209 | - # proxy auth: each auth scheme on all http versions on all implementations. |
210 | - tppa_tests, remaining_tests = tests.split_suite_by_condition( |
211 | - remaining_tests, tests.condition_isinstance(( |
212 | - TestProxyAuth, |
213 | - ))) |
214 | - proxy_auth_scheme_scenarios = [ |
215 | + return transport_scenarios |
216 | + |
217 | + |
218 | +def vary_by_http_protocol_version(): |
219 | + """Test on http/1.0 and 1.1""" |
220 | + return [ |
221 | + ('HTTP/1.0', dict(_protocol_version='HTTP/1.0')), |
222 | + ('HTTP/1.1', dict(_protocol_version='HTTP/1.1')), |
223 | + ] |
224 | + |
225 | + |
226 | +def vary_by_http_proxy_auth_scheme(): |
227 | + return [ |
228 | ('basic', dict(_auth_server=http_utils.ProxyBasicAuthServer)), |
229 | ('digest', dict(_auth_server=http_utils.ProxyDigestAuthServer)), |
230 | ('basicdigest', |
231 | - dict(_auth_server=http_utils.ProxyBasicAndDigestAuthServer)), |
232 | + dict(_auth_server=http_utils.ProxyBasicAndDigestAuthServer)), |
233 | ] |
234 | - tppa_scenarios = tests.multiply_scenarios(tp_scenarios, |
235 | - proxy_auth_scheme_scenarios) |
236 | - tests.multiply_tests(tppa_tests, tppa_scenarios, result) |
237 | - |
238 | - # auth: each auth scheme on all http versions on all implementations. |
239 | - tpa_tests, remaining_tests = tests.split_suite_by_condition( |
240 | - remaining_tests, tests.condition_isinstance(( |
241 | - TestAuth, |
242 | - ))) |
243 | - auth_scheme_scenarios = [ |
244 | + |
245 | + |
246 | +def vary_by_http_auth_scheme(): |
247 | + return [ |
248 | ('basic', dict(_auth_server=http_utils.HTTPBasicAuthServer)), |
249 | ('digest', dict(_auth_server=http_utils.HTTPDigestAuthServer)), |
250 | ('basicdigest', |
251 | - dict(_auth_server=http_utils.HTTPBasicAndDigestAuthServer)), |
252 | + dict(_auth_server=http_utils.HTTPBasicAndDigestAuthServer)), |
253 | ] |
254 | - tpa_scenarios = tests.multiply_scenarios(tp_scenarios, |
255 | - auth_scheme_scenarios) |
256 | - tests.multiply_tests(tpa_tests, tpa_scenarios, result) |
257 | - |
258 | - # activity: on all http[s] versions on all implementations |
259 | - tpact_tests, remaining_tests = tests.split_suite_by_condition( |
260 | - remaining_tests, tests.condition_isinstance(( |
261 | - TestActivity, |
262 | - ))) |
263 | + |
264 | + |
265 | +def vary_by_http_activity(): |
266 | activity_scenarios = [ |
267 | ('urllib,http', dict(_activity_server=ActivityHTTPServer, |
268 | - _transport=_urllib.HttpTransport_urllib,)), |
269 | + _transport=_urllib.HttpTransport_urllib,)), |
270 | ] |
271 | if tests.HTTPSServerFeature.available(): |
272 | activity_scenarios.append( |
273 | ('urllib,https', dict(_activity_server=ActivityHTTPSServer, |
274 | - _transport=_urllib.HttpTransport_urllib,)),) |
275 | + _transport=_urllib.HttpTransport_urllib,)),) |
276 | if features.pycurl.available(): |
277 | activity_scenarios.append( |
278 | ('pycurl,http', dict(_activity_server=ActivityHTTPServer, |
279 | - _transport=PyCurlTransport,)),) |
280 | + _transport=PyCurlTransport,)),) |
281 | if tests.HTTPSServerFeature.available(): |
282 | from bzrlib.tests import ( |
283 | ssl_certs, |
284 | @@ -181,16 +138,8 @@ |
285 | |
286 | activity_scenarios.append( |
287 | ('pycurl,https', dict(_activity_server=ActivityHTTPSServer, |
288 | - _transport=HTTPS_pycurl_transport,)),) |
289 | - |
290 | - tpact_scenarios = tests.multiply_scenarios(activity_scenarios, |
291 | - protocol_scenarios) |
292 | - tests.multiply_tests(tpact_tests, tpact_scenarios, result) |
293 | - |
294 | - # No parametrization for the remaining tests |
295 | - result.addTests(remaining_tests) |
296 | - |
297 | - return result |
298 | + _transport=HTTPS_pycurl_transport,)),) |
299 | + return activity_scenarios |
300 | |
301 | |
302 | class FakeManager(object): |
303 | @@ -401,6 +350,8 @@ |
304 | class TestHttpTransportUrls(tests.TestCase): |
305 | """Test the http urls.""" |
306 | |
307 | + scenarios = vary_by_http_client_implementation() |
308 | + |
309 | def test_abs_url(self): |
310 | """Construction of absolute http URLs""" |
311 | t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/') |
312 | @@ -413,7 +364,7 @@ |
313 | |
314 | def test_invalid_http_urls(self): |
315 | """Trap invalid construction of urls""" |
316 | - t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/') |
317 | + self._transport('http://bazaar-vcs.org/bzr/bzr.dev/') |
318 | self.assertRaises(errors.InvalidURL, |
319 | self._transport, |
320 | 'http://http://bazaar-vcs.org/bzr/bzr.dev/') |
321 | @@ -475,6 +426,11 @@ |
322 | class TestHTTPConnections(http_utils.TestCaseWithWebserver): |
323 | """Test the http connections.""" |
324 | |
325 | + scenarios = multiply_scenarios( |
326 | + vary_by_http_client_implementation(), |
327 | + vary_by_http_protocol_version(), |
328 | + ) |
329 | + |
330 | def setUp(self): |
331 | http_utils.TestCaseWithWebserver.setUp(self) |
332 | self.build_tree(['foo/', 'foo/bar'], line_endings='binary', |
333 | @@ -525,6 +481,8 @@ |
334 | class TestHttpTransportRegistration(tests.TestCase): |
335 | """Test registrations of various http implementations""" |
336 | |
337 | + scenarios = vary_by_http_client_implementation() |
338 | + |
339 | def test_http_registered(self): |
340 | t = transport.get_transport('%s://foo.com/' % self._url_protocol) |
341 | self.assertIsInstance(t, transport.Transport) |
342 | @@ -533,6 +491,11 @@ |
343 | |
344 | class TestPost(tests.TestCase): |
345 | |
346 | + scenarios = multiply_scenarios( |
347 | + vary_by_http_client_implementation(), |
348 | + vary_by_http_protocol_version(), |
349 | + ) |
350 | + |
351 | def test_post_body_is_received(self): |
352 | server = RecordingServer(expect_body_tail='end-of-body', |
353 | scheme=self._url_protocol) |
354 | @@ -585,6 +548,11 @@ |
355 | Daughter classes are expected to override _req_handler_class |
356 | """ |
357 | |
358 | + scenarios = multiply_scenarios( |
359 | + vary_by_http_client_implementation(), |
360 | + vary_by_http_protocol_version(), |
361 | + ) |
362 | + |
363 | # Provide a useful default |
364 | _req_handler_class = http_server.TestingHTTPRequestHandler |
365 | |
366 | @@ -841,7 +809,7 @@ |
367 | t = self.get_readonly_transport() |
368 | # force transport to issue multiple requests |
369 | t._get_max_size = 2 |
370 | - l = list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4)))) |
371 | + list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4)))) |
372 | # The server should have issued 3 requests |
373 | self.assertEqual(3, server.GET_request_nb) |
374 | self.assertEqual('0123456789', t.get_bytes('a')) |
375 | @@ -924,6 +892,8 @@ |
376 | def get_multiple_ranges(self, file, file_size, ranges): |
377 | self.send_response(206) |
378 | self.send_header('Accept-Ranges', 'bytes') |
379 | + # XXX: this is strange; the 'random' name below seems undefined and |
380 | + # yet the tests pass -- mbp 2010-10-11 bug 658773 |
381 | boundary = "%d" % random.randint(0,0x7FFFFFFF) |
382 | self.send_header("Content-Type", |
383 | "multipart/byteranges; boundary=%s" % boundary) |
384 | @@ -1055,6 +1025,11 @@ |
385 | class TestLimitedRangeRequestServer(http_utils.TestCaseWithWebserver): |
386 | """Tests readv requests against a server erroring out on too much ranges.""" |
387 | |
388 | + scenarios = multiply_scenarios( |
389 | + vary_by_http_client_implementation(), |
390 | + vary_by_http_protocol_version(), |
391 | + ) |
392 | + |
393 | # Requests with more range specifiers will error out |
394 | range_limit = 3 |
395 | |
396 | @@ -1134,6 +1109,11 @@ |
397 | to the file names). |
398 | """ |
399 | |
400 | + scenarios = multiply_scenarios( |
401 | + vary_by_http_client_implementation(), |
402 | + vary_by_http_protocol_version(), |
403 | + ) |
404 | + |
405 | # FIXME: We don't have an https server available, so we don't |
406 | # test https connections. --vila toolongago |
407 | |
408 | @@ -1236,6 +1216,11 @@ |
409 | class TestRanges(http_utils.TestCaseWithWebserver): |
410 | """Test the Range header in GET methods.""" |
411 | |
412 | + scenarios = multiply_scenarios( |
413 | + vary_by_http_client_implementation(), |
414 | + vary_by_http_protocol_version(), |
415 | + ) |
416 | + |
417 | def setUp(self): |
418 | http_utils.TestCaseWithWebserver.setUp(self) |
419 | self.build_tree_contents([('a', '0123456789')],) |
420 | @@ -1281,6 +1266,11 @@ |
421 | class TestHTTPRedirections(http_utils.TestCaseWithRedirectedWebserver): |
422 | """Test redirection between http servers.""" |
423 | |
424 | + scenarios = multiply_scenarios( |
425 | + vary_by_http_client_implementation(), |
426 | + vary_by_http_protocol_version(), |
427 | + ) |
428 | + |
429 | def setUp(self): |
430 | super(TestHTTPRedirections, self).setUp() |
431 | self.build_tree_contents([('a', '0123456789'), |
432 | @@ -1349,6 +1339,11 @@ |
433 | -- vila 20070212 |
434 | """ |
435 | |
436 | + scenarios = multiply_scenarios( |
437 | + vary_by_http_client_implementation(), |
438 | + vary_by_http_protocol_version(), |
439 | + ) |
440 | + |
441 | def setUp(self): |
442 | if (features.pycurl.available() |
443 | and self._transport == PyCurlTransport): |
444 | @@ -1399,6 +1394,11 @@ |
445 | class TestDoCatchRedirections(http_utils.TestCaseWithRedirectedWebserver): |
446 | """Test transport.do_catching_redirections.""" |
447 | |
448 | + scenarios = multiply_scenarios( |
449 | + vary_by_http_client_implementation(), |
450 | + vary_by_http_protocol_version(), |
451 | + ) |
452 | + |
453 | def setUp(self): |
454 | super(TestDoCatchRedirections, self).setUp() |
455 | self.build_tree_contents([('a', '0123456789'),],) |
456 | @@ -1446,6 +1446,12 @@ |
457 | class TestAuth(http_utils.TestCaseWithWebserver): |
458 | """Test authentication scheme""" |
459 | |
460 | + scenarios = multiply_scenarios( |
461 | + vary_by_http_client_implementation(), |
462 | + vary_by_http_protocol_version(), |
463 | + vary_by_http_auth_scheme(), |
464 | + ) |
465 | + |
466 | _auth_header = 'Authorization' |
467 | _password_prompt_prefix = '' |
468 | _username_prompt_prefix = '' |
469 | @@ -1655,6 +1661,12 @@ |
470 | class TestProxyAuth(TestAuth): |
471 | """Test proxy authentication schemes.""" |
472 | |
473 | + scenarios = multiply_scenarios( |
474 | + vary_by_http_client_implementation(), |
475 | + vary_by_http_protocol_version(), |
476 | + vary_by_http_proxy_auth_scheme(), |
477 | + ) |
478 | + |
479 | _auth_header = 'Proxy-authorization' |
480 | _password_prompt_prefix = 'Proxy ' |
481 | _username_prompt_prefix = 'Proxy ' |
482 | @@ -1716,6 +1728,11 @@ |
483 | |
484 | class SmartHTTPTunnellingTest(tests.TestCaseWithTransport): |
485 | |
486 | + scenarios = multiply_scenarios( |
487 | + vary_by_http_client_implementation(), |
488 | + vary_by_http_protocol_version(), |
489 | + ) |
490 | + |
491 | def setUp(self): |
492 | super(SmartHTTPTunnellingTest, self).setUp() |
493 | # We use the VFS layer as part of HTTP tunnelling tests. |
494 | @@ -1810,6 +1827,8 @@ |
495 | |
496 | class Test_redirected_to(tests.TestCase): |
497 | |
498 | + scenarios = vary_by_http_client_implementation() |
499 | + |
500 | def test_redirected_to_subdir(self): |
501 | t = self._transport('http://www.example.com/foo') |
502 | r = t._redirected_to('http://www.example.com/foo', |
503 | @@ -2061,6 +2080,11 @@ |
504 | |
505 | class TestActivity(tests.TestCase, TestActivityMixin): |
506 | |
507 | + scenarios = multiply_scenarios( |
508 | + vary_by_http_activity(), |
509 | + vary_by_http_protocol_version(), |
510 | + ) |
511 | + |
512 | def setUp(self): |
513 | TestActivityMixin.setUp(self) |
514 | |
515 | @@ -2087,6 +2111,8 @@ |
516 | class TestAuthOnRedirected(http_utils.TestCaseWithRedirectedWebserver): |
517 | """Test authentication on the redirected http server.""" |
518 | |
519 | + scenarios = vary_by_http_protocol_version() |
520 | + |
521 | _auth_header = 'Authorization' |
522 | _password_prompt_prefix = '' |
523 | _username_prompt_prefix = '' |
524 | |
525 | === added file 'bzrlib/tests/test_scenarios.py' |
526 | --- bzrlib/tests/test_scenarios.py 1970-01-01 00:00:00 +0000 |
527 | +++ bzrlib/tests/test_scenarios.py 2010-10-11 22:43:41 +0000 |
528 | @@ -0,0 +1,110 @@ |
529 | +# Copyright (C) 2005-2010 Canonical Ltd |
530 | +# |
531 | +# This program is free software; you can redistribute it and/or modify |
532 | +# it under the terms of the GNU General Public License as published by |
533 | +# the Free Software Foundation; either version 2 of the License, or |
534 | +# (at your option) any later version. |
535 | +# |
536 | +# This program is distributed in the hope that it will be useful, |
537 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
538 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
539 | +# GNU General Public License for more details. |
540 | +# |
541 | +# You should have received a copy of the GNU General Public License |
542 | +# along with this program; if not, write to the Free Software |
543 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
544 | + |
545 | + |
546 | +"""Tests for generating multiple tests for scenarios.""" |
547 | + |
548 | +from bzrlib.tests import ( |
549 | + TestCase, |
550 | + TestLoader, |
551 | + iter_suite_tests, |
552 | + multiply_tests, |
553 | + ) |
554 | + |
555 | +from bzrlib.tests.scenarios import ( |
556 | + load_tests_apply_scenarios, |
557 | + multiply_scenarios, |
558 | + multiply_tests_by_their_scenarios, |
559 | + ) |
560 | + |
561 | + |
562 | +# There aren't any actually parameterized tests here, but this exists as a |
563 | +# demonstration; so that you can interactively observe them being multiplied; |
564 | +# and so that we check everything hooks up properly. |
565 | +load_tests = load_tests_apply_scenarios |
566 | + |
567 | + |
568 | +def vary_by_color(): |
569 | + """Very simple static variation example""" |
570 | + for color in ['red', 'green', 'blue']: |
571 | + yield (color, {'color': color}) |
572 | + |
573 | + |
574 | +def vary_named_attribute(attr_name): |
575 | + """More sophisticated: vary a named parameter""" |
576 | + yield ('a', {attr_name: 'a'}) |
577 | + yield ('b', {attr_name: 'b'}) |
578 | + |
579 | + |
580 | +def get_generated_test_attributes(suite, attr_name): |
581 | + """Return the `attr_name` attribute from all tests in the suite""" |
582 | + return sorted([ |
583 | + getattr(t, attr_name) for t in iter_suite_tests(suite)]) |
584 | + |
585 | + |
586 | +class TestTestScenarios(TestCase): |
587 | + |
588 | + def test_multiply_tests(self): |
589 | + loader = TestLoader() |
590 | + suite = loader.suiteClass() |
591 | + multiply_tests( |
592 | + self, |
593 | + vary_by_color(), |
594 | + suite) |
595 | + self.assertEquals( |
596 | + ['blue', 'green', 'red'], |
597 | + get_generated_test_attributes(suite, 'color')) |
598 | + |
599 | + def test_multiply_scenarios_from_generators(self): |
600 | + """It's safe to multiply scenarios that come from generators""" |
601 | + s = multiply_scenarios( |
602 | + vary_named_attribute('one'), |
603 | + vary_named_attribute('two'), |
604 | + ) |
605 | + self.assertEquals( |
606 | + 2*2, |
607 | + len(s), |
608 | + s) |
609 | + |
610 | + def test_multiply_tests_by_their_scenarios(self): |
611 | + loader = TestLoader() |
612 | + suite = loader.suiteClass() |
613 | + test_instance = PretendVaryingTest('test_nothing') |
614 | + multiply_tests_by_their_scenarios( |
615 | + test_instance, |
616 | + suite) |
617 | + self.assertEquals( |
618 | + ['a', 'a', 'b', 'b'], |
619 | + get_generated_test_attributes(suite, 'value')) |
620 | + |
621 | + def test_multiply_tests_no_scenarios(self): |
622 | + """Tests with no scenarios attribute aren't multiplied""" |
623 | + suite = TestLoader().suiteClass() |
624 | + multiply_tests_by_their_scenarios(self, |
625 | + suite) |
626 | + self.assertLength(1, list(iter_suite_tests(suite))) |
627 | + |
628 | + |
629 | +class PretendVaryingTest(TestCase): |
630 | + |
631 | + scenarios = multiply_scenarios( |
632 | + vary_named_attribute('value'), |
633 | + vary_named_attribute('other'), |
634 | + ) |
635 | + |
636 | + def test_nothing(self): |
637 | + """This test exists just so it can be multiplied""" |
638 | + pass |
639 | |
640 | === modified file 'doc/developers/testing.txt' |
641 | --- doc/developers/testing.txt 2010-10-07 07:51:54 +0000 |
642 | +++ doc/developers/testing.txt 2010-10-11 22:43:41 +0000 |
643 | @@ -811,17 +811,11 @@ |
644 | whether a test should be added for that particular implementation, |
645 | or for all implementations of the interface. |
646 | |
647 | -The multiplication of tests for different implementations is normally |
648 | -accomplished by overriding the ``load_tests`` function used to load tests |
649 | -from a module. This function typically loads all the tests, then applies |
650 | -a TestProviderAdapter to them, which generates a longer suite containing |
651 | -all the test variations. |
652 | - |
653 | See also `Per-implementation tests`_ (above). |
654 | |
655 | |
656 | -Test scenarios |
657 | --------------- |
658 | +Test scenarios and variations |
659 | +----------------------------- |
660 | |
661 | Some utilities are provided for generating variations of tests. This can |
662 | be used for per-implementation tests, or other cases where the same test |
663 | @@ -832,8 +826,24 @@ |
664 | values to which the test should be applied. The test suite should then |
665 | also provide a list of scenarios in which to run the tests. |
666 | |
667 | -Typically ``multiply_tests_from_modules`` should be called from the test |
668 | -module's ``load_tests`` function. |
669 | +A single *scenario* is defined by a `(name, parameter_dict)` tuple. The |
670 | +short string name is combined with the name of the test method to form the |
671 | +test instance name. The parameter dict is merged into the instance's |
672 | +attributes. |
673 | + |
674 | +For example:: |
675 | + |
676 | + load_tests = load_tests_apply_scenarios |
677 | + |
678 | + class TestCheckout(TestCase): |
679 | + |
680 | + variations = multiply_scenarios( |
681 | + VaryByRepositoryFormat(), |
682 | + VaryByTreeFormat(), |
683 | + ) |
684 | + |
685 | +The `load_tests` declaration or definition should be near the top of the |
686 | +file so its effect can be seen. |
687 | |
688 | |
689 | Test support |
After some discussion with Robert and bug 658044, I think I can make this converge a bit more with what's already done in testscenarios:
* name the attribute 'scenarios'
* make it an iterable of scenarios directly, rather than a list of TestVariations to multiply
* generate the cross product by calling a function rather than implicitly; to work well with scenario generators that come from registries this function should be quite lazy
* change the variations to generators or functions rather than classes
Also stylistically
* clearer tests
* remove extra VWS
* module docstring