Forum: Projekte & Code uCTest: unit test framework


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Sebastian K. (seplog)


Angehängte Dateien:

Bewertung
1 lesenswert
nicht lesenswert
Hallo,

ich würde gerne einmal ein kleines Projekt von mir vorstellen. Dazu ein 
wenig zur Vorgeschichte.

AVR entwickel ich schon länger. Habe aber immer wieder Probleme mit 
automatischen Tests gehabt. Irgendwann bin ich dann auf das google-test 
gestoßen und habe dann einfach meine Libs (welche nicht 
prozessorspezifisch waren) dagegen automatisch getestet. Jedoch ist mir 
dann aufgefallen, dass ein wenig inline Assembler manchmal ziemliche 
Performance-Vorteile bringen kann. ^^
Dann war es leider vorbei mit gtest...

Dann habe ich mich wieder ein wenig auf die Suche gemacht und auch was 
gefunden... Wie uCUnit. Aber... gtest gefiel mir von dem Overhead 
ziemlich gut. Also hab ich mich versucht. ;)

Nun einmal ein kleines Bsp.
Um eine Tests zu definieren ist eine Suite nötig und dann kann schon der 
Test kommen.
1
#include <uCTest/uCTest.h>
2
3
TESTSUITE( Simple );
4
5
TEST( Simple, add ) {
6
    EXPECT_EQ( ( 2 + 2 ), 4 );
7
    EXPECT_EQ( ( 1 + 1 ), 2 );
8
}
Das kann dann übersetzt und (bisher) in simulavr ausgeführt werden.
1
$ avr-gcc -o simple.elf simple.c -L ./uctest-master/build -l uCTest -I ./uctest-master/include -mmcu=atmega328
2
$ simulavr -d atmega328 -f simple.elf -W 0x20,- -e 0x21 ; echo $?
3
Running main
4
[==========] Running 1 tests from 1 test suites.
5
[----------] 1 tests from Simple
6
[ RUN      ] Simple.add
7
[       OK ] Simple.add (55 cycles)
8
[----------] 1 tests from Simple (55 cycles)
9
10
[==========] 1 tests from 1 test suites ran. (55 cycles total)
11
[  PASSED  ] 1 tests.
12
0

Mehr ist eigentlich nicht nötig.
Ein bisschen mehr zu den Features.
Es kann ein system setup und teardown implementiert werden. In den 
Tests. dazu muss uctest_system_setup/teardown implementiert werden. 
Diese sind schwach (weak) gebunden.

Es werden Fixture tests zur Verfügung gestellt. Dazu muss TEST_F genutzt 
werden. Dort ist es dann erforderlich SETUP(suite) und TEARDOWN(suite) 
zu implementieren.

Wenn die lib mit CONFIG_COUNT_CYCLES übersetzt wird, werden die Takte 
"gezählt". Dies geschieht über den 16 bit Timer.

Ansonsten...
Die ASSERT und EXPECT sind noch ausbaufähig. Dort habe ich bisher 
getestet mit ASSERT_EQ und ASSERT_NEAR. Mehr ist dort noch nicht 
implementiert. Das steht aber auch noch ganz groß auf meiner ToDo.

Weiterhin auf der ToDo steht auch noch ein JSON output, welcher dann 
nach XML konvertiert werden kann um JUNIT-Kompatible outputs zu 
erzeugen.

Guckt euch das gerne einmal an. Ich habe dort viel Freude beim 
implementieren gehabt und hoffe es ist vielleicht nützlich für einige! 
;)

Viele Grüße
Sebastian

von Testi (Gast)


Bewertung
0 lesenswert
nicht lesenswert
In dem Zusammenhang evtl. für den einen oder anderen noch von Interesse:
https://colinholzman.xyz/2020/08/22/unit-testing-embedded-c

von Sebastian K. (seplog)


Bewertung
0 lesenswert
nicht lesenswert
Hallo Testi,

vielen Dank für den guten Link. So in der Art habe ich das auch mit dem 
gtest jeweils versucht. Prozessorspezifische Register lassen sich noch 
recht gut abstrahieren.

