Running C unit tests with Pytest
p403n1x87.github.io
p403n1x87.github.io
pytest is by far the most productive testing framework I've ever tried, for any language. It's so good that it switched me from treating tests as a necessary chore to actively enjoying writing them.
Using ctypes to exercise a C module like this is brilliant - especially the mechanisms used here to work around segmentation faults.
Reminds me of SQLite, which is written in C but uses TCL for most of its test suite.
For example, you're calling code which internally does Too().bar() and you want to advance your time mock after it's called.
You’d probably use something like `wraps` or `side_effect` in `unittest.mock` to delegate to the original with some extra behaviours.
And / or use freezegun’s tick features, when you’re specifically dealing with time.
This could effectively be something like a decorator.
As far as mocking time objects, look into pytest.freezegun. You should be able to control the date and time as you move forward.
I was able to mock out the datetime.now() at some point in the past. It wasn't with freezegun...
The Python interpreter does not even use malloc()/free() directly by default; it layers its own memory allocator on top called PyMalloc (https://docs.python.org/3/c-api/memory.html#pymalloc). You can disable this by setting an environment variable (PYTHONMALLOC=malloc), but even then you will see many Valgrind warnings inside the Python interpreter itself.
This docker image is what we’re using in CI to do memory safety checks against the pillow c extension.
@pytest.fixture
def libfact():
yield CDLL("./fact.so")
FWIW that's pretty confusing as it gives the impression the library is reloaded for every test, but iirc dlopen() will just return a handle to the existing one. I don't think ctypes has a good way to unload dlls so it should probably be `fixture(scope='session')`.That gets more relevant when on-the-fly compilation is added to the mix, spawning a compiler for every test is a complete waste of time.
import _ctypes
import shutil
import tempfile
@pytest.fixture
def libfact():
tmp = tempfile.NamedTemporaryFile(delete=True)
shutil.copy2("./fact.so", tmp.name)
lib = CDLL(tmp.name)
yield lib
_ctypes.dlclose(lib.handle)
EDIT: fixed error mentioned in reply. import shutil
import tempfile
@pytest.fixture
def libfact():
tmp = tempfile.TemporaryFile()
tmp_name = f"/dev/fd/{tmp.name}"
shutil.copy2("./fact.so", tmp_name)
lib = CDLL(tmp_name)
yield lib
It's better than relying on your application to clean up the file for you. With application-level cleanup hooks, you're still vulnerable to a resource leak if the process gets a SIGKILL or crashes or otherwise ends before your hooks run.For the rare occurrence of sigkill before cleanup, I think your /tmp has ample of space to keep a few more kb until the next reboot. Servers running tests probably restart more often than desktops nowadays, when wrapped in containers.
https://github.com/rubenvannieuwpoort/c_unit_tests
Feels a bit more in line with the spirit of C (small language, few dependencies).
You don't need malloc if using local variables either, which is helpful when your target doesn't have malloc or when debugging memory problems on targets that do. It's nice to know the test framework cannot be leaking or use-after-free anything.
This implementation only works with gcc though probably (it uses the automatic __start_SECTION and __stop_SECTION that gcc generates but clang doesn't seem to... there are likely hacks to make this work anyway though).
In principle, this approach allows interspersing your tests throughout a shared library instead of all in one file (though in that case you wouldn't want the testing function to be called main and you would need a separate driver program for test).
Maybe not so much in the UX space, but for ETL and back-end work, I'm a fan.
i've used check in the past and was happy enough with it, it didn't take long to learn. all unit test frameworks are basically the same at their core.
interop between c and python with ctypes is fun though.
You can import the C header file directly now so it's literally trivial.
This example can be useful to generate rich test-data. Imagine you have some C program parsing json. Generating a sample json input in C would be very long and error prone, whereas in python it’s just a few loops for the generation and finally json.dumps.
This also decouples your test from your program, so you don’t do the same mistake in both of them, negating the test.
In same manner, parameterizing tests is surely possibly with GTest but it’s awkward and complicated, a lot easier in py.
Pytest also comes with lots of fixtures and possibility to make your own, such as spinning up a mock-db, although then we are more into integration testing rather than unit.
Not saying everyone should go rewrite their tests like this, but for some cases there is value that can be utilized.
It is used at my company to test our firmware written in C with python and was developed by a colleague.
It was rather nice to write java unit tests with python, because they didn't need to be compiled, they just ran in the interpreter.