Fix unicode encoding issues with records module. (#460)


diff --git a/mobly/records.py b/mobly/records.py
index d9cb555..74dd8f9 100644
--- a/mobly/records.py
+++ b/mobly/records.py
@@ -14,6 +14,8 @@
 """This module has classes for test result collection, and test result output.
 """
 
+from io import open
+
 import collections
 import copy
 import enum
@@ -115,8 +117,13 @@
         new_content['Type'] = entry_type.value
         # Both user code and Mobly code can trigger this dump, hence the lock.
         with self._lock:
-            # Use safe_dump here to avoid language-specific tags in final output.
-            with open(self._path, 'a') as f:
+            # For Python3, setting the encoding on yaml.safe_dump does not work
+            # because Python3 file descriptors set an encoding by default, which
+            # PyYAML uses instead of the encoding on yaml.safe_dump. So, the
+            # encoding has to be set on the open call instead.
+            with open(self._path, 'a', encoding='utf-8') as f:
+                # Use safe_dump here to avoid language-specific tags in final
+                # output.
                 yaml.safe_dump(
                     new_content,
                     f,
diff --git a/tests/mobly/records_test.py b/tests/mobly/records_test.py
index 9c795aa..fbbc4f4 100755
--- a/tests/mobly/records_test.py
+++ b/tests/mobly/records_test.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 from builtins import str
+from io import open
 
 import copy
 import mock
@@ -355,10 +356,33 @@
         dump_path = os.path.join(self.tmp_path, 'ha.yaml')
         writer = records.TestSummaryWriter(dump_path)
         writer.dump(record1.to_dict(), records.TestSummaryEntryType.RECORD)
-        with open(dump_path, 'r') as f:
+        with open(dump_path, 'r', encoding='utf-8') as f:
             content = yaml.load(f)
             self.assertEqual(content['Type'],
                              records.TestSummaryEntryType.RECORD.value)
+            self.assertEqual(content[records.TestResultEnums.RECORD_DETAILS],
+                             self.details)
+            self.assertEqual(content[records.TestResultEnums.RECORD_EXTRAS],
+                             self.float_extra)
+
+    def test_summary_write_dump_with_unicode(self):
+        unicode_details = u'\u901a'  # utf-8 -> b'\xe9\x80\x9a'
+        unicode_extras = u'\u8fc7'  # utf-8 -> b'\xe8\xbf\x87'
+        s = signals.TestFailure(unicode_details, unicode_extras)
+        record1 = records.TestResultRecord(self.tn)
+        record1.test_begin()
+        record1.test_fail(s)
+        dump_path = os.path.join(self.tmp_path, 'ha.yaml')
+        writer = records.TestSummaryWriter(dump_path)
+        writer.dump(record1.to_dict(), records.TestSummaryEntryType.RECORD)
+        with open(dump_path, 'r', encoding='utf-8') as f:
+            content = yaml.load(f)
+            self.assertEqual(content['Type'],
+                             records.TestSummaryEntryType.RECORD.value)
+            self.assertEqual(content[records.TestResultEnums.RECORD_DETAILS],
+                             unicode_details)
+            self.assertEqual(content[records.TestResultEnums.RECORD_EXTRAS],
+                             unicode_extras)
 
     def test_summary_user_data(self):
         user_data1 = {'a': 1}
@@ -368,7 +392,7 @@
         writer = records.TestSummaryWriter(dump_path)
         for data in user_data:
             writer.dump(data, records.TestSummaryEntryType.USER_DATA)
-        with open(dump_path, 'r') as f:
+        with open(dump_path, 'r', encoding='utf-8') as f:
             contents = []
             for c in yaml.load_all(f):
                 contents.append(c)