| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 file changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -32,6 +32,8 @@ | |||
| 32 | 32 | import io | |
| 33 | 33 | import contextlib | |
| 34 | 34 | import gc | |
| 35 | + import os | ||
| 36 | + import functools | ||
| 35 | 37 | from test.support import run_unittest | |
| 36 | 38 | ||
| 37 | 39 | # emit warnings about uncollectable objects | |
@@ -65,10 +67,75 @@ def require_one_thread(testcase): | |||
| 65 | 67 | return testcase | |
| 66 | 68 | ||
| 67 | 69 | ||
| 68 | - def get_current_watchdog_list(): | ||
| 70 | + def testcase_leaks_references(leak_reason, soft_switching=None): | ||
| 71 | + """Skip test, which leak references during leak tests. | ||
| 72 | + | ||
| 73 | + If a test leaks references, which happens if the thread of a tasklet with a C-stack dies, | ||
| 74 | + you must decorate the test case with this decorator. | ||
| 75 | + | ||
| 76 | + Note: If you know the leaking object, you can try to use the hackish function | ||
| 77 | + decref_leaked_object() | ||
| 78 | + """ | ||
| 79 | + def decorator(testcase): | ||
| 80 | + @functools.wraps(testcase) | ||
| 81 | + def wrapper(self): | ||
| 82 | + if soft_switching is not None and stackless.enable_softswitch(None) != soft_switching: | ||
| 83 | + # the leak happens only if soft switching is enables/disabled | ||
| 84 | + return testcase(self) | ||
| 85 | + for frameinfo in inspect.stack(0): | ||
| 86 | + # print("frameinfo[3]", frameinfo[3], file=sys.stderr) | ||
| 87 | + if frameinfo[3] == "dash_R": | ||
| 88 | + # it is a test.regrtest -R: run | ||
| 89 | + return self.skipTest("Test leaks references: " + leak_reason) | ||
| 90 | + return testcase(self) | ||
| 91 | + return wrapper | ||
| 92 | + return decorator | ||
| 93 | + | ||
| 94 | + | ||
| 95 | + # decref_leaked_object() works, but it is not as useful as I expected, because | ||
| 96 | + # a C-stack usually contains several references to None and other objects. All in all | ||
| 97 | + # there are more references than we can identify and handle manually. | ||
| 98 | + # | ||
| 99 | + # _Py_DecRef = ctypes.pythonapi.Py_DecRef | ||
| 100 | + # _Py_DecRef.argtypes = [ctypes.py_object] | ||
| 101 | + # _Py_DecRef.restype = None | ||
| 102 | + # | ||
| 103 | + # | ||
| 104 | + # def decref_leaked_object(obj): | ||
| 105 | + # """Py_DecRef an object, that would otherwise leak. | ||
| 106 | + # | ||
| 107 | + # Used to forget references held by a C-stack, whose thread already died. | ||
| 108 | + # """ | ||
| 109 | + # # Check, that obj is really leaked | ||
| 110 | + # def get_rc_delta(obj): | ||
| 111 | + # gc.collect() | ||
| 112 | + # rc = sys.getrefcount(obj) - 1 | ||
| 113 | + # n_refferers = len(gc.get_referrers(obj)) | ||
| 114 | + # return rc - n_refferers | ||
| 115 | + # # Quality check | ||
| 116 | + # assert get_rc_delta(object()) == 0 | ||
| 117 | + # delta = get_rc_delta(obj) | ||
| 118 | + # if delta > 0: | ||
| 119 | + # _Py_DecRef(obj) | ||
| 120 | + # else: | ||
| 121 | + # raise RuntimeError("There is no ref leak for obj. Missing referrers %d" % (delta,)) | ||
| 122 | + | ||
| 123 | + | ||
| 124 | + def get_watchdog_list(threadid): | ||
| 125 | + """Get the watchdog list of a thread. | ||
| 126 | + | ||
| 127 | + Contrary to :func:`get_current_watchdog_list` this function does | ||
| 128 | + not create a watchdog list, if it does not already exist. | ||
| 129 | + """ | ||
| 69 | 130 | # The second argument of get_thread_info() is intentionally undocumented. | |
| 70 | 131 | # See C source. | |
| 71 | - watchdog_list = stackless.get_thread_info(-1, 1 << 31)[3] | ||
| 132 | + return stackless.get_thread_info(threadid, 1 << 31)[3] | ||
| 133 | + | ||
| 134 | + | ||
| 135 | + def get_current_watchdog_list(): | ||
| 136 | + """Get the watchdog list of the current thread | ||
| 137 | + """ | ||
| 138 | + watchdog_list = get_watchdog_list(-1) | ||
| 72 | 139 | if isinstance(watchdog_list, list): | |
| 73 | 140 | return watchdog_list | |
| 74 | 141 | # The watchdog list has not been created. Force its creation. | |
@@ -87,11 +154,72 @@ def get_current_watchdog_list(): | |||
| 87 | 154 | t.insert() | |
| 88 | 155 | if scheduled: | |
| 89 | 156 | assert stackless.current.next == scheduled[0] | |
| 90 | - watchdog_list = stackless.get_thread_info(-1, 1 << 31)[3] | ||
| 157 | + watchdog_list = get_watchdog_list(-1) | ||
| 91 | 158 | assert isinstance(watchdog_list, list) | |
| 92 | 159 | return watchdog_list | |
| 93 | 160 | ||
| 94 | 161 | ||
| 162 | + def get_tasklets_with_cstate(): | ||
| 163 | + """Return a list of all tasklets with a C-stack. | ||
| 164 | + """ | ||
| 165 | + tlets = [] | ||
| 166 | + current = stackless.current | ||
| 167 | + if current.nesting_level > 0: | ||
| 168 | + tlets.append(current) | ||
| 169 | + with stackless.atomic(): | ||
| 170 | + cscurrent = current.cstate | ||
| 171 | + cs = cscurrent.next | ||
| 172 | + while cs is not cscurrent: | ||
| 173 | + t = cs.task | ||
| 174 | + if (t is not None and | ||
| 175 | + t.cstate is cs and | ||
| 176 | + t.alive and | ||
| 177 | + t.nesting_level > 0): | ||
| 178 | + assert t not in tlets | ||
| 179 | + tlets.append(t) | ||
| 180 | + cs = cs.next | ||
| 181 | + return tlets | ||
| 182 | + | ||
| 183 | + | ||
| 184 | + class SwitchRecorder(object): | ||
| 185 | + def __init__(self, old_scb): | ||
| 186 | + self.ids = {} | ||
| 187 | + if stackless.main is stackless.current: | ||
| 188 | + self.ids[id(stackless.main)] = "main/current" | ||
| 189 | + else: | ||
| 190 | + self.ids[id(stackless.main)] = "main" | ||
| 191 | + self.ids[id(stackless.current)] = "current" | ||
| 192 | + self.switches = [] | ||
| 193 | + self.old_scb = old_scb | ||
| 194 | + | ||
| 195 | + def __call__(self, prev_tlet, next_tlet): | ||
| 196 | + # print("%s -> %s" % (id(prev_tlet), id(next_tlet)), file=sys.stderr) | ||
| 197 | + self.switches.append((None if prev_tlet is None else id(prev_tlet), | ||
| 198 | + None if next_tlet is None else id(next_tlet))) | ||
| 199 | + if self.old_scb is not None: | ||
| 200 | + return self.old_scb(prev_tlet, next_tlet) | ||
| 201 | + | ||
| 202 | + def id2str(self, tlet_id): | ||
| 203 | + if tlet_id is None: | ||
| 204 | + return "None" | ||
| 205 | + try: | ||
| 206 | + nr = self.ids[tlet_id] | ||
| 207 | + except KeyError: | ||
| 208 | + self.ids[tlet_id] = nr = "tlet %d" % (len(self.ids),) | ||
| 209 | + return nr | ||
| 210 | + | ||
| 211 | + def __str__(self): | ||
| 212 | + s = ["", "tasklet switches (%d)" % (len(self.switches),)] | ||
| 213 | + for (p, n) in self.switches: | ||
| 214 | + s.append("%s -> %s" % (self.id2str(p), self.id2str(n))) | ||
| 215 | + return os.linesep.join(s) | ||
| 216 | + | ||
| 217 | + def print(self, file=None): | ||
| 218 | + if file is None: | ||
| 219 | + file = sys.stderr | ||
| 220 | + print(str(self), file=file) | ||
| 221 | + | ||
| 222 | + | ||
| 95 | 223 | class StacklessTestCaseMixin(object): | |
| 96 | 224 | def skipUnlessSoftswitching(self): | |
| 97 | 225 | if not stackless.enable_softswitch(None): | |
@@ -190,6 +318,109 @@ def _checkSignature(self, func, nb_mandatory, accept_arbitrary, additionalArg, * | |||
| 190 | 318 | # only required args as kw-args | |
| 191 | 319 | yield func(**dict((k, kwargs[k]) for k in names[:nb_mandatory])) | |
| 192 | 320 | ||
| 321 | + def trace_tasklet_switches(self): | ||
| 322 | + old_scb = stackless.get_schedule_callback() | ||
| 323 | + switch_recorder = SwitchRecorder(old_scb) | ||
| 324 | + self.addCleanup(stackless.set_schedule_callback, old_scb) | ||
| 325 | + self.addCleanup(switch_recorder.print) | ||
| 326 | + stackless.set_schedule_callback(switch_recorder) | ||
| 327 | + | ||
| 328 | + def register_tasklet_name(self, tlet, name): | ||
| 329 | + scb = stackless.get_schedule_callback() | ||
| 330 | + if isinstance(scb, SwitchRecorder): | ||
| 331 | + if isinstance(tlet, stackless.tasklet): | ||
| 332 | + tlet = id(tlet) | ||
| 333 | + self.assertIsInstance(tlet, int) | ||
| 334 | + scb.ids[tlet] = name | ||
| 335 | + | ||
| 336 | + def refleak_hunting_record_baseline(self): | ||
| 337 | + """Record a baseline for hunting reference leaks. | ||
| 338 | + | ||
| 339 | + This method and the methods refleak_hunting_find_leaks() and | ||
| 340 | + refleak_hunting_print() can be used to identify reference leaks. | ||
| 341 | + | ||
| 342 | + The basic idea is simple: For objects which take part in garbage collection, | ||
| 343 | + the number of referrers matches the reference count. In case of a missing Py_DECREF, | ||
| 344 | + we can observe a surplus reference count. Unfortunately, in reality the garbage collector | ||
| 345 | + doesn't care about references from non container objects or local variables of C-functions. | ||
| 346 | + Therefore, we record a baseline first. | ||
| 347 | + | ||
| 348 | + Application: Just call bracket the suspicious code like this:: | ||
| 349 | + | ||
| 350 | + self.refleak_hunting_record_baseline() | ||
| 351 | + # suspicious code goes here | ||
| 352 | + ... | ||
| 353 | + self.refleak_hunting_find_leaks() | ||
| 354 | + | ||
| 355 | + In case you need this functions in a C-Python test suite module, use the following | ||
| 356 | + code snippet:: | ||
| 357 | + | ||
| 358 | + import os | ||
| 359 | + import sysconfig | ||
| 360 | + import sys | ||
| 361 | + sys.path.append(os.path.join(sysconfig.get_path("data"), "Stackless", "unittests")) | ||
| 362 | + from support import StacklessTestCaseMixin | ||
| 363 | + | ||
| 364 | + ... | ||
| 365 | + | ||
| 366 | + class TestCase(unittest.TestCase, StacklessTestCaseMixin): | ||
| 367 | + ... | ||
| 368 | + """ | ||
| 369 | + gc.collect() | ||
| 370 | + self.singleton_ref_counts_baseline = [sys.getrefcount(None)] | ||
| 371 | + oids = set(id(o) for o in gc.get_objects() if len(gc.get_referrers(o)) <= 2) | ||
| 372 | + oids.add(id(sys._getframe(1))) | ||
| 373 | + self.oids_insufficient_referrers = oids | ||
| 374 | + | ||
| 375 | + def refleak_hunting_find_leaks(self, objects=None): | ||
| 376 | + """Detect potential leaked objects. | ||
| 377 | + """ | ||
| 378 | + gc.collect() | ||
| 379 | + if objects is None: | ||
| 380 | + objects = gc.get_objects() | ||
| 381 | + gc.collect() | ||
| 382 | + self.singleton_ref_counts = [sys.getrefcount(None)] | ||
| 383 | + self.assertIsInstance(objects, list) | ||
| 384 | + oids = self.oids_insufficient_referrers | ||
| 385 | + # add the watchdog list to the baseline. | ||
| 386 | + wd_list = get_watchdog_list(-1) | ||
| 387 | + if isinstance(wd_list, list): | ||
| 388 | + oids.add(id(wd_list)) | ||
| 389 | + # add the current frame | ||
| 390 | + oids.add(id(sys._getframe())) | ||
| 391 | + # add the parent frame | ||
| 392 | + oids.add(id(sys._getframe(1))) | ||
| 393 | + # compute objects, which are alive, but have to less referrers to | ||
| 394 | + # justify their live. | ||
| 395 | + candidates = [] | ||
| 396 | + for i in xrange(len(objects)): | ||
| 397 | + if id(objects[i]) in oids: | ||
| 398 | + continue | ||
| 399 | + if len(gc.get_referrers(objects[i])) <= 1: | ||
| 400 | + candidates.append(objects[i]) | ||
| 401 | + del objects[:] | ||
| 402 | + del self.oids_insufficient_referrers | ||
| 403 | + self.refleak_candidates = candidates | ||
| 404 | + self.addCleanup(self.refleak_hunting_print) | ||
| 405 | + return candidates | ||
| 406 | + | ||
| 407 | + def refleak_hunting_print(self, file=None): | ||
| 408 | + """Print the result. | ||
| 409 | + | ||
| 410 | + Print the result of the reference leak hunting to sys.stderr. | ||
| 411 | + """ | ||
| 412 | + if file is None: | ||
| 413 | + file = sys.stderr | ||
| 414 | + print("", file=file) | ||
| 415 | + print("Leak hunting:", file=file) | ||
| 416 | + for (i, name) in enumerate(("None",)): | ||
| 417 | + a = self.singleton_ref_counts_baseline[i] | ||
| 418 | + b = self.singleton_ref_counts[i] | ||
| 419 | + print("Ref count of %s: %d - %d = %d" % (name, a, b, a - b), file=file) | ||
| 420 | + print("Number of candidates", len(self.refleak_candidates), file=file) | ||
| 421 | + for (i, o) in enumerate(self.refleak_candidates): | ||
| 422 | + print("%d: %r %r" % (i, type(o), o), file=file) | ||
| 423 | + | ||
| 193 | 424 | ||
| 194 | 425 | # call the class method prepare_test_methods(cls) after creating the | |
| 195 | 426 | # class. This method can be used to modify the newly created class | |
@@ -251,7 +482,8 @@ def prepare_pickle_test_method(cls, func, name=None): | |||
| 251 | 482 | for i in range(0, pickle.HIGHEST_PROTOCOL + 1): | |
| 252 | 483 | for p_letter in ("C", "P"): | |
| 253 | 484 | def test(self, method=func, proto=i, pickle_module=p_letter, unpickle_module=p_letter): | |
| 254 | - self.assertTrue(self._StacklessTestCase__setup_called, "Broken test case: it didn't call super(..., self).setUp()") | ||
| 485 | + self.assertTrue(self._StacklessTestCase__setup_called, | ||
| 486 | + "Broken test case: it didn't call super(..., self).setUp()") | ||
| 255 | 487 | self._pickle_protocol = proto | |
| 256 | 488 | self._pickle_module = pickle_module | |
| 257 | 489 | self._unpickle_module = unpickle_module | |
@@ -284,7 +516,7 @@ def prepare_test_methods(cls): | |||
| 284 | 516 | if inspect.ismethod(m): | |
| 285 | 517 | m = m.__func__ | |
| 286 | 518 | prepare = getattr(m, "prepare", cls.prepare_test_method) | |
| 287 | - for x in prepare.__func__(cls, m, n): | ||
| 519 | + for x in prepare.__func__(cls, m, n): # @UnusedVariable | ||
| 288 | 520 | pass | |
| 289 | 521 | ||
| 290 | 522 | __setup_called = False | |
@@ -317,20 +549,45 @@ def setUpStacklessTestCase(self): | |||
| 317 | 549 | self.addCleanup(stackless.enable_softswitch, stackless.enable_softswitch(self.__enable_softswitch)) | |
| 318 | 550 | ||
| 319 | 551 | self.__active_test_cases[id(self)] = self | |
| 552 | + watchdog_list = get_watchdog_list(-1) | ||
| 553 | + if watchdog_list is not None: | ||
| 554 | + self.assertListEqual(watchdog_list, [None], "Watchdog list is not empty: " + repr(watchdog_list)) | ||
| 320 | 555 | if withThreads and self.__preexisting_threads is None: | |
| 321 | 556 | self.__preexisting_threads = frozenset(threading.enumerate()) | |
| 557 | + for (watchdog_list, tid) in [(get_watchdog_list(tid), tid) | ||
| 558 | + for tid in stackless.threads if tid != stackless.current.thread_id]: | ||
| 559 | + if watchdog_list is None: | ||
| 560 | + continue | ||
| 561 | + self.assertListEqual(watchdog_list, [None], | ||
| 562 | + "Thread %d: watchdog list is not empty: %r" % (tid, watchdog_list)) | ||
| 322 | 563 | return len(self.__preexisting_threads) | |
| 323 | 564 | return 1 | |
| 324 | 565 | ||
| 325 | 566 | def setUp(self): | |
| 326 | - self.assertEqual(stackless.getruncount(), 1, "Leakage from other tests, with %d tasklets still in the scheduler" % (stackless.getruncount() - 1)) | ||
| 567 | + self.assertEqual(stackless.getruncount(), 1, | ||
| 568 | + "Leakage from other tests, with %d tasklets still in the scheduler" % | ||
| 569 | + (stackless.getruncount() - 1)) | ||
| 327 | 570 | expected_thread_count = self.setUpStacklessTestCase() | |
| 328 | 571 | if withThreads: | |
| 329 | 572 | active_count = threading.active_count() | |
| 330 | - self.assertEqual(active_count, expected_thread_count, "Leakage from other threads, with %d threads running (%d expected)" % (active_count, expected_thread_count)) | ||
| 573 | + self.assertEqual(active_count, expected_thread_count, | ||
| 574 | + "Leakage from other threads, with %d threads running (%d expected)" % | ||
| 575 | + (active_count, expected_thread_count)) | ||
| 331 | 576 | ||
| 332 | 577 | def tearDown(self): | |
| 333 | - # Tasklets created in pickling tests can be left in the scheduler when they finish. We can feel free to | ||
| 578 | + # Test, that stackless errorhandler is None and reset it | ||
| 579 | + self.assertIsNone(stackless.set_error_handler(None)) | ||
| 580 | + | ||
| 581 | + # Test, that switch_trap level is 0 and set the level back to 0 | ||
| 582 | + try: | ||
| 583 | + # get the level without changing it | ||
| 584 | + st_level = stackless.switch_trap(0) | ||
| 585 | + self.assertEqual(st_level, 0, "switch_trap is %d" % (st_level,)) | ||
| 586 | + except AssertionError: | ||
| 587 | + # change the level so that the result is 0 | ||
| 588 | + stackless.switch_trap(-st_level) | ||
| 589 | + raise | ||
| 590 | + # Tasklets created in various tests can be left in the scheduler when they finish. We can feel free to | ||
| 334 | 591 | # clean them up for the tests. Any tests that expect to exit with no leaked tasklets should do explicit | |
| 335 | 592 | # assertions to check. | |
| 336 | 593 | self.assertTrue(self.__setup_called, "Broken test case: it didn't call super(..., self).setUp()") | |
@@ -341,9 +598,27 @@ def tearDown(self): | |||
| 341 | 598 | next_ = current.next | |
| 342 | 599 | current.kill() | |
| 343 | 600 | current = next_ | |
| 601 | + # Tasklets with C-stack can create reference leaks, if the C-stack holds a reference, | ||
| 602 | + # that keeps the tasklet-object alive. A common case is the call of a tasklet or channel method, | ||
| 603 | + # which causes a tasklet switch. The transient bound-method object keeps the tasklet alive. | ||
| 604 | + # Here we kill such tasklets. | ||
| 605 | + for current in get_tasklets_with_cstate(): | ||
| 606 | + if current.blocked: | ||
| 607 | + # print("Killing blocked tasklet", current, file=sys.stderr) | ||
| 608 | + current.kill() | ||
| 344 | 609 | run_count = stackless.getruncount() | |
| 345 | - self.assertEqual(run_count, 1, "Leakage from this test, with %d tasklets still in the scheduler" % (run_count - 1)) | ||
| 610 | + self.assertEqual(run_count, 1, | ||
| 611 | + "Leakage from this test, with %d tasklets still in the scheduler" % (run_count - 1)) | ||
| 612 | + watchdog_list = get_watchdog_list(-1) | ||
| 613 | + if watchdog_list is not None: | ||
| 614 | + self.assertListEqual(watchdog_list, [None], "Watchdog list is not empty: " + repr(watchdog_list)) | ||
| 346 | 615 | if withThreads: | |
| 616 | + for (watchdog_list, tid) in [(get_watchdog_list(tid), tid) | ||
| 617 | + for tid in stackless.threads if tid != stackless.current.thread_id]: | ||
| 618 | + if watchdog_list is None: | ||
| 619 | + continue | ||
| 620 | + self.assertListEqual(watchdog_list, [None], | ||
| 621 | + "Thread %d: watchdog list is not empty: %r" % (tid, watchdog_list)) | ||
| 347 | 622 | preexisting_threads = self.__preexisting_threads | |
| 348 | 623 | self.__preexisting_threads = None # avoid pickling problems, see _addSkip | |
| 349 | 624 | expected_thread_count = len(preexisting_threads) | |
@@ -355,7 +630,9 @@ def tearDown(self): | |||
| 355 | 630 | while activeThreads: | |
| 356 | 631 | activeThreads.pop().join(0.5) | |
| 357 | 632 | active_count = threading.active_count() | |
| 358 | - self.assertEqual(active_count, expected_thread_count, "Leakage from other threads, with %d threads running (%d expected)" % (active_count, expected_thread_count)) | ||
| 633 | + self.assertEqual(active_count, expected_thread_count, | ||
| 634 | + "Leakage from other threads, with %d threads running (%d expected)" % | ||
| 635 | + (active_count, expected_thread_count)) | ||
| 359 | 636 | gc.collect() # emits warnings about uncollectable objects after each test | |
| 360 | 637 | ||
| 361 | 638 | def dumps(self, obj, protocol=None, *, fix_imports=True): | |
| Back | FazBrowse Home | New Git URL |
0 commit comments