
Ich bin ein großer Fan von Unit-Tests bei der Entwicklung von APIs wie den Datenkodierungs- und -dekodierungsfunktionen von Ruuvi. Die Tests ermöglichen es uns, die Dokumentation und die Ausgabe des Codes miteinander zu verknüpfen, da die dokumentierten Normalfälle, Grenzfälle und Fehler als Tests ausgeschrieben werden.
Der Nachteil ist, dass wir nicht dieselben Tools verwenden können, die wir von Integrationstests des gesamten Programms kennen, wie zum Beispiel unseren eingebetteten Geräte-IDE-Debugger, wenn wir die Unit-Tests selbst debuggen wollen.
Obwohl das Ausgeben von Nachrichten während der Programmausführung seinen Platz hat, z. B. bei der Ausführung von Code mit harten Echtzeitanforderungen, wo ein Debugger, der die Ausführung stoppt, zu einem Systemausfall führt, bevorzuge ich, wenn möglich, einen richtigen Debugger zu verwenden.
Das hier verwendete Codebeispiel sind in Arbeit befindliche Kommunikations-De-/Encoder für die Ruuvi Gateway nRF52- und ESP32-Kommunikation, verfügbar auf GitHub im Branch ruuviblog-ceedling.
Struktur unseres Projekts
Die meisten meiner C-Projekte haben heutzutage dieselbe Struktur:
- Der Ordner .github, in dem sich unsere GitHub Actions befinden, um die Tests, die Doxygen-Generierung, die Stilprüfung usw. automatisch auszuführen, bevor Code in den Master-Branch gemergt werden kann.
- Der Ordner build, in dem die Ausgaben des Unit-Test-Frameworks Ceedling gespeichert werden.
- Der Ordner src, in dem sich der eigentliche Quellcode befindet.
- Der Ordner test zum Speichern des Unit-Test-Codes.
- Verschiedene Konfigurationsdateien für die Tools und die Dokumentation auf Root-Ebene

Den Debugger ausführen
Ceedling arbeitet intern, indem es für jeden Test eine ausführbare Datei erstellt und deren Ausgabe überprüft. Das gibt uns einen hervorragenden Einstiegspunkt in unseren Test.
Nehmen wir den letzten fehlgeschlagenen Testfall, test_ruuvi_endpoint_ca_uart_adv_decode. Dieser bezieht sich auf die Dekodierung eines serialisierten Bluetooth-Werbescan-Berichts in ein strukturiertes Objekt mit MAC-Adresse des Senders, RSSI und Nutzlastdaten.
void test_ruuvi_endpoint_ca_uart_adv_decode (void)
{
re_status_t err_code = RE_SUCCESS;
uint8_t data[] =
{
RE_CA_UART_STX,
41U + CMD_IN_LEN,
RE_CA_UART_SET_PHY,
0xC9U, 0x44U, 0x54U, 0x29U, 0xE3U, 0x8DU, RE_CA_UART_FIELD_DELIMITER, //!< MAC
0x02U, 0x01U, 0x04U, 0x1BU, 0xFFU, 0x99U, 0x04U, 0x05U, 0x0FU, 0x27U, 0x40U,
0x35U, 0xC4U, 0x54U, 0x00U, 0x50U, 0x00U, 0xC8U, 0xFCU, 0x20U, 0xA4U, 0x56U,
0xF0U, 0x30U, 0xE5U, 0xC9U, 0x44U, 0x54U, 0x29U, 0xE3U, 0x8DU,
RE_CA_UART_FIELD_DELIMITER, //!< Data
0xD8U, RE_CA_UART_FIELD_DELIMITER, //RSSI
RE_CA_UART_ETX
};
re_ca_uart_ble_adv_t expect_params =
{
{ 0xC9U, 0x44U, 0x54U, 0x29U, 0xE3U, 0x8DU }, //!< MAC
{
0x02U, 0x01U, 0x04U, 0x1BU, 0xFFU, 0x99U, 0x04U, 0x05U, 0x0FU, 0x27U, 0x40U,
0x35U, 0xC4U, 0x54U, 0x00U, 0x50U, 0x00U, 0xC8U, 0xFCU, 0x20U, 0xA4U, 0x56U,
0xF0U, 0x30U, 0xE5U, 0xC9U, 0x44U, 0x54U, 0x29U, 0xE3U, 0x8DU
}, //!< Data
31U, //!< Data length
-40 //RSSI
};
re_ca_uart_cmd_t expect_cmd = RE_CA_UART_ADV_RPRT;
re_ca_uart_payload_t payload = {0};
err_code = re_ca_uart_decode (data, &payload);
TEST_ASSERT (RE_SUCCESS == err_code);
TEST_ASSERT (!memcmp (&expect_params, &payload.params.adv, sizeof (expect_params)));
TEST_ASSERT (!memcmp (&expect_cmd, &payload.cmd, sizeof (expect_cmd)));
}Unser Testfall definiert die Daten, die an den Decoder gehen, und die erwartete Ausgabe. Daten werden geparst und die geparsten Ausgaben mit unseren Erwartungen verglichen.
static re_status_t re_ca_uart_decode_adv_rprt (const uint8_t * const buffer,
re_ca_uart_payload_t * const payload)
{
re_status_t err_code = RE_SUCCESS;
if (buffer[RE_CA_UART_LEN_INDEX] != RE_CA_UART_CMD_PHY_LEN)
{
err_code |= RE_ERROR_DECODING;
}
else
{
payload->cmd = RE_CA_UART_ADV_RPRT;
memcpy (payload->params.adv.mac,
buffer + RE_CA_UART_PAYLOAD_INDEX, RE_CA_UART_MAC_BYTES);
payload->params.adv.adv_len = buffer[RE_CA_UART_LEN_INDEX]
- RE_CA_UART_MAC_BYTES - RE_CA_UART_RSSI_BYTES;
memcpy (payload->params.adv.adv,
buffer + RE_CA_UART_PAYLOAD_INDEX + RE_CA_UART_MAC_BYTES,
payload->params.adv.adv_len);
payload->params.adv.rssi_db = u8toi8 (
buffer[RE_CA_UART_PAYLOAD_INDEX
+ buffer[RE_CA_UART_LEN_INDEX]
- RE_CA_UART_RSSI_BYTES]);
}
return err_code;
}
re_status_t re_ca_uart_decode (const uint8_t * const buffer,
re_ca_uart_payload_t * const payload)
{
re_status_t err_code = RE_SUCCESS;
// Sanity check buffer format
if (NULL == buffer)
{
err_code |= RE_ERROR_NULL;
}
else if (NULL == payload)
{
err_code |= RE_ERROR_NULL;
}
else if (RE_CA_UART_STX != buffer[RE_CA_UART_STX_INDEX])
{
err_code |= RE_ERROR_DECODING;
}
else if (RE_CA_UART_ETX != buffer[buffer[RE_CA_UART_LEN_INDEX] + RE_CA_UART_HEADER_SIZE])
{
err_code |= RE_ERROR_DECODING;
}
else if (RE_CA_UART_NOT_CODED != payload->cmd)
{
err_code |= RE_ERROR_INVALID_PARAM;
}
else
{
switch (buffer[RE_CA_UART_CMD_INDEX])
{
case RE_CA_UART_SET_FLTR:
err_code |= re_ca_uart_decode_set_fltr (buffer, payload);
break;
case RE_CA_UART_CLR_FLTR:
err_code |= re_ca_uart_decode_clr_fltr (buffer, payload);
break;
case RE_CA_UART_SET_CH:
err_code |= re_ca_uart_decode_set_ch (buffer, payload);
break;
case RE_CA_UART_SET_PHY:
err_code |= re_ca_uart_decode_set_phy (buffer, payload);
break;
case RE_CA_UART_ADV_RPRT:
err_code |= re_ca_uart_decode_adv_rprt (buffer, payload);
break;
default:
err_code |= RE_ERROR_DECODING;
break;
}
}
return err_code;
}Unser Code führt einige Plausibilitätsprüfungen durch und leitet die Daten dann an den korrekten Parser weiter, wie durch den in den Daten enthaltenen Nutzlasttyp bestimmt.
Um eine bessere Vorstellung davon zu bekommen, was schiefgelaufen ist, führen wir aus
lldb build/test/out/test_ruuvi_endpoint_ca_uart.outwas die Testdatei in lldb öffnet. GDB funktioniert genauso gut, wenn du das bevorzugst.
(lldb)type format add --format hex uint8_t
(lldb) breakpoint set --file ruuvi_endpoint_ca_uart.c --line 150
(lldb) breakpoint set --file test_ruuvi_endpoint_ca_uart.c --line 411
(lldb) runWir setzen Breakpoints in der Testdatei vor der Zeile, die den Decoder ausführt, und im Decoder am Anfang der Plausibilitätsprüfungen.


