Obsah
1. Analýza zdrojového kódu nástrojem gcov
2. První seznámení s nástrojem gcov
3. Zavolání nástroje gcov z příkazové řádky
4. Jednoduchý program napsaný v C, který budeme zkoumat nástrojem gcov
5. Zjištění živého a mrtvého kódu nástrojem gcov
6. Vygenerování čitelného protokolu s informací o živém a mrtvém kódu
7. Úprava testovacího příkladu: vložení uzlu do binárního stromu
8. Opětovné vygenerování protokolu
9. Úprava testovacího příkladu tak, aby se v runtime zavolaly všechny programové řádky
10. Vygenerování výsledného protokolu
11. Analýza, kolikrát jsou jednotlivé řádky programového kódu volány
12. Vizualizace frekvence volání jednotlivých programových řádků
13. Jak se vlastně interně zjišťuje, které řádky byly zavolány?
14. Překlad několika funkcí z C do assembleru
15. Překlad s použitím přepínačů -fprofile-arcs a -ftest-coverage
16. Podrobnější porovnání obou vygenerovaných strojových kódů
18. Repositář s demonstračními příklady
19. Předchozí články s tématem testování
1. Analýza zdrojového kódu nástrojem gcov
V dnešním článku se seznámíme s velmi užitečným a poměrně často používaným nástrojem nazvaným gcov. Tento nástroj je součástí ekosystému GCC a slouží pro zjištění, které řádky v programovém kódu jsou skutečně volány a které naopak nikoli. Navíc je u volaných řádků 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 věnovali v článcích uvedených v devatenácté kapitole.
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.
2. První seznámení s nástrojem gcov
Ve skutečnosti samotný gcov nedokáže zjistit, které části kódu jsou skutečně volány. V případě gcov se jedná „pouze“ o nástroj analyzující data získaná ze dvou zdrojů a produkující analýzu v čitelném (textovém) formátu. Mezi oba zmíněné zdroje patří:
- První množina dat je vygenerována samotným překladačem. Jedná se o soubor (soubory) obsahující – stručně řečeno – mapování mezi řádky ve zdrojovém kódu a adresami v paměťové oblasti alokované pro běžící program. Tyto soubory jsou binární a jsou přímo určeny pro zpracování nástrojemgcov (tedy neměly by se používat jiným nástrojem, protože jejich struktura se může měnit). Jejich koncovka je typicky .gcno, kde „no“ znamená „notes“.
- Druhá množina dat je vygenerována v čase běhu testovaného programu. Jedná se o soubor (soubory) s čítači přístupů k jednotlivým řádkům. I tyto soubory jsou binární a mají koncovku .gcda, kde „da“ znamená „data“.
Teprve na základě obsahu těchto dvou množin dat dokáže gcov vytvořit čitelný výstup obsahující informace o tom, které řádky kódu byly volány a které nikoli. Výsledek provedené analýzy je přitom možné získat v čistě textové podobě, v podobě obarveného výstupu určeného pro terminál, nebo je možné si nechat výsledek analýzy uložit v podobě vhodné pro další strojové zpracování (například v integrovaném vývojovém prostředí atd.).
3. Zavolání nástroje gcov z příkazové řádky
Nástroj gcov se volá z příkazové řádky příkazem gcov a je mu možné předat poměrně velké množství přepínačů, z nichž některé jsou určeny pro konkrétní programovací jazyk (již tradičně je problematický C++ s jeho komolením jmen funkcí a metod – name mangling).
Všechny přepínače je možné si nechat vypsat:
$ gcov
S výsledkem:
Usage: gcov [OPTION...] SOURCE|OBJ... Print code coverage information. -a, --all-blocks Show information for every basic block -b, --branch-probabilities Include branch probabilities in output -c, --branch-counts Output counts of branches taken rather than percentages -d, --display-progress Display progress information -f, --function-summaries Output summaries for each function -h, --help Print this help, then exit -i, --json-format Output JSON intermediate format into .gcov.json.gz file -j, --human-readable Output human readable numbers -k, --use-colors Emit colored output -l, --long-file-names Use long output file names for included source files -m, --demangled-names Output demangled function names -n, --no-output Do not create an output file -o, --object-directory DIR|FILE Search for object files in DIR or called FILE -p, --preserve-paths Preserve all pathname components -q, --use-hotness-colors Emit perf-like colored output for hot lines -r, --relative-only Only show data for relative sources -s, --source-prefix DIR Source prefix to elide -t, --stdout Output to stdout instead of a file -u, --unconditional-branches Show unconditional branch counts too -v, --version Print version number, then exit -w, --verbose Print verbose informations -x, --hash-filenames Hash long pathnames For bug reporting instructions, please see: <file:///usr/share/doc/gcc-9/README.Bugs>.
4. Jednoduchý program napsaný v C, který budeme zkoumat nástrojem gcov
Základní vlastnosti nástroje gcov si postupně otestujeme na několika demonstračních příkladech. První příklad, který si dnes ukážeme, je psaný v programovacím jazyku C. Najdeme v něm triviální implementaci konstrukce binárního stromu určeného pro uložení řetězců společně s funkcí pro průchod (traverzaci) tímto stromem. Ovšem průchod stromem je (alespoň prozatím) realizován nad prázdným stromem, takže již dopředu můžeme 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; }
5. Zjištění živého a mrtvého kódu nástrojem gcov
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 tree1.c (navíc je více než vhodné nepoužívat optimalizace):
$ gcc -fprofile-arcs -ftest-coverage tree1.c
Výsledkem překladu bude (podle očekávání) soubor a.out a navíc i soubor tree1.gcno obsahující – jak jsme si již ve stručnosti řekli – mapování mezi řá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 28544 Apr 15 12:28 a.out -rw-r--r-- 1 ptisnovs ptisnovs 1111 Apr 13 18:50 tree1.c -rw-rw-r-- 1 ptisnovs ptisnovs 1488 Apr 15 12:28 tree1.gcno
Nyní spustíme testovaný program naprosto stejným způsobem, jako bychom ho spouštěli bez zjišťování volaných řádků:
$ ./a.out
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 28544 Apr 15 12:28 a.out -rw-r--r-- 1 ptisnovs ptisnovs 1111 Apr 13 18:50 tree1.c -rw-rw-r-- 1 ptisnovs ptisnovs 256 Apr 15 12:29 tree1.gcda -rw-rw-r-- 1 ptisnovs ptisnovs 1488 Apr 15 12:28 tree1.gcno
Oba dva výše zmíněné soubory, tedy tree1.gcno a tree1.gcda je nyní nutné sloučit nástrojem gcov.
6. Vygenerování čitelného protokolu s informací o živém a mrtvém kódu
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
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 výsledku patrné:
File 'tree1.c' Lines executed:25.00% of 24 Creating 'tree1.c.gcov'
Navíc je možné zjistit podrobnější informace o jednotlivých funkcích (metodách):
$ gcov -f tree1.c Function 'main' Lines executed:100.00% of 3 Function 'callback_function' Lines executed:0.00% of 3 Function 'traverse_tree' Lines executed:50.00% of 6 Function 'insert_new_node' Lines executed:0.00% of 12 File 'tree1.c' Lines executed:25.00% of 24 Creating 'tree1.c.gcov'
Předchozí příkaz současně vytvořil nový soubor pojmenovaný „tree1.c.gcov“:
$ ls -l total 132 -rwxrwxr-x 1 ptisnovs ptisnovs 28544 Apr 15 12:28 a.out -rw-r--r-- 1 ptisnovs ptisnovs 1111 Apr 13 18:50 tree1.c -rw-rw-r-- 1 ptisnovs ptisnovs 2190 Apr 15 12:30 tree1.c.gcov -rw-rw-r-- 1 ptisnovs ptisnovs 256 Apr 15 12:29 tree1.gcda -rw-rw-r-- 1 ptisnovs ptisnovs 1488 Apr 15 12:28 tree1.gcno
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 nejsou 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:
Alternativně je možné nechat si vygenerovat výstupní soubor, který bude obsahovat původní zdrojový text, ovšem obarvený podle toho, které řádky (příkazy) byly zavolány a které nikoli. V takovém souboru nalezneme řídicí kódy dle ANSI, které by mělo být možné interpretovat na prakticky jakémkoli emulátoru terminálu:
$ gcov -k tree1.c
Výsledný soubor tree1.c.gcov si můžeme zobrazit na terminálu například standardním nástrojem cat:
$ cat tree1.c.gcov
S následujícím výsledkem:
Obrázek 1: Obarvený výstup z nástroje gcov.
7. Úprava testovacího příkladu: vložení uzlu do binárního stromu
Nyní nastal čas na úpravu původního testovacího příkladu takovým způsobem, aby se využila (resp. přesněji řečeno zavolala) větší část jeho kódu. Zejména do binárního stromu vložíme uzel, což (nepřímo) ovlivní i funkci pro procházení (traverzaci) binárním stromem. Upravený příklad vypadá následovně:
#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; insert_new_node(&root, "xxx"); traverse_tree(root, callback_function); return 0; }
8. Opětovné vygenerování protokolu
Opět si můžeme nechat vygenerovat protokol s analýzou zdrojového kódu:
$ gcov tree2.c
Průběh analýzy:
File 'tree2.c' Lines executed:84.00% of 25 Creating 'tree2.c.gcov'
Hodnota s poměrnou „živostí“ kódu je nyní vyšší, takže se podívejme na podrobnější informace rozdělené po jednotlivých funkcích:
$ gcov -f tree2.c
S výsledkem:
Function 'main' Lines executed:100.00% of 4 Function 'callback_function' Lines executed:100.00% of 3 Function 'traverse_tree' Lines executed:100.00% of 6 Function 'insert_new_node' Lines executed:66.67% of 12 File 'tree2.c' Lines executed:84.00% of 25 Creating 'tree2.c.gcov'
Vlastní protokol se změnil, protože ubyly programové řádky označené symbolem „#####“ a naopak je patrné, že některé řádky (resp. přesněji řečeno příkazy na těchto řádcích) jsou zavolány vícekrát, což se týká například rekurzivně volané funkce traverse_tree, jenž je přímo zavolána jednou a dvakrát zavolána v rekurzi:
-: 0:Source:tree2.c -: 0:Graph:tree2.gcno -: 0:Data:tree2.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: 1: 12:void insert_new_node(Node **root, char *value) -: 13:{ -: 14: int cmp; -: 15: 1: 16: if (*root == NULL) -: 17: { 1: 18: *root = (Node *)malloc(sizeof(Node)); 1: 19: (*root)->value = (char*)calloc(strlen(value), sizeof(char)); 1: 20: strcpy((*root)->value, value); 1: 21: (*root)->left = NULL; 1: 22: (*root)->right = NULL; 1: 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: 3: 36:void traverse_tree(Node *root, void (*callback_function)(char *)) -: 37:{ 3: 38: if (root == NULL) -: 39: { 2: 40: return; -: 41: } 1: 42: traverse_tree(root->left, callback_function); 1: 43: callback_function(root->value); 1: 44: traverse_tree(root->right, callback_function); -: 45:} -: 46: 1: 47:void callback_function(char *value) -: 48:{ 1: 49: printf("%s\n", value); 1: 50:} -: 51: 1: 52:int main(void) -: 53:{ -: 54: static Node *root = NULL; -: 55: 1: 56: insert_new_node(&root, "xxx"); -: 57: 1: 58: traverse_tree(root, callback_function); -: 59: 1: 60: return 0; -: 61:} -: 62:
Obrázek 2: Obarvený výstup z nástroje gcov.
9. Úprava testovacího příkladu tak, aby se v runtime zavolaly všechny programové řádky
Další, již poslední úprava testovacího příkladu s implementací naivního binárního stromu spočívá v tom, že do stromu vložíme větší množství uzlů a to navíc v takovém pořadí, aby byly nové poduzly vytvořeny jak napravo od nadřazeného uzlu, tak i nalevo od něj. To vede ke konstrukci binárního stromu, při jehož průchodu se využijí všechny implementované možnosti:
#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; insert_new_node(&root, "xxx"); insert_new_node(&root, "aaa"); insert_new_node(&root, "bbb"); insert_new_node(&root, "ccc"); insert_new_node(&root, "yyy"); insert_new_node(&root, "yyy"); traverse_tree(root, callback_function); return 0; }
10. Vygenerování výsledného protokolu
Vygenerování výsledného protokolu provedeme nám již známým postupem, a to buď bez zobrazení využití jednotlivých funkcí, nebo s podrobnějším výpisem:
$ gcov tree3.c
Průběh analýzy:
File 'tree3.c' Lines executed:100.00% of 30 Creating 'tree3.c.gcov'
Analýza naznačila, že jsou pokryty všechny řádky, což si ještě ověříme tak, že zjistíme pokrytí jednotlivých funkcí:
$ gcov -f tree3.c Function 'main' Lines executed:100.00% of 9 Function 'callback_function' Lines executed:100.00% of 3 Function 'traverse_tree' Lines executed:100.00% of 6 Function 'insert_new_node' Lines executed:100.00% of 12 File 'tree3.c' Lines executed:100.00% of 30 Creating 'tree3.c.gcov'
A takto bude vypadat výsledek analýzy:
-: 0:Source:tree3.c -: 0:Graph:tree3.gcno -: 0:Data:tree3.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: 15: 12:void insert_new_node(Node **root, char *value) -: 13:{ -: 14: int cmp; -: 15: 15: 16: if (*root == NULL) -: 17: { 6: 18: *root = (Node *)malloc(sizeof(Node)); 6: 19: (*root)->value = (char*)calloc(strlen(value), sizeof(char)); 6: 20: strcpy((*root)->value, value); 6: 21: (*root)->left = NULL; 6: 22: (*root)->right = NULL; 6: 23: return; -: 24: } 9: 25: cmp = strcmp(value, (*root)->value); 9: 26: if (cmp < 0) -: 27: { 3: 28: insert_new_node(&(*root)->left, value); -: 29: } -: 30: else -: 31: { 6: 32: insert_new_node(&(*root)->right, value); -: 33: } -: 34:} -: 35: 13: 36:void traverse_tree(Node *root, void (*callback_function)(char *)) -: 37:{ 13: 38: if (root == NULL) -: 39: { 7: 40: return; -: 41: } 6: 42: traverse_tree(root->left, callback_function); 6: 43: callback_function(root->value); 6: 44: traverse_tree(root->right, callback_function); -: 45:} -: 46: 6: 47:void callback_function(char *value) -: 48:{ 6: 49: printf("%s\n", value); 6: 50:} -: 51: 1: 52:int main(void) -: 53:{ -: 54: static Node *root = NULL; -: 55: 1: 56: insert_new_node(&root, "xxx"); 1: 57: insert_new_node(&root, "aaa"); 1: 58: insert_new_node(&root, "bbb"); 1: 59: insert_new_node(&root, "ccc"); 1: 60: insert_new_node(&root, "yyy"); 1: 61: insert_new_node(&root, "yyy"); -: 62: 1: 63: traverse_tree(root, callback_function); -: 64: 1: 65: return 0; -: 66:} -: 67:
11. Analýza, kolikrát jsou jednotlivé řádky programového kódu volány
Mnohdy je zapotřebí zjistit nejenom to, které části kódu byly volány a které nikoli, ale i informaci o tom, kolikrát byla funkce, blok či jednotlivý programový řádek volán. To je sice primárně úloha jiných nástrojů (například nástroje gprof, pokud zůstaneme v ekosystému GCC), ale základní a především velmi přehledně podané informace nám dá i nástroj gcov (jenž ovšem nezjistí časy vykonání ani kumulativní časy!). Vše si otestujeme na tomto jednoduchém a naivním rekurzivním výpočtu faktoriálu:
#include <stdio.h> long factorial(long n) { if (n<=1) { return 1; } return n*factorial(n-1); } int main(int argc, char **argv) { printf("%ld\n", factorial(20)); return 0; }
12. Vizualizace frekvence volání jednotlivých programových řádků
Po spuštění výpočtu faktoriálu si opět můžeme nechat vygenerovat protokol s informacemi o řádcích kódu, které byly volány a které nikoli. U volaných řádků se podle očekávání vypíše i celkový počet volání:
$ gcov factorial.c File 'factorial.c' Lines executed:100.00% of 7 Creating 'factorial.c.gcov'
Výsledek bude vypadat následovně:
-: 0:Source:factorial.c -: 0:Graph:factorial.gcno -: 0:Data:factorial.gcda -: 0:Runs:1 -: 1:#include <stdio.h> -: 2: 20: 3:long factorial(long n) -: 4:{ 20: 5: if (n<=1) { 1: 6: return 1; -: 7: } 19: 8: return n*factorial(n-1); -: 9:} -: 10: 1: 11:int main(int argc, char **argv) -: 12:{ 1: 13: printf("%ld\n", factorial(20)); 1: 14: return 0; -: 15:} -: 16:
Kromě tohoto výstupu však můžeme použít i přepínač -q, jenž zajistí určitou formu vizualizace – volané řádky jsou obarveny jednou ze tří barev; viz zobrazená legenda v pravém horním rohu:
$ gcov -q factorial.c File 'factorial.c' Lines executed:100.00% of 7 Creating 'factorial.c.gcov'
Tento výstup je možné v případě potřeby kombinovat s „barvovým“ výstupem, jenž již známe – s přepínačem -k:
$ gcov -k -q factorial.c File 'factorial.c' Lines executed:100.00% of 7 Creating 'factorial.c.gcov'
Výsledek může vypadat následovně:
Obrázek 3: Vizuální zvýraznění programových řádků podle frekvence jejich volání (získáno pro jinou vstupní hodnotu pro výpočet faktoriálu). Nyní jsou červeně označené řádky označují kód, který tvoří více než polovinu všech volání za celou dobu běhu aplikace (a profiler by nám navíc zobrazil, jak dlouhou dobu program strávil v těchto blocích).
13. Jak se vlastně interně zjišťuje, které řádky byly zavolány?
Na tomto místě se pravděpodobně čtenář může ptát, jakým způsobem se vlastně vytváří soubory .gcda. Připomeňme si, že se jedná o soubory obsahující informace o „zásazích“ řádků zdrojového kódu, které jsou získány v době běhu testovaného programu, tedy v runtime. Co to ale ve skutečnosti znamená? Překladač musel do výsledného strojového kódu vložit další instrukce, které nějakým způsobem zajišťují získání informací o oněch „zásazích“. V navazujících kapitolách si ukážeme, jak tyto instrukce reálně vypadají, resp. přesněji řečeno jak se liší původní strojový kód od strojového kódu, který zajišťuje vygenerování souborů .gcda.
14. Překlad několika funkcí z C do assembleru
Nejprve si ukažme zdrojový kód napsaný v jazyku C, který obsahuje deklaraci několika jednoduchých funkcí. Tyto funkce akceptují různý počet parametrů a všechny jsou volány z funkce main, čímž je zajištěno, že je překladač neodstraní jako nepoužitý kód:
int zero(void) { return 0; } int inc(int x) { return x+1; } int add(int x, int y) { return x+y; } int foo(int x) { int r = 0; while (x--) { r++; } return r; } int main(void) { int a = zero(); int b = inc(a); int c = add(a, b); int d = foo(c); return c; }
Překlad tohoto programu provedeme s přepínačem -g, kterým je zajištěno vložení ladicích informací do výsledného binárního spustitelného kódu:
gcc -g test.c
Nyní zjistíme, jakým způsobem byly výše zmíněné funkce přeloženy do assembleru (a dále do strojového kódu). Pro zpětný překlad přitom využijeme nástroj objdump, kterému jsme se již na stránkách Roota věnovali:
objdump -drw -Mintel -S a.out > normal.asm
0000000000001129 <zero>: int zero(void) { 1129: f3 0f 1e fa endbr64 112d: 55 push rbp 112e: 48 89 e5 mov rbp,rsp return 0; 1131: b8 00 00 00 00 mov eax,0x0 } 1136: 5d pop rbp 1137: c3 ret 0000000000001138 <inc>: int inc(int x) { 1138: f3 0f 1e fa endbr64 113c: 55 push rbp 113d: 48 89 e5 mov rbp,rsp 1140: 89 7d fc mov DWORD PTR [rbp-0x4],edi return x+1; 1143: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 1146: 83 c0 01 add eax,0x1 } 1149: 5d pop rbp 114a: c3 ret 000000000000114b <add>: int add(int x, int y) { 114b: f3 0f 1e fa endbr64 114f: 55 push rbp 1150: 48 89 e5 mov rbp,rsp 1153: 89 7d fc mov DWORD PTR [rbp-0x4],edi 1156: 89 75 f8 mov DWORD PTR [rbp-0x8],esi return x+y; 1159: 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 115c: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 115f: 01 d0 add eax,edx } 1161: 5d pop rbp 1162: c3 ret 0000000000001163 <foo>: int foo(int x) { 1163: f3 0f 1e fa endbr64 1167: 55 push rbp 1168: 48 89 e5 mov rbp,rsp 116b: 89 7d ec mov DWORD PTR [rbp-0x14],edi int r = 0; 116e: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 while (x--) { 1175: eb 04 jmp 117b <foo+0x18> r++; 1177: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 while (x--) { 117b: 8b 45 ec mov eax,DWORD PTR [rbp-0x14] 117e: 8d 50 ff lea edx,[rax-0x1] 1181: 89 55 ec mov DWORD PTR [rbp-0x14],edx 1184: 85 c0 test eax,eax 1186: 75 ef jne 1177 <foo+0x14> } return r; 1188: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] } 118b: 5d pop rbp 118c: c3 ret
15. Překlad s použitím přepínačů -fprofile-arcs a -ftest-coverage
Ve druhém kroku přeložíme stejný zdrojový kód, ovšem nyní při překladu použijeme i přepínače -fprofile-arcs a -ftest-coverage:
gcc -g -fprofile-arcs -ftest-coverage test.c
Opět se podíváme na způsob překladu všech výše uvedených funkcí do assembleru a strojového kódu:
objdump -drw -Mintel -S a.out > coverage.asm
Tentokrát ovšem bude sekvence instrukcí odlišná. Přidané instrukce jsou pro větší přehlednost zvýrazněny:
00000000000014b9 <zero>: int zero(void) { 14b9: f3 0f 1e fa endbr64 14bd: 55 push rbp 14be: 48 89 e5 mov rbp,rsp 14c1: 48 8b 05 e8 3d 00 00 mov rax,QWORD PTR [rip+0x3de8] # 52b0 <__gcov0.zero> 14c8: 48 83 c0 01 add rax,0x1 14cc: 48 89 05 dd 3d 00 00 mov QWORD PTR [rip+0x3ddd],rax # 52b0 <__gcov0.zero> return 0; 14d3: b8 00 00 00 00 mov eax,0x0 } 14d8: 5d pop rbp 14d9: c3 ret 00000000000014da <inc>: int inc(int x) { 14da: f3 0f 1e fa endbr64 14de: 55 push rbp 14df: 48 89 e5 mov rbp,rsp 14e2: 89 7d fc mov DWORD PTR [rbp-0x4],edi 14e5: 48 8b 05 bc 3d 00 00 mov rax,QWORD PTR [rip+0x3dbc] # 52a8 <__gcov0.inc> 14ec: 48 83 c0 01 add rax,0x1 14f0: 48 89 05 b1 3d 00 00 mov QWORD PTR [rip+0x3db1],rax # 52a8 <__gcov0.inc> return x+1; 14f7: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 14fa: 83 c0 01 add eax,0x1 } 14fd: 5d pop rbp 14fe: c3 ret 00000000000014ff <add>: int add(int x, int y) { 14ff: f3 0f 1e fa endbr64 1503: 55 push rbp 1504: 48 89 e5 mov rbp,rsp 1507: 89 7d fc mov DWORD PTR [rbp-0x4],edi 150a: 89 75 f8 mov DWORD PTR [rbp-0x8],esi 150d: 48 8b 05 8c 3d 00 00 mov rax,QWORD PTR [rip+0x3d8c] # 52a0 <__gcov0.add> 1514: 48 83 c0 01 add rax,0x1 1518: 48 89 05 81 3d 00 00 mov QWORD PTR [rip+0x3d81],rax # 52a0 <__gcov0.add> return x+y; 151f: 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 1522: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 1525: 01 d0 add eax,edx } 1527: 5d pop rbp 1528: c3 ret 0000000000001529 <foo>: int foo(int x) { 1529: f3 0f 1e fa endbr64 152d: 55 push rbp 152e: 48 89 e5 mov rbp,rsp 1531: 89 7d ec mov DWORD PTR [rbp-0x14],edi 1534: 48 8b 05 55 3d 00 00 mov rax,QWORD PTR [rip+0x3d55] # 5290 <__gcov0.foo> 153b: 48 83 c0 01 add rax,0x1 153f: 48 89 05 4a 3d 00 00 mov QWORD PTR [rip+0x3d4a],rax # 5290 <__gcov0.foo> int r = 0; 1546: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 while (x--) { 154d: eb 16 jmp 1565 <foo+0x3c> r++; 154f: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 1553: 48 8b 05 3e 3d 00 00 mov rax,QWORD PTR [rip+0x3d3e] # 5298 <__gcov0.foo+0x8> 155a: 48 83 c0 01 add rax,0x1 155e: 48 89 05 33 3d 00 00 mov QWORD PTR [rip+0x3d33],rax # 5298 <__gcov0.foo+0x8> while (x--) { 1565: 8b 45 ec mov eax,DWORD PTR [rbp-0x14] 1568: 8d 50 ff lea edx,[rax-0x1] 156b: 89 55 ec mov DWORD PTR [rbp-0x14],edx 156e: 85 c0 test eax,eax 1570: 75 dd jne 154f <foo+0x26> } return r; 1572: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] } 1575: 5d pop rbp 1576: c3 ret
Ze zvýrazněných částí kódů je patrné, že byly přidány instrukce určené pro zvýšení obsahu čítačů uložených v operační paměti. A tyto instrukce nejsou přidány pouze jedenkrát do každé funkce, ale navíc i do každého bloku, tedy i do těla programové smyčky. Pro krátké funkce a bloky tedy kód může podstatným způsobem narůst.
16. Podrobnější porovnání obou vygenerovaných strojových kódů
Abychom mohli obě varianty strojových kódů vzniklých kompilací a slinkováním porovnat, odstraníme všechny jeho „pohyblivé“ části, zejména tedy absolutní adresy jednotlivých instrukcí. Kód bez instrukcí přidaných pro gcov bude po ruční úpravě vypadat takto:
int zero(void) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp return 0; b8 00 00 00 00 mov eax,0x0 } 5d pop rbp c3 ret int inc(int x) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp 89 7d fc mov DWORD PTR [rbp-0x4],edi return x+1; 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 83 c0 01 add eax,0x1 } 5d pop rbp c3 ret int add(int x, int y) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp 89 7d fc mov DWORD PTR [rbp-0x4],edi 89 75 f8 mov DWORD PTR [rbp-0x8],esi return x+y; 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 01 d0 add eax,edx } 5d pop rbp c3 ret int foo(int x) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp 89 7d ec mov DWORD PTR [rbp-0x14],edi int r = 0; c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 while (x--) { eb 04 jmp 117b <foo+0x18> r++; 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 while (x--) { 8b 45 ec mov eax,DWORD PTR [rbp-0x14] 8d 50 ff lea edx,[rax-0x1] 89 55 ec mov DWORD PTR [rbp-0x14],edx 85 c0 test eax,eax 75 ef jne 1177 <foo+0x14> } return r; 8b 45 fc mov eax,DWORD PTR [rbp-0x4] } 5d pop rbp c3 ret
Kód s instrukcemi přidanými pro účely nástroje gcov:
int zero(void) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp 48 8b 05 e8 3d 00 00 mov rax,QWORD PTR [rip+0x3de8] # 52b0 <__gcov0.zero> 48 83 c0 01 add rax,0x1 48 89 05 dd 3d 00 00 mov QWORD PTR [rip+0x3ddd],rax # 52b0 <__gcov0.zero> return 0; b8 00 00 00 00 mov eax,0x0 } 5d pop rbp c3 ret int inc(int x) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp 89 7d fc mov DWORD PTR [rbp-0x4],edi 48 8b 05 bc 3d 00 00 mov rax,QWORD PTR [rip+0x3dbc] # 52a8 <__gcov0.inc> 48 83 c0 01 add rax,0x1 48 89 05 b1 3d 00 00 mov QWORD PTR [rip+0x3db1],rax # 52a8 <__gcov0.inc> return x+1; 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 83 c0 01 add eax,0x1 } 5d pop rbp c3 ret int add(int x, int y) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp 89 7d fc mov DWORD PTR [rbp-0x4],edi 89 75 f8 mov DWORD PTR [rbp-0x8],esi 48 8b 05 8c 3d 00 00 mov rax,QWORD PTR [rip+0x3d8c] # 52a0 <__gcov0.add> 48 83 c0 01 add rax,0x1 48 89 05 81 3d 00 00 mov QWORD PTR [rip+0x3d81],rax # 52a0 <__gcov0.add> return x+y; 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 01 d0 add eax,edx } 5d pop rbp c3 ret int foo(int x) { f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp 89 7d ec mov DWORD PTR [rbp-0x14],edi 48 8b 05 55 3d 00 00 mov rax,QWORD PTR [rip+0x3d55] # 5290 <__gcov0.foo> 48 83 c0 01 add rax,0x1 48 89 05 4a 3d 00 00 mov QWORD PTR [rip+0x3d4a],rax # 5290 <__gcov0.foo> int r = 0; c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 while (x--) { eb 16 jmp 1565 <foo+0x3c> r++; 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 48 8b 05 3e 3d 00 00 mov rax,QWORD PTR [rip+0x3d3e] # 5298 <__gcov0.foo+0x8> 48 83 c0 01 add rax,0x1 48 89 05 33 3d 00 00 mov QWORD PTR [rip+0x3d33],rax # 5298 <__gcov0.foo+0x8> while (x--) { 8b 45 ec mov eax,DWORD PTR [rbp-0x14] 8d 50 ff lea edx,[rax-0x1] 89 55 ec mov DWORD PTR [rbp-0x14],edx 85 c0 test eax,eax 75 dd jne 154f <foo+0x26> } return r; 8b 45 fc mov eax,DWORD PTR [rbp-0x4] } 5d pop rbp c3 ret
Textové porovnání jasně ukazuje, že samotný strojový kód zůstal nezměněn, pouze do něj byly (mechanicky) vloženy úpravy čítačů, jejichž hodnoty jsou na konci uloženy do souboru .gcda (a tím došlo k posunutí adres instrukcí, teoreticky se rozšíří šířka relativních skoků atd.):
--- normal.asm 2023-04-14 14:33:34.712189422 +0200 +++ coverage.asm 2023-04-14 14:33:45.540229057 +0200 @@ -3,6 +3,9 @@ f3 0f 1e fa endbr64 55 push rbp 48 89 e5 mov rbp,rsp + 48 8b 05 e8 3d 00 00 mov rax,QWORD PTR [rip+0x3de8] # 52b0 <__gcov0.zero> + 48 83 c0 01 add rax,0x1 + 48 89 05 dd 3d 00 00 mov QWORD PTR [rip+0x3ddd],rax # 52b0 <__gcov0.zero> return 0; b8 00 00 00 00 mov eax,0x0 } @@ -16,6 +19,9 @@ 55 push rbp 48 89 e5 mov rbp,rsp 89 7d fc mov DWORD PTR [rbp-0x4],edi + 48 8b 05 bc 3d 00 00 mov rax,QWORD PTR [rip+0x3dbc] # 52a8 <__gcov0.inc> + 48 83 c0 01 add rax,0x1 + 48 89 05 b1 3d 00 00 mov QWORD PTR [rip+0x3db1],rax # 52a8 <__gcov0.inc> return x+1; 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 83 c0 01 add eax,0x1 @@ -31,6 +37,9 @@ 48 89 e5 mov rbp,rsp 89 7d fc mov DWORD PTR [rbp-0x4],edi 89 75 f8 mov DWORD PTR [rbp-0x8],esi + 48 8b 05 8c 3d 00 00 mov rax,QWORD PTR [rip+0x3d8c] # 52a0 <__gcov0.add> + 48 83 c0 01 add rax,0x1 + 48 89 05 81 3d 00 00 mov QWORD PTR [rip+0x3d81],rax # 52a0 <__gcov0.add> return x+y; 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] @@ -46,18 +55,24 @@ 55 push rbp 48 89 e5 mov rbp,rsp 89 7d ec mov DWORD PTR [rbp-0x14],edi + 48 8b 05 55 3d 00 00 mov rax,QWORD PTR [rip+0x3d55] # 5290 <__gcov0.foo> + 48 83 c0 01 add rax,0x1 + 48 89 05 4a 3d 00 00 mov QWORD PTR [rip+0x3d4a],rax # 5290 <__gcov0.foo> int r = 0; c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 while (x--) { - eb 04 jmp 117b <foo+0x18> + eb 16 jmp 1565 <foo+0x3c> r++; 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 + 48 8b 05 3e 3d 00 00 mov rax,QWORD PTR [rip+0x3d3e] # 5298 <__gcov0.foo+0x8> + 48 83 c0 01 add rax,0x1 + 48 89 05 33 3d 00 00 mov QWORD PTR [rip+0x3d33],rax # 5298 <__gcov0.foo+0x8> while (x--) { 8b 45 ec mov eax,DWORD PTR [rbp-0x14] 8d 50 ff lea edx,[rax-0x1] 89 55 ec mov DWORD PTR [rbp-0x14],edx 85 c0 test eax,eax - 75 ef jne 1177 <foo+0x14> + 75 dd jne 154f <foo+0x26> } return r; 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
Na rozdíl se můžeme podívat i ve vizuální podobě:
Obrázek 4: Rozdíl v obou strojových kódech vzniklých překladem.
17. Nástroj gcov a Clang
Nástroj gcov je sice primárně určen pro ekosystém GCC, ale lze ho využít i v ekosystému LLVM, konkrétně s Clangem. Pro překlad programu do podoby, která po svém spuštění vytvoří výstup kompatibilní s gcov se používají přepínače -ftest-coverage popř. –coverage. Ovšem nativně Clang (a vlastně i celé LLVM) namísto nástroje gcov používá SanitizerCoverage (viz též https://clang.llvm.org/docs/SanitizerCoverage.html). Příslušný přepínač předávaný překladači vypadá takto: -fsanitize-coverage=trace-pc-guard.
18. Repositář 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/. 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 |
19. Předchozí články s tématem testování
Tématem testování jsme se již na stránkách Rootu několikrát zabývali. Jedná se mj. o následující články:
- Použití Pythonu pro tvorbu testů: od jednotkových testů až po testy UI
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-od-jednotkovych-testu-az-po-testy-ui/ - Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-pouziti-tridy-mock-z-knihovny-unittest-mock/ - Použití nástroje pytest pro tvorbu jednotkových testů a benchmarků
https://www.root.cz/clanky/pouziti-nastroje-pytest-pro-tvorbu-jednotkovych-testu-a-benchmarku/ - Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů
https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-fixtures-vyjimky-parametrizace-testu/ - Nástroj pytest a jednotkové testy: životní cyklus testů, užitečné tipy a triky
https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-zivotni-cyklus-testu-uzitecne-tipy-a-triky/ - Struktura projektů s jednotkovými testy, využití Travis CI
https://www.root.cz/clanky/struktura-projektu-s-jednotkovymi-testy-vyuziti-travis-ci/ - Omezení stavového prostoru testovaných funkcí a metod
https://www.root.cz/clanky/omezeni-stavoveho-prostoru-testovanych-funkci-a-metod/ - Testování aplikací s využitím nástroje Hypothesis
https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis/ - Testování aplikací s využitím nástroje Hypothesis (dokončení)
https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis-dokonceni/ - Testování webových aplikací s REST API z Pythonu
https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu/ - Testování webových aplikací s REST API z Pythonu (2)
https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu-2/ - Testování webových aplikací s REST API z Pythonu (3)
https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu-3/ - Behavior-driven development v Pythonu s využitím knihovny Behave
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave/ - Behavior-driven development v Pythonu s využitím knihovny Behave (druhá část)
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-druha-cast/ - Behavior-driven development v Pythonu s využitím knihovny Behave (závěrečná část)
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-zaverecna-cast/ - Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-pomoci-knihoven-schemagic-a-schema/ - Validace datových struktur v Pythonu (2. část)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/ - Validace datových struktur v Pythonu (dokončení)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/ - Univerzální testovací nástroj Robot Framework
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/ - Univerzální testovací nástroj Robot Framework a BDD testy
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/ - Úvod do problematiky fuzzingu a fuzz testování
https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/ - Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru
https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani-slozeni-vlastniho-fuzzeru/ - Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure
https://www.root.cz/clanky/knihovny-a-moduly-usnadnujici-testovani-aplikaci-naprogramovanych-v-jazyce-clojure/ - Validace dat s využitím knihovny spec v Clojure 1.9.0
https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/ - Testování aplikací naprogramovaných v jazyce Go
https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/ - Knihovny určené pro tvorbu testů v programovacím jazyce Go
https://www.root.cz/clanky/knihovny-urcene-pro-tvorbu-testu-v-programovacim-jazyce-go/ - Testování aplikací psaných v Go s využitím knihoven Goblin a Frisby
https://www.root.cz/clanky/testovani-aplikaci-psanych-v-go-s-vyuzitim-knihoven-goblin-a-frisby/ - Testování Go aplikací s využitím knihovny GΩmega a frameworku Ginkgo
https://www.root.cz/clanky/testovani-go-aplikaci-s-vyuzitim-knihovny-gomega-mega-a-frameworku-ginkgo/ - Tvorba BDD testů s využitím jazyka Go a nástroje godog
https://www.root.cz/clanky/tvorba-bdd-testu-s-vyuzitim-jazyka-go-a-nastroje-godog/ - Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem
https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem/ - Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem (dokončení)
https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem-dokonceni/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure-2/
20. Odkazy na Internetu
- 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