Skip to content

tests.pl

Xaeian edited this page Oct 26, 2026 · 1 revision

Testy

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.

Program testowy

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.

Jeden moduł, dwa buildy

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 jednym TEST(name).
  • TEST_Check(cond) albo TEST_Check(cond, fmt, ...) sprawdza warunek, a TEST_Equal, TEST_Near i TEST_Bytes przy niepowodzeniu pokazują wyrażenie w postaci z kodu, jego wartość i wartość oczekiwaną.
  • TEST_Logged(text) sprawdza, czy firmware zapisał w logu linię z text od początku przypadku albo od poprzedniego TEST_Logged.
  • TEST_Run(ms) wykonuje ms kroków czasu wirtualnego, a każdy krok to tick zegara i wszystkie zadania, na które przyszła pora.

Uruchamianie

make test              # wszystkie programy z `.tests/`
make test T=test_ramp  # tylko `.tests/test_ramp.c`, z jego wyjściem

Każ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.

Zasady

  • Czekanie przesuwa zegar: delay() i timeout() 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 w build/.
  • #define przed #include może zmienić ustawienia dołączanego modułu, np. LOG_LEVEL. Rozmiar albo typ wspólny z resztą Core, np. BUFF_MSG_LIMIT, ustawia się w main.h, bo inaczej test i Core po cichu się rozjadą.
  • Core trafia do testu skompilowany z hal/host i lib, 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ł z plc/, brd/, drv/ albo hal/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.

GitHub

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.1

Workflow 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

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 go

Przy 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/.

Clone this wiki locally