Howto debug Ceedling Unit-Tests

Hintergrund

Ceedling ist ein (zu Recht) weit verbreitetes und in Ruby entwickeltes Build-System für C-Projekte.   Zusammen mit Unity und CMock wird es in der Embedded Software-Entwicklung gerne eingesetzt. Nach einer gewissen Einarbeitungszeit sind Unit-Tests normalerweise recht zügig erstellt. Idealerweise erstellt man die Tests zusammen mit der eigentlichen Implementierung. (Auf Test-Driven-Development will ich hier allerdings nicht näher eingehen.)

Es passiert allerdings immer wieder mal, dass ein Test „abraucht“ – sprich: sich sang- und klanglos wegen einer Exception verabschiedet.

Hier ein Beispiel einer solchen wenig hilfreichen Fehlermeldung:

Linking test_adcCtrl.out…
Running test_adcCtrl.out…

ERROR: Test executable „test_adcCtrl.out“ failed.
> Produced no output to $stdout.
> And exited with status: [0] (count of failed tests).
> This is often a symptom of a bad memory access in source or test code.

rake aborted!

C:/testprj/tools/vendor/ceedling/lib/ceedling/generator_helper.rb:36:in `test_results_error_handler‘
C:/testprj/tools/vendor/ceedling/lib/ceedling/generator.rb:173:in `generate_test_results‘
C:/testprj/tools/vendor/ceedling/lib/ceedling/rules_tests.rake:55:in `block in <top (required)>‘
C:/testprj/tools/vendor/ceedling/lib/ceedling/task_invoker.rb:107:in `invoke_test_results‘
C:/testprj/tools/vendor/ceedling/lib/ceedling/test_invoker.rb:124:in `block in setup_and_invoke‘
C:/testprj/tools/vendor/ceedling/lib/ceedling/test_invoker.rb:51:in `each‘
C:/testprj/tools/vendor/ceedling/lib/ceedling/test_invoker.rb:51:in `setup_and_invoke‘
C:/testprj/tools/vendor/ceedling/lib/ceedling/rules_tests.rake:70:in `block (2 levels) in <top (required)>‘
../../tools/vendor/ceedling/bin/ceedling:346:in `block in <main>‘
../../tools/vendor/ceedling/bin/ceedling:333:in `<main>‘
Tasks: TOP => build/test/results/test_adcCtrl.pass
(See full trace by running task with –trace)

Ursache sind z.B. Aufrufe von SDK-Funktionen, die auf Hardware-Adressen zugreifen und die es in der Simulation natürlich nicht gibt. In solchen Fällen die Fehlerursache zu finden, kann ohne weitere Analysemöglichkeiten auf längliches printf-Debugging hinauslaufen. Schön ist anders!

Hinweis: Im Folgenden gehe ich davon aus, dass bereits Unittests (unter Windows) mit Ceedling laufen; für das Einrichten von Ceedling gibt es gute Anleitungen ;-)

Wir verwenden in verschiedenen Projekten Ruby mit DevTools (Ruby-Installation https://rubyinstaller.org/downloads/, z.B. rubyinstaller-devkit-2.7.5-1-x86.exe). Der Compiler gcc kommt mit den MSYS2-Tools (https://www.msys2.org/) und ist da schon inklusive.

Back to the roots: gdb

Es ist schon seeehr lange her, dass ich den GNU Debugger gdb direkt verwendet habe. Zum Applikations-Debugging verwendet man normalerweise eine IDE (Segger/CubeIDE) oder ein anderes graphisches Debugger-Frontend (xgdb, DDD, KDbg,…) um sich das Leben nicht unnötig zu erschweren.

Wer Cygwin verwendet, hat den gdb normalerweise schon installiert. Wer Ruby mit MSYS2 verwendet, muss den gdb als optionales Packet installieren. Tipps dazu finden Sie am Ende („MSYS2 gdb-Installation“).

Die old-school-Version, um die Absturzursache eines Tests zu finden ist

  • eine Konsole (cmd) öffnen und den gdb mit dem absoluten Pfad zum Test-Executable starten:

    C:\testprj\scripts>gdb C:\testprj\Unittest\build\test\out\test_adcCtrl.out

  • gdb lädt das Executable und liest die darin enthaltene Symbolinformation:

    Reading symbols from C:\testprj\Unittest\build\test\out\test_adcCtrl.out…
    (gdb)

  • Mit “run” (oder kurz “r“) starten man das Executable:

    (gdb) rStarting program: C:\tesprj\Unittest\build\test\out\test_adcCtrl.out
    [New Thread 29368.0x700c]

    Thread 1 received signal SIGSEGV, Segmentation fault.
    test_adcCtrl_daqComplete () at test/test_adcCtrl.c:68
    68          adc_handler.Instance->CR = 0xFF;(gdb)

  • Falls man’s noch etwas genauer haben möchten – den Stacktrace erhält man mittels „where“:

    (gdb) where
    #0 test_adcCtrl_daqComplete () at test/test_adcCtrl.c:68
    #1  0x00f816f0 in run_test (func=0xf818b4 <test_adcCtrl_daqComplete>, name=0xfc8053 „test_adcCtrl_daqComplete“, line_num=57) at build/test/runners/test_adcCtrl_runner.c:93
    #2  0x00f81755 in main () at build/test/runners/test_adcCtrl_runner.c:109(gdb)

Mit diesen Informationen kommt man zumeist schon sehr viel weiter. (In diesem konkreten Fall greift der Applikationscode auf einen Pointer in der Datenstruktur ADC_HandleTypeDef  zu – und der ist NULL).

Etwas mehr Komfort bitte

Hardcore-gdb-ing ist ok – aber im Alter mag man’s gern auch etwas bequemer. Als gelegentlich VS Code-Nutzer lag die Suche nach einem Plug-In nahe, welches den Ceedling Unit-Tests Workflow unterstützt. Dabei bin auf über mehrere Blog-Beiträge etc. gestoßen. Am Ende des Artikels finden Sie eine Liste der verwendeten Quellen. Die Quintessenz ist in diesem Artikel zusammengefasst.

Jetzt geht’s also um die Zutaten die’s braucht, um einen Testcase zu Debuggen – und das möglichst komfortabel.

VS Code – mehr als ein Editor

VS Code ist ein Open-Source-Projekt (MIT-Lizenz) und wird manchmal auch nur als Editor verwendet. Damit bleibt man aber weit unter seinen Möglichkeiten. Mit den zahlreichen Erweiterungen wird er schnell zur eierlegenden Wollmilchsau – und dem diesem Fall zur „Ceedling-IDE“ :-)

Installation