Der erste Fehler ist nun klar: In Zeile 8 der obigen Testfunktion haben wir falsche Daten eingerichtet, und der Parser gibt korrekterweise einen Fehlercode zurück, der einen Assert auslöst und den Test fehlschlagen lässt.
Die Fehler beheben
Von nun an ist die Fehlerbehebung ein iterativer Prozess. Wir korrigieren die Testdaten, führen den Test erneut aus, nur um auf eine fehlerhafte Längenvalidierung im Decoder zu stoßen. Wir korrigieren die Längenvalidierung und sehen, dass das RSSI-Feld ein festes Trennzeichen anstelle des tatsächlichen RSSI-Wertes erhält. Wir korrigieren das RSSI-Feld, um festzustellen, dass die Werbelänge falsch berechnet wird, was zu einem Pufferüberlauf führt und das RSSI-Feld immer noch überschreibt. Wir beheben den Überlauf usw. usw. … Bis der Test schließlich bestanden wird.
Nur weil der Test bestanden wird, heißt das nicht, dass der Code perfekt ist. Wir fügen einen Testfall hinzu, der die Trennzeichen nicht in den Daten hat, und beginnen, neue Pufferüberläufe zu beheben. Schließlich sind wir mit den getesteten Randfällen zufrieden und bereit zu überprüfen, ob die Funktion von unseren Testfällen abgedeckt wird.
ceedling gcov:all utils:gcovMit diesem Befehl haben wir einen HTML-Bericht unserer Testausführung erstellt und können überprüfen, ob unsere Tests tatsächlich das abdecken, was wir von ihnen erwarten.

Es stellt sich heraus, dass das Entfernen des letzten Trennzeichens unsere Annahmen über die Positionierung der Elemente innerhalb der Daten bricht und eine frühere Überprüfung die Dekodierung stoppt, und unser Test überprüft keine Bitfehler am letzten Trennzeichen. Das beheben wir auch.

Fazit
Unit-Tests sind ein mächtiges Werkzeug zur Reduzierung von Fehlern in unserem Code, aber um den vollen Nutzen von Unit-Tests zu ziehen, benötigen wir einen separaten Satz von Tools, um zu überprüfen, ob unsere Tests tatsächlich das testen, was wir erwarten, und um die Tests selbst zu debuggen.
Hier haben wir durchgesprochen, wie man LLDB oder GDB verwendet, um die Fehler in Ceedling zu debuggen. Wenn du an weiteren Beiträgen zur Codequalität interessiert bist, lass es mich in Ruuvi Slack oder den Foren wissen, und ich werde Folgebeiträge schreiben.
Diese Beiträge werden durch deine Käufe von RuuviTags unterstützt. Wenn sie dir gefallen, miss deine Welt!