CONAN Builds mit CTest

Tests gehören zum Build-Vorgang fix dazu. Besonders wenn daraus ein Conan Paket werden soll.

Es macht also Sinn einen CTest Lauf nach jedem CMake-Build auszuführen.


Einer der größeren Fehler, die mir und meinen Kollegen beruflich passiert ist, war die fälschliche Nutzung von Conan Test-Packages für Unit-Tests.
Test-Packages sind (wie in der Doku beschreiben) nur dafür da, die Pakete und ihren Inhalt auf Anwesenheit zu testen, und nicht um komplexe interne Funktionstests auszuführen.

Hier wurde viel herumgescriptet um im Endeffekt eine suboptimale Testreihe auszuführen.
Die Lösung wäre so nahe gelegen … aber wir konnten sie alle nicht sehen: cmake bzw ctest

CTest

Der größte Vorteil von CTest ist, dass sich Tests als Targets in den CMake Baum einfügen, eine von außen abrufbare Schnittstelle haben und daher von IDEs direkt oder indirekt unterstützt werden.

Ein Test ist in C/C++ in der Regel ein eigener Prozess, der mitgebaut wird, aber nicht per cmake --install weiterverteilt wird. Jedoch kann er durch den Aufruf von ctest im Build-Verzeichnis ausgeführt werden und sein Ergebnis wird per Exit-Code als Erfolg oder Misserfolg in einen Bericht eingefügt.

So entstehen JUnit XML Dateien, die wiederum das Test-Ergebnis an Kontrollsysteme melden können.

Tests werden aktiviert, wenn man in der Haupt-CMakeLists.txt-Datei die Zeilen

1include(CTest)
2enable_testing()

einfügt.
Für jedes Testprogramm ist dann noch ein eigenes Target notwendig, das als Test registriert wird:

1project(mytest)
2
3file(GLOB my_sources "*.h" "*.c" "*.cpp")
4add_executable(${PROJECT_NAME} ${my_sources})
5
6add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})

Idealerweise nutzt man dann noch ein fertiges Test-Framework wie boost::test oder Google gtest, welch per Standard den Exit-Code auf 0 oder !=0 im Erfolgs- und Fehlerfall setzen.

Das CONAN-Dependency-Problem

Tja, und wenn man jetzt sein CONAN-Paket erstellt, sieht der Build+Test Vorgang etwa so aus:

1def build(self):
2  cmake = CMake(self)
3  cmake.configure()
4  cmake.build()
5  cmake.ctest()

Das bedeutet, dass in einem conan create oder conan build Prozess auch alles funktioniert, da CONAN hier die Umgebungsvariablen so setzt, dass Abhängigkeiten im CONAN-Cache per PATH oder LD_LIBRARY_PATH gefunden werden.

Doch wer nur conan install einsetzt und dann seinen Code z.B. in Visual Studio Code ausführen und debuggen will, dem fehlen nun diverse .dll/.so Pfade.
Bibliotheken die direkt gelinkt werden haben noch eine “gute Chance” gefunden zu werden, aber alles was zur Laufzeit geladen wird wie z.B. Plugins oder optionale Komponenten finden mit LoadLibrary() oder dl_open() ihr Ziel im Conan Cache nicht.

Meine Lösung für dieses Problem war die Erzeugung einer .env Environment Datei, die mit allen Pfaden von bekannten Abhängigkeiten befüllt war. Eine solche Datei konnte dann auch in CMakeLists.txt der Test-Ausführung zugeführt werden um somit die Tests mit dem vollständigen Environment auszuführen.

 1def gen_env_file_with_conan_deps(self, envfile_path: str):
 2  # init with system-defined values
 3  all_bin_paths = os.getenv('PATH', '').split(os.pathsep)
 4  all_lib_paths = os.getenv('LD_LIBRARY_PATH', '').split(os.pathsep)
 5
 6  # add conan dependencies
 7  for dep in self.dependencies.values():
 8    for lib in dep.cpp_info.libdirs:
 9      add_lib_paths.append(str(lib))
10    for bin in dep.cpp_info.bindirs:
11       add_bin_paths.append(str(bin))
12
13  with open(envfile_path, 'w') as f:
14    f.write('PATH={}'.format( os.pathsep.join(add_bin_paths)))
15    f.write('LD_LIBRARY_PATH={}'.format( os.pathsep.join(add_bin_paths)))

Wird diese Methode von Conan’s generate() Methode aus mit
self.gen_env_file_with_conan_deps(os.path.join(self.build_folder, '.env'))
aufgerufen, dann kann diese Datei von Visual-Studio-Code Launch-tasks benutzt oder von CMake ausgelesen werden und mit ein paar Hacks als ENVIRONMENT Eigenschaft des Tests gesetzt werden:

1if(EXISTS "${CMAKE_BINARY_DIR}/.env")
2  file(READ "${CMAKE_BINARY_DIR}/.env" envfile_content)
3  string(REPLACE "\r" "" envfile_content "${envfile_content}")
4  string(REPLACE ";" "\\;" envfile_content "${envfile_content}")
5  string(REPLACE "\n" ";" envfile_entries "${envfile_content}")
6  set_property(TEST ${PROJECT_NAME} PROPERTY ENVIRONMENT "${envfile_entries}")
7endif()

Fazit

Tests sind super, Tests sind gut … aber können bei erhöhter Komplexität auch anstrengend werden.

Ein grobes Problem mit Conan-Builds ist, dass Tests innerhalb der Conan Umgebung immer anders laufen, als während der Entwicklung innerhalb einer IDE.
Und CMake kennt in erster Line nur Build-Details, jedoch keine erweiterten Umgebungs- und Ausführungsparameter.

Tja, so ist das Leben.
Aber es wäre ja langweilig, wenn immer gleich alles auf Anhieb funktioniert.

📧 📋 🐘 | 🔗 🔔
 

Meine Dokus über:
 
Weitere externe Links zu:
Alle extern verlinkten Webseiten stehen nicht in Zusammenhang mit opengate.at.
Für deren Inhalt wird keine Haftung übernommen.



Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!