Generating a coverage report

Renode offers many execution tracing features, as described in the Execution tracing section, and allows you to dump the entire execution of a binary into a single file. With an execution trace of a properly built binary and a dedicated script, you can create a code coverage report.

The script combines data in the DWARF format (debug information available in ELF files which are typically used to load software to Renode) with the number of executions of each instruction counted by Renode. The following tutorial will walk you through the steps necessary to generate such a report in Renode.

Prerequisites

To generate a report, you need to have Renode installed along with the execution tracer script. On Linux you can simply download a portable package by following these instructions.

It’s assumed that you execute the rest of commands in the root directory of the Renode repository. Otherwise, you might need to adjust them accordingly.

To run the script you can install it as a CLI utility, for example using pipx:

pipx install tools/execution_tracer/

You will then be able to use call:

renode-retracer --help

to see the list of available commands

Alternatively, it’s possible to invoke the script directly from the Renode directory. Let’s assume that the variable RENODE_PATH points to your Renode installation, or cloned repository. First, initialize the virtual environment and install the dependencies:

python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r ${RENODE_PATH}/tools/execution_tracer/requirements.txt

Then, you should be able to run:

python3 ${RENODE_PATH}/tools/execution_tracer/execution_tracer_reader.py --help

to see the list of available commands.

Building a binary for coverage gathering

Renode can trace the execution of a binary built in any way, but to extract coverage information we require a binary with debug information in the DWARF format. To provide the best result, it’s recommended to use the -g and -O0 (or -Og) flags (for GCC) during compilation. Please note that different compilers may require different options.

The -g switch adds debug information to the binary, specifically code line numbers for each machine instruction. The coverage functionality requires that information, and you will see an error if it’s missing from the ELF file. It’s recommended to use the highest available debug level (at this time -g3).

Using the -O0 switch to disable all optimizations is strongly recommended. The -Og switch disables most optimizations and is intended to make the debugging process easier.

Any optimization done by a compiler may prevent redundant lines of code from being executed. The compiler might also replicate the same code line across several addresses in the program or reorder the execution flow, which will make obtaining legitimate results impossible. For the purpose of generating a coverage report, same as for debugging, it’s recommended to execute the code without any optimizations. Generating a report for an optimized binary may create imprecise or even unexpected results.

Gathering coverage for a sample program

For the purpose for this tutorial, we prepared a sample program consisting of two files. The tutorial describes how to gather the execution metrics for this program, running on the emulated Kendryte K210 RISC-V platform.

Note

The pre-built sample and sources are available for download here.

The program’s sources look as follows:

const int buf_size = 100;

void funB(int *buf, int b) {
    for (int i = 0; i < buf_size; i++) {
        buf[i] -= b;
    }
}

void funA(int *buf, int b);
void funC();

void buf_increment_all(int *buf) {
    for (int i = 0; i < buf_size; i++) {
        buf[i] += 1;
    }
}

int main() {
    int buf[buf_size];
    for (int i = 0; i < buf_size; i++) {
        buf[i] = i;
    }

    buf_increment_all(buf);

    for (int i = 0; i < buf_size; i += 1) {
        if (buf[i] % 3 == 0) {
            funA(buf, i);
        }
        if (buf[i] % 5 == 0) {
            funB(buf, i);
        }
    }

    int i = 11;

    if (i < 10) {
        funC();
    }

    buf_increment_all(buf);

    while (1) ; // Don't let the program run-off!

    return 0;
}
extern const int buf_size;

int funC() {
        return 42;
}

void funA(int *buf, int b) {
        for (int i = 0; i < buf_size; i++) {
                buf[i] += b;
        }
}

To compile it, first you need to obtain a riscv64 toolchain. Refer to the package repositories of your OS distribution, or compile it yourself from the sources.

Note

Hint: You can use prebuilt toolchains, shipped with the Zephyr SDK.

Then, compile the program with (adjust the compiler name, as needed):

riscv64-unknown-elf-gcc -O0 -g3 main.c additional.c --freestanding -nostdlib -Wl,-emain -o coverage-sample.elf

The additional flags inform the compiler that we operate in a bare-metal environment, so we don’t want to link with the standard libraries. They have no impact on the coverage report.

The platform and tracing the execution

To trace execution of the binary from the previous section, you can simply use the pre-prepared RESC script.

Note

The script runs a binary pre-built for the Kendryte K210 platform, but the tracing mechanism itself (CreateExecutionTracing) is generic and works for all platforms.

The script is functionally similar to this one:

$bin=$CWD/coverage-sample.elf
include @scripts/single-node/kendryte_k210.resc
cpu2 IsHalted true
cpu1 SP 0x1000

cpu1 CreateExecutionTracing "trace" $CWD/trace.bin.gz PC isBinary=True compress=True

We focus on a single core and disable the other one. We also set the Stack Pointer manually, to simplify the software.

You can load the script in Renode, by typing:

include @scripts/complex/coverage/kendryte_k210_coverage.resc

emulation RunFor "0.003" # Run 300 000 instructions (default performance is 100MIPS)
quit

The command above gathers the trace for 0.003 seconds of the guest’s time. After the execution finishes, close the Renode instance with quit. The trace will be saved in a binary format to a file named trace.bin.gz. The file is additionally compressed to reduce size.

The CreateExecutionTracing command initializes execution tracing. The Execution tracing section contains details on configuring the command. The script that generates the coverage report requires at least PCs (addresses of executed instructions) to be available in the trace file.

Note

