dconf changes landing. watch for falling rocks.
I’ve been putting a lot of work into dconf lately. I just reached a landmark today by landing that work on master. There have been over 100 commits touching 108 files with 8109 insertions and 3023 removals. That may seem like a rather large increase in code size, but nearly half of the additions come in the form of tests (which there were almost none of before) and the rest is inflated by a relatively liberal commenting of the newly-introduced code.
The story behind that is that the first pass at writing dconf aimed at efficiency and speed at the cost of… almost everything else. There has been a lot of renewed interested in adding new features to dconf lately:
- an API for writing to system-level databases
- an API for managing lockdown
- an API for managing lists of items (aka GSettingsList)
- removing some cruft from the way GSettings dealt with “delayed” writes
- proper NFS support
- some stuff I forget
The code was in too brittle of a state to get away with adding any of these features in a clean way (and not for lack of trying by various people on both the GSettingsList and NFS fronts).
One of the reasons that I’m blogging is because with such a massive refactoring of code (almost a rewrite, I’d say) there are bound to be some issues. The new code will be going out with GNOME 3.5.4 next week, so keep your eyes peeled and let me know if you notice anything.
some thoughts on (unit) testing
The other reason I’m blogging is to talk about the major focus of the rewrite: testing and testability.
dconf was the nightmare situation for testability. It’s a relatively complicated system that requires complex interactions between client programs and the service via the filesystem, shared memory and D-Bus. This is not your typical dbus-test-runner situation. To make matters worse, it’s a component that tries to read and write files in /etc and the user’s home directory. People tend to get upset when they end up having their personal data written to by a testcase…
I decided to focus on the client library for testing. The service is a relatively simple component and it operates in relative isolation, safely on the other side of D-Bus, receiving simple requests. The client library is used in a far more interesting set of situations.
The first step was to identify the interactions that dconf on the client side has with the world around it:
- environment variables like DCONF_PROFILE and XDG_CONFIG_HOME
- reading profile files from /etc/dconf/profile/ via fopen()
- reading database files using gvdb
- communicating with the service via the shared memory region (shm)
- communicating with the service via D-Bus
The first item is easy to deal with: testcases can easily set environment variables. The second item is solved in a bit of a dirty way using the classic trick of ELF symbol interposition to replace the implementation of fopen().
The remaining three items are solved by turning each of them into a separate module, compiled as an in-tree static library that is used as an intermediate step to making the final library. In addition to the standard niceness you feel about “good software engineering” from using modules, each static library can now be used in separate unit tests — and dconf now has a unit test for each of gvdb, shm and its various D-Bus backends.
The most powerful part of doing this is the ability to compile the core dconf code (called “engine”) as a static library too, excluding these modules that it depends on. The dconf testsuite now features “mock” versions of each of the underlying components components that can be linked with the dconf engine library for unit-testing of the engine code. We don’t have to interact with the real filesystem or talk on the real D-Bus — it can all be mocked.
Of course, there is a lot to be said for proper integration testing. Again, this is a hard problem, but I have some creative ideas about how I could address those situations…
test coverage reporting is easy
One tool that I’ve found to be absolutely invaluable during this process is lcov. Coverage reports make testing seem a whole lot more purposeful (it’s fun to try to get to 100%!) and they point out areas that you may have missed testing. Sometimes you failed to test particular cases because they’re actually impossible (in which case they help you remove dead code).
Getting coverage reporting setup was a lot easier than I imagined it would be. There are basically only two requirements to doing that. First you need to build the project with the proper CFLAGS and LDFLAGS. These couple of lines in configure.ac will do (linked to instead of inlined due to WordPress’ inability to do most things properly):
http://git.gnome.org/browse/dconf/tree/configure.ac#n59
The next step is to have some way of actually generating the report. Here’s a fragment from my toplevel Makefile.am that I stole from glib’s gtester Makefile and heavily modified:
http://git.gnome.org/browse/dconf/tree/Makefile.am#n14
One more thing to mention: lcov often generates rather silly coverage results: marking lines like ‘g_assert_not_reached();’ as untested, or marking lines like ‘g_assert();’ as only having tested one of two possible branches. There is a small script that I wrote (now living in dconf’s git) that attempts to deal with some of those situations:
http://git.gnome.org/browse/dconf/tree/trim-lcov.py