GITLAB mit Tests und Code Coverage

Gitlab wird als Quellcode Verwaltungssystem in vielen Firmen eingesetzt, auch in denen für die ich gearbeitet habe und arbeite.

Gitlab ist simpel … und deshalb sehr kompliziert.
Den jeder Anwender kann sich seinen eigenen Weg durch einen Berg von Funktionen graben und deshalb sieht jeder Tunnel dann etwas anders aus.

Und eine interessante Herausforderung ist das Sammeln von Code-Coverage Daten in C++.


CI Pipelines und Jobs

Das Continuous Integration Feature (kurz CI) gestatt es automatisiert “Operationen” bei Bestimmten Ereignissen auszuführen.
Der Klassiker ist, dass man bei jedem Commit in einem GIT Repository dann eine Build-Pipeline startet, die den neuen Code kompiliert und gegebenenfalls abtestet.

Eine Pipeline besteht dann aus einer oder mehreren “Stages” mit den größeren Teilschritten und in jeder Stage laufen dann die eigentlichen “Jobs”, z.B.: kann jeder “Job” eine Plattform wie Windows oder Linux abdecken oder für unterschiedliche Testframeworks.

Ein mir vertrauter einfacher Ablauf solcher Pipeline “Stages” ist:

  • Build
    Alle Sourcen werden kompiliert
  • Test
    Unit Tests werden ausgeführt um Test und Code-Coverage Berichte zu erstellen
  • Deploy
    Fertige Binaries werden irgendwo hin hochgeladen, wo sie direkt eingesetzt werden oder auf einen Download warten.

Report Formate

Unit test Frameworks können ihre Ergebnisse in unterschiedlichen Formaten ausgeben, was man meist über Commandline-Switches einstellen kann.
Zwei recht bekannte Formate sind:

  • JUnit für das Zusammenfassen von Tests
  • Cobertura für das Sammeln von Code-Coverage Informationen

JUnit Test Reports

JUnit und Cobertura entstanden ursprünglich von Java Projekten, sie wurde aber von vielen Sprachen genutzt um eigene Tests standardisiert auszugeben.
Das C++ Framework BOOST unterstützt in seinen boost/test Klassen auch JUnit und mit eben diesem durfte ich die letzten Jahre arbeiten.

Der Test:

 1#define BOOST_AUTO_TEST_MAIN
 2#define BOOST_TEST_MODULE test_my_code_module
 3
 4#include <boost/test/auto_unit_test.hpp>
 5
 6BOOST_AUTO_TEST_SUITE(my_test_suite)
 7
 8BOOST_AUTO_TEST_CASE(my_test_case)
 9{
10  throw 1;
11}
12
13BOOST_AUTO_TEST_SUITE_END()

führt dann zu sowas wie:

 1<testsuite tests="5" skipped="0" errors="1" failures="0" id="0" name="test_my_code_module" time="0.130668">
 2  <testcase classname="my_test_suite" name="my_test_case">
 3    <error message="unexpected exception" type="system error">
 4      <![CDATA[
 5        UNCAUGHT EXCEPTION ...
 6      ]]>
 7    </error>
 8  </testcase>
 9</testsuite>

Ein Gitlab CI Job in gitlab_ci.yml, der für einen Boost-Test in etwa so aussieht:

 1test_job:
 2  script:
 3    - mkdir ${CI_PROJECT_DIR}/test_results
 4    - > 
 5      path/to/test_my_code_module 
 6      --report_sink=${CI_PROJECT_DIR}/test_results/my_test_results_sumary.txt
 7      --logger=JUNIT,all,${CI_PROJECT_DIR}/test_results/my_test_results.xml
 8    - echo "Done"
 9  artifacts:
10    paths:
11      - ${CI_PROJECT_DIR}/test_results/*
12    reports:
13      junit: ${CI_PROJECT_DIR}/test_results/my_test_results.xml

Lädt die Ergebnisse dann zum Gitlab-Coordinator hoch und der kann dann die my_test_results.xml sofort parsen und im Web-UI schön anzeigen.

Code Coverage

Bei der Code Coverage (oder auch Test-Coverage) wird im Sourcecode mitgezählt, welche Zeilen ausgeführt wurden und welche nicht. Am Ende wissen wir, ob unsere Tests sinnvoll waren und alles abgetestet haben, oder viele Codezeilen nie angesprungen und damit ungetestet verblieben sind.

Je nach Umgebung muss man den Sourcecode speziell kompilieren und mit Hilfsprogrammen die Coverage errechnen lassen.

Beim GCC kann man die Sourcen mit --coverage kompilieren womit bei der Ausführung .gcno und .gcda automatisch erstellt werden. Tools wie gcovr können diese Dateien dann in das Cobertura-XML Format umwandeln.

1gcovr --xml-pretty -o coverage.xml --root path/to/source

Unter Windows kann z.B.: OpenCppCoverage ein Debug-Binary ausführen und misst dessen Ausführung per Debugger mit.

1OpenCppCoverage.exe --source path/to/sources --export_type cobertura:coverage.xml -- test.exe

Doch leider reicht es jetzt nicht ganz aus, dieses Ergebnis einfach zum Gitlab Coordinator hochzuladen mit:

 1test_job:
 2  script:
 3    - ...
 4  artifacts:
 5    paths:
 6      - ${CI_PROJECT_DIR}/test_results/*
 7    reports:
 8      coverage_report:
 9        coverage_format: cobertura
10        path: ${CI_PROJECT_DIR}/test_results/coverage.xml

Denn um die % Zahl der Coverage registriert zu bekommen, nutzt Gitlab nicht die XML-Datei (die wird nur bei Mergerequests herangezogen).
Gitlab erwartet, dass das Ergebnis auf die Konsole geschrieben wird, und dass man es von dort per Regular-Expression auslesen kann.

Bei gcovr muss man noch den Commandline-Switch --print-summary anfügen, damit der Output so aussieht:

1gcovr --xml-pretty --print-summary -o coverage.xml --root /path/to/src
2lines: 85.0% (425 out of 500)
3branches: 30.0% (17106 out of 56994)

Mit dem coverage Zusatz im Gitlab Job wird die lines Zeile extrahiert:

 1test_job:
 2  script:
 3    - ...
 4  coverage: /^\s*lines:\s*\d+.\d+\%/
 5  artifacts:
 6    paths:
 7      - ${CI_PROJECT_DIR}/test_results/*
 8    reports:
 9      coverage_report:
10        coverage_format: cobertura
11        path: ${CI_PROJECT_DIR}/test_results/coverage.xml

Fazit

Am Ende hat man dann Test-Reports und Coverage-Reports, die man im Gitlab Web-UI anzeigen und per Rest-API abholen kann.

Durch Gitlab-Badges lässt sich die Coverage-Zahl auch direkt auf der Projektseite anzeigen und diese kleinen SVG-Bilder lassen sich auch hervorragend in andere Gitlab-Seiten einbauen um einen Überblick zu erhalten.