Update 2025-04-24_11:44:19

This commit is contained in:
oib
2025-04-24 11:44:23 +02:00
commit e748c737f4
3408 changed files with 717481 additions and 0 deletions

View File

@ -0,0 +1,240 @@
# -*- coding: utf-8 -*-
"""
Tests for greenlet.
"""
import os
import sys
import unittest
from gc import collect
from gc import get_objects
from threading import active_count as active_thread_count
from time import sleep
from time import time
import psutil
from greenlet import greenlet as RawGreenlet
from greenlet import getcurrent
from greenlet._greenlet import get_pending_cleanup_count
from greenlet._greenlet import get_total_main_greenlets
from . import leakcheck
PY312 = sys.version_info[:2] >= (3, 12)
PY313 = sys.version_info[:2] >= (3, 13)
# XXX: First tested on 3.14a7. Revisit all uses of this on later versions to ensure they
# are still valid.
PY314 = sys.version_info[:2] >= (3, 14)
WIN = sys.platform.startswith("win")
RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS')
RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS
RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR')
RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR
RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX')
class TestCaseMetaClass(type):
# wrap each test method with
# a) leak checks
def __new__(cls, classname, bases, classDict):
# pylint and pep8 fight over what this should be called (mcs or cls).
# pylint gets it right, but we can't scope disable pep8, so we go with
# its convention.
# pylint: disable=bad-mcs-classmethod-argument
check_totalrefcount = True
# Python 3: must copy, we mutate the classDict. Interestingly enough,
# it doesn't actually error out, but under 3.6 we wind up wrapping
# and re-wrapping the same items over and over and over.
for key, value in list(classDict.items()):
if key.startswith('test') and callable(value):
classDict.pop(key)
if check_totalrefcount:
value = leakcheck.wrap_refcount(value)
classDict[key] = value
return type.__new__(cls, classname, bases, classDict)
class TestCase(unittest.TestCase, metaclass=TestCaseMetaClass):
cleanup_attempt_sleep_duration = 0.001
cleanup_max_sleep_seconds = 1
def wait_for_pending_cleanups(self,
initial_active_threads=None,
initial_main_greenlets=None):
initial_active_threads = initial_active_threads or self.threads_before_test
initial_main_greenlets = initial_main_greenlets or self.main_greenlets_before_test
sleep_time = self.cleanup_attempt_sleep_duration
# NOTE: This is racy! A Python-level thread object may be dead
# and gone, but the C thread may not yet have fired its
# destructors and added to the queue. There's no particular
# way to know that's about to happen. We try to watch the
# Python threads to make sure they, at least, have gone away.
# Counting the main greenlets, which we can easily do deterministically,
# also helps.
# Always sleep at least once to let other threads run
sleep(sleep_time)
quit_after = time() + self.cleanup_max_sleep_seconds
# TODO: We could add an API that calls us back when a particular main greenlet is deleted?
# It would have to drop the GIL
while (
get_pending_cleanup_count()
or active_thread_count() > initial_active_threads
or (not self.expect_greenlet_leak
and get_total_main_greenlets() > initial_main_greenlets)):
sleep(sleep_time)
if time() > quit_after:
print("Time limit exceeded.")
print("Threads: Waiting for only", initial_active_threads,
"-->", active_thread_count())
print("MGlets : Waiting for only", initial_main_greenlets,
"-->", get_total_main_greenlets())
break
collect()
def count_objects(self, kind=list, exact_kind=True):
# pylint:disable=unidiomatic-typecheck
# Collect the garbage.
for _ in range(3):
collect()
if exact_kind:
return sum(
1
for x in get_objects()
if type(x) is kind
)
# instances
return sum(
1
for x in get_objects()
if isinstance(x, kind)
)
greenlets_before_test = 0
threads_before_test = 0
main_greenlets_before_test = 0
expect_greenlet_leak = False
def count_greenlets(self):
"""
Find all the greenlets and subclasses tracked by the GC.
"""
return self.count_objects(RawGreenlet, False)
def setUp(self):
# Ensure the main greenlet exists, otherwise the first test
# gets a false positive leak
super().setUp()
getcurrent()
self.threads_before_test = active_thread_count()
self.main_greenlets_before_test = get_total_main_greenlets()
self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test)
self.greenlets_before_test = self.count_greenlets()
def tearDown(self):
if getattr(self, 'skipTearDown', False):
return
self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test)
super().tearDown()
def get_expected_returncodes_for_aborted_process(self):
import signal
# The child should be aborted in an unusual way. On POSIX
# platforms, this is done with abort() and signal.SIGABRT,
# which is reflected in a negative return value; however, on
# Windows, even though we observe the child print "Fatal
# Python error: Aborted" and in older versions of the C
# runtime "This application has requested the Runtime to
# terminate it in an unusual way," it always has an exit code
# of 3. This is interesting because 3 is the error code for
# ERROR_PATH_NOT_FOUND; BUT: the C runtime abort() function
# also uses this code.
#
# If we link to the static C library on Windows, the error
# code changes to '0xc0000409' (hex(3221226505)), which
# apparently is STATUS_STACK_BUFFER_OVERRUN; but "What this
# means is that nowadays when you get a
# STATUS_STACK_BUFFER_OVERRUN, it doesnt actually mean that
# there is a stack buffer overrun. It just means that the
# application decided to terminate itself with great haste."
#
#
# On windows, we've also seen '0xc0000005' (hex(3221225477)).
# That's "Access Violation"
#
# See
# https://devblogs.microsoft.com/oldnewthing/20110519-00/?p=10623
# and
# https://docs.microsoft.com/en-us/previous-versions/k089yyh0(v=vs.140)?redirectedfrom=MSDN
# and
# https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655
expected_exit = (
-signal.SIGABRT,
# But beginning on Python 3.11, the faulthandler
# that prints the C backtraces sometimes segfaults after
# reporting the exception but before printing the stack.
# This has only been seen on linux/gcc.
-signal.SIGSEGV,
) if not WIN else (
3,
0xc0000409,
0xc0000005,
)
return expected_exit
def get_process_uss(self):
"""
Return the current process's USS in bytes.
uss is available on Linux, macOS, Windows. Also known as
"Unique Set Size", this is the memory which is unique to a
process and which would be freed if the process was terminated
right now.
If this is not supported by ``psutil``, this raises the
:exc:`unittest.SkipTest` exception.
"""
try:
return psutil.Process().memory_full_info().uss
except AttributeError as e:
raise unittest.SkipTest("uss not supported") from e
def run_script(self, script_name, show_output=True):
import subprocess
script = os.path.join(
os.path.dirname(__file__),
script_name,
)
try:
return subprocess.check_output([sys.executable, script],
encoding='utf-8',
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as ex:
if show_output:
print('-----')
print('Failed to run script', script)
print('~~~~~')
print(ex.output)
print('------')
raise
def assertScriptRaises(self, script_name, exitcodes=None):
import subprocess
with self.assertRaises(subprocess.CalledProcessError) as exc:
output = self.run_script(script_name, show_output=False)
__traceback_info__ = output
# We're going to fail the assertion if we get here, at least
# preserve the output in the traceback.
if exitcodes is None:
exitcodes = self.get_expected_returncodes_for_aborted_process()
self.assertIn(exc.exception.returncode, exitcodes)
return exc.exception

View File

@ -0,0 +1,231 @@
/* This is a set of functions used by test_extension_interface.py to test the
* Greenlet C API.
*/
#include "../greenlet.h"
#ifndef Py_RETURN_NONE
# define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None
#endif
#define TEST_MODULE_NAME "_test_extension"
static PyObject*
test_switch(PyObject* self, PyObject* greenlet)
{
PyObject* result = NULL;
if (greenlet == NULL || !PyGreenlet_Check(greenlet)) {
PyErr_BadArgument();
return NULL;
}
result = PyGreenlet_Switch((PyGreenlet*)greenlet, NULL, NULL);
if (result == NULL) {
if (!PyErr_Occurred()) {
PyErr_SetString(PyExc_AssertionError,
"greenlet.switch() failed for some reason.");
}
return NULL;
}
Py_INCREF(result);
return result;
}
static PyObject*
test_switch_kwargs(PyObject* self, PyObject* args, PyObject* kwargs)
{
PyGreenlet* g = NULL;
PyObject* result = NULL;
PyArg_ParseTuple(args, "O!", &PyGreenlet_Type, &g);
if (g == NULL || !PyGreenlet_Check(g)) {
PyErr_BadArgument();
return NULL;
}
result = PyGreenlet_Switch(g, NULL, kwargs);
if (result == NULL) {
if (!PyErr_Occurred()) {
PyErr_SetString(PyExc_AssertionError,
"greenlet.switch() failed for some reason.");
}
return NULL;
}
Py_XINCREF(result);
return result;
}
static PyObject*
test_getcurrent(PyObject* self)
{
PyGreenlet* g = PyGreenlet_GetCurrent();
if (g == NULL || !PyGreenlet_Check(g) || !PyGreenlet_ACTIVE(g)) {
PyErr_SetString(PyExc_AssertionError,
"getcurrent() returned an invalid greenlet");
Py_XDECREF(g);
return NULL;
}
Py_DECREF(g);
Py_RETURN_NONE;
}
static PyObject*
test_setparent(PyObject* self, PyObject* arg)
{
PyGreenlet* current;
PyGreenlet* greenlet = NULL;
if (arg == NULL || !PyGreenlet_Check(arg)) {
PyErr_BadArgument();
return NULL;
}
if ((current = PyGreenlet_GetCurrent()) == NULL) {
return NULL;
}
greenlet = (PyGreenlet*)arg;
if (PyGreenlet_SetParent(greenlet, current)) {
Py_DECREF(current);
return NULL;
}
Py_DECREF(current);
if (PyGreenlet_Switch(greenlet, NULL, NULL) == NULL) {
return NULL;
}
Py_RETURN_NONE;
}
static PyObject*
test_new_greenlet(PyObject* self, PyObject* callable)
{
PyObject* result = NULL;
PyGreenlet* greenlet = PyGreenlet_New(callable, NULL);
if (!greenlet) {
return NULL;
}
result = PyGreenlet_Switch(greenlet, NULL, NULL);
Py_CLEAR(greenlet);
if (result == NULL) {
return NULL;
}
Py_INCREF(result);
return result;
}
static PyObject*
test_raise_dead_greenlet(PyObject* self)
{
PyErr_SetString(PyExc_GreenletExit, "test GreenletExit exception.");
return NULL;
}
static PyObject*
test_raise_greenlet_error(PyObject* self)
{
PyErr_SetString(PyExc_GreenletError, "test greenlet.error exception");
return NULL;
}
static PyObject*
test_throw(PyObject* self, PyGreenlet* g)
{
const char msg[] = "take that sucka!";
PyObject* msg_obj = Py_BuildValue("s", msg);
PyGreenlet_Throw(g, PyExc_ValueError, msg_obj, NULL);
Py_DECREF(msg_obj);
if (PyErr_Occurred()) {
return NULL;
}
Py_RETURN_NONE;
}
static PyObject*
test_throw_exact(PyObject* self, PyObject* args)
{
PyGreenlet* g = NULL;
PyObject* typ = NULL;
PyObject* val = NULL;
PyObject* tb = NULL;
if (!PyArg_ParseTuple(args, "OOOO:throw", &g, &typ, &val, &tb)) {
return NULL;
}
PyGreenlet_Throw(g, typ, val, tb);
if (PyErr_Occurred()) {
return NULL;
}
Py_RETURN_NONE;
}
static PyMethodDef test_methods[] = {
{"test_switch",
(PyCFunction)test_switch,
METH_O,
"Switch to the provided greenlet sending provided arguments, and \n"
"return the results."},
{"test_switch_kwargs",
(PyCFunction)test_switch_kwargs,
METH_VARARGS | METH_KEYWORDS,
"Switch to the provided greenlet sending the provided keyword args."},
{"test_getcurrent",
(PyCFunction)test_getcurrent,
METH_NOARGS,
"Test PyGreenlet_GetCurrent()"},
{"test_setparent",
(PyCFunction)test_setparent,
METH_O,
"Se the parent of the provided greenlet and switch to it."},
{"test_new_greenlet",
(PyCFunction)test_new_greenlet,
METH_O,
"Test PyGreenlet_New()"},
{"test_raise_dead_greenlet",
(PyCFunction)test_raise_dead_greenlet,
METH_NOARGS,
"Just raise greenlet.GreenletExit"},
{"test_raise_greenlet_error",
(PyCFunction)test_raise_greenlet_error,
METH_NOARGS,
"Just raise greenlet.error"},
{"test_throw",
(PyCFunction)test_throw,
METH_O,
"Throw a ValueError at the provided greenlet"},
{"test_throw_exact",
(PyCFunction)test_throw_exact,
METH_VARARGS,
"Throw exactly the arguments given at the provided greenlet"},
{NULL, NULL, 0, NULL}
};
#define INITERROR return NULL
static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,
TEST_MODULE_NAME,
NULL,
0,
test_methods,
NULL,
NULL,
NULL,
NULL};
PyMODINIT_FUNC
PyInit__test_extension(void)
{
PyObject* module = NULL;
module = PyModule_Create(&moduledef);
if (module == NULL) {
return NULL;
}
PyGreenlet_Import();
return module;
}

