Skip to content
Mathias Plans edited this page Mar 24, 2022 · 12 revisions

The setup

There are three steps to the setup:

  1. Using GUnit affirmations
  2. Setting up a GDB server
  3. Using GUnit Python functions to execute the tests

Using GUnit affirmations

GUnit organizes tests into suites, one suite per file. Tests are encapsulated in gunit::test objects. Here is an example of a test

gunit::test testobj("Test name", [](){
  int x = 10;
  int y = 12;
  gunit::affirm(x + y) == 22; // Test will succeed
});

The gunit::test object's constructor needs two arguments: the name of the test, and the lambda function that has the test logic in it. There is also a third argument key which will be explained later.

The test logic uses GUnit affirmations. The affirmation consists of the object x + y, the relation ==, and subject 22. The object and subject can be any of any type, but those types have to be convertible to each other. The relation can be any of the comparison operators that are supported in C++ (except <=>). The affirmation will use the most suitable algorithm for comparing two objects.

Often it is unclear how to compare two objects because the objects might not include all the necessary information. For example, comparing two arrays requires knowledge of the length of the array. In this case, the length must be given to the affirmation:

gunit::test testobj("Test name", [](){
  int array1[] = {3, 2, 1};
  int array2[] = {3, 4, 5};

  // If length is not given, the affirmation will compare pointers
  gunit::affirm(array1, 3) != array2;
});

One test can have multiple affirmations in it. When an affirmation fails, the code that follows it will not be executed and the test will be exited immediately. The GUnit will report the failure in the test report.

gunit::test testobj("Test name", [](){
  int x = 10;
  int y = 12;
  gunit::affirm(x + y) == 23; // Test will fail ..

  // .. therefore this code will not be executed
  int z = 2;
  gunit::affirm(y + z) == 4;
});

Sometimes it is useful to fail deliberately. One way would be to create an affirmation that will always be wrong. But GUnit offers another function that also takes in the reason for the failure as the argument.

gunit::test failtest("Alwasy fail", [](){
   gunit::fail("This test fails no matter what!");
});

The suite

All the tests defined in the same file will be in a single suite which will be named after the file. To have multiple suites, defined tests in different source files. The suite is wrapped up by the call to gunit::suite::run. It takes three arguments: the function to be executed before each unit test, the function the be executed after each successful unit test, and the function to be executed after each failed test. Usually, the last two arguments are the same. The gunit::end has to be called after all the suites have executed their tests. This will signal the GUnit to end its gdb phase.

void before() {
  // do some setup
}

void after() {
  // do some deinitialization
}

int main() {
  gunit::suite::run(before, after, after);
  gunit::end();
  return 0;
}

Here is an example of a test suite:

#include "GUnit.hpp"

gunit::test t1(
  "test1",                  // Name of the test case
  [](){                     // Lambda expression
    gunit::affirm(20) > 10; // The test case to check
  }
);

gunit::test t2("test2", [](){
  gunit::affirm(10) > 20;
  gunit::affirm(11) > 20;
});

gunit::test t3("test3", [](){
  std::list l1 = std::list<int>();
  l1.push_back(10);
  l1.push_back(12);
  l1.push_back(14);

  std::list l2 = std::list<int>();
  l2.push_back(10);
  l2.push_back(12);
  l2.push_back(14);

  gunit::affirm(l1) == l2;
});

gunit::test t4("test4", [](){
  std::list l1 = std::list<int>();
  l1.push_back(10);
  l1.push_back(12);
  l1.push_back(14);

  std::list l2 = std::list<int>();
  l2.push_back(10);
  l2.push_back(22);
  l2.push_back(34);

  gunit::affirm(l1) == l2;
});

gunit::test t5("test5", [](){
  struct a {
    int b;
    char c;
    char d;
    char e;
    char f;
  };

  struct a one(10, 'a', 'e', 'i', 'o');
  struct a two(10, 'a', 'e', 'i', 'o');

  gunit::affirm(one) == two;
});

gunit::test t6("test6", [](){
  struct a {
    int b;
    char c;
    char d;
    char e;
    char f;
  };

  struct a one(10, 'a', 'e', 'i', 'o');
  struct a two(22, 'x', 'e', 'o', 'm');

  gunit::affirm(one) == two;
});

gunit::test t7("test7", [](){
  gunit::affirm(10) > 5;
  gunit::affirm(10) >= 10;
  gunit::affirm(10) <= 10;
  gunit::affirm(10) <= 15;
  gunit::affirm(1.0) == 1.0;
  gunit::affirm(2.0) != 1.0;
});

int main() {
  gunit::suite::run(); // The test are added into the global test list on creation of the test struct
  gunit::end();        // Signal the end of the unit test
  while(1);
}

Setting up a GDB server

This is not handled by GUnit. The server has to be set up by the user. GUnit will act as a client.

Using GUnit Python functions to execute the tests

This step depends on the target server.

Object creation

Different targets have different initialization and loading sequences. For this reason, different constructors exist

GUnit.gdbserver(gdb_uri, build_dir='.')

This is the constructor for the gdbserver client. The arguments are gdb_uri, which is the address to the GDB server, and build_dir, which is the path where the test report will be created.

GUnit.openOCD(gdb_uri, build_dir='.', executable='gdb')

This is the constructor for the OpenOCD client. The arguments are gdb_uri, which is the address to the GDB server, build_dir, which is the path where the test report will be created, and executable, which determines what GDB executable should be used. The executable can be arm-none-eabi-gdb for ARM MCUs, for example.

Testing

After the GUnit object is created, tests can be executed. Here is an example program:

gunit = GUnit.openOCD('localhost:9000', executable='arm-none-eabi-gdb')
gunit.test('test.elf')
gunit.junit()

GUnit.test(self, path=None)

TODO: why can the path be None? Calling this will execute the tests and generate a raw test report. The argument path has to be set to the compiled executable which uses the macros (as shown above). Note that the executable has to include debug symbols and variables need to be tracked. As such, -fvar-tracking and -g compiler flags should be used when compiling for GUnit.

GUnit.junit(self)

This function will convert the raw test report into JUnit report. JUnit is standard, so the file can be provided to Jenkins, for example. The JUnit report will be created next to the raw report.

Miscellaneous

GUnit.get_header(build_dir='.')

This will generate GUnit.hpp.