Streamlining Keypad Testing: C++ Macros and Template Metaprogramming

Programming

Testing a keypad with multiple buttons can be a daunting task, especially when dealing with both short and long press functionalities for each button. In this blog post, we explore how to streamline this process using C++ macros and template meta-programming, particularly in the context of the ESP-IDF unity unit testing framework. This approach is not only efficient but also makes your code more maintainable and readable.

Traditional Approach

Initially, testing each button on a keypad might involve writing separate functions for each test case. For instance, if you have a keypad with eight buttons, and you need to test both short and long presses for each button, you end up with a total of 32 functions - eight buttons times two press types times two roles (master and slave).

Example of Traditional Test Functions

void test_button_1_short_press_master() {
    // Master logic for button 1 short press
}

void test_button_1_short_press_slave() {
    // Slave logic for button 1 short press
}

void test_button_1_long_press_master() {
    // Master logic for button 1 long press
}

void test_button_1_long_press_slave() {
    // Slave logic for button 1 long press
}

// Repeat for each button and press type

This approach quickly becomes cumbersome and hard to manage. Imagine the amount of redundant code and the effort needed to maintain or update these test cases.

Leveraging C++ Templates and Macros

To tackle this challenge, we turn to C++ templates and macros. The idea is to define a generic test function template and then instantiate this template for each specific button and press type using macros. This method significantly reduces the amount of code and makes it more manageable.

Template Function Definitions

// Enum to differentiate between press types
enum class PressType {
    ShortPress,
    LongPress
};

template <int Button, PressType Press>
void test_button_master() {
    // Master logic for button press, customized based on Button and Press
}

template <int Button, PressType Press>
void test_button_slave() {
    // Slave logic for button press, customized based on Button and Press
}

These templates act as blueprints for generating the specific test functions we need. They take two template parameters: the button number and the press type (short or long).

Macro for Instantiating Test Cases

#define CREATE_TEST_CASES(Button) \
    TEST_CASE_MULTIPLE_DEVICES("Button " #Button " Short Press Test", "[keypad]", \
                               test_button_master<Button, PressType::ShortPress>, \
                               test_button_slave<Button, PressType::ShortPress>); \
    TEST_CASE_MULTIPLE_DEVICES("Button " #Button " Long Press Test", "[keypad]", \
                               test_button_master<Button, PressType::LongPress>, \
                               test_button_slave<Button, PressType::LongPress>)

This macro CREATE_TEST_CASES takes a button number and expands to create two test cases: one for a short press and one for a long press. It uses the C++ preprocessor stringizing operator # to convert the button number into a string.

Final Code Structure

With the templates and macro in place, our final code structure for creating all the necessary test cases becomes remarkably succinct:

CREATE_TEST_CASES(1);
CREATE_TEST_CASES(2);
// ... up to button 8

Conclusion

By embracing the power of C++ templates and macros, we’ve transformed a potentially unwieldy testing task into a manageable and scalable solution. This approach not only saves time and effort in the initial coding phase but also simplifies future maintenance and updates, allowing for more robust and reliable testing of complex hardware like keypads.