This is the second post of a series about GNATcoverage and source code instrumentation. The previous post introduced how GNATcoverage worked originally and why we extended it to support source instrumentation-based code coverage computation. Let’s now see it in action in the most simple case: a basic program running on the host machine, i.e. the Linux/Windows machine that runs GNATcoverage itself.
Source traces handling
Here is a bit of context to fully understand the next section. In the original GNATcoverage scheme, coverage is inferred from low level execution trace files (“*.trace”) produced by the execution environment. These traces essentially contain a summary of program machine instructions that were executed. We call these “binary traces”, as the information they refer to is binary (machine) code.
With the new scheme, based on the instrumentation of source code, it is instead the goal of each instrumented program to create trace files. This time, the information in traces refers directly to source constructs (declarations, statements, IF conditions, …), so we call them “source traces” (“*.srctrace” files).
The data stored in these files is conceptually simple: some metadata to identify the sources to cover and a sequence of booleans that indicate whether each coverage obligation is satisfied. However, for efficiency reasons, instrumented programs must encode this information in source traces files using a compact format, which is not trivial to produce. To assist instrumented programs in this task, GNATcoverage provides a “runtime for instrumented programs” as a library project: gnatcov_rts_full.gpr, for native programs which have access to a full runtime (we will cover embedded targets in a future post).
First, the GNATcoverage instrumenter needs a project file that properly describes the closure of source files to instrument as well as the program main unit. This is similar to what a compiler needs: access to all the dependencies of a source file in order to compile it.
Next, this blog series assumes the use of a recent GPRbuild (release 20 or beyond), for the support of two switches specifically introduced to facilitate building instrumented sources without modifying project files. What the new options do is conceptually simple so it would be possible to build without this, just less convenient.
Then the source constructs added by the instrumentation expect an Ada 95 compiler. The instrumenter makes several compiler-specific assumptions (for instance when handling Pure/Preelaborate units), so for now we recommend using a recent GNAT compiler.
Finally, users need to build and install the “runtime for instrumented programs” described in the previous section. To make sure the library code can be linked with the program to analyze, the library first needs to be built with the same toolchain, then installed:
# Create a working copy of the runtime project. # This assumes that GNATcoverage was installed # in the /install/path/ directory. $ rm -rf /tmp/gnatcov_rts $ cp -r /install/path/share/gnatcoverage/gnatcov_rts /tmp/gnatcov_rts $ cd /tmp/gnatcov_rts # Build the gnatcov_rts_full.gpr project and install it in # gprinstall’s default prefix (most likely where the toolchain is installed). $ gprbuild -Pgnatcov_rts_full $ gprinstall -Pgnatcov_rts_full
Note that depending on your specific setup, the above may not work without special filesystem permissions, for instance if the toolchain/GPRbuild was installed by a superuser. In that case, you can install the runtime to a dedicated directory and update your environment so that GPRbuild can find it: add the --prefix=/dedicated/directory argument to the gprinstall command, and add that directory to the GPR_PROJECT_PATH environment variable.
A first example
Now that prerequisites are set up, we can now go ahead with our first example. Let’s create a very simple program:
-- example.adb procedure Example is begin […] end Example; -- example.gpr project Example is for Main use (“example.adb”); for Object_Dir use “obj”; end Example;
Before running gnatcov, let’s make sure that this project builds fine:
$ gprbuild -Pexample -p $ obj/example […]
Great. So now, let’s instrument this program to compute its code coverage:
$ gnatcov instrument -Pexample --level=stmt --dump-trigger=atexit
As its name suggests, the “gnatcov instrument” command instruments the source code of the given project. The -Pexample and --level=stmt options should be familiar to current GNATcoverage users: the former requests the use of the “example.gpr” project, to compute the code coverage of all of its units, and --level=stmt tells gnatcov to analyze statement coverage.
The --dump-trigger=atexit option is interesting. As discussed earlier, instrumented programs need to dump their coverage state into a file (the trace file), that “gnatcov coverage” reads in order to produce a coverage report. But when should that dump happen? Since one generally wants reports to show all discharged obligations (fancy words meaning: executed statements, decision outcomes exercized, …), the goal is to create the trace file after all code has executed, right before the program exits. However some programs are designed to never stop, running an endless loop (Ravenscar profile), so this trace file creation moment needs to be configurable. --dump-trigger=atexit tells the instrumenter to use the libc’s atexit routine to trigger file creation when the process is about to exit. It’s suitable for most programs running on native platforms, and makes trace file creation automatic, which is very convenient.
Now is the time to build the instrumented program:
$ gprbuild -Pexample -p --src-subdirs=gnatcov-instr --implicit-with=gnatcov_rts_full
Even seasoned GPRbuild users will wonder about the two last options.
--src-subdirs=gnatcov-instr asks GPRbuild to consider, in addition to the regular source directories, all “gnatcov-instr” folders in object directories. Here that means that GPRbuild will first look for sources in “obj/gnatcov-instr” (as “obj” is example.gpr’s object directory), then for sources in “.” (example.gpr’s regular source directory).
But what is “obj/gnatcov-instr” anyway? When it instruments a project, gnatcov must not modify the original sources, so instead it stores instrumented sources in a new directory. The general rule of thumb for programs that deal with project files is to use projects’ object directory (Object_Dir attribute) to store artifacts; “gnatcov instrument” thus creates a “gnatcov-instr” subdirectory there and puts instrumented sources in it. Afterwards, passing --src-subdirs to GPRbuild is the way to tell it to build instrumented sources instead of the original ones.
The job of --implicit-with=gnatcov_rts_full is simple: make GPRbuild consider that all projects use the gnatcov_rts_full.gpr project, even though they don’t contain a “with “gnatcov_rts_full”;” clause. This allows instrumented sources (in obj/gnatcov-instr) to use features in the gnatcov_rts_full project even though “example.gpr” does not request it.
In other words, both --src-subdirs and --implicit-with options allow GPRbuild to build instrumented sources with their extra requirements without having to modify the project file of the project to test/cover (example.gpr).
We are getting closer to the coverage report. All we have to do it to finally run the instrumented program, to create a source trace file:
$ obj/example Fact (1) = 1 $ ls *.srctrace example.srctrace
So far, so good. By default, the instrumented program creates in the current directory a trace file called “XXX.srctrace” where XXX is the basename of the executed binary, but one can choose a different filename by setting the GNATCOV_TRACE_FILE environment variable to the name of the trace file to create. Now that we have a trace file, the rest will be familiar to GNATcoverage users:
$ gnatcov coverage -Pexample --level=stmt --annotate=xcov example.srctrace $ cat obj/example.adb.xcov example.adb: 75% of 4 lines covered Coverage level: stmt 1 .: with Ada.Text_IO; use Ada.Text_IO; 2 .: 3 .: procedure Example is 4 .: function Fact (N : Natural) return Natural is 5 .: begin 6 +: if N <= 1 then 7 +: return 1; 8 .: else 9 -: return N * Fact (N - 1); 10 .: end if; 11 .: end Fact; 12 .: begin 13 +: Put_Line ("Fact (1) =" & Fact (1)'Image); 14 .: end Example;
And voilà! That’s all for today. The next post will demonstrate how to handle programs running on embedded targets.