View File

@ -0,0 +1,226 @@
/* This is a set of functions used to test C++ exceptions are not
* broken during greenlet switches
*/
#include "../greenlet.h"
#include "../greenlet_compiler_compat.hpp"
#include <exception>
#include <stdexcept>
struct exception_t {
int depth;
exception_t(int depth) : depth(depth) {}
};
/* Functions are called via pointers to prevent inlining */
static void (*p_test_exception_throw_nonstd)(int depth);
static void (*p_test_exception_throw_std)();
static PyObject* (*p_test_exception_switch_recurse)(int depth, int left);
static void
test_exception_throw_nonstd(int depth)
{
throw exception_t(depth);
}
static void
test_exception_throw_std()
{
throw std::runtime_error("Thrown from an extension.");
}
static PyObject*
test_exception_switch_recurse(int depth, int left)
{
if (left > 0) {
return p_test_exception_switch_recurse(depth, left - 1);
}
PyObject* result = NULL;
PyGreenlet* self = PyGreenlet_GetCurrent();
if (self == NULL)
return NULL;
try {
if (PyGreenlet_Switch(PyGreenlet_GET_PARENT(self), NULL, NULL) == NULL) {
Py_DECREF(self);
return NULL;
}
p_test_exception_throw_nonstd(depth);
PyErr_SetString(PyExc_RuntimeError,
"throwing C++ exception didn't work");
}
catch (const exception_t& e) {
if (e.depth != depth)
PyErr_SetString(PyExc_AssertionError, "depth mismatch");
else
result = PyLong_FromLong(depth);
}
catch (...) {
PyErr_SetString(PyExc_RuntimeError, "unexpected C++ exception");
}
Py_DECREF(self);
return result;
}
/* test_exception_switch(int depth)
* - recurses depth times
* - switches to parent inside try/catch block
* - throws an exception that (expected to be caught in the same function)
* - verifies depth matches (exceptions shouldn't be caught in other greenlets)
*/
static PyObject*
test_exception_switch(PyObject* UNUSED(self), PyObject* args)
{
int depth;
if (!PyArg_ParseTuple(args, "i", &depth))
return NULL;
return p_test_exception_switch_recurse(depth, depth);
}
static PyObject*
py_test_exception_throw_nonstd(PyObject* self, PyObject* args)
{
if (!PyArg_ParseTuple(args, ""))
return NULL;
p_test_exception_throw_nonstd(0);
PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw");
return NULL;
}
static PyObject*
py_test_exception_throw_std(PyObject* self, PyObject* args)
{
if (!PyArg_ParseTuple(args, ""))
return NULL;
p_test_exception_throw_std();
PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw");
return NULL;
}
static PyObject*
py_test_call(PyObject* self, PyObject* arg)
{
PyObject* noargs = PyTuple_New(0);
PyObject* ret = PyObject_Call(arg, noargs, nullptr);
Py_DECREF(noargs);
return ret;
}
/* test_exception_switch_and_do_in_g2(g2func)
* - creates new greenlet g2 to run g2func
* - switches to g2 inside try/catch block
* - verifies that no exception has been caught
*
* it is used together with test_exception_throw to verify that unhandled
* exceptions thrown in one greenlet do not propagate to other greenlet nor
* segfault the process.
*/
static PyObject*
test_exception_switch_and_do_in_g2(PyObject* self, PyObject* args)
{
PyObject* g2func = NULL;
PyObject* result = NULL;
if (!PyArg_ParseTuple(args, "O", &g2func))
return NULL;
PyGreenlet* g2 = PyGreenlet_New(g2func, NULL);
if (!g2) {
return NULL;
}
try {
result = PyGreenlet_Switch(g2, NULL, NULL);
if (!result) {
return NULL;
}
}
catch (const exception_t& e) {
/* if we are here the memory can be already corrupted and the program
* might crash before below py-level exception might become printed.
* -> print something to stderr to make it clear that we had entered
* this catch block.
* See comments in inner_bootstrap()
*/
#if defined(WIN32) || defined(_WIN32)
fprintf(stderr, "C++ exception unexpectedly caught in g1\n");
PyErr_SetString(PyExc_AssertionError, "C++ exception unexpectedly caught in g1");
Py_XDECREF(result);
return NULL;
#else
throw;
#endif
}
Py_XDECREF(result);
Py_RETURN_NONE;
}
static PyMethodDef test_methods[] = {
{"test_exception_switch",
(PyCFunction)&test_exception_switch,
METH_VARARGS,
"Switches to parent twice, to test exception handling and greenlet "
"switching."},
{"test_exception_switch_and_do_in_g2",
(PyCFunction)&test_exception_switch_and_do_in_g2,
METH_VARARGS,
"Creates new greenlet g2 to run g2func and switches to it inside try/catch "
"block. Used together with test_exception_throw to verify that unhandled "
"C++ exceptions thrown in a greenlet doe not corrupt memory."},
{"test_exception_throw_nonstd",
(PyCFunction)&py_test_exception_throw_nonstd,
METH_VARARGS,
"Throws non-standard C++ exception. Calling this function directly should abort the process."
},
{"test_exception_throw_std",
(PyCFunction)&py_test_exception_throw_std,
METH_VARARGS,
"Throws standard C++ exception. Calling this function directly should abort the process."
},
{"test_call",
(PyCFunction)&py_test_call,
METH_O,
"Call the given callable. Unlike calling it directly, this creates a "
"new C-level stack frame, which may be helpful in testing."
},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,
"greenlet.tests._test_extension_cpp",
NULL,
0,
test_methods,
NULL,
NULL,
NULL,
NULL};
PyMODINIT_FUNC
PyInit__test_extension_cpp(void)
{
PyObject* module = NULL;
module = PyModule_Create(&moduledef);
if (module == NULL) {
return NULL;
}
PyGreenlet_Import();
if (_PyGreenlet_API == NULL) {
return NULL;
}
p_test_exception_throw_nonstd = test_exception_throw_nonstd;
p_test_exception_throw_std = test_exception_throw_std;
p_test_exception_switch_recurse = test_exception_switch_recurse;
return module;
}

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
If we have a run callable passed to the constructor or set as an
attribute, but we don't actually use that (because ``__getattribute__``
or the like interferes), then when we clear callable before beginning
to run, there's an opportunity for Python code to run.
"""
import greenlet
g = None
main = greenlet.getcurrent()
results = []
class RunCallable:
def __del__(self):
results.append(('RunCallable', '__del__'))
main.switch('from RunCallable')
class G(greenlet.greenlet):
def __getattribute__(self, name):
if name == 'run':
results.append(('G.__getattribute__', 'run'))
return run_func
return object.__getattribute__(self, name)
def run_func():
results.append(('run_func', 'enter'))
g = G(RunCallable())
# Try to start G. It will get to the point where it deletes
# its run callable C++ variable in inner_bootstrap. That triggers
# the __del__ method, which switches back to main before g
# actually even starts running.
x = g.switch()
results.append(('main: g.switch()', x))
# In the C++ code, this results in g->g_switch() appearing to return, even though
# it has yet to run.
print('In main with', x, flush=True)
g.switch()
print('RESULTS', results)

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""
Helper for testing a C++ exception throw aborts the process.
Takes one argument, the name of the function in :mod:`_test_extension_cpp` to call.
"""
import sys
import greenlet
from greenlet.tests import _test_extension_cpp
print('fail_cpp_exception is running')
def run_unhandled_exception_in_greenlet_aborts():
def _():
_test_extension_cpp.test_exception_switch_and_do_in_g2(
_test_extension_cpp.test_exception_throw_nonstd
)
g1 = greenlet.greenlet(_)
g1.switch()
func_name = sys.argv[1]
try:
func = getattr(_test_extension_cpp, func_name)
except AttributeError:
if func_name == run_unhandled_exception_in_greenlet_aborts.__name__:
func = run_unhandled_exception_in_greenlet_aborts
elif func_name == 'run_as_greenlet_target':
g = greenlet.greenlet(_test_extension_cpp.test_exception_throw_std)
func = g.switch
else:
raise
print('raising', func, flush=True)
func()

