Commit c6686730 authored by Jason Madden's avatar Jason Madden

Add util.print_run_info and limit params for stack traces.

parent c7efa2b7
...@@ -11,7 +11,7 @@ from collections import deque ...@@ -11,7 +11,7 @@ from collections import deque
from itertools import islice as _islice from itertools import islice as _islice
from gevent import monkey from gevent import monkey
from gevent._compat import PY3 from gevent._compat import thread_mod_name
__all__ = [ __all__ = [
...@@ -21,9 +21,8 @@ __all__ = [ ...@@ -21,9 +21,8 @@ __all__ = [
] ]
thread_name = '_thread' if PY3 else 'thread' start_new_thread, Lock, get_thread_ident, = monkey.get_original(thread_mod_name, [
start_new_thread, Lock, = monkey.get_original(thread_name, [ 'start_new_thread', 'allocate_lock', 'get_ident',
'start_new_thread', 'allocate_lock',
]) ])
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
from __future__ import absolute_import from __future__ import absolute_import
import sys import sys
import os import os
from weakref import ref as wref
from greenlet import greenlet as RawGreenlet
from gevent._compat import integer_types from gevent._compat import integer_types
from gevent.hub import _get_hub_noargs as get_hub from gevent.hub import _get_hub_noargs as get_hub
from gevent.hub import getcurrent from gevent.hub import getcurrent
...@@ -11,12 +16,39 @@ from gevent.event import AsyncResult ...@@ -11,12 +16,39 @@ from gevent.event import AsyncResult
from gevent.greenlet import Greenlet from gevent.greenlet import Greenlet
from gevent.pool import GroupMappingMixin from gevent.pool import GroupMappingMixin
from gevent.lock import Semaphore from gevent.lock import Semaphore
from gevent._threading import Lock, Queue, start_new_thread
from gevent._threading import Lock
from gevent._threading import Queue
from gevent._threading import start_new_thread
from gevent._threading import get_thread_ident
__all__ = [
'ThreadPool',
'ThreadResult',
]
__all__ = ['ThreadPool',
'ThreadResult']
class _WorkerGreenlet(RawGreenlet):
# Exists to produce a more useful repr for worker pool
# threads/greenlets.
def __init__(self, threadpool):
RawGreenlet.__init__(self, threadpool._worker)
self.thread_ident = get_thread_ident()
self._threadpool_wref = wref(threadpool)
# Inform the gevent.util.GreenletTree that this should be
# considered the root (for printing purposes) and to
# ignore the parent attribute. (We can't set parent to None.)
self.greenlet_tree_is_root = True
self.parent.greenlet_tree_is_ignored = True
def __repr__(self):
return "<ThreadPoolWorker at 0x%x thread_ident=0x%x %s>" % (
id(self),
self.thread_ident,
self._threadpool_wref())
class ThreadPool(GroupMappingMixin): class ThreadPool(GroupMappingMixin):
""" """
...@@ -58,7 +90,11 @@ class ThreadPool(GroupMappingMixin): ...@@ -58,7 +90,11 @@ class ThreadPool(GroupMappingMixin):
maxsize = property(_get_maxsize, _set_maxsize) maxsize = property(_get_maxsize, _set_maxsize)
def __repr__(self): def __repr__(self):
return '<%s at 0x%x %s/%s/%s>' % (self.__class__.__name__, id(self), len(self), self.size, self.maxsize) return '<%s at 0x%x %s/%s/%s hub=<%s at 0x%x thread_ident=0x%s>>' % (
self.__class__.__name__,
id(self),
len(self), self.size, self.maxsize,
self.hub.__class__.__name__, id(self.hub), self.hub.thread_ident)
def __len__(self): def __len__(self):
# XXX just do unfinished_tasks property # XXX just do unfinished_tasks property
...@@ -155,7 +191,7 @@ class ThreadPool(GroupMappingMixin): ...@@ -155,7 +191,7 @@ class ThreadPool(GroupMappingMixin):
with self._lock: with self._lock:
self._size += 1 self._size += 1
try: try:
start_new_thread(self._worker, ()) start_new_thread(self.__trampoline, ())
except: except:
with self._lock: with self._lock:
self._size -= 1 self._size -= 1
...@@ -210,6 +246,13 @@ class ThreadPool(GroupMappingMixin): ...@@ -210,6 +246,13 @@ class ThreadPool(GroupMappingMixin):
if hub is not None and hub.periodic_monitoring_thread is not None: if hub is not None and hub.periodic_monitoring_thread is not None:
hub.periodic_monitoring_thread.ignore_current_greenlet_blocking() hub.periodic_monitoring_thread.ignore_current_greenlet_blocking()
def __trampoline(self):
# The target that we create new threads with. It exists
# solely to create the _WorkerGreenlet and switch to it.
# (the __class__ of a raw greenlet cannot be changed.)
g = _WorkerGreenlet(self)
g.switch()
def _worker(self): def _worker(self):
# pylint:disable=too-many-branches # pylint:disable=too-many-branches
need_decrease = True need_decrease = True
......
...@@ -5,20 +5,24 @@ Low-level utilities. ...@@ -5,20 +5,24 @@ Low-level utilities.
from __future__ import absolute_import, print_function, division from __future__ import absolute_import, print_function, division
import gc
import functools import functools
import gc
import pprint import pprint
import sys
import traceback import traceback
from greenlet import getcurrent from greenlet import getcurrent
from greenlet import greenlet as RawGreenlet from greenlet import greenlet as RawGreenlet
from gevent._compat import PYPY from gevent._compat import PYPY
from gevent._compat import thread_mod_name
from gevent._util import _NONE
__all__ = [ __all__ = [
'wrap_errors',
'format_run_info', 'format_run_info',
'print_run_info',
'GreenletTree', 'GreenletTree',
'wrap_errors',
] ]
# PyPy is very slow at formatting stacks # PyPy is very slow at formatting stacks
...@@ -81,41 +85,77 @@ class wrap_errors(object): ...@@ -81,41 +85,77 @@ class wrap_errors(object):
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self.__func, name) return getattr(self.__func, name)
def print_run_info(thread_stacks=True, greenlet_stacks=True, limit=_NONE, file=None):
"""
Call `format_run_info` and print the results to *file*.
If *file* is not given, `sys.stderr` will be used.
.. versionadded:: 1.3b1
"""
lines = format_run_info(thread_stacks=thread_stacks,
greenlet_stacks=greenlet_stacks,
limit=limit)
file = sys.stderr if file is None else file
for l in lines:
print(l, file=file)
def format_run_info(thread_stacks=True, def format_run_info(thread_stacks=True,
greenlet_stacks=True, greenlet_stacks=True,
limit=_NONE,
current_thread_ident=None): current_thread_ident=None):
""" """
format_run_info(thread_stacks=True, greenlet_stacks=True) -> [str] format_run_info(thread_stacks=True, greenlet_stacks=True, limit=None) -> [str]
Request information about the running threads of the current process. Request information about the running threads of the current process.
This is a debugging utility. Its output has no guarantees other than being This is a debugging utility. Its output has no guarantees other than being
intended for human consumption. intended for human consumption.
:keyword bool thread_stacks: If true, then include the stacks for
running threads.
:keyword bool greenlet_stacks: If true, then include the stacks for
running greenlets. (Spawning stacks will always be printed.)
Setting this to False can reduce the output volume considerably
without reducing the overall information if *thread_stacks* is true
and you can associate a greenlet to a thread (using ``thread_ident``
printed values).
:keyword int limit: If given, passed directly to `traceback.format_stack`.
If not given, this defaults to the whole stack under CPython, and a
smaller stack under PyPy.
:return: A sequence of text lines detailing the stacks of running :return: A sequence of text lines detailing the stacks of running
threads and greenlets. (One greenlet will duplicate one thread, threads and greenlets. (One greenlet will duplicate one thread,
the current thread and greenlet. If there are multiple running threads, the current thread and greenlet. If there are multiple running threads,
the stack for the current greenlet may be incorrectly duplicated in multiple the stack for the current greenlet may be incorrectly duplicated in multiple
greenlets.) greenlets.)
Extra information about Extra information about
:class:`gevent.greenlet.Greenlet` object will also be returned. :class:`gevent.Greenlet` object will also be returned.
.. versionadded:: 1.3a1 .. versionadded:: 1.3a1
.. versionchanged:: 1.3a2 .. versionchanged:: 1.3a2
Renamed from ``dump_stacks`` to reflect the fact that this Renamed from ``dump_stacks`` to reflect the fact that this
prints additional information about greenlets, including their prints additional information about greenlets, including their
spawning stack, parent, locals, and any spawn tree locals. spawning stack, parent, locals, and any spawn tree locals.
.. versionchanged:: 1.3b1
Added the *thread_stacks*, *greenlet_stacks*, and *limit* params.
""" """
if current_thread_ident is None:
from gevent import monkey
current_thread_ident = monkey.get_original(thread_mod_name, 'get_ident')()
lines = [] lines = []
_format_thread_info(lines, thread_stacks, current_thread_ident) limit = _STACK_LIMIT if limit is _NONE else limit
_format_greenlet_info(lines, greenlet_stacks) _format_thread_info(lines, thread_stacks, limit, current_thread_ident)
_format_greenlet_info(lines, greenlet_stacks, limit)
return lines return lines
def _format_thread_info(lines, thread_stacks, current_thread_ident):
def _format_thread_info(lines, thread_stacks, limit, current_thread_ident):
import threading import threading
import sys
threads = {th.ident: th for th in threading.enumerate()} threads = {th.ident: th for th in threading.enumerate()}
...@@ -134,7 +174,7 @@ def _format_thread_info(lines, thread_stacks, current_thread_ident): ...@@ -134,7 +174,7 @@ def _format_thread_info(lines, thread_stacks, current_thread_ident):
name = '%s) (CURRENT' % (name,) name = '%s) (CURRENT' % (name,)
lines.append('Thread 0x%x (%s)\n' % (thread_ident, name)) lines.append('Thread 0x%x (%s)\n' % (thread_ident, name))
if thread_stacks: if thread_stacks:
lines.append(''.join(traceback.format_stack(frame, _STACK_LIMIT))) lines.append(''.join(traceback.format_stack(frame, limit)))
else: else:
lines.append('\t...stack elided...') lines.append('\t...stack elided...')
...@@ -145,14 +185,17 @@ def _format_thread_info(lines, thread_stacks, current_thread_ident): ...@@ -145,14 +185,17 @@ def _format_thread_info(lines, thread_stacks, current_thread_ident):
del lines del lines
del threads del threads
def _format_greenlet_info(lines, greenlet_stacks): def _format_greenlet_info(lines, greenlet_stacks, limit):
# Use the gc module to inspect all objects to find the greenlets # Use the gc module to inspect all objects to find the greenlets
# since there isn't a global registry # since there isn't a global registry
lines.append('*' * 80) lines.append('*' * 80)
lines.append('* Greenlets') lines.append('* Greenlets')
lines.append('*' * 80) lines.append('*' * 80)
for tree in GreenletTree.forest(): for tree in GreenletTree.forest():
lines.extend(tree.format_lines(details={'running_stacks': greenlet_stacks})) lines.extend(tree.format_lines(details={
'running_stacks': greenlet_stacks,
'running_stack_limit': limit,
}))
del lines del lines
...@@ -273,6 +316,7 @@ class GreenletTree(object): ...@@ -273,6 +316,7 @@ class GreenletTree(object):
DEFAULT_DETAILS = { DEFAULT_DETAILS = {
'running_stacks': True, 'running_stacks': True,
'running_stack_limit': _STACK_LIMIT,
'spawning_stacks': True, 'spawning_stacks': True,
'locals': True, 'locals': True,
} }
...@@ -309,9 +353,9 @@ class GreenletTree(object): ...@@ -309,9 +353,9 @@ class GreenletTree(object):
return self.format(False) return self.format(False)
@staticmethod @staticmethod
def __render_tb(tree, label, frame): def __render_tb(tree, label, frame, limit):
tree.child_data(label) tree.child_data(label)
tb = ''.join(traceback.format_stack(frame, _STACK_LIMIT)) tb = ''.join(traceback.format_stack(frame, limit))
tree.child_multidata(tb) tree.child_multidata(tb)
@staticmethod @staticmethod
...@@ -349,12 +393,14 @@ class GreenletTree(object): ...@@ -349,12 +393,14 @@ class GreenletTree(object):
tree.child_data('Monitoring Thread:' + repr(self.greenlet.gevent_monitoring_thread())) tree.child_data('Monitoring Thread:' + repr(self.greenlet.gevent_monitoring_thread()))
if self.greenlet and tree.details and tree.details['running_stacks']: if self.greenlet and tree.details and tree.details['running_stacks']:
self.__render_tb(tree, 'Running:', self.greenlet.gr_frame) self.__render_tb(tree, 'Running:', self.greenlet.gr_frame,
tree.details['running_stack_limit'])
spawning_stack = getattr(self.greenlet, 'spawning_stack', None) spawning_stack = getattr(self.greenlet, 'spawning_stack', None)
if spawning_stack and tree.details and tree.details['spawning_stacks']: if spawning_stack and tree.details and tree.details['spawning_stacks']:
self.__render_tb(tree, 'Spawned at:', spawning_stack) # We already placed a limit on the spawning stack when we captured it.
self.__render_tb(tree, 'Spawned at:', spawning_stack, None)
spawning_parent = self.__spawning_parent(self.greenlet) spawning_parent = self.__spawning_parent(self.greenlet)
tree_locals = getattr(self.greenlet, 'spawn_tree_locals', None) tree_locals = getattr(self.greenlet, 'spawn_tree_locals', None)
...@@ -399,7 +445,7 @@ class GreenletTree(object): ...@@ -399,7 +445,7 @@ class GreenletTree(object):
@staticmethod @staticmethod
def _root_greenlet(greenlet): def _root_greenlet(greenlet):
while greenlet.parent is not None: while greenlet.parent is not None and not getattr(greenlet, 'greenlet_tree_is_root', False):
greenlet = greenlet.parent greenlet = greenlet.parent
return greenlet return greenlet
...@@ -416,6 +462,8 @@ class GreenletTree(object): ...@@ -416,6 +462,8 @@ class GreenletTree(object):
for ob in gc.get_objects(): for ob in gc.get_objects():
if not isinstance(ob, RawGreenlet): if not isinstance(ob, RawGreenlet):
continue continue
if getattr(ob, 'greenlet_tree_is_ignored', False):
continue
spawn_parent = cls.__spawning_parent(ob) spawn_parent = cls.__spawning_parent(ob)
......
...@@ -10,10 +10,11 @@ from gevent.threadpool import ThreadPool ...@@ -10,10 +10,11 @@ from gevent.threadpool import ThreadPool
import gevent import gevent
from greentest import ExpectedException from greentest import ExpectedException
from greentest import six from greentest import six
from greentest import PYPY
import gc import gc
PYPY = hasattr(sys, 'pypy_version_info') # pylint:disable=too-many-ancestors
@contextlib.contextmanager @contextlib.contextmanager
...@@ -151,6 +152,22 @@ if greentest.PYPY and (greentest.WIN or greentest.RUN_COVERAGE): ...@@ -151,6 +152,22 @@ if greentest.PYPY and (greentest.WIN or greentest.RUN_COVERAGE):
class TestPool(_AbstractPoolTest): class TestPool(_AbstractPoolTest):
def test_greenlet_class(self):
from greenlet import getcurrent
from gevent.threadpool import _WorkerGreenlet
worker_greenlet = self.pool.apply(getcurrent)
self.assertIsInstance(worker_greenlet, _WorkerGreenlet)
r = repr(worker_greenlet)
self.assertIn('ThreadPoolWorker', r)
self.assertIn('thread_ident', r)
self.assertIn('hub=', r)
from gevent.util import format_run_info
info = '\n'.join(format_run_info())
self.assertIn("<ThreadPoolWorker", info)
def test_apply(self): def test_apply(self):
papply = self.pool.apply papply = self.pool.apply
self.assertEqual(papply(sqr, (5,)), sqr(5)) self.assertEqual(papply(sqr, (5,)), sqr(5))
......
...@@ -14,6 +14,8 @@ import gevent ...@@ -14,6 +14,8 @@ import gevent
from gevent import util from gevent import util
from gevent import local from gevent import local
from gevent._compat import NativeStrIO
class MyLocal(local.local): class MyLocal(local.local):
def __init__(self, foo): def __init__(self, foo):
self.foo = foo self.foo = foo
...@@ -40,14 +42,15 @@ class TestFormat(greentest.TestCase): ...@@ -40,14 +42,15 @@ class TestFormat(greentest.TestCase):
l = MyLocal(42) l = MyLocal(42)
assert l assert l
gevent.getcurrent().spawn_tree_locals['a value'] = 42 gevent.getcurrent().spawn_tree_locals['a value'] = 42
g = gevent.spawn(util.format_run_info) io = NativeStrIO()
g = gevent.spawn(util.print_run_info, file=io)
g.join() g.join()
return g.value return io.getvalue()
g = gevent.spawn(root) g = gevent.spawn(root)
g.name = 'Printer' g.name = 'Printer'
g.join() g.join()
value = '\n'.join(g.value) value = g.value
self.assertIn("Spawned at", value) self.assertIn("Spawned at", value)
self.assertIn("Parent:", value) self.assertIn("Parent:", value)
...@@ -63,6 +66,7 @@ class TestTree(greentest.TestCase): ...@@ -63,6 +66,7 @@ class TestTree(greentest.TestCase):
super(TestTree, self).setUp() super(TestTree, self).setUp()
self.track_greenlet_tree = gevent.config.track_greenlet_tree self.track_greenlet_tree = gevent.config.track_greenlet_tree
gevent.config.track_greenlet_tree = True gevent.config.track_greenlet_tree = True
self.maxDiff = None
def tearDown(self): def tearDown(self):
gevent.config.track_greenlet_tree = self.track_greenlet_tree gevent.config.track_greenlet_tree = self.track_greenlet_tree
...@@ -92,7 +96,9 @@ class TestTree(greentest.TestCase): ...@@ -92,7 +96,9 @@ class TestTree(greentest.TestCase):
def t2(): def t2():
l = MyLocal(16) l = MyLocal(16)
assert l assert l
return s(t1) g = s(t1)
g.name = 'CustomName-' + str(g.minimal_ident)
return g
s1 = s(t2) s1 = s(t2)
s1.join() s1.join()
...@@ -108,7 +114,6 @@ class TestTree(greentest.TestCase): ...@@ -108,7 +114,6 @@ class TestTree(greentest.TestCase):
s3.spawn_tree_locals['stl'] = 'STL' s3.spawn_tree_locals['stl'] = 'STL'
s3.join() s3.join()
s4 = s(util.GreenletTree.current_tree) s4 = s(util.GreenletTree.current_tree)
s4.join() s4.join()
...@@ -116,15 +121,8 @@ class TestTree(greentest.TestCase): ...@@ -116,15 +121,8 @@ class TestTree(greentest.TestCase):
return tree, str(tree), tree.format(details={'running_stacks': False, return tree, str(tree), tree.format(details={'running_stacks': False,
'spawning_stacks': False}) 'spawning_stacks': False})
@greentest.ignores_leakcheck def _normalize_tree_format(self, value):
def test_tree(self):
import re import re
tree, str_tree, tree_format = self._build_tree()
self.assertTrue(tree.root)
self.assertNotIn('Parent', str_tree) # Simple output
value = tree_format
hexobj = re.compile('0x[0123456789abcdef]+L?', re.I) hexobj = re.compile('0x[0123456789abcdef]+L?', re.I)
value = hexobj.sub('X', value) value = hexobj.sub('X', value)
value = value.replace('epoll', 'select') value = value.replace('epoll', 'select')
...@@ -132,8 +130,17 @@ class TestTree(greentest.TestCase): ...@@ -132,8 +130,17 @@ class TestTree(greentest.TestCase):
value = value.replace('test__util', '__main__') value = value.replace('test__util', '__main__')
value = re.compile(' fileno=.').sub('', value) value = re.compile(' fileno=.').sub('', value)
value = value.replace('ref=-1', 'ref=0') value = value.replace('ref=-1', 'ref=0')
return value
@greentest.ignores_leakcheck
def test_tree(self):
tree, str_tree, tree_format = self._build_tree()
self.assertTrue(tree.root)
self.assertNotIn('Parent', str_tree) # Simple output
value = self._normalize_tree_format(tree_format)
self.maxDiff = None
expected = """\ expected = """\
<greenlet.greenlet object at X> <greenlet.greenlet object at X>
: Parent: None : Parent: None
...@@ -142,21 +149,21 @@ class TestTree(greentest.TestCase): ...@@ -142,21 +149,21 @@ class TestTree(greentest.TestCase):
: {'foo': 42} : {'foo': 42}
+--- <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> +--- <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
: Parent: <greenlet.greenlet object at X> : Parent: <greenlet.greenlet object at X>
+--- <Greenlet "Greenlet-1" at X: _run>; finished with value <Greenlet "Greenlet-0" at X +--- <Greenlet "Greenlet-1" at X: _run>; finished with value <Greenlet "CustomName-0" at 0x
: Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> : Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
| +--- <Greenlet "Greenlet-0" at X: _run>; finished with exception ExpectedException() | +--- <Greenlet "CustomName-0" at X: _run>; finished with exception ExpectedException()
: Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> : Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
+--- <Greenlet "Greenlet-2" at X: _run>; finished with value <Greenlet "Greenlet-4" at X +--- <Greenlet "Greenlet-2" at X: _run>; finished with value <Greenlet "CustomName-4" at 0x
: Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> : Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
| +--- <Greenlet "Greenlet-4" at X: _run>; finished with exception ExpectedException() | +--- <Greenlet "CustomName-4" at X: _run>; finished with exception ExpectedException()
: Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> : Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
+--- <Greenlet "Greenlet-3" at X: _run>; finished with value <Greenlet "Greenlet-5" at X +--- <Greenlet "Greenlet-3" at X: _run>; finished with value <Greenlet "Greenlet-5" at X
: Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> : Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
: Spawn Tree Locals : Spawn Tree Locals
: {'stl': 'STL'} : {'stl': 'STL'}
| +--- <Greenlet "Greenlet-5" at X: _run>; finished with value <Greenlet "Greenlet-6" at X | +--- <Greenlet "Greenlet-5" at X: _run>; finished with value <Greenlet "CustomName-6" at 0x
: Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> : Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
| +--- <Greenlet "Greenlet-6" at X: _run>; finished with exception ExpectedException() | +--- <Greenlet "CustomName-6" at X: _run>; finished with exception ExpectedException()
: Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> : Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
+--- <Greenlet "Greenlet-7" at X: _run>; finished with value <gevent.util.GreenletTree obje +--- <Greenlet "Greenlet-7" at X: _run>; finished with value <gevent.util.GreenletTree obje
Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X> Parent: <QuietHub '' at X default default pending=0 ref=0 thread_ident=X>
...@@ -169,5 +176,32 @@ class TestTree(greentest.TestCase): ...@@ -169,5 +176,32 @@ class TestTree(greentest.TestCase):
self._build_tree() self._build_tree()
@greentest.ignores_leakcheck
def test_forest_fake_parent(self):
from greenlet import greenlet as RawGreenlet
def t4():
# Ignore this one, make the child the parent,
# and don't be a child of the hub.
c = RawGreenlet(util.GreenletTree.current_tree)
c.parent.greenlet_tree_is_ignored = True
c.greenlet_tree_is_root = True
return c.switch()
g = RawGreenlet(t4)
tree = g.switch()
tree_format = tree.format(details={'running_stacks': False,
'spawning_stacks': False})
value = self._normalize_tree_format(tree_format)
expected = """\
<greenlet.greenlet object at X>; not running
: Parent: <greenlet.greenlet object at X>
""".strip()
self.assertEqual(expected, value)
if __name__ == '__main__': if __name__ == '__main__':
greentest.main() greentest.main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment