Unit Testing

Introduction

In the tutorials so far we have introduced many features of the FSDK which you can use to produce spacecraft flight software, but we haven’t yet discussed mechanisms you can use to ensure the code you build using the FSDK is correct.

To make best use of reusable Components in space systems (where the consequences of failure can be painful!) you need to have confidence in the Components you use, and you need to give others confidence in the Component you write.

To help with this, the codegen and FSDK build system have Unit Testing support built into them.

This tutorial will show you how to use this Unit Testing support when testing your own Components.

In this tutorial, you will:

  • Learn how to use codegen to generate unit test files for a given Component Type.

  • Learn about the basic structure used for Unit Testing Component Types.

  • Create, implement and run some Unit Tests for the SimpleImager Component Type you worked on in the previous tutorial.

Before You Begin

The GEN1_ROOT environment variable should be set to the FSDK root directory. This is used by codegen to locate the necessary files and directories.

Open a terminal in the FSDK root directory and navigate to the Documentation/Tutorials directory using the following command.

 cd Documentation/Tutorials

Execute the script to set the GEN1_ROOT environment variable.

 . set_gen1_root.sh

Echo the GEN1_ROOT environment variable to confirm it has been set to the correct directory.

 echo $GEN1_ROOT

If this command fails, check that GEN1_ROOT is correctly set.

Unity and CMock

The FSDK uses two third-party tools to assist with Unit Testing Component code:

  • Unity, a Unit Test framework built for embedded C applications, and

  • CMock, a mock generator designed to work with Unity.

Unity provides helper functions and a set of assertions for composing Unit Tests.

CMock is used to generate mocks. These are functions used to fulfil the requirements of the code being tested in place of the real functions, which are often unavailable when Unit Testing.

For example, when working on code which commands Deployment of a solar array, it would be preferable to mock the function responsible for handling that command. CMock can be used to check that the function is called correctly without requiring the code, or the attached solar array hardware, to be present. With the correct selection of mocks, code which is dependent on embedded hardware can be tested in a low risk environment - like on your Linux development machine.

There is a wealth of information about Unit Testing available online. In this tutorial we assume a basic familiarity with the concepts involved, and focus on how to write and run tests when working with the FSDK.

We will cover various Unity and CMock features we often use, and where relevant we will link to the documentation provided by those projects.

Unit Tests in the FSDK

In the FSDK Component Types are the principle unit of reuse. This means that it is reasonable to expect a given Component Type to work correctly when dropped into new Deployments.

To try and ensure that a Component Type will work correctly in new contexts, we typically write a set of tests which attempt to cover all of the interfaces of a Component Type.

This usually means:

  • For Service providers, writing tests which make sure they implement each operation of that Service correctly.

  • For Components with Actions and Parameters, writing tests which invoke each Action with various Arguments, and get and set each Parameter with various values, to make sure each case is handled correctly.

  • For Service consumers, mocking the Service operations which the Component Type calls. This gives us opportunity to make sure the correct Service operations are issued at the correct times.

For complex Components there are more interfaces to consider, but they follow the same principles: exercise your inputs, and mock your requirements.

Testing SimpleImager

In the following sections you will create, implement and run a Unit Test to demonstrate the SimpleImager Component Type's loadSettings Action, which you implemented in previous tutorial.

If you haven’t yet completed the previous tutorial, we suggest you do so now.

Creating the Component Type

For the sake of brevity we will quickly create a new components library and add the SimpleImager Component Type to it and finish the implementation we worked through in the previous tutorial.

Create a new component library to store the component type using the following command:

codegen library new components

The above command generates the following directory structure within your working directory for the new library:

components
├── config
|   └── project.mk
├── inc
├── src
├── test
|   └── src
|       └── common
|           └── deployment
|               └── Deployment.c
└── Makefile

The project.mk file controls how the library is built, and the Deployment.c file is used to facilitate building and running unit tests.

This library can now be used to hold new component types.

Since we are using FSS, we need to include the framework directory in the component library’s project.mk.

Update component libraries project.mk to include the framework directory.

# Dependencies (library directories)
DEPEND_DIRS = $(OBSW_ROOT)/framework

We use OBSW_ROOT to refer to the root directory of the Flight Software Development Kit repository. You wil need to set GEN1_ROOT environment variable as shown in: Before you Begin

While we are here we should also increase our DUTIL_LOG_LEVEL to 3 to enable more verbose logging.

Update component libraries DUTIL_LOG_LEVEL to 3.

# Preprocessor flags
CPPFLAGS += -DUTIL_LOG_LEVEL=3

Create the Component Type using the following command:

codegen componenttype new components --name SimpleImager

This creates a componentType.xml for your new Component Type in the components/inc/SimpleImager directory.

Open the componentType.xml file.

Complete the Component Type definition by adding the following content to the componentType.xml file:

Show the completed componentType.xml file
<?xml version="1.0" encoding="UTF-8"?>
<!--
    This file describes the SimpleImager component type.
-->
<ModelElement xmlns="http://www.brightascension.com/schemas/gen1/model"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <ComponentType name="SimpleImager">
    <Description>
      The SimpleImager component.
      <!-- Note: The order of the tags is important and should not be changed.  -->
    </Description>
    <!-- Component Imports -->
    <!-- Exceptions (Status Codes) -->
    <!-- Events -->
    <!-- Required Components -->
    <!-- Services (Provided and Required) -->
    <Services>
      <Required>
        <Service name="fs" type="io.FSS">
          <Description>
            Access to the file system where SimpleImager settings files are found
          </Description>
        </Service>
      </Required>
    </Services>
    <!-- Tasks -->
    <!-- Event Sources -->
    <!-- Event Sinks -->
    <!-- Actions -->
    <Actions>
      <Action name="captureImage">
        <Description>
          Capture an image, using the settings previously loaded by loadSettings
        </Description>
      </Action>
      <Action name="loadSettings">
        <Description>
          Load imager settings from the given file
        </Description>
        <Argument name="settingsPath" minBytes="2" maxBytes="127">
          <Description>
            The absolute path to the settings file to be loaded
          </Description>
          <Documentation><Markdown>
            This path *must* start with a `/` character and have at least 1
            following character.
          </Markdown></Documentation>
        </Argument>
      </Action>
    </Actions>
    <!-- Parameters -->
    <Parameters>
      <Parameter name="settingsLoaded" readOnly="true">
        <Description>
          Indicates whether imager settings were successfully loaded using loadSettings
        </Description>
        <Value type="bitfield" bits="1"/>
      </Parameter>
      <Parameter name="exposureTime" readOnly="true">
        <Description>
          The exposure time, in milliseconds, loaded from the settings file
        </Description>
        <Value type="unsigned" bits="16"/>
      </Parameter>
      <Parameter name="imageHeight" readOnly="true">
        <Description>
          The image height, in pixels, loaded from the settings file
        </Description>
        <Value type="unsigned" bits="16"/>
      </Parameter>
      <Parameter name="imageWidth" readOnly="true">
        <Description>
          The image width, in pixels, loaded from the settings file
        </Description>
        <Value type="unsigned" bits="16"/>
      </Parameter>
    </Parameters>
    <!-- Implementation -->
  </ComponentType>
