]> granicus.if.org Git - python/commitdiff
bpo-30524: Write unit tests for FASTCALL (#2022)
authorVictor Stinner <victor.stinner@gmail.com>
Fri, 9 Jun 2017 14:48:45 +0000 (16:48 +0200)
committerGitHub <noreply@github.com>
Fri, 9 Jun 2017 14:48:45 +0000 (16:48 +0200)
Test C functions:

* _PyObject_FastCall()
* _PyObject_FastCallDict()
* _PyObject_FastCallKeywords()

Lib/test/test_call.py
Modules/_testcapimodule.c

index 0992a0e3612a581ace7bfba1b7ac4e4691b9608e..d929cfc0887ba8dc3c71d6d4af97074aef5cab4b 100644 (file)
@@ -1,5 +1,10 @@
+import datetime
 import unittest
 from test.support import cpython_only
+try:
+    import _testcapi
+except ImportError:
+    _testcapi = None
 
 # The test cases here cover several paths through the function calling
 # code.  They depend on the METH_XXX flag that is used to define a C
@@ -176,5 +181,175 @@ class CFunctionCallsErrorMessages(unittest.TestCase):
         self.assertRaisesRegex(TypeError, msg, [].count, x=2, y=2)
 
 
+def pyfunc(arg1, arg2):
+    return [arg1, arg2]
+
+
+def pyfunc_noarg():
+    return "noarg"
+
+
+class PythonClass:
+    def method(self, arg1, arg2):
+        return [arg1, arg2]
+
+    def method_noarg(self):
+        return "noarg"
+
+    @classmethod
+    def class_method(cls):
+        return "classmethod"
+
+    @staticmethod
+    def static_method():
+        return "staticmethod"
+
+
+PYTHON_INSTANCE = PythonClass()
+
+
+IGNORE_RESULT = object()
+
+
+@cpython_only
+class FastCallTests(unittest.TestCase):
+    # Test calls with positional arguments
+    CALLS_POSARGS = (
+        # (func, args: tuple, result)
+
+        # Python function with 2 arguments
+        (pyfunc, (1, 2), [1, 2]),
+
+        # Python function without argument
+        (pyfunc_noarg, (), "noarg"),
+
+        # Python class methods
+        (PythonClass.class_method, (), "classmethod"),
+        (PythonClass.static_method, (), "staticmethod"),
+
+        # Python instance methods
+        (PYTHON_INSTANCE.method, (1, 2), [1, 2]),
+        (PYTHON_INSTANCE.method_noarg, (), "noarg"),
+        (PYTHON_INSTANCE.class_method, (), "classmethod"),
+        (PYTHON_INSTANCE.static_method, (), "staticmethod"),
+
+        # C function: METH_NOARGS
+        (globals, (), IGNORE_RESULT),
+
+        # C function: METH_O
+        (id, ("hello",), IGNORE_RESULT),
+
+        # C function: METH_VARARGS
+        (dir, (1,), IGNORE_RESULT),
+
+        # C function: METH_VARARGS | METH_KEYWORDS
+        (min, (5, 9), 5),
+
+        # C function: METH_FASTCALL
+        (divmod, (1000, 33), (30, 10)),
+
+        # C type static method: METH_FASTCALL | METH_CLASS
+        (int.from_bytes, (b'\x01\x00', 'little'), 1),
+
+        # bpo-30524: Test that calling a C type static method with no argument
+        # doesn't crash (ignore the result): METH_FASTCALL | METH_CLASS
+        (datetime.datetime.now, (), IGNORE_RESULT),
+    )
+
+    # Test calls with positional and keyword arguments
+    CALLS_KWARGS = (
+        # (func, args: tuple, kwargs: dict, result)
+
+        # Python function with 2 arguments
+        (pyfunc, (1,), {'arg2': 2}, [1, 2]),
+        (pyfunc, (), {'arg1': 1, 'arg2': 2}, [1, 2]),
+
+        # Python instance methods
+        (PYTHON_INSTANCE.method, (1,), {'arg2': 2}, [1, 2]),
+        (PYTHON_INSTANCE.method, (), {'arg1': 1, 'arg2': 2}, [1, 2]),
+
+        # C function: METH_VARARGS | METH_KEYWORDS
+        (max, ([],), {'default': 9}, 9),
+
+        # C type static method: METH_FASTCALL | METH_CLASS
+        (int.from_bytes, (b'\x01\x00',), {'byteorder': 'little'}, 1),
+        (int.from_bytes, (), {'bytes': b'\x01\x00', 'byteorder': 'little'}, 1),
+    )
+
+    def check_result(self, result, expected):
+        if expected is IGNORE_RESULT:
+            return
+        self.assertEqual(result, expected)
+
+    def test_fastcall(self):
+        # Test _PyObject_FastCall()
+
+        for func, args, expected in self.CALLS_POSARGS:
+            with self.subTest(func=func, args=args):
+                result = _testcapi.pyobject_fastcall(func, args)
+                self.check_result(result, expected)
+
+                if not args:
+                    # args=NULL, nargs=0
+                    result = _testcapi.pyobject_fastcall(func, None)
+                    self.check_result(result, expected)
+
+    def test_fastcall_dict(self):
+        # Test _PyObject_FastCallDict()
+
+        for func, args, expected in self.CALLS_POSARGS:
+            with self.subTest(func=func, args=args):
+                # kwargs=NULL
+                result = _testcapi.pyobject_fastcalldict(func, args, None)
+                self.check_result(result, expected)
+
+                # kwargs={}
+                result = _testcapi.pyobject_fastcalldict(func, args, {})
+                self.check_result(result, expected)
+
+                if not args:
+                    # args=NULL, nargs=0, kwargs=NULL
+                    result = _testcapi.pyobject_fastcalldict(func, None, None)
+                    self.check_result(result, expected)
+
+                    # args=NULL, nargs=0, kwargs={}
+                    result = _testcapi.pyobject_fastcalldict(func, None, {})
+                    self.check_result(result, expected)
+
+        for func, args, kwargs, expected in self.CALLS_KWARGS:
+            with self.subTest(func=func, args=args, kwargs=kwargs):
+                result = _testcapi.pyobject_fastcalldict(func, args, kwargs)
+                self.check_result(result, expected)
+
+    def test_fastcall_keywords(self):
+        # Test _PyObject_FastCallKeywords()
+
+        for func, args, expected in self.CALLS_POSARGS:
+            with self.subTest(func=func, args=args):
+                # kwnames=NULL
+                result = _testcapi.pyobject_fastcallkeywords(func, args, None)
+                self.check_result(result, expected)
+
+                # kwnames=()
+                result = _testcapi.pyobject_fastcallkeywords(func, args, ())
+                self.check_result(result, expected)
+
+                if not args:
+                    # kwnames=NULL
+                    result = _testcapi.pyobject_fastcallkeywords(func, None, None)
+                    self.check_result(result, expected)
+
+                    # kwnames=()
+                    result = _testcapi.pyobject_fastcallkeywords(func, None, ())
+                    self.check_result(result, expected)
+
+        for func, args, kwargs, expected in self.CALLS_KWARGS:
+            with self.subTest(func=func, args=args, kwargs=kwargs):
+                kwnames = tuple(kwargs.keys())
+                args = args + tuple(kwargs.values())
+                result = _testcapi.pyobject_fastcallkeywords(func, args, kwnames)
+                self.check_result(result, expected)
+
+
 if __name__ == "__main__":
     unittest.main()
index 7f7a13ee51d6c4782ae0a6ef88c3f42593e40624..8c44ad205bff305da00d25f5f7b57257bbe9e8f3 100644 (file)
@@ -4051,6 +4051,104 @@ raise_SIGINT_then_send_None(PyObject *self, PyObject *args)
 }
 
 
