THRIFT-162 Thrift structures are unhashable, preventing them from being used as set elements
Client: Python
Patch: David Reiss, Nobuaki Sukegawa

This closes #714
diff --git a/test/py/RunClientServer.py b/test/py/RunClientServer.py
index fa2a264..f084a41 100755
--- a/test/py/RunClientServer.py
+++ b/test/py/RunClientServer.py
@@ -37,6 +37,7 @@
 DEFAULT_LIBDIR_PY3 = os.path.join(ROOT_DIR, 'lib', 'py', 'build', 'lib')
 
 SCRIPTS = [
+  'TestFrozen.py',
   'TSimpleJSONProtocolTest.py',
   'SerializationTest.py',
   'TestEof.py',
diff --git a/test/py/TestFrozen.py b/test/py/TestFrozen.py
new file mode 100755
index 0000000..76750ad
--- /dev/null
+++ b/test/py/TestFrozen.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from DebugProtoTest.ttypes import CompactProtoTestStruct, Empty, Wrapper
+from thrift.Thrift import TFrozenDict
+from thrift.transport import TTransport
+from thrift.protocol import TBinaryProtocol
+import collections
+import unittest
+
+
+class TestFrozenBase(unittest.TestCase):
+  def _roundtrip(self, src, dst):
+    otrans = TTransport.TMemoryBuffer()
+    optoro = self.protocol(otrans)
+    src.write(optoro)
+    itrans = TTransport.TMemoryBuffer(otrans.getvalue())
+    iproto = self.protocol(itrans)
+    return dst.read(iproto) or dst
+
+  def test_dict_is_hashable_only_after_frozen(self):
+    d0 = {}
+    self.assertFalse(isinstance(d0, collections.Hashable))
+    d1 = TFrozenDict(d0)
+    self.assertTrue(isinstance(d1, collections.Hashable))
+
+  def test_struct_with_collection_fields(self):
+    pass
+
+  def test_set(self):
+    """Test that annotated set field can be serialized and deserialized"""
+    x = CompactProtoTestStruct(set_byte_map={
+      frozenset([42, 100, -100]): 99,
+      frozenset([0]): 100,
+      frozenset([]): 0,
+    })
+    x2 = self._roundtrip(x, CompactProtoTestStruct())
+    self.assertEqual(x2.set_byte_map[frozenset([42, 100, -100])], 99)
+    self.assertEqual(x2.set_byte_map[frozenset([0])], 100)
+    self.assertEqual(x2.set_byte_map[frozenset([])], 0)
+
+  def test_map(self):
+    """Test that annotated map field can be serialized and deserialized"""
+    x = CompactProtoTestStruct(map_byte_map={
+      TFrozenDict({42: 42, 100: -100}): 99,
+      TFrozenDict({0: 0}): 100,
+      TFrozenDict({}): 0,
+    })
+    x2 = self._roundtrip(x, CompactProtoTestStruct())
+    self.assertEqual(x2.map_byte_map[TFrozenDict({42: 42, 100: -100})], 99)
+    self.assertEqual(x2.map_byte_map[TFrozenDict({0: 0})], 100)
+    self.assertEqual(x2.map_byte_map[TFrozenDict({})], 0)
+
+  def test_list(self):
+    """Test that annotated list field can be serialized and deserialized"""
+    x = CompactProtoTestStruct(list_byte_map={
+      (42, 100, -100): 99,
+      (0,): 100,
+      (): 0,
+    })
+    x2 = self._roundtrip(x, CompactProtoTestStruct())
+    self.assertEqual(x2.list_byte_map[(42, 100, -100)], 99)
+    self.assertEqual(x2.list_byte_map[(0,)], 100)
+    self.assertEqual(x2.list_byte_map[()], 0)
+
+  def test_empty_struct(self):
+    """Test that annotated empty struct can be serialized and deserialized"""
+    x = CompactProtoTestStruct(empty_struct_field=Empty())
+    x2 = self._roundtrip(x, CompactProtoTestStruct())
+    self.assertEqual(x2.empty_struct_field, Empty())
+
+  def test_struct(self):
+    """Test that annotated struct can be serialized and deserialized"""
+    x = Wrapper(foo=Empty())
+    self.assertEqual(x.foo, Empty())
+    x2 = self._roundtrip(x, Wrapper)
+    self.assertEqual(x2.foo, Empty())
+
+
+class TestFrozen(TestFrozenBase):
+  def protocol(self, trans):
+    return TBinaryProtocol.TBinaryProtocolFactory().getProtocol(trans)
+
+
+class TestFrozenAccelerated(TestFrozenBase):
+  def protocol(self, trans):
+    return TBinaryProtocol.TBinaryProtocolAcceleratedFactory().getProtocol(trans)
+
+
+def suite():
+  suite = unittest.TestSuite()
+  loader = unittest.TestLoader()
+  suite.addTest(loader.loadTestsFromTestCase(TestFrozen))
+  suite.addTest(loader.loadTestsFromTestCase(TestFrozenAccelerated))
+  return suite
+
+if __name__ == "__main__":
+  unittest.main(defaultTest="suite", testRunner=unittest.TextTestRunner(verbosity=2))