View File

@ -0,0 +1,78 @@
"""
Testing initialstub throwing an already started exception.
"""
import greenlet
a = None
b = None
c = None
main = greenlet.getcurrent()
# If we switch into a dead greenlet,
# we go looking for its parents.
# if a parent is not yet started, we start it.
results = []
def a_run(*args):
#results.append('A')
results.append(('Begin A', args))
def c_run():
results.append('Begin C')
b.switch('From C')
results.append('C done')
class A(greenlet.greenlet): pass
class B(greenlet.greenlet):
doing_it = False
def __getattribute__(self, name):
if name == 'run' and not self.doing_it:
assert greenlet.getcurrent() is c
self.doing_it = True
results.append('Switch to b from B.__getattribute__ in '
+ type(greenlet.getcurrent()).__name__)
b.switch()
results.append('B.__getattribute__ back from main in '
+ type(greenlet.getcurrent()).__name__)
if name == 'run':
name = '_B_run'
return object.__getattribute__(self, name)
def _B_run(self, *arg):
results.append(('Begin B', arg))
results.append('_B_run switching to main')
main.switch('From B')
class C(greenlet.greenlet):
pass
a = A(a_run)
b = B(parent=a)
c = C(c_run, b)
# Start a child; while running, it will start B,
# but starting B will ALSO start B.
result = c.switch()
results.append(('main from c', result))
# Switch back to C, which was in the middle of switching
# already. This will throw the ``GreenletStartedWhileInPython``
# exception, which results in parent A getting started (B is finished)
c.switch()
results.append(('A dead?', a.dead, 'B dead?', b.dead, 'C dead?', c.dead))
# A and B should both be dead now.
assert a.dead
assert b.dead
assert not c.dead
result = c.switch()
results.append(('main from c.2', result))
# Now C is dead
assert c.dead
print("RESULTS:", results)

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
"""
A test helper for seeing what happens when slp_switch()
fails.
"""
# pragma: no cover
import greenlet
print('fail_slp_switch is running', flush=True)
runs = []
def func():
runs.append(1)
greenlet.getcurrent().parent.switch()
runs.append(2)
greenlet.getcurrent().parent.switch()
runs.append(3)
g = greenlet._greenlet.UnswitchableGreenlet(func)
g.switch()
assert runs == [1]
g.switch()
assert runs == [1, 2]
g.force_slp_switch_error = True
# This should crash.
g.switch()

View File

@ -0,0 +1,44 @@
"""
Uses a trace function to switch greenlets at unexpected times.
In the trace function, we switch from the current greenlet to another
greenlet, which switches
"""
import greenlet
g1 = None
g2 = None
switch_to_g2 = False
def tracefunc(*args):
print('TRACE', *args)
global switch_to_g2
if switch_to_g2:
switch_to_g2 = False
g2.switch()
print('\tLEAVE TRACE', *args)
def g1_run():
print('In g1_run')
global switch_to_g2
switch_to_g2 = True
from_parent = greenlet.getcurrent().parent.switch()
print('Return to g1_run')
print('From parent', from_parent)
def g2_run():
#g1.switch()
greenlet.getcurrent().parent.switch()
greenlet.settrace(tracefunc)
g1 = greenlet.greenlet(g1_run)
g2 = greenlet.greenlet(g2_run)
# This switch didn't actually finish!
# And if it did, it would raise TypeError
# because g1_run() doesn't take any arguments.
g1.switch(1)
print('Back in main')
g1.switch(2)

View File

@ -0,0 +1,55 @@
"""
Like fail_switch_three_greenlets, but the call into g1_run would actually be
valid.
"""
import greenlet
g1 = None
g2 = None
switch_to_g2 = True
results = []
def tracefunc(*args):
results.append(('trace', args[0]))
print('TRACE', *args)
global switch_to_g2
if switch_to_g2:
switch_to_g2 = False
g2.switch('g2 from tracefunc')
print('\tLEAVE TRACE', *args)
def g1_run(arg):
results.append(('g1 arg', arg))
print('In g1_run')
from_parent = greenlet.getcurrent().parent.switch('from g1_run')
results.append(('g1 from parent', from_parent))
return 'g1 done'
def g2_run(arg):
#g1.switch()
results.append(('g2 arg', arg))
parent = greenlet.getcurrent().parent.switch('from g2_run')
global switch_to_g2
switch_to_g2 = False
results.append(('g2 from parent', parent))
return 'g2 done'
greenlet.settrace(tracefunc)
g1 = greenlet.greenlet(g1_run)
g2 = greenlet.greenlet(g2_run)
x = g1.switch('g1 from main')
results.append(('main g1', x))
print('Back in main', x)
x = g1.switch('g2 from main')
results.append(('main g2', x))
print('back in amain again', x)
x = g1.switch('g1 from main 2')
results.append(('main g1.2', x))
x = g2.switch()
results.append(('main g2.2', x))
print("RESULTS:", results)

View File

@ -0,0 +1,41 @@
"""
Uses a trace function to switch greenlets at unexpected times.
In the trace function, we switch from the current greenlet to another
greenlet, which switches
"""
import greenlet
g1 = None
g2 = None
switch_to_g2 = False
def tracefunc(*args):
print('TRACE', *args)
global switch_to_g2
if switch_to_g2:
switch_to_g2 = False
g2.switch()
print('\tLEAVE TRACE', *args)
def g1_run():
print('In g1_run')
global switch_to_g2
switch_to_g2 = True
greenlet.getcurrent().parent.switch()
print('Return to g1_run')
print('Falling off end of g1_run')
def g2_run():
g1.switch()
print('Falling off end of g2')
greenlet.settrace(tracefunc)
g1 = greenlet.greenlet(g1_run)
g2 = greenlet.greenlet(g2_run)
g1.switch()
print('Falling off end of main')
g2.switch()

View File

