Qbs and code coverage reports
You know that I'm not an early adopter. That's why it was only a couple of weeks ago when I decided to give Qbs a try, by using the good old Mappero (and its spin-off, Mappero Geotagger) as a test bench. Yes, I know that the Qt company is not going to maintain Qbs anymore in the future, but the little I knew about Qbs was enough to convince me that it's a project worth supporting. So, better late than never -- and hopefully the community (me included) will do a good job in keeping Qbs thriving.
Having Mappero build with Qbs was the simplest thing ever. The only issue I met
was in building the unit tests, because I'm used to set the rpath
on test
executables in order to make it easy to run them uninstalled, and with qmake
I achieved that with this:
QMAKE_RPATHDIR = $${QMAKE_LIBDIR}
In turns out that with Qbs you can do it in almost the same way, but for some reason I couldn't figure it out and I even reported a bug to which I got some nice suggestions, before eventually settling on this:
import qbs 1.0 Test { name: "path-test" files: [ "path-test.cpp", "path-test.h", "paths.qrc", ] Depends { name: "Mappero" } cpp.rpaths: cpp.libraryPaths // <-- this does the trick! }
It's surprisingly similar to how it's done in qmake, so it's not clear even to me why I didn't guess that immediately. Anyway, that was literally my only problem, and you can see the whole set of Qbs files I wrote by having a look at this commit.
Given how easy the migration was, I thought I should also try to add a code coverage report; that's not something I had in my qmake build either, but it's something I really want to have in all my newer projects.
Teaching Qbs to make a code coverage report
Unfortunately, my search for examples on how to have Qbs prepare a coverage report was mostly insuccessful, but thanks to some amazing help from Christian in the #qbs IRC channel, this was not hard to achieve. So, I hope to be of some help myself too, by sharing how this works.
First of all, it must be said that Qbs doesn't know anything about code
coverage, at all. However, it's possible (and often easy) to extend Qbs by
adding your own Product
with its own set of build rules, so here's the
CoverageReport
item for Mappero (though, it should be general enough to be
reusable in your own project):
import qbs Product { name: "coverage" property string outputDirectory: "coverage-html" property stringList extractPatterns: [] builtByDefault: false files: ["**"] type: ["coverage.html"] Depends { productTypes: ["autotest-result"] } Rule { multiplex: true explicitlyDependsOnFromDependencies: ["autotest-result"] outputFileTags: "coverage.html" requiresInputs: false prepare: { var commands = [] var captureCmd = new Command("lcov", [ "--directory", project.sourceDirectory, "--capture", "--output-file", "coverage.info", "--no-checksum", "--compat-libtool", ]); captureCmd.description = "Collecting coverage data"; captureCmd.highlight = "coverage"; captureCmd.silent = false; commands.push(captureCmd); var extractArgs = [] for (var i = 0; i < product.extractPatterns.length; i++) { extractArgs.push("--extract"); extractArgs.push("coverage.info"); extractArgs.push(product.extractPatterns[i]); } if (product.extractPatterns.length > 0) { extractArgs.push("-o"); extractArgs.push("coverage.info"); var extractCmd = new Command("lcov", extractArgs); extractCmd.description = "Extracting coverage data"; extractCmd.highlight = "coverage"; extractCmd.silent = false; commands.push(extractCmd); } var filterCmd = new Command("lcov", [ "--remove", "coverage.info", 'moc_*.cpp', "--remove", "coverage.info", 'qrc_*.cpp', "--remove", "coverage.info", '*/tests/*', "-o", "coverage.info", ]); filterCmd.description = "Filtering coverage data"; filterCmd.highlight = "coverage"; filterCmd.silent = false; commands.push(filterCmd); var genhtmlCmd = new Command("genhtml", [ "--prefix", project.sourceDirectory, "--output-directory", product.outputDirectory, "--title", "Code coverage", "--legend", "--show-details", "coverage.info", ]); genhtmlCmd.description = "Generate HTML coverage report"; genhtmlCmd.highlight = "coverage"; genhtmlCmd.silent = false; commands.push(genhtmlCmd); return commands; } } }
The most important thing here are the references to the autotest-result
tag:
this is the tag used by the AutotestRunner
Qbs item, which is responsible for
running the unit tests. Referencing its product's tag in the Depends
item and
in the explicitlyDependsOnFromDependencies
properties ensures that "building"
our product will cause the unit tests to run. Other needed bits are the
requiresInputs: false
property, which means that our rule doesn't have any
required inputs, and the builtByDefault: false
property, which says that our
coverage report should not be generated when just typing qbs
. Instead, to run
the tests and get the code coverage report one will have to request it
explicitly, by typing
qbs -p coverage
The prepare
property of the Rule
is where the commands to generate the code
coverage report are defined. Here we can use the Command
item to invoke
external programs, and we return a list of such items, so that the commands
will be executed in sequence. Note that here I'm using lcov
and expecting to
find the coverage data produced by gcov
, so this is probably not portable
outside of Linux/gcc.
Using the CoverageReport
item is quite easy: you just need to declare it, and
specify which paths contain the coverage data that you are interested in
(otherwise, lcov will collect data from all object files that it find under the
build directory, which might not be what you desire):
CoverageReport { condition: project.enableCoverage extractPatterns: [ '*/src/*.cpp', '*/lib/*.cpp' ] }
There's little more than that to be done. Of course, you need to find a way to
pass the --coverage
option to gcc when building your products, and for this I
created a small buildconfig
module in
qbs/modules/buildconfig/BuildConfig.qbs
which I depend on in all products
which I wish to build with coverage enabled:
import qbs Module { cpp.cxxFlags: project.enableCoverage ? ["--coverage"] : undefined cpp.dynamicLibraries: project.enableCoverage ? ["gcov"] : undefined Depends { name: "cpp" } }
If all this looks scary, you should probably have a look at the diff which shows how I added code coverage reporting to qbs: hopefully you'll find that it's not that complex, after all.
I hope that Qbs users will find this interesting, and possibly improving my setup. Ideally we should try to get something like this part of Qbs itself, but portability outside of Linux / gcc is going to be an issue.
Comments
There's also webmention support.