You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
8.1 KiB
Python
292 lines
8.1 KiB
Python
3 months ago
|
from __future__ import print_function
|
||
|
import sys
|
||
|
import greenlet
|
||
|
import unittest
|
||
|
|
||
|
from . import TestCase
|
||
|
from . import PY312
|
||
|
|
||
|
# https://discuss.python.org/t/cpython-3-12-greenlet-and-tracing-profiling-how-to-not-crash-and-get-correct-results/33144/2
|
||
|
DEBUG_BUILD_PY312 = (
|
||
|
PY312 and hasattr(sys, 'gettotalrefcount'),
|
||
|
"Broken on debug builds of Python 3.12"
|
||
|
)
|
||
|
|
||
|
class SomeError(Exception):
|
||
|
pass
|
||
|
|
||
|
class GreenletTracer(object):
|
||
|
oldtrace = None
|
||
|
|
||
|
def __init__(self, error_on_trace=False):
|
||
|
self.actions = []
|
||
|
self.error_on_trace = error_on_trace
|
||
|
|
||
|
def __call__(self, *args):
|
||
|
self.actions.append(args)
|
||
|
if self.error_on_trace:
|
||
|
raise SomeError
|
||
|
|
||
|
def __enter__(self):
|
||
|
self.oldtrace = greenlet.settrace(self)
|
||
|
return self.actions
|
||
|
|
||
|
def __exit__(self, *args):
|
||
|
greenlet.settrace(self.oldtrace)
|
||
|
|
||
|
|
||
|
class TestGreenletTracing(TestCase):
|
||
|
"""
|
||
|
Tests of ``greenlet.settrace()``
|
||
|
"""
|
||
|
|
||
|
def test_a_greenlet_tracing(self):
|
||
|
main = greenlet.getcurrent()
|
||
|
def dummy():
|
||
|
pass
|
||
|
def dummyexc():
|
||
|
raise SomeError()
|
||
|
|
||
|
with GreenletTracer() as actions:
|
||
|
g1 = greenlet.greenlet(dummy)
|
||
|
g1.switch()
|
||
|
g2 = greenlet.greenlet(dummyexc)
|
||
|
self.assertRaises(SomeError, g2.switch)
|
||
|
|
||
|
self.assertEqual(actions, [
|
||
|
('switch', (main, g1)),
|
||
|
('switch', (g1, main)),
|
||
|
('switch', (main, g2)),
|
||
|
('throw', (g2, main)),
|
||
|
])
|
||
|
|
||
|
def test_b_exception_disables_tracing(self):
|
||
|
main = greenlet.getcurrent()
|
||
|
def dummy():
|
||
|
main.switch()
|
||
|
g = greenlet.greenlet(dummy)
|
||
|
g.switch()
|
||
|
with GreenletTracer(error_on_trace=True) as actions:
|
||
|
self.assertRaises(SomeError, g.switch)
|
||
|
self.assertEqual(greenlet.gettrace(), None)
|
||
|
|
||
|
self.assertEqual(actions, [
|
||
|
('switch', (main, g)),
|
||
|
])
|
||
|
|
||
|
def test_set_same_tracer_twice(self):
|
||
|
# https://github.com/python-greenlet/greenlet/issues/332
|
||
|
# Our logic in asserting that the tracefunction should
|
||
|
# gain a reference was incorrect if the same tracefunction was set
|
||
|
# twice.
|
||
|
tracer = GreenletTracer()
|
||
|
with tracer:
|
||
|
greenlet.settrace(tracer)
|
||
|
|
||
|
|
||
|
class PythonTracer(object):
|
||
|
oldtrace = None
|
||
|
|
||
|
def __init__(self):
|
||
|
self.actions = []
|
||
|
|
||
|
def __call__(self, frame, event, arg):
|
||
|
# Record the co_name so we have an idea what function we're in.
|
||
|
self.actions.append((event, frame.f_code.co_name))
|
||
|
|
||
|
def __enter__(self):
|
||
|
self.oldtrace = sys.setprofile(self)
|
||
|
return self.actions
|
||
|
|
||
|
def __exit__(self, *args):
|
||
|
sys.setprofile(self.oldtrace)
|
||
|
|
||
|
def tpt_callback():
|
||
|
return 42
|
||
|
|
||
|
class TestPythonTracing(TestCase):
|
||
|
"""
|
||
|
Tests of the interaction of ``sys.settrace()``
|
||
|
with greenlet facilities.
|
||
|
|
||
|
NOTE: Most of this is probably CPython specific.
|
||
|
"""
|
||
|
|
||
|
maxDiff = None
|
||
|
|
||
|
def test_trace_events_trivial(self):
|
||
|
with PythonTracer() as actions:
|
||
|
tpt_callback()
|
||
|
# If we use the sys.settrace instead of setprofile, we get
|
||
|
# this:
|
||
|
|
||
|
# self.assertEqual(actions, [
|
||
|
# ('call', 'tpt_callback'),
|
||
|
# ('call', '__exit__'),
|
||
|
# ])
|
||
|
|
||
|
self.assertEqual(actions, [
|
||
|
('return', '__enter__'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('call', '__exit__'),
|
||
|
('c_call', '__exit__'),
|
||
|
])
|
||
|
|
||
|
def _trace_switch(self, glet):
|
||
|
with PythonTracer() as actions:
|
||
|
glet.switch()
|
||
|
return actions
|
||
|
|
||
|
def _check_trace_events_func_already_set(self, glet):
|
||
|
actions = self._trace_switch(glet)
|
||
|
self.assertEqual(actions, [
|
||
|
('return', '__enter__'),
|
||
|
('c_call', '_trace_switch'),
|
||
|
('call', 'run'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('return', 'run'),
|
||
|
('c_return', '_trace_switch'),
|
||
|
('call', '__exit__'),
|
||
|
('c_call', '__exit__'),
|
||
|
])
|
||
|
|
||
|
def test_trace_events_into_greenlet_func_already_set(self):
|
||
|
def run():
|
||
|
return tpt_callback()
|
||
|
|
||
|
self._check_trace_events_func_already_set(greenlet.greenlet(run))
|
||
|
|
||
|
def test_trace_events_into_greenlet_subclass_already_set(self):
|
||
|
class X(greenlet.greenlet):
|
||
|
def run(self):
|
||
|
return tpt_callback()
|
||
|
self._check_trace_events_func_already_set(X())
|
||
|
|
||
|
def _check_trace_events_from_greenlet_sets_profiler(self, g, tracer):
|
||
|
g.switch()
|
||
|
tpt_callback()
|
||
|
tracer.__exit__()
|
||
|
self.assertEqual(tracer.actions, [
|
||
|
('return', '__enter__'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('return', 'run'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('call', '__exit__'),
|
||
|
('c_call', '__exit__'),
|
||
|
])
|
||
|
|
||
|
|
||
|
def test_trace_events_from_greenlet_func_sets_profiler(self):
|
||
|
tracer = PythonTracer()
|
||
|
def run():
|
||
|
tracer.__enter__()
|
||
|
return tpt_callback()
|
||
|
|
||
|
self._check_trace_events_from_greenlet_sets_profiler(greenlet.greenlet(run),
|
||
|
tracer)
|
||
|
|
||
|
def test_trace_events_from_greenlet_subclass_sets_profiler(self):
|
||
|
tracer = PythonTracer()
|
||
|
class X(greenlet.greenlet):
|
||
|
def run(self):
|
||
|
tracer.__enter__()
|
||
|
return tpt_callback()
|
||
|
|
||
|
self._check_trace_events_from_greenlet_sets_profiler(X(), tracer)
|
||
|
|
||
|
@unittest.skipIf(*DEBUG_BUILD_PY312)
|
||
|
def test_trace_events_multiple_greenlets_switching(self):
|
||
|
tracer = PythonTracer()
|
||
|
|
||
|
g1 = None
|
||
|
g2 = None
|
||
|
|
||
|
def g1_run():
|
||
|
tracer.__enter__()
|
||
|
tpt_callback()
|
||
|
g2.switch()
|
||
|
tpt_callback()
|
||
|
return 42
|
||
|
|
||
|
def g2_run():
|
||
|
tpt_callback()
|
||
|
tracer.__exit__()
|
||
|
tpt_callback()
|
||
|
g1.switch()
|
||
|
|
||
|
g1 = greenlet.greenlet(g1_run)
|
||
|
g2 = greenlet.greenlet(g2_run)
|
||
|
|
||
|
x = g1.switch()
|
||
|
self.assertEqual(x, 42)
|
||
|
tpt_callback() # ensure not in the trace
|
||
|
self.assertEqual(tracer.actions, [
|
||
|
('return', '__enter__'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('c_call', 'g1_run'),
|
||
|
('call', 'g2_run'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('call', '__exit__'),
|
||
|
('c_call', '__exit__'),
|
||
|
])
|
||
|
|
||
|
@unittest.skipIf(*DEBUG_BUILD_PY312)
|
||
|
def test_trace_events_multiple_greenlets_switching_siblings(self):
|
||
|
# Like the first version, but get both greenlets running first
|
||
|
# as "siblings" and then establish the tracing.
|
||
|
tracer = PythonTracer()
|
||
|
|
||
|
g1 = None
|
||
|
g2 = None
|
||
|
|
||
|
def g1_run():
|
||
|
greenlet.getcurrent().parent.switch()
|
||
|
tracer.__enter__()
|
||
|
tpt_callback()
|
||
|
g2.switch()
|
||
|
tpt_callback()
|
||
|
return 42
|
||
|
|
||
|
def g2_run():
|
||
|
greenlet.getcurrent().parent.switch()
|
||
|
|
||
|
tpt_callback()
|
||
|
tracer.__exit__()
|
||
|
tpt_callback()
|
||
|
g1.switch()
|
||
|
|
||
|
g1 = greenlet.greenlet(g1_run)
|
||
|
g2 = greenlet.greenlet(g2_run)
|
||
|
|
||
|
# Start g1
|
||
|
g1.switch()
|
||
|
# And it immediately returns control to us.
|
||
|
# Start g2
|
||
|
g2.switch()
|
||
|
# Which also returns. Now kick of the real part of the
|
||
|
# test.
|
||
|
x = g1.switch()
|
||
|
self.assertEqual(x, 42)
|
||
|
|
||
|
tpt_callback() # ensure not in the trace
|
||
|
self.assertEqual(tracer.actions, [
|
||
|
('return', '__enter__'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('c_call', 'g1_run'),
|
||
|
('call', 'tpt_callback'),
|
||
|
('return', 'tpt_callback'),
|
||
|
('call', '__exit__'),
|
||
|
('c_call', '__exit__'),
|
||
|
])
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
unittest.main()
|