Hacker News new | past | comments | ask | show | jobs | submit login
Running C unit tests with Pytest (p403n1x87.github.io)
132 points by p403n1x87 on Feb 12, 2022 | hide | past | favorite | 41 comments



This is brilliant.

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.


How do you add a side effect and still call the original in pytest? This is easy in rspec but everywhere I google people say there's no good way.

For example, you're calling code which internally does Too().bar() and you want to advance your time mock after it's called.


That… has nothing to do with pytest?

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.


Replace the original function with a wrapper that calls the function then does the side effect.

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...


There are lots of good mocking fixtures available for pytest. I've used this one for clock stuff in the past: https://github.com/adamchainz/time-machine#pytest-plugin


A notable downside that is not mentioned: the Python interpreter is far from Valgrind-clean. Valgrind is generally a powerful tool for debugging memory errors, but if you wrap your C code in Python, Valgrind will be so noisy as to be ineffective. Python startup is also very heavyweight; combined with the ~60x slowdown from Valgrind this is something you are going to notice.

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.


That's a good argument for keeping these kind of tests separate. Indeed Austin has dedicated Valgrind integration tests just for this reason.


I’ve had success running valgrind on python code with a very small suppression list to cover all of the python interpreter issues (at least for safety, not leaks) and let me focus on the code.


Is such a suppression list publicly available somewhere?


https://github.com/python-pillow/docker-images/blob/main/ubu...

This docker image is what we’re using in CI to do memory safety checks against the pillow c extension.


That is indeed very short! Thanks!


We've had a lot of success combining that approach (cffi instead of ctypes) with property-based testing (https://github.com/HypothesisWorks/hypothesis) for the query engine at backtrace: https://engineering.backtrace.io/2020-03-11-how-hard-is-it-t... .


Have you exposed the C library to Python via ctypes as OP or have you taken a different approach?


Just cffi, and keeping the headers free of inline implementation noise (a side benefit, in my opinion).


    @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.


You can use tempfiles and (on OSX) use the private API of ctypes to dlclose to close the handle. Different call on Windows I think. Something like

  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.


Did you mean to have the `tmp.name` as the library passed into `CDDL`? If not, what’s the purpose of the copy?


Yes! Thank you. Fixed it.


If you're using Linux, I would recommend using TemporaryFile instead of NamedTemporaryFile. It takes advantage of the O_TMPFILE flag, which guarantees that the kernel will clean up the file for you when the process exits:

  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.


Does this even work? The documentation states that one should not rely on TemporaryFile having a name. Otherwise why would NamedTemporaryFile exist?

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.


That's a good observation. Towards the end the fixture is dropped in favour of a module-like object, which spaws the compiler once per test run. One could enhance this to skip compilation in the sandbox process to avoid unnecessary compilations.


Shameless self-plug since I recently made a small header-only C testing "framework":

https://github.com/rubenvannieuwpoort/c_unit_tests

Feels a bit more in line with the spirit of C (small language, few dependencies).


This is really nice. I wonder if there is a way to make this work without constructors.


Can possibly be done with a custom section, though not sure that is better and might require fancy compiler flags.


Indeed, I just made a POC with compiler sections: https://github.com/cozzyd/examc

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).


Nice. Now how could I get this to work on a small embedded device compiker (gbdk using lcc), which likely doesn't have that constructor attitude.


One approach is to build test traversal machinery out of function static variables and have every TEST() macro turn into a function that takes a pointer to some state controlling which test to run next.

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.


Interesting. But how would one test "know" about the next one, especially if they are in different compilation units.


Discovery is at runtime. Syntax goes something like MODULE(foo) { DEPENDS(bar); TEST("I like string names") { CHECK(42 == life()); } } where DEPENDS either sends control flow off to bar or onwards towards the test case depending on the value of an argument to the function MODULE created.


...after some thinking, maybe it would be possible to do a manual preprocess using a simple python script that collects all the tests and inserts them in the test main.


I doubt that it's pytest-specific, but the idea of using a unit test tool to explore a new problem space and build up to working product is powerful.

Maybe not so much in the UX space, but for ETL and back-end work, I'm a fan.


this seems cool, but... brittle? especially embedding build steps in python.

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.


Really cool. Several jobs ago, I used cffi to create bindings to a library that controlled a camera's pan/tilt/zoom motors. Those were used to implement a test suite that validates the cameras in the manufacturing facility before shipping. The embedded developers also found being able to use the cffi bindings in the REPL really useful when prototyping changes. Python is a really useful tool for these kinds of interfaces.


When I need to test C I just use D unittest blocks.

You can import the C header file directly now so it's literally trivial.


I'm just amazed the lengths people go to complicate debugging (two runtimes), building (two things) and deploying (not single executable). Okay, maybe it's worth it for something (e.g. real "C" code used in Python, but just for the sake of testing... hmmm... no)


Depending on the size of your program, investing in test is something that shouldn’t be underrated. For big projects never call it “just” testing.

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.


This similar project could also be interesting https://github.com/mrh1997/headlock

It is used at my company to test our firmware written in C with python and was developed by a colleague.


I did something similar with java and jython many years ago.

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.


How would you go to do this for c++? Specifically for classes and templates, is there an easy way to call code without having to manually write the demangled names of the functions?


Not that I know of. If I were doing it I'd expose a library / interface with pybind11 to test, but really I find Googles unit testing framework or boosts to both be pretty effective.


You should search/replace leverage -> use.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: