Detekce neaktivního kódu a analýza pokrytí kódu testy s nástroji gcov, gcovr a lcov

Dnes
Doba čtení: 38 minut

Sdílet

Profesionální programátorka pracující v monitorovací kontrolní místnosti, obklopená velkými obrazovkami zobrazujícími řádky programovacího jazyka. Portrét ženy vytvářející software a kódování.
Autor: Shutterstock
Ukážeme si využití nástrojů gcov, gcovr a lcov při analýzách zdrojových kódů psaných v C, C++, Adě atd. Tyto nástroje dokážou zjistit, které příkazy jsou skutečně volány a které nikoli.

Obsah

1. Detekce neaktivního kódu a analýza pokrytí kódu testy s nástroji gcov, gcovrlcov

2. Zdrojový kód použitý pro ukázku nástrojů gcovrlcov

3. Příprava pro analýzu kódu s využitím nástroje gcov

4. Vygenerování čitelného protokolu s informacemi získanými v runtime

5. Nástroj gcovr určený pro vytvoření čitelných informací o volání příkazů v analyzovaných programech

6. Vyhodnocení informací získaných v runtime nástrojem gcovr

7. Vytvoření HTML stránek s výsledkem analýzy

8. Další výstupní formáty podporované nástrojem gcovr

9. Nástroj lcov

10. Praktické použití nástroje lcov

11. Reálný příklad: funkce pro manipulaci s rastrovými obrázky

12. Testy funkce image_create

13. Překlad knihovny pro manipulaci s obrázky současně s testy

14. Spuštění testů

15. Vyhodnocení pokrytí kódu testy: základní varianta

16. Grafická reprezentace vyhodnocení pokrytí kódu testy, ignorování testů ve výsledku

17. Výsledky získané nástrojem lcov

18. Soubor Makefile se všemi cíli

19. Repositáře s demonstračními příklady

20. Odkazy na Internetu

1. Detekce neaktivního kódu a analýza pokrytí kódu testy s nástroji gcov, gcovrlcov

Na stránkách Roota jsme se již seznámili s velmi užitečným a často používaným nástrojem gcov. Připomeňme si, že tento nástroj je součástí ekosystému GCC a slouží pro zjištění, které příkazy (nikoli celé programové řádky!) v programovém kódu jsou skutečně volány a které naopak nikoli. Navíc je u volaných příkazů možné zjistit, kolikrát byly volány. K čemu se však tato informace používá? V první řadě nám umožňuje detekovat mrtvé části kódu, které se v praxi nikdy nevolají, takže je možné se zamyslet nad tím, jestli tyto části zcela neodstranit. A ve druhé řadě lze snadno zjistit, které části kódu jsou pokryty jednotkovými testy (unit tests), což je problematika, které jsme se již taktéž poměrně dopodrobna věnovali.

Poznámka: v předchozím odstavci jsme si řekli, že nástroj gcov je součástí ekosystému GCC, což znamená, že je ho možné použít společně s překladači, jenž jsou do GCC zahrnuty. Kromě klasického překladače jazyka C se jedná o C++, ale například i o Objective-C, Objective-C++, Fortran, Adu, jazyk D a taktéž jazyk Go.

Nástroj gcov lze relativně dobře integrovat s vývojovými prostředími, ovšem v současnosti je taktéž zapotřebí, aby byly zjištěné informace (o pokrytí kódu testy) dostupné například na CI popř. aby bylo možné tyto informace vhodnou formou zobrazit i dalším členům týmu (například formou HTML stránek atd.). A právě k těmto účelům slouží další dva nástroje pojmenované gcovr a lcov. lcov patří mezi spíše konzervativnější nástroje (mimochodem je psaný v Perlu), zatímco gcovr se snaží o podporu mnoha výstupních formátů (a je psaný v Pythonu).

2. Zdrojový kód použitý pro ukázku nástrojů gcovrlcov

Základní vlastnosti nástrojů gcovr a lcov si otestujeme na několika jednoduchých demonstračních příkladech. První demonstrační příklad, který si v dnešním článku ukážeme, je napsaný v programovacím jazyku C, i když by bylo možné použít i další jazyky podporované překladači z rodiny GCC. Ve zdrojovém kódu tohoto příkladu nalezneme triviální implementaci konstrukce binárního stromu (binary tree) určeného pro uložení řetězců společně s funkcí určenou pro realizaci průchodu (traverzace) tímto stromem. Při průchodu stromem je pro každý uzel, který se projde, volána funkce callback_function. Ovšem průchod stromem je (alespoň prozatím) realizován nad prázdným stromem, takže již dopředu lze velmi snadno odhadnout, že zdaleka ne všechny řádky programového kódu budou v čase běhu programu (tedy v runtime) využity (zavolány):

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
 
 
typedef struct Node
{
    struct Node *left;
    struct Node *right;
    char *value;
} Node;
 
 
 
void insert_new_node(Node **root, char *value)
{
    int cmp;
 
    if (*root == NULL)
    {
        *root = (Node *)malloc(sizeof(Node));
        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
        strcpy((*root)->value, value);
        (*root)->left = NULL;
        (*root)->right = NULL;
        return;
    }
    cmp = strcmp(value, (*root)->value);
    if (cmp < 0)
    {
        insert_new_node(&(*root)->left, value);
    }
    else
    {
        insert_new_node(&(*root)->right, value);
    }
}
 
 
 
void traverse_tree(Node *root, void (*callback_function)(char *))
{
    if (root == NULL)
    {
        return;
    }
    traverse_tree(root->left, callback_function);
    callback_function(root->value);
    traverse_tree(root->right, callback_function);
}
 
 
 
void callback_function(char *value)
{
    printf("%s\n", value);
}
 
 
 
int main(void)
{
    static Node *root = NULL;
 
    traverse_tree(root, callback_function);
 
    return 0;
}
Poznámka: úplný zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/sli­des/blob/master/sources/tre­e1.c.
Obrázek

Obrázek 1: Běžný překlad a slinkování zdrojového kódu uloženého v souboru tree1.c do spustitelného souboru nazvaného tree1

Autor: tisnik, podle licence: Rights Managed

3. Příprava pro analýzu kódu s využitím nástroje gcov

Jak jsme se již dozvěděli v úvodní kapitole, jsou nástroje gcovr a lcov do jisté míry závislé na utilitě gcov, která je součástí nástrojů GCC. Ukažme si tedy v rychlosti, jak se gcov používá. Pro zjištění, které části kódu jsou živé (volané) a které nikoli, je zapotřebí provést překlad zdrojového kódu s využitím přepínačů -fprofile-arcs a -ftest-coverage. Navíc je více než vhodné nepoužívat optimalizace, protože potřebujeme mít co nejlepší mapování mezi řádky zdrojového kódu a vygenerovaným nativním strojovým kódem.

Obrázek

Obrázek 2: Při překladu s přepínači -fprofile-arcs -ftest-coverage se vytvoří upravený spustitelný soubor a navíc i binární soubor s koncovkou .gcno.

Autor: tisnik, podle licence: Rights Managed

Překlad a následné slinkování provedeme tímto příkazem:

$ gcc -v -fprofile-arcs -ftest-coverage tree1.c -o tree1

Vzhledem k tomu, že byl při překladu použit přepínač -v, vypíšou se podrobné informace o jednotlivých operacích, které se interně provádí. Povšimněte si především (zvýrazněné) knihovny gcov, která je automaticky přidána do fáze linkování:

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/15/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,objc,obj-c++,ada,go,d,m2,cobol,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --enable-libstdcxx-backtrace --with-libstdcxx-zoneinfo=/usr/share/zoneinfo --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --with-isl=/builddir/build/BUILD/gcc-15.2.1-build/gcc-15.2.1-20251111/obj-x86_64-redhat-linux/isl-install --enable-offload-targets=nvptx-none,amdgcn-amdhsa --enable-offload-defaulted --without-cuda-driver --enable-gnu-indirect-function --enable-cet --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux --with-build-config=bootstrap-lto --enable-link-serialization=1
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 15.2.1 20251111 (Red Hat 15.2.1-4) (GCC)
COLLECT_GCC_OPTIONS='-v' '-fprofile-arcs' '-ftest-coverage' '-o' 'tree1' '-mtune=generic' '-march=x86-64'
 /usr/libexec/gcc/x86_64-redhat-linux/15/cc1 -quiet -v tree1.c -quiet -dumpbase tree1.c -dumpbase-ext .c -mtune=generic -march=x86-64 -version -fprofile-arcs -ftest-coverage -o /tmp/ccRLbcUX.s
GNU C23 (GCC) version 15.2.1 20251111 (Red Hat 15.2.1-4) (x86_64-redhat-linux)
        compiled by GNU C version 15.2.1 20251111 (Red Hat 15.2.1-4), GMP version 6.3.0, MPFR version 4.2.2, MPC version 1.3.1, isl version isl-0.24-GMP
 
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/15/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/15/../../../../x86_64-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-redhat-linux/15/include
 /usr/local/include
 /usr/include
End of search list.
Compiler executable checksum: cb5b2100d753cda12335fdede84b8cff
COLLECT_GCC_OPTIONS='-v' '-fprofile-arcs' '-ftest-coverage' '-o' 'tree1' '-mtune=generic' '-march=x86-64'
 as -v --64 -o /tmp/ccOQH5cZ.o /tmp/ccRLbcUX.s
GNU assembler version 2.44 (x86_64-redhat-linux) using BFD version version 2.44-12.fc42
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/15/:/usr/libexec/gcc/x86_64-redhat-linux/15/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/15/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/15/:/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/15/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-fprofile-arcs' '-ftest-coverage' '-o' 'tree1' '-mtune=generic' '-march=x86-64' '-dumpdir' 'tree1.'
 /usr/libexec/gcc/x86_64-redhat-linux/15/collect2 -plugin
 /usr/libexec/gcc/x86_64-redhat-linux/15/liblto_plugin.so
 -plugin-opt=/usr/libexec/gcc/x86_64-redhat-linux/15/lto-wrapper
 -plugin-opt=-fresolution=/tmp/ccZZUkQZ.res -plugin-opt=-pass-through=-lgcc
 -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc
 -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id
 --no-add-needed --eh-frame-hdr --hash-style=gnu -m elf_x86_64 -dynamic-linker
 /lib64/ld-linux-x86-64.so.2 -o tree1
 /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crt1.o
 /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crti.o
 /usr/lib/gcc/x86_64-redhat-linux/15/crtbegin.o
 -L/usr/lib/gcc/x86_64-redhat-linux/15
 -L/usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64 -L/lib/../lib64
 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/15/../../.. -L/lib
 -L/usr/lib /tmp/ccOQH5cZ.o -lgcov -lgcc --push-state
 --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s
 --pop-state /usr/lib/gcc/x86_64-redhat-linux/15/crtend.o
 /usr/lib/gcc/x86_64-redhat-linux/15/../../../../lib64/crtn.o
COLLECT_GCC_OPTIONS='-v' '-fprofile-arcs' '-ftest-coverage' '-o' 'tree1' '-mtune=generic' '-march=x86-64' '-dumpdir' 'tree1.'

Výsledkem překladu bude (podle očekávání) soubor nazvaný tree1 (bez koncovky) a navíc i soubor se jménem tree1.gcno obsahující – jak jsme si již ve stručnosti řekli – mapování mezi programovými řádky ve zdrojovém kódu a adresami v paměťové oblasti alokované pro běžící program:

$ ls -l
 
total 108
-rwxrwxr-x 1 ptisnovs ptisnovs 28776 Jan 15 12:28 tree1
-rw-r--r-- 1 ptisnovs ptisnovs  1111 Jan 13 18:50 tree1.c
-rw-rw-r-- 1 ptisnovs ptisnovs  1471 Jan 15 12:28 tree1.gcno

Nyní testovaný program tree1 spustíme stejným způsobem, jako bychom ho spouštěli bez zjišťování volaných řádků:

$ ./tree1

Po ukončení běhu programu by měl být vytvořen nový soubor tree1.gcda s čítači přístupů k jednotlivým řádkům:

$ ls -l
 
total 120
-rwxrwxr-x 1 ptisnovs ptisnovs 28776 Jan 15 12:28 tree1
-rw-r--r-- 1 ptisnovs ptisnovs  1111 Jan 13 18:50 tree1.c
-rw-rw-r-- 1 ptisnovs ptisnovs  1471 Jan 15 12:28 tree1.gcno
-rw-rw-r-- 1 ptisnovs ptisnovs   204 Jan 15 12:29 tree1.gcda

Oba dva výše zmíněné soubory, tedy tree1.gcno a tree1.gcda je nyní nutné sloučit nástrojem gcov, popř. přímo zpracovat dalšími nástroji.

Poznámka: oba výše zmíněné soubory tree1.gcnotree1.gcda mají binární formát a nejsou tedy (dobře) čitelné.
Obrázek

Obrázek 3: Po spuštění nativního spustitelného souboru tree1 dojde mj. i k vygenerování binárního souboru s koncovkou gcda.

Autor: tisnik, podle licence: Rights Managed

4. Vygenerování čitelného protokolu s informacemi získanými v runtime

Nyní nastal čas na vygenerování čitelného protokolu, z něhož zjistíme, které řádky ve vstupním zdrojovém textu jsou skutečně volány a které naopak nikoli. Tento protokol si necháme vygenerovat příkazem:

$ gcov tree1.c
Obrázek

Obrázek 4: Nástroj gcov zpracovává jak vstupní zdrojový kód, tak i oba binární soubory s koncovkami .gcno a .gcda. Výsledkem je jak ucelená informace o pokrytí kódu, tak i podrobnější informace vztažené k jednotlivým výrazům.

Autor: tisnik, podle licence: Rights Managed

Nástroj gcov v průběhu své činnosti zobrazí, které vstupní soubory se zdrojovými texty jsou zpracovávány a mj. taktéž ukáže velmi důležitou informaci – jaké procento řádků se zdrojovým kódem obsahuje živý kód. V našem konkrétním případě se reálně využila jen čtvrtina zapsaného kódu, což je z vypsaných výsledků jasně patrné:

File 'tree1.c'
Lines executed:25.00% of 24
Creating 'tree1.c.gcov'
 
Lines executed:25.00% of 24

Navíc je možné zjistit podrobnější informace o jednotlivých funkcích (nebo v případě objektově orientovaných jazyků i o metodách):

$ gcov -f tree1.c
Function 'main'
Lines executed:100.00% of 3
No branches
Calls executed:100.00% of 1
 
Function 'callback_function'
Lines executed:0.00% of 3
No branches
Calls executed:0.00% of 1
 
Function 'traverse_tree'
Lines executed:50.00% of 6
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
Calls executed:0.00% of 3
 
Function 'insert_new_node'
Lines executed:0.00% of 12
Branches executed:0.00% of 4
Taken at least once:0.00% of 4
Calls executed:0.00% of 2
 
File 'tree1.c'
Lines executed:25.00% of 24
Creating 'tree1.c.gcov'
 
Lines executed:25.00% of 24