Mir ging es dann aber später um Optimierungen von lib Funktionen.
Als Bsp.
1
#ifndef __AVR__
2
#undef __flash
3
#define __flash
4
#include <gtest/gtest.h>
5
#include <signal/filter/filter_bq.c>
6
#else
7
#include <uCTest/uCTest.h>
8
TESTSUITE( FilterBqTest );
9
#endif
10
#include <fmath/signal/filter.h>
11
12
TEST( FilterBqTest, filterBessel ) {
13
    static const __flash struct filter_bq_cfg f_cfg = { .neg_a = { 10232, -3426 },
14
                                                        .b = { 5462, -10925, 5462 } };
15
16
    static const __flash int16_t data[] = {
17
        120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120,
18
        0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
19
    };
20
    static const __flash int16_t res[] = {
21
        80, 20, -8, -18, -19, -16, -12, -8, -5, -3, -2, -1, 0, 0, 0,
22
        -80, -20, 8, 18, 19, 16, 12, 8, 5, 3, 2, 1, 0, 0, 0
23
    };
24
    filter_bq_t filter;
25
26
    filter_bq_init( &filter, &f_cfg );
27
28
    int16_t y;
29
    int i;
30
    for( i = 0; i < ( int ) ( sizeof( data ) / sizeof( int16_t ) ); i++ ) {
31
        int16_t x = data[ i ];
32
        y = filter_bq_compute( &filter, x );
33
        ASSERT_EQ( y, res[ i ] );
34
    }
35
36
    EXPECT_NEAR( y, 0, 5 );
37
}

Dieser Filter greift auf eine ShiftRoundAdd Funktion zu. Diese kann man 
einmal generisch implementieren. Dann läuft das auch alles und ich kann 
das auch wieder auf x86 testen und kann das gegen gtest linken lassen. 
Jedoch habe ich mit einer Asm-Implementierung einige hundert Takte 
sparen können.
Pro filter_bq_compute... ...
Das ist dann schon ein enormer performance-Vorteil. Dort sollte dann 
natürlich auch getestet werden. ;)

Die generische implementierung
1
[----------] 1 tests from FilterBqTest
2
[ RUN      ] FilterBqTest.filterBessel
3
[       OK ] FilterBqTest.filterBessel (47240 cycles)
4
[----------] 1 tests from FilterBqTest (47240 cycles)

Per inline Asm optimiertes ShiftRoundAdd
1
[----------] 1 tests from FilterBqTest
2
[ RUN      ] FilterBqTest.filterBessel
3
[       OK ] FilterBqTest.filterBessel (19190 cycles)
4
[----------] 1 tests from FilterBqTest (19190 cycles)

Aso. Noch eine Kleinigkeit zu dem Beispiel oben. Das hat den Vorteil, 
dass dies mit dem gtest übersetzt werden kann, und auch mit dem uCTest, 
sodass es auf dem Controller lauffähig ist ;)

Noch ein Nachtrag:
Ein Grund für die Entwicklung vom uCTest war unter anderem auch, dass 
ich nicht jeden Test neu implementieren muss. Sondern das ich auf 
bestehende Tests vom gtest zurück greifen kann.

Viele Grüße
Sebastian

: Bearbeitet durch User
von Sebastian K. (seplog)


Angehängte Dateien:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

ich habe einmal ein kleines Update. Ein ToDo auf meiner Liste war ja 
noch die Erweiterung der Assertions. Das war ein wenig Fleißarbeit und 
das habe ich einmal getan.
Es stehen nun weiterhin ASSERT_LT, _LE, _GT, GE zur Verfügung. Weiter 
habe ich mich auch um ein ASSERT_DEATH gekümmert. Also dass eine 
Assertion vom Programm aus Fehl schlägt. Dazu muss dann aber die 
verwendete Lib ohne NDEBUG übersetzt werden, sodass die asserts aus der 
assert.h auch mit übersetzt werden.
Wichtig bei dem ASSERT_DEATH ist noch zu sagen... Ich prüfe dort bisher 
nur, ob ein abort aufgerufen wird. Dann wird die Ausführung abgebrochen 
für die Funktion. Es wird aber bisher nicht die Meldung geprüft!

