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

* _PyObject_FastCall()
* _PyObject_FastCallDict()
* _PyObject_FastCallKeywords()
(cherry picked from commit 3b5cf85edc188345668f987c824a2acb338a7816)

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

index e2b8e0fd1233d9009f05a885f25d33a196b69d7b..2e8819be5dac68cd4c606186f12a81c410396214 100644 (file)
@@ -1,4 +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
@@ -122,5 +128,175 @@ class CFunctionCalls(unittest.TestCase):
         self.assertRaises(TypeError, [].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 c76eefab4e80cfc6a678344db747a527ad512693..5e73293e0e0eaf09c0902de439a3b2e4eb0a705b 100644 (file)
@@ -4027,6 +4027,104 @@ dict_get_version(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},
@@ -4230,6 +4328,9 @@ static PyMethodDef TestMethods[] = {
     {"tracemalloc_untrack", tracemalloc_untrack, METH_VARARGS},
     {"tracemalloc_get_traceback", tracemalloc_get_traceback, METH_VARARGS},
     {"dict_get_version", dict_get_version, 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 */
 };