Bug 1295486 - Mock codecs.open with MockedOpen; r?glandium draft
authorGregory Szorc <gps@mozilla.com>
Tue, 13 Sep 2016 16:07:40 -0700
changeset 413297 f4d1be71f91322d33fe07d9a8583d68e18104fdd
parent 413296 e4489160d703562e52e8907390623367f34f982d
child 413298 84dfa535f53ae5ec7a03a318e945684a2f8ca519
push id29400
push userbmo:gps@mozilla.com
push dateWed, 14 Sep 2016 00:02:37 +0000
reviewersglandium
bugs1295486
milestone51.0a1
Bug 1295486 - Mock codecs.open with MockedOpen; r?glandium TaskCluster's unit tests test MockedOpen. A future change I want to make introduces usage of codecs.open(). This resulted in test failures because codecs.open() isn't mocked by MockedOpen. This commit teaches MockedOpen to handle codecs.open(). As part of this, the mocked open() function was taught a "buffering" argument because codecs.open() calls __builtins__.open() with this argument. I had to introduce a new class to represent a mocked file opened with codecs.open() because files opened in this mode must raise errors when encodings aren't sane or why str/bytes instances are seen. The io.StringIO type fortunately enforces most of this for us. MozReview-Commit-ID: Bo1Ke516pxn
config/mozunit.py
config/tests/unit-mozunit.py
--- a/config/mozunit.py
+++ b/config/mozunit.py
@@ -1,15 +1,18 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+import codecs
 from unittest import TextTestRunner as _TestRunner, TestResult as _TestResult
 import unittest
 import inspect
+import io
+import locale
 from StringIO import StringIO
 import os
 import sys
 
 '''Helper to make python unit tests report the way that the Mozilla
 unit test infrastructure expects tests to report.
 
 Usage:
@@ -103,16 +106,43 @@ class MockedFile(StringIO):
         StringIO.close(self)
 
     def __enter__(self):
         return self
 
     def __exit__(self, type, value, traceback):
         self.close()
 
+
+class MockedUnicodeFile(io.StringIO):
+    """Represents a mock file opened in Unicode mode.
+
+    It is backed by an io.StringIO, which won't allow bytes in
+    (unlike StringIO.StringIO which coerces).
+    """
+    def __init__(self, context, filename, encoding, errors='strict',
+                 content=u''):
+        self.context = context
+        self.name = filename
+        self._encoding = encoding
+        self._errors = errors
+        io.StringIO.__init__(self, content)
+
+    def close(self):
+        value = self.getvalue().encode(self._encoding, self._errors)
+        self.context.files[self.name] = value
+        io.StringIO.close(self)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_tb):
+        self.close()
+
+
 def normcase(path):
     '''
     Normalize the case of `path`.
 
     Don't use `os.path.normcase` because that also normalizes forward slashes
     to backslashes on Windows.
     '''
     if sys.platform.startswith('win'):
@@ -138,44 +168,68 @@ class MockedOpen(object):
         f.write('foo')
     self.assertRaises(Exception,f.open('foo', 'r'))
     '''
     def __init__(self, files = {}):
         self.files = {}
         for name, content in files.iteritems():
             self.files[normcase(os.path.abspath(name))] = content
 
-    def _open(self, name, mode='r'):
+    def _open(self, name, mode='r', buffering=1):
         absname = normcase(os.path.abspath(name))
         if 'w' in mode:
             file = MockedFile(self, absname)
         elif absname in self.files:
             file = MockedFile(self, absname, self.files[absname])
         elif 'a' in mode:
-            file = MockedFile(self, absname, self._orig_open(name, 'r').read())
+            value = self._orig_open(name, 'r', buffering=buffering).read()
+            file = MockedFile(self, absname, value)
         else:
-            file = self._orig_open(name, mode)
+            file = self._orig_open(name, mode, buffering=buffering)
         if 'a' in mode:
             file.seek(0, os.SEEK_END)
         return file
 
+    def _codecs_open(self, name, mode='r', encoding=None, errors='strict'):
+        encoding = encoding or locale.getpreferredencoding()
+
+        absname = normcase(os.path.abspath(name))
+        if 'w' in mode:
+            file = MockedUnicodeFile(self, absname, encoding, errors)
+        elif absname in self.files:
+            value = self.files[absname].decode(encoding, errors)
+            file = MockedUnicodeFile(self, absname, encoding, errors, value)
+        elif 'a' in mode:
+            value = self._orig_codecs_open(name, 'r', encoding, errors=errors).read()
+            file = MockedUnicodeFile(self, absname, encoding, errors, value)
+        else:
+            file = self._orig_codecs_open(name, mode, encoding, errors)
+
+        if 'a' in mode:
+            file.seek(0, os.SEEK_END)
+
+        return file
+
     def __enter__(self):
         import __builtin__
         self._orig_open = __builtin__.open