@ -0,0 +1,319 @@
# Copyright (c) 2018 gevent community
# Copyright (c) 2021 greenlet community
#
# This was originally part of gevent's test suite. The main author
# (Jason Madden) vendored a copy of it into greenlet.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import print_function
import os
import sys
import gc
from functools import wraps
import unittest
import objgraph
# graphviz 0.18 (Nov 7 2021), available only on Python 3.6 and newer,
# has added type hints (sigh). It wants to use ``typing.Literal`` for
# some stuff, but that's only available on Python 3.9+. If that's not
# found, it creates a ``unittest.mock.MagicMock`` object and annotates
# with that. These are GC'able objects, and doing almost *anything*
# with them results in an explosion of objects. For example, trying to
# compare them for equality creates new objects. This causes our
# leakchecks to fail, with reports like:
#
# greenlet.tests.leakcheck.LeakCheckError: refcount increased by [337, 1333, 343, 430, 530, 643, 769]
# _Call 1820 +546
# dict 4094 +76
# MagicProxy 585 +73
# tuple 2693 +66
# _CallList 24 +3
# weakref 1441 +1
# function 5996 +1
# type 736 +1
# cell 592 +1
# MagicMock 8 +1
#
# To avoid this, we *could* filter this type of object out early. In
# principle it could leak, but we don't use mocks in greenlet, so it
# doesn't leak from us. However, a further issue is that ``MagicMock``
# objects have subobjects that are also GC'able, like ``_Call``, and
# those create new mocks of their own too. So we'd have to filter them
# as well, and they're not public. That's OK, we can workaround the
# problem by being very careful to never compare by equality or other
# user-defined operators, only using object identity or other builtin
# functions.
RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS')
RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS
RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR')
RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR
RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX')
SKIP_LEAKCHECKS = RUNNING_ON_MANYLINUX or os.environ.get('GREENLET_SKIP_LEAKCHECKS')
SKIP_FAILING_LEAKCHECKS = os.environ.get('GREENLET_SKIP_FAILING_LEAKCHECKS')
ONLY_FAILING_LEAKCHECKS = os.environ.get('GREENLET_ONLY_FAILING_LEAKCHECKS')
def ignores_leakcheck(func):
"""
Ignore the given object during leakchecks.
Can be applied to a method, in which case the method will run, but
will not be subject to leak checks.
If applied to a class, the entire class will be skipped during leakchecks. This
is intended to be used for classes that are very slow and cause problems such as
test timeouts; typically it will be used for classes that are subclasses of a base
class and specify variants of behaviour (such as pool sizes).
"""
func.ignore_leakcheck = True
return func
def fails_leakcheck(func):
"""
Mark that the function is known to leak.
"""
func.fails_leakcheck = True
if SKIP_FAILING_LEAKCHECKS:
func = unittest.skip("Skipping known failures")(func)
return func
class LeakCheckError(AssertionError):
pass
if hasattr(sys, 'getobjects'):
# In a Python build with ``--with-trace-refs``, make objgraph
# trace *all* the objects, not just those that are tracked by the
# GC
class _MockGC(object):
def get_objects(self):
return sys.getobjects(0) # pylint:disable=no-member
def __getattr__(self, name):
return getattr(gc, name)
objgraph.gc = _MockGC()
fails_strict_leakcheck = fails_leakcheck
else:
def fails_strict_leakcheck(func):
"""
Decorator for a function that is known to fail when running
strict (``sys.getobjects()``) leakchecks.
This type of leakcheck finds all objects, even those, such as
strings, which are not tracked by the garbage collector.
"""
return func
class ignores_types_in_strict_leakcheck(object):
def __init__(self, types):
self.types = types
def __call__(self, func):
func.leakcheck_ignore_types = self.types
return func
class _RefCountChecker(object):
# Some builtin things that we ignore
# XXX: Those things were ignored by gevent, but they're important here,
# presumably.
IGNORED_TYPES = () #(tuple, dict, types.FrameType, types.TracebackType)
def __init__(self, testcase, function):
self.testcase = testcase
self.function = function
self.deltas = []
self.peak_stats = {}
self.ignored_types = ()
# The very first time we are called, we have already been
# self.setUp() by the test runner, so we don't need to do it again.
self.needs_setUp = False
def _include_object_p(self, obj):
# pylint:disable=too-many-return-statements
#
# See the comment block at the top. We must be careful to
# avoid invoking user-defined operations.
if obj is self:
return False
kind = type(obj)
# ``self._include_object_p == obj`` returns NotImplemented
# for non-function objects, which causes the interpreter
# to try to reverse the order of arguments...which leads
# to the explosion of mock objects. We don't want that, so we implement
# the check manually.
if kind == type(self._include_object_p):
try:
# pylint:disable=not-callable
exact_method_equals = self._include_object_p.__eq__(obj)
except AttributeError:
# Python 2.7 methods may only have __cmp__, and that raises a
# TypeError for non-method arguments
# pylint:disable=no-member
exact_method_equals = self._include_object_p.__cmp__(obj) == 0
if exact_method_equals is not NotImplemented and exact_method_equals:
return False
# Similarly, we need to check identity in our __dict__ to avoid mock explosions.
for x in self.__dict__.values():
if obj is x:
return False
if kind in self.ignored_types or kind in self.IGNORED_TYPES:
return False
return True
def _growth(self):
return objgraph.growth(limit=None, peak_stats=self.peak_stats,
filter=self._include_object_p)
def _report_diff(self, growth):
if not growth:
return "<Unable to calculate growth>"
lines = []
width = max(len(name) for name, _, _ in growth)
for name, count, delta in growth:
lines.append('%-*s%9d %+9d' % (width, name, count, delta))
diff = '\n'.join(lines)
return diff
def _run_test(self, args, kwargs):
gc_enabled = gc.isenabled()
gc.disable()
if self.needs_setUp:
self.testcase.setUp()
self.testcase.skipTearDown = False
try:
self.function(self.testcase, *args, **kwargs)
finally:
self.testcase.tearDown()
self.testcase.doCleanups()
self.testcase.skipTearDown = True
self.needs_setUp = True
if gc_enabled:
gc.enable()
def _growth_after(self):
# Grab post snapshot
# pylint:disable=no-member
if 'urlparse' in sys.modules:
sys.modules['urlparse'].clear_cache()
if 'urllib.parse' in sys.modules:
sys.modules['urllib.parse'].clear_cache()
return self._growth()
def _check_deltas(self, growth):
# Return false when we have decided there is no leak,
# true if we should keep looping, raises an assertion
# if we have decided there is a leak.
deltas = self.deltas
if not deltas:
# We haven't run yet, no data, keep looping
return True
if gc.garbage:
raise LeakCheckError("Generated uncollectable garbage %r" % (gc.garbage,))
# the following configurations are classified as "no leak"
# [0, 0]
# [x, 0, 0]
# [... a, b, c, d] where a+b+c+d = 0
#
# the following configurations are classified as "leak"
# [... z, z, z] where z > 0
if deltas[-2:] == [0, 0] and len(deltas) in (2, 3):
return False
if deltas[-3:] == [0, 0, 0]:
return False
if len(deltas) >= 4 and sum(deltas[-4:]) == 0:
return False
if len(deltas) >= 3 and deltas[-1] > 0 and deltas[-1] == deltas[-2] and deltas[-2] == deltas[-3]:
diff = self._report_diff(growth)
raise LeakCheckError('refcount increased by %r\n%s' % (deltas, diff))
# OK, we don't know for sure yet. Let's search for more
if sum(deltas[-3:]) <= 0 or sum(deltas[-4:]) <= 0 or deltas[-4:].count(0) >= 2:
# this is suspicious, so give a few more runs
limit = 11
else:
limit = 7
if len(deltas) >= limit:
raise LeakCheckError('refcount increased by %r\n%s'
% (deltas,
self._report_diff(growth)))
# We couldn't decide yet, keep going
return True
def __call__(self, args, kwargs):
for _ in range(3):
gc.collect()
expect_failure = getattr(self.function, 'fails_leakcheck', False)
if expect_failure:
self.testcase.expect_greenlet_leak = True
self.ignored_types = getattr(self.function, "leakcheck_ignore_types", ())
# Capture state before; the incremental will be
# updated by each call to _growth_after
growth = self._growth()
try:
while self._check_deltas(growth):
self._run_test(args, kwargs)
growth = self._growth_after()
self.deltas.append(sum((stat[2] for stat in growth)))
except LeakCheckError:
if not expect_failure:
raise
else:
if expect_failure:
raise LeakCheckError("Expected %s to leak but it did not." % (self.function,))
def wrap_refcount(method):
if getattr(method, 'ignore_leakcheck', False) or SKIP_LEAKCHECKS:
return method
@wraps(method)
def wrapper(self, *args, **kwargs): # pylint:disable=too-many-branches
if getattr(self, 'ignore_leakcheck', False):
raise unittest.SkipTest("This class ignored during leakchecks")
if ONLY_FAILING_LEAKCHECKS and not getattr(method, 'fails_leakcheck', False):
raise unittest.SkipTest("Only running tests that fail leakchecks.")
return _RefCountChecker(self, method)(args, kwargs)
return wrapper

View File