Tracing an additional type of data may cause an increase in the size of the output file. It’s also recommended to use a binary format and compression by setting the fourth and fifth arguments to True, as shown above.

Generating the report

At this point, you can run the script that generates the report:

renode-retracer coverage trace.bin.gz \
  --binary coverage-sample.elf \
  --sources main.c additional.c \
  --output coverage.zip \
  --export-for-coverview

Note

If no sources are provided using the --sources argument, the script will attempt to automatically discover their locations based on the DWARF data extracted from the binary.

You might need to perform path substitution if the sources’ locations have changed from the time when the binary was built (or binaries were built on a different machine).

To do this, you can use the --sub-source-path argument by providing old_path:new_path for each pair of paths to be substituted; this argument can be provided multiple times.

By adding --export-for-coverview to the command line, the tool can pack an archive, ready to be processed by Coverview - a tool for generating coverage dashboards. For the main.c file, the output will look similar to the one shown below.

Coverage in Coverview

It’s also possible to use a format (.info) compatible with LCOV. While not as easy to read for a human, it’s more easily processed by automated scripts and tools.

Report formats

Normally, the script will output the data in a format (.info) compatible with LCOV. The format is described in the manual pages for geninfo (man geninfo). renode-retracer supports generation of line coverage info (DA). This format can be later used with various third-party tools (e.g. genhtml) to display the coverage data.

By using the --export-for-coverview switch, the data will be packed into an archive, ready to be processed by Coverview.

renode-retracer also supports a legacy text-based report format for easy result interpretation, as shown in the last section.

Coverview integration

The execution tracer can export the data in a format which can be directly loaded into Coverview. Use the --export-for-coverview switch for this and remember to provide an output file name with the --output filename.zip option.

Coverview is a tool for generating coverage dashboards. The tool runs fully on the client’s side, and you can build it locally via npm or you can visit a page deployed on GitHub pages. You can upload the archives obtained from the scenarios presented in this chapter, and investigate the line coverage. No uploaded data is stored on the server.

You can then load the archive into the dashboard and browse it file by file.

Gathering Zephyr’s coverage

This example uses the script to gather coverage of a Zephyr’s app running on an emulated Nucleo H753ZI. Follow along to prepare a test scenario and see how much code is covered by the test.

First, download Zephyr and set up your environment as described in the official guide.

After following the steps as described in the Getting Started section, building a shell sample requires just one command:

west build -b nucleo_h753zi zephyr/samples/subsys/shell/shell_module/ -- -DCONFIG_NO_OPTIMIZATIONS=y

Notice that the optimizations are disabled completely to increase the quality of gathered coverage info. Alternatively you could build the sample with just the debug optimizations enabled (-DCONFIG_DEBUG_OPTIMIZATIONS=y). After running west you can find the compiled zephyr.elf file in the build/zephyr/ directory - the shell application for which want to obtain coverage info.

Below is a deterministic Robot test file that tests the coverage of our case:

*** Test Cases ***
Should Report Shell Coverage
    Execute Command           include @platforms/boards/nucleo_h753zi.repl
    Execute Command           sysbus LoadELF $CWD/build/zephyr/zephyr.elf
    Execute Command           cpu CreateExecutionTracing "trace" @${CURDIR}/trace.bin.gz PC True True
    Create Terminal Tester    sysbus.usart3  defaultPauseEmulation=True

    Wait For Prompt On Uart   uart:~$
    Write Line To Uart        demo board
    Wait For Line On Uart     nucleo_h753zi
    Wait For Prompt On Uart   uart:~$

In this sample, we await the prompt which signals that the shell is ready to receive data. Then we query the shell module for the board name, and again await the prompt, after the command finishes. Immediately after receiving the response on the UART, the emulation stops, and the execution trace is dumped into the file.

To run the test, execute renode-test path/to/test.robot.

The trace will be saved into a trace.bin.gz file, located in the same directory as the Robot test file.

To create a coverage report, execute the following command:

renode-retracer coverage trace.bin.gz \
  --binary build/zephyr/zephyr.elf \
  --output coverage.zip \
  --export-for-coverview

Intentionally, no sources are provided. The script will discover and load them automatically, provided that they still exist on the current machine, where the build happened. Otherwise, you might need to adjust paths with --sub-source-path.

Generating coverage data can take some time to process, depending on the application’s codebase size and the execution time of the program (size of the trace).

Afterwards, the archive can be loaded into Coverview and browsed interactively. For the code responsible for printing the board name (samples/subsys/shell/shell_module/src/main.c), it might look like so:

image

Legacy report mode

It’s also possible to use a simple text-based mode for investigating coverage. To do so, pass the --legacy switch. For example, to obtain coverage data for main.c from the sample application described above, it’s possible to do the following:

renode-retracer coverage trace.bin.gz \
  --binary coverage-sample.elf \
  --sources main.c \
  --legacy

The output should start with the lines, as shown below. The number before the colon indicates the number of executions of each line.

     0:   const int buf_size = 100;
     0:   
    28:   void funB(int *buf, int b) {
  2828:       for (int i = 0; i < buf_size; i++) {
  2800:           buf[i] -= b;
     0:       }
    28:   }
     0:   
     0:   void funA(int *buf, int b);
     0:   void funC();
     0:   
     2:   void buf_increment_all(int *buf) {
   202:       for (int i = 0; i < buf_size; i++) {
   200:           buf[i] += 1;
     0:       }
     2:   }
...

Last update: 2025-03-06