niedziela, 23 stycznia 2022

uCUnit Test

Ten post chciałbym poświęcić na opisanie Framerowk-u do przeprowadzania testów. Dedykowany na mikrokontrolery.

[Źródło: http://www.ucunit.org/]

Projekt przetestuje na mikrokontrolerze STM32F4. Można go pobrać pod tym linkiem.

Ten framework testowy pozwala na jego implementację i dostosowanie do każdego układu w ciągu kilku minut. Wystarczy jedynie dołączyć trzy podstawowe pliki do projektu.

W projekcie należy utworzyć nowy folder zawierający pliki System.c, System.h oraz uCUnit.h


Następnie dodajemy ścieżki do plików we właściwościach projektu:
C/C++ General Paths and Symbols->Includes->GNU C oraz Source Location. Po wyczyszczeniu i przebudowaniu indeksów w projekcie pliki powinny być widoczne. 

Biblioteka wykorzystuje funkcję printf do przesyłania danych z testów na konsolę. Wobec tego można wykonać przekierowanie printf'a na konsolę przez modyfikację funkcji _write. 

Najpierw należy się upewnić, że odpowiednio został skonfigurowany interfejs UART. W przykładzie posłużę się interfejsem USART2. Potrzebne będzie jedynie wysyłanie danych:

  1. static void MX_USART2_UART_Init(void)
  2. {
  3.   huart2.Instance = USART2;
  4.   huart2.Init.BaudRate = 115200;
  5.   huart2.Init.WordLength = UART_WORDLENGTH_8B;
  6.   huart2.Init.StopBits = UART_STOPBITS_1;
  7.   huart2.Init.Parity = UART_PARITY_NONE;
  8.   huart2.Init.Mode = UART_MODE_TX_RX;
  9.   huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  10.   huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  11.   if (HAL_UART_Init(&huart2) != HAL_OK)
  12.   {
  13.     Error_Handler();
  14.   }
  15. }

Test komunikacji przeprowadzę w prosty sposób:

  1. HAL_UART_Transmit(&huart2, (uint8_t *)"Test123\r\n" , 9, 100);

Jeśli terminal odebrał wiadomość można przejść do poprawy funkcji _write i obsłużenia funkcji printf.

  1. printf("test");
  2. fflush(stdout);

Wykorzystanie samej funkcji printf może nie być wystarczające. Funkcja fflush wywoła wysłanie bufora, gdy do funkcji printf nie zostanie podany znak przejścia do nowej linii. 

Funkcja printf zalety wynikającej z łatwego sposobu jej wywołania ma też dość sporą wadę, gdyż zajmuje dosyć dużo pamięci mikrokontrolera. W przypadku dużych układów może to nie być problem, natomiast w przypadku wykonywania testów na małych układach dosyć szybko można dobić do końca pamięci. Dlatego może się okazać, że lepszym rozwiązaniem będzie przygotowanie własnego zestawu funkcji który będzie przesyłał dane na ekran. 

Poniżej opiszę strukturę elementów środowiska testowego oraz przedstawię jego proste zastosowanie na przykładzie. 

Wybranie sposobu wysyłania informacji o wykonanych testach:

  1. /**
  2.  * Verbose Mode.
  3.  * UCUNIT_MODE_SILENT: Checks are performed silently.
  4.  * UCUNIT_MODE_NORMAL: Only checks that fail are displayes
  5.  * UCUNIT_MODE_VERBOSE: Passed and failed checks are displayed
  6.  */
  7. //#define UCUNIT_MODE_NORMAL
  8. #define UCUNIT_MODE_VERBOSE

Do wyboru są trzy rodzaje wiadomości. Pierwszy z nich nie wysyła żadnych informacji o testach. Drugie rozwiązanie wysyła tylko informację tylko o niepoprawnych testach. Ostatni sposób przesyła informację o poprawnych i negatywnych testach.

Do dyspozycji znajduje się spora liczba makr pozwalających na wykonanie testu:

  1. #define UCUNIT_Check(condition, msg, args)             \
  2.     if ( (condition) ) { UCUNIT_PassCheck(msg, args); } else { UCUNIT_FailCheck(msg, args); }
  3. #define UCUNIT_CheckIsEqual(expected,actual)         \
  4.     UCUNIT_Check( (expected) == (actual), "IsEqual", #expected "," #actual )
  5. #define UCUNIT_CheckIsNull(pointer)                  \
  6.     UCUNIT_Check( (pointer) == NULL, "IsNull", #pointer)
  7. #define UCUNIT_CheckIsNotNull(pointer)               \
  8.     UCUNIT_Check( (pointer) != NULL, "IsNotNull", #pointer)
  9. #define UCUNIT_CheckIsInRange(value, lower, upper)   \
  10.     UCUNIT_Check( ( (value>=lower) && (value<=upper) ), "IsInRange", #value "," #lower "," #upper)
  11. #define UCUNIT_CheckIs8Bit(value)                      \
  12.     UCUNIT_Check( value==(value & 0xFF), "Is8Bit", #value )
  13. #define UCUNIT_CheckIs16Bit(value)                     \
  14.     UCUNIT_Check( value==(value & 0xFFFF), "Is16Bit", #value )
  15. #define UCUNIT_CheckIs32Bit(value)                     \
  16.     UCUNIT_Check( value==(value & 0xFFFFFFFF), "Is32Bit", #value )
  17. #define UCUNIT_CheckIsBitSet(value, bitno) \
  18.     UCUNIT_Check( (1==(((value)>>(bitno)) & 0x01) ), "IsBitSet", #value "," #bitno)
  19. #define UCUNIT_CheckIsBitClear(value, bitno) \
  20.     UCUNIT_Check( (0==(((value)>>(bitno)) & 0x01) ), "IsBitClear", #value "," #bitno)

Poniżej przedstawię kilka przykładów zastosowania powyższych Assercji. 

Zacznę od makr sprawdzających czy dany bit został ustawiony/wyczyszczony. 

  1. #define SET_BIT(val, n)     val |= 1UL << n;
  2. #define CLEAR_BIT(val, n)   val &= ~(1UL << n);
  3. #define TOOGLE_BIT(val, n)  val ^= 1UL << n;
  4.  
  5. uint8_t Set_Clear_Bit_U8(uint8_t val, uint8_t pos, uint8_t set_clear)
  6. {
  7.     if(set_clear == 1) { SET_BIT(val, pos); }
  8.     else if(set_clear == 0) { CLEAR_BIT(val, pos); }
  9.     return val;
  10. }
  11.  
  12. uint16_t Set_Clear_Bit_U16(uint16_t val, uint8_t pos, uint8_t set_clear)
  13. {
  14.     if(set_clear == 1) { SET_BIT(val, pos); }
  15.     else if(set_clear == 0) { CLEAR_BIT(val, pos); }
  16.     return val;
  17. }
  18.  
  19. uint32_t Set_Clear_Bit_U32(uint32_t val, uint8_t pos, uint8_t set_clear)
  20. {
  21.     if(set_clear == 1) { SET_BIT(val, pos); }
  22.     else if(set_clear == 0) { CLEAR_BIT(val, pos); }
  23.     return val;
  24. }

Przygotowane testy wyglądają następująco:

  1. static void Set_Clear_Bit_U8_Test_ValuesOK(void)
  2. {
  3.     UCUNIT_TestcaseBegin("\r\nSet_Clear_Bit_U8_Test_ValuesOK");
  4.  
  5.     uint8_t u8_val_test = 0x00;
  6.     u8_val_test = Set_Clear_Bit_U8(u8_val_test, 0, 1);
  7.     UCUNIT_CheckIsBitSet(u8_val_test, 0);
  8.  
  9.     u8_val_test = 0xFF;
  10.     u8_val_test = Set_Clear_Bit_U8(u8_val_test, 7, 0);
  11.     UCUNIT_CheckIsBitClear(u8_val_test, 7);
  12.  
  13.     UCUNIT_TestcaseEnd(); /* Fail */
  14. }
  15.  
  16. static void Set_Clear_Bit_U16_Test_ValuesOK(void)
  17. {
  18.     UCUNIT_TestcaseBegin("\r\nSet_Clear_Bit_U16_Test_ValuesOK");
  19.  
  20.     uint16_t u16_val_test = 0x00;
  21.     u16_val_test = Set_Clear_Bit_U16(u16_val_test, 0, 1);
  22.     UCUNIT_CheckIsBitSet(u16_val_test, 0);
  23.  
  24.     u16_val_test = 0xFFFF;
  25.     u16_val_test = Set_Clear_Bit_U16(u16_val_test, 15, 0);
  26.     UCUNIT_CheckIsBitClear(u16_val_test, 15);
  27.  
  28.     UCUNIT_TestcaseEnd(); /* Fail */
  29. }
  30.  
  31. static void Set_Clear_Bit_U32_Test_ValuesOK(void)
  32. {
  33.     UCUNIT_TestcaseBegin("\r\nSet_Clear_Bit_U32_Test_ValuesOK");
  34.  
  35.     uint32_t u32_val_test = 0x00;
  36.     u32_val_test = Set_Clear_Bit_U32(u32_val_test, 0, 1);
  37.     UCUNIT_CheckIsBitSet(u32_val_test, 0);
  38.  
  39.     u32_val_test = 0xFFFFFFFF;
  40.     u32_val_test = Set_Clear_Bit_U32(u32_val_test, 31, 0);
  41.     UCUNIT_CheckIsBitClear(u32_val_test, 30);
  42.  
  43.     UCUNIT_TestcaseEnd(); /* Fail */
  44. }

Jak można zobaczyć powyżej każdy test powinien rozpoczynać się od UCUNIT_TestcaseBegin() oraz kończyć UCUNIT_TestcaseEnd(). Pozwoli to na łatwe oddzielenie poszczególnych testów od siebie.

Wywołanie procedury testowej należy umieścić w funkcji main() projektu:

  1. int test(void)
  2. {
  3.     UCUNIT_Init();
  4.     UCUNIT_WriteString("\r\n**************************************");
  5.     UCUNIT_WriteString("\r\nName:     ");
  6.     UCUNIT_WriteString("uCUnit demo application");
  7.     UCUNIT_WriteString("\r\nCompiled: ");
  8.     UCUNIT_WriteString(__DATE__);
  9.     UCUNIT_WriteString("\r\nTime:     ");
  10.     UCUNIT_WriteString(__TIME__);
  11.     UCUNIT_WriteString("\r\nVersion:  ");
  12.     UCUNIT_WriteString(UCUNIT_VERSION);
  13.     UCUNIT_WriteString("\r\n**************************************");
  14.     Testsuite_RunTests();
  15.     UCUNIT_Shutdown();
  16.  
  17.     return 0;
  18. }

Przykładowy wynik testu z zastosowanie opcji Verbose:

  1. Init of hardware finished.
  2. *************************************
  3. Name:    
  4. uCUnit demo application
  5. Compiled: Jan 22 2022
  6. Time:     23:10:37
  7. Version:  v1.0
  8. **************************************
  9. ======================================
  10. Set_Clear_Bit_U8_Test_ValuesOK
  11. ======================================
  12. ../ucunit/test/Testsuite.c:49: passed:IsBitSet(u8_val_test,0)
  13. ../ucunit/test/Testsuite.c:53: passed:IsBitClear(u8_val_test,7)
  14. ======================================
  15. Testcase passed.
  16. ======================================
  17. ======================================
  18. Set_Clear_Bit_U16_Test_ValuesOK
  19. ======================================
  20. ../ucunit/test/Testsuite.c:64: passed:IsBitSet(u16_val_test,0)
  21. ../ucunit/test/Testsuite.c:68: passed:IsBitClear(u16_val_test,15)
  22. ======================================
  23. Testcase passed.
  24. ======================================
  25. ======================================
  26. Set_Clear_Bit_U32_Test_ValuesOK
  27. ======================================
  28. ../ucunit/test/Testsuite.c:79: passed:IsBitSet(u16_val_test,0)
  29. ../ucunit/test/Testsuite.c:83: passed:IsBitClear(u16_val_test,32)
  30. ======================================
  31. Testcase passed.
  32. ======================================
  33. **************************************
  34. Testcases: failed: 0          
  35. passed: 3
  36. Checks:    
  37. failed: 0          
  38. passed: 6
  39. **************************************

Przykładowy wynik testu z opcją Normal:

  1. Init of hardware finished.
  2. **************************************
  3. Name:    
  4. uCUnit demo application
  5. Compiled: Jan 22 2022
  6. Time:     23:15:54
  7. Version:  v1.0
  8. **************************************
  9. ======================================
  10. Set_Clear_Bit_U8_Test_ValuesOK
  11. ======================================
  12. ======================================
  13. Testcase passed.
  14. ======================================
  15. ======================================
  16. Set_Clear_Bit_U16_Test_ValuesOK
  17. ======================================
  18. ======================================
  19. Testcase passed.
  20. ======================================
  21. ======================================
  22. Set_Clear_Bit_U32_Test_ValuesOK
  23. ======================================
  24. ======================================
  25. Testcase passed.
  26. ======================================
  27. **************************************
  28. Testcases:
  29. failed: 0          
  30. passed: 3
  31. Checks:    
  32. failed: 0          
  33. passed: 6
  34. **************************************

Jak widać zostały okrojone informacje o poprawnym wykonaniu każdego z tych testów. Dopiero w przypadku błędu zostaną umieszczone dane o miejscu jego wystąpienia.

  1. Init of hardware finished.
  2. **************************************
  3. Name:     uCUnit demo application
  4. Compiled: Jan 22 2022
  5. Time:     23:21:46
  6. Version:  v1.0
  7. **************************************
  8. ======================================
  9. Set_Clear_Bit_U8_Test_ValuesOK
  10. ======================================
  11. ======================================
  12. Testcase passed.
  13. ======================================
  14. ======================================
  15. Set_Clear_Bit_U16_Test_ValuesOK
  16. ======================================
  17. ======================================
  18. Testcase passed.
  19. ======================================
  20. ======================================
  21. Set_Clear_Bit_U32_Test_ValuesOK
  22. ======================================
  23. ../ucunit/test/Testsuite.c:83: failed:IsBitClear(u32_val_test,30)
  24. ======================================
  25. ../ucunit/test/Testsuite.c:85: failed:EndTestcase()
  26. ======================================
  27. **************************************
  28. Testcases:
  29. failed: 1
  30. passed: 2
  31. Checks:    
  32. failed: 1          
  33. passed: 5
  34. **************************************

Poniżej opcja Silent:

  1. Init of hardware finished.
  2. **************************************
  3. Name:     uCUnit demo application
  4. Compiled: Jan 22 2022
  5. Time:     23:27:50
  6. Version:  v1.0
  7. **************************************
  8. ======================================
  9. Set_Clear_Bit_U8_Test_ValuesOK
  10. ======================================
  11. ======================================
  12. Testcase passed.
  13. ======================================
  14. ======================================
  15. Set_Clear_Bit_U16_Test_ValuesOK
  16. ======================================
  17. ======================================
  18. Testcase passed.
  19. ======================================
  20. ======================================
  21. Set_Clear_Bit_U32_Test_ValuesOK
  22. ======================================
  23. ======================================
  24. ======================================
  25. **************************************
  26. Testcases:
  27. failed: 1          
  28. passed: 2
  29. Checks:    
  30. failed: 1          
  31. passed: 5
  32. **************************************

Nie zostaną wyświetlone informacje o tym który test nie przeszedł poprawnie. Jedynie brak informacji Testcase passed oraz udostępnienie informacji w podsumowania pozwala na zlokalizowanie o miejscu błędu. 

Poniżej krótkie testy dla obsługi funkcji dodającej dwie wartości 32 bitowe:

  1. uint32_t AddFunction(uint32_t val1, uint32_t val2) {
  2.     uint64_t checkVal = (uint64_t)(val1) + (uint64_t)(val2);
  3.     if(checkVal >= UINT32_MAX) {
  4.         return 0;
  5.     }
  6.  
  7.     return (uint32_t)checkVal;
  8. }
  9.  
  10. static void TestAddFunction_ProperValue(void)
  11. {
  12.     UCUNIT_TestcaseBegin("\r\nTestAddFunction_ProperValue");
  13.  
  14.     uint32_t getValue = AddFunction(155, 954);
  15.     UCUNIT_CheckIsEqual( 1109, getValue );
  16.  
  17.     getValue = AddFunction(0xFFFFFFF1, 0xFF);
  18.     UCUNIT_CheckIsEqual( 0, getValue );
  19.  
  20.     UCUNIT_TestcaseEnd();
  21. }

W teście wykorzystuje makro które porównuje otrzymaną wartość z informacją jaką się spodziewam.

Oprócz wspomnianych asercji dostępne jest jeszcze sprawdzanie czy liczba jest w zadanym zakresie, sprawdzanie wskaźników, sprawdzenie program wchodził w odpowiednie miejsce (tracepoint). Przykłady wszystkich zastosowań można pobrać ze strony GitHub projektu ucUnit.

Opisywana biblioteka jest bardzo łatwy w użyciu. Składa się tylko z kilku plików i nie zajmuje dużej ilości miejsca. Przez co można go łatwo zaimplementować w każdym rozwiązaniu sprzętowym. Daje bardzo dużo możliwości w testowaniu całej aplikacji. Zaletą jej stosowania jest możliwość sprawdzania rozwiązań programowych bezpośrednio na urządzeniu. Co w niektórych przypadkach pozwoli na wyeliminowanie błędów, których może się nie udać wyłapać przy testach na zewnętrznych bibliotekach.