Skip to content

Commit 24bbca2

Browse files
gh-135120: Add test.support.subTests()
1 parent 3612d8f commit 24bbca2

File tree

6 files changed

+318
-365
lines changed

6 files changed

+318
-365
lines changed

Lib/test/support/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,27 @@ def check_sizeof(test, o, size):
945945
% (type(o), result, size)
946946
test.assertEqual(result, size, msg)
947947

948+
def subTests(arg_names, arg_values, /, *, _do_cleanups=False):
949+
"""Run multiple subtests with different parameters.
950+
"""
951+
if isinstance(arg_names, str):
952+
arg_names = arg_names.split(',')
953+
def decorator(func):
954+
if isinstance(func, type):
955+
raise TypeError('subTests() can only decorate methods, not classes')
956+
@functools.wraps(func)
957+
def wrapper(self, /, *args, **kwargs):
958+
for values in arg_values:
959+
if len(arg_names) == 1:
960+
values = values,
961+
subtest_kwargs = dict(zip(arg_names, values))
962+
with self.subTest(**subtest_kwargs):
963+
func(self, *args, **kwargs, **subtest_kwargs)
964+
if _do_cleanups:
965+
self.doCleanup()
966+
return wrapper
967+
return decorator
968+
948969
#=======================================================================
949970
# Decorator/context manager for running a code in a different locale,
950971
# correctly resetting it afterwards.

Lib/test/test_http_cookiejar.py

Lines changed: 88 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import stat
55
import sys
66
import re
7+
from test import support
78
from test.support import os_helper
89
from test.support import warnings_helper
910
import time
@@ -105,8 +106,7 @@ def test_http2time_formats(self):
105106
self.assertEqual(http2time(s.lower()), test_t, s.lower())
106107
self.assertEqual(http2time(s.upper()), test_t, s.upper())
107108

108-
def test_http2time_garbage(self):
109-
for test in [
109+
@support.subTests('test', [
110110
'',
111111
'Garbage',
112112
'Mandag 16. September 1996',
@@ -121,10 +121,9 @@ def test_http2time_garbage(self):
121121
'08-01-3697739',
122122
'09 Feb 19942632 22:23:32 GMT',
123123
'Wed, 09 Feb 1994834 22:23:32 GMT',
124-
]:
125-
self.assertIsNone(http2time(test),
126-
"http2time(%s) is not None\n"
127-
"http2time(test) %s" % (test, http2time(test)))
124+
])
125+
def test_http2time_garbage(self, test):
126+
self.assertIsNone(http2time(test))
128127

129128
def test_http2time_redos_regression_actually_completes(self):
130129
# LOOSE_HTTP_DATE_RE was vulnerable to malicious input which caused catastrophic backtracking (REDoS).
@@ -149,9 +148,7 @@ def parse_date(text):
149148
self.assertEqual(parse_date("1994-02-03 19:45:29 +0530"),
150149
(1994, 2, 3, 14, 15, 29))
151150

152-
def test_iso2time_formats(self):
153-
# test iso2time for supported dates.
154-
tests = [
151+
@support.subTests('s', [
155152
'1994-02-03 00:00:00 -0000', # ISO 8601 format
156153
'1994-02-03 00:00:00 +0000', # ISO 8601 format
157154
'1994-02-03 00:00:00', # zone is optional
@@ -164,16 +161,15 @@ def test_iso2time_formats(self):
164161
# A few tests with extra space at various places
165162
' 1994-02-03 ',
166163
' 1994-02-03T00:00:00 ',
167-
]
168-
164+
])
165+
def test_iso2time_formats(self, s):
166+
# test iso2time for supported dates.
169167
test_t = 760233600 # assume broken POSIX counting of seconds
170-
for s in tests:
171-
self.assertEqual(iso2time(s), test_t, s)
172-
self.assertEqual(iso2time(s.lower()), test_t, s.lower())
173-
self.assertEqual(iso2time(s.upper()), test_t, s.upper())
168+
self.assertEqual(iso2time(s), test_t, s)
169+
self.assertEqual(iso2time(s.lower()), test_t, s.lower())
170+
self.assertEqual(iso2time(s.upper()), test_t, s.upper())
174171

175-
def test_iso2time_garbage(self):
176-
for test in [
172+
@support.subTests('test', [
177173
'',
178174
'Garbage',
179175
'Thursday, 03-Feb-94 00:00:00 GMT',
@@ -186,9 +182,9 @@ def test_iso2time_garbage(self):
186182
'01-01-1980 00:00:62',
187183
'01-01-1980T00:00:62',
188184
'19800101T250000Z',
189-
]:
190-
self.assertIsNone(iso2time(test),
191-
"iso2time(%r)" % test)
185+
])
186+
def test_iso2time_garbage(self, test):
187+
self.assertIsNone(iso2time(test))
192188

193189
def test_iso2time_performance_regression(self):
194190
# If ISO_DATE_RE regresses to quadratic complexity, this test will take a very long time to succeed.
@@ -199,24 +195,23 @@ def test_iso2time_performance_regression(self):
199195

200196
class HeaderTests(unittest.TestCase):
201197

202-
def test_parse_ns_headers(self):
203-
# quotes should be stripped
204-
expected = [[('foo', 'bar'), ('expires', 2209069412), ('version', '0')]]
205-
for hdr in [
198+
@support.subTests('hdr', [
206199
'foo=bar; expires=01 Jan 2040 22:23:32 GMT',
207200
'foo=bar; expires="01 Jan 2040 22:23:32 GMT"',
208-
]:
209-
self.assertEqual(parse_ns_headers([hdr]), expected)
210-
211-
def test_parse_ns_headers_version(self):
212-
201+
])
202+
def test_parse_ns_headers(self, hdr):
213203
# quotes should be stripped
214-
expected = [[('foo', 'bar'), ('version', '1')]]
215-
for hdr in [
204+
expected = [[('foo', 'bar'), ('expires', 2209069412), ('version', '0')]]
205+
self.assertEqual(parse_ns_headers([hdr]), expected)
206+
207+
@support.subTests('hdr', [
216208
'foo=bar; version="1"',
217209
'foo=bar; Version="1"',
218-
]:
219-
self.assertEqual(parse_ns_headers([hdr]), expected)
210+
])
211+
def test_parse_ns_headers_version(self, hdr):
212+
# quotes should be stripped
213+
expected = [[('foo', 'bar'), ('version', '1')]]
214+
self.assertEqual(parse_ns_headers([hdr]), expected)
220215

221216
def test_parse_ns_headers_special_names(self):
222217
# names such as 'expires' are not special in first name=value pair
@@ -226,8 +221,7 @@ def test_parse_ns_headers_special_names(self):
226221
expected = [[("expires", "01 Jan 2040 22:23:32 GMT"), ("version", "0")]]
227222
self.assertEqual(parse_ns_headers([hdr]), expected)
228223

229-
def test_join_header_words(self):
230-
for src, expected in [
224+
@support.subTests('src,expected', [
231225
([[("foo", None), ("bar", "baz")]], "foo; bar=baz"),
232226
(([]), ""),
233227
(([[]]), ""),
@@ -237,12 +231,11 @@ def test_join_header_words(self):
237231
'n; foo="foo;_", bar=foo_bar'),
238232
([[("n", "m"), ("foo", None)], [("bar", "foo_bar")]],
239233
'n=m; foo, bar=foo_bar'),
240-
]:
241-
with self.subTest(src=src):
242-
self.assertEqual(join_header_words(src), expected)
234+
])
235+
def test_join_header_words(self, src, expected):
236+
self.assertEqual(join_header_words(src), expected)
243237

