1# -*- coding: utf-8 -*- 2# 3# This document is free and open-source software, subject to the OSI-approved 4# BSD license below. 5# 6# Copyright (c) 2014 Alexis Petrounias <www.petrounias.org>, 7# All rights reserved. 8# 9# Redistribution and use in source and binary forms, with or without 10# modification, are permitted provided that the following conditions are met: 11# 12# * Redistributions of source code must retain the above copyright notice, this 13# list of conditions and the following disclaimer. 14# 15# * Redistributions in binary form must reproduce the above copyright notice, 16# this list of conditions and the following disclaimer in the documentation 17# and/or other materials provided with the distribution. 18# 19# * Neither the name of the author nor the names of its contributors may be used 20# to endorse or promote products derived from this software without specific 21# prior written permission. 22# 23# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33""" 34Unit tests for collections 35""" 36from __future__ import absolute_import, division, print_function, unicode_literals 37from collections import OrderedDict, defaultdict 38import sys 39 40import pytest 41 42import jsonpickle 43from jsonpickle.compat import PY2 44 45__status__ = "Stable" 46__version__ = "1.0.0" 47__maintainer__ = "Alexis Petrounias <www.petrounias.org>" 48__author__ = "Alexis Petrounias <www.petrounias.org>" 49 50 51""" 52Classes featuring various collections used by 53:mod:`restorable_collections_tests` unit tests; these classes must be 54module-level (including the default dictionary factory method) so that they 55can be pickled. 56 57Tests restorable collections by creating pickled structures featuring 58no cycles, self cycles, and mutual cycles, for all supported dictionary and 59set wrappers. Python bult-in dictionaries and sets are also tested with 60expectation of failure via raising exceptions. 61 62""" 63 64 65class Group(object): 66 def __init__(self, name): 67 super(Group, self).__init__() 68 self.name = name 69 self.elements = [] 70 71 def __repr__(self): 72 return 'Group({})'.format(self.name) 73 74 75class C(object): 76 def __init__(self, v): 77 super(C, self).__init__() 78 self.v = v 79 self.plain = dict() 80 self.plain_ordered = OrderedDict() 81 self.plain_default = defaultdict(c_factory) 82 83 def add(self, key, value): 84 self.plain[key] = (key, value) 85 self.plain_ordered[key] = (key, value) 86 self.plain_default[key] = (key, value) 87 88 def __hash__(self): 89 # return id(self) 90 # the original __hash__() below means the hash can change after being 91 # stored, which JP does not support on Python 2. 92 if PY2: 93 return id(self) 94 return hash(self.v) if hasattr(self, 'v') else id(self) 95 96 def __repr__(self): 97 return 'C({})'.format(self.v) 98 99 100def c_factory(): 101 return (C(0), '_') 102 103 104class D(object): 105 def __init__(self, v): 106 super(D, self).__init__() 107 self.v = v 108 self.plain = set() 109 110 def add(self, item): 111 self.plain.add(item) 112 113 def __hash__(self): 114 if PY2: 115 return id(self) 116 return hash(self.v) if hasattr(self, 'v') else id(self) 117 118 def __repr__(self): 119 return 'D({})'.format(self.v) 120 121 122def pickle_and_unpickle(obj): 123 encoded = jsonpickle.encode(obj, keys=True) 124 return jsonpickle.decode(encoded, keys=True) 125 126 127def test_dict_no_cycle(): 128 g = Group('group') 129 c1 = C(42) 130 g.elements.append(c1) 131 c2 = C(67) 132 g.elements.append(c2) 133 c1.add(c2, 'a') # points to c2, which does not point to anything 134 135 assert c2 in c1.plain 136 assert c2 in c1.plain_ordered 137 assert c2 in c1.plain_default 138 139 gu = pickle_and_unpickle(g) 140 c1u = gu.elements[0] 141 c2u = gu.elements[1] 142 143 # check existence of keys directly 144 assert c2u in c1u.plain.keys() 145 assert c2u in c1u.plain_ordered.keys() 146 assert c2u in c1u.plain_default.keys() 147 148 # check direct key-based lookup 149 assert c2u == c1u.plain[c2u][0] 150 assert c2u == c1u.plain_ordered[c2u][0] 151 assert c2u == c1u.plain_default[c2u][0] 152 153 # check key lookup with key directly from keys() 154 plain_keys = list(c1u.plain.keys()) 155 ordered_keys = list(c1u.plain_ordered.keys()) 156 default_keys = list(c1u.plain_default.keys()) 157 assert c2u == c1u.plain[plain_keys[0]][0] 158 assert c2u == c1u.plain_ordered[ordered_keys[0]][0] 159 assert c2u == c1u.plain_default[default_keys[0]][0] 160 assert c2u == c1u.plain[plain_keys[0]][0] 161 assert c2u == c1u.plain_ordered[ordered_keys[0]][0] 162 assert c2u == c1u.plain_default[default_keys[0]][0] 163 164 165def test_dict_self_cycle(): 166 g = Group('group') 167 c1 = C(42) 168 g.elements.append(c1) 169 c2 = C(67) 170 g.elements.append(c2) 171 c1.add(c1, 'a') # cycle to itself 172 c1.add(c2, 'b') # c2 does not point to itself nor c1 173 174 assert c1 in c1.plain 175 assert c1 in c1.plain_ordered 176 assert c1 in c1.plain_default 177 178 gu = pickle_and_unpickle(g) 179 c1u = gu.elements[0] 180 c2u = gu.elements[1] 181 182 # check existence of keys directly 183 # key c1u 184 assert c1u in list(c1u.plain.keys()) 185 assert c1u in list(c1u.plain_ordered.keys()) 186 assert c1u in list(c1u.plain_default.keys()) 187 188 # key c2u 189 assert c2u in list(c1u.plain.keys()) 190 assert c2u in list(c1u.plain_ordered.keys()) 191 assert c2u in list(c1u.plain_default.keys()) 192 193 # check direct key-based lookup 194 195 # key c1u 196 assert 42 == c1u.plain[c1u][0].v 197 assert 42 == c1u.plain_ordered[c1u][0].v 198 199 assert len(c1u.plain_default) == 2 200 assert 42 == c1u.plain_default[c1u][0].v 201 # No new entries were created. 202 assert len(c1u.plain_default) == 2 203 204 # key c2u 205 # succeeds because c2u does not have a cycle to itself 206 assert c2u == c1u.plain[c2u][0] 207 # succeeds because c2u does not have a cycle to itself 208 assert c2u == c1u.plain_ordered[c2u][0] 209 # succeeds because c2u does not have a cycle to itself 210 assert c2u == c1u.plain_default[c2u][0] 211 assert c2u == c1u.plain[c2u][0] 212 assert c2u == c1u.plain_ordered[c2u][0] 213 assert c2u == c1u.plain_default[c2u][0] 214 215 # check key lookup with key directly from keys() 216 # key c1u 217 plain_keys = list(c1u.plain.keys()) 218 ordered_keys = list(c1u.plain_ordered.keys()) 219 default_keys = list(c1u.plain_default.keys()) 220 if PY2: 221 value = 67 222 else: 223 value = 42 224 assert value == c1u.plain[plain_keys[0]][0].v 225 42 == c1u.plain_ordered[ordered_keys[0]][0].v 226 42 == c1u.plain_default[default_keys[0]][0].v 227 228 # key c2u 229 # succeeds because c2u does not have a cycle to itself 230 if PY2: 231 key = c1u 232 else: 233 key = c2u 234 assert key == c1u.plain[plain_keys[1]][0] 235 # succeeds because c2u does not have a cycle to itself 236 assert c2u == c1u.plain_ordered[ordered_keys[1]][0] 237 assert 67 == c1u.plain_default[default_keys[1]][0].v 238 239 240def test_dict_mutual_cycle(): 241 g = Group('group') 242 c1 = C(42) 243 g.elements.append(c1) 244 c2 = C(67) 245 g.elements.append(c2) 246 247 c1.add(c2, 'a') # points to c2, which points to c1, forming cycle 248 c2.add(c1, 'a') # points to c1 in order to form cycle 249 250 assert c2 in c1.plain 251 assert c2 in c1.plain_ordered 252 assert c2 in c1.plain_default 253 254 assert c1 in c2.plain 255 assert c1 in c2.plain_ordered 256 assert c1 in c2.plain_default 257 258 gu = pickle_and_unpickle(g) 259 c1u = gu.elements[0] 260 c2u = gu.elements[1] 261 262 # check existence of keys directly 263 # key c2u 264 assert c2u in c1u.plain.keys() 265 assert c2u in c1u.plain_ordered.keys() 266 assert c2u in c1u.plain_default.keys() 267 268 # key c1u 269 assert c1u in list(c2u.plain.keys()) 270 assert c1u in list(c2u.plain_ordered.keys()) 271 assert c1u in list(c2u.plain_default.keys()) 272 273 # check direct key-based lookup 274 # key c2u, succeed because c2u added to c1u after __setstate__ 275 assert c2u == c1u.plain[c2u][0] 276 assert c2u == c1u.plain_ordered[c2u][0] 277 assert c2u == c1u.plain_default[c2u][0] 278 279 # key c1u 280 assert isinstance(c2u.plain[c1u][0], C) 281 assert 42 == c2u.plain[c1u][0].v 282 assert isinstance(c2u.plain_ordered[c1u][0], C) 283 284 # succeeds 285 assert len(c2u.plain_default) == 1 286 assert 42 == c2u.plain_default[c1u][0].v 287 # Ensure that no new entry is created by verifying that the length 288 # has not changed. 289 assert len(c2u.plain_default) == 1 290 291 # check key lookup with key directly from keys() 292 293 # key c2u, succeed because c2u added to c1u after __setstate__ 294 plain_keys = list(c1u.plain.keys()) 295 ordered_keys = list(c1u.plain_ordered.keys()) 296 default_keys = list(c1u.plain_default.keys()) 297 assert c2u == c1u.plain[plain_keys[0]][0] 298 assert c2u == c1u.plain_ordered[ordered_keys[0]][0] 299 assert c2u == c1u.plain_default[default_keys[0]][0] 300 301 # key c1u 302 plain_keys = list(c2u.plain.keys()) 303 ordered_keys = list(c2u.plain_ordered.keys()) 304 default_keys = list(c2u.plain_default.keys()) 305 assert 42 == c2u.plain[plain_keys[0]][0].v 306 assert 42 == c2u.plain_ordered[ordered_keys[0]][0].v 307 assert 42 == c2u.plain_default[default_keys[0]][0].v 308 assert isinstance(c2u.plain[plain_keys[0]], tuple) 309 assert isinstance(c2u.plain_ordered[ordered_keys[0]], tuple) 310 311 312def test_set_no_cycle(): 313 g = Group('group') 314 d1 = D(42) 315 g.elements.append(d1) 316 d2 = D(67) 317 g.elements.append(d2) 318 d1.add(d2) # d1 points to d1, d2 does not point to anything, no cycles 319 320 assert d2 in d1.plain 321 322 gu = pickle_and_unpickle(g) 323 d1u = gu.elements[0] 324 d2u = gu.elements[1] 325 326 # check element directly 327 assert d2u in d1u.plain 328 # check element taken from elements 329 assert list(d1u.plain)[0] in d1u.plain 330 331 332def test_set_self_cycle(): 333 g = Group('group') 334 d1 = D(42) 335 g.elements.append(d1) 336 d2 = D(67) 337 g.elements.append(d2) 338 d1.add(d1) # cycle to itself 339 d1.add(d2) # d1 also points to d2, but d2 does not point to d1 340 341 assert d1 in d1.plain 342 343 gu = pickle_and_unpickle(g) 344 d1u = gu.elements[0] 345 d2u = gu.elements[1] 346 assert d2u is not None 347 348 # check element directly 349 assert d1u in d1u.plain 350 # check element taken from elements 351 assert list(d1u.plain)[0] in d1u.plain 352 # succeeds because d2u added to d1u after __setstate__ 353 assert list(d1u.plain)[1] in d1u.plain 354 355 356def test_set_mutual_cycle(): 357 g = Group('group') 358 d1 = D(42) 359 g.elements.append(d1) 360 d2 = D(67) 361 g.elements.append(d2) 362 d1.add(d2) # points to d2, which points to d1, forming cycle 363 d2.add(d1) # points to d1 in order to form cycle 364 365 assert d2 in d1.plain 366 assert d1 in d2.plain 367 368 gu = pickle_and_unpickle(g) 369 d1u = gu.elements[0] 370 d2u = gu.elements[1] 371 372 # check element directly 373 # succeeds because d2u added to d1u after __setstate__ 374 assert d2u in d1u.plain 375 assert d1u in d2u.plain 376 # check element taken from elements 377 # succeeds because d2u added to d1u after __setstate__ 378 assert list(d1u.plain)[0] in d1u.plain 379 assert list(d2u.plain)[0] in d2u.plain 380 381 382if __name__ == '__main__': 383 pytest.main([__file__] + sys.argv[1:]) 384