@ -0,0 +1,312 @@
from __future__ import print_function
import gc
import sys
import unittest
from functools import partial
from unittest import skipUnless
from unittest import skipIf
from greenlet import greenlet
from greenlet import getcurrent
from . import TestCase
from . import PY314
try:
from contextvars import Context
from contextvars import ContextVar
from contextvars import copy_context
# From the documentation:
#
# Important: Context Variables should be created at the top module
# level and never in closures. Context objects hold strong
# references to context variables which prevents context variables
# from being properly garbage collected.
ID_VAR = ContextVar("id", default=None)
VAR_VAR = ContextVar("var", default=None)
ContextVar = None
except ImportError:
Context = ContextVar = copy_context = None
# We don't support testing if greenlet's built-in context var support is disabled.
@skipUnless(Context is not None, "ContextVar not supported")
class ContextVarsTests(TestCase):
def _new_ctx_run(self, *args, **kwargs):
return copy_context().run(*args, **kwargs)
def _increment(self, greenlet_id, callback, counts, expect):
ctx_var = ID_VAR
if expect is None:
self.assertIsNone(ctx_var.get())
else:
self.assertEqual(ctx_var.get(), expect)
ctx_var.set(greenlet_id)
for _ in range(2):
counts[ctx_var.get()] += 1
callback()
def _test_context(self, propagate_by):
# pylint:disable=too-many-branches
ID_VAR.set(0)
callback = getcurrent().switch
counts = dict((i, 0) for i in range(5))
lets = [
greenlet(partial(
partial(
copy_context().run,
self._increment
) if propagate_by == "run" else self._increment,
greenlet_id=i,
callback=callback,
counts=counts,
expect=(
i - 1 if propagate_by == "share" else
0 if propagate_by in ("set", "run") else None
)
))
for i in range(1, 5)
]
for let in lets:
if propagate_by == "set":
let.gr_context = copy_context()
elif propagate_by == "share":
let.gr_context = getcurrent().gr_context
for i in range(2):
counts[ID_VAR.get()] += 1
for let in lets:
let.switch()
if propagate_by == "run":
# Must leave each context.run() in reverse order of entry
for let in reversed(lets):
let.switch()
else:
# No context.run(), so fine to exit in any order.
for let in lets:
let.switch()
for let in lets:
self.assertTrue(let.dead)
# When using run(), we leave the run() as the greenlet dies,
# and there's no context "underneath". When not using run(),
# gr_context still reflects the context the greenlet was
# running in.
if propagate_by == 'run':
self.assertIsNone(let.gr_context)
else:
self.assertIsNotNone(let.gr_context)
if propagate_by == "share":
self.assertEqual(counts, {0: 1, 1: 1, 2: 1, 3: 1, 4: 6})
else:
self.assertEqual(set(counts.values()), set([2]))
def test_context_propagated_by_context_run(self):
self._new_ctx_run(self._test_context, "run")
def test_context_propagated_by_setting_attribute(self):
self._new_ctx_run(self._test_context, "set")
def test_context_not_propagated(self):
self._new_ctx_run(self._test_context, None)
def test_context_shared(self):
self._new_ctx_run(self._test_context, "share")
def test_break_ctxvars(self):
let1 = greenlet(copy_context().run)
let2 = greenlet(copy_context().run)
let1.switch(getcurrent().switch)
let2.switch(getcurrent().switch)
# Since let2 entered the current context and let1 exits its own, the
# interpreter emits:
# RuntimeError: cannot exit context: thread state references a different context object
let1.switch()
def test_not_broken_if_using_attribute_instead_of_context_run(self):
let1 = greenlet(getcurrent().switch)
let2 = greenlet(getcurrent().switch)
let1.gr_context = copy_context()
let2.gr_context = copy_context()
let1.switch()
let2.switch()
let1.switch()
let2.switch()
def test_context_assignment_while_running(self):
# pylint:disable=too-many-statements
ID_VAR.set(None)
def target():
self.assertIsNone(ID_VAR.get())
self.assertIsNone(gr.gr_context)
# Context is created on first use
ID_VAR.set(1)
self.assertIsInstance(gr.gr_context, Context)
self.assertEqual(ID_VAR.get(), 1)
self.assertEqual(gr.gr_context[ID_VAR], 1)
# Clearing the context makes it get re-created as another
# empty context when next used
old_context = gr.gr_context
gr.gr_context = None # assign None while running
self.assertIsNone(ID_VAR.get())
self.assertIsNone(gr.gr_context)
ID_VAR.set(2)
self.assertIsInstance(gr.gr_context, Context)
self.assertEqual(ID_VAR.get(), 2)
self.assertEqual(gr.gr_context[ID_VAR], 2)
new_context = gr.gr_context
getcurrent().parent.switch((old_context, new_context))
# parent switches us back to old_context
self.assertEqual(ID_VAR.get(), 1)
gr.gr_context = new_context # assign non-None while running
self.assertEqual(ID_VAR.get(), 2)
getcurrent().parent.switch()
# parent switches us back to no context
self.assertIsNone(ID_VAR.get())
self.assertIsNone(gr.gr_context)
gr.gr_context = old_context
self.assertEqual(ID_VAR.get(), 1)
getcurrent().parent.switch()
# parent switches us back to no context
self.assertIsNone(ID_VAR.get())
self.assertIsNone(gr.gr_context)
gr = greenlet(target)
with self.assertRaisesRegex(AttributeError, "can't delete context attribute"):
del gr.gr_context
self.assertIsNone(gr.gr_context)
old_context, new_context = gr.switch()
self.assertIs(new_context, gr.gr_context)
self.assertEqual(old_context[ID_VAR], 1)
self.assertEqual(new_context[ID_VAR], 2)
self.assertEqual(new_context.run(ID_VAR.get), 2)
gr.gr_context = old_context # assign non-None while suspended
gr.switch()
self.assertIs(gr.gr_context, new_context)
gr.gr_context = None # assign None while suspended
gr.switch()
self.assertIs(gr.gr_context, old_context)
gr.gr_context = None
gr.switch()
self.assertIsNone(gr.gr_context)
# Make sure there are no reference leaks
gr = None
gc.collect()
# Python 3.14 elides reference counting operations
# in some cases. See https://github.com/python/cpython/pull/130708
self.assertEqual(sys.getrefcount(old_context), 2 if not PY314 else 1)
self.assertEqual(sys.getrefcount(new_context), 2 if not PY314 else 1)
def test_context_assignment_different_thread(self):
import threading
VAR_VAR.set(None)
ctx = Context()
is_running = threading.Event()
should_suspend = threading.Event()
did_suspend = threading.Event()
should_exit = threading.Event()
holder = []
def greenlet_in_thread_fn():
VAR_VAR.set(1)
is_running.set()
should_suspend.wait(10)
VAR_VAR.set(2)
getcurrent().parent.switch()
holder.append(VAR_VAR.get())
def thread_fn():
gr = greenlet(greenlet_in_thread_fn)
gr.gr_context = ctx
holder.append(gr)
gr.switch()
did_suspend.set()
should_exit.wait(10)
gr.switch()
del gr
greenlet() # trigger cleanup
thread = threading.Thread(target=thread_fn, daemon=True)
thread.start()
is_running.wait(10)
gr = holder[0]
# Can't access or modify context if the greenlet is running
# in a different thread
with self.assertRaisesRegex(ValueError, "running in a different"):
getattr(gr, 'gr_context')
with self.assertRaisesRegex(ValueError, "running in a different"):
gr.gr_context = None
should_suspend.set()
did_suspend.wait(10)
# OK to access and modify context if greenlet is suspended
self.assertIs(gr.gr_context, ctx)
self.assertEqual(gr.gr_context[VAR_VAR], 2)
gr.gr_context = None
should_exit.set()
thread.join(10)
self.assertEqual(holder, [gr, None])
# Context can still be accessed/modified when greenlet is dead:
self.assertIsNone(gr.gr_context)
gr.gr_context = ctx
self.assertIs(gr.gr_context, ctx)
# Otherwise we leak greenlets on some platforms.
# XXX: Should be able to do this automatically
del holder[:]
gr = None
thread = None
def test_context_assignment_wrong_type(self):
g = greenlet()
with self.assertRaisesRegex(TypeError,
"greenlet context must be a contextvars.Context or None"):
g.gr_context = self
@skipIf(Context is not None, "ContextVar supported")
class NoContextVarsTests(TestCase):
def test_contextvars_errors(self):
let1 = greenlet(getcurrent().switch)
self.assertFalse(hasattr(let1, 'gr_context'))
with self.assertRaises(AttributeError):
getattr(let1, 'gr_context')
with self.assertRaises(AttributeError):
let1.gr_context = None
let1.switch()
with self.assertRaises(AttributeError):
getattr(let1, 'gr_context')
with self.assertRaises(AttributeError):
let1.gr_context = None
del let1
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,73 @@
from __future__ import print_function
from __future__ import absolute_import
import subprocess
import unittest
import greenlet
from . import _test_extension_cpp
from . import TestCase
from . import WIN
class CPPTests(TestCase):
def test_exception_switch(self):
greenlets = []
for i in range(4):
g = greenlet.greenlet(_test_extension_cpp.test_exception_switch)
g.switch(i)
greenlets.append(g)
for i, g in enumerate(greenlets):
self.assertEqual(g.switch(), i)
def _do_test_unhandled_exception(self, target):
import os
import sys
script = os.path.join(
os.path.dirname(__file__),
'fail_cpp_exception.py',
)
args = [sys.executable, script, target.__name__ if not isinstance(target, str) else target]
__traceback_info__ = args
with self.assertRaises(subprocess.CalledProcessError) as exc:
subprocess.check_output(
args,
encoding='utf-8',
stderr=subprocess.STDOUT
)
ex = exc.exception
expected_exit = self.get_expected_returncodes_for_aborted_process()
self.assertIn(ex.returncode, expected_exit)
self.assertIn('fail_cpp_exception is running', ex.output)
return ex.output
def test_unhandled_nonstd_exception_aborts(self):
# verify that plain unhandled throw aborts
self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_nonstd)
def test_unhandled_std_exception_aborts(self):
# verify that plain unhandled throw aborts
self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_std)
@unittest.skipIf(WIN, "XXX: This does not crash on Windows")
# Meaning the exception is getting lost somewhere...
def test_unhandled_std_exception_as_greenlet_function_aborts(self):
# verify that plain unhandled throw aborts
output = self._do_test_unhandled_exception('run_as_greenlet_target')
self.assertIn(
# We really expect this to be prefixed with "greenlet: Unhandled C++ exception:"
# as added by our handler for std::exception (see TUserGreenlet.cpp), but
# that's not correct everywhere --- our handler never runs before std::terminate
# gets called (for example, on arm32).
'Thrown from an extension.',
output
)
def test_unhandled_exception_in_greenlet_aborts(self):
# verify that unhandled throw called in greenlet aborts too
self._do_test_unhandled_exception('run_unhandled_exception_in_greenlet_aborts')
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,115 @@
from __future__ import print_function
from __future__ import absolute_import
import sys
import greenlet
from . import _test_extension
from . import TestCase
# pylint:disable=c-extension-no-member
class CAPITests(TestCase):
def test_switch(self):
self.assertEqual(
50, _test_extension.test_switch(greenlet.greenlet(lambda: 50)))
def test_switch_kwargs(self):
def adder(x, y):
return x * y
g = greenlet.greenlet(adder)
self.assertEqual(6, _test_extension.test_switch_kwargs(g, x=3, y=2))
def test_setparent(self):
# pylint:disable=disallowed-name
def foo():
def bar():
greenlet.getcurrent().parent.switch()
# This final switch should go back to the main greenlet, since
# the test_setparent() function in the C extension should have
# reparented this greenlet.
greenlet.getcurrent().parent.switch()
raise AssertionError("Should never have reached this code")
child = greenlet.greenlet(bar)
child.switch()
greenlet.getcurrent().parent.switch(child)
greenlet.getcurrent().parent.throw(
AssertionError("Should never reach this code"))
foo_child = greenlet.greenlet(foo).switch()
self.assertEqual(None, _test_extension.test_setparent(foo_child))
def test_getcurrent(self):
_test_extension.test_getcurrent()
def test_new_greenlet(self):
self.assertEqual(-15, _test_extension.test_new_greenlet(lambda: -15))
def test_raise_greenlet_dead(self):
self.assertRaises(
greenlet.GreenletExit, _test_extension.test_raise_dead_greenlet)
def test_raise_greenlet_error(self):
self.assertRaises(
greenlet.error, _test_extension.test_raise_greenlet_error)
def test_throw(self):
seen = []
def foo(): # pylint:disable=disallowed-name
try:
greenlet.getcurrent().parent.switch()
except ValueError:
seen.append(sys.exc_info()[1])
except greenlet.GreenletExit:
raise AssertionError
g = greenlet.greenlet(foo)
g.switch()
_test_extension.test_throw(g)
self.assertEqual(len(seen), 1)
self.assertTrue(
isinstance(seen[0], ValueError),
"ValueError was not raised in foo()")
self.assertEqual(
str(seen[0]),
'take that sucka!',
"message doesn't match")
def test_non_traceback_param(self):
with self.assertRaises(TypeError) as exc:
_test_extension.test_throw_exact(
greenlet.getcurrent(),
Exception,
Exception(),
self
)
self.assertEqual(str(exc.exception),
"throw() third argument must be a traceback object")
def test_instance_of_wrong_type(self):
with self.assertRaises(TypeError) as exc:
_test_extension.test_throw_exact(
greenlet.getcurrent(),
Exception(),
BaseException(),
None,
)
self.assertEqual(str(exc.exception),
"instance exception may not have a separate value")
def test_not_throwable(self):
with self.assertRaises(TypeError) as exc:
_test_extension.test_throw_exact(
greenlet.getcurrent(),
"abc",
None,
None,
)
self.assertEqual(str(exc.exception),
"exceptions must be classes, or instances, not str")
if __name__ == '__main__':
import unittest
unittest.main()

View File

@ -0,0 +1,86 @@
import gc
import weakref
import greenlet
from . import TestCase
from .leakcheck import fails_leakcheck
# These only work with greenlet gc support
# which is no longer optional.
assert greenlet.GREENLET_USE_GC
class GCTests(TestCase):
def test_dead_circular_ref(self):
o = weakref.ref(greenlet.greenlet(greenlet.getcurrent).switch())
gc.collect()
if o() is not None:
import sys
print("O IS NOT NONE.", sys.getrefcount(o()))
self.assertIsNone(o())
self.assertFalse(gc.garbage, gc.garbage)
def test_circular_greenlet(self):
class circular_greenlet(greenlet.greenlet):
self = None
o = circular_greenlet()
o.self = o
o = weakref.ref(o)
gc.collect()
self.assertIsNone(o())
self.assertFalse(gc.garbage, gc.garbage)
def test_inactive_ref(self):
class inactive_greenlet(greenlet.greenlet):
def __init__(self):
greenlet.greenlet.__init__(self, run=self.run)
def run(self):
pass
o = inactive_greenlet()
o = weakref.ref(o)
gc.collect()
self.assertIsNone(o())
self.assertFalse(gc.garbage, gc.garbage)
@fails_leakcheck
def test_finalizer_crash(self):
# This test is designed to crash when active greenlets
# are made garbage collectable, until the underlying
# problem is resolved. How does it work:
# - order of object creation is important
# - array is created first, so it is moved to unreachable first
# - we create a cycle between a greenlet and this array
# - we create an object that participates in gc, is only
# referenced by a greenlet, and would corrupt gc lists
# on destruction, the easiest is to use an object with
# a finalizer
# - because array is the first object in unreachable it is
# cleared first, which causes all references to greenlet
# to disappear and causes greenlet to be destroyed, but since
# it is still live it causes a switch during gc, which causes
# an object with finalizer to be destroyed, which causes stack
# corruption and then a crash
class object_with_finalizer(object):
def __del__(self):
pass
array = []
parent = greenlet.getcurrent()
def greenlet_body():
greenlet.getcurrent().object = object_with_finalizer()
try:
parent.switch()
except greenlet.GreenletExit:
print("Got greenlet exit!")
finally:
del greenlet.getcurrent().object
g = greenlet.greenlet(greenlet_body)
g.array = array
array.append(g)
g.switch()
del array
del g
greenlet.getcurrent()
gc.collect()

