Makefiles: Part 3 — Test, Lint, Deploy!

Be sure to check out Makefiles: Part 1 — A Gentle Introduction and Makefiles: Part 2 — Makefiles Can Make Webpages?.

This time we want to be able to:

  • run make test to run a few tests on our code,
  • run make lint to have some static checks that tell us if we have bad code quality (or code “smells” as some authors call it),
  • run make deploy to have it uploaded unto our target.

The perceptive reader might be able to see where this leads— but let’s restrain ourselves to the aforementioned goal in this blog post.

Make Rule: Deploy

Let’s start at the last step, because this is the most important rule (what is the point if we can not deploy?).

We’ll use all the help we can get, so we install PlatformIO.

PlatformIO simplifies a lot of IoT development, making it easy to build and upload to lots of targets (Arduino variants, Espressif variants, AVR in general, ARM in general, et cetera).

Head into some folder after installing PlatformIO, and run platformio init. This setups the folder structure.

Edit platformio.ini to contain this section:

[env:uno]
platform = atmelavr
framework = arduino
board = uno

This is all the information needed in order to build and upload to an Arduino Uno, which is the target we will use in this example.

Now make the file src/main.cpp and have it simply contain:

#include <Arduino.h>

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
    digitalWrite(LED_BUILTIN, LOW);
    delay(100);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(100);
}

Now, running platformio run -t upload should auto-detect a connected Arduino Uno and upload this code after compiling it.

Thus, our command for deploying is now simply the above command. Let’s wait until we have the other rules ready before putting everything into a makefile.

Having Something To Test

Let’s write a module that we can use for testing and linting.

Now we are simply blinking our LED. Let’s make a simple module that creates a more interesting LED effect.

Put this into lib/speeder/speeder.h:

#ifndef SPEEDER_H
#define SPEEDER_H

#define MAX_DELAY_MS 150

int speeder_get_delay();

#endif

 

Put this into lib/speeder/speeder.cpp:

#include "speeder.h"

int speeder_get_delay()
{
    static int delay = MAX_DELAY_MS;

    delay -= 1;
    delay %= MAX_DELAY_MS;

    return delay + 1;
}

The goal is simply to delay our blink using the value returned from the above.
We expect to first get 250 ms, then 249 ms, then 248 ms, eventually 1 ms, then it should wrap around
and start over since we used the modulo operator.

What could possibly go wrong (hehe)?

Change main.cpp to use this new module:

#include <Arduino.h>
#include <speeder.h>

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
    int delay_ms = speeder_get_delay();

    digitalWrite(LED_BUILTIN, LOW);
    delay(delay_ms);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(delay_ms);
}

Nice, now upload this via platformio run -t upload.
It seems to work— but after blinking very rapidly it simply stops!
What’s going on? Let’s write a test to find out.

Make Rule: Test

Now we will write some unit tests. There are lots of frameworks for testing C and C++ code (we could use Unity, or CppUTest, …) but let’s use Catch2 since it is so very convenient and pleasant to use. It’s a single header!

Put the header in test/catch2/catch.hpp. Now to write test cases, we have to define a main test file. This is very simple— simply create test/test_main.cpp and put this into it:


#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

That’s it!

Now let’s start writing the tests for our speeder module.
Create test/test_speeder.cpp and put this into it:

#include <catch2/catch.hpp>
#include <speeder.h>

TEST_CASE("The first value is MAX delay")
{
        REQUIRE(speeder_get_delay() == MAX_DELAY_MS);
}

Now we have to be able to compile the test framework, the test itself, and the dependencies of the test.

Here is a makefile doing just that (and it has the rule for deployment too):

.PHONY: deploy test clean

LIB_DIR=lib
TEST_DIR=test
CPP_FLAGS=-std=c++11 -Wall

INCLUDES=\
    -I$(LIB_DIR)/speeder\
    -I$(TEST_DIR)

SRCS=\
    $(TEST_DIR)/test_main.cpp\
    $(LIB_DIR)/speeder/speeder.cpp

OBJS=$(SRCS:.cpp=.o)

deploy:
    platformio run -t upload

test: $(OBJS)
    @g++ $(CPP_FLAGS) $(INCLUDES) -o run_test $(OBJS) $(TEST_SRC)
    @./run_test

%.o : %.cpp
    g++ $(CPP_FLAGS) $(INCLUDES) -c $< -o $@

clean:
    rm $(OBJS)

A bit going on here. Let’s break down what happens when using it on a test file, e.g. by running make test TEST_SRC=test/test_speeder.cpp.

  • Make sees that the target rule, test, has dependencies: The OBJS variable.
  • The OBJS variable is made by taking the sources in the SRCS variable and changing the file endings to object file endings.
  • Make now knows it has to create object files somehow. It sees the %.o rule, which matches any object file target. Notice this rule updates any object file if its corresponding source file has changed (since it lists it as a dependency).
  • We output the object file by using g++, some common flags, some include paths. We use the -c flag to get an object file, and we use the -o flag to put the resulting files alongside the sources.
  • Lastly we compile the single test specified by TEST_SRC, and output a run_test binary, which we then execute.

Run the above command to get the output: All tests passed (1 assertion in 1 test case)

Great start!

A More Useful Test

Now let’s do a more interesting test case. Let’s see that the returned delays are what we expect; 0 at the least, MAX_DELAY_MS at most.

Add the test:

TEST_CASE("Min is zero max is MAX")
{
    int min = MAX_DELAY_MS + 1;
    int max = -1;

    for(int i = 0; i < 1000; i++) {
        int val = speeder_get_delay();
        if(val < min) { min = val; } else if(val > max) {
            max = val;
        }
    }

    REQUIRE(min == 0);
    REQUIRE(max == MAX_DELAY_MS);
}

This time around, the output is:

test/test_speeder.cpp:23: FAILED:
REQUIRE( min == 0 )
with expansion:
-148 == 0

We found our bug! We can get negative values. If we check out how the modulo operator works we see why this is the case. We skip fixing the bug here and go on to show linting.

Rule: Lint

We quickly add this as a bonus.

If we install OCLint we can add a simple rule for linting our code.

Add a lint target to .PHONY, and a rule using it:

lint:
    oclint $(LINT_SRC) -- $(CPP_FLAGS) $(INCLUDES)

Now add this strange bit of code to lib/speeder/speeder.cpp:

if(MAX_DELAY_MS > 250) {
    return 250;
}

and run make lint LINT_SRC=lib/speeder/speeder.cpp.

The relevant output is:

Summary: TotalFiles=1 FilesWithViolations=1 P1=0 P2=1 P3=0
/home/grindv1k/NorwegianCreations/blog/make/make-pipeline/app/lib/speeder/speeder.cpp:11:12: constant if expression [basic|P2]

This tells us we have a priority 2 violation, since we are using an if expression where the arguments never change.

There are lots of useful things that can be detected, and as long as you can easily compile your code you can easily lint it as well.

Summary and Next Time

We can now test, lint, and deploy our code. Note: Checking coverage and doing integration tests on the target was skipped, but are also recommended steps in ensuring code quality.

As might have been expected, this is leading us to continous integration and continuous deployment.

Stay tuned for the next blogpost, in which we will use these steps in a CI-CD pipeline!

Related Posts