Skip to content

Commit 3ec7c33

Browse files
authored
Merge branch 'master' into setup-teardown-deferreds
2 parents 50fa530 + b130900 commit 3ec7c33

File tree

5 files changed

+195
-70
lines changed

5 files changed

+195
-70
lines changed

NEWS

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ Changes
1111

1212
Fully migrate away from Launchpad to GitHub.
1313

14+
Improvements
15+
------------
16+
17+
* Support binary contents in ``FileContains`` matcher.
18+
(Jelmer Vernooij, #538)
19+
20+
* Allow stream=None to be passed to various TestResult
21+
classes that now support verbosity; fixes a regression
22+
from 2.8.0.
23+
(Jelmer Vernooij)
24+
1425
2.8.1
1526
~~~~~
1627

tests/matchers/test_filesystem.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,41 @@ def test_does_not_contain(self):
175175
Equals(mismatch.describe()),
176176
)
177177

178+
def test_binary_content(self):
179+
tempdir = self.mkdtemp()
180+
filename = os.path.join(tempdir, "binary_file")
181+
binary_data = b"\x00\x01\x02\x03\xff\xfe"
182+
with open(filename, "wb") as f:
183+
f.write(binary_data)
184+
self.assertThat(filename, FileContains(binary_data))
185+
186+
def test_binary_content_mismatch(self):
187+
tempdir = self.mkdtemp()
188+
filename = os.path.join(tempdir, "binary_file")
189+
with open(filename, "wb") as f:
190+
f.write(b"\x00\x01\x02")
191+
mismatch = FileContains(b"\xff\xfe\xfd").match(filename)
192+
self.assertThat(
193+
Equals(b"\xff\xfe\xfd").match(b"\x00\x01\x02").describe(),
194+
Equals(mismatch.describe()),
195+
)
196+
197+
def test_text_with_encoding(self):
198+
tempdir = self.mkdtemp()
199+
filename = os.path.join(tempdir, "utf8_file")
200+
text_data = "Hello 世界!"
201+
with open(filename, "w", encoding="utf-8") as f:
202+
f.write(text_data)
203+
self.assertThat(filename, FileContains(text_data, encoding="utf-8"))
204+
205+
def test_text_default_encoding(self):
206+
tempdir = self.mkdtemp()
207+
filename = os.path.join(tempdir, "text_file")
208+
text_data = "Hello World!"
209+
with open(filename, "w") as f:
210+
f.write(text_data)
211+
self.assertThat(filename, FileContains(text_data))
212+
178213

179214
class TestTarballContains(TestCase, PathHelpers):
180215
def test_match(self):

tests/test_testresult.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,65 @@ def run_tests():
20782078
),
20792079
)
20802080

2081+
def test_none_stream_is_accepted(self):
2082+
"""TextTestResult should accept None as stream for backward compatibility."""
2083+
result = TextTestResult(None)
2084+
test = make_test()
2085+
# Should not raise AttributeError
2086+
result.startTestRun()
2087+
result.startTest(test)
2088+
result.addSuccess(test)
2089+
result.stopTest(test)
2090+
result.stopTestRun()
2091+
2092+
def test_none_stream_with_failure(self):
2093+
"""TextTestResult with None stream should handle failures."""
2094+
result = TextTestResult(None)
2095+
test = make_failing_test()
2096+
# Should not raise AttributeError
2097+
test.run(result)
2098+
self.assertEqual(1, len(result.failures))
2099+
2100+
def test_verbosity_zero_produces_no_output(self):
2101+
"""TextTestResult with verbosity=0 should produce no per-test output."""
2102+
stream = io.StringIO()
2103+
result = TextTestResult(stream, verbosity=0)
2104+
test = make_test()
2105+
result.startTestRun()
2106+
result.startTest(test)
2107+
result.addSuccess(test)
2108+
result.stopTest(test)
2109+
# Get output up to stopTestRun (which still outputs summary)
2110+
output_before_stop = stream.getvalue()
2111+
# Should only have "Tests running...\n" from startTestRun
2112+
self.assertEqual("Tests running...\n", output_before_stop)
2113+
2114+
def test_verbosity_zero_allows_subclass_control(self):
2115+
"""Subclasses can use verbosity=0 to control their own output."""
2116+
2117+
class CustomResult(TextTestResult):
2118+
def __init__(self, stream):
2119+
super().__init__(stream, verbosity=0)
2120+
2121+
def addSuccess(self, test, details=None):
2122+
super().addSuccess(test, details)
2123+
self.stream.write("CUSTOM_SUCCESS_MARKER\n")
2124+
2125+
stream = io.StringIO()
2126+
result = CustomResult(stream)
2127+
test = make_test()
2128+
result.startTestRun()
2129+
result.startTest(test)
2130+
result.addSuccess(test)
2131+
result.stopTest(test)
2132+
output = stream.getvalue()
2133+
# Should have custom output but not parent's dot or "ok"
2134+
self.assertIn("CUSTOM_SUCCESS_MARKER\n", output)
2135+
self.assertNotIn("ok\n", output)
2136+
# Count dots - "Tests running..." has 3 dots, should not have a 4th
2137+
# from the success indicator
2138+
self.assertEqual(output.count("."), 3)
2139+
20812140

