💾 Archived View for gemini.hitchhiker-linux.org › gemlog › tdd_in_c.gmi captured on 2024-02-05 at 09:34:33. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-09-28)
-=-=-=-=-=-=-
There are a few things I used to think I'd miss going back to C from a more "modern" programming language. Lately I've been writing a lot of C again and finding that not to be the case, either because I just don't actually miss the language feature or because it's nowhere near as hard to simulate or manually implement as I thought it would be. Take sum types (tagged unions) for instance. At it's heart all that's required to add some type safety to a union is to associate it with an enum tag. In C this can be as simple as stuffing an enum and a union into struct fields and providing functions to access them only after checking the tag field.
One thing that I think languages like Rust, Go and Zig have put to the fore is including a nice test framework right at language level. Going back to C, I thought it might be even more beneficial to do test driven development than in "safer" languages, but the tooling doesn't really exist out of the box. So on one of my recent scribbles I set out to see how hard it would be to do some TDD using just what you might have on any POSIX system - namely the compiler and POSIX make.
Aside: There's a bit of an art to writing Makefiles that will work on both BSD and GNU systems. Most people these days (if they're even writing Makefiles by hand at all) write them for one or the other, usually for GNU make. I find this highly annoying. It's not actually very hard to write a portable Makefile.
Basically my method boiled down to this:
How does that actually look? It's surprisingly concise.
include ../config.mk CFLAGS += -I../include LDLIBS += ../libhaggis.a LDLIBS += $(LIBS) tests += store_u16 tests += load_u16 tests += store_u32 # more tests omitted total != echo $(tests) | wc -w | awk '{ print $1 }' .PHONY: test test: $(tests) output @echo -e "\n\t=== \e[0;33mRunning $(total) tests\e[0m ===\n" @idx=1 ; success=0 ; fail=0; for t in $(tests) ; \ do printf "[%02i/$(total)] %-25s" ${idx} ${t} ; \ idx=$(expr ${idx} + 1) ; \ ./${t} ; \ if [ $? -eq 0 ] ; \ then echo -e '\e[0;32mSuccess\e[0m' ; \ success=$(expr ${success} + 1) ; \ else echo -e '\e[0;31mFailure\e[0m' ; \ fail=$(expr ${fail} + 1) ; \ fi ; done || true ; \ if [ ${fail} == 0 ] ; \ then echo -e '\nResults: \e[0;32mOk\e[0m.' "${success} succeeded; ${fail} failed" ; \ else echo -e '\nResults: \e[0;31mFAILED\e[0m.' "${success} succeeded; ${fail} failed\n" ; \ fi output: @ [-d $@ ] 2>/dev/null || install -d $@ .PHONY: clean clean:
The most annoying part is making sure that 'make' doesn't fire up a new shell for each line, which you have to do by terminating the lines with '; \'. Then there's the double sigil '$' which is needed for proper variable expansion by the shell. I also had to figure out a least common denominator set of 'echo' and 'printf' that works on almost every possible combination of BSD, GNU or Solaris tools. There's some other idiosyncrasies as well, like figuring out how to stuff a variable with shell output in a way that works for both BSD and GNU make, but hey it's not bad in the end.
What you get is a nice pretty printed test run showing the number of passes and fails. Adding a new test incolves creating a test executable and adding it to the "tests" variable in the Makefile. Easy peasy, and allows me to do TDD without any extra frameworks or tooling.
All content for this site is licensed as CC BY-SA.
© 2023 by JeanG3nie