</ModelElement>

Generate the Component Type implementation files using the following command:

codegen componenttype generate components --name SimpleImager

Complete the SimpleImager.c file by adding the following content:

Show the completed SimpleImager.c file
/**
 * @file SimpleImager.c
 * @brief Brief description of file.
 *
 * Full description of file
 *
 * @author
 * @date
 */
/*
 * Copyright (c) Bright Ascension Ltd, 2023
 * Licensed Material - Property of Bright Ascension Ltd
 */

#include "core/types.h"
#include "core/status.h"
#include "SimpleImager/SimpleImager.h"
#include "SimpleImager/SimpleImager_Container_package.h"
#include "SimpleImager/SimpleImager_Parser_package.h"
#include "SimpleImager/SimpleImager_FSS_package.h"
#include "SimpleImager/SimpleImager_config.h"
#include "task/Task_ProtectionLock.h"
#include "util/Util_Log.h"
#include "util/Util_String.h"
#include <string.h>

/* Doxygen commands to add this component to the generated documentation */
/**
 * @addtogroup SimpleImager
 * @{
 */

/*---------------------------------------------------------------------------*
 * Global variables
 *---------------------------------------------------------------------------*/

/** The lock access timeout */
static const ShortTime_t gt_LockTimeout = SIMPLEIMAGER_CONFIG_LOCK_TIMEOUT;

/*---------------------------------------------------------------------------*
 * Public functions
 *---------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------*
 * Carry out local component initialisation for the SimpleImager component
 *---------------------------------------------------------------------------*/

status_t SimpleImager_localInit
(
    SimpleImager_t *pt_SimpleImager,
    const SimpleImager_Init_t *pt_InitData
)
{
    /* Initialise the protection lock */
    return Task_ProtectionLock_init(
        &pt_SimpleImager->t_Lock,
        SIMPLEIMAGER_CONTAINER_TASKING_TYPE(pt_SimpleImager));
}

/*---------------------------------------------------------------------------*
 * Carry out local component finalisation for the SimpleImager component.
 *---------------------------------------------------------------------------*/

status_t SimpleImager_localFini
(
    SimpleImager_t *pt_SimpleImager
)
{
    /* Finalise the protection lock */
    return Task_ProtectionLock_fini(&pt_SimpleImager->t_Lock);
}

/*---------------------------------------------------------------------------*
 * Capture an image, using the settings previously loaded by loadSettings
 *---------------------------------------------------------------------------*/

status_t SimpleImager_captureImage
(
    SimpleImager_t *pt_SimpleImager
)
{
    status_t t_Status;          /* The current status */

    TASK_PROTECTED_START(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status)
    {
        if (pt_SimpleImager->b_SettingsLoaded)
        {
            /* This is not a real imager! Just print out the settings we would
             * use to capture an image. */
            UTIL_LOG_INFO("Capturing image with loaded settings:");
            UTIL_LOG_INFO(
                "  Exposure time: %u ms",
                pt_SimpleImager->t_Settings.u16_ExposureTime);
            UTIL_LOG_INFO(
                "  Image height:  %u px",
                pt_SimpleImager->t_Settings.u16_ImageHeight);
            UTIL_LOG_INFO(
                "  Image width:   %u px",
                pt_SimpleImager->t_Settings.u16_ImageWidth);
        }
        else
        {
            UTIL_LOG_ERROR("Cannot capture an image with no settings loaded!");
            UTIL_LOG_ERROR("Invoke SimpleImager.loadSettings first");
            t_Status = STATUS_FAILURE;
        }
    }
    TASK_PROTECTED_END(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);

    return t_Status;
}

/*---------------------------------------------------------------------------*
 * Load imager settings from the given file
 *---------------------------------------------------------------------------*/

status_t SimpleImager_loadSettings
(
    SimpleImager_t *pt_SimpleImager,
    const ui8_t *pu8_SettingsPath,
    ui8_t u8_Length
)
{
    status_t t_Status;          /* The current status */

    /* Check the length of the argument is valid */
    if (u8_Length < SIMPLEIMAGER_LOAD_SETTINGS_MIN_ARG_SIZE)
    {
        /* Error: invalid argument length */
        t_Status = STATUS_INVALID_PARAM;
    }
    else if (u8_Length > SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE)
    {
        /* Error: invalid argument length */
        t_Status = STATUS_INVALID_PARAM;
    }
    else
    {
        /* Make a copy of pu8_SettingsPath guaranteed to be null terminated */
        ui8_t ru8_SettingsPath[SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE + 1];

        strncpy(
            (string_t)&ru8_SettingsPath[0],
            (cstring_t)pu8_SettingsPath,
            u8_Length);
        ru8_SettingsPath[u8_Length] = 0;

        TASK_PROTECTED_START(
            &pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status)
        {
            Service_SelectionID_t t_SettingsFile;

            /* We haven't loaded any settings yet. */
            pt_SimpleImager->b_SettingsLoaded = FALSE;

            t_Status = SimpleImager_FSS_openFileFs(
                        pt_SimpleImager,
                        NULL,
                        (cstring_t)&ru8_SettingsPath[0],
                        FALSE,
                        FALSE,
                        FSS_ACCESSMODE_READ_ONLY,
                        FALSE,
                        &t_SettingsFile);

            if (t_Status == STATUS_SUCCESS)
            {
                ui8_t ru8_SettingsData[SIMPLEIMAGER_PARSER_MAX_SETTINGS_BYTES];
                ui32_t u32_SettingsDataLen = ARRAY_COUNT(ru8_SettingsData);
                status_t t_CloseStatus;

                t_Status = SimpleImager_FSS_readFileFs(
                            pt_SimpleImager,
                            NULL,
                            t_SettingsFile,
                            &ru8_SettingsData[0],
                            &u32_SettingsDataLen);

                if (t_Status == STATUS_SUCCESS)
                {
                    t_Status = SimpleImager_Parser_parseSettings(
                                &ru8_SettingsData[0],
                                u32_SettingsDataLen,
                                &pt_SimpleImager->t_Settings);
                }

                if (t_Status == STATUS_SUCCESS)
                {
                    UTIL_LOG_INFO(
                        "Successfully loaded settings from '%s'!",
                        (cstring_t)&ru8_SettingsPath[0]);
                    pt_SimpleImager->b_SettingsLoaded = TRUE;
                }

                t_CloseStatus = SimpleImager_FSS_closeFileFs(
                                    pt_SimpleImager, NULL, t_SettingsFile);

                if (t_CloseStatus != STATUS_SUCCESS)
                {
                    UTIL_LOG_ERROR(
                        "Failed to close settings file '%s'",
                        (cstring_t)&ru8_SettingsPath[0]);
                }
            }
        }
        TASK_PROTECTED_END(
            &pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);
    }

    return t_Status;
}