View File

@ -0,0 +1,59 @@
from greenlet import greenlet
from . import TestCase
class genlet(greenlet):
parent = None
def __init__(self, *args, **kwds):
self.args = args
self.kwds = kwds
def run(self):
fn, = self.fn
fn(*self.args, **self.kwds)
def __iter__(self):
return self
def __next__(self):
self.parent = greenlet.getcurrent()
result = self.switch()
if self:
return result
raise StopIteration
next = __next__
def Yield(value):
g = greenlet.getcurrent()
while not isinstance(g, genlet):
if g is None:
raise RuntimeError('yield outside a genlet')
g = g.parent
g.parent.switch(value)
def generator(func):
class Generator(genlet):
fn = (func,)
return Generator
# ____________________________________________________________
class GeneratorTests(TestCase):
def test_generator(self):
seen = []
def g(n):
for i in range(n):
seen.append(i)
Yield(i)
g = generator(g)
for _ in range(3):
for j in g(5):
seen.append(j)
self.assertEqual(seen, 3 * [0, 0, 1, 1, 2, 2, 3, 3, 4, 4])

View File

@ -0,0 +1,168 @@
from greenlet import greenlet
from . import TestCase
from .leakcheck import fails_leakcheck
class genlet(greenlet):
parent = None
def __init__(self, *args, **kwds):
self.args = args
self.kwds = kwds
self.child = None
def run(self):
# Note the function is packed in a tuple
# to avoid creating a bound method for it.
fn, = self.fn
fn(*self.args, **self.kwds)
def __iter__(self):
return self
def set_child(self, child):
self.child = child
def __next__(self):
if self.child:
child = self.child
while child.child:
tmp = child
child = child.child
tmp.child = None
result = child.switch()
else:
self.parent = greenlet.getcurrent()
result = self.switch()
if self:
return result
raise StopIteration
next = __next__
def Yield(value, level=1):
g = greenlet.getcurrent()
while level != 0:
if not isinstance(g, genlet):
raise RuntimeError('yield outside a genlet')
if level > 1:
g.parent.set_child(g)
g = g.parent
level -= 1
g.switch(value)
def Genlet(func):
class TheGenlet(genlet):
fn = (func,)
return TheGenlet
# ____________________________________________________________
def g1(n, seen):
for i in range(n):
seen.append(i + 1)
yield i
def g2(n, seen):
for i in range(n):
seen.append(i + 1)
Yield(i)
g2 = Genlet(g2)
def nested(i):
Yield(i)
def g3(n, seen):
for i in range(n):
seen.append(i + 1)
nested(i)
g3 = Genlet(g3)
def a(n):
if n == 0:
return
for ii in ax(n - 1):
Yield(ii)
Yield(n)
ax = Genlet(a)
def perms(l):
if len(l) > 1:
for e in l:
# No syntactical sugar for generator expressions
x = [Yield([e] + p) for p in perms([x for x in l if x != e])]
assert x
else:
Yield(l)
perms = Genlet(perms)
def gr1(n):
for ii in range(1, n):
Yield(ii)
Yield(ii * ii, 2)
gr1 = Genlet(gr1)
def gr2(n, seen):
for ii in gr1(n):
seen.append(ii)
gr2 = Genlet(gr2)
class NestedGeneratorTests(TestCase):
def test_layered_genlets(self):
seen = []
for ii in gr2(5, seen):
seen.append(ii)
self.assertEqual(seen, [1, 1, 2, 4, 3, 9, 4, 16])
@fails_leakcheck
def test_permutations(self):
gen_perms = perms(list(range(4)))
permutations = list(gen_perms)
self.assertEqual(len(permutations), 4 * 3 * 2 * 1)
self.assertIn([0, 1, 2, 3], permutations)
self.assertIn([3, 2, 1, 0], permutations)
res = []
for ii in zip(perms(list(range(4))), perms(list(range(3)))):
res.append(ii)
self.assertEqual(
res,
[([0, 1, 2, 3], [0, 1, 2]), ([0, 1, 3, 2], [0, 2, 1]),
([0, 2, 1, 3], [1, 0, 2]), ([0, 2, 3, 1], [1, 2, 0]),
([0, 3, 1, 2], [2, 0, 1]), ([0, 3, 2, 1], [2, 1, 0])])
# XXX Test to make sure we are working as a generator expression
def test_genlet_simple(self):
for g in g1, g2, g3:
seen = []
for _ in range(3):
for j in g(5, seen):
seen.append(j)
self.assertEqual(seen, 3 * [1, 0, 2, 1, 3, 2, 4, 3, 5, 4])
def test_genlet_bad(self):
try:
Yield(10)
except RuntimeError:
pass
def test_nested_genlets(self):
seen = []
for ii in ax(5):
seen.append(ii)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
"""
Tests for greenlets interacting with the CPython trash can API.
The CPython trash can API is not designed to be re-entered from a
single thread. But this can happen using greenlets, if something
during the object deallocation process switches greenlets, and this second
greenlet then causes the trash can to get entered again. Here, we do this
very explicitly, but in other cases (like gevent) it could be arbitrarily more
complicated: for example, a weakref callback might try to acquire a lock that's
already held by another greenlet; that would allow a greenlet switch to occur.
See https://github.com/gevent/gevent/issues/1909
This test is fragile and relies on details of the CPython
implementation (like most of the rest of this package):
- We enter the trashcan and deferred deallocation after
``_PyTrash_UNWIND_LEVEL`` calls. This constant, defined in
CPython's object.c, is generally 50. That's basically how many objects are required to
get us into the deferred deallocation situation.
- The test fails by hitting an ``assert()`` in object.c; if the
build didn't enable assert, then we don't catch this.
- If the test fails in that way, the interpreter crashes.
"""
from __future__ import print_function, absolute_import, division
import unittest
class TestTrashCanReEnter(unittest.TestCase):
def test_it(self):
try:
# pylint:disable-next=no-name-in-module
from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=unused-import
except ImportError:
import sys
# Python 3.13 has not "trash delete nesting" anymore (but "delete later")
assert sys.version_info[:2] >= (3, 13)
self.skipTest("get_tstate_trash_delete_nesting is not available.")
# Try several times to trigger it, because it isn't 100%
# reliable.
for _ in range(10):
self.check_it()
def check_it(self): # pylint:disable=too-many-statements
import greenlet
from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=no-name-in-module
main = greenlet.getcurrent()
assert get_tstate_trash_delete_nesting() == 0
# We expect to be in deferred deallocation after this many
# deallocations have occurred. TODO: I wish we had a better way to do
# this --- that was before get_tstate_trash_delete_nesting; perhaps
# we can use that API to do better?
TRASH_UNWIND_LEVEL = 50
# How many objects to put in a container; it's the container that
# queues objects for deferred deallocation.
OBJECTS_PER_CONTAINER = 500
class Dealloc: # define the class here because we alter class variables each time we run.
"""
An object with a ``__del__`` method. When it starts getting deallocated
from a deferred trash can run, it switches greenlets, allocates more objects
which then also go in the trash can. If we don't save state appropriately,
nesting gets out of order and we can crash the interpreter.
"""
#: Has our deallocation actually run and switched greenlets?
#: When it does, this will be set to the current greenlet. This should
#: be happening in the main greenlet, so we check that down below.
SPAWNED = False
#: Has the background greenlet run?
BG_RAN = False
BG_GLET = None
#: How many of these things have ever been allocated.
CREATED = 0
#: How many of these things have ever been deallocated.
DESTROYED = 0
#: How many were destroyed not in the main greenlet. There should always
#: be some.
#: If the test is broken or things change in the trashcan implementation,
#: this may not be correct.
DESTROYED_BG = 0
def __init__(self, sequence_number):
"""
:param sequence_number: The ordinal of this object during
one particular creation run. This is used to detect (guess, really)
when we have entered the trash can's deferred deallocation.
"""
self.i = sequence_number
Dealloc.CREATED += 1
def __del__(self):
if self.i == TRASH_UNWIND_LEVEL and not self.SPAWNED:
Dealloc.SPAWNED = greenlet.getcurrent()
other = Dealloc.BG_GLET = greenlet.greenlet(background_greenlet)
x = other.switch()
assert x == 42
# It's important that we don't switch back to the greenlet,
# we leave it hanging there in an incomplete state. But we don't let it
# get collected, either. If we complete it now, while we're still
# in the scope of the initial trash can, things work out and we
# don't see the problem. We need this greenlet to complete
# at some point in the future, after we've exited this trash can invocation.
del other
elif self.i == 40 and greenlet.getcurrent() is not main:
Dealloc.BG_RAN = True
try:
main.switch(42)
except greenlet.GreenletExit as ex:
# We expect this; all references to us go away
# while we're still running, and we need to finish deleting
# ourself.
Dealloc.BG_RAN = type(ex)
del ex
# Record the fact that we're dead last of all. This ensures that
# we actually get returned too.
Dealloc.DESTROYED += 1
if greenlet.getcurrent() is not main:
Dealloc.DESTROYED_BG += 1
def background_greenlet():
# We direct through a second function, instead of
# directly calling ``make_some()``, so that we have complete
# control over when these objects are destroyed: we need them
# to be destroyed in the context of the background greenlet
t = make_some()
del t # Triggere deletion.
def make_some():
t = ()
i = OBJECTS_PER_CONTAINER
while i:
# Nest the tuples; it's the recursion that gets us
# into trash.
t = (Dealloc(i), t)
i -= 1
return t
some = make_some()
self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER)
self.assertEqual(Dealloc.DESTROYED, 0)
# If we're going to crash, it should be on the following line.
# We only crash if ``assert()`` is enabled, of course.
del some
# For non-debug builds of CPython, we won't crash. The best we can do is check
# the nesting level explicitly.
self.assertEqual(0, get_tstate_trash_delete_nesting())
# Discard this, raising GreenletExit into where it is waiting.
Dealloc.BG_GLET = None
# The same nesting level maintains.
self.assertEqual(0, get_tstate_trash_delete_nesting())
# We definitely cleaned some up in the background
self.assertGreater(Dealloc.DESTROYED_BG, 0)
# Make sure all the cleanups happened.
self.assertIs(Dealloc.SPAWNED, main)
self.assertTrue(Dealloc.BG_RAN)
self.assertEqual(Dealloc.BG_RAN, greenlet.GreenletExit)
self.assertEqual(Dealloc.CREATED, Dealloc.DESTROYED )
self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER * 2)
import gc
gc.collect()
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,447 @@
# -*- coding: utf-8 -*-
"""
Testing scenarios that may have leaked.
"""
from __future__ import print_function, absolute_import, division
import sys
import gc
import time
import weakref
import threading
import greenlet
from . import TestCase
from . import PY314
from .leakcheck import fails_leakcheck
from .leakcheck import ignores_leakcheck
from .leakcheck import RUNNING_ON_MANYLINUX
# pylint:disable=protected-access
assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0
class HasFinalizerTracksInstances(object):
EXTANT_INSTANCES = set()
def __init__(self, msg):
self.msg = sys.intern(msg)
self.EXTANT_INSTANCES.add(id(self))
def __del__(self):
self.EXTANT_INSTANCES.remove(id(self))
def __repr__(self):
return "<HasFinalizerTracksInstances at 0x%x %r>" % (
id(self), self.msg
)
@classmethod
def reset(cls):
cls.EXTANT_INSTANCES.clear()
class TestLeaks(TestCase):
def test_arg_refs(self):
args = ('a', 'b', 'c')
refcount_before = sys.getrefcount(args)
# pylint:disable=unnecessary-lambda
g = greenlet.greenlet(
lambda *args: greenlet.getcurrent().parent.switch(*args))
for _ in range(100):
g.switch(*args)
self.assertEqual(sys.getrefcount(args), refcount_before)
def test_kwarg_refs(self):
kwargs = {}
self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1)
# pylint:disable=unnecessary-lambda
g = greenlet.greenlet(
lambda **gkwargs: greenlet.getcurrent().parent.switch(**gkwargs))
for _ in range(100):
g.switch(**kwargs)
# Python 3.14 elides reference counting operations
# in some cases. See https://github.com/python/cpython/pull/130708
self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1)
@staticmethod
def __recycle_threads():
# By introducing a thread that does sleep we allow other threads,
# that have triggered their __block condition, but did not have a
# chance to deallocate their thread state yet, to finally do so.
# The way it works is by requiring a GIL switch (different thread),
# which does a GIL release (sleep), which might do a GIL switch
# to finished threads and allow them to clean up.
def worker():
time.sleep(0.001)
t = threading.Thread(target=worker)
t.start()
time.sleep(0.001)
t.join(10)
def test_threaded_leak(self):
gg = []
def worker():
# only main greenlet present
gg.append(weakref.ref(greenlet.getcurrent()))
for _ in range(2):
t = threading.Thread(target=worker)
t.start()
t.join(10)
del t
greenlet.getcurrent() # update ts_current
self.__recycle_threads()
greenlet.getcurrent() # update ts_current
gc.collect()
greenlet.getcurrent() # update ts_current
for g in gg:
self.assertIsNone(g())
def test_threaded_adv_leak(self):
gg = []
def worker():
# main and additional *finished* greenlets
ll = greenlet.getcurrent().ll = []
def additional():
ll.append(greenlet.getcurrent())
for _ in range(2):
greenlet.greenlet(additional).switch()
gg.append(weakref.ref(greenlet.getcurrent()))
for _ in range(2):
t = threading.Thread(target=worker)
t.start()
t.join(10)
del t
greenlet.getcurrent() # update ts_current
self.__recycle_threads()
greenlet.getcurrent() # update ts_current
gc.collect()
greenlet.getcurrent() # update ts_current
for g in gg:
self.assertIsNone(g())
def assertClocksUsed(self):
used = greenlet._greenlet.get_clocks_used_doing_optional_cleanup()
self.assertGreaterEqual(used, 0)
# we don't lose the value
greenlet._greenlet.enable_optional_cleanup(True)
used2 = greenlet._greenlet.get_clocks_used_doing_optional_cleanup()
self.assertEqual(used, used2)
self.assertGreater(greenlet._greenlet.CLOCKS_PER_SEC, 1)
def _check_issue251(self,
manually_collect_background=True,
explicit_reference_to_switch=False):
# See https://github.com/python-greenlet/greenlet/issues/251
# Killing a greenlet (probably not the main one)
# in one thread from another thread would
# result in leaking a list (the ts_delkey list).
# We no longer use lists to hold that stuff, though.
# For the test to be valid, even empty lists have to be tracked by the
# GC
assert gc.is_tracked([])
HasFinalizerTracksInstances.reset()
greenlet.getcurrent()
greenlets_before = self.count_objects(greenlet.greenlet, exact_kind=False)
background_glet_running = threading.Event()
background_glet_killed = threading.Event()
background_greenlets = []
# XXX: Switching this to a greenlet subclass that overrides
# run results in all callers failing the leaktest; that
# greenlet instance is leaked. There's a bound method for
# run() living on the stack of the greenlet in g_initialstub,
# and since we don't manually switch back to the background
# greenlet to let it "fall off the end" and exit the
# g_initialstub function, it never gets cleaned up. Making the
# garbage collector aware of this bound method (making it an
# attribute of the greenlet structure and traversing into it)
# doesn't help, for some reason.
def background_greenlet():
# Throw control back to the main greenlet.
jd = HasFinalizerTracksInstances("DELETING STACK OBJECT")
greenlet._greenlet.set_thread_local(
'test_leaks_key',
HasFinalizerTracksInstances("DELETING THREAD STATE"))
# Explicitly keeping 'switch' in a local variable
# breaks this test in all versions
if explicit_reference_to_switch:
s = greenlet.getcurrent().parent.switch
s([jd])
else:
greenlet.getcurrent().parent.switch([jd])
bg_main_wrefs = []
def background_thread():
glet = greenlet.greenlet(background_greenlet)
bg_main_wrefs.append(weakref.ref(glet.parent))
background_greenlets.append(glet)
glet.switch() # Be sure it's active.
# Control is ours again.
del glet # Delete one reference from the thread it runs in.
background_glet_running.set()
background_glet_killed.wait(10)
# To trigger the background collection of the dead
# greenlet, thus clearing out the contents of the list, we
# need to run some APIs. See issue 252.
if manually_collect_background:
greenlet.getcurrent()
t = threading.Thread(target=background_thread)
t.start()
background_glet_running.wait(10)
greenlet.getcurrent()
lists_before = self.count_objects(list, exact_kind=True)
assert len(background_greenlets) == 1
self.assertFalse(background_greenlets[0].dead)
# Delete the last reference to the background greenlet
# from a different thread. This puts it in the background thread's
# ts_delkey list.
del background_greenlets[:]
background_glet_killed.set()
# Now wait for the background thread to die.
t.join(10)
del t
# As part of the fix for 252, we need to cycle the ceval.c
# interpreter loop to be sure it has had a chance to process
# the pending call.
self.wait_for_pending_cleanups()
lists_after = self.count_objects(list, exact_kind=True)
greenlets_after = self.count_objects(greenlet.greenlet, exact_kind=False)
# On 2.7, we observe that lists_after is smaller than
# lists_before. No idea what lists got cleaned up. All the
# Python 3 versions match exactly.
self.assertLessEqual(lists_after, lists_before)
# On versions after 3.6, we've successfully cleaned up the
# greenlet references thanks to the internal "vectorcall"
# protocol; prior to that, there is a reference path through
# the ``greenlet.switch`` method still on the stack that we
# can't reach to clean up. The C code goes through terrific
# lengths to clean that up.
if not explicit_reference_to_switch \
and greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None:
# If cleanup was disabled, though, we may not find it.
self.assertEqual(greenlets_after, greenlets_before)
if manually_collect_background:
# TODO: Figure out how to make this work!
# The one on the stack is still leaking somehow
# in the non-manually-collect state.
self.assertEqual(HasFinalizerTracksInstances.EXTANT_INSTANCES, set())
else:
# The explicit reference prevents us from collecting it
# and it isn't always found by the GC either for some
# reason. The entire frame is leaked somehow, on some
# platforms (e.g., MacPorts builds of Python (all
# versions!)), but not on other platforms (the linux and
# windows builds on GitHub actions and Appveyor). So we'd
# like to write a test that proves that the main greenlet
# sticks around, and we can on my machine (macOS 11.6,
# MacPorts builds of everything) but we can't write that
# same test on other platforms. However, hopefully iteration
# done by leakcheck will find it.
pass
if greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None:
self.assertClocksUsed()
def test_issue251_killing_cross_thread_leaks_list(self):
self._check_issue251()
def test_issue251_with_cleanup_disabled(self):
greenlet._greenlet.enable_optional_cleanup(False)
try:
self._check_issue251()
finally:
greenlet._greenlet.enable_optional_cleanup(True)
@fails_leakcheck
def test_issue251_issue252_need_to_collect_in_background(self):
# Between greenlet 1.1.2 and the next version, this was still
# failing because the leak of the list still exists when we
# don't call a greenlet API before exiting the thread. The
# proximate cause is that neither of the two greenlets from
# the background thread are actually being destroyed, even
# though the GC is in fact visiting both objects. It's not
# clear where that leak is? For some reason the thread-local
# dict holding it isn't being cleaned up.
#
# The leak, I think, is in the CPYthon internal function that
# calls into green_switch(). The argument tuple is still on
# the C stack somewhere and can't be reached? That doesn't
# make sense, because the tuple should be collectable when
# this object goes away.
#
# Note that this test sometimes spuriously passes on Linux,
# for some reason, but I've never seen it pass on macOS.
self._check_issue251(manually_collect_background=False)
@fails_leakcheck
def test_issue251_issue252_need_to_collect_in_background_cleanup_disabled(self):
self.expect_greenlet_leak = True
greenlet._greenlet.enable_optional_cleanup(False)
try:
self._check_issue251(manually_collect_background=False)
finally:
greenlet._greenlet.enable_optional_cleanup(True)
@fails_leakcheck
def test_issue251_issue252_explicit_reference_not_collectable(self):
self._check_issue251(
manually_collect_background=False,
explicit_reference_to_switch=True)
UNTRACK_ATTEMPTS = 100
def _only_test_some_versions(self):
# We're only looking for this problem specifically on 3.11,
# and this set of tests is relatively fragile, depending on
# OS and memory management details. So we want to run it on 3.11+
# (obviously) but not every older 3.x version in order to reduce
# false negatives. At the moment, those false results seem to have
# resolved, so we are actually running this on 3.8+
assert sys.version_info[0] >= 3
if sys.version_info[:2] < (3, 8):
self.skipTest('Only observed on 3.11')
if RUNNING_ON_MANYLINUX:
self.skipTest("Slow and not worth repeating here")
@ignores_leakcheck
# Because we're just trying to track raw memory, not objects, and running
# the leakcheck makes an already slow test slower.
def test_untracked_memory_doesnt_increase(self):
# See https://github.com/gevent/gevent/issues/1924
# and https://github.com/python-greenlet/greenlet/issues/328
self._only_test_some_versions()
def f():
return 1
ITER = 10000
def run_it():
for _ in range(ITER):
greenlet.greenlet(f).switch()
# Establish baseline
for _ in range(3):
run_it()
# uss: (Linux, macOS, Windows): aka "Unique Set Size", this is
# the memory which is unique to a process and which would be
# freed if the process was terminated right now.
uss_before = self.get_process_uss()
for count in range(self.UNTRACK_ATTEMPTS):
uss_before = max(uss_before, self.get_process_uss())
run_it()
uss_after = self.get_process_uss()
if uss_after <= uss_before and count > 1:
break
self.assertLessEqual(uss_after, uss_before)
def _check_untracked_memory_thread(self, deallocate_in_thread=True):
self._only_test_some_versions()
# Like the above test, but what if there are a bunch of
# unfinished greenlets in a thread that dies?
# Does it matter if we deallocate in the thread or not?
EXIT_COUNT = [0]
def f():
try:
greenlet.getcurrent().parent.switch()
except greenlet.GreenletExit:
EXIT_COUNT[0] += 1
raise
return 1
ITER = 10000
def run_it():
glets = []
for _ in range(ITER):
# Greenlet starts, switches back to us.
# We keep a strong reference to the greenlet though so it doesn't
# get a GreenletExit exception.
g = greenlet.greenlet(f)
glets.append(g)
g.switch()
return glets
test = self
class ThreadFunc:
uss_before = uss_after = 0
glets = ()
ITER = 2
def __call__(self):
self.uss_before = test.get_process_uss()
for _ in range(self.ITER):
self.glets += tuple(run_it())
for g in self.glets:
test.assertIn('suspended active', str(g))
# Drop them.
if deallocate_in_thread:
self.glets = ()
self.uss_after = test.get_process_uss()
# Establish baseline
uss_before = uss_after = None
for count in range(self.UNTRACK_ATTEMPTS):
EXIT_COUNT[0] = 0
thread_func = ThreadFunc()
t = threading.Thread(target=thread_func)
t.start()
t.join(30)
self.assertFalse(t.is_alive())
if uss_before is None:
uss_before = thread_func.uss_before
uss_before = max(uss_before, thread_func.uss_before)
if deallocate_in_thread:
self.assertEqual(thread_func.glets, ())
self.assertEqual(EXIT_COUNT[0], ITER * thread_func.ITER)
del thread_func # Deallocate the greenlets; but this won't raise into them
del t
if not deallocate_in_thread:
self.assertEqual(EXIT_COUNT[0], 0)
if deallocate_in_thread:
self.wait_for_pending_cleanups()
uss_after = self.get_process_uss()
# See if we achieve a non-growth state at some point. Break when we do.
if uss_after <= uss_before and count > 1:
break
self.wait_for_pending_cleanups()
uss_after = self.get_process_uss()
self.assertLessEqual(uss_after, uss_before, "after attempts %d" % (count,))
@ignores_leakcheck
# Because we're just trying to track raw memory, not objects, and running
# the leakcheck makes an already slow test slower.
def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_thread(self):
self._check_untracked_memory_thread(deallocate_in_thread=True)
@ignores_leakcheck
# Because the main greenlets from the background threads do not exit in a timely fashion,
# we fail the object-based leakchecks.
def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main(self):
self._check_untracked_memory_thread(deallocate_in_thread=False)
if __name__ == '__main__':
__import__('unittest').main()

