Repository navigation
tests.pl
Testy działają na komputerze, także w projekcie STM32, i sprawdzają właściwy kod firmware'u, a nie jego kopię.
make test buduje każdy program z .tests/ i go uruchamia, a jeden plik workflow robi to samo na GitHubie przy każdym pushu.
Podstawy Forge są w readme.
Każdy plik .c leżący bezpośrednio w .tests/ w katalogu głównym projektu to jeden program testowy, domyślnie nazwany test_<moduł>.c od modułu, który sprawdza.
Katalog .tests/ leży zawsze w katalogu głównym projektu, więc od razu widać, że nie jest częścią firmware'u.
myapp/
├─ main.c
├─ main.h
├─ ctrl/ramp.c
└─ .tests/
├─ test_ramp.c # program testowy modułu ramp
├─ fake/link.c # atrapa modułu link, nie program
└─ data/ # stałe wejścia i oczekiwane wartości, w repo
Test dołącza sprawdzany moduł po samej nazwie, #include "ramp.c", bo moduły mają jedną płaską przestrzeń nazw, tak jak w firmware.
Dzięki temu wykonuje prawdziwy kod firmware'u, łącznie z funkcjami static.
Kompiluje się tylko to, co test dołączył, więc rejestry w innych miejscach projektu nigdy nie przeszkadzają.
Funkcję main() dostarcza harness, więc main.c nigdy nie trafia do testu.
Program testowy nie może nazywać się jak moduł: #include w cudzysłowie szuka najpierw obok pliku, więc .tests/ramp.c zastąpiłby moduł w każdym teście.
Przedrostek test_ to najprostsze rozwiązanie, ale nada się każda nazwa, której nie ma żaden moduł.
// .tests/test_ramp.c
#include "test.h"
#include "ramp.c"
static void ramp_starts_at_zero(void)
{
RAMP_t ramp = { .target = 100, .step = 30 };
TEST_Equal(ramp.value, 0, "value before the first step");
}
static void ramp_stops_at_target(void)
{
RAMP_t ramp = { .target = 100, .step = 30 };
for(int i = 0; i < 5; i++) RAMP_Step(&ramp);
TEST_Equal(ramp.value, 100, "no overshoot");
}
void TEST_Cases(void)
{
TEST(ramp_starts_at_zero);
TEST(ramp_stops_at_target);
}-
TEST_Cases()wymienia przypadki w kolejności wykonania, po jednymTEST(name). -
TEST_Check(cond)alboTEST_Check(cond, fmt, ...)sprawdza warunek, aTEST_Equal,TEST_NeariTEST_Bytesprzy niepowodzeniu pokazują wyrażenie w postaci z kodu, jego wartość i wartość oczekiwaną. -
TEST_Logged(text)sprawdza, czy firmware zapisał w logu linię ztextod początku przypadku albo od poprzedniegoTEST_Logged. -
TEST_Run(ms)wykonujemskroków czasu wirtualnego, a każdy krok to tick zegara i wszystkie zadania, na które przyszła pora.
make test # wszystkie programy z `.tests/`
make test T=test_ramp # tylko `.tests/test_ramp.c`, z jego wyjściemKażdy program uruchamia się we własnym, czystym katalogu i dostaje jedną linię z liczbą udanych sprawdzeń na tle wszystkich, a na końcu pojawia się podsumowanie:
INF test_frame 12/12 OK
INF test_ramp 2/2 OK
INF Test programs 2/2, checks 14/14 in 0.2s OK
Pod linią programu, który nie przeszedł, pojawia się jego wyjście, dokładnie tak, jak je wypisał. Nieudane sprawdzenie podaje plik, linię oraz wyrażenie z wartością i kończy swój przypadek, po czym rusza następny:
INF test_frame 12/12 OK
ERR test_ramp 1/2
OK ramp_starts_at_zero
ERR projects/myapp/.tests/test_ramp.c:16 ramp_stops_at_target: no overshoot
ramp.value is 150, expected 100
ERR Test programs 1/2, checks 13/14 in 0.5s
Gdy uruchamiasz jeden program, przez T= albo w projekcie z jednym testem, jego wyjście widać także wtedy, gdy test przechodzi.
Program, który zakończy się przedwcześnie, też jest błędem, nawet przez exit(0) przy resecie firmware'u, a zawieszony program Forge przerywa po minucie.
make test uruchamia wszystkie programy i kończy się błędem, jeśli któryś nie przeszedł.
Nowy plik w .tests/ nie wymaga przeładowania projektu.
- Czekanie przesuwa zegar:
delay()itimeout()wykonują kroki czasu wirtualnego, więc test timeoutu nie trwa ani chwili.sleep()blokuje rdzeń, więc czas płynie, ale żadne zadanie się nie wykonuje. - Moduł wywoływany przez testowany kod dołączasz tak samo albo jako atrapę całego modułu z podkatalogu
.tests/,#include "fake/link.c", bo atrapa nazywa się jak moduł, który zastępuje. Atrapa tylko części modułu Core nie zadziała, bo linker dociągnąłby oryginał z archiwum i zdublował resztę. - Stałe dane wejściowe i oczekiwane wartości, np. nagrana ramka albo podpis z Forge, leżą w
data/i również dołącza się je ze ścieżką katalogu,#include "data/forge.h". Tworzy się je ręcznie albo skryptem leżącym obok i commituje, a uruchomienie testów nic tam nie zapisuje: pliki zapisane przez program zostają w jego czystym katalogu wbuild/. -
#defineprzed#includemoże zmienić ustawienia dołączanego modułu, np.LOG_LEVEL. Rozmiar albo typ wspólny z resztą Core, np.BUFF_MSG_LIMIT, ustawia się wmain.h, bo inaczej test i Core po cichu się rozjadą. - Core trafia do testu skompilowany z
hal/hostilib, a ich moduły dołącza się po nazwie. Po nazwie dołącza się też nagłówki sterowników wybranych w projekcie, tak jak robi to płytka. Moduł zplc/,brd/,drv/albohal/stm32*dołącza się ze ścieżką od katalogu głównego Core, np.#include "hal/stm32/sys/bld.c". - Do projektu STM32 z testami Forge dokłada MinGW obok
arm-gcc.
Jeden plik w repozytorium projektu uruchamia make test i make przy każdym pushu:
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
uses: OpenCPLC/Forge/.github/workflows/project.yml@0.5.1Workflow robi to samo, co Ty w świeżym workspace: dla każdego projektu w repozytorium, na Windowsie i z narzędziami Forge.
Każdy projekt ładuje się według swojego main.h, więc projekt bez płytki i bez układu zostaje pominięty.
Wersję Core wybiera PRO_FRAMEWORK, tak jak na Twoim komputerze.
Tag ustala wydanie Forge, więc nowa wersja Forge nie zmieni wyniku bez Twojej wiedzy.
Note
W prywatnym repozytorium uruchomienia zużywają darmowe minuty z planu, a minuta na Windowsie kosztuje więcej niż na Linuksie.
Core testuje się tak samo, z .tests/ w swoim katalogu głównym:
opencplc/develop/.tests/
├─ test_bld.c # program testowy modułu bld
├─ test_cron.c
├─ data/forge.h # klucze i podpis zrobione przez Forge, z forge.py
├─ g081/ # projekt: test kompilacji STM32G081
├─ g0c1/
├─ wb55/
└─ uno/ # projekt: warstwa PLC na płytce Uno
Plik .c leżący bezpośrednio w .tests/ to program testowy, katalog z main.h to projekt, a każdy inny katalog zawiera pomoce do testów, np. fake/ i data/.
Core nie ma atrap, bo jego modelem układu jest hal/host: tamtejszy flash kasuje się, programuje i traci zasilanie jak prawdziwy układ, a test odcina to zasilanie przez FLASH_HostCut.
opencplc opencplc/develop # sam Core
make test # jego programy testowe
opencplc opencplc/develop/.tests/g081 # test kompilacji
make # buduje goPrzy każdym pushu i pull requeście GitHub robi make w każdym projekcie z .tests/ oraz make test w Core.
Nowy moduł albo poprawka błędu przychodzi razem z testem, a nowy układ albo płytka z projektem w .tests/.