/*---------------------------------------------------------------------------*
 * Get the settingsLoaded parameter
 *---------------------------------------------------------------------------*/

status_t SimpleImager_getSettingsLoaded
(
    SimpleImager_t *pt_SimpleImager,
    boolean_t *pb_Value
)
{
    status_t t_Status;          /* The current status */

    TASK_PROTECTED_START(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status)
    {
        *pb_Value = pt_SimpleImager->b_SettingsLoaded;
    }
    TASK_PROTECTED_END(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);

    return t_Status;
}

/*---------------------------------------------------------------------------*
 * Get the exposureTime parameter
 *---------------------------------------------------------------------------*/

status_t SimpleImager_getExposureTime
(
    SimpleImager_t *pt_SimpleImager,
    ui16_t *pu16_Value
)
{
    status_t t_Status;          /* The current status */

    TASK_PROTECTED_START(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status)
    {
        if (pt_SimpleImager->b_SettingsLoaded)
        {
            *pu16_Value = pt_SimpleImager->t_Settings.u16_ExposureTime;
        }
        else
        {
            UTIL_LOG_ERROR("Cannot get exposureTime with no settings loaded!");
            UTIL_LOG_ERROR("Invoke SimpleImager.loadSettings first");
            t_Status = STATUS_FAILURE;
        }
    }
    TASK_PROTECTED_END(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);

    return t_Status;
}

/*---------------------------------------------------------------------------*
 * Get the imageHeight parameter
 *---------------------------------------------------------------------------*/

status_t SimpleImager_getImageHeight
(
    SimpleImager_t *pt_SimpleImager,
    ui16_t *pu16_Value
)
{
    status_t t_Status;          /* The current status */

    TASK_PROTECTED_START(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status)
    {
        if (pt_SimpleImager->b_SettingsLoaded)
        {
            *pu16_Value = pt_SimpleImager->t_Settings.u16_ImageHeight;
        }
        else
        {
            UTIL_LOG_ERROR("Cannot get imageHeight with no settings loaded!");
            UTIL_LOG_ERROR("Invoke SimpleImager.loadSettings first");
            t_Status = STATUS_FAILURE;
        }
    }
    TASK_PROTECTED_END(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);

    return t_Status;
}

/*---------------------------------------------------------------------------*
 * Get the imageWidth parameter
 *---------------------------------------------------------------------------*/

status_t SimpleImager_getImageWidth
(
    SimpleImager_t *pt_SimpleImager,
    ui16_t *pu16_Value
)
{
    status_t t_Status;          /* The current status */

    TASK_PROTECTED_START(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status)
    {
        if (pt_SimpleImager->b_SettingsLoaded)
        {
            *pu16_Value = pt_SimpleImager->t_Settings.u16_ImageWidth;
        }
        else
        {
            UTIL_LOG_ERROR("Cannot get imageWidth with no settings loaded!");
            UTIL_LOG_ERROR("Invoke SimpleImager.loadSettings first");
            t_Status = STATUS_FAILURE;
        }
    }
    TASK_PROTECTED_END(&pt_SimpleImager->t_Lock, &gt_LockTimeout, t_Status);

    return t_Status;
}

/** @} */

Add a new file SimpleImager_Parser.c to the components/src/SimpleImager directory.

Show the completed SimpleImager_Parser.c file
/**
 * @file SimpleImager_Parser.c
 * @brief Provides functionality for parsing the SimpleImager settings format
 *
 * Provides functionality for parsing the SimpleImager settings format
 *
 * @author
 * @date
 */
/*
 * Copyright (c) Bright Ascension Ltd, 2023
 * Licensed Material - Property of Bright Ascension Ltd
 */


#include "core/types.h"
#include "core/status.h"
#include "SimpleImager/SimpleImager_Parser_package.h"
#include "util/Util_Log.h"
#include "util/Util_String.h"

/* Doxygen commands to add this component to the generated documentation */
/**
 * @addtogroup SimpleImager
 * @{
 */

/*---------------------------------------------------------------------------*
 * Package functions
 *---------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------*
 * Take data read from a SimpleImager settings file and parse it into a struct
 *---------------------------------------------------------------------------*/

status_t SimpleImager_Parser_parseSettings
(
    const ui8_t *pu8_SettingsData,
    ui32_t u32_SettingsDataLen,
    SimpleImager_Settings_t *pt_Settings
)
{
    boolean_t b_HaveT, b_HaveH, b_HaveW;
    SimpleImager_Settings_t t_TempSettings;
    ui32_t u32_SettingsIdx = 0;
    status_t t_Status = STATUS_SUCCESS;

    b_HaveT = b_HaveH = b_HaveW = FALSE;

    /* Iterate over pu8_SettingsData as long as there are no errors, we're not
     * reading off the end, and we are missing one of the settings. */
    while ((t_Status == STATUS_SUCCESS) &&
           (u32_SettingsIdx < u32_SettingsDataLen) &&
           (!b_HaveT || !b_HaveH || !b_HaveW))
    {
        ui32_t u32_AsciiLen;
        ui16_t u16_SettingValue;

        /* The setting character is the first we read each loop iteration. We
         * will validate it after we've parsed the value so we only need one
         * switch-case. */
        char_t c_Setting = pu8_SettingsData[u32_SettingsIdx];
        u32_SettingsIdx++;

        /* Require the = sign */
        if (pu8_SettingsData[u32_SettingsIdx] != '=')
        {
            t_Status = STATUS_FAILURE;
        }
        else
        {
            u32_SettingsIdx++;
        }

        if (t_Status == STATUS_SUCCESS)
        {
            /* Work out how many ASCII digits we have by looking for a
             * semicolon. */
            u32_AsciiLen = 0;

            while (((u32_SettingsIdx + u32_AsciiLen) < u32_SettingsDataLen) &&
                   (pu8_SettingsData[u32_SettingsIdx + u32_AsciiLen] != ';'))
            {
                u32_AsciiLen++;
            }

            /* Catch the case where no ASCII digits are provided. */
            if (u32_AsciiLen == 0)
            {
                t_Status = STATUS_FAILURE;
            }
        }

        if (t_Status == STATUS_SUCCESS)
        {
            /* Try and extract the 16-bit unsigned integer field value. */
            t_Status = Util_String_u16FromDec(
                        (const char_t *)&pu8_SettingsData[u32_SettingsIdx],
                        u32_AsciiLen,
                        &u16_SettingValue);
        }

        if (t_Status == STATUS_SUCCESS)
        {
            /* If we got here then we parsed a valid value into
             * u16_SettingValue and the format is OK. Now check c_Setting to
             * determine which setting we've parsed. */
            switch (c_Setting)
            {
                case 'T':
                    if (b_HaveT)
                    {
                        t_Status = STATUS_FAILURE;
                    }
                    else
                    {
                        b_HaveT = TRUE;
                        t_TempSettings.u16_ExposureTime = u16_SettingValue;
                    }
                    break;
                case 'H':
                    if (b_HaveH)
                    {
                        t_Status = STATUS_FAILURE;
                    }
                    else
                    {
                        b_HaveH = TRUE;
                        t_TempSettings.u16_ImageHeight = u16_SettingValue;
                    }
                    break;
                case 'W':
                    if (b_HaveW)
                    {
                        t_Status = STATUS_FAILURE;
                    }
                    else
                    {
                        b_HaveW = TRUE;
                        t_TempSettings.u16_ImageWidth = u16_SettingValue;
                    }
                    break;
                default:
                    t_Status = STATUS_FAILURE;
            }
        }

        if (t_Status == STATUS_SUCCESS)
        {
            /* Move on to the next field by adding the number of ASCII digits
             * we've read, and 1 more for the semicolon. */
            u32_SettingsIdx += (u32_AsciiLen + 1);
        }
    }

    /* If we parsed all of the settings return t_TempSettings, else fail */
    if (t_Status == STATUS_SUCCESS)
    {
        if (b_HaveT && b_HaveH && b_HaveW)
        {
            *pt_Settings = t_TempSettings;
        }
        else
        {
            t_Status = STATUS_FAILURE;
        }
    }

    return t_Status;
}

