Aller au contenu

Exercises related to Testing

Write a test program for unique_ptr

Exercice 1

Based on the instructions given in the testing codelab, add the following test function to the “test-ptr” test: test that the ownership is passed correctly between two instances of `std::unique_ptr.

Implement the test and verify that it is successful.

Solution
/**
 * Test that multiple instances of unique pointers correctly manage the
 * ownership of the the object
*/
void test_unique_ptr() {
  std::unique_ptr<Test> unique_ptr1(nullptr);

  // Sanity-check value of counter
  TEST_ASSERT_EQUAL(0, Test::_instanceCount);

  // Create and destroy shared pointer in given scope
  {
    std::unique_ptr<Test> unique_ptr2(new Test);
    TEST_ASSERT_EQUAL(1, Test::_instanceCount);
    // move ownership of unique_ptr2 to unique_ptr1
    // unique_ptr1 = unique_ptr2; is not allowed
    // must use the std::move() semantics
    unique_ptr1 = std::move(unique_ptr2);
    // still one instance only
    TEST_ASSERT_EQUAL(1, Test::_instanceCount);
    TEST_ASSERT_EQUAL(Test::kMagicNumber, unique_ptr1->_value);
    // unique_ptr2 does not own ptr any more
    TEST_ASSERT_EQUAL(nullptr, unique_ptr2.get());
  }

  // unique_ptr1 still owns an instance
  TEST_ASSERT_EQUAL(1, Test::_instanceCount);

  // release unique_ptr1
  unique_ptr1 = nullptr;

  // unique_ptr instance has been released
  TEST_ASSERT_EQUAL(0, Test::_instanceCount);
}  

Write a test program for raw pointers

Exercice 2

Based on the instructions given in the testing codelab, add the following test function to the “test-ptr” test: test that when creating a raw pointer and deallocating it correctly, the destructor is called as expected.

Implement the test and verify that it is successful.

Solution
/**
 * Test that when delete is not called with a raw pointer then a memory leak occurs
*/
void test_raw_pointers() {
  Test* raw_ptr1(nullptr);

  // Sanity-check value of counter
  TEST_ASSERT_EQUAL(0, Test::_instanceCount);

  // allocate the raw pointer in a given scope
  {
    raw_ptr1 = new Test;
    TEST_ASSERT_EQUAL(1, Test::_instanceCount);
    Test* raw_ptr2 = raw_ptr1;
    // still one instance only
    TEST_ASSERT_EQUAL(1, Test::_instanceCount);
    TEST_ASSERT_EQUAL(Test::kMagicNumber, raw_ptr1->_value);
    TEST_ASSERT(raw_ptr1 == raw_ptr2);
  }

  // no delete called yet
  TEST_ASSERT_EQUAL(1, Test::_instanceCount);

  // call delete
  delete raw_ptr1;
  raw_ptr1 = nullptr;

  // delete was called -> the object must be destroyed
  TEST_ASSERT_EQUAL(0, Test::_instanceCount);
}

Write a test program for the Mbed OS Queue component

In the Multi-tasking using Mbed OS codelab, a number of mechanisms for programming multi-tasking applications have been reviewed. Among those, the Mbed OS queue mechanism was presented with a number of examples.

Exercice 3

Many different test cases can be written for verifying the correctness of the class implementation:

  • The function for getting an element from the queue must return false if one tries to get an element from an empty queue with a timeout value of 0. This test is implemented in the get_empty_no_timeout() function.
  • The function for inserting an element to the queue must succeed when the queue is not full. It must return false if one tries to insert an element to a full queue with a given timeout value. In addition, the time spent in the try_put_for() method must correspond to the timeout value. This test is implemented in the put_full_timeout() function.
  • Testing two scenarios where two separate threads attempt to get and put elements from a queue with correct behavior: elements cannot be inserted in the queue when the queue is full and elements cannot be obtained from the queue when the queue is empty. These two tests are implemented in the test_get_empty_waitforever() and test_put_full_waitforever functions.

The tests described above must be integrated in a test program under “TESTS/simple-test/basic-queue-test”.

Solution
TESTS/simple-test/basic-queue-test/main.cpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file main.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief Simple example of test program for the Queue class
 *
 * @date 2022-09-01
 * @version 0.1.0
 ***************************************************************************/

#include <chrono>
#include <cstdint>
#include <ratio>

#include "greentea-client/test_env.h"
#include "mbed.h"
#include "unity/unity.h"
#include "utest/utest.h"

using namespace utest::v1;

// constants used in test functions
static constexpr std::chrono::milliseconds TEST_TIMEOUT = 50ms;
static constexpr std::chrono::milliseconds DELTA        = 1ms;

// test handler functions

/** Test get empty without timeout
 *
 * Given an empty queue with one slot for uint32_t data.
 * When a thread tries to get one messages without timeout
 *  then the try_get() method returns false.
 */
static void get_empty_no_timeout() {
    // create a simple queue
    Queue<uint32_t, 1> q;

    // try to get an element from the empty queue
    uint32_t* v = nullptr;
    bool res    = q.try_get(&v);
    // should not succeed
    TEST_ASSERT_FALSE(res);
}

/** Test put full timeout
 *
 * Given a queue with two slots for uint32_t data.
 * When a thread tries to insert two messages with @ TEST_TIMEOUT timeout,
 * then the first try_put_for() operation succeeds and
 * the second try_put_for() operation fails with the expected timeout.
 */
static void put_full_timeout() {
    // create a queue with 2 elements
    Queue<uint32_t, 2> q;

    // insert one element
    uint32_t localMsg = 0;
    bool res          = q.try_put_for(TEST_TIMEOUT, &localMsg);
    // should succeed
    TEST_ASSERT_TRUE(res);

    // insert a second element
    res = q.try_put_for(TEST_TIMEOUT, &localMsg);
    // should succeed
    TEST_ASSERT_TRUE(res);

    // try to get an element with a given timeout
    Timer timer;
    timer.start();

    res = q.try_put_for(TEST_TIMEOUT, &localMsg);
    // should fail
    TEST_ASSERT_FALSE(res);
    // elapsed time should match the timeout value
    TEST_ASSERT_INT_WITHIN(std::chrono::microseconds(DELTA).count(),
                           std::chrono::microseconds(TEST_TIMEOUT).count(),
                           timer.elapsed_time().count());
}

/** Test put full with forever timeout.
 *
 * Given the main thread and a consumer thread- Given a queue with one slot for uint32_t
 * data. When the main thread puts a message to the queue and tries to put second one with
 * @a Kernel::wait_for_u32_forever timeout Then thread waits for a slot to become empty in
 * the queue When the consumer thread takes one message out of the queue Then the main
 * thread successfully inserts message into the queue.
 */

// global variable used for checking messages between consumer and producer
uint32_t msg = 0;

// function used by a separate thread for putting one message to a queue
void thread_put_uint_msg(Queue<uint32_t, 1>* q) {
    // wait before producing
    ThisThread::sleep_for(TEST_TIMEOUT);
    // try to consume one element with infinite timeout
    msg      = 2;
    bool res = q->try_put_for(Kernel::wait_for_u32_forever, &msg);
    // should succeed (since one element was consumed)
    TEST_ASSERT_TRUE(res);
}

void test_get_empty_waitforever() {
    // create a queue with one element
    Queue<uint32_t, 1> q;

    // The consumer thread will run as the main thread
    // launch a separate thread as a producer of data (with limited stack size)
    static constexpr uint32_t THREAD_STACK_SIZE = 512;
    Thread t(osPriorityNormal, THREAD_STACK_SIZE);
    t.start(callback(thread_put_uint_msg, &q));

    // try to consume one element
    uint32_t* value = nullptr;
    Timer timer;
    timer.start();
    bool res = q.try_get_for(Kernel::wait_for_u32_forever, &value);
    // should succeed since one element was produced
    TEST_ASSERT_TRUE(res);
    // the consumed value should match the produced one
    TEST_ASSERT_EQUAL(&msg, value);
    // elapsed time should match the timeout value
    TEST_ASSERT_INT_WITHIN(std::chrono::microseconds(DELTA).count(),
                           std::chrono::microseconds(TEST_TIMEOUT).count(),
                           timer.elapsed_time().count());

    // wait for the producer thread to exit
    t.join();
}

// function used by a separate thread for getting one message from a queue
void thread_get_uint_msg(Queue<uint32_t, 1>* q) {
    // wait before consuming
    ThisThread::sleep_for(TEST_TIMEOUT);
    // try to consume one element with infinite timeout
    uint32_t* value = nullptr;
    bool res        = q->try_get_for(Kernel::wait_for_u32_forever, &value);
    // should succeed (since one element was produced)
    TEST_ASSERT_TRUE(res);
    // the value consumed should match the value produced
    TEST_ASSERT_EQUAL(&msg, value);
}

void test_put_full_waitforever() {
    // create a queue with one element
    Queue<uint32_t, 1> q;

    // The producer thread will run as the main thread
    // launch a separate thread as a consumer of data (with limited stack size)
    static constexpr uint32_t THREAD_STACK_SIZE = 512;
    Thread t(osPriorityNormal, THREAD_STACK_SIZE);
    t.start(callback(thread_get_uint_msg, &q));

    // producing one element should succeed
    msg      = 1;
    bool res = q.try_put(&msg);
    TEST_ASSERT_TRUE(res);

    // try to produce a second element
    Timer timer;
    timer.start();
    res = q.try_put_for(Kernel::wait_for_u32_forever, &msg);
    // should succeed since the element produced above was consumed
    TEST_ASSERT_TRUE(res);
    // elapsed time should match the timeout value
    TEST_ASSERT_INT_WITHIN(std::chrono::microseconds(DELTA).count(),
                           std::chrono::microseconds(TEST_TIMEOUT).count(),
                           timer.elapsed_time().count());
    // TEST_ASSERT_DURATION_WITHIN(TEST_TIMEOUT / 10, TEST_TIMEOUT, timer.elapsed_time());

    //
    // wait for the consumer thread to exit
    t.join();
}

static utest::v1::status_t greentea_setup(const size_t number_of_cases) {
    // Here, we specify the timeout (60s) and the host test (a built-in host test or the
    // name of our Python file)
    GREENTEA_SETUP(60, "default_auto");

    return greentea_test_setup_handler(number_of_cases);
}

// List of test cases in this file
static Case cases[] = {
    Case("try to get an element from empty queue without timeout", get_empty_no_timeout),
    Case("try to put an element to full queue with timeout", put_full_timeout),
    Case("try to get elements from empty queue forever", test_get_empty_waitforever),
    Case("try to put elements to full queue forever", test_put_full_waitforever)};

static Specification specification(greentea_setup, cases);

int main() { return !Harness::run(specification); }