← Code coverage for black-box tests, in any language

Code coverage of tomato tests: other languages

coveragedotnetrubyphpc

The approach does not depend on the language: turn coverage on for the process tomato starts, make sure it exits cleanly on SIGTERM, and read the data afterwards. These are the usual tools for languages the demo shop does not use.

LanguageTurn it onRead it
.NETdotnet-coverage collect around the command, or Coverlet’s instrumented buildreportgenerator
RubySimpleCov started first thing in the processSimpleCov writes HTML and JSON at exit
PHPpcov or Xdebug, with a collector started per requestphpcov merges the per-request files
C, C++--coverage (gcov) or -fprofile-instr-generate -fcoverage-mapping (clang)gcovr, or llvm-cov as for Rust

.NET

dotnet-coverage wraps the process and writes the report when it exits:

# tomato.yml
app:
  command: dotnet-coverage collect --output coverage/blackbox.cobertura.xml --output-format cobertura -- dotnet bin/Orders.dll

That puts coverage into the command, so keep a second config, or set the command through an environment variable (command: ${APP_COMMAND}). An ASP.NET Core host stops gracefully on SIGTERM by default, within its ShutdownTimeout; keep that under tomato’s five seconds.

Ruby

SimpleCov has to start before your code loads, and writes its results in an at_exit hook:

# at the very top of config/boot.rb, or the server's entry point
if ENV["BLACKBOX_COVERAGE"]
  require "simplecov"
  SimpleCov.command_name "tomato"
  SimpleCov.start { add_filter "/vendor/" }
end

Puma and Unicorn fork workers: start SimpleCov in each worker (on_worker_boot) and use SimpleCov.command_name per worker, so the results merge instead of overwriting each other. Both servers shut down gracefully on SIGTERM.

PHP

PHP is the odd one out. A PHP-FPM worker serves many requests and seldom exits, so coverage has to be collected per request. With pcov loaded, an auto_prepend_file starts collection and a shutdown function writes one file per request; phpcov merge combines them after the run. Make sure the collector only runs when an environment variable says so.

C and C++

Both GCC and Clang can instrument a binary, which writes its data in an atexit handler, so the program must return from main or call exit, not die from the signal:

clang -fprofile-instr-generate -fcoverage-mapping -O1 -o server server.c
LLVM_PROFILE_FILE=$PWD/coverage/server-%p.profraw tomato run
llvm-profdata merge -sparse coverage/*.profraw -o coverage/server.profdata
llvm-cov report ./server -instr-profile=coverage/server.profdata

A SIGTERM handler that sets a flag your main loop checks is enough.