View File

@ -0,0 +1,19 @@
import greenlet
from . import TestCase
class Test(TestCase):
def test_stack_saved(self):
main = greenlet.getcurrent()
self.assertEqual(main._stack_saved, 0)
def func():
main.switch(main._stack_saved)
g = greenlet.greenlet(func)
x = g.switch()
self.assertGreater(x, 0)
self.assertGreater(g._stack_saved, 0)
g.switch()
self.assertEqual(g._stack_saved, 0)

View File

@ -0,0 +1,128 @@
import sys
from greenlet import greenlet
from . import TestCase
def switch(*args):
return greenlet.getcurrent().parent.switch(*args)
class ThrowTests(TestCase):
def test_class(self):
def f():
try:
switch("ok")
except RuntimeError:
switch("ok")
return
switch("fail")
g = greenlet(f)
res = g.switch()
self.assertEqual(res, "ok")
res = g.throw(RuntimeError)
self.assertEqual(res, "ok")
def test_val(self):
def f():
try:
switch("ok")
except RuntimeError:
val = sys.exc_info()[1]
if str(val) == "ciao":
switch("ok")
return
switch("fail")
g = greenlet(f)
res = g.switch()
self.assertEqual(res, "ok")
res = g.throw(RuntimeError("ciao"))
self.assertEqual(res, "ok")
g = greenlet(f)
res = g.switch()
self.assertEqual(res, "ok")
res = g.throw(RuntimeError, "ciao")
self.assertEqual(res, "ok")
def test_kill(self):
def f():
switch("ok")
switch("fail")
g = greenlet(f)
res = g.switch()
self.assertEqual(res, "ok")
res = g.throw()
self.assertTrue(isinstance(res, greenlet.GreenletExit))
self.assertTrue(g.dead)
res = g.throw() # immediately eaten by the already-dead greenlet
self.assertTrue(isinstance(res, greenlet.GreenletExit))
def test_throw_goes_to_original_parent(self):
main = greenlet.getcurrent()
def f1():
try:
main.switch("f1 ready to catch")
except IndexError:
return "caught"
return "normal exit"
def f2():
main.switch("from f2")
g1 = greenlet(f1)
g2 = greenlet(f2, parent=g1)
with self.assertRaises(IndexError):
g2.throw(IndexError)
self.assertTrue(g2.dead)
self.assertTrue(g1.dead)
g1 = greenlet(f1)
g2 = greenlet(f2, parent=g1)
res = g1.switch()
self.assertEqual(res, "f1 ready to catch")
res = g2.throw(IndexError)
self.assertEqual(res, "caught")
self.assertTrue(g2.dead)
self.assertTrue(g1.dead)
g1 = greenlet(f1)
g2 = greenlet(f2, parent=g1)
res = g1.switch()
self.assertEqual(res, "f1 ready to catch")
res = g2.switch()
self.assertEqual(res, "from f2")
res = g2.throw(IndexError)
self.assertEqual(res, "caught")
self.assertTrue(g2.dead)
self.assertTrue(g1.dead)
def test_non_traceback_param(self):
with self.assertRaises(TypeError) as exc:
greenlet.getcurrent().throw(
Exception,
Exception(),
self
)
self.assertEqual(str(exc.exception),
"throw() third argument must be a traceback object")
def test_instance_of_wrong_type(self):
with self.assertRaises(TypeError) as exc:
greenlet.getcurrent().throw(
Exception(),
BaseException()
)
self.assertEqual(str(exc.exception),
"instance exception may not have a separate value")
def test_not_throwable(self):
with self.assertRaises(TypeError) as exc:
greenlet.getcurrent().throw(
"abc"
)
self.assertEqual(str(exc.exception),
"exceptions must be classes, or instances, not str")