20822141
class TestThreadSafeForwardingResult(TestCase):
20832142
"""Tests for `TestThreadSafeForwardingResult`."""

testtools/matchers/_filesystem.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def match(self, path):
9494
class FileContains(Matcher):
9595
"""Matches if the given file has the specified contents."""
9696

97-
def __init__(self, contents=None, matcher=None):
97+
def __init__(self, contents=None, matcher=None, encoding=None):
9898
"""Construct a ``FileContains`` matcher.
9999
100100
Can be used in a basic mode where the file contents are compared for
@@ -103,9 +103,14 @@ def __init__(self, contents=None, matcher=None):
103103
matched against an arbitrary matcher (by passing ``matcher`` instead).
104104
105105
:param contents: If specified, match the contents of the file with
106-
these contents.
106+
these contents. If bytes, the file will be opened in binary mode.
107+
If str, the file will be opened in text mode using the specified
108+
encoding (or the default encoding if not specified).
107109
:param matcher: If specified, match the contents of the file against
108110
this matcher.
111+
:param encoding: Optional text encoding to use when opening the file
112+
in text mode. Only used when contents is a str (or when using a
113+
matcher for text content). Defaults to the system default encoding.
109114
"""
110115
if contents == matcher is None:
111116
raise AssertionError("Must provide one of `contents` or `matcher`.")
@@ -115,19 +120,23 @@ def __init__(self, contents=None, matcher=None):
115120
)
116121
if matcher is None:
117122
self.matcher = Equals(contents)
123+
self._binary_mode = isinstance(contents, bytes)
118124
else:
119125
self.matcher = matcher
126+
self._binary_mode = False
127+
self.encoding = encoding
120128

121129
def match(self, path):
122130
mismatch = PathExists().match(path)
123131
if mismatch is not None:
124132
return mismatch
125-
f = open(path)
126-
try:
127-
actual_contents = f.read()
128-
return self.matcher.match(actual_contents)
129-
finally:
130-
f.close()
133+
if self._binary_mode:
134+
with open(path, "rb") as f:
135+
actual_contents: bytes | str = f.read()
136+
else:
137+
with open(path, encoding=self.encoding) as f:
138+
actual_contents = f.read()
139+
return self.matcher.match(actual_contents)
131140

132141
def __str__(self):
133142
return f"File at path exists and contains {self.matcher}"

testtools/testresult/real.py

Lines changed: 73 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,8 @@ def _delta_to_float(self, a_timedelta, precision):
12441244
)
12451245

12461246
def _show_list(self, label, error_list):
1247+
if self.stream is None:
1248+
return
12471249
for test, output in error_list:
12481250
self.stream.write(self.sep1)
12491251
self.stream.write(f"{label}: {test.id()}\n")
@@ -1252,69 +1254,76 @@ def _show_list(self, label, error_list):
12521254

12531255
def startTest(self, test):
12541256
super().startTest(test)
1255-
if self.verbosity >= 2:
1257+
if self.stream is not None and self.verbosity >= 2:
12561258
self.stream.write(f"{test.id()} ... ")
12571259
self.stream.flush()
12581260

12591261
def addSuccess(self, test, details=None):
12601262
super().addSuccess(test, details=details)
1261-
if self.verbosity == 1:
1262-
self.stream.write(".")
1263-
self.stream.flush()
1264-
self._progress_printed = True
1265-
elif self.verbosity >= 2:
1266-
self.stream.write("ok\n")
1267-
self.stream.flush()
1263+
if self.stream is not None:
1264+
if self.verbosity == 1:
1265+
self.stream.write(".")
1266+
self.stream.flush()
1267+
self._progress_printed = True
1268+
elif self.verbosity >= 2:
1269+
self.stream.write("ok\n")
1270+
self.stream.flush()
12681271

12691272
def addError(self, test, err=None, details=None):
12701273
super().addError(test, err=err, details=details)
1271-
if self.verbosity == 1:
1272-
self.stream.write("E")
1273-
self.stream.flush()
1274-
elif self.verbosity >= 2:
1275-
self.stream.write("ERROR\n")
1276-
self.stream.flush()
1274+
if self.stream is not None:
1275+
if self.verbosity == 1:
1276+
self.stream.write("E")
1277+
self.stream.flush()
1278+
elif self.verbosity >= 2:
1279+
self.stream.write("ERROR\n")
1280+
self.stream.flush()
12771281

12781282
def addFailure(self, test, err=None, details=None):
12791283
super().addFailure(test, err=err, details=details)
1280-
if self.verbosity == 1:
1281-
self.stream.write("F")
1282-
self.stream.flush()
1283-
elif self.verbosity >= 2:
1284-
self.stream.write("FAIL\n")
1285-
self.stream.flush()
1284+
if self.stream is not None:
1285+
if self.verbosity == 1:
1286+
self.stream.write("F")
1287+
self.stream.flush()
1288+
elif self.verbosity >= 2:
1289+
self.stream.write("FAIL\n")
1290+
self.stream.flush()
12861291

12871292
def addSkip(self, test, reason=None, details=None):
12881293
super().addSkip(test, reason=reason, details=details)
1289-
if self.verbosity == 1:
1290-
self.stream.write("s")
1291-
self.stream.flush()
1292-
elif self.verbosity >= 2:
1293-
self.stream.write(f"skipped {reason!r}\n")
1294-
self.stream.flush()
1294+
if self.stream is not None:
1295+
if self.verbosity == 1:
1296+
self.stream.write("s")
1297+
self.stream.flush()
1298+
elif self.verbosity >= 2:
1299+
self.stream.write(f"skipped {reason!r}\n")
1300+
self.stream.flush()
12951301

12961302
def addExpectedFailure(self, test, err=None, details=None):
12971303
super().addExpectedFailure(test, err=err, details=details)
1298-
if self.verbosity == 1:
1299-
self.stream.write("x")
1300-
self.stream.flush()
1301-
elif self.verbosity >= 2:
1302-
self.stream.write("expected failure\n")
1303-
self.stream.flush()
1304+
if self.stream is not None:
1305+
if self.verbosity == 1:
1306+
self.stream.write("x")
1307+
self.stream.flush()
1308+
elif self.verbosity >= 2:
1309+
self.stream.write("expected failure\n")
1310+
self.stream.flush()
13041311

13051312
def addUnexpectedSuccess(self, test, details=None):
13061313
super().addUnexpectedSuccess(test, details=details)
1307-
if self.verbosity == 1:
1308-
self.stream.write("u")
1309-
self.stream.flush()
1310-
elif self.verbosity >= 2:
1311-
self.stream.write("unexpected success\n")
1312-
self.stream.flush()
1314+
if self.stream is not None:
1315+
if self.verbosity == 1:
1316+
self.stream.write("u")
1317+
self.stream.flush()
1318+
elif self.verbosity >= 2:
1319+
self.stream.write("unexpected success\n")
1320+
self.stream.flush()
13131321

13141322
def startTestRun(self):
13151323
super().startTestRun()
13161324
self.__start = self._now()
1317-
self.stream.write("Tests running...\n")
1325+
if self.stream is not None:
1326+
self.stream.write("Tests running...\n")
13181327

13191328
def stopTestRun(self):
13201329
if self.testsRun != 1:
@@ -1324,31 +1333,33 @@ def stopTestRun(self):
13241333
stop = self._now()
13251334
self._show_list("ERROR", self.errors)
13261335
self._show_list("FAIL", self.failures)
1327-
for test in self.unexpectedSuccesses:
1336+
if self.stream is not None:
1337+
for test in self.unexpectedSuccesses:
1338+
self.stream.write(
1339+
f"{self.sep1}UNEXPECTED SUCCESS: {test.id()}\n{self.sep2}"
1340+
)
1341+
# Add newline(s) before summary
1342+
# If we printed progress indicators (dots), add extra newline
1343+
if self._progress_printed:
1344+
self.stream.write("\n\n")
1345+
else:
1346+
self.stream.write("\n")
13281347
self.stream.write(
1329-
f"{self.sep1}UNEXPECTED SUCCESS: {test.id()}\n{self.sep2}"
1330-
)
1331-
# Add newline(s) before summary
1332-
# If we printed progress indicators (dots), add extra newline
1333-
if self._progress_printed:
1334-
self.stream.write("\n\n")
1335-
else:
1336-
self.stream.write("\n")
1337-
self.stream.write(
1338-
f"Ran {self.testsRun} test{plural} in "
1339-
f"{self._delta_to_float(stop - self.__start, 3):.3f}s\n"
1340-
)
1341-
if self.wasSuccessful():
1342-
self.stream.write("OK\n")
1343-
else:
1344-
self.stream.write("FAILED (")
1345-
details = []
1346-
failure_count = sum(
1347-
len(x) for x in (self.failures, self.errors, self.unexpectedSuccesses)
1348+
f"Ran {self.testsRun} test{plural} in "
1349+
f"{self._delta_to_float(stop - self.__start, 3):.3f}s\n"
13481350
)
1349-
details.append(f"failures={failure_count}")
1350-
self.stream.write(", ".join(details))
1351-
self.stream.write(")\n")
1351+
if self.wasSuccessful():
1352+
self.stream.write("OK\n")
1353+
else:
1354+
self.stream.write("FAILED (")
1355+
details = []
1356+
failure_count = sum(
1357+
len(x)
1358+
for x in (self.failures, self.errors, self.unexpectedSuccesses)
1359+
)
1360+
details.append(f"failures={failure_count}")
1361+
self.stream.write(", ".join(details))
1362+
self.stream.write(")\n")
13521363
super().stopTestRun()
13531364

13541365

0 commit comments

Comments
 (0)