Rev 5483: (mbp) cleanup test_http to use scenarios; in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Tue Oct 12 00:21:25 BST 2010
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 5483 [merge]
revision-id: pqm at pqm.ubuntu.com-20101011232122-g2ejz5grpjes5brf
parent: pqm at pqm.ubuntu.com-20101011155334-d7g4ro1ofnjiyfbd
parent: mbp at sourcefrog.net-20101011224023-3pz8wxn2dz7gm6do
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Tue 2010-10-12 00:21:22 +0100
message:
(mbp) cleanup test_http to use scenarios;
add load_tests_from_scenarios (Martin Pool)
added:
bzrlib/tests/scenarios.py variations.py-20101008080748-qpn38ldpyvzvkrhg-1
bzrlib/tests/test_scenarios.py test_variations.py-20101008084845-jy33m3zbh2ck1ka1-1
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/tests/__init__.py selftest.py-20050531073622-8d0e3c8845c97a64
bzrlib/tests/test_http.py testhttp.py-20051018020158-b2eef6e867c514d9
doc/developers/testing.txt testing.txt-20080812140359-i70zzh6v2z7grqex-1
=== modified file 'NEWS'
--- a/NEWS 2010-10-11 15:18:38 +0000
+++ b/NEWS 2010-10-11 23:21:22 +0000
@@ -158,6 +158,12 @@
Testing
*******
+* Add a new simpler way to generate multiple test variations, by setting
+ the `scenarios` attribute of a test class to a list of scenarios
+ descriptions, then using `load_tests_apply_scenarios`. (See the testing
+ guide and `bzrlib.tests.scenarios`.) Simplify `test_http` using this.
+ (Martin Pool, #597791)
+
* Add ``tests/ssl_certs/ca.crt`` to the required test files list. Test
involving the pycurl https test server fail otherwise when running
selftest from an installed version. (Vincent Ladeuil, #651706)
=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py 2010-10-08 10:16:12 +0000
+++ b/bzrlib/tests/__init__.py 2010-10-11 22:36:23 +0000
@@ -3809,6 +3809,7 @@
'bzrlib.tests.test_rio',
'bzrlib.tests.test_rules',
'bzrlib.tests.test_sampler',
+ 'bzrlib.tests.test_scenarios',
'bzrlib.tests.test_script',
'bzrlib.tests.test_selftest',
'bzrlib.tests.test_serializer',
@@ -3991,7 +3992,19 @@
return suite
-def multiply_scenarios(scenarios_left, scenarios_right):
+def multiply_scenarios(*scenarios):
+ """Multiply two or more iterables of scenarios.
+
+ It is safe to pass scenario generators or iterators.
+
+ :returns: A list of compound scenarios: the cross-product of all
+ scenarios, with the names concatenated and the parameters
+ merged together.
+ """
+ return reduce(_multiply_two_scenarios, map(list, scenarios))
+
+
+def _multiply_two_scenarios(scenarios_left, scenarios_right):
"""Multiply two sets of scenarios.
:returns: the cartesian product of the two sets of scenarios, that is
=== added file 'bzrlib/tests/scenarios.py'
--- a/bzrlib/tests/scenarios.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/scenarios.py 2010-10-11 22:40:23 +0000
@@ -0,0 +1,61 @@
+# Copyright (C) 2005-2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+"""Generate multiple variations in different scenarios.
+
+For a class whose tests should be repeated in varying scenarios, set a
+`scenarios` member to a list of scenarios where it should be repeated.
+
+This is similar to the interface provided by
+<http://launchpad.net/testscenarios/>.
+"""
+
+
+from bzrlib.tests import (
+ iter_suite_tests,
+ multiply_scenarios,
+ multiply_tests,
+ )
+
+
+def load_tests_apply_scenarios(standard_tests, module, loader):
+ """Multiply tests depending on their 'scenarios' attribute.
+
+ This can be assigned to 'load_tests' in any test module to make this
+ automatically work across tests in the module.
+ """
+ result = loader.suiteClass()
+ multiply_tests_by_their_scenarios(standard_tests, result)
+ return result
+
+
+def multiply_tests_by_their_scenarios(some_tests, into_suite):
+ """Multiply the tests in the given suite by their declared scenarios.
+
+ Each test must have a 'scenarios' attribute which is a list of
+ (name, params) pairs.
+
+ :param some_tests: TestSuite or Test.
+ :param into_suite: A TestSuite into which the resulting tests will be
+ inserted.
+ """
+ for test in iter_suite_tests(some_tests):
+ scenarios = getattr(test, 'scenarios', None)
+ if scenarios is None:
+ into_suite.addTest(test)
+ else:
+ multiply_tests(test, test.scenarios, into_suite)
=== modified file 'bzrlib/tests/test_http.py'
--- a/bzrlib/tests/test_http.py 2010-08-24 13:03:18 +0000
+++ b/bzrlib/tests/test_http.py 2010-10-11 22:40:23 +0000
@@ -23,10 +23,7 @@
# TODO: Should be renamed to bzrlib.transport.http.tests?
# TODO: What about renaming to bzrlib.tests.transport.http ?
-from cStringIO import StringIO
import httplib
-import os
-import select
import SimpleHTTPServer
import socket
import sys
@@ -42,7 +39,6 @@
tests,
transport,
ui,
- urlutils,
)
from bzrlib.tests import (
features,
@@ -50,6 +46,10 @@
http_utils,
test_server,
)
+from bzrlib.tests.scenarios import (
+ load_tests_apply_scenarios,
+ multiply_scenarios,
+ )
from bzrlib.transport import (
http,
remote,
@@ -64,17 +64,11 @@
from bzrlib.transport.http._pycurl import PyCurlTransport
-def load_tests(standard_tests, module, loader):
- """Multiply tests for http clients and protocol versions."""
- result = loader.suiteClass()
-
- # one for each transport implementation
- t_tests, remaining_tests = tests.split_suite_by_condition(
- standard_tests, tests.condition_isinstance((
- TestHttpTransportRegistration,
- TestHttpTransportUrls,
- Test_redirected_to,
- )))
+load_tests = load_tests_apply_scenarios
+
+
+def vary_by_http_client_implementation():
+ """Test the two libraries we can use, pycurl and urllib."""
transport_scenarios = [
('urllib', dict(_transport=_urllib.HttpTransport_urllib,
_server=http_server.HttpServer_urllib,
@@ -85,85 +79,48 @@
('pycurl', dict(_transport=PyCurlTransport,
_server=http_server.HttpServer_PyCurl,
_url_protocol='http+pycurl',)))
- tests.multiply_tests(t_tests, transport_scenarios, result)
-
- protocol_scenarios = [
- ('HTTP/1.0', dict(_protocol_version='HTTP/1.0')),
- ('HTTP/1.1', dict(_protocol_version='HTTP/1.1')),
- ]
-
- # some tests are parametrized by the protocol version only
- p_tests, remaining_tests = tests.split_suite_by_condition(
- remaining_tests, tests.condition_isinstance((
- TestAuthOnRedirected,
- )))
- tests.multiply_tests(p_tests, protocol_scenarios, result)
-
- # each implementation tested with each HTTP version
- tp_tests, remaining_tests = tests.split_suite_by_condition(
- remaining_tests, tests.condition_isinstance((
- SmartHTTPTunnellingTest,
- TestDoCatchRedirections,
- TestHTTPConnections,
- TestHTTPRedirections,
- TestHTTPSilentRedirections,
- TestLimitedRangeRequestServer,
- TestPost,
- TestProxyHttpServer,
- TestRanges,
- TestSpecificRequestHandler,
- )))
- tp_scenarios = tests.multiply_scenarios(transport_scenarios,
- protocol_scenarios)
- tests.multiply_tests(tp_tests, tp_scenarios, result)
-
- # proxy auth: each auth scheme on all http versions on all implementations.
- tppa_tests, remaining_tests = tests.split_suite_by_condition(
- remaining_tests, tests.condition_isinstance((
- TestProxyAuth,
- )))
- proxy_auth_scheme_scenarios = [
+ return transport_scenarios
+
+
+def vary_by_http_protocol_version():
+ """Test on http/1.0 and 1.1"""
+ return [
+ ('HTTP/1.0', dict(_protocol_version='HTTP/1.0')),
+ ('HTTP/1.1', dict(_protocol_version='HTTP/1.1')),
+ ]
+
+
+def vary_by_http_proxy_auth_scheme():
+ return [
('basic', dict(_auth_server=http_utils.ProxyBasicAuthServer)),
('digest', dict(_auth_server=http_utils.ProxyDigestAuthServer)),
('basicdigest',
- dict(_auth_server=http_utils.ProxyBasicAndDigestAuthServer)),
+ dict(_auth_server=http_utils.ProxyBasicAndDigestAuthServer)),
]
- tppa_scenarios = tests.multiply_scenarios(tp_scenarios,
- proxy_auth_scheme_scenarios)
- tests.multiply_tests(tppa_tests, tppa_scenarios, result)
-
- # auth: each auth scheme on all http versions on all implementations.
- tpa_tests, remaining_tests = tests.split_suite_by_condition(
- remaining_tests, tests.condition_isinstance((
- TestAuth,
- )))
- auth_scheme_scenarios = [
+
+
+def vary_by_http_auth_scheme():
+ return [
('basic', dict(_auth_server=http_utils.HTTPBasicAuthServer)),
('digest', dict(_auth_server=http_utils.HTTPDigestAuthServer)),
('basicdigest',
- dict(_auth_server=http_utils.HTTPBasicAndDigestAuthServer)),
+ dict(_auth_server=http_utils.HTTPBasicAndDigestAuthServer)),
]
- tpa_scenarios = tests.multiply_scenarios(tp_scenarios,
- auth_scheme_scenarios)
- tests.multiply_tests(tpa_tests, tpa_scenarios, result)
-
- # activity: on all http[s] versions on all implementations
- tpact_tests, remaining_tests = tests.split_suite_by_condition(
- remaining_tests, tests.condition_isinstance((
- TestActivity,
- )))
+
+
+def vary_by_http_activity():
activity_scenarios = [
('urllib,http', dict(_activity_server=ActivityHTTPServer,
- _transport=_urllib.HttpTransport_urllib,)),
+ _transport=_urllib.HttpTransport_urllib,)),
]
if tests.HTTPSServerFeature.available():
activity_scenarios.append(
('urllib,https', dict(_activity_server=ActivityHTTPSServer,
- _transport=_urllib.HttpTransport_urllib,)),)
+ _transport=_urllib.HttpTransport_urllib,)),)
if features.pycurl.available():
activity_scenarios.append(
('pycurl,http', dict(_activity_server=ActivityHTTPServer,
- _transport=PyCurlTransport,)),)
+ _transport=PyCurlTransport,)),)
if tests.HTTPSServerFeature.available():
from bzrlib.tests import (
ssl_certs,
@@ -181,16 +138,8 @@
activity_scenarios.append(
('pycurl,https', dict(_activity_server=ActivityHTTPSServer,
- _transport=HTTPS_pycurl_transport,)),)
-
- tpact_scenarios = tests.multiply_scenarios(activity_scenarios,
- protocol_scenarios)
- tests.multiply_tests(tpact_tests, tpact_scenarios, result)
-
- # No parametrization for the remaining tests
- result.addTests(remaining_tests)
-
- return result
+ _transport=HTTPS_pycurl_transport,)),)
+ return activity_scenarios
class FakeManager(object):
@@ -401,6 +350,8 @@
class TestHttpTransportUrls(tests.TestCase):
"""Test the http urls."""
+ scenarios = vary_by_http_client_implementation()
+
def test_abs_url(self):
"""Construction of absolute http URLs"""
t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/')
@@ -413,7 +364,7 @@
def test_invalid_http_urls(self):
"""Trap invalid construction of urls"""
- t = self._transport('http://bazaar-vcs.org/bzr/bzr.dev/')
+ self._transport('http://bazaar-vcs.org/bzr/bzr.dev/')
self.assertRaises(errors.InvalidURL,
self._transport,
'http://http://bazaar-vcs.org/bzr/bzr.dev/')
@@ -475,6 +426,11 @@
class TestHTTPConnections(http_utils.TestCaseWithWebserver):
"""Test the http connections."""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
def setUp(self):
http_utils.TestCaseWithWebserver.setUp(self)
self.build_tree(['foo/', 'foo/bar'], line_endings='binary',
@@ -525,6 +481,8 @@
class TestHttpTransportRegistration(tests.TestCase):
"""Test registrations of various http implementations"""
+ scenarios = vary_by_http_client_implementation()
+
def test_http_registered(self):
t = transport.get_transport('%s://foo.com/' % self._url_protocol)
self.assertIsInstance(t, transport.Transport)
@@ -533,6 +491,11 @@
class TestPost(tests.TestCase):
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
def test_post_body_is_received(self):
server = RecordingServer(expect_body_tail='end-of-body',
scheme=self._url_protocol)
@@ -585,6 +548,11 @@
Daughter classes are expected to override _req_handler_class
"""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
# Provide a useful default
_req_handler_class = http_server.TestingHTTPRequestHandler
@@ -841,7 +809,7 @@
t = self.get_readonly_transport()
# force transport to issue multiple requests
t._get_max_size = 2
- l = list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
+ list(t.readv('a', ((0, 1), (1, 1), (2, 4), (6, 4))))
# The server should have issued 3 requests
self.assertEqual(3, server.GET_request_nb)
self.assertEqual('0123456789', t.get_bytes('a'))
@@ -924,6 +892,8 @@
def get_multiple_ranges(self, file, file_size, ranges):
self.send_response(206)
self.send_header('Accept-Ranges', 'bytes')
+ # XXX: this is strange; the 'random' name below seems undefined and
+ # yet the tests pass -- mbp 2010-10-11 bug 658773
boundary = "%d" % random.randint(0,0x7FFFFFFF)
self.send_header("Content-Type",
"multipart/byteranges; boundary=%s" % boundary)
@@ -1055,6 +1025,11 @@
class TestLimitedRangeRequestServer(http_utils.TestCaseWithWebserver):
"""Tests readv requests against a server erroring out on too much ranges."""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
# Requests with more range specifiers will error out
range_limit = 3
@@ -1134,6 +1109,11 @@
to the file names).
"""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
# FIXME: We don't have an https server available, so we don't
# test https connections. --vila toolongago
@@ -1236,6 +1216,11 @@
class TestRanges(http_utils.TestCaseWithWebserver):
"""Test the Range header in GET methods."""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
def setUp(self):
http_utils.TestCaseWithWebserver.setUp(self)
self.build_tree_contents([('a', '0123456789')],)
@@ -1281,6 +1266,11 @@
class TestHTTPRedirections(http_utils.TestCaseWithRedirectedWebserver):
"""Test redirection between http servers."""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
def setUp(self):
super(TestHTTPRedirections, self).setUp()
self.build_tree_contents([('a', '0123456789'),
@@ -1349,6 +1339,11 @@
-- vila 20070212
"""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
def setUp(self):
if (features.pycurl.available()
and self._transport == PyCurlTransport):
@@ -1399,6 +1394,11 @@
class TestDoCatchRedirections(http_utils.TestCaseWithRedirectedWebserver):
"""Test transport.do_catching_redirections."""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
def setUp(self):
super(TestDoCatchRedirections, self).setUp()
self.build_tree_contents([('a', '0123456789'),],)
@@ -1446,6 +1446,12 @@
class TestAuth(http_utils.TestCaseWithWebserver):
"""Test authentication scheme"""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ vary_by_http_auth_scheme(),
+ )
+
_auth_header = 'Authorization'
_password_prompt_prefix = ''
_username_prompt_prefix = ''
@@ -1655,6 +1661,12 @@
class TestProxyAuth(TestAuth):
"""Test proxy authentication schemes."""
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ vary_by_http_proxy_auth_scheme(),
+ )
+
_auth_header = 'Proxy-authorization'
_password_prompt_prefix = 'Proxy '
_username_prompt_prefix = 'Proxy '
@@ -1716,6 +1728,11 @@
class SmartHTTPTunnellingTest(tests.TestCaseWithTransport):
+ scenarios = multiply_scenarios(
+ vary_by_http_client_implementation(),
+ vary_by_http_protocol_version(),
+ )
+
def setUp(self):
super(SmartHTTPTunnellingTest, self).setUp()
# We use the VFS layer as part of HTTP tunnelling tests.
@@ -1810,6 +1827,8 @@
class Test_redirected_to(tests.TestCase):
+ scenarios = vary_by_http_client_implementation()
+
def test_redirected_to_subdir(self):
t = self._transport('http://www.example.com/foo')
r = t._redirected_to('http://www.example.com/foo',
@@ -2061,6 +2080,11 @@
class TestActivity(tests.TestCase, TestActivityMixin):
+ scenarios = multiply_scenarios(
+ vary_by_http_activity(),
+ vary_by_http_protocol_version(),
+ )
+
def setUp(self):
TestActivityMixin.setUp(self)
@@ -2087,6 +2111,8 @@
class TestAuthOnRedirected(http_utils.TestCaseWithRedirectedWebserver):
"""Test authentication on the redirected http server."""
+ scenarios = vary_by_http_protocol_version()
+
_auth_header = 'Authorization'
_password_prompt_prefix = ''
_username_prompt_prefix = ''
=== added file 'bzrlib/tests/test_scenarios.py'
--- a/bzrlib/tests/test_scenarios.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/test_scenarios.py 2010-10-11 22:40:23 +0000
@@ -0,0 +1,110 @@
+# Copyright (C) 2005-2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+"""Tests for generating multiple tests for scenarios."""
+
+from bzrlib.tests import (
+ TestCase,
+ TestLoader,
+ iter_suite_tests,
+ multiply_tests,
+ )
+
+from bzrlib.tests.scenarios import (
+ load_tests_apply_scenarios,
+ multiply_scenarios,
+ multiply_tests_by_their_scenarios,
+ )
+
+
+# There aren't any actually parameterized tests here, but this exists as a
+# demonstration; so that you can interactively observe them being multiplied;
+# and so that we check everything hooks up properly.
+load_tests = load_tests_apply_scenarios
+
+
+def vary_by_color():
+ """Very simple static variation example"""
+ for color in ['red', 'green', 'blue']:
+ yield (color, {'color': color})
+
+
+def vary_named_attribute(attr_name):
+ """More sophisticated: vary a named parameter"""
+ yield ('a', {attr_name: 'a'})
+ yield ('b', {attr_name: 'b'})
+
+
+def get_generated_test_attributes(suite, attr_name):
+ """Return the `attr_name` attribute from all tests in the suite"""
+ return sorted([
+ getattr(t, attr_name) for t in iter_suite_tests(suite)])
+
+
+class TestTestScenarios(TestCase):
+
+ def test_multiply_tests(self):
+ loader = TestLoader()
+ suite = loader.suiteClass()
+ multiply_tests(
+ self,
+ vary_by_color(),
+ suite)
+ self.assertEquals(
+ ['blue', 'green', 'red'],
+ get_generated_test_attributes(suite, 'color'))
+
+ def test_multiply_scenarios_from_generators(self):
+ """It's safe to multiply scenarios that come from generators"""
+ s = multiply_scenarios(
+ vary_named_attribute('one'),
+ vary_named_attribute('two'),
+ )
+ self.assertEquals(
+ 2*2,
+ len(s),
+ s)
+
+ def test_multiply_tests_by_their_scenarios(self):
+ loader = TestLoader()
+ suite = loader.suiteClass()
+ test_instance = PretendVaryingTest('test_nothing')
+ multiply_tests_by_their_scenarios(
+ test_instance,
+ suite)
+ self.assertEquals(
+ ['a', 'a', 'b', 'b'],
+ get_generated_test_attributes(suite, 'value'))
+
+ def test_multiply_tests_no_scenarios(self):
+ """Tests with no scenarios attribute aren't multiplied"""
+ suite = TestLoader().suiteClass()
+ multiply_tests_by_their_scenarios(self,
+ suite)
+ self.assertLength(1, list(iter_suite_tests(suite)))
+
+
+class PretendVaryingTest(TestCase):
+
+ scenarios = multiply_scenarios(
+ vary_named_attribute('value'),
+ vary_named_attribute('other'),
+ )
+
+ def test_nothing(self):
+ """This test exists just so it can be multiplied"""
+ pass
=== modified file 'doc/developers/testing.txt'
--- a/doc/developers/testing.txt 2010-10-07 07:51:54 +0000
+++ b/doc/developers/testing.txt 2010-10-11 22:40:23 +0000
@@ -811,17 +811,11 @@
whether a test should be added for that particular implementation,
or for all implementations of the interface.
-The multiplication of tests for different implementations is normally
-accomplished by overriding the ``load_tests`` function used to load tests
-from a module. This function typically loads all the tests, then applies
-a TestProviderAdapter to them, which generates a longer suite containing
-all the test variations.
-
See also `Per-implementation tests`_ (above).
-Test scenarios
---------------
+Test scenarios and variations
+-----------------------------
Some utilities are provided for generating variations of tests. This can
be used for per-implementation tests, or other cases where the same test
@@ -832,8 +826,24 @@
values to which the test should be applied. The test suite should then
also provide a list of scenarios in which to run the tests.
-Typically ``multiply_tests_from_modules`` should be called from the test
-module's ``load_tests`` function.
+A single *scenario* is defined by a `(name, parameter_dict)` tuple. The
+short string name is combined with the name of the test method to form the
+test instance name. The parameter dict is merged into the instance's
+attributes.
+
+For example::
+
+ load_tests = load_tests_apply_scenarios
+
+ class TestCheckout(TestCase):
+
+ variations = multiply_scenarios(
+ VaryByRepositoryFormat(),
+ VaryByTreeFormat(),
+ )
+
+The `load_tests` declaration or definition should be near the top of the
+file so its effect can be seen.
Test support
More information about the bazaar-commits
mailing list