Debugger-based On-Target Testing
|
Component testing with DOTT is also referred to as 'halt mode' testing. This essentially means DOTT is used to download the firmware to the target and boot it up and then halt it in a defined location called the DOTT test hook. From there, functions implemented in the firmware can be called in a component/unit testing style.
To execute the included component test examples please use the reference target platform describe in setup section.
This section shows how to run component (unit) test examples where software functions implemented in the target's firmware are called from tests implemented on the host (cp. DOTT architecture).
First, open a Python command prompt (and activate the DOTT venv if using a virtual environment). In the command prompt now navigate to the dott_ng_doc_examples_xxx\examples\01_component_testing
of the dott examples package downloaded from GitHub:
As you can see in the snippet above, the folder contains host and a target folders. The target folder contains the firmware under test which is downloaded to the target device as part of test execution. The target folder contains both the source code as well as pre-compiled firmware binaries. The host folder contains the actual tests implemented in Python. Now lets change into the host folder and invoke pytest to start the test execution:
Upon first execution you will be prompted with the following license information dialog from the Segger J-Link debug probe. Please make sure to tick the checkbox on the bottom left (highlighted in green) and then click accept (highlighted in green). Please note that you will get this message only when using the reference board with an ST-Link converted to a J-Link. If you are using a standalone J-Link debug probe you will not get this message.
As a result, you should see an output similar to this:
The following sections show selected functions implemented in the firmware under test (executed on the reference board) and the corresponding tests implemented in Python on the host. The source files for the example functions under test and corresponding host-side tests are given in the table below. Note that not all examples in the source files are discussed here.
Content | Source File |
---|---|
Firmware functions on target | examples/01_component_testing/target/testexamples.c |
Corresponding host-side tests | examples/01_component_testing/host/test_example_functions.py |
The following examples show how target functions are called by their symbol name from the host and how scalar parameters and return values are handled. Note that it is also no problem to call static functions.
Target functions
The following functions take either no argument or take scalars as arguments and return scalar results.
Host-side tests
On the host, the target functions are called in PyTest test cases shown below. Note that the tests take test fixtures as arguments. DOTT offers pre-defined test fixtures for re-occurring tasks such as downloading the firmware onto the target or resetting the target. Note that for most tests it is important to bring the target into a defined, well known state before starting with the test execution. More information on DOTT test fixtures can be found in the Developer's Guide.
The following examples illustrate how to call target functions which take or return pointers or structs. In contrast to simple scalars, memory on the target must be allocated which is then loaded with the intended values. Note that this does not imply the existence of a heap on the target but a DOTT-specific mechanism is used for memory allocation. Details on that are provided in the Developer's Guide.
Target functions with pointer arguments
A target function example taking three pointers, one used for the result of the addition and two for the operands.
Host-side tests with pointer arguments
First, memory has to be allocated on the target. For memory allocation and initialization a DOTT function called mem.alloc_type is used. Note that this function does not require dynamic memory (heap) in the target. Details on on-target memory allocation are provided in the Developer's Guide. The allocated memory can be accessed either via GDB convenience variables (starting with a $ prefix) or via their addresses as returned by mem.alloc_type. The names of the GDB convenience variables can be set as optional arguments when calling mem.alloc_type.
The following two, functionally identical, tests show how to call a target function taking pointer arguments. The first variant uses GDB convenience variables while the second one directly uses the memory addresses returned by mem.alloc_type in Python f-Strings.
Target functions with struct arguments
This on-target function takes a struct as argument and performs an addition of two of the struct elements and saves the result in another struct element.
Host-side tests with struct arguments
In the host-side test, again on-traget memory is allocated for the struct and the address is stored in $add_data. The struct elements are then filled and the The de-referenced struct pointer is then passed as argument to the target function example_AdditionStruct.
Note that filling the (on-target) struct elements with desired values is a relatively slow process since every value setting operation translates to a single debugger memory access via GDB. To enhance performances, target memory can be also accessed in bulk operations which leads to a performance improvement. Details on this can be found in the Developer Guide.
With DOTT is is also possible to pass strings and other array data to target functions which is illustrated in this example.
Target functions
The first example implements a function returning the length of a string. The second example takes an array of integers and returns the sum of the elements.
Host-side tests
On the host, for both tests first memory has to be allocated on the target to hold the string/ array content. Next, the array on target has to be filled with content using the mem.write function provided by DOTT. As a convenience feature, initial initialization can also be performed when calling mem.alloc_type. Note that the second examples uses Python's ctypes module to convert the Python integer array into an array of uint16_t types. Thereafter, the example functions on the target can be called and the results can be checked against the expected ones.
This example demonstrates how to to deal with target functions which take function pointers as arguments.
Target functions
On the target, two functions are defined taking two integer arguments, performing and addition or subtraction, and returning the result. A third function, example_CustomOperation, takes three arguments: A function pointer and two integer operands. It calls the function pointed two by the function pointer with the two integers as arguments and returns the result.
Host-side tests
On the host, a test is implemented which first gets the address of subtraction target function (example_FunctorSub). Next, it calls the target's example_CustomOperation function and passes this address as first argument and checks the result. It then performs the same steps with example_FunctorAdd.
An important part when testing is to inject well-defined data into the test execution. With DOTT this is possible by overriding the return values of functions which are called by other functions. Technically, this is done by setting breakpoints in these sub functions and altering the return values of these functions. This is illustrated in the following example.
Target functions
On the target, there is a function (example_AdditionSubcalls) performing an addition of two values which are supplied by two sub functions, example_GetA and example_GetB. Note that these two sub functions are marked with DOTT_NO_OPTIMIZE which prevents the compiler from optimizing out these two functions and hence making the example fail. More information on DOTT_NO_OPTIMIZE is provided in the Developer's Guide.
Host-side tests
On the host, the first test calls the exmaple_AdditionSubcalls function without any injection. In the next test, two InterceptPoints are set up to intercept the two sub-functions example_GetA and example_GetB.
Note: An intercept point is a a special variant of a breakpoint. DOTT supports different types of breakpoints where not all of them can be used under all circumstances. Please check the Developer Guide for additional details.
In the intercept points' reached methods the return values of the functions are altered. The intercept points are located at function entry. The calls to ret() let the functions return and hence the function bodies are not executed. Instances of the two intercept point classes are created and attached to the target functions. After calling example_AdditionSubcalls the return value reflects the data that was injected into the sub-functions.
Note: Using a DOTT_LABEL it is also be possible to perform the interception of the sub-functions not only on function entry but at arbitrary locations within the function. Please check the Developer Guide for additional details.