diff --git a/NEWS b/NEWS index a7352d37..066bb4db 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,12 @@ Changes Fully migrate away from Launchpad to GitHub. +Improvements +------------ + +* Support binary contents in ``FileContains`` matcher. + (Jelmer Vernooij, #538) + 2.8.1 ~~~~~ diff --git a/tests/matchers/test_filesystem.py b/tests/matchers/test_filesystem.py index fa7440ee..e88e2e1c 100644 --- a/tests/matchers/test_filesystem.py +++ b/tests/matchers/test_filesystem.py @@ -175,6 +175,41 @@ def test_does_not_contain(self): Equals(mismatch.describe()), ) + def test_binary_content(self): + tempdir = self.mkdtemp() + filename = os.path.join(tempdir, "binary_file") + binary_data = b"\x00\x01\x02\x03\xff\xfe" + with open(filename, "wb") as f: + f.write(binary_data) + self.assertThat(filename, FileContains(binary_data)) + + def test_binary_content_mismatch(self): + tempdir = self.mkdtemp() + filename = os.path.join(tempdir, "binary_file") + with open(filename, "wb") as f: + f.write(b"\x00\x01\x02") + mismatch = FileContains(b"\xff\xfe\xfd").match(filename) + self.assertThat( + Equals(b"\xff\xfe\xfd").match(b"\x00\x01\x02").describe(), + Equals(mismatch.describe()), + ) + + def test_text_with_encoding(self): + tempdir = self.mkdtemp() + filename = os.path.join(tempdir, "utf8_file") + text_data = "Hello 世界!" + with open(filename, "w", encoding="utf-8") as f: + f.write(text_data) + self.assertThat(filename, FileContains(text_data, encoding="utf-8")) + + def test_text_default_encoding(self): + tempdir = self.mkdtemp() + filename = os.path.join(tempdir, "text_file") + text_data = "Hello World!" + with open(filename, "w") as f: + f.write(text_data) + self.assertThat(filename, FileContains(text_data)) + class TestTarballContains(TestCase, PathHelpers): def test_match(self): diff --git a/testtools/matchers/_filesystem.py b/testtools/matchers/_filesystem.py index 5b131b4f..4c101dfc 100644 --- a/testtools/matchers/_filesystem.py +++ b/testtools/matchers/_filesystem.py @@ -94,7 +94,7 @@ def match(self, path): class FileContains(Matcher): """Matches if the given file has the specified contents.""" - def __init__(self, contents=None, matcher=None): + def __init__(self, contents=None, matcher=None, encoding=None): """Construct a ``FileContains`` matcher. 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): matched against an arbitrary matcher (by passing ``matcher`` instead). :param contents: If specified, match the contents of the file with - these contents. + these contents. If bytes, the file will be opened in binary mode. + If str, the file will be opened in text mode using the specified + encoding (or the default encoding if not specified). :param matcher: If specified, match the contents of the file against this matcher. + :param encoding: Optional text encoding to use when opening the file + in text mode. Only used when contents is a str (or when using a + matcher for text content). Defaults to the system default encoding. """ if contents == matcher is None: raise AssertionError("Must provide one of `contents` or `matcher`.") @@ -115,19 +120,23 @@ def __init__(self, contents=None, matcher=None): ) if matcher is None: self.matcher = Equals(contents) + self._binary_mode = isinstance(contents, bytes) else: self.matcher = matcher + self._binary_mode = False + self.encoding = encoding def match(self, path): mismatch = PathExists().match(path) if mismatch is not None: return mismatch - f = open(path) - try: - actual_contents = f.read() - return self.matcher.match(actual_contents) - finally: - f.close() + if self._binary_mode: + with open(path, "rb") as f: + actual_contents: bytes | str = f.read() + else: + with open(path, encoding=self.encoding) as f: + actual_contents = f.read() + return self.matcher.match(actual_contents) def __str__(self): return f"File at path exists and contains {self.matcher}"