/** @} */

Update SimpleImager_types.h to include the following content:

Show the updated SimpleImager_types.h file
/**
 * @file SimpleImager_types.h
 * @brief Brief description of file.
 *
 * Full description of file
 *
 * @author
 * @date
 */
/*
 * Copyright (c) Bright Ascension Ltd, 2023
 * Licensed Material - Property of Bright Ascension Ltd
 */


#ifndef SIMPLEIMAGER_TYPES_H_
#define SIMPLEIMAGER_TYPES_H_

#include "core/types.h"
#include "core/status.h"
#include "SimpleImager/SimpleImager_sizes.h"
#include "SimpleImager/SimpleImager_config.h"
#include "task/Task_ProtectionLock.h"

/* Doxygen commands to add this component to the generated documentation */
/**
 * @addtogroup SimpleImager
 * @{
 */

/*---------------------------------------------------------------------------*
 * Type definitions
 *---------------------------------------------------------------------------*/

/** The SimpleImager component initialisation data */
typedef struct
{
    /* No component initialisation data required */
}
SimpleImager_Init_t;

/** A structure to hold SimpleImager's settings */
typedef struct
{
    /** The exposure time, in milliseconds */
    ui16_t u16_ExposureTime;
    /** The image height, in pixels */
    ui16_t u16_ImageHeight;
    /** The image width, in pixels */
    ui16_t u16_ImageWidth;
}
SimpleImager_Settings_t;

/** The SimpleImager component */
typedef struct
{
    /** A protection lock to protect the SimpleImager state */
    Task_ProtectionLock_t t_Lock;
    /**
     * Indicates whether imager settings were successfully loaded using
     * loadSettings.
     */
    boolean_t b_SettingsLoaded;
    /** The settings loaded, if b_SettingsLoaded is TRUE */
    SimpleImager_Settings_t t_Settings;
}
SimpleImager_t;

/** @} */

#endif /*SIMPLEIMAGER_TYPES_H_*/

Create a new file SimpleImager_Parser_package.h in the components/inc/SimpleImager directory.

Show the completed SimpleImager_Parser_package.h file
/**
 * @file SimpleImager_Parser_package.h
 * @brief Provides functionality for parsing the SimpleImager settings format
 *
 * Provides functionality for parsing the SimpleImager settings format
 *
 * @author ed
 * @date 2023-01-13
 */
/*
 * Copyright (c) Bright Ascension Ltd, 2023
 * Licensed Material - Property of Bright Ascension Ltd
 */



#ifndef SIMPLEIMAGER_PARSER_PACKAGE_H_
#define SIMPLEIMAGER_PARSER_PACKAGE_H_

#include "core/types.h"
#include "core/status.h"
#include "SimpleImager/SimpleImager.h"

/* Doxygen commands to add this component to the generated documentation */
/**
 * @addtogroup SimpleImager
 * @{
 */

/*---------------------------------------------------------------------------*
 * Defines
 *---------------------------------------------------------------------------*/

/** The maximum number of bytes required to hold the 3 settings members. */
#define SIMPLEIMAGER_PARSER_MAX_SETTINGS_BYTES  (24u)

/*---------------------------------------------------------------------------*
 * Package functions
 *---------------------------------------------------------------------------*/

/**
 * Take data read from a SimpleImager settings file and parse it into a struct
 * @param pu8_SettingsData The input data to parse
 * @param u32_SettingsDataLen The amount of data, in bytes, in pu8_SettingsData
 * @param pt_Settings Pointer to a struct populated on successful exit
 * @return Whether the data in pu8_SettingsData was successfully parsed into
 * pt_Settings
 * @note The expected format of one setting is:
 *
 * - A single character setting indicator: T for exposureTime, H for
 *   imageHeight, W for imageWidth.
 * - A single "=" character
 * - Up to 5 ASCII digits, given the setting value
 * - A single ";" character
 *
 * Settings data must consist of exactly three settings, each with unique
 * setting indicators.
 *
 * Some example valid setting data is "T=10000;H=640;W=480;"
 */
status_t SimpleImager_Parser_parseSettings
(
    const ui8_t *pu8_SettingsData,
    ui32_t u32_SettingsDataLen,
    SimpleImager_Settings_t *pt_Settings
);

/** @} */

#endif /*SIMPLEIMAGER_PARSER_PACKAGE_H_*/

Creating the Test File

Generating the files associated with a Component Type is very simple, and is built into the codegen tool.

It is achieved when you generate a Component Type by providing the --unit-tests (or -u) flag.

Generate a test file for the provided SimpleImager Component by executing the following codegen command:

 codegen componenttype generate components/inc/SimpleImager --unit-tests

This creates a test file for the SimpleImager Component in the following location: components/test/src/SimpleImager/test_SimpleImager.c

In general, a library’s test directory will contain a tree of all the Unit Tests for the Component Types in that library.

Open test_SimpleImager.c.

As you can see, test_SimpleImager.c already contains various data structures and functions required to write some Unit Tests. There are TODO comments indicating where you will need to make changes. There are also, at the bottom, a number of autogenerated Unit Tests which you will implement later on.

From top to bottom, the file includes:

  • A set of structures which are used instead of the usual Component management mechanisms of a Deployment. Importantly, gt_InitData is a structure which can be used to provide different initialisation data values to SimpleImager, and gpt_SimpleImager is the global pointer to the SimpleImager component instance which must be passed when calling its public functions.

  • Next is an empty section for placing helper functions. If you need to provide stubs to CMock, you can define them here.

  • Below that are the setUp() and tearDown() functions. These functions are called by the Unity test framework before and after every test, respectively. These can carry out any convenient steps, but, as is generated by default, are usually used to call the Component Type's local initialisation and finalisation functions.

  • The remaining functions are the tests themselves. Unity treats every function starting with test_ as a test, and will call these functions in order from top to bottom. The basic Unit Test file automatically includes some Unit Tests which the tooling can trivially generate from the componentType.xml In the case of SimpleImager, these tests exercise the captureImage and loadSettings Actions.

In the following sections you will design and implement some tests to demonstrate that the SimpleImager settings loading code works correctly.

Designing the Tests

As is the case with any software, it makes sense to think about how you’re going to approach writing your Unit Tests before you begin.

Tests should usually cover the broad functional jobs which a Component Type fulfils. In the case of SimpleImager, we are mostly interested in testing the code which uses FSS which you added in the previous tutorial.

To test the settings reading and parsing functionality, you will implement Unit Tests which exercise the following specific cases:

  1. Trying to capture an image before any settings are loaded.

  2. Trying to read settings from a file which doesn’t exist.

  3. Trying to read settings from a file which contains malformed settings. There are many possible ways the settings data can be malformed, but you will just write a single test covering one kind of error.

  4. Reading settings from a well formed file, and checking that the settings are correctly presented in the SimpleImager Parameters.

We will work through this list in order in the remaining sections of the tutorial, referring back to it as required.

Initialisation Data

As you can see in test_SimpleImager.c, the first TODO suggests you set initialisation data values for the Component under test. SimpleImager does not have any initialisation data values (as you can see in its types header - components/inc/SimpleImager/SimpleImager_types.h) so you can remove this TODO.

Update the comment to indicate that no initialisation data is required.

Show the updated TODO
/** The initialisation data for the component */
static SimpleImager_Init_t gt_InitData =
{
-   /* TODO: Set initial values for initialisation data */
+   /* SimpleImager does not have any initialisation data */
};

When testing other Components you may wish to put invalid values in gt_InitData in order to test validation. You may also want to provide different initialisation data for different tests - this is why gt_InitData is not const by default.

Test 1: Capture Without Settings

The first test outlined above needs to check that invoking captureImage before settings are loaded is handled correctly.

We’ll show you the code needed for this first test, but you might like to try and work it out on your own first.

If so, look at how the code already generated in test_SimpleImager.c is written, and take a look at the various Unity assertions available in unity/inc/unity.h within the FSDK.

First, rename the first test function to have a more accurate name, and update the comment.

Test function names are printed when the test is run, so it’s important to be descriptive. This test will test capturing an image without loading settings. Capture that succinctly in the name.