Ich habe einmal ein kleines Bsp. mit einer Sqrt-Implementierung, welche 
ich auch hier aus dem Forum habe. Leider finde ich den Thread gerade 
nicht mehr.

Aber hier einmal ein wenig Code als Bsp:
1
#include <assert.h>
2
#include <stdint.h>
3
4
#include <uCTest/uCTest.h>
5
6
#define maxRes( msk ) ( ( ( msk << 1 ) - 1 ) * ( ( msk << 1 ) - 1 ) )
7
8
uint8_t sqrt_msk( uint16_t x, const uint8_t msk ) {
9
    assert( x <= maxRes( msk ) );
10
11
    uint8_t res = 0;
12
    uint8_t tmp_msk = msk;
13
14
    do {
15
        res += tmp_msk;
16
        if( res * res > x ) {
17
            res -= tmp_msk;
18
        }
19
    } while( tmp_msk >>= 1 );
20
    return res;
21
}
22
23
static inline uint8_t sqrt_u16( uint16_t x ) {
24
    return sqrt_msk( x, ( 1 << 7 ) );
25
}
26
27
TESTSUITE( SqrtTest );
28
29
TEST( SqrtTest, results ) {
30
    EXPECT_EQ( sqrt_u16( 25 ), 5 );
31
    EXPECT_EQ( sqrt_u16( 24 ), 4 );
32
    EXPECT_EQ( sqrt_u16( 9 ), 3 );
33
    EXPECT_EQ( sqrt_u16( 7 ), 2 );
34
    EXPECT_EQ( sqrt_u16( 65025 ), 255 );
35
}
36
37
TEST( SqrtTest, assertFail ) {
38
    ASSERT_DEATH( sqrt_msk( 65025, ( 1 << 6 ) ), ".*x <= maxRes.*failed." );
39
    EXPECT_LT( sqrt_u16( 24 ), 5 );
40
}
1
$ avr-gcc -Os -std=gnu99 -o simple.elf simple.c -L ./uctest-master/build -l uCTest -I ./uctest-master/include -mmcu=atmega328
2
$ simulavr -d atmega328 -f simple.elf -W 0x20,- -e 0x21 ; echo $?
3
Running main
4
[==========] Running 2 tests from 1 test suites.
5
[----------] 2 tests from SqrtTest
6
[ RUN      ] SqrtTest.results
7
[       OK ] SqrtTest.results (729 cycles)
8
[ RUN      ] SqrtTest.assertFail
9
[       OK ] SqrtTest.assertFail (274 cycles)
10
[----------] 2 tests from SqrtTest (1003 cycles)
11
12
[==========] 1 tests from 2 test suites ran. (1003 cycles total)
13
[  PASSED  ] 2 tests.

: Bearbeitet durch User
von Sebastian K. (seplog)


Angehängte Dateien:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

ich habe einmal ein wenig weiter gemacht und mich um ein JSON output 
gekümmert. Dies klappt nun auch. Man kann das in der Makefile mit 
anschalten indem man CONFIG_OUTPUT_JSON als Symbol definiert. Auch ein 
CONFIG_OUTPUT_NONE ist mit dazu gekommen. Dort kommt dann halt kein 
output.
Um das dann noch ein wenig abzurunden gibt es unter tools ein kleines 
Python-Script, welches das JSON in ein JUNIT-kompatibles XML wandelt.

Auch habe ich einen bug beim ASSERT_DEATH behoben. Dort muss natürlich 
komplett der Kontext gesichert werden. Denn wenn ein assert aus der libc 
das abort aufruft, wird ja an ein Label gesprungen. Dort müssen 
natürlich der Stack und auch alle Register wiederhergestellt werden.

Viel Spaß und viele Grüße
Sebastian

Beitrag #6582400 wurde von einem Moderator gelöscht.
Beitrag #6594225 wurde von einem Moderator gelöscht.

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.