+static int
+fastcall_args(PyObject *args, PyObject ***stack, Py_ssize_t *nargs)
+{
+    if (args == Py_None) {
+        *stack = NULL;
+        *nargs = 0;
+    }
+    else if (PyTuple_Check(args)) {
+        *stack = &PyTuple_GET_ITEM(args, 0);
+        *nargs = PyTuple_GET_SIZE(args);
+    }
+    else {
+        PyErr_SetString(PyExc_TypeError, "args must be None or a tuple");
+        return -1;
+    }
+    return 0;
+}
+
+
+static PyObject *
+test_pyobject_fastcall(PyObject *self, PyObject *args)
+{
+    PyObject *func, *func_args;
+    PyObject **stack;
+    Py_ssize_t nargs;
+
+    if (!PyArg_ParseTuple(args, "OO", &func, &func_args)) {
+        return NULL;
+    }
+
+    if (fastcall_args(func_args, &stack, &nargs) < 0) {
+        return NULL;
+    }
+    return _PyObject_FastCall(func, stack, nargs);
+}
+
+
+static PyObject *
+test_pyobject_fastcalldict(PyObject *self, PyObject *args)
+{
+    PyObject *func, *func_args, *kwargs;
+    PyObject **stack;
+    Py_ssize_t nargs;
+
+    if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwargs)) {
+        return NULL;
+    }
+
+    if (fastcall_args(func_args, &stack, &nargs) < 0) {
+        return NULL;
+    }
+
+    if (kwargs == Py_None) {
+        kwargs = NULL;
+    }
+    else if (!PyDict_Check(kwargs)) {
+        PyErr_SetString(PyExc_TypeError, "kwnames must be None or a dict");
+        return NULL;
+    }
+
+    return _PyObject_FastCallDict(func, stack, nargs, kwargs);
+}
+
+
+static PyObject *
+test_pyobject_fastcallkeywords(PyObject *self, PyObject *args)
+{
+    PyObject *func, *func_args, *kwnames = NULL;
+    PyObject **stack;
+    Py_ssize_t nargs, nkw;
+
+    if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwnames)) {
+        return NULL;
+    }
+
+    if (fastcall_args(func_args, &stack, &nargs) < 0) {
+        return NULL;
+    }
+
+    if (kwnames == Py_None) {
+        kwnames = NULL;
+    }
+    else if (PyTuple_Check(kwnames)) {
+        nkw = PyTuple_GET_SIZE(kwnames);
+        if (nargs < nkw) {
+            PyErr_SetString(PyExc_ValueError, "kwnames longer than args");
+            return NULL;
+        }
+        nargs -= nkw;
+    }
+    else {
+        PyErr_SetString(PyExc_TypeError, "kwnames must be None or a tuple");
+        return NULL;
+    }
+    return _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
+}
+
+
 static PyMethodDef TestMethods[] = {
     {"raise_exception",         raise_exception,                 METH_VARARGS},
     {"raise_memoryerror",   (PyCFunction)raise_memoryerror,  METH_NOARGS},
@@ -4256,6 +4354,9 @@ static PyMethodDef TestMethods[] = {
     {"tracemalloc_get_traceback", tracemalloc_get_traceback, METH_VARARGS},
     {"dict_get_version", dict_get_version, METH_VARARGS},
     {"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS},
+    {"pyobject_fastcall", test_pyobject_fastcall, METH_VARARGS},
+    {"pyobject_fastcalldict", test_pyobject_fastcalldict, METH_VARARGS},
+    {"pyobject_fastcallkeywords", test_pyobject_fastcallkeywords, METH_VARARGS},
     {NULL, NULL} /* sentinel */
 };