View File

@ -0,0 +1,291 @@
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()

View File

@ -0,0 +1,41 @@
#! /usr/bin/env python
from __future__ import absolute_import
from __future__ import print_function
import sys
import os
from unittest import TestCase as NonLeakingTestCase
import greenlet
# No reason to run this multiple times under leakchecks,
# it doesn't do anything.
class VersionTests(NonLeakingTestCase):
def test_version(self):
def find_dominating_file(name):
if os.path.exists(name):
return name
tried = []
here = os.path.abspath(os.path.dirname(__file__))
for i in range(10):
up = ['..'] * i
path = [here] + up + [name]
fname = os.path.join(*path)
fname = os.path.abspath(fname)
tried.append(fname)
if os.path.exists(fname):
return fname
raise AssertionError("Could not find file " + name + "; checked " + str(tried))
try:
setup_py = find_dominating_file('setup.py')
except AssertionError as e:
self.skipTest("Unable to find setup.py; must be out of tree. " + str(e))
invoke_setup = "%s %s --version" % (sys.executable, setup_py)
with os.popen(invoke_setup) as f:
sversion = f.read().strip()
self.assertEqual(sversion, greenlet.__version__)

View File

@ -0,0 +1,35 @@
import gc
import weakref
import greenlet
from . import TestCase
class WeakRefTests(TestCase):
def test_dead_weakref(self):
def _dead_greenlet():
g = greenlet.greenlet(lambda: None)
g.switch()
return g
o = weakref.ref(_dead_greenlet())
gc.collect()
self.assertEqual(o(), None)
def test_inactive_weakref(self):
o = weakref.ref(greenlet.greenlet())
gc.collect()
self.assertEqual(o(), None)
def test_dealloc_weakref(self):
seen = []
def worker():
try:
greenlet.getcurrent().parent.switch()
finally:
seen.append(g())
g = greenlet.greenlet(worker)
g.switch()
g2 = greenlet.greenlet(lambda: None, g)
g = weakref.ref(g2)
g2 = None
self.assertEqual(seen, [None])