244-
def test_split_header_words(self):
245-
tests = [
238+
@support.subTests('arg,expect', [
246239
("foo", [[("foo", None)]]),
247240
("foo=bar", [[("foo", "bar")]]),
248241
(" foo ", [[("foo", None)]]),
@@ -259,24 +252,22 @@ def test_split_header_words(self):
259252
(r'foo; bar=baz, spam=, foo="\,\;\"", bar= ',
260253
[[("foo", None), ("bar", "baz")],
261254
[("spam", "")], [("foo", ',;"')], [("bar", "")]]),
262-
]
263-
264-
for arg, expect in tests:
265-
try:
266-
result = split_header_words([arg])
267-
except:
268-
import traceback, io
269-
f = io.StringIO()
270-
traceback.print_exc(None, f)
271-
result = "(error -- traceback follows)\n\n%s" % f.getvalue()
272-
self.assertEqual(result, expect, """
255+
])
256+
def test_split_header_words(self, arg, expect):
257+
try:
258+
result = split_header_words([arg])
259+
except:
260+
import traceback, io
261+
f = io.StringIO()
262+
traceback.print_exc(None, f)
263+
result = "(error -- traceback follows)\n\n%s" % f.getvalue()
264+
self.assertEqual(result, expect, """
273265
When parsing: '%s'
274266
Expected: '%s'
275267
Got: '%s'
276268
""" % (arg, expect, result))
277269

278-
def test_roundtrip(self):
279-
tests = [
270+
@support.subTests('arg,expect', [
280271
("foo", "foo"),
281272
("foo=bar", "foo=bar"),
282273
(" foo ", "foo"),
@@ -309,12 +300,11 @@ def test_roundtrip(self):
309300
310301
('n; foo="foo;_", bar="foo,_"',
311302
'n; foo="foo;_", bar="foo,_"'),
312-
]
313-
314-
for arg, expect in tests:
315-
input = split_header_words([arg])
316-
res = join_header_words(input)
317-
self.assertEqual(res, expect, """
303+
])
304+
def test_roundtrip(self, arg, expect):
305+
input = split_header_words([arg])
306+
res = join_header_words(input)
307+
self.assertEqual(res, expect, """
318308
When parsing: '%s'
319309
Expected: '%s'
320310
Got: '%s'
@@ -516,14 +506,7 @@ class CookieTests(unittest.TestCase):
516506
## just the 7 special TLD's listed in their spec. And folks rely on
517507
## that...
518508

519-
def test_domain_return_ok(self):
520-
# test optimization: .domain_return_ok() should filter out most
521-
# domains in the CookieJar before we try to access them (because that
522-
# may require disk access -- in particular, with MSIECookieJar)
523-
# This is only a rough check for performance reasons, so it's not too
524-
# critical as long as it's sufficiently liberal.
525-
pol = DefaultCookiePolicy()
526-
for url, domain, ok in [
509+
@support.subTests('url,domain,ok', [
527510
("http://foo.bar.com/", "blah.com", False),
528511
("http://foo.bar.com/", "rhubarb.blah.com", False),
529512
("http://foo.bar.com/", "rhubarb.foo.bar.com", False),
@@ -543,11 +526,18 @@ def test_domain_return_ok(self):
543526
("http://foo/", ".local", True),
544527
("http://barfoo.com", ".foo.com", False),
545528
("http://barfoo.com", "foo.com", False),
546-
]:
547-
request = urllib.request.Request(url)
548-
r = pol.domain_return_ok(domain, request)
549-
if ok: self.assertTrue(r)
550-
else: self.assertFalse(r)
529+
])
530+
def test_domain_return_ok(self, url, domain, ok):
531+
# test optimization: .domain_return_ok() should filter out most
532+
# domains in the CookieJar before we try to access them (because that
533+
# may require disk access -- in particular, with MSIECookieJar)
534+
# This is only a rough check for performance reasons, so it's not too
535+
# critical as long as it's sufficiently liberal.
536+
pol = DefaultCookiePolicy()
537+
request = urllib.request.Request(url)
538+
r = pol.domain_return_ok(domain, request)
539+
if ok: self.assertTrue(r)
540+
else: self.assertFalse(r)
551541

552542
def test_missing_value(self):
553543
# missing = sign in Cookie: header is regarded by Mozilla as a missing
@@ -581,10 +571,7 @@ def test_missing_value(self):
581571
self.assertEqual(interact_netscape(c, "http://www.acme.com/foo/"),
582572
'"spam"; eggs')
583573

584-
def test_rfc2109_handling(self):
585-
# RFC 2109 cookies are handled as RFC 2965 or Netscape cookies,
586-
# dependent on policy settings
587-
for rfc2109_as_netscape, rfc2965, version in [
574+
@support.subTests('rfc2109_as_netscape,rfc2965,version', [
588575
# default according to rfc2965 if not explicitly specified
589576
(None, False, 0),
590577
(None, True, 1),
@@ -593,24 +580,27 @@ def test_rfc2109_handling(self):
593580
(False, True, 1),
594581
(True, False, 0),
595582
(True, True, 0),
596-
]:
597-
policy = DefaultCookiePolicy(
598-
rfc2109_as_netscape=rfc2109_as_netscape,
599-
rfc2965=rfc2965)
600-
c = CookieJar(policy)
601-
interact_netscape(c, "http://www.example.com/", "ni=ni; Version=1")
602-
try:
603-
cookie = c._cookies["www.example.com"]["/"]["ni"]
604-
except KeyError:
605-
self.assertIsNone(version) # didn't expect a stored cookie
606-
else:
607-
self.assertEqual(cookie.version, version)
608-
# 2965 cookies are unaffected
609-
interact_2965(c, "http://www.example.com/",
610-
"foo=bar; Version=1")
611-
if rfc2965:
612-
cookie2965 = c._cookies["www.example.com"]["/"]["foo"]
613-
self.assertEqual(cookie2965.version, 1)
583+
])
584+
def test_rfc2109_handling(self, rfc2109_as_netscape, rfc2965, version):
585+
# RFC 2109 cookies are handled as RFC 2965 or Netscape cookies,
586+
# dependent on policy settings
587+
policy = DefaultCookiePolicy(
588+
rfc2109_as_netscape=rfc2109_as_netscape,
589+
rfc2965=rfc2965)
590+
c = CookieJar(policy)
591+
interact_netscape(c, "http://www.example.com/", "ni=ni; Version=1")
592+
try:
593+
cookie = c._cookies["www.example.com"]["/"]["ni"]
594+
except KeyError:
595+
self.assertIsNone(version) # didn't expect a stored cookie
596+
else:
597+
self.assertEqual(cookie.version, version)
598+
# 2965 cookies are unaffected
599+
interact_2965(c, "http://www.example.com/",
600+
"foo=bar; Version=1")
601+
if rfc2965:
602+
cookie2965 = c._cookies["www.example.com"]["/"]["foo"]
603+
self.assertEqual(cookie2965.version, 1)
614604

615605
def test_ns_parser(self):
616606
c = CookieJar()
@@ -778,8 +768,7 @@ def test_default_path_with_query(self):
778768
# Cookie is sent back to the same URI.
779769
self.assertEqual(interact_netscape(cj, uri), value)
780770

781-
def test_escape_path(self):
782-
cases = [
771+
@support.subTests('arg,result', [
783772
# quoted safe
784773
("/foo%2f/bar", "/foo%2F/bar"),
785774
("/foo%2F/bar", "/foo%2F/bar"),
@@ -799,9 +788,9 @@ def test_escape_path(self):
799788
("/foo/bar\u00fc", "/foo/bar%C3%BC"), # UTF-8 encoded
800789
# unicode
801790
("/foo/bar\uabcd", "/foo/bar%EA%AF%8D"), # UTF-8 encoded
802-
]
803-
for arg, result in cases:
804-
self.assertEqual(escape_path(arg), result)
791+
])
792+
def test_escape_path(self, arg, result):
793+
self.assertEqual(escape_path(arg), result)
805794

806795
def test_request_path(self):
807796
# with parameters

0 commit comments

Comments
 (0)