Folgende Tools sind erforderlich

  1. VS Code (https://code.visualstudio.com/download – ich verwende die 64bit Version)
  2. Die VS Code-Erweiterungen
    • Ceedling Test Explorer: Startet Ceedling und die generierten Test-Runner im Hintergrund, parst deren Ausgaben, listet die Testcases, …
    • C/C++ : Notwendig zum Debuggen (setzen von Breakpoints, Single stepping, etc.)
      Am Ende des Blogs finden Sie eine Liste der installierten Plug-Ins.
  3. gdb – der GNU-Debugger. Wie anfangs erwähnt, ist er in einer Cygwin-Toolchain üblicherweise enthalten. Verwendet man MSYS, wird’s unter Umständen knifflig. Dazu finden Sie Am Ende noch zwei Links, mit Hilfe dere ich den gdb installieren konnte.

Die Installation sollte keine Schwierigkeit darstellen. Falls doch – bitte melden.

Bringing everything together

  1. Zunächst öffnet man in VC Code das Verzeichnis, in dem sich das project.yaml befindet: File -> Open Folder oder in der „Explorer View“
  2. Für diesen „Workspace“ muss zunächst eine „launch“-Konfiguration erstellt werden.
    Dazu wechseln Sie in die „Run View„: Auf der „Activity Bar“ (am linken Fensterrand) klicken Sie auf das Run-Icon  (Dreieck mit Käfer). Nun klicken Sie auf „create a launch.json file„.

    Jetzt erscheint eine Auswahlliste mit verschiedenen Konfigurationsmöglichkeiten. Sie können irgendeine Option auswählen. Es spielt keine Rolle, denn den Inhalt der im Verzeichnis .vscode erzeugten Datei ersetzen wir mit dem folgenden.
  3. Den folgenden Text – die sogenannte Launch-Konfiguration – kopieren Sie in die geöffnete launch.json-Datei:
    
    {
        "version": "0.2.0",
        "configurations": [
     
            {
                "name": "ceedlingExplorer",
                "type": "cppdbg",
                "request": "launch",
                "program": "${workspaceFolder}/build/test/out/${command:ceedlingExplorer.debugTestExecutable}",
                "args": [],
                "stopAtEntry": false,
                "cwd": "${workspaceFolder}",
                "environment": [],
                "externalConsole": false,
                "MIMode": "gdb",
                "miDebuggerPath": "gdb.exe",
                "setupCommands": [
                    {
                        "description": "Enable pretty-printing for gdb",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ],
                "debugConfiguration": {
                    "description": "Debug configuration to run during test debug.",
                    "type": "string",
                    "scope": "resource"
                  },
                "problemMatching": {
                    "mode": "gcc"
                }
            }
        ]
    }

    Mit der Konfiguration teilt man dem Ceedling Test Explorer mit

      • wo die Test-Sourcen liegen.
        Den Pfad „build/test/out“ im Eintrag „program“ müssen Sie ggf. an Ihre Ceedling-Konfiguration (siehe project.yml) angleichen.
      • welcher Debugger verwendet werden soll.
        Hiefür müssen Sie den Parameter für miDebuggerPath an Ihre Buildumgebung anpassen. Ist gdb.exe nicht im Suchpfad, können Sie hier auch einen absoluten Pfad angeben, z.B.:
        „miDebuggerPath“: „C:/Ruby27/msys32/mingw32/bin/gdb.exe„,

    Die weiteren Parameter muss man normalerweise nicht anfassen.

  4. In den VS Code-Settings (via File->Preferences->Settings) unter „Extensions > Ceedling Test Explorer configuration“ muss der Name der Konfiguration, wie im Launch.json angegeben, eingetragen werden:
  5. Damit der Ceedling-Test-Explorer die Testausgaben parsen kann, muss in der Projekt-Datei project.yml noch das Plug-In „xml_test_report“ ergänzt werden::plugins:
    :load_paths:
      - ../../tools/vendor/ceedling/plugins
    :enabled:
      - stdout_pretty_tests_report
      - xml_tests_report
  1. Nun noch in die „Test“-Ansicht wechseln, auf „Refresh“ klicken (zweites Icon neben „TESTING“) – und voila :-)

Danach werden alle Testcases aufgelistet.

Nun können Sie wie in jeder anderen IDE auch den Test editieren, Tests starten, Breakpoints setzen und debuggen. Was will man mehr!

Eine Einschränkung gibt es derzeit noch: Man kann einzelne Test-Cases eines Unit-Tests (noch?) nicht separat ausführen oder debuggen. Wollen Sie also einen Test-Case debuggen, müssen Sie dort zuerst einen Breakpoint setzen und dann den Bug-Button  klicken („Debug this test“).

Sollten Sie Fragen oder Anregungen haben, schreiben Sie einen Kommentar. Ich helfe ihnen gerne weiter.

Referenzen

MSYS2 gdb-Installation:

  • GDB installieren: Eine ältere Ruby/MSYS2-Installation hatte sich energisch geweigert, ein Paket zu installieren. Mit Ruby27+MSYS2 war das kein Problem. Einfach die MSYS32-Console starten (in meinem Fall msys2.exe im Verzeichnis C:\Ruby27\msys32) und mittels pacman -S gdb wird gdb installiert.
    Hier eine weitere Anleitung: https://gist.github.com/bd2357/b2d69ab18849c1e2f70959eef426ff09
    Hinweis: Sie können problemlos die 64Bit-Version von gdb installieren (mingw64/mingw-w64-x86_64-gdb), wie in der Anleitung beschrieben, auch wenn Sie die 32-Bit-Version von Ruby/MSYS2 verwenden.
  • Probleme mit Signaturen: https://github.com/msys2/MSYS2-packages/issues/2343

Links:

VS Code-Plugins

Kontaktieren Sie uns!

Autor

  • Jürgen Welzenbach

    Jürgen hat nach seinem Elektrotechnikstudium in Erlangen seine Diplomarbeit in Kooperation mit einem Hersteller von ophthalmologischen Geräten und der Universitätsaugenklinik durchgeführt. In zwei Erlanger Unternehmen fand er zur Embedded Software und hat vor allem HMIs für Baumaschinen und Laboranalysegeräte entwickelt.

Auch interessant:

Anforderungen an eine Software-Architektur

Die Zeiten, als embedded System einfache, dedizierte und überschaubare Aufgaben zu erledigen hatten, sind längst vorbei. Funktionen wie Bluetooth-Anbindung, Safety, Security, weitreichende Konfigurationsmöglichkeiten und Zusammenfassung von mehreren Systemen zu einem Größeren (weil die µCs leistungsfähiger geworden sind) lassen die Code-Basis schnell anwachsen. Wenn die zugrunde liegende Software-Architektur nicht dafür ausgelegt…