Skocz do zawartości

ESP-IDF Platformio i Google (Unit) Test, Mockup native


Gieneq

Pomocna odpowiedź

Szukałem lepszego środowiska od Eclipsowego Espressif IDE - nie ma w nim nic złego, ale nie ma tego czego potrzebuję. Pomyślałem że dam szansę Platformio - makerska asocjacja trochę mnie odstraszała, ale wygląda że jest to naprawdę udane środowisko, a na pewno dynamiczniej rozwijane od Eclipsa. Ciekawe że Google nie podpowiedział mi tego tematu, może Platformio nie jest zbyt dobrze zaindeksowane.

Potrzebowałem:

  1. wybrać pliki niezwiązane ze sprzętem i skompilować na maszynie hosta (windowsie) np. z G++,
  2. wykonać testy jednostkowe z GoogleTest wybranych plików,
  3. skompilować cały projekt dla targetu i wgrać na platformę docelową.

Można to zrobić ręcznie:

  1. dodając osobny plik cmake wybierający pliki źródłowe do kompilacji i linkujący google test,
  2. podlinkować nagłówki w wybranym IDE np VSCode,
  3. odpalić testy poleceniem ctest.

W platformio da się to jednak połączyć i najwyraźniej uprościć. Na pewno da się też więcej, ale póki co więcej nie potrzebuję.

Przeglądamy 2 linki:

Po założeniu projektu z frameworkiem ESP-IDF edytuję plik .ini dla 2 wariantów:

  • esp32-xx - kod dla platformy docelowej,
  • native - kod kompilowany z G++ dla windowsa, np z MinGW,

Oba warianty da siętestować, ale wariant dla ESP wymaga wgrania testów na płytkę i to trochę mija się z celem przyspieszenia testowania bez powolnej pętli: kompiluj, wgraj, sprawdź wyniki.

[env:esp32-c3-devkitm-1]
platform = espressif32
board = esp32-c3-devkitm-1
framework = espidf
monitor_speed = 115200
test_framework = googletest
test_ignore = test_desktop test_animations
build_flags = -D USING_ESPIDF=1

[env:native]
platform = native
test_framework = googletest
build_flags = -D USING_NATIVE=1

W katalogu test zakładam podkatalogi test_desktop test_animations z testami.

Kod który testuję to kontroler animacji diody RGB. Składa się z kontrolera sterowanego czasem, kilku parametrów, callbacka do obiektu implementującego funkcję sterującą. Wydzielam z kontrolera bazę AnimationController którego zadaniem jest podstawowa funkcjonalność i jego klasa potomna z konkretnym przypadkiem.

Animation.h

#pragma once
#include "algorithm"

class AnimationController {
public:
    virtual ~AnimationController() = default;
    void start();
    void stop();
    void tick(float dt);
    bool looping{false};
    void set_interval(float interval);
    bool is_running();

protected:
    virtual void on_animation_frame(float progress_norm);
    bool running{false};
private:
    float interval{1000};
    float accumulator{0};
};

Animation.cpp

#include "Animation.h"

void AnimationController::start() {
    running = true;
    accumulator = 0;
}

void AnimationController::stop() {
    running = false;
}

void AnimationController::tick(float dt) {
    if(running) {
        accumulator += dt;
        if(accumulator > interval) {
            accumulator -= interval;
            if(!looping){
                accumulator = 0;
                stop();
            }
        }

        float progress_norm = accumulator / interval;
        progress_norm = std::max(0.0F, progress_norm);
        progress_norm = std::min(1.0F, progress_norm);
        on_animation_frame(progress_norm);
    }
}

bool AnimationController::is_running() {
    return running;
}

void AnimationController::set_interval(float interval) {
    this->interval = interval;
    accumulator = std::min(accumulator, interval);
}

void AnimationController::on_animation_frame(float progress_norm) {
    // override
}

AnimationRGB.h

#pragma once
#include "Animation.h"
#include "AnimationListener.h"

enum class RGBAnimationType:int {
    NONE = -1,
    CYCLE = 0,
};

class AnimationRGBController : public AnimationController {
public:
    inline void set_animation_listener(AnimationRGBListener* listener) {
        this->listener = listener;
    }
RGBAnimationType type{RGBAnimationType::NONE};
protected:
    virtual void on_animation_frame(float progress_norm) override;
private:
    AnimationRGBListener* listener;
};

AnimationRGB.cpp

#include "AnimationRGB.h"


void AnimationRGBController::on_animation_frame(float progress_norm) {
    if (listener) {
        if(type == RGBAnimationType::NONE)
            listener->on_animation(progress_norm, 0, 0, 0);
        else {
            float r{255};
            float g{255};
            float b{255};
            //todo do it better
            listener->on_animation(progress_norm, r, g, b);
        }
    }
}

AnimationListener.h

#pragma once
#include "stdint.h"

class AnimationRGBListener {
public:
    virtual ~AnimationRGBListener() = default;
    virtual void on_animation(float progrs_norm, uint8_t r, uint8_t g, uint8_t b) = 0;
};

Funkcja main może wyglądać tak - dla wariantu native musi być funkcja main bo się nie skompiluje:

#ifdef USING_ESPIDF
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "Device.h"

Device device;

extern "C" {
    void app_main() {
        device.init();

        while (1) {
            device.tick();
            vTaskDelay(1);
        }
    }
}
#endif

#ifdef USING_NATIVE
int main(int argc, char **argv) {
    return 0;
}
#endif

 

Testy

Kod testujący w pliku test/test_animations/test_main.cpp

#include <gtest/gtest.h>
#include <gmock/gmock.h> 

#include "AnimationRGB.h"
#include "AnimationListener.h"

using ::testing::AtLeast;
using ::testing::_;

TEST(AnimationTest, ShouldCreateController)
{
    AnimationController ctrl;
    
    EXPECT_FALSE(ctrl.looping);
    EXPECT_FALSE(ctrl.is_running());
}

class MockAnimationRGBListener : public AnimationRGBListener {
public:
    MOCK_METHOD( void, on_animation, (float progrs_norm, uint8_t r, uint8_t g, uint8_t b), (override) );
};

TEST(RGBAnimationTest, ShouldTickAnimationOnce) {
    AnimationRGBController rgb_ctrl;
    MockAnimationRGBListener device;

    EXPECT_CALL(device, on_animation(_,_,_,_)).Times(AtLeast(1));
    rgb_ctrl.set_animation_listener(&device);

    rgb_ctrl.start();
    rgb_ctrl.tick(10.0F);
}


TEST(RGBAnimationTest, UseDefaultAnimation) {
    constexpr float animation_interval{2000.0F};
    AnimationRGBController rgb_ctrl;
    MockAnimationRGBListener device;

    EXPECT_EQ(rgb_ctrl.type, RGBAnimationType::NONE);

    EXPECT_CALL(device, on_animation(0.5F,0,0,0)).Times(AtLeast(1));

    rgb_ctrl.set_interval(animation_interval);
    rgb_ctrl.set_animation_listener(&device);

    rgb_ctrl.start();
    rgb_ctrl.tick(1000.0F);
}


int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    if (RUN_ALL_TESTS()){}
    return 0;
}

Odświeżam widok pod ikoną testów, daję filtr dla native i przechodzą:

image.thumb.png.d0803ce2c48962012921979afa1ee197.png

Test zajmuje 16 sekund - dużo mniej niż wgranie czegoś na płytkę i obejrzenie rezultatów czy odpalenie debugera.

W kodzie widać użycie mockapu - myślę że to ciekawa koncepcja, bo w przypadku interface jak tu można sprawdzić ile razy callback został wykonany i jakie miał wartości:

EXPECT_CALL(device, on_animation(0.5F,0,0,0)).Times(AtLeast(1));

Jak nie potrzeba jakiejś wartości, to można ją dopasować do jakiejkolwiek używając _.

W tym cyklu rozwijania oprogramowania, gdy testy przejdą i mam skończoną funkcjonalność to wgrywam na platformę docelową:

image.thumb.png.86889551a71ab5ad98c50aed6b8f2517.png

Mam jedynie wątpliwość jak działa dodawanie plików źródłowych. Używając samego cmake kompilacja i uruchomienie zdecydowanie większego kodu zajmuje z 4 sekundy. Tu mam za to bardzo ładne informacje zwrotne przypadku niepowodzenia:

image.thumb.png.58cf7d407be648816f7bec75050e35a8.png

 

Jak ktoś ma swoje doświadczenia i lepszy pomysł na użycie Platformio to chętnie się zapoznam 🙂 

 

  • Lubię! 2
Link do komentarza
Share on other sites

Drobna aktualizacja - temat nieco komplikuje się gdy testowany kod jest komponentami ESP-IDF. Przygotowałem szablon który może być pomocny w postawieniu projektu. W repo jest opis jak krok po kroku przygotować build system: https://github.com/Gieneq/TemplateTargetNativeGTest

 

  • Lubię! 1
Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.