+        self._orig_codecs_open = codecs.open
         self._orig_path_exists = os.path.exists
         self._orig_path_isdir = os.path.isdir
         self._orig_path_isfile = os.path.isfile
         __builtin__.open = self._open
+        codecs.open = self._codecs_open
         os.path.exists = self._wrapped_exists
         os.path.isdir = self._wrapped_isdir
         os.path.isfile = self._wrapped_isfile
 
     def __exit__(self, type, value, traceback):
         import __builtin__
         __builtin__.open = self._orig_open
+        codecs.open = self._orig_codecs_open
         os.path.exists = self._orig_path_exists
         os.path.isdir = self._orig_path_isdir
         os.path.isfile = self._orig_path_isfile
 
     def _wrapped_exists(self, p):
         return (self._wrapped_isfile(p) or
                 self._wrapped_isdir(p) or
                 self._orig_path_exists(p))
--- a/config/tests/unit-mozunit.py
+++ b/config/tests/unit-mozunit.py
@@ -1,13 +1,13 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-import sys
+import codecs
 import os
 from mozunit import main, MockedOpen
 import unittest
 from tempfile import mkstemp
 
 class TestMozUnit(unittest.TestCase):
     def test_mocked_open(self):
         # Create a temporary file on the file system.
@@ -23,46 +23,60 @@ class TestMozUnit(unittest.TestCase):
             self.assertTrue(os.path.exists('file1'))
             self.assertTrue(os.path.exists('file2'))
             self.assertFalse(os.path.exists('foo/file1'))
 
             # Check the contents of the files given at MockedOpen creation.
             self.assertEqual(open('file1', 'r').read(), 'content1')
             self.assertEqual(open('file2', 'r').read(), 'content2')
 
+            # codecs.open should also work.
+            self.assertEqual(codecs.open('file1', 'r').read(), u'content1')
+            self.assertEqual(codecs.open('file2', 'r').read(), u'content2')
+
             # Check that overwriting these files alters their content.
             with open('file1', 'w') as file:
                 file.write('foo')
             self.assertTrue(os.path.exists('file1'))
             self.assertEqual(open('file1', 'r').read(), 'foo')
+            self.assertEqual(codecs.open('file1', 'r').read(), u'foo')
 
             # ... but not until the file is closed.
             file = open('file2', 'w')
             file.write('bar')
             self.assertEqual(open('file2', 'r').read(), 'content2')
+            self.assertEqual(codecs.open('file2', 'r').read(), u'content2')
             file.close()
             self.assertEqual(open('file2', 'r').read(), 'bar')
+            self.assertEqual(codecs.open('file2', 'r').read(), u'bar')
 
             # Check that appending to a file does append
             with open('file1', 'a') as file:
                 file.write('bar')
             self.assertEqual(open('file1', 'r').read(), 'foobar')
+            self.assertEqual(codecs.open('file1', 'r').read(), u'foobar')
 
             self.assertFalse(os.path.exists('file3'))
 
             # Opening a non-existing file ought to fail.
             self.assertRaises(IOError, open, 'file3', 'r')
+            self.assertRaises(IOError, codecs.open, 'file3', 'r')
             self.assertFalse(os.path.exists('file3'))
 
             # Check that writing a new file does create the file.
             with open('file3', 'w') as file:
                 file.write('baz')
             self.assertEqual(open('file3', 'r').read(), 'baz')
             self.assertTrue(os.path.exists('file3'))
 
+            with codecs.open('file4', 'w') as file:
+                file.write(u'baz')
+            self.assertEqual(codecs.open('file4', 'r').read(), 'baz')
+            self.assertTrue(os.path.exists('file4'))
+
             # Check the content of the file created outside MockedOpen.
             self.assertEqual(open(path, 'r').read(), 'foobar')
 
             # Check that overwriting a file existing on the file system
             # does modify its content.
             with open(path, 'w') as file:
                 file.write('bazqux')
             self.assertEqual(open(path, 'r').read(), 'bazqux')
@@ -77,10 +91,25 @@ class TestMozUnit(unittest.TestCase):
         # Check that the file was not actually modified on the file system.
         self.assertEqual(open(path, 'r').read(), 'foobar')
         os.remove(path)
 
         # Check that the file created inside MockedOpen wasn't actually
         # created.
         self.assertRaises(IOError, open, 'file3', 'r')
 
+        # codecs.open should raise if data doesn't is valid encoding.
+        with MockedOpen():
+            data = b''.join(chr(i) for i in range(256))
+
+            with open('binary', 'wb') as fh:
+                fh.write(data)
+            self.assertEqual(open('binary', 'rb').read(), data)
+
+            with self.assertRaises(UnicodeDecodeError):
+                codecs.open('binary', 'r', 'utf-8')
+
+            self.assertEqual(codecs.open('binary', 'r', 'latin1').read(),
+                             data.decode('latin1'))
+
+
 if __name__ == "__main__":
     main()