Předchozí příkaz současně vytvořil nový soubor pojmenovaný „tree1.c.gcov“. Protokol obsahuje původní zdrojový kód doplněný o další informace. V levém sloupci je zobrazen počet volání příslušného řádku popř. řada znaků „#####“ na těch řádcích kódu, které obsahují příkazy, ale ty nejsou v runtimu volány. Naopak ty řádky kódu, které příkazy neobsahují, začínají znakem „-“. Za dvojtečkou je uvedeno číslo řádku popř. hodnota 0 pro ty řádky protokolu, které obsahují nějaké metainformace. A konečně ve třetím sloupci za další dvojtečkou je kopie zdrojového kódu popř. metainformace:

        -:    0:Source:tree1.c
        -:    0:Graph:tree1.gcno
        -:    0:Data:tree1.gcda
        -:    0:Runs:1
        -:    1:#include <stdlib.h>
        -:    2:#include <stdio.h>
        -:    3:#include <string.h>
        -:    4:
        -:    5:typedef struct Node
        -:    6:{
        -:    7:    struct Node *left;
        -:    8:    struct Node *right;
        -:    9:    char *value;
        -:   10:} Node;
        -:   11:
    #####:   12:void insert_new_node(Node **root, char *value)
        -:   13:{
        -:   14:    int cmp;
        -:   15:
    #####:   16:    if (*root == NULL)
        -:   17:    {
    #####:   18:        *root = (Node *)malloc(sizeof(Node));
    #####:   19:        (*root)->value = (char*)calloc(strlen(value), sizeof(char));
    #####:   20:        strcpy((*root)->value, value);
    #####:   21:        (*root)->left = NULL;
    #####:   22:        (*root)->right = NULL;
    #####:   23:        return;
        -:   24:    }
    #####:   25:    cmp = strcmp(value, (*root)->value);
    #####:   26:    if (cmp < 0)
        -:   27:    {
    #####:   28:        insert_new_node(&(*root)->left, value);
        -:   29:    }
        -:   30:    else
        -:   31:    {
    #####:   32:        insert_new_node(&(*root)->right, value);
        -:   33:    }
        -:   34:}
        -:   35:
        1:   36:void traverse_tree(Node *root, void (*callback_function)(char *))
        -:   37:{
        1:   38:    if (root == NULL)
        -:   39:    {
        1:   40:        return;
        -:   41:    }
    #####:   42:    traverse_tree(root->left, callback_function);
    #####:   43:    callback_function(root->value);
    #####:   44:    traverse_tree(root->right, callback_function);
        -:   45:}
        -:   46:
    #####:   47:void callback_function(char *value)
        -:   48:{
    #####:   49:    printf("%s\n", value);
    #####:   50:}
        -:   51:
        1:   52:int main(void)
        -:   53:{
        -:   54:    static Node *root = NULL;
        -:   55:
        1:   56:    traverse_tree(root, callback_function);
        -:   57:
        1:   58:    return 0;
        -:   59:}
        -:   60:

5. Nástroj gcovr určený pro vytvoření čitelných informací o volání příkazů v analyzovaných programech

Ve druhé části dnešního článku se seznámíme se základními vlastnostmi a možnostmi poskytovanými nástrojem nazvaným gcovr (s „r“ na konci). Tento nástroj zpracovává, podobně jako gcov, informace získané ze zdrojových kódů a taktéž z binárních souborů s koncovkami .gcno a .gcda. Výsledky je možné zobrazit různými způsoby popř. je možné je vyexportovat do několika (více či méně) standardních formátů používaných například pro zpracování výsledků jednotkových textů atd. Nástroj gcovr je většinou součástí repositářů distribucí Linuxu, takže je ho možné nainstalovat například přes apt nebo dnf:

$ sudo dnf install gcovr
 
Updating and loading repositories:
Repositories loaded.
Package                        Arch    Version        Repository      Size
Installing:
 gcovr                         noarch  8.3-1.fc42     fedora       1.8 MiB
Installing dependencies:
 python3-colorlog              noarch  6.9.0-2.fc42   fedora      53.3 KiB
 
Transaction Summary:
 Installing:         2 packages
 
Total size of inbound packages is 431 KiB. Need to download 431 KiB.
After this operation, 2 MiB extra will be used (install 2 MiB, remove 0 B).
Is this ok [y/N]: y
[1/2] python3-colorlog-0:6.9.0-2.fc42.noarch                 100% | 132.1 KiB/s |  26.6 KiB |  00m00s
[2/2] gcovr-0:8.3-1.fc42.noarch                              100% |   1.1 MiB/s | 404.1 KiB |  00m00s
-----------------------------------------------------------------------------------------------------
[2/2] Total                                                  100% | 358.3 KiB/s | 430.7 KiB |  00m01s
Running transaction
[1/4] Verify package files                                   100% | 250.0   B/s |   2.0   B |  00m00s
[2/4] Prepare transaction                                    100% |   5.0   B/s |   2.0   B |  00m00s
[3/4] Installing python3-colorlog-0:6.9.0-2.fc42.noarch      100% |   1.9 MiB/s |  57.8 KiB |  00m00s
[4/4] Installing gcovr-0:8.3-1.fc42.noarch                   100% |   3.1 MiB/s |   1.9 MiB |  00m01s
Complete!

Na to, že možnosti nástroje gcovr jsou poměrně široké, ukazuje i fakt, že pouze výpis a stručný popis všech přepínačů přesahuje svou velikostí 20kB:

usage: gcovr [options] [search_paths...]
 
A utility to run gcov and summarize the coverage in simple reports.
 
Options:
  -h, --help            Show this help message, then exit.
  --version             Print the version number, then exit.
  -v, --verbose         Print progress messages. Please include this output in bug reports. Config key(s): verbose.
  --no-color            Turn off colored logging. Is also set if environment variable NO_COLOR is present. Ignored if --force-color is
                        used. Config key(s): no-color.
  --force-color         Force colored logging, this is the default for a terminal. Is also set if environment variable FORCE_COLOR is
                        present. Has precedence over --no-color. Config key(s): force-color.
  -r, --root ROOT       The root directory of your source files. Defaults to '.', the current directory. File names are reported
                        relative to this root. The --root is the default --filter. Config key(s): root.
  --config CONFIG       Load that configuration file. Defaults to gcovr.cfg in the --root directory.
  --no-markers          Turn off exclusion markers. Any exclusion markers specified in source files will be ignored. Config key(s):
                        no-markers.
  --fail-under-line MIN
                        Exit with a status of 2 if the total line coverage is less than MIN. Can be ORed with exit status of '--fail-
                        under-branch', '--fail-under-decision', and '--fail-under-function' option. Config key(s): fail-under-line.
  --fail-under-branch MIN
                        Exit with a status of 4 if the total branch coverage is less than MIN. Can be ORed with exit status of '--
                        fail-under-line', '--fail-under-decision', and '--fail-under-function' option. Config key(s): fail-under-
                        branch.
  --fail-under-decision MIN
                        Exit with a status of 8 if the total decision coverage is less than MIN. Can be ORed with exit status of '--
                        fail-under-line', '--fail-under-branch', and '--fail-under-function' option. Config key(s): fail-under-
                        decision.
  --fail-under-function MIN
                        Exit with a status of 16 if the total function coverage is less than MIN. Can be ORed with exit status of '--
                        fail-under-line', '--fail-under-branch', and '--fail-under-decision' option. Config key(s): fail-under-
                        function.
  --source-encoding SOURCE_ENCODING
                        Select the source file encoding. Defaults to the system default encoding (UTF-8). Config key(s): source-
                        encoding.
                        ...
                        ...
                        ...

6. Vyhodnocení informací získaných v runtime nástrojem gcovr

Nástroj gcovr sice nabízí téměř nepřeberné množství přepínačů zadávaných na příkazové řádce, ovšem jeho základní způsob použití je relativně jednoduchý. Nejdříve spustíme program, který jsme přeložili v rámci předchozích kapitol, aby měl gcovr k dispozici všechny potřebné soubory, tedy i tree1.gcno a tree1.gcda:

$ ./tree1

Nyní gcovr spustíme. Vzhledem k tomu, že se v pracovním adresáři nachází pouze jediný zdrojový kód a výsledky jeho běhu, můžeme nástroj spustit bez parametrů:

$ gcovr

První dva vytištěné řádky naznačují, že se načetly oba soubory tree1.gcnotree1.gcda. Následně se zobrazí základní informace o zdrojových textech i o řádcích, které nebyly zavolány:

(INFO) Reading coverage data...
(INFO) Writing coverage report...
------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: .
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
tree1.c                                       24       6    25%   12,16,18-23,25-26,28,32,42-44,47,49-50
------------------------------------------------------------------------------
TOTAL                                         24       6    25%
------------------------------------------------------------------------------
gcov, gcovr a lcov.

Obrázek 5: Nástroj gcovr zpracovává stejné soubory, jako gcov, ovšem produkuje odlišné formáty s výsledky analýzy kódu. 

Autor: tisnik, podle licence: Rights Managed

Můžeme si vyžádat i podrobnější výpis. Jedná se o obdobu předchozího výpisu, ovšem navíc se na konci zobrazí statistické informace rozdělené na programové řádky, funkce a větve:

$ gcovr --txt-summary

Zobrazené výsledky by měly vypadat takto:

(INFO) Reading coverage data...
(INFO) Writing coverage report...
------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: .
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
tree1.c                                       24       6    25%   12,16,18-23,25-26,28,32,42-44,47,49-50
------------------------------------------------------------------------------
TOTAL                                         24       6    25%
------------------------------------------------------------------------------
lines: 25.0% (6 out of 24)
functions: 50.0% (2 out of 4)
branches: 16.7% (1 out of 6)

7. Vytvoření HTML stránek s výsledkem analýzy

Nejdůležitější vlastností nástroje gcovr je podpora velkého množství výstupných formátů s analýzou zdrojových textů. Některé z těchto formátů jsou určeny přímo pro vývojáře (HTML výstup), další pak spíše pro integraci s dalšími systémy, například s Jenkinsem atd.

Začneme popisem výstupu do formátu HTML, který vypadá následovně:

gcov, gcovr a lcov.

Obrázek 6: Úvodní stránka analýzy převedené do formátu HTML. Jsou zde zobrazeny základní statistiky a odkazy stránky s analýzami zdrojových kódů. 

Autor: tisnik, podle licence: Rights Managed

HTML stránky s výsledkem analýzy se vytváří tímto příkazem:

$ gcovr --html-details coverage.html

Výsledkem je vždy minimálně stránka se všemi statistickými informacemi a seznamem stránek vygenerovaných pro každý zdrojový soubor zvlášť. Tyto HTML stránky (může jich být velký počet – záleží na projektu) mají následující strukturu:

gcov, gcovr a lcov.

Obrázek 7: Zeleně jsou zobrazeny řádky, které jsou zavolány, červeně řádky, které zavolány nejsou a žlutě ty řádky, na kterých došlo jen k částečnému vyhodnocení. 

Autor: tisnik, podle licence: Rights Managed

Zajímavá situace nastává u rozeskoků, resp. u každé programové struktury provádějící rozvětvení. Pokud kód nějakou větví neprošel, je pochopitelně taková větev označena červenou barvou. Ovšem navíc u podmínek nemusí dojít k jejich plnému vyhodnocení (kvůli zkrácenému vyhodnocování při použití operátorů && a ||). V takových situacích je příslušný řádek označen žlutou barvou a navíc je možné po rozbalení šipky v levém sloupci zjistit, do jak velké míry byla podmínka popř. struktura rozeskoku v čase běhu využita:

gcov, gcovr a lcov.

Obrázek 8: Podrobnější informace o „žlutém“ řádku, na kterém došlo jen k částečnému vyhodnocení. 

Autor: tisnik, podle licence: Rights Managed
Poznámka: zajímavé je, že i některé poměrně sofistikované nástroje a frameworky určené pro jiné programovací jazyky, nepočítají s možností, že se vyhodnotí jen část výrazu (a zkrácené vyhodnocování je implementováno v mnoha dalších programovacích jazycích, nejedná se v žádném případě o specialitu céčka).

8. Další výstupní formáty podporované nástrojem gcovr

Nástroj gcovr podporuje i další výstupní formáty. Jedním z často používaných formátů ve světě Javy je formát využívaný mj. knihovnou JaCoCo. Jedná se o formát založený na XML. Výstup v tomto formátu se vytvoří následovně:

$ gcovr --jacoco > coverage.xml

Pro větší čitelnost výsledek naformátujeme (jinak je vše uloženo na jediném textovém řádku):

$ xmllint --format coverage.xml > coverage_.xml

Výsledek obsahuje informace o volání či naopak nevolání příkazů na jednotlivých programových řádcích. Navíc se na začátku nachází uzly se statistickými informacemi:

<?xml version='1.0' encoding='UTF-8'?>
<coverage clover="1768559261" generated="1768559261">
  <project timestamp="1768559261">
    <metrics complexity="0" elements="24" coveredelements="6" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0" packages="1" classes="1" files="1" loc="58" ncloc="24"/>
    <package name="root">
      <metrics complexity="0" elements="24" coveredelements="6" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0" classes="1" files="1" loc="58" ncloc="24"/>
      <file name="tree1.c" path="tree1.c">
        <metrics complexity="0" elements="24" coveredelements="6" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0" classes="1" loc="58" ncloc="24"/>
        <class name="id$d135ac4a322c826db586ae97a7367d33">
          <metrics complexity="0" elements="24" coveredelements="6" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0"/>
        </class>
        <line num="12" type="stmt" count="0"/>
        <line num="16" type="stmt" count="0"/>
        <line num="18" type="stmt" count="0"/>
        <line num="19" type="stmt" count="0"/>
        <line num="20" type="stmt" count="0"/>
        <line num="21" type="stmt" count="0"/>
        <line num="22" type="stmt" count="0"/>
        <line num="23" type="stmt" count="0"/>
        <line num="25" type="stmt" count="0"/>
        <line num="26" type="stmt" count="0"/>
        <line num="28" type="stmt" count="0"/>
        <line num="32" type="stmt" count="0"/>
        <line num="36" type="stmt" count="1"/>
        <line num="38" type="stmt" count="1"/>
        <line num="40" type="stmt" count="1"/>
        <line num="42" type="stmt" count="0"/>
        <line num="43" type="stmt" count="0"/>
        <line num="44" type="stmt" count="0"/>
        <line num="47" type="stmt" count="0"/>
        <line num="49" type="stmt" count="0"/>
        <line num="50" type="stmt" count="0"/>
        <line num="52" type="stmt" count="1"/>
        <line num="56" type="stmt" count="1"/>
        <line num="58" type="stmt" count="1"/>
      </file>
    </package>
  </project>
  <testproject timestamp="1768559261">
    <metrics complexity="0" elements="0" coveredelements="0" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0"/>
    <package name="dummy">
      <metrics complexity="0" elements="0" coveredelements="0" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0"/>
      <file name="dummy" path="dummy">
        <metrics complexity="0" elements="0" coveredelements="0" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0"/>
        <class name="id$275876e34cf609db118f3d84b799a790">
          <metrics complexity="0" elements="0" coveredelements="0" conditionals="0" coveredconditionals="0" statements="0" coveredstatements="0" coveredmethods="0" methods="0"/>
        </class>
      </file>
    </package>
  </testproject>
</coverage>

Podobně lze získat výsledky kompatibilní s Coberturou (viz též https://cobertura.github.i­o/cobertura/). I tento formát je založený na XML:

$ gcovr --cobertura > coverage.xml
$ xmllint --format cobertura.xml > cobertura.xml

Ukázka výsledků:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage line-rate="0.25" branch-rate="0.16666666666666666" lines-covered="6" lines-valid="24" branches-covered="1" branches-valid="6" complexity="0.0" timestamp="1768748596" version="gcovr 8.3">
  <sources>
    <source>/home/ptisnovs/</source>
  </sources>
  <packages>
    <package name="" line-rate="0.25" branch-rate="0.16666666666666666" complexity="0.0">
      <classes>
        <class name="tree1_c" filename="tree1.c" line-rate="0.25" branch-rate="0.16666666666666666" complexity="0.0">
          <methods>
            <method name="insert_new_node" signature="()" line-rate="0.0" branch-rate="0.0" complexity="0.0">
              <lines>
                <line number="12" hits="0" branch="false"/>
                <line number="16" hits="0" branch="true" condition-coverage="0% (0/2)">
                  <conditions>
                    <condition number="0" type="jump" coverage="0%"/>
                  </conditions>
                </line>
                <line number="18" hits="0" branch="false"/>
                <line number="19" hits="0" branch="false"/>
                <line number="20" hits="0" branch="false"/>
                <line number="21" hits="0" branch="false"/>
                <line number="22" hits="0" branch="false"/>
                <line number="23" hits="0" branch="false"/>
                <line number="25" hits="0" branch="false"/>
                <line number="26" hits="0" branch="true" condition-coverage="0% (0/2)">
                  <conditions>
                    <condition number="0" type="jump" coverage="0%"/>
                  </conditions>
                </line>
                <line number="28" hits="0" branch="false"/>
                <line number="32" hits="0" branch="false"/>
              </lines>
            </method>
            <method name="traverse_tree" signature="()" line-rate="0.5" branch-rate="0.5" complexity="0.0">
              <lines>
                <line number="36" hits="1" branch="false"/>
                <line number="38" hits="1" branch="true" condition-coverage="50% (1/2)">
                  <conditions>
                    <condition number="0" type="jump" coverage="50%"/>
                  </conditions>
                </line>
                <line number="40" hits="1" branch="false"/>
                <line number="42" hits="0" branch="false"/>
                <line number="43" hits="0" branch="false"/>
                <line number="44" hits="0" branch="false"/>
              </lines>
            </method>
            <method name="callback_function" signature="()" line-rate="0.0" branch-rate="1.0" complexity="0.0">
              <lines>
                <line number="47" hits="0" branch="false"/>
                <line number="49" hits="0" branch="false"/>
                <line number="50" hits="0" branch="false"/>
              </lines>
            </method>
            <method name="main" signature="()" line-rate="1.0" branch-rate="1.0" complexity="0.0">
              <lines>
                <line number="52" hits="1" branch="false"/>
                <line number="56" hits="1" branch="false"/>
                <line number="58" hits="1" branch="false"/>
              </lines>
            </method>
          </methods>
          <lines>
            <line number="12" hits="0" branch="false"/>
            <line number="16" hits="0" branch="true" condition-coverage="0% (0/2)">
              <conditions>
                <condition number="0" type="jump" coverage="0%"/>
              </conditions>
            </line>
            <line number="18" hits="0" branch="false"/>
            <line number="19" hits="0" branch="false"/>
            <line number="20" hits="0" branch="false"/>
            <line number="21" hits="0" branch="false"/>
            <line number="22" hits="0" branch="false"/>
            <line number="23" hits="0" branch="false"/>
            <line number="25" hits="0" branch="false"/>
            <line number="26" hits="0" branch="true" condition-coverage="0% (0/2)">
              <conditions>
                <condition number="0" type="jump" coverage="0%"/>
              </conditions>
            </line>
            <line number="28" hits="0" branch="false"/>
            <line number="32" hits="0" branch="false"/>
            <line number="36" hits="1" branch="false"/>
            <line number="38" hits="1" branch="true" condition-coverage="50% (1/2)">
              <conditions>
                <condition number="0" type="jump" coverage="50%"/>
              </conditions>
            </line>
            <line number="40" hits="1" branch="false"/>
            <line number="42" hits="0" branch="false"/>
            <line number="43" hits="0" branch="false"/>
            <line number="44" hits="0" branch="false"/>
            <line number="47" hits="0" branch="false"/>
            <line number="49" hits="0" branch="false"/>
            <line number="50" hits="0" branch="false"/>
            <line number="52" hits="1" branch="false"/>
            <line number="56" hits="1" branch="false"/>
            <line number="58" hits="1" branch="false"/>
          </lines>
        </class>
      </classes>
    </package>
  </packages>
</coverage>

Dále je možné získat výsledek analýzy ve formátu JSON. To se provede následovně:

$ gcovr --json > coverage.json

Opět si necháme výsledný soubor naformátovat, tentokrát ovšem pomocí filtru jq a nikoli xmllint:

$ jq . coverage.json > coverage_.json

Výsledek obsahuje informace rozdělené na příkazy, nikoli na programové řádky, takže je přesnější:

{
  "gcovr/format_version": "0.11",
  "files": [
    {
      "file": "tree1.c",
      "lines": [
        {
          "line_number": 12,
          "function_name": "insert_new_node",
          "count": 0,
          "branches": [],
          "block_ids": [],
          "gcovr/md5": "58c5ee95089f39f73d893466c061e5d3"
        },
        {
          "line_number": 16,
          "function_name": "insert_new_node",
          "count": 0,
          "branches": [
            {
              "source_block_id": 2,
              "count": 0,
              "fallthrough": true,
              "throw": false,
              "destination_block_id": 3
            },
            {
              "source_block_id": 2,
              "count": 0,
              "fallthrough": false,
              "throw": false,
              "destination_block_id": 4
            }
          ],
          "block_ids": [
            2
          ],
          "gcovr/md5": "82480b52fec8b3bb3ef50b08ebb9e0e1"
        },
        {
          "line_number": 18,
          "function_name": "insert_new_node",
          "count": 0,
          "branches": [],
          "block_ids": [],
          "gcovr/md5": "33d2b5c6951cb55d1444bf08bd766963"
        },
        {
          "line_number": 19,
          "function_name": "insert_new_node",
          "count": 0,
          "branches": [],
          "block_ids": [],
          "gcovr/md5": "cb058e7b35a45c047e5fadc0d8bbf298"
        },
        {
          "line_number": 20,
          "function_name": "insert_new_node",
          "count": 0,
          "branches": [],
          "block_ids": [],
          "gcovr/md5": "b51316ddf9a8d85d19be12b1ff3e64a6"
        },
        {
          "line_number": 21,
          "function_name": "insert_new_node",
          "count": 0,
          "branches": [],
          "block_ids": [],
          "gcovr/md5": "e85f3f74b34dde201758e55bf8b5acfd"
        },
        ...
        ...
        ...
        {
          "name": "insert_new_node",
          "demangled_name": "insert_new_node",
          "lineno": 12,
          "execution_count": 0,
          "blocks_percent": 0.0,
          "pos": [
            "12:6",
            "34:1"
          ]
        },
        {
          "name": "main",
          "demangled_name": "main",
          "lineno": 52,
          "execution_count": 1,
          "blocks_percent": 100.0,
          "pos": [
            "52:5",
            "59:1"
          ]
        },
        {
          "name": "traverse_tree",
          "demangled_name": "traverse_tree",
          "lineno": 36,
          "execution_count": 1,
          "blocks_percent": 50.0,
          "pos": [
            "36:6",
            "45:1"
          ]
        }
      ]
    }
  ]
}

9. Nástroj lcov

Druhý nástroj, se kterým se v dnešní článku seznámíme, se jmenuje lcov. Tento nástroj taktéž umožňuje vygenerovat HTML stránky s informacemi o tom, které příkazy v analyzovaném programovém kódu byly zavolány a které nikoli. Celý proces je rozdělený na dva kroky. V kroku prvním se z binárních souborů .gcno a .gcda vygeneruje textový soubor, ve kterém jsou (i když poněkud krypticky) zapsány informace o volaných příkazech. Tento soubor je možné zpracovat dalšími filtry, především pak filtrem nazvaným genhtml, který na základě předaných údajů vytvoří HTML stránky s podobným obsahem, jaký lze získat z výše popsaného nástroje gcovr.

Poznámka: interně se ve druhém kroku volají i další filtry, například genpng.

10. Praktické použití nástroje lcov

Vyzkoušejme si nyní použití nástroje lcov. Jak již bylo napsáno v předchozí kapitole, je nejdříve nutné vygenerovat textový soubor coverage.info, který bude následně předán do genhtml:

$ lcov --capture --directory . --output-file coverage.info

Výsledkem bude soubor, jehož obsah připomíná některé starší značkovací jazyky:

TN:
SF:/home/ptisnovs/lcov/tree1.c
FN:12,34,insert_new_node
FN:36,45,traverse_tree
FN:47,50,callback_function
FN:52,59,main
FNDA:0,insert_new_node
FNDA:1,traverse_tree
FNDA:0,callback_function
FNDA:1,main
FNF:4
FNH:2
DA:12,0
DA:16,0
DA:18,0
DA:19,0
DA:20,0
DA:21,0
DA:22,0
DA:23,0
DA:25,0
DA:26,0
DA:28,0
DA:32,0
DA:36,1
DA:38,1
DA:40,1
DA:42,0
DA:43,0
DA:44,0
DA:47,0
DA:49,0
DA:50,0
DA:52,1
DA:56,1
DA:58,1
LF:24
LH:6
end_of_record

Dále si necháme vytvořit HTML stránky s čitelnými informacemi o volaných příkazech:

$ genhtml coverage.info --output-directory .
 
Found 1 entries.
Found common filename prefix "/home/ptisnovs/xy/xxx"
Generating output.
Processing file zzz/tree1.c
  lines=24 hit=6 functions=4 hit=2
Overall coverage rate:
  lines......: 25.0% (6 of 24 lines)
  functions......: 50.0% (2 of 4 functions)

Výsledné HTML stránky do značné míry připomínají výsledky, které jsme získali přes gcovr:

Nástroj lcov

Obrázek 9: Přehledová stránka vygenerovaná přes genhtml. 

Autor: tisnik, podle licence: Rights Managed
Nástroj lcov

Obrázek 10: Přehled příkazů, které byly skutečně zavolány a které naopak ne. 

Autor: tisnik, podle licence: Rights Managed

11. Reálný příklad: funkce pro manipulaci s rastrovými obrázky

V závěrečné třetině dnešního článku si ukážeme příklad z praxe (ovšem značně zjednodušený). Jedná se o projekt, po jehož překladu vznikne knihovna určená pro manipulaci s rastrovými obrázky, aplikace filtrů, kombinace většího množství obrázků atd. Kvůli stručnosti ovšem zdrojové kódy této knihovny zkrátíme na definici datových struktur a taktéž na definici dvou funkcí, konkrétně funkcí image_size a image_create.

Zdrojové kódy se skládají z hlavičkového souboru pojmenovaného image.h a souboru s implementací všech potřebných funkcí. Tento soubor se jmenuje image.c. Hlavičkový soubor vypadá následovně:

#ifndef _IMAGE_H_
#define _IMAGE_H_
 
/* Image types */
#define GRAYSCALE 1
#define RGB 3
#define RGBA 4
 
/* Maximum image resolution */
#define MAX_WIDTH 8192
#define MAX_HEIGHT 8192
 
/**
 * Structure that represents raster image of configurable resolution and bits
 * per pixel format.
 */
typedef struct {
    unsigned int   width;
    unsigned int   height;
    unsigned int   bpp;
    unsigned char *pixels;
} image_t;
 
enum error {
    OK,
    NULL_POINTER,
    NULL_IMAGE_POINTER,
    NULL_PIXELS_POINTER,
    INVALID_IMAGE_DIMENSION,
    INVALID_IMAGE_TYPE
};
 
/* function headers */
size_t  image_size(const image_t *image);
image_t image_create(const unsigned int width, const unsigned int height, const unsigned int bpp);
 
#endif

Soubor image.c s implementacemi všech potřebných funkcí má tuto podobu:

#include <stdlib.h>
 
#include "image.h"
 
 
 
/**
 * Compute the total size in bytes of an image's pixel buffer.
 *
 * @param image Pointer to the image whose buffer size will be computed.
 *
 * @returns Total number of bytes required for the image's pixel buffer
 *          (width * height * bpp).
 */
size_t image_size(const image_t *image) {
    if (image == NULL) {
        return 0;
    }
    /* cast to size_t before multiplication to prevent overflow */
    return (size_t)image->width * (size_t)image->height * (size_t)image->bpp;
}
 
 
 
/**
 * Create an image_t with the given width, height, and bytes-per-pixel,
 * allocating a pixel buffer.
 *
 * The returned image_t fields width, height, and bpp are initialized and
 * pixels points to a newly allocated buffer of size width * height * bpp. If
 * allocation fails, pixels will be NULL.
 *
 * @param width  Image width specified in pixels.
 * @param height Image height specified in pixels.
 * @param bpp    Bytes per pixel (bytes used to store a single pixel).
 *
 * @returns The initialized image_t; its `pixels` member points to the
 *          allocated buffer or NULL on allocation failure.
 */
image_t image_create(const unsigned int width, const unsigned int height, const unsigned int bpp) {
    image_t image;
 
    /* validate image size */
    if (width == 0 || height == 0 || width > MAX_WIDTH || height > MAX_HEIGHT) {
        image.width = 0;
        image.height = 0;
        image.bpp = 0;
        image.pixels = NULL;
        return image;
    }
 
    /* validate image type */
    if (bpp != GRAYSCALE && bpp != RGB && bpp != RGBA) {
        image.width = 0;
        image.height = 0;
        image.bpp = 0;
        image.pixels = NULL;
        return image;
    }
 
    /* initialize image */
    image.width = width;
    image.height = height;
    image.bpp = bpp;
 
    /* callers must check that image.pixels != NULL */
    image.pixels = (unsigned char *)malloc(image_size(&image));
 
    /* make sure the image will be 'zero value' when pixels are not allocated */
    if (image.pixels == NULL) {
        image.width = 0;
        image.height = 0;
        image.bpp = 0;
    }
    return image;
}

12. Testy funkce image_create

Funkce přítomné v knihovně pro manipulaci s rastrovými obrázky by pochopitelně bylo vhodné otestovat. Pokusme se tedy napsat základní testy, které zjistí, jak se chová funkce image_create. Testy jsou pochopitelně uloženy ve zvláštním souboru (nebude zařazen do výsledné knihovny) a kromě maker TEST_BEGIN a TEST_END, které pouze zajišťují funkcionalitu v ANSI C, se jedná o přímočarý kód, jenž namísto různých testovacích frameworků pouze používá makro assert:

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
 
#include "image.h"
 
#define TEST_BEGIN \
    puts(__FUNCTION__); \
    {
 
#define TEST_END \
    }
 
void test_image_create_zero_width(void) {
    TEST_BEGIN
    image_t image = image_create(0, 100, 4);
    assert(image.width == 0);
    assert(image.height == 0);
    assert(image.bpp == 0);
    assert(image.pixels == NULL);
    TEST_END
}
 
void test_image_create_too_wide(void) {
    TEST_BEGIN
    image_t image = image_create(MAX_WIDTH+1, 100, 4);
    assert(image.width == 0);
    assert(image.height == 0);
    assert(image.bpp == 0);
    assert(image.pixels == NULL);
    TEST_END
}
 
void test_image_create_zero_height(void) {
    TEST_BEGIN
    image_t image = image_create(100, 0, 4);
    assert(image.width == 0);
    assert(image.height == 0);
    assert(image.bpp == 0);
    assert(image.pixels == NULL);
    TEST_END
}
 
void test_image_create_wrong_image_type(void) {
    TEST_BEGIN
    image_t image = image_create(100, 100, 0);
    assert(image.width == 0);
    assert(image.height == 0);
    assert(image.bpp == 0);
    assert(image.pixels == NULL);
    TEST_END
}
 
void test_image_create_grayscale(void) {
    TEST_BEGIN
    image_t image = image_create(100, 100, GRAYSCALE);
    assert(image.pixels != NULL);
    free(image.pixels);
    TEST_END
}
 
void test_image_create_rgba(void) {
    TEST_BEGIN
    image_t image = image_create(100, 100, RGBA);
    assert(image.pixels != NULL);
    free(image.pixels);
    TEST_END
}
 
int main(void) {
    test_image_create_zero_width();
    test_image_create_too_wide();
    test_image_create_zero_height();
    test_image_create_wrong_image_type();
    test_image_create_grayscale();
    test_image_create_rgba();
    return 0;
}
Poznámka: některé testy jsem naschvál vynechal. Například se netestuje situace, v níž je požadován obrázek s větším počtem obrazových řádků, než je povoleno. Taktéž není otestováno, zda se vytvoří rastrový obrázek s 24 bity na pixel (RGB).

13. Překlad knihovny pro manipulaci s obrázky současně s testy

Připomeňme si, že všechny výše uvedené nástroje provádí analýzu na základě dat získaných v době běhu programu (runtime), nikoli statickou analýzu zdrojových kódů. Z tohoto důvodu je nutné nejdříve program i s testy přeložit a následně testy spustit.

V prvním kroku přeložíme všechny funkce, které tvoří výslednou knihovnu. Nesmíme přitom zapomenout na přepínače -fprofile-arcs -ftest-coverage, které zajistí, že se do výsledného kódu vloží i instrukce, které ve výsledku vedou ke vzniku souboru s runtime informacemi o volání příkazů:

$ gcc -v -c -fprofile-arcs -ftest-coverage image.c -o image.o

Dále stejným způsobem přeložíme soubor s implementací testů. Použijeme naprosto stejné přepínače překladače:

$ gcc -v -c -fprofile-arcs -ftest-coverage test.c -o test.o

Ve třetím kroku oba objektové soubory, které vznikly překladem, slinkujeme. V tomto případě ovšem nesmíme zapomenout na slinkování oproti knihovně gcov (linker nás případně upozorní, pokud na to zapomeneme):

$ gcc -v image.o test.o -lgcov -o test

Nyní by se v pracovním adresáři mělo nacházet osm souborů. Pro každý zdrojový soubor vznikl překladem odpovídající objektový soubor a taktéž soubor *.gcno. A navíc výše uvedeným slinkováním vznikl spustitelný soubor nazvaný jednoduše test:

$ ls -l
total 80
-rw-r--r--. 1 ptisnovs ptisnovs  2175 Jan 16 14:13 image.c
-rw-r--r--. 1 ptisnovs ptisnovs  1410 Jan 16 14:20 image.gcno
-rw-r--r--. 1 ptisnovs ptisnovs   740 Jan 16 14:15 image.h
-rw-r--r--. 1 ptisnovs ptisnovs  4264 Jan 16 14:20 image.o
-rwxr-xr-x. 1 ptisnovs ptisnovs 30472 Jan 16 14:20 test
-rw-r--r--. 1 ptisnovs ptisnovs  1749 Jan 16 14:19 test.c
-rw-r--r--. 1 ptisnovs ptisnovs  4775 Jan 16 14:19 test.gcno
-rw-r--r--. 1 ptisnovs ptisnovs 12120 Jan 16 14:19 test.o

14. Spuštění testů

Nyní nám pouze zbývá testy spustit, což je triviální:

$ ./test

V případě, že je implementace funkce image_create korektní, měly by se vypsat pouze jména testů. V opačném případě se zobrazí i informace o tom, že nějaká podmínka (aserce) nebyla splněna, což by však v demonstračním projektu nastat nemělo:

test_image_create_zero_width
test_image_create_too_wide
test_image_create_zero_height
test_image_create_wrong_image_type
test_image_create_grayscale
test_image_create_rgba

Po proběhnutí testů se v pracovním adresáři objeví dvojice nových souborů nazvaných image.gcda a test.gcda, které obsahují informace o volání příkazů získané v runtime:

total 88
-rw-r--r--. 1 ptisnovs ptisnovs  2175 Jan 16 14:13 image.c
-rw-r--r--. 1 ptisnovs ptisnovs   188 Jan 16 14:21 image.gcda
-rw-r--r--. 1 ptisnovs ptisnovs  1410 Jan 16 14:20 image.gcno
-rw-r--r--. 1 ptisnovs ptisnovs   740 Jan 16 14:15 image.h
-rw-r--r--. 1 ptisnovs ptisnovs  4264 Jan 16 14:20 image.o
-rwxr-xr-x. 1 ptisnovs ptisnovs 30472 Jan 16 14:20 test
-rw-r--r--. 1 ptisnovs ptisnovs  1749 Jan 16 14:19 test.c
-rw-r--r--. 1 ptisnovs ptisnovs   576 Jan 16 14:21 test.gcda
-rw-r--r--. 1 ptisnovs ptisnovs  4775 Jan 16 14:19 test.gcno
-rw-r--r--. 1 ptisnovs ptisnovs 12120 Jan 16 14:19 test.o

15. Vyhodnocení pokrytí kódu testy: základní varianta

Získáme základní informace o pokrytí kódu testy, a to s využitím nástroje gcov:

$ gcov image.c
 
File 'image.c'
Lines executed:84.62% of 26
Creating 'image.c.gcov'
 
Lines executed:84.62% of 26

Samozřejmě si můžeme nechat zobrazit podrobnější informace o volaných nebo nevolaných funkcích:

$ gcov -f image.c
Function 'image_create'
Lines executed:86.36% of 22
Branches executed:100.00% of 16
Taken at least once:81.25% of 16
Calls executed:100.00% of 1
 
Function 'image_size'
Lines executed:75.00% of 4
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
No calls
 
File 'image.c'
Lines executed:84.62% of 26
Creating 'image.c.gcov'
 
Lines executed:84.62% of 26

Současně vznikne soubor nazvaný image.c.gcov, ve kterém jsou získané informace zapsány k příslušným řádkům původního zdrojového kódu:

        -:    0:Source:image.c
        -:    0:Graph:image.gcno
        -:    0:Data:image.gcda
        -:    0:Runs:1
        -:    1:#include <stdlib.h>
        -:    2:
        -:    3:#include "image.h"
        -:    4:
        -:    5:
        -:    6:
        -:    7:/**
        -:    8: * Compute the total size in bytes of an image's pixel buffer.
        -:    9: *
        -:   10: * @param image Pointer to the image whose buffer size will be computed.
        -:   11: *
        -:   12: * @returns Total number of bytes required for the image's pixel buffer
        -:   13: *          (width * height * bpp).
        -:   14: */
        2:   15:size_t image_size(const image_t *image) {
        2:   16:    if (image == NULL) {
    #####:   17:        return 0;
        -:   18:    }
        -:   19:    /* cast to size_t before multiplication to prevent overflow */
        2:   20:    return (size_t)image->width * (size_t)image->height * (size_t)image->bpp;
        -:   21:}
        -:   22:
        -:   23:
        -:   24:
        -:   25:/**
        -:   26: * Create an image_t with the given width, height, and bytes-per-pixel,
        -:   27: * allocating a pixel buffer.
        -:   28: *
        -:   29: * The returned image_t fields width, height, and bpp are initialized and
        -:   30: * pixels points to a newly allocated buffer of size width * height * bpp. If
        -:   31: * allocation fails, pixels will be NULL.
        -:   32: *
        -:   33: * @param width  Image width specified in pixels.
        -:   34: * @param height Image height specified in pixels.
        -:   35: * @param bpp    Bytes per pixel (bytes used to store a single pixel).
        -:   36: *
        -:   37: * @returns The initialized image_t; its `pixels` member points to the
        -:   38: *          allocated buffer or NULL on allocation failure.
        -:   39: */
        6:   40:image_t image_create(const unsigned int width, const unsigned int height, const unsigned int bpp) {
        -:   41:    image_t image;
        -:   42:
        -:   43:    /* validate image size */
        6:   44:    if (width == 0 || height == 0 || width > MAX_WIDTH || height > MAX_HEIGHT) {
        3:   45:        image.width = 0;
        3:   46:        image.height = 0;
        3:   47:        image.bpp = 0;
        3:   48:        image.pixels = NULL;
        3:   49:        return image;
        -:   50:    }
        -:   51:
        -:   52:    /* validate image type */
        3:   53:    if (bpp != GRAYSCALE && bpp != RGB && bpp != RGBA) {
        1:   54:        image.width = 0;
        1:   55:        image.height = 0;
        1:   56:        image.bpp = 0;
        1:   57:        image.pixels = NULL;
        1:   58:        return image;
        -:   59:    }
        -:   60:
        -:   61:    /* initialize image */
        2:   62:    image.width = width;
        2:   63:    image.height = height;
        2:   64:    image.bpp = bpp;
        -:   65:
        -:   66:    /* callers must check that image.pixels != NULL */
        2:   67:    image.pixels = (unsigned char *)malloc(image_size(&image));
        -:   68:
        -:   69:    /* make sure the image will be 'zero value' when pixels are not allocated */
        2:   70:    if (image.pixels == NULL) {
    #####:   71:        image.width = 0;
    #####:   72:        image.height = 0;
    #####:   73:        image.bpp = 0;
        -:   74:    }
        2:   75:    return image;
        -:   76:}
        -:   77:

16. Grafická reprezentace vyhodnocení pokrytí kódu testy, ignorování testů ve výsledku

HTML stránky s přehlednými informacemi o tom, které části programového kódu jsou pokryty testy, se vygenerují tímto příkazem:

$ gcovr --html-details coverage.html

Výsledkem jsou HTML stránky, které si můžete prohlédnout na adrese https://tisnik.github.io/test-dependabot-no-devs/image_coverage1/coverage.html.

Ve skutečnosti výsledné stránky obsahují i informace o tom, které příkazy testů jsou volány. Tato informace nás většinou nezajímá, takže ji odstraníme pomocí přepínače -e (od slova exclude):

$ gcovr --html-details coverage.html -e test.c

Nyní budou HTML stránky, které jsou vygenerovány, obsahovat pouze informace o pokrytí původního kódu (nikoli testů): https://tisnik.github.io/test-dependabot-no-devs/image_coverage2/coverage.html.

17. Výsledky získané nástrojem lcov

Pro vygenerování výsledků získaných v runtime je pochopitelně možné namísto nástroje gcovr použít konkurenční nástroj lcov. Poslouží k tomu dvojice příkazů, z nichž první vytvoří mezivýsledky uložené do souboru coverage.info a druhý příkaz vygeneruje všechny potřebné HTML stránky, soubory se styly, obrázky atd.:

$ lcov --capture --directory . --output-file coverage.info
$ genhtml coverage.info --output-directory .

Výsledné HTML stránky vygenerované tímto způsobem jsou dostupné na adrese https://tisnik.github.io/test-dependabot-no-devs/image_coverage3/index.html.

18. Soubor Makefile se všemi cíli

Na závěr si ještě ukažme soubor Makefile, který obsahuje všechny potřebné cíle: sestavení výsledné knihovny, přeložení testů (s podporou gcov), spuštění testů, vygenerování HTML stránek s výsledky pokrytí testů nástrojem gcovr a konečně vygenerování HTML stránek s výsledky pokrytí testů, ovšem tentokrát nástrojem lcov, vyčištění pracovního adresáře od dočasných souborů atd.:

CC=gcc
CFLAGS=-Wall -ansi -pedantic
LFLAGS=-lm
 
LIBRARY_NAME=libimage.a
TESTNAME=test
 
all:    $(LIBRARY_NAME)
 
clean:
        rm -f $(LIBRARY_NAME)
        rm -f test
        rm -f *.o
        rm -f *.gcno
        rm -f *.gcda
        rm -f *.html
 
$(LIBRARY_NAME):        image.o
        ar rcs $(LIBRARY_NAME) image.o
 
image.o:        image.c
        $(CC) $(CFLAGS) -c -o $@ $^ $(LFLAGS)
 
image-cov.o:    image.c
        $(CC) $(CFLAGS) -c -fprofile-arcs -ftest-coverage -o $@ $^ $(LFLAGS)
 
test.o: test.c
        $(CC) $(CFLAGS) -c -fprofile-arcs -ftest-coverage -o $@ $^ $(LFLAGS)
 
$(TESTNAME):    test.o image-cov.o
        $(CC) image-cov.o test.o -lgcov -o $(TESTNAME)
 
coverage.html:  test.gcda
        gcovr --html-details coverage.html -e test.c
 
test.gcda:      test
        ./$(TESTNAME)
 
coverage.info:
        lcov --capture --directory . --output-file coverage.info
 
index.html:     coverage.info
        genhtml coverage.info --output-directory .

Překlad, spuštění testů a vytvoření HTML stránek s informacemi o pokrytí kódu testy, se tedy provede takto:

Školení Hacking

$ make coverage.html

Průběh celého procesu:

gcc -Wall -ansi -pedantic -c -fprofile-arcs -ftest-coverage -o test.o test.c -lm
gcc -Wall -ansi -pedantic -c -fprofile-arcs -ftest-coverage -o image-cov.o image.c -lm
gcc image-cov.o test.o -lgcov -o test
./test
test_image_create_zero_width
test_image_create_too_wide
test_image_create_zero_height
test_image_create_wrong_image_type
test_image_create_grayscale
test_image_create_rgba
gcovr --html-details coverage.html -e test.c
(INFO) Reading coverage data...
(INFO) Writing coverage report...

19. Repositáře s demonstračními příklady

Demonstrační soubory použité v dnešním článku byly uloženy do Git repositáře, jenž je dostupný na adrese https://github.com/tisnik/slides/, některé pak na adrese https://github.com/tisnik/test-dependabot-no-devs/. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé zdrojové soubory, které naleznete v následující tabulce:

# Soubor Stručný popis Adresa
1 tree1.c průchod prázdným binárním stromem bez jeho konstrukce https://github.com/tisnik/sli­des/blob/master/sources/tre­e1.c
2 tree2.c konstrukce binárního stromu s jediným uzlem; průchod tímto stromem https://github.com/tisnik/sli­des/blob/master/sources/tre­e2.c
3 tree3.c konstrukce binárního stromu s více uzly; průchod tímto stromem https://github.com/tisnik/sli­des/blob/master/sources/tre­e3.c
4 factorial.c naivní rekurzivní výpočet faktoriálu https://github.com/tisnik/sli­des/blob/master/sources/fac­torial.c
5 test.c několik funkcí s různým počtem parametrů, které jsou volány z main https://github.com/tisnik/sli­des/blob/master/sources/tes­t.c
       
6 image.h datová struktura představující obrázek, hlavičky funkcí pro práci s obrázkem, chybové kódy https://github.com/tisnik/test-dependabot-no-devs/blob/master/image/image.h
7 image.c implementace základních funkcí pro konstrukci rastrového obrázku https://github.com/tisnik/test-dependabot-no-devs/blob/master/image/image.c
8 test.c testy funkce určené pro konstrukci rastrového obrázku https://github.com/tisnik/test-dependabot-no-devs/blob/master/image/test.c
9 Makefile Makefile pro překlad, otestování a vygenerování informací o pokrytí kódu https://github.com/tisnik/test-dependabot-no-devs/blob/master/image/Makefile

20. Odkazy na Internetu

  1. GCC, the GNU Compiler Collection
    https://gcc.gnu.org/
  2. gcovr: online dokumentace
    https://gcovr.com/en/stable/
  3. lcov: online dokumentace
    https://lcov.readthedocs.i­o/en/latest/index.html
  4. gcov manual: Test Coverage Program
    https://gcc.gnu.org/online­docs/gcc/Gcov.html
  5. How to Analyze Code Coverage with gcov
    https://www.linuxtoday.com/blog/a­nalyzing-code-coverage-with-gcov/
  6. gcov – Unix, Linux Command
    https://www.tutorialspoin­t.com/unix_commands/gcov.htm
  7. Testing code coverage in C using GCOV
    https://www.youtube.com/wat­ch?v=UOGMNRcV9–4
  8. Nástroj objdump: švýcarský nožík pro vývojáře
    https://www.root.cz/clanky/nastroj-objdump-svycarsky-nozik-pro-vyvojare/
  9. What is code coverage?
    https://www.atlassian.com/continuous-delivery/software-testing/code-coverage
  10. Everything you need to know about code coverage
    https://www.codegrip.tech/pro­ductivity/everything-you-need-to-know-about-code-coverage/
  11. GCC, the GNU Compiler Collection
    https://gcc.gnu.org/
  12. Clang 17.0.0: Source-based Code Coverage
    https://clang.llvm.org/doc­s/SourceBasedCodeCoverage­.html
  13. Clang 17.0.0: SanitizerCoverage
    https://clang.llvm.org/doc­s/SanitizerCoverage.html
  14. Name mangling
    https://en.wikipedia.org/wi­ki/Name_mangling
  15. Pokrytí kódu testy (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Pokryt%C3%AD_k%C3%B3du_tes­ty
  16. Code coverage (Wikipedia)
    https://en.wikipedia.org/wi­ki/Code_coverage
  17. Using the GNU Compiler Collection (GCC)
    https://gcc.gnu.org/online­docs/gcc/index.html#Top
  18. Programming Languages Supported by GCC
    https://gcc.gnu.org/online­docs/gcc/G_002b_002b-and-GCC.html#G_002b_002b-and-GCC
  19. Generating Code Coverage Report Using GNU Gcov & Lcov.
    https://medium.com/@naveen­.maltesh/generating-code-coverage-report-using-gnu-gcov-lcov-ee54a4de3f11
  20. Tree traversal
    https://en.wikipedia.org/wi­ki/Tree_traversal
  21. Tree Traversal Techniques
    https://www.geeksforgeeks.org/dsa/tree-traversals-inorder-preorder-and-postorder/
  22. What is the difference between lcov and gcovr?
    https://www.gcovr.com/en/sta­ble/faq.html

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.