Show the updated test function name
-/** Test successfully invoking the captureImage action */
-void test_CaptureImageSuccessful(void)
+/** Test that captureImage fails if invoked before settings are loaded */
+void test_CaptureWithoutSettings(void)
{
    status_t t_Status;              /* The current status */

    /* TODO: Set up any expected external function calls for this action */

    t_Status = SimpleImager_captureImage(gpt_SimpleImager);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Next, get the settingsLoaded Parameter, and confirm it is FALSE.

This ensures that settingsLoaded is initialised to FALSE correctly, and also ensures the Component is in the state we expect for the test.

Show the added Parameter get and test assertion
/** Test that captureImage fails if invoked before settings are loaded */
void test_CaptureWithoutSettings(void)
{
    status_t t_Status;              /* The current status */
+   boolean_t b_SettingsLoaded;     /* Whether the settings are loaded */

-    /* TODO: Set up any expected external function calls for this action */
+   t_Status = SimpleImager_getSettingsLoaded(gpt_SimpleImager, &b_SettingsLoaded);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_FALSE(b_SettingsLoaded);

    t_Status = SimpleImager_captureImage(gpt_SimpleImager);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Finally, change the autogenerated assertion so we require t_Status to not equal STATUS_SUCCESS.

Show the updated t_Status assertion.
/** Test that captureImage fails if invoked before settings are loaded */
void test_CaptureWithoutSettings(void)
{
    status_t t_Status;              /* The current status */
    boolean_t b_SettingsLoaded;     /* Whether the settings are loaded */

    t_Status = SimpleImager_getSettingsLoaded(gpt_SimpleImager, &b_SettingsLoaded);
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
    TEST_ASSERT_FALSE(b_SettingsLoaded);

    t_Status = SimpleImager_captureImage(gpt_SimpleImager);
-   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Running Tests

Building and running tests is built into the FSDK's build system. Build targets are automatically generated for any Component with a Unit Test.

Build and run the SimpleImager Unit Test by running the following command:

 make -C components testrunSimpleImager

You should see output for each test executed, and a summary of how many failed.

Confirm that you see test_CaptureWithoutSettings:PASS in the output.

You should also see error output from SimpleImager reporting that loadSettings must be invoked before captureImage. This indicates that the updated test_CaptureWithoutSettings() test function is testing what we expect, and SimpleImager is behaving as it should.

Don’t worry about other failures. In the following sections you will add the remainder of the tests outlined above.

Tests Using Mocks

You now have some basic passing SimpleImager tests. These include the test you added in the previous section, and the autogenerated tests which check the argument validating for the loadSettings Action.

We’ll now guide you through adding tests which will mock the FSS functions which SimpleImager calls. Mocking these functions means a connected Component to provide FSS is not required. This ensures that as little effort as possible is expended testing or debugging code which is not part of the Component under test.

Generating Mocks

CMock can generate mocking macros which for use in your Unit Test. These macros can validate input arguments to mocked functions, populate output arguments, and generate specific return values. If these macros are not powerful enough, CMock allows you to provide callbacks to use in place of the mocked functions at runtime.

The FSDK build system tells CMock which mocks to generate by looking in test files for file names in #include directives which start with Mock. When generating test files, codegen makes an educated guess about which headers you will need mocks for and includes them using this special markup.

At the top of test_SimpleImager.c, check that the FSS functions in SimpleImager_FSS_package.h will be mocked.

You should see the following #include directive:

#include "SimpleImager/MockSimpleImager_FSS_package.h"

Having built and run this test earlier, these mocks, and the others specified in test_SimpleImager.c, have already been generated. You can find the mocked FSS header here: components/build/linux/test/mock/SimpleImager/MockSimpleImager_FSS_package.h

Open the MockSimpleImager_FSS_package.h mock header.

This file contains a lot of content generated by CMock, and we won’t run through it in much detail. It is worth noting, however, the kinds of macros which are available.

The SimpleImager_FSS_openFileFs_ExpectAndReturn() macro, for example, instructs CMock to:

  • Expect a call to SimpleImager_FSS_openFileFs(),

  • Check the input arguments, and

  • Return with the given status.

This is the function which SimpleImager calls when it needs to open a file. By mocking it, we can check that SimpleImager is opening the correct file, in the correct way.

We will not go further into the specifics of the kinds of mocks CMock generates. In the following sections you will see how some of these mocks work by example. If you would like to read about them in more detail, documentation is available in the CMock Github repository.

Test 2: Settings File Not Found

We are now ready to demonstrate how to use FSS mocks to test the loadSettings Action.

As outlined above, the next test to implement should check that loading settings from a file which doesn’t exist fails gracefully.

Add a new test_SettingsFileNotFound() function immediately after test_LoadSettingsFailureLengthTooLarge().

In the new test function you will need to expect a call to SimpleImager_FSS_openFileFs(). Each input argument will need to be validated or ignored.

Use SimpleImager_FSS_openFileFs_ExpectAndReturn() to expect the FSS openFile operation.

The returned status should be FSS_STATUS_FILE_NOT_FOUND, since we want to indicate to SimpleImager that the provided file was not found.

Note that in this test pt_Timeout and pt_SelectionID are not needed so can be ignored. z_FileName can be ignored as well because, regardless of which file SimpleImager tries to open, CMock will return a "file not found" status.

Show the new test.
+/** Test that loadSettings fails when opening a non-existent file */
+void test_SettingsFileNotFound(void)
+{
+    SimpleImager_FSS_openFileFs_ExpectAndReturn(
+        gpt_SimpleImager,
+        NULL,
+        NULL,
+        FALSE,
+        FALSE,
+        FSS_ACCESSMODE_READ_ONLY,
+        FALSE,
+        NULL,
+        FSS_STATUS_FILE_NOT_FOUND);
+    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
+    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
+    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
+    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
+}

Now that the expectation is set up, you can call SimpleImager_loadSettings() and ensure it does not return STATUS_SUCCESS.

Add a call to SimpleImager_loadSettings() and use an assertion to check the returned status.

Note that all mocks need to be configured before the Component code is called. That is why the word "expect" is used - you are telling CMock what it should expect to happen next.

Show the added Action invocation and t_Status assertion.
/** Test that loadSettings fails when opening a non-existent file */
void test_SettingsFileNotFound(void)
{
+   status_t t_Status;
+   cstring_t z_FileName = "/this/file/does/not/exist";

    SimpleImager_FSS_openFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
        NULL,
        FALSE,
        FALSE,
        FSS_ACCESSMODE_READ_ONLY,
        FALSE,
        NULL,
        FSS_STATUS_FILE_NOT_FOUND);
    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
+
+   t_Status = SimpleImager_loadSettings(
+       gpt_SimpleImager, (const ui8_t *)z_FileName, strlen(z_FileName));
+   TEST_ASSERT_EQUAL_UINT(FSS_STATUS_FILE_NOT_FOUND, t_Status);
}

The above code snippet uses the strlen() function, so you’ll need to include the <string.h> header before building the tests.

Add <string.h> to the headers included at the top of test_SimpleImager.c.

Show the added #include directive.
#include "SimpleImager/MockSimpleImager_FSS_package.h"
#include "util/Util_Log.h"
+#include <string.h>

/*---------------------------------------------------------------------------*
 * Global variables
 *---------------------------------------------------------------------------*/

Build and run the SimpleImager tests and confirm that test_SettingsFileNotFound() passes.

As before, run the tests with the following command:

 make -C components testrunSimpleImager

Test 3: Malformed Settings Data

You’ll now add another test which is a little more complicated. We need to check that SimpleImager correctly handles files which contain badly formatted settings.

To do this you will report success from the openFile and readFile FSS operations, but you will tell CMock to return data from readFile which is malformed. You will also need to expect a call to closeFile once SimpleImager is done with the file.

Add a new test_MalformedSettingsData() function immediately after test_SettingsFileNotFound().

As in the previous test, you will need to expect a call to SimpleImager_FSS_openFileFs(), and configure various arguments to ignore.

Use SimpleImager_FSS_openFileFs_ExpectAndReturn() to expect the FSS openFile operation.

The returned status should be STATUS_SUCCESS, since we want SimpleImager to continue and read from the file.

In this test pt_Timeout and z_FileName can be ignored as before, but, while its value can be ignored, we need to return a file handle through pt_SelectionID. SimpleImager should use this as the file handle when it reads from the file, and in the test you will check this is the case.

Show the new test.
+/** Test that loadSettings fails when reading a malformed settings file */
+void test_MalformedSettingsData(void)
+{
+    /* A dummy selection ID to use as a file handle */
+    Service_SelectionID_t t_File = (Service_SelectionID_t)99;
+
+    SimpleImager_FSS_openFileFs_ExpectAndReturn(
+        gpt_SimpleImager,
+        NULL,
+        NULL,
+        FALSE,
+        FALSE,
+        FSS_ACCESSMODE_READ_ONLY,
+        FALSE,
+        NULL,
+        STATUS_SUCCESS);
+    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
+    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
+    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
+    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
+    /* Return a dummy test value to SimpleImager through pt_SelectionID */
+    SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);
+}

Next, you need to expect SimpleImager to read from the file it has "opened". This is achieved in a very similar way to how the openFile operation was expected: you can expect readFile using the SimpleImager_FSS_readFileFs_ExpectAndReturn() generated CMock macro.

Use SimpleImager_FSS_readFileFs_ExpectAndReturn() to expect the FSS readFile operation.

Pass in the same selection ID which was returned through pt_SelectionID above. Use SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data() to return some malformed data, and return the length using SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength().

As for openFile, the returned status should be STATUS_SUCCESS. The tests needs to make sure SimpleImager determines that the returned data is not well-formed by itself, even if the read works correctly.

Show the second mock setup added to the new test.
/** Test that loadSettings fails when reading a malformed settings file */
void test_MalformedSettingsData(void)
{
    /* A dummy selection ID to use as a file handle */
    Service_SelectionID_t t_File = (Service_SelectionID_t)99;
+
+   /* Some malformed data */
+   ui8_t ru8_MalformedData[] = "Malformed Data";
+   ui32_t u32_MalformedDataLength = ARRAY_COUNT(ru8_MalformedData);

    SimpleImager_FSS_openFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
        NULL,
        FALSE,
        FALSE,
        FSS_ACCESSMODE_READ_ONLY,
        FALSE,
        NULL,
        STATUS_SUCCESS);
    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
    /* Return a dummy test value to SimpleImager through pt_SelectionID */
    SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);
+
+   /* Expect SimpleImager to read from the opened file */
+   SimpleImager_FSS_readFileFs_ExpectAndReturn(
+       gpt_SimpleImager,
+       NULL,
+       t_File,
+       NULL,
+       NULL,
+       STATUS_SUCCESS);
+   /* Ignore the values of pt_Timeout, pu8_Data and pu32_DataLength */
+   SimpleImager_FSS_readFileFs_IgnoreArg_pt_Timeout();
+   SimpleImager_FSS_readFileFs_IgnoreArg_pu8_Data();
+   SimpleImager_FSS_readFileFs_IgnoreArg_pu32_DataLength();
+
+   /* Return the malformed data and its length through the right pointers */
+   SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
+       &ru8_MalformedData[0], u32_MalformedDataLength);
+   SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
+       &u32_MalformedDataLength);
}

After SimpleImager reads the settings data, it must close it. You can tell CMock to expect this call by using SimpleImager_FSS_closeFileFs_ExpectAndReturn().

Expect SimpleImager_FSS_closeFileFs_ExpectAndReturn() to be called, making sure SimpleImager closes the same file it opened.

Show the added closeFile mock setup.
    /* Return the malformed data and its length through the right pointers */
    SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
        &ru8_MalformedData[0], u32_MalformedDataLength);
    SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
        &u32_MalformedDataLength);
+
+   /* Expect SimpleImager to then close the file */
+   SimpleImager_FSS_closeFileFs_ExpectAndReturn(
+       gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
+   SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();
}

As before, add the call to SimpleImager_loadSettings() and check its returned status.

Show the added t_Status and z_FileName variables
/** Test that loadSettings fails when reading a malformed settings file */
void test_MalformedSettingsData(void)
{
    /* A dummy selection ID to use as a file handle */
    Service_SelectionID_t t_File = (Service_SelectionID_t)99;

    /* Some malformed data */
    ui8_t ru8_MalformedData[] = "Malformed Data";
    ui32_t u32_MalformedDataLength = ARRAY_COUNT(ru8_MalformedData);
+
+   status_t t_Status;
+   cstring_t z_FileName = "/this/file/is/malformed";

    SimpleImager_FSS_openFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
Show the added Action invocation and t_Status assertion.
    /* Expect SimpleImager to then close the file */
    SimpleImager_FSS_closeFileFs_ExpectAndReturn(
        gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
    SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();
+
+   t_Status = SimpleImager_loadSettings(
+       gpt_SimpleImager, (const ui8_t *)z_FileName, strlen(z_FileName));
+   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Build and run the SimpleImager tests and confirm that test_MalformedSettingsData() passes.

In the next section you’ll write the final test discussed above. In doing so you’ll reuse the code in test_MalformedSettingsData() so that the Unit Test remains maintainable.

Helper Functions

Up to this point the Unit Tests you have written for SimpleImager are each fairly independent. There isn’t very much overlap in the contents of each test function. This is good - it makes the tests maintainable. If something changes in SimpleImager, you typically wouldn’t need to make the same changes in lots of tests.

The next test you’re going to add, however, is very similar to test_MalformedSettingsData(). Rather than giving SimpleImager malformed settings data, you’ll give it valid settings data, and then check the parsed Parameter values.

To do this in a maintainable way, you will define a helper function for testing the loadSettings Action with an arbitrary string.

Define loadSettingsFromString()

Under the Helper functions banner, define a loadSettingsFromString() function using the body of the test_MalformedSettingsData() test.

Use the status code from the SimpleImager_loadSettings() function call as the returned status from loadSettingsFromString().

This allows tests which call loadSettingsFromString() to check the returned status code themselves, depending on what they expect.

Rather than using local variables to define the settings data, give the loadSettingsFromString() function a z_SettingsData argument.

Derive the length of the settings data to pass to the mocked FSS read using strlen(z_SettingsData).

There are a few comments and strings which should also be updated when implementing the generic loadSettingsFromString() function.

Show the changes in the body of the loadSettingsFromString() helper function.
/*---------------------------------------------------------------------------*
 * Helper functions
 *---------------------------------------------------------------------------*/

/**
 * Test the loadSettings action using the given settings data
 * @param z_SettingsData The settings data to load
 * @return The status returned from the loadSettings action
 */
static status_t loadSettingsFromString
(
    string_t z_SettingsData
)
{
    /* A dummy selection ID to use as a file handle */
    Service_SelectionID_t t_File = (Service_SelectionID_t)99;

-   /* Some malformed data */
-   ui8_t ru8_MalformedData[] = "Malformed Data";
-   ui32_t u32_MalformedDataLength = ARRAY_COUNT(ru8_MalformedData);
+   /* Get the length of the settings string */
+   ui32_t u32_SettingsDataLength = strlen(z_SettingsData);

-   status_t t_Status;
-   cstring_t z_FileName = "/this/file/is/malformed";
+   cstring_t z_FileName = "/dummy/file/path";

    SimpleImager_FSS_openFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
        NULL,
        FALSE,
        FALSE,
        FSS_ACCESSMODE_READ_ONLY,
        FALSE,
        NULL,
        STATUS_SUCCESS);
    /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
    SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
    SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
    /* Return a dummy test value to SimpleImager through pt_SelectionID */
    SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);

    /* Expect SimpleImager to read from the opened file */
    SimpleImager_FSS_readFileFs_ExpectAndReturn(
        gpt_SimpleImager,
        NULL,
        t_File,
        NULL,
        NULL,
        STATUS_SUCCESS);
    /* Ignore the values of pt_Timeout, pu8_Data and pu32_DataLength */
    SimpleImager_FSS_readFileFs_IgnoreArg_pt_Timeout();
    SimpleImager_FSS_readFileFs_IgnoreArg_pu8_Data();
    SimpleImager_FSS_readFileFs_IgnoreArg_pu32_DataLength();

-   /* Return the malformed data and its length through the right pointers */
-   SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
-       &ru8_MalformedData[0], u32_MalformedDataLength);
-   SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
-       &u32_MalformedDataLength);
+   /* Return the data and its length through the right pointers */
+   SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
+       (ui8_t *)&z_SettingsData[0], u32_SettingsDataLength);
+   SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
+       &u32_SettingsDataLength);

    /* Expect SimpleImager to then close the file */
    SimpleImager_FSS_closeFileFs_ExpectAndReturn(
        gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
    SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();

-   t_Status = SimpleImager_loadSettings(
-       gpt_SimpleImager, (const ui8_t *)z_FileName, strlen(z_FileName));
-   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   return SimpleImager_loadSettings(
+       gpt_SimpleImager, (const ui8_t *)z_FileName, strlen(z_FileName));
}
Show the use of loadSettingsFromString() in test_MalformedSettingsData().
/** Test that loadSettings fails when reading a malformed settings file */
void test_LoadSettingsMalformedData(void)
{
-   /* A dummy selection ID to use as a file handle */
-   Service_SelectionID_t t_File = (Service_SelectionID_t)99;
-
-   /* Some malformed data */
-   ui8_t ru8_MalformedData[] = "Malformed Data";
-   ui32_t u32_MalformedDataLength = ARRAY_COUNT(ru8_MalformedData);
-
-   status_t t_Status;
-   cstring_t z_FileName = "/this/file/is/malformed";
-
-   SimpleImager_FSS_openFileFs_ExpectAndReturn(
-       gpt_SimpleImager,
-       NULL,
-       NULL,
-       FALSE,
-       FALSE,
-       FSS_ACCESSMODE_READ_ONLY,
-       FALSE,
-       NULL,
-       STATUS_SUCCESS);
-   /* Ignore the values of pt_Timeout, z_FileName and pt_SelectionID */
-   SimpleImager_FSS_openFileFs_IgnoreArg_pt_Timeout();
-   SimpleImager_FSS_openFileFs_IgnoreArg_z_Filename();
-   SimpleImager_FSS_openFileFs_IgnoreArg_pt_SelectionID();
-   /* Return a dummy test value to SimpleImager through pt_SelectionID */
-   SimpleImager_FSS_openFileFs_ReturnThruPtr_pt_SelectionID(&t_File);
-
-   /* Expect SimpleImager to read from the opened file */
-   SimpleImager_FSS_readFileFs_ExpectAndReturn(
-       gpt_SimpleImager,
-       NULL,
-       t_File,
-       NULL,
-       NULL,
-       STATUS_SUCCESS);
-   /* Ignore the values of pt_Timeout, pu8_Data and pu32_DataLength */
-   SimpleImager_FSS_readFileFs_IgnoreArg_pt_Timeout();
-   SimpleImager_FSS_readFileFs_IgnoreArg_pu8_Data();
-   SimpleImager_FSS_readFileFs_IgnoreArg_pu32_DataLength();
-
-   /* Return the malformed data and its length through the right pointers */
-   SimpleImager_FSS_readFileFs_ReturnMemThruPtr_pu8_Data(
-       &ru8_MalformedData[0], u32_MalformedDataLength);
-   SimpleImager_FSS_readFileFs_ReturnThruPtr_pu32_DataLength(
-       &u32_MalformedDataLength);
-
-   /* Expect SimpleImager to then close the file */
-   SimpleImager_FSS_closeFileFs_ExpectAndReturn(
-       gpt_SimpleImager, NULL, t_File, STATUS_SUCCESS);
-   SimpleImager_FSS_closeFileFs_IgnoreArg_pt_Timeout();
-
-   t_Status = SimpleImager_loadSettings(
-       gpt_SimpleImager, (const ui8_t *)z_FileName, strlen(z_FileName));
-   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   /* Call the loadSettings test function with malformed data */
+   status_t t_Status = loadSettingsFromString("Malformed data");
+   TEST_ASSERT_NOT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

You could now very easily use loadSettingsFromString() to define a whole host of tests for SimpleImager which would focus on the settings parser.

If the way in which SimpleImager loaded settings changed, only the loadSettingsFromString() function would need to be updated, and the Unit Test functions themselves would not require any changes.

At this stage it would be wise to build and run the revised test_LoadSettingsMalformedData() test and make sure nothing is broken.

Build and run the SimpleImager tests again and confirm that test_MalformedSettingsData() still passes.

Test 4: loadSettings Successful

The fourth test needs to make sure that, when given valid settings data, SimpleImager parses and presents the values correctly. You’ll make use of the loadSettingsFromString() function you just added to do this.

codegen has already generated a test_LoadSettingsSuccessful() test function for this case, so you can add your test code there.

In test_LoadSettingsSuccessful(), call loadSettingsFromString() with a valid settings data string.

Recall from the previous tutorial that valid settings are of the form T=0;H=0;W=0;, where T, H and W can be any 16-bit unsigned integer.

Show the updated test.
/** Test successfully invoking the loadSettings action */
void test_LoadSettingsSuccessful(void)
{
    status_t t_Status;              /* The current status */
-   ui8_t ru8_SettingsPath[SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE] = { 0 };
-
-   /* TODO: Set the ru8_SettingsPath argument */
-
-   /* TODO: Set up any expected external function calls for this action */
-
-   t_Status = SimpleImager_loadSettings(
-       gpt_SimpleImager,
-       &ru8_SettingsPath[0],
-       SIMPLEIMAGER_LOAD_SETTINGS_MAX_ARG_SIZE);
-   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+
+   t_Status = loadSettingsFromString("T=145;H=480;W=640;");
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
}

Build and run the SimpleImager tests and confirm that test_LoadSettingsSuccessful() now passes.

All of the SimpleImager Unit Tests should now be passing!

To complete this test, however, you need to check that the correct values were parsed by SimpleImager. To do this, you can check the values of the exposureTime, imageHeight and imageWidth Parameters.

Add a call to SimpleImager_getExposureTime() and use Unity assertions to check its returned status and Parameter value.

Add a call to SimpleImager_getImageHeight() and use Unity assertions to check its returned status and Parameter value.

Add a call to SimpleImager_getImageWidth() and use Unity assertions to check its returned status and Parameter value.

Show the Parameter checks added to test_LoadSettingsSuccessful().
/** Test successfully invoking the loadSettings action */
void test_LoadSettingsSuccessful(void)
{
    status_t t_Status;
+   ui16_t u16_ExposureTime, u16_ImageHeight, u16_ImageWidth;

    t_Status = loadSettingsFromString("T=145;H=480;W=640;");
    TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+
+   t_Status = SimpleImager_getExposureTime(
+       gpt_SimpleImager, &u16_ExposureTime);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(145, u16_ExposureTime);
+
+   t_Status = SimpleImager_getImageHeight(
+       gpt_SimpleImager, &u16_ImageHeight);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(480, u16_ImageHeight);
+
+   t_Status = SimpleImager_getImageWidth(
+       gpt_SimpleImager, &u16_ImageWidth);
+   TEST_ASSERT_EQUAL_UINT(STATUS_SUCCESS, t_Status);
+   TEST_ASSERT_EQUAL_UINT16(640, u16_ImageWidth);
}

Build and run the SimpleImager tests again and confirm that test_LoadSettingsSuccessful() still passes.

This test completes the set of tests outlined above!

Using the techniques and code you’ve written in this tutorial there are more tests you could add to further exercise SimpleImager.

Think about what other tests you could add.

Consider how you might write Unit Tests for the MessagePrinter and EventRaiser Component Types you encountered in the first three tutorials.

Wrap Up

In this tutorial you have:

  • Learned about how Unit Testing is supported by the FSDK.

  • Learned how to use codegen and the FSDK build system to generate, build and run Unit Tests.

  • Implemented and run Unit Tests for the SimpleImager Component Type, making use of mocking to fulfil its FSS requirement.

In the next tutorial you will meet new Services which will allow your Component Types to interact with hardware.

Click here to move on to that now.