Obsah
1. Detekce neaktivního kódu a analýza pokrytí kódu testy s nástroji gcov, gcovr a lcov
2. Zdrojový kód použitý pro ukázku nástrojů gcovr a lcov
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
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
10. Praktické použití nástroje lcov
11. Reálný příklad: funkce pro manipulaci s rastrovými obrázky
13. Překlad knihovny pro manipulaci s obrázky současně s testy
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
1. Detekce neaktivního kódu a analýza pokrytí kódu testy s nástroji gcov, gcovr a lcov
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.
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ů gcovr a lcov
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;
}
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.
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 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.
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.
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.
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 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.
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.gcno i tree1.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%
------------------------------------------------------------------------------
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.
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ě:
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ů.
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:
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í.
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:
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.io/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.
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:
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;
}
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:
$ 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/slides/blob/master/sources/tree1.c |
| 2 | tree2.c | konstrukce binárního stromu s jediným uzlem; průchod tímto stromem | https://github.com/tisnik/slides/blob/master/sources/tree2.c |
| 3 | tree3.c | konstrukce binárního stromu s více uzly; průchod tímto stromem | https://github.com/tisnik/slides/blob/master/sources/tree3.c |
| 4 | factorial.c | naivní rekurzivní výpočet faktoriálu | https://github.com/tisnik/slides/blob/master/sources/factorial.c |
| 5 | test.c | několik funkcí s různým počtem parametrů, které jsou volány z main | https://github.com/tisnik/slides/blob/master/sources/test.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
- GCC, the GNU Compiler Collection
https://gcc.gnu.org/ - gcovr: online dokumentace
https://gcovr.com/en/stable/ - lcov: online dokumentace
https://lcov.readthedocs.io/en/latest/index.html - gcov manual: Test Coverage Program
https://gcc.gnu.org/onlinedocs/gcc/Gcov.html - How to Analyze Code Coverage with gcov
https://www.linuxtoday.com/blog/analyzing-code-coverage-with-gcov/ - gcov – Unix, Linux Command
https://www.tutorialspoint.com/unix_commands/gcov.htm - Testing code coverage in C using GCOV
https://www.youtube.com/watch?v=UOGMNRcV9–4 - Nástroj objdump: švýcarský nožík pro vývojáře
https://www.root.cz/clanky/nastroj-objdump-svycarsky-nozik-pro-vyvojare/ - What is code coverage?
https://www.atlassian.com/continuous-delivery/software-testing/code-coverage - Everything you need to know about code coverage
https://www.codegrip.tech/productivity/everything-you-need-to-know-about-code-coverage/ - GCC, the GNU Compiler Collection
https://gcc.gnu.org/ - Clang 17.0.0: Source-based Code Coverage
https://clang.llvm.org/docs/SourceBasedCodeCoverage.html - Clang 17.0.0: SanitizerCoverage
https://clang.llvm.org/docs/SanitizerCoverage.html - Name mangling
https://en.wikipedia.org/wiki/Name_mangling - Pokrytí kódu testy (Wikipedia)
https://cs.wikipedia.org/wiki/Pokryt%C3%AD_k%C3%B3du_testy - Code coverage (Wikipedia)
https://en.wikipedia.org/wiki/Code_coverage - Using the GNU Compiler Collection (GCC)
https://gcc.gnu.org/onlinedocs/gcc/index.html#Top - Programming Languages Supported by GCC
https://gcc.gnu.org/onlinedocs/gcc/G_002b_002b-and-GCC.html#G_002b_002b-and-GCC - Generating Code Coverage Report Using GNU Gcov & Lcov.
https://medium.com/@naveen.maltesh/generating-code-coverage-report-using-gnu-gcov-lcov-ee54a4de3f11 - Tree traversal
https://en.wikipedia.org/wiki/Tree_traversal - Tree Traversal Techniques
https://www.geeksforgeeks.org/dsa/tree-traversals-inorder-preorder-and-postorder/ - What is the difference between lcov and gcovr?
https://www.gcovr.com/en/stable/faq.html



