diff --git a/pkg/fff/Makefile.include b/pkg/fff/Makefile.include index 5f9a0f644251..ef26e830063d 100644 --- a/pkg/fff/Makefile.include +++ b/pkg/fff/Makefile.include @@ -3,3 +3,6 @@ INCLUDES += -I$(PKGDIRBASE)/fff # There's nothing to build in this package, it's used as a header only library. # So it's declared as a pseudo-module PSEUDOMODULES += fff + +# Tests don't need pedantic. Pedantic throws errors in variadic macros when compiling for C++ +CXXEXFLAGS += -Wno-pedantic diff --git a/sys/include/cppunit.hpp b/sys/include/cppunit.hpp new file mode 100644 index 000000000000..15ad20344975 --- /dev/null +++ b/sys/include/cppunit.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 Jens Wetterich + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @ingroup sys + * @defgroup unittests_cpp C++ Unittests + * @brief RIOT unit tests for C++ + * @details The C++ unit test framework syntax is loosely based on + * GoogleTest, but offers far less functionality. + * For mocking the package @ref pkg_fff can be used. + * @{ + * @file + * @brief RIOT unit tests for C++ + * @details The C++ unit test framework syntax is loosely based on GoogleTest, + * but offers far less functionality. + * For mocking the package @ref pkg_fff can be used. + * @author Jens Wetterich + * + */ +#ifndef CPPUNIT_H +#define CPPUNIT_H +#if __cplusplus >= 201103L || defined(DOXYGEN) +#include "cppunit/cppunit_base.hpp" +#include "cppunit/cppunit_expect.hpp" +#include "cppunit/cppunit_fff.hpp" +#else +#error This library needs C++11 and newer +#endif +#endif +/** @} */ diff --git a/sys/include/cppunit/cppunit_base.hpp b/sys/include/cppunit/cppunit_base.hpp new file mode 100644 index 000000000000..51cd1ae41c91 --- /dev/null +++ b/sys/include/cppunit/cppunit_base.hpp @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2021 Jens Wetterich + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @addtogroup unittests_cpp + * @{ + * @file + * @brief RIOT unit tests for C++ base classes and macros + * @author Jens Wetterich + * + */ +#ifndef CPPUNIT_BASE_H +#define CPPUNIT_BASE_H +#if __cplusplus >= 201103L || defined(DOXYGEN) +#include +#include +#include +#include +#ifndef CPPUNIT_SUITE_CNT +/** + * @brief Maximum amount of tests in one test suite + */ +#define CPPUNIT_SUITE_CNT (10) +#endif +/** + * @brief RIOT C++ namespace + */ +namespace riot { +/** + * @brief namespace for cpp unit tests + */ +namespace testing { +/** + * @brief Test base class + * @details Should not be instantiated directly. + * @sa #TEST(suite, name) macro + */ +class test { +private: + bool suc = true; ///< indicates success of the test after running +public: + /** + * @brief Run the test + * @details Should not be called directly, only via macros + * @sa #RUN_SUITE(name) + * @return whether the test completed without errors + */ + virtual bool run() = 0; + /** + * @brief Indicate failure during test run + * @details Should be called by assertions macros + * @sa #EXPECT_EQ(expected, actual, msg) + * @sa #EXPECT_STREQ(expected, actual, msg) + * @sa #EXPECT_FFF_CALL_COUNT(name, count) + * @sa #EXPECT_FFF_CALL_PARAMS(mock, ...) + */ + void fail() { + suc = false; + } + /** + * @brief Check whether the test completed successfully + * @return whether the test completed without errors + */ + bool success() const { + return suc; + } +}; +/** + * @brief Test suite base class + * @details Should not be instantiated directly. + * To customize a test suite with custom set_up and tear down methods, + * inherit from this class and override set_up() and/or tear_down(). + * They will before / after every test. + * @sa #TEST_SUITE(name) macro + * @sa #TEST_SUITE_F(suite, name) macro + * @sa test_suite::set_up() + * @sa test_suite::tear_down() + */ +class test_suite { +protected: + bool suc = true; ///< Indicates success of all tests after running the suite + std::array tests{}; ///< array of tests to run +public: + /** + * @brief method to run before each test + */ + virtual void set_up() { + } + /** + * @brief method to run after each test + */ + virtual void tear_down() { + } + /** + * @brief get the name of the test suite + * @return name string + */ + virtual const char* get_name() const { + return ""; + }; + /** + * @brief Run all tests in the suite + */ + virtual void run() { + printf("----\nStarting Test suite %s\n", get_name()); + for (auto test : tests) { + if (test) { + suc = test->run() && suc; + } + } + printf("Suite %s completed: %s\n----\n", get_name(), suc ? "SUCCESS" : "FAILURE"); + } + /** + * @brief Run all tests in the suite + */ + void addTest(test* test) { + for (int i = 0; i < CPPUNIT_SUITE_CNT; i++) { + if (!tests[i]) { + tests[i] = test; + break; + } + } + } +}; +}// namespace testing +}// namespace riot +/** + * @brief Run the test suite \a name + * @hideinitializer + * @param[in] name Name of the suite + */ +#define RUN_SUITE(name) test_suite_##name.run(); + +/** + * @brief Instantiate a test suite with custom test fixture + * @hideinitializer + * @param[in] suite Name of the custom test fixture class + * @param[in] name Instantiation name of the suite + */ +#define TEST_SUITE_F(suite, name) \ + static_assert(sizeof(#suite) > 1, "test fixture class must not be empty"); \ + static_assert(sizeof(#name) > 1, "test_suite name must not be empty"); \ + class test_suite_##name : public suite { \ + const char* get_name() const override { \ + return #name; \ + }; \ + }; \ + test_suite_##name test_suite_##name; + +/** + * @brief Instantiate a test suite + * @hideinitializer + * @param[in] name Instantiation name of the suite + */ +#define TEST_SUITE(name) TEST_SUITE_F(::riot::testing::test_suite, name) + +/** + * @brief Begin the definition of a test + * @details Insert the test body after the macro + * @hideinitializer + * @param[in] suite Name of the suite to add the test to + * @param[in] name Instantiation name of the test + */ +#define TEST(suite, name) \ + class Test_##name : public ::riot::testing::test { \ + private: \ + void test_body(); \ + \ + public: \ + Test_##name() { \ + test_suite_##suite.addTest(this); \ + } \ + bool run() { \ + test_suite_##suite.set_up(); \ + printf("Running test " #name "...\n"); \ + test_body(); \ + printf("Test " #name ": %s\n", success() ? "SUCCESS" : "FAILURE"); \ + test_suite_##suite.tear_down(); \ + return success(); \ + }; \ + }; \ + void Test_##name::test_body() +#else +#error This library needs C++11 and newer +#endif +#endif +/** @} */ diff --git a/sys/include/cppunit/cppunit_expect.hpp b/sys/include/cppunit/cppunit_expect.hpp new file mode 100644 index 000000000000..eeaaa5f6f619 --- /dev/null +++ b/sys/include/cppunit/cppunit_expect.hpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2021 Jens Wetterich + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @addtogroup unittests_cpp + * @{ + * @file + * @brief RIOT unit tests for C++ assertion macros + * @author Jens Wetterich + * + */ +#ifndef CPPUNIT_EXPECT_H +#define CPPUNIT_EXPECT_H +#if __cplusplus >= 201103L || defined(DOXYGEN) +/** + * @brief Expect equality of the \a actual and \a expected value + * @hideinitializer + * @param[in] expected Expected value + * @param[in] actual Actual value + * @param[in] msg Message to print in case of failure + */ +#define EXPECT_EQ(expected, actual, msg) \ + static_assert(std::is_integral::value, \ + "EXPECT_EQ requires an integral type "); \ + if ((actual) != (expected)) { \ + fail(); \ + if (std::is_same::value) { \ + printf("Expected: %s, actual: %s\n" msg "\n", (expected) ? "true" : "false", \ + (actual) ? "true" : "false"); \ + } \ + else if (std::is_unsigned::value) { \ + printf("Expected: %u, actual: %u\n" msg "\n", static_cast(expected), \ + static_cast(actual)); \ + } \ + else { \ + printf("Expected: %d, actual: %d\n" msg "\n", static_cast(expected), \ + static_cast(actual)); \ + } \ + } +/** + * @brief Expect string equality of the \a actual and \a expected value + * @details Interprets both values as const char* string + * @hideinitializer + * @param[in] expected Expected value + * @param[in] actual Actual value + * @param[in] msg Message to print in case of failure + */ +#define EXPECT_STREQ(expected, actual, msg) \ + auto expected_str = static_cast(expected); \ + auto actual_str = static_cast(actual); \ + if (strcmp(expected_str, actual_str) != 0) { \ + fail(); \ + printf(msg " not equal! Expected: %s, actual: %s\n", expected_str, actual_str); \ + } +#else +#error This library needs C++11 and newer +#endif +#endif +/** @} */ diff --git a/sys/include/cppunit/cppunit_fff.hpp b/sys/include/cppunit/cppunit_fff.hpp new file mode 100644 index 000000000000..eebb3e8e9db4 --- /dev/null +++ b/sys/include/cppunit/cppunit_fff.hpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2021 Jens Wetterich + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ +/** + * @addtogroup unittests_cpp + * @{ + * @file + * @brief RIOT unit tests for C++ assertion macros for @ref pkg_fff + * @author Jens Wetterich + * + */ +#ifndef CPPUNIT_FFF_H +#define CPPUNIT_FFF_H +#if __cplusplus >= 201103L || defined(DOXYGEN) +/** + * @brief Expect \a count calls to \a name mock + * @details Needs the @ref pkg_fff for mocks + * @hideinitializer + * @param[in] name Name of the mock + * @param[in] count Expected calls + */ +#define EXPECT_FFF_CALL_COUNT(name, count) \ + if (name##_fake.call_count != (count)) { \ + fail(); \ + printf("Expected %d calls to " #name ", but got %d\n", count, name##_fake.call_count); \ + } + +/** @cond Helper macros for the EXPECT_FFF_CALL_PARAMS macro */ +#define EXPECT_FFF_CALL_1(name, val1) \ + if (name##_fake.arg0_val != (val1)) { \ + fail(); \ + puts("Argument 0 to mock " #name " doesn't match the expectation.\n"); \ + } +#define EXPECT_FFF_CALL_2(name, val1, val2) \ + if (name##_fake.arg0_val != (val1)) { \ + fail(); \ + puts("Argument 1 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg1_val != (val2)) { \ + fail(); \ + puts("Argument 2 to mock " #name " doesn't match the expectation.\n"); \ + } +#define EXPECT_FFF_CALL_3(name, val1, val2, val3) \ + if (name##_fake.arg0_val != (val1)) { \ + fail(); \ + puts("Argument 1 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg1_val != (val2)) { \ + fail(); \ + puts("Argument 2 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg2_val != (val3)) { \ + fail(); \ + puts("Argument 3 to mock " #name " doesn't match the expectation.\n"); \ + } +#define EXPECT_FFF_CALL_4(name, val1, val2, val3, val4) \ + if (name##_fake.arg0_val != (val1)) { \ + fail(); \ + puts("Argument 1 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg1_val != (val2)) { \ + fail(); \ + puts("Argument 2 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg2_val != (val3)) { \ + fail(); \ + puts("Argument 3 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg3_val != (val4)) { \ + fail(); \ + puts("Argument 4 to mock " #name " doesn't match the expectation.\n"); \ + } +#define EXPECT_FFF_CALL_5(name, val1, val2, val3, val4, val5) \ + if (name##_fake.arg0_val != (val1)) { \ + fail(); \ + puts("Argument 1 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg1_val != (val2)) { \ + fail(); \ + puts("Argument 2 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg2_val != (val3)) { \ + fail(); \ + puts("Argument 3 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg3_val != (val4)) { \ + fail(); \ + puts("Argument 4 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg4_val != (val5)) { \ + fail(); \ + puts("Argument 5 to mock " #name " doesn't match the expectation.\n"); \ + } +#define EXPECT_FFF_CALL_6(name, val1, val2, val3, val4, val5, val6) \ + if (name##_fake.arg0_val != (val1)) { \ + fail(); \ + puts("Argument 1 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg1_val != (val2)) { \ + fail(); \ + puts("Argument 2 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg2_val != (val3)) { \ + fail(); \ + puts("Argument 3 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg3_val != (val4)) { \ + fail(); \ + puts("Argument 4 to mock " #name " doesn't match the expectation.\n"); \ + } \ + if (name##_fake.arg5_val != (val6)) { \ + fail(); \ + puts("Argument 6 to mock " #name " doesn't match the expectation.\n"); \ + } +#define GET_FFF_MACRO(_1, _2, _3, _4, _5, _6, NAME, ...) NAME +/** @endcond */ +/** + * @brief Expect that the last call to \a mock was made with the given parameters + * @details Needs the @ref pkg_fff for mocks + * @hideinitializer + * @param[in] mock Name of the mock + * @param[in] ... params + */ +#define EXPECT_FFF_CALL_PARAMS(mock, ...) \ + GET_FFF_MACRO(__VA_ARGS__, EXPECT_FFF_CALL_6, EXPECT_FFF_CALL_5, EXPECT_FFF_CALL_4, \ + EXPECT_FFF_CALL_3, EXPECT_FFF_CALL_2, EXPECT_FFF_CALL_1) \ + (mock, __VA_ARGS__) +#else +#error This library needs C++11 and newer +#endif +#endif +/** @} */ diff --git a/sys/include/irq.hpp b/sys/include/irq.hpp new file mode 100644 index 000000000000..a412e88a46b2 --- /dev/null +++ b/sys/include/irq.hpp @@ -0,0 +1,67 @@ +/* +* Copyright (C) 2021 Jens Wetterich +* +* This file is subject to the terms and conditions of the GNU Lesser +* General Public License v2.1. See the file LICENSE in the top level +* directory for more details. +*/ +/** +* @ingroup core_irq +* @{ +* @file +* @brief Provides a C++ RAI based API to control interrupt processing +* @author Jens Wetterich +* +*/ +#ifndef IRQ_HPP +#define IRQ_HPP +#include "irq.h" + +namespace riot { +/** + * @brief RAII based IRQ lock + * @details While this object is on the stack IRQ is disabled. + * During destruction it will be restored to the previous state. + */ +class irq_lock { +public: + /** + * @brief Test whether IRQs are currently enabled. + * @return IRQ enabled + */ + static inline bool is_locked() noexcept { + return irq_is_enabled() == 0; + } + + /** + * @brief Check whether called from interrupt service routine. + * @return in ISR context + */ + static inline bool is_isr() noexcept { + return irq_is_in() != 0; + } + + /** + * @brief This sets the IRQ disable bit in the status register. + */ + inline irq_lock() : state(irq_disable()) { + } + + /** + * @brief This restores the IRQ disable bit in the status register + * to the value saved during construction of the object + * @see irq_disable + */ + inline ~irq_lock() { + irq_restore(state); + } + + irq_lock(irq_lock const& irq) = delete; + irq_lock(irq_lock const&& irq) = delete; + +private: + unsigned int state; +}; +}// namespace riot +#endif /* IRQ_HPP */ +/** @} */ diff --git a/tests/irq_cpp/Makefile b/tests/irq_cpp/Makefile new file mode 100644 index 000000000000..ed9ef849abc5 --- /dev/null +++ b/tests/irq_cpp/Makefile @@ -0,0 +1,13 @@ +include ../Makefile.tests_common +FEATURES_REQUIRED += cpp +FEATURES_REQUIRED += libstdcpp +USEPKG += fff + +# Some boards don't define irq functions as static inline. Then they can't be mocked. +FEATURES_BLACKLIST += \ + arch_esp32 \ + arch_esp8266 \ + arch_mips32r2 \ + arch_native + +include $(RIOTBASE)/Makefile.include diff --git a/tests/irq_cpp/Makefile.ci b/tests/irq_cpp/Makefile.ci new file mode 100644 index 000000000000..e2bfd595ea06 --- /dev/null +++ b/tests/irq_cpp/Makefile.ci @@ -0,0 +1,5 @@ +BOARD_INSUFFICIENT_MEMORY := \ + nucleo-l011k4 \ + samd10-xmini \ + stm32f030f4-demo \ + # diff --git a/tests/irq_cpp/irq.h b/tests/irq_cpp/irq.h new file mode 100644 index 000000000000..89a327f47e29 --- /dev/null +++ b/tests/irq_cpp/irq.h @@ -0,0 +1,30 @@ +/* +* Copyright (C) 2021 Jens Wetterich +* +* This file is subject to the terms and conditions of the GNU Lesser +* General Public License v2.1. See the file LICENSE in the top level +* directory for more details. +*/ +/** + * @ingroup tests + * @brief Dummy irq header to allow mocking. Prevents inclusion of the original IRQ header + * @{ + * + * @file + * + * @author Jens Wetterich + */ +#ifndef IRQ_H +#define IRQ_H +#ifdef __cplusplus +extern "C" { +#endif +unsigned irq_disable(); +unsigned irq_enable(); +int irq_is_enabled(); +int irq_is_in(); +void irq_restore(unsigned state); +#ifdef __cplusplus +} +#endif +#endif /* IRQ_H */ diff --git a/tests/irq_cpp/main.cpp b/tests/irq_cpp/main.cpp new file mode 100644 index 000000000000..e85ab4f8cf7c --- /dev/null +++ b/tests/irq_cpp/main.cpp @@ -0,0 +1,92 @@ +/* +* Copyright (C) 2021 Jens Wetterich +* +* This file is subject to the terms and conditions of the GNU Lesser +* General Public License v2.1. See the file LICENSE in the top level +* directory for more details. +*/ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Unit tests for the C++ IRQ wrapper irq.hpp + * + * @author Jens Wetterich + */ + +#define FFF_ARG_HISTORY_LEN 1u +#define FFF_CALL_HISTORY_LEN 1u +#include "cppunit.hpp" +#include "fff.h" +#include "irq.h" +#include "irq.hpp" +DEFINE_FFF_GLOBALS + +FAKE_VALUE_FUNC(unsigned, irq_disable) +FAKE_VALUE_FUNC(unsigned, irq_enable) +FAKE_VALUE_FUNC(int, irq_is_enabled) +FAKE_VALUE_FUNC(int, irq_is_in) +FAKE_VOID_FUNC(irq_restore, unsigned) + +class irq_suite : public riot::testing::test_suite { +public: + void set_up() override { + RESET_FAKE(irq_restore); + RESET_FAKE(irq_disable); + RESET_FAKE(irq_enable); + RESET_FAKE(irq_is_enabled); + RESET_FAKE(irq_is_in); + } +}; + +TEST_SUITE_F(irq_suite, irq); + +TEST(irq, is_isr) { + // Setup test data + irq_is_in_fake.return_val = 0; + // Run test + auto en = riot::irq_lock::is_isr(); + irq_is_in_fake.return_val = 1; + auto en2 = riot::irq_lock::is_isr(); + // Assert results + EXPECT_EQ(en, false, "Return Value"); + EXPECT_EQ(en2, true, "Return Value"); + EXPECT_FFF_CALL_COUNT(irq_is_in, 2); +} + +TEST(irq, is_enabled) { + // Setup test data + irq_is_enabled_fake.return_val = 0; + // Run test + auto en = riot::irq_lock::is_locked(); + irq_is_enabled_fake.return_val = 1; + auto en2 = riot::irq_lock::is_locked(); + // Assert results + EXPECT_STREQ("s", "s", ""); + EXPECT_EQ(en, true, "Return Value"); + EXPECT_EQ(en2, false, "Return Value"); + EXPECT_FFF_CALL_COUNT(irq_is_enabled, 2); +} + +TEST(irq, irq_disable_restore) { + // Setup test data + auto reg = 7u; + irq_disable_fake.return_val = reg; + // Run test + { + riot::irq_lock lock; + } + // Assert results + EXPECT_FFF_CALL_COUNT(irq_disable, 1); + EXPECT_FFF_CALL_COUNT(irq_restore, 1); + EXPECT_FFF_CALL_PARAMS(irq_restore, reg); + EXPECT_EQ(irq_restore_fake.arg0_val, reg, "Restore Value"); +} + +int main() { + puts("Testing irq wrapper"); + RUN_SUITE(irq); +} +/** @} */ diff --git a/tests/irq_cpp/tests/01-run.py b/tests/irq_cpp/tests/01-run.py new file mode 100755 index 000000000000..75da33551bb9 --- /dev/null +++ b/tests/irq_cpp/tests/01-run.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2021 Jens Wetterich +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +import sys +from testrunner import run + + +def testfunc(child): + child.expect_exact("Suite irq completed: SUCCESS") + print("All tests successful") + + +if __name__ == "__main__": + sys.exit(run(testfunc, timeout=1, echo=True, traceback=True))