4

So, I have a repo to build a python C extension laid out as follows:

setup.py
demo.c
MANIFEST.in

The contents of the C file are:

#include <Python.h>

static PyObject* print_message(PyObject* self, PyObject* args)
{
    const char* str_arg;
    if(!PyArg_ParseTuple(args, "s", &str_arg)) {
        puts("Could not parse the python arg!");
        return NULL;
    }
#ifdef USE_PRINTER
    printf("printer %s\n", str_arg);
#else
    printf("msg %s\n", str_arg);
#endif
    // This can also be done with Py_RETURN_NONE
    Py_INCREF(Py_None);
    return Py_None;
}

static PyMethodDef myMethods[] = {
    { "print_message", print_message, METH_VARARGS, "Prints a called string" },
    { NULL, NULL, 0, NULL }
};

// Our Module Definition struct
static struct PyModuleDef myModule = {
    PyModuleDef_HEAD_INIT,
    "DemoPackage",
    "A demo module for python c extensions",
    -1,
    myMethods
};

// Initializes our module using our above struct
PyMODINIT_FUNC PyInit_DemoPackage(void)
{
    return PyModule_Create(&myModule);
}

In my setup.py, I have the following code:

from distutils.core import setup, Extension

module1 = Extension('DemoPackage',
                    define_macros = [('USE_PRINTER', '1')],
                    include_dirs = ['include'],
                    sources = ['src/demo.c'])

setup (name = 'DemoPackage',
       version = '1.0',
       description = 'This is a demo package',
       author = '<first> <last>',
       author_email = '[email protected]',
       url = 'https://docs.python.org/extending/building',
       long_description = open('README.md').read(),
       ext_modules = [module1])

My question here is, if I can build and install the package with the commands:

$ python setup.py build $ python setup.py install

How do I incorporate or write a unit test in the scenario of a C extension? I am looking for the unit test to be run in conjunction with setup.py, similarly to how a test would work for cmake.

1 Answer 1

3

How I've been approaching the problem of unit-testing Python extension module code written in C is to write unit tests in Python for pytest but to then embed the Python interpreter in a separate C test runner which uses Check to wrap the call to pytest. This might seem kinda silly (it does to me honestly), but since C code can leak memory and cause all sorts of nasty signals to be raised, using a Check runner runs everything in a separate address space so the test runner can catch any signals and (hopefully) say something meaningful about them instead of me just seeing the Python interpreter crash. I would say the way to go about unit testing C extension code is to build the extension inplace, i.e. using python3 setup.py build_ext --inplace which will put the shared object (I'm on Linux) in the same directory as your demo.c file (or source folder), and then you can use whatever you would like for unit testing. I personally use make and the build target for my test runner depends on the C extension being successfully built inplace.

It's unfortunate that no one has yet answered this question; I was actually hoping someone would have a better method of tackling this exact problem. If you're interested in doing something similar, here's sample code for a Check test runner and relevant Makefile recipe to build the runner.

/* check/pytest_suite.h */

#ifndef PYTEST_SUITE_H
#define PYTEST_SUITE_H

#include <check.h>

Suite *pytest_suite();

#endif /* PYTEST_SUITE_H */
/* check/pytest_suite.c */

#define PY_SSIZE_T_CLEAN
#include "Python.h"

#include <stdio.h>
#include <check.h>
#include "pytest_suite.h"

START_TEST(print_stuff)
{
  // initialize interpreter, load pytest main and run (relies on pytest.ini)
  Py_Initialize();
  PyRun_SimpleString("from pytest import main\nmain()\n");
  // required finalization function; if something went wrong, exit immediately
  if (Py_FinalizeEx() < 0) {
    // __func__ only defined in C99+
    fprintf(stderr, "error: %s: Py_FinalizeEx error\n", __func__);
    exit(120);
  }
}
END_TEST

Suite *pytest_suite() {
  // create suite called pytest_suite + add test case named core
  Suite *suite = suite_create("pytest_suite");
  TCase *tc_core = tcase_create("core");
  // register case together with test func, add to suite, and return suite
  tcase_add_test(tc_core, print_stuff);
  suite_add_tcase(suite, tc_core);
  return suite;
}
/* check/runner.c */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <check.h>

#include "pytest_suite.h"

int main(int argc, char **argv) {
  // instantiate our test suite. note this does not have to be freed!
  Suite *suite = pytest_suite();
  // create our suite runner and run all tests (CK_ENV -> set CK_VERBOSITY and
  // if not set, default to CK_NORMAL, i.e. only show failed)
  SRunner *runner = srunner_create(suite);
  srunner_run_all(runner, CK_ENV);
  // get number of failed tests and free runner
  int n_failed = srunner_ntests_failed(runner);
  srunner_free(runner);
  // succeed/fail depending on value of number of failed cases
  return (n_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
# Makefile (GNU make)

CC             = gcc
PYTHON        ?= python3
# dependencies for test running code
CHECK_DEPS     = $(wildcard check/*.c)
# use python3-config to get python compiler and linker flags for use when
# linking python into external C code (our test runner)
PY_CFLAGS     ?= -fPIE $(shell python3-config --cflags)
# --embed is required on ubuntu or -lpythonx.y is omitted by --ldflags
PY_LDFLAGS    ?= $(shell python3-config --embed --ldflags)
# linker flags specifically for compiling the test runner (libcheck)
CHECK_LDFLAGS  = $(PY_LDFLAGS) -lcheck

# build C extension module inplace. change dependencies as needed.
inplace: demo.c
    @$(PYTHON) setup.py build_ext --inplace

# build test runner and run unit tests using check
check: $(CHECK_DEPS) inplace
    @$(CC) $(PY_CFLAGS) -o runner $(CHECK_DEPS) $(CHECK_LDFLAGS)
    @./runner

Of course, for this to work, you'll need to have Check and GNU Make installed on your system. I'm using WSL Ubuntu 18.04; make came with the distribution and I built Check from scratch using their latest release. Using this setup, when I execute make check, make inplace gets called whenever demo.c is updated or touched, and my test runner is built and run.

Hope this at least gives you some new ideas.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.