Coverage for libs/sm/refcounter.py: 64%
416 statements
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
1#
2# Copyright (C) Citrix Systems Inc.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU Lesser General Public License as published
6# by the Free Software Foundation; version 2.1 only.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16#
17# Persistent reference counter. This refcounter can maintain two separate
18# refcounts: one binary (which can have a value of 0 or 1) and one normal. The
19# parameter "binary" specifies which of the two counters to update, while the
20# return value is zero IFF both counters are zero
21#
22# Synchronization must be done at a higher level, by the users of this module
23#
25import os
26from sm.core import util
27from sm.core.lock import Lock
28import errno
31class RefCounterException(util.SMException):
32 pass
35class RefCounter:
36 """Persistent local-FS file-based reference counter. The
37 operations are get() and put(), and they are atomic."""
39 BASE_DIR = "/run/sm/refcount"
41 def get(obj, binary, ns=None):
42 """Get (inc ref count) 'obj' in namespace 'ns' (optional).
43 Returns new ref count"""
44 if binary:
45 return RefCounter._adjust(ns, obj, 0, 1)
46 else:
47 return RefCounter._adjust(ns, obj, 1, 0)
48 get = staticmethod(get)
50 def put(obj, binary, ns=None):
51 """Put (dec ref count) 'obj' in namespace 'ns' (optional). If ref
52 count was zero already, this operation is a no-op.
53 Returns new ref count"""
54 if binary:
55 return RefCounter._adjust(ns, obj, 0, -1)
56 else:
57 return RefCounter._adjust(ns, obj, -1, 0)
58 put = staticmethod(put)
60 def set(obj, count, binaryCount, ns=None):
61 """Set normal & binary counts explicitly to the specified values.
62 Returns new ref count"""
63 (obj, ns) = RefCounter._getSafeNames(obj, ns)
64 assert(count >= 0 and binaryCount >= 0)
65 if binaryCount > 1:
66 raise RefCounterException("Binary count = %d > 1" % binaryCount)
67 RefCounter._set(ns, obj, count, binaryCount)
68 set = staticmethod(set)
70 def check(obj, ns=None):
71 """Get the ref count values for 'obj' in namespace 'ns' (optional)"""
72 (obj, ns) = RefCounter._getSafeNames(obj, ns)
73 return RefCounter._get(ns, obj)
74 check = staticmethod(check)
76 def checkLocked(obj, ns):
77 """Lock-protected access"""
78 lock = Lock(obj, ns)
79 lock.acquire()
80 try:
81 return RefCounter.check(obj, ns)
82 finally:
83 lock.release()
84 checkLocked = staticmethod(checkLocked)
86 def reset(obj, ns=None):
87 """Reset ref counts for 'obj' in namespace 'ns' (optional) to 0."""
88 RefCounter.resetAll(ns, obj)
89 reset = staticmethod(reset)
91 def resetAll(ns=None, obj=None):
92 """Reset ref counts of 'obj' in namespace 'ns' to 0. If obj is not
93 provided, reset all existing objects in 'ns' to 0. If neither obj nor
94 ns are supplied, do this for all namespaces"""
95 if obj:
96 (obj, ns) = RefCounter._getSafeNames(obj, ns)
97 if ns:
98 nsList = [ns]
99 else:
100 if not util.pathexists(RefCounter.BASE_DIR):
101 return
102 try:
103 nsList = os.listdir(RefCounter.BASE_DIR)
104 except OSError:
105 raise RefCounterException("failed to get namespace list")
106 for ns in nsList:
107 RefCounter._reset(ns, obj)
108 resetAll = staticmethod(resetAll)
110 def _adjust(ns, obj, delta, binaryDelta):
111 """Add 'delta' to the normal refcount and 'binaryDelta' to the binary
112 refcount of 'obj' in namespace 'ns'.
113 Returns new ref count"""
114 if binaryDelta > 1 or binaryDelta < -1: 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never true
115 raise RefCounterException("Binary delta = %d outside [-1;1]" % \
116 binaryDelta)
117 (obj, ns) = RefCounter._getSafeNames(obj, ns)
118 (count, binaryCount) = RefCounter._get(ns, obj)
120 newCount = count + delta
121 newBinaryCount = binaryCount + binaryDelta
122 if newCount < 0:
123 util.SMlog("WARNING: decrementing normal refcount of 0")
124 newCount = 0
125 if newBinaryCount < 0:
126 util.SMlog("WARNING: decrementing binary refcount of 0")
127 newBinaryCount = 0
128 if newBinaryCount > 1:
129 newBinaryCount = 1
130 util.SMlog("Refcount for %s:%s (%d, %d) + (%d, %d) => (%d, %d)" % \
131 (ns, obj, count, binaryCount, delta, binaryDelta,
132 newCount, newBinaryCount))
133 RefCounter._set(ns, obj, newCount, newBinaryCount)
134 return newCount + newBinaryCount
135 _adjust = staticmethod(_adjust)
137 def _get(ns, obj):
138 """Get the ref count values for 'obj' in namespace 'ns'"""
139 objFile = os.path.join(RefCounter.BASE_DIR, ns, obj)
140 (count, binaryCount) = (0, 0)
141 if util.pathexists(objFile):
142 (count, binaryCount) = RefCounter._readCount(objFile)
143 return (count, binaryCount)
144 _get = staticmethod(_get)
146 def _set(ns, obj, count, binaryCount):
147 """Set the ref count values for 'obj' in namespace 'ns'"""
148 util.SMlog("Refcount for %s:%s set => (%d, %db)" % \
149 (ns, obj, count, binaryCount))
150 if count == 0 and binaryCount == 0:
151 RefCounter._removeObject(ns, obj)
152 else:
153 objFile = os.path.join(RefCounter.BASE_DIR, ns, obj)
155 while not RefCounter._writeCount(objFile, count, binaryCount):
156 RefCounter._createNamespace(ns)
158 _set = staticmethod(_set)
160 def _getSafeNames(obj, ns):
161 """Get a name that can be used as a file name"""
162 if not ns:
163 ns = obj.split('/')[0]
164 if not ns:
165 ns = "default"
166 for char in ['/', '*', '?', '\\']:
167 obj = obj.replace(char, "_")
168 return (obj, ns)
169 _getSafeNames = staticmethod(_getSafeNames)
171 def _createNamespace(ns):
172 nsDir = os.path.join(RefCounter.BASE_DIR, ns)
173 try:
174 os.makedirs(nsDir)
175 except OSError as e:
176 if e.errno != errno.EEXIST:
177 raise RefCounterException("failed to makedirs '%s' (%s)" % \
178 (nsDir, e))
179 _createNamespace = staticmethod(_createNamespace)
181 def _removeObject(ns, obj):
182 nsDir = os.path.join(RefCounter.BASE_DIR, ns)
183 objFile = os.path.join(nsDir, obj)
184 if not util.pathexists(objFile):
185 return
186 try:
187 os.unlink(objFile)
188 except OSError:
189 raise RefCounterException("failed to remove '%s'" % objFile)
191 try:
192 os.rmdir(nsDir)
193 except OSError as e:
194 namespaceAlreadyCleanedUp = e.errno == errno.ENOENT
195 newObjectAddedToNamespace = e.errno == errno.ENOTEMPTY
197 if namespaceAlreadyCleanedUp or newObjectAddedToNamespace: 197 ↛ 200line 197 didn't jump to line 200, because the condition on line 197 was never false
198 pass
199 else:
200 raise RefCounterException("failed to remove '%s'" % nsDir)
201 _removeObject = staticmethod(_removeObject)
203 def _reset(ns, obj=None):
204 nsDir = os.path.join(RefCounter.BASE_DIR, ns)
205 if not util.pathexists(nsDir):
206 return
207 if obj:
208 if not util.pathexists(os.path.join(nsDir, obj)): 208 ↛ 209line 208 didn't jump to line 209, because the condition on line 208 was never true
209 return
210 objList = [obj]
211 else:
212 try:
213 objList = os.listdir(nsDir)
214 except OSError:
215 raise RefCounterException("failed to list '%s'" % ns)
216 for obj in objList:
217 RefCounter._removeObject(ns, obj)
218 _reset = staticmethod(_reset)
220 def _readCount(fn):
221 try:
222 f = open(fn, 'r')
223 line = f.readline()
224 nums = line.split()
225 count = int(nums[0])
226 binaryCount = int(nums[1])
227 f.close()
228 except IOError:
229 raise RefCounterException("failed to read file '%s'" % fn)
230 return (count, binaryCount)
231 _readCount = staticmethod(_readCount)
233 def _writeCount(fn, count, binaryCount):
234 try:
235 f = open(fn, 'w')
236 f.write("%d %d\n" % (count, binaryCount))
237 f.close()
238 return True
239 except IOError as e:
240 fileNotFound = e.errno == errno.ENOENT
241 if fileNotFound: 241 ↛ 243line 241 didn't jump to line 243, because the condition on line 241 was never false
242 return False
243 raise RefCounterException("failed to write '(%d %d)' to '%s': %s" \
244 % (count, binaryCount, fn, e))
245 _writeCount = staticmethod(_writeCount)
247 def _runTests():
248 "Unit tests"
250 RefCounter.resetAll()
252 # A
253 (cnt, bcnt) = RefCounter.check("X", "A")
254 if cnt != 0 or bcnt != 0: 254 ↛ 255line 254 didn't jump to line 255, because the condition on line 254 was never true
255 print("Error: check = %d != 0 in the beginning" % cnt)
256 return -1
258 cnt = RefCounter.get("X", False, "A")
259 if cnt != 1: 259 ↛ 260line 259 didn't jump to line 260, because the condition on line 259 was never true
260 print("Error: count = %d != 1 after first get()" % cnt)
261 return -1
262 (cnt, bcnt) = RefCounter.check("X", "A")
263 if cnt != 1: 263 ↛ 264line 263 didn't jump to line 264, because the condition on line 263 was never true
264 print("Error: check = %d != 1 after first get()" % cnt)
265 return -1
267 cnt = RefCounter.put("X", False, "A")
268 if cnt != 0: 268 ↛ 269line 268 didn't jump to line 269, because the condition on line 268 was never true
269 print("Error: count = %d != 0 after get-put" % cnt)
270 return -1
271 (cnt, bcnt) = RefCounter.check("X", "A")
272 if cnt != 0: 272 ↛ 273line 272 didn't jump to line 273, because the condition on line 272 was never true
273 print("Error: check = %d != 0 after get-put" % cnt)
274 return -1
276 cnt = RefCounter.get("X", False, "A")
277 if cnt != 1: 277 ↛ 278line 277 didn't jump to line 278, because the condition on line 277 was never true
278 print("Error: count = %d != 1 after get-put-get" % cnt)
279 return -1
281 cnt = RefCounter.get("X", False, "A")
282 if cnt != 2: 282 ↛ 283line 282 didn't jump to line 283, because the condition on line 282 was never true
283 print("Error: count = %d != 2 after second get()" % cnt)
284 return -1
286 cnt = RefCounter.get("X", False, "A")
287 if cnt != 3: 287 ↛ 288line 287 didn't jump to line 288, because the condition on line 287 was never true
288 print("Error: count = %d != 3 after third get()" % cnt)
289 return -1
290 (cnt, bcnt) = RefCounter.check("X", "A")
291 if cnt != 3: 291 ↛ 292line 291 didn't jump to line 292, because the condition on line 291 was never true
292 print("Error: check = %d != 3 after third get()" % cnt)
293 return -1
295 cnt = RefCounter.put("Y", False, "A")
296 if cnt != 0: 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true
297 print("Error: count = %d != 0 after first put()" % cnt)
298 return -1
299 (cnt, bcnt) = RefCounter.check("Y", "A")
300 if cnt != 0: 300 ↛ 301line 300 didn't jump to line 301, because the condition on line 300 was never true
301 print("Error: check = %d != 0 after first put()" % cnt)
302 return -1
304 cnt = RefCounter.put("X", False, "A")
305 if cnt != 2: 305 ↛ 306line 305 didn't jump to line 306, because the condition on line 305 was never true
306 print("Error: count = %d != 2 after 3get-1put" % cnt)
307 return -1
309 cnt = RefCounter.put("X", False, "A")
310 if cnt != 1: 310 ↛ 311line 310 didn't jump to line 311, because the condition on line 310 was never true
311 print("Error: count = %d != 1 after 3get-2put" % cnt)
312 return -1
314 cnt = RefCounter.get("X", False, "A")
315 if cnt != 2: 315 ↛ 316line 315 didn't jump to line 316, because the condition on line 315 was never true
316 print("Error: count = %d != 2 after 4get-2put" % cnt)
317 return -1
318 (cnt, bcnt) = RefCounter.check("X", "A")
319 if cnt != 2: 319 ↛ 320line 319 didn't jump to line 320, because the condition on line 319 was never true
320 print("Error: check = %d != 2 after 4get-2put" % cnt)
321 return -1
323 cnt = RefCounter.put("X", False, "A")
324 if cnt != 1: 324 ↛ 325line 324 didn't jump to line 325, because the condition on line 324 was never true
325 print("Error: count = %d != 0 after 4get-3put" % cnt)
326 return -1
328 cnt = RefCounter.put("X", False, "A")
329 if cnt != 0: 329 ↛ 330line 329 didn't jump to line 330, because the condition on line 329 was never true
330 print("Error: count = %d != 0 after 4get-4put" % cnt)
331 return -1
332 (cnt, bcnt) = RefCounter.check("X", "A")
333 if cnt != 0: 333 ↛ 334line 333 didn't jump to line 334, because the condition on line 333 was never true
334 print("Error: check = %d != 0 after 4get-4put" % cnt)
335 return -1
337 # B
338 cnt = RefCounter.put("Z", False, "B")
339 if cnt != 0: 339 ↛ 340line 339 didn't jump to line 340, because the condition on line 339 was never true
340 print("Error: count = %d != 0 after new put()" % cnt)
341 return -1
343 cnt = RefCounter.get("Z", False, "B")
344 if cnt != 1: 344 ↛ 345line 344 didn't jump to line 345, because the condition on line 344 was never true
345 print("Error: count = %d != 1 after put-get" % cnt)
346 return -1
348 cnt = RefCounter.put("Z", False, "B")
349 if cnt != 0: 349 ↛ 350line 349 didn't jump to line 350, because the condition on line 349 was never true
350 print("Error: count = %d != 0 after put-get-put" % cnt)
351 return -1
352 (cnt, bcnt) = RefCounter.check("Z", "B")
353 if cnt != 0: 353 ↛ 354line 353 didn't jump to line 354, because the condition on line 353 was never true
354 print("Error: check = %d != 0 after put-get-put" % cnt)
355 return -1
357 cnt = RefCounter.get("Z", False, "B")
358 if cnt != 1: 358 ↛ 359line 358 didn't jump to line 359, because the condition on line 358 was never true
359 print("Error: count = %d != 1 after put-get-put-get" % cnt)
360 return -1
361 (cnt, bcnt) = RefCounter.check("Z", "B")
362 if cnt != 1: 362 ↛ 363line 362 didn't jump to line 363, because the condition on line 362 was never true
363 print("Error: check = %d != 1 after put-get-put-get" % cnt)
364 return -1
366 # set
367 (cnt, bcnt) = RefCounter.check("a/b")
368 if cnt != 0: 368 ↛ 369line 368 didn't jump to line 369, because the condition on line 368 was never true
369 print("Error: count = %d != 0 initially" % cnt)
370 return -1
371 RefCounter.set("a/b", 2, 0)
372 (cnt, bcnt) = RefCounter.check("a/b")
373 if cnt != 2 or bcnt != 0: 373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true
374 print("Error: count = (%d,%d) != (2,0) after set(2,0)" % (cnt, bcnt))
375 return -1
376 cnt = RefCounter.put("a/b", False)
377 if cnt != 1: 377 ↛ 378line 377 didn't jump to line 378, because the condition on line 377 was never true
378 print("Error: count = %d != 1 after set(2)-put" % cnt)
379 return -1
380 cnt = RefCounter.get("a/b", False)
381 if cnt != 2: 381 ↛ 382line 381 didn't jump to line 382, because the condition on line 381 was never true
382 print("Error: count = %d != 2 after set(2)-put-get" % cnt)
383 return -1
384 RefCounter.set("a/b", 100, 0)
385 (cnt, bcnt) = RefCounter.check("a/b")
386 if cnt != 100 or bcnt != 0: 386 ↛ 387line 386 didn't jump to line 387, because the condition on line 386 was never true
387 print("Error: cnt,bcnt = (%d,%d) != (100,0) after set(100,0)" % \
388 (cnt, bcnt))
389 return -1
390 cnt = RefCounter.get("a/b", False)
391 if cnt != 101: 391 ↛ 392line 391 didn't jump to line 392, because the condition on line 391 was never true
392 print("Error: count = %d != 101 after get" % cnt)
393 return -1
394 RefCounter.set("a/b", 100, 1)
395 (cnt, bcnt) = RefCounter.check("a/b")
396 if cnt != 100 or bcnt != 1: 396 ↛ 397line 396 didn't jump to line 397, because the condition on line 396 was never true
397 print("Error: cnt,bcnt = (%d,%d) != (100,1) after set(100,1)" % \
398 (cnt, bcnt))
399 return -1
400 RefCounter.reset("a/b")
401 (cnt, bcnt) = RefCounter.check("a/b")
402 if cnt != 0: 402 ↛ 403line 402 didn't jump to line 403, because the condition on line 402 was never true
403 print("Error: check = %d != 0 after reset" % cnt)
404 return -1
406 # binary
407 cnt = RefCounter.get("A", True)
408 if cnt != 1: 408 ↛ 409line 408 didn't jump to line 409, because the condition on line 408 was never true
409 print("Error: count = %d != 1 after get(bin)" % cnt)
410 return -1
411 cnt = RefCounter.get("A", True)
412 if cnt != 1: 412 ↛ 413line 412 didn't jump to line 413, because the condition on line 412 was never true
413 print("Error: count = %d != 1 after get(bin)*2" % cnt)
414 return -1
415 cnt = RefCounter.put("A", True)
416 if cnt != 0: 416 ↛ 417line 416 didn't jump to line 417, because the condition on line 416 was never true
417 print("Error: count = %d != 0 after get(bin)*2-put(bin)" % cnt)
418 return -1
419 cnt = RefCounter.put("A", True)
420 if cnt != 0: 420 ↛ 421line 420 didn't jump to line 421, because the condition on line 420 was never true
421 print("Error: count = %d != 0 after get(bin)*2-put(bin)*2" % cnt)
422 return -1
423 try:
424 RefCounter.set("A", 0, 2)
425 print("Error: set(0,2) was allowed")
426 return -1
427 except RefCounterException:
428 pass
429 cnt = RefCounter.get("A", True)
430 if cnt != 1: 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true
431 print("Error: count = %d != 1 after get(bin)" % cnt)
432 return -1
433 cnt = RefCounter.get("A", False)
434 if cnt != 2: 434 ↛ 435line 434 didn't jump to line 435, because the condition on line 434 was never true
435 print("Error: count = %d != 2 after get(bin)-get" % cnt)
436 return -1
437 cnt = RefCounter.get("A", False)
438 if cnt != 3: 438 ↛ 439line 438 didn't jump to line 439, because the condition on line 438 was never true
439 print("Error: count = %d != 3 after get(bin)-get-get" % cnt)
440 return -1
441 cnt = RefCounter.get("A", True)
442 if cnt != 3: 442 ↛ 443line 442 didn't jump to line 443, because the condition on line 442 was never true
443 print("Error: count = %d != 3 after get(bin)-get*2-get(bin)" % cnt)
444 return -1
445 cnt = RefCounter.put("A", False)
446 if cnt != 2: 446 ↛ 447line 446 didn't jump to line 447, because the condition on line 446 was never true
447 print("Error: count = %d != 2 after get(bin)*2-get*2-put" % cnt)
448 return -1
449 cnt = RefCounter.put("A", True)
450 if cnt != 1: 450 ↛ 451line 450 didn't jump to line 451, because the condition on line 450 was never true
451 print("Error: cnt = %d != 1 after get(b)*2-get*2-put-put(b)" % cnt)
452 return -1
453 cnt = RefCounter.put("A", False)
454 if cnt != 0: 454 ↛ 455line 454 didn't jump to line 455, because the condition on line 454 was never true
455 print("Error: cnt = %d != 0 after get(b)*2-get*2-put*2-put(b)" % cnt)
456 return -1
458 # names
459 cnt = RefCounter.get("Z", False)
460 if cnt != 1: 460 ↛ 461line 460 didn't jump to line 461, because the condition on line 460 was never true
461 print("Error: count = %d != 1 after get (no ns 1)" % cnt)
462 return -1
464 cnt = RefCounter.get("Z/", False)
465 if cnt != 1: 465 ↛ 466line 465 didn't jump to line 466, because the condition on line 465 was never true
466 print("Error: count = %d != 1 after get (no ns 2)" % cnt)
467 return -1
469 cnt = RefCounter.get("/Z", False)
470 if cnt != 1: 470 ↛ 471line 470 didn't jump to line 471, because the condition on line 470 was never true
471 print("Error: count = %d != 1 after get (no ns 3)" % cnt)
472 return -1
474 cnt = RefCounter.get("/Z/*/?/\\", False)
475 if cnt != 1: 475 ↛ 476line 475 didn't jump to line 476, because the condition on line 475 was never true
476 print("Error: count = %d != 1 after get (no ns 4)" % cnt)
477 return -1
479 cnt = RefCounter.get("Z", False)
480 if cnt != 2: 480 ↛ 481line 480 didn't jump to line 481, because the condition on line 480 was never true
481 print("Error: count = %d != 2 after get (no ns 1)" % cnt)
482 return -1
484 cnt = RefCounter.get("Z/", False)
485 if cnt != 2: 485 ↛ 486line 485 didn't jump to line 486, because the condition on line 485 was never true
486 print("Error: count = %d != 2 after get (no ns 2)" % cnt)
487 return -1
489 cnt = RefCounter.get("/Z", False)
490 if cnt != 2: 490 ↛ 491line 490 didn't jump to line 491, because the condition on line 490 was never true
491 print("Error: count = %d != 2 after get (no ns 3)" % cnt)
492 return -1
494 cnt = RefCounter.get("/Z/*/?/\\", False)
495 if cnt != 2: 495 ↛ 496line 495 didn't jump to line 496, because the condition on line 495 was never true
496 print("Error: count = %d != 2 after get (no ns 4)" % cnt)
497 return -1
499 # resetAll
500 RefCounter.resetAll("B")
501 cnt = RefCounter.get("Z", False, "B")
502 if cnt != 1: 502 ↛ 503line 502 didn't jump to line 503, because the condition on line 502 was never true
503 print("Error: count = %d != 1 after resetAll-get" % cnt)
504 return -1
506 cnt = RefCounter.get("Z", False, "C")
507 if cnt != 1: 507 ↛ 508line 507 didn't jump to line 508, because the condition on line 507 was never true
508 print("Error: count = %d != 1 after C.get" % cnt)
509 return -1
511 RefCounter.resetAll("B")
512 cnt = RefCounter.get("Z", False, "B")
513 if cnt != 1: 513 ↛ 514line 513 didn't jump to line 514, because the condition on line 513 was never true
514 print("Error: count = %d != 1 after second resetAll-get" % cnt)
515 return -1
517 cnt = RefCounter.get("Z", False, "C")
518 if cnt != 2: 518 ↛ 519line 518 didn't jump to line 519, because the condition on line 518 was never true
519 print("Error: count = %d != 2 after second C.get" % cnt)
520 return -1
522 RefCounter.resetAll("D")
523 RefCounter.resetAll()
524 cnt = RefCounter.put("Z", False, "B")
525 if cnt != 0: 525 ↛ 526line 525 didn't jump to line 526, because the condition on line 525 was never true
526 print("Error: count = %d != 0 after resetAll-put" % cnt)
527 return -1
529 cnt = RefCounter.put("Z", False, "C")
530 if cnt != 0: 530 ↛ 531line 530 didn't jump to line 531, because the condition on line 530 was never true
531 print("Error: count = %d != 0 after C.resetAll-put" % cnt)
532 return -1
534 RefCounter.resetAll()
536 return 0
537 _runTests = staticmethod(_runTests)
540if __name__ == '__main__': 540 ↛ 541line 540 didn't jump to line 541, because the condition on line 540 was never true
541 print("Running unit tests...")
542 try:
543 if RefCounter._runTests() == 0:
544 print("All done, no errors")
545 except RefCounterException as e:
546 print("FAIL: Got exception: %s" % e)
547 raise