Forum: Projekte & Code uCTest: unit test framework


von Sebastian K. (seplog)


Angehängte Dateien:

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)


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)


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:

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:

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.
von Sebastian K. (seplog)


Angehängte Dateien:

Lesenswert?

Hallo,

ich habe einmal wieder an meinem Test-Framework weiter gearbeitet.
Zuerst einmal gab es ein paar kleinere Bugfixes und Verbesserungen.
 * Aus den Headern ist nun die Abhängigkeit zur avr/io.h verschwunden.
 * Die Nachrichten werden nun nicht mehr via stdout gesendet sondern 
haben ein eigenes FILE bekommen
 * Wenn bei einem FAIL kein format-String angegeben ist, dann stürzt der 
Controller auch nicht mehr ab.

Weiterhin kam dann noch ein recht gutes Feature dazu.
Ich nutze nun ein Simulator und habe dafür ein Frontend entwickelt. Als 
Simulator nutze ich buserror/simavr. Das Frontend sucht nach einer 
AVR-ELF-Datei mit dem gleichen Namen und führt diese aus. Weiterhin ist 
das Frontend soweit zu GTest kompatibel, dass man es nun in VSCode im 
Testexplorer nutzen kann.
Weiterhin können einzelne Tests ausgeführt werden und die Tests gelistet 
werden.
1
$ ./test/build/testSuite.avr --mcu atmega328p --freq 11059200 --list-tests
2
Loaded 11348 .text at address 0x0
3
Loaded 1608 .data
4
SqrtTest.
5
  calculations_u16
6
  calculations_u24
7
  assertDeaths
8
BCDTest.
9
  init
10
FilterBqTest.
11
  filterBessel
12
DivTest.
13
  int
14
  fixpt
15
SinTest.
16
  sin
17
SqrtTest.
18
  calculations_u16
19
  calculations_u24
20
  assertDeaths
21
BCDTest.
22
  init
23
FilterBqTest.
24
  filterBessel
25
DivTest.
26
  int
27
  fixpt
28
SinTest.
29
  sin
Auch lassen sich über einen Filter nur bestimmte Tests ausführen.
1
$ ./test/build/testSuite.avr --mcu atmega328p --freq 11059200 --filter=FilterBqTest.filterBessel
2
Loaded 11348 .text at address 0x0
3
Loaded 1608 .data
4
Running main
5
[==========] Running 1 tests from 1 test suites.
6
[----------] 1 tests from FilterBqTest
7
[ RUN      ] FilterBqTest.filterBessel
8
[       OK ] FilterBqTest.filterBessel (2 ms)
9
[----------] 1 tests from FilterBqTest (2 ms total)
10
11
[==========] 1 tests from 1 test suites ran. (2 ms total)
12
[  PASSED  ] 1 tests.

Warum simavr... ...
Ein hauptgrund war, dass das Framework recht einfach ist und er recht 
gut erweiterbar ist. Auch habe ich dann in den Beispielen zu dem 
Simulator gefunden, dass man dort auch Hardware simulieren kann. Ganz so 
schwierig ist das auch nicht. Sobald der Simulator initialisiert ist, 
gibt es einen Hook welchen man nutzen kann um die Hardware zu 
simulieren.
Dazu überschreibt man den test_init_hook und linkt sein Objekt gegen die 
libtestRunner.so.
1
void test_init_hook( avr_t *avr );
2
void test_start_hook( avr_t *avr, char *name );
Die zweite Funktion ist ein Hook, welcher aufgerufen wird wenn ein Test 
startet. Der Name des Tests wird dann in name übergeben.

Ich wünsche viel Freude und ich würde mich über Nachfragen und Beiträge 
freuen.

Viele Grüße
Sebastian

von Sebastian K. (seplog)


Angehängte Dateien:

Lesenswert?

Hallo,

ich habe einmal wieder ein Update parat. Mein Ziel war es nun das 
Framework auf andere Architekturen zu portieren. Die einfachste war nun 
erst einmal die x86-Architektur. Dort ist mir aber ein Problem bei dem 
Konzept aufgefallen.

Beim AVR habe ich die Tests via der .init7 auf den Stack gepusht beim 
registrieren. Das ist weder bei x86 ordentlich möglich noch bei RISC-V 
(zweiter port)... Aus dem Grund bin ich nun einmal an die Linkerfile 
ran. Dort habe ich nun für den test eine extra Section eingeführt. Das 
funktioniert nun bei x86 sehr gut (dort kann man Sections einfügen) bei 
AVR und RISC-V hab ich das noch nicht ganz so schön hinbekommen.. -.-
Sei es drum, dort stelle ich eine gepatchte Linkerfile mit zur 
Verfügung.

Lauffähig ist das Framework nun auf
  - AVR
  - x86
  - RISC-V (picolibc)

Beim Simulator-Frontend für den AVR habe ich noch einen 
Use-After-Free-Bug behoben wenn FIXTURE-Tests ausgeführt werden. 
Weiterhin habe ich nun noch ein wenig mehr dokumentiert und auch in der 
README nun zwei kleine Beispiele drin. Für AVR und für RISC-V.

Das war es nun aber ;)

Viele Grüße
Sebastian

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.