[ I wrote this article in mid 2019 to persuade my collaborators that we need not labor under the burdens of an overwrought mocking framework. I hope others find encouragement here to seek simple solutions. ]

Motivation

I want an approach to unit testing with a near-zero learning curve that works practically everywhere, with no exotic dependencies and no performance penalties.

Simplicity is the height of sophistication. - Clare Boothe Luce

What is Unit Testing?

Unit testing is isolating code until it is observably deterministic and then arranging initial conditions to verify expected outcomes. This is my definition.

More concretely, a function to be tested is isolated by providing mock versions of any extraneous code it calls. Each test then initializes global variables as needed, runs the function with a given set of arguments, and checks the return value, output parameters, and side effects.

Ideally, I want 100% branch coverage of any non-trivial function. This requires every possible path through the code to be traversed; all error conditions are tested, and also the “happy path”. Traversing these error paths is possible by controlling the return values and side effects of the mocked functions.

How I make these mock functions is the primary focus of this document.

Methodology

The only essential tool used here is the C preprocessor; everything else is window dressing. In a nutshell, in a C file write macros to replace certain function calls, then #include the code to test. Provide a main function that calls the code under test and checks assertions. Rinse and repeat. Use gcov to check test coverage of the code under test.

Below are examples and explanations taken from tests of the mbox functions in the lwIP port for FreeRTOS. More examples can be seen in CCAN, the C code archive network, where I first encountered the approach.

Here’s a short function, sys_mbox_new().

err_t sys_mbox_new( sys_mbox_t *pxMailBox, int iSize )
{
    err_t xReturn = ERR_MEM;
    sys_mbox_t pxTempMbox;

    pxTempMbox.xMbox = xQueueCreate( iSize, sizeof( void * ) );

    if( pxTempMbox.xMbox != NULL )
    {
        pxTempMbox.xTask = NULL;
        *pxMailBox = pxTempMbox;
        xReturn = ERR_OK;
    }

    return xReturn;
}

It wraps a call to xQueueCreate(), populates a structure, and returns an error code.

I’ll start by testing the happy path and assume the function is alone in a file, save for an initial #include "sys_arch.h". After running the function, the following should be true:

  • the return value is ERR_OK
  • the xTask element of the passed structure is NULL
  • the xMbox element of the passed structure is the result of calling xQueueCreate()

Here is a minimal unit test that I’ll refine in steps.

#include <stddef.h>
#include <assert.h>

/* prevent the inclusion of sys_arch.h */
#define __SYS_ARCH_H__

/* provide dummy versions of the types and macros */
typedef struct sys_mbox {
    void *xMbox;
    void *xTask;
} sys_mbox_t;
typedef int err_t;

#define ERR_OK  0
#define ERR_MEM 1

/* mock xQueueCreate */
void *mbox;
#define xQueueCreate(x, y) ( &mbox )

#include "sys_mbox_new.c"

int main(void){
    int err;
    sys_mbox_t foo = { &foo, &foo };

    /* happy path */
    err = sys_mbox_new(&foo, 123);
    assert(err == ERR_OK);
    assert(foo.xTask == NULL);
    assert(foo.xMbox == &mbox);

    return 0;
}

The macro definition of xQueueCreate() causes the call in sys_mbox_new() to be rewritten as pxTempMbox.xMbox = (&mbox); Should any of the assertions prove false, the programs aborts immediately with an error message.

Statement Expressions

I’ll test that the second argument to sys_mbox_new() is passed as the first argument to xQueueCreate().

int keep;
void *mbox;
#define xQueueCreate(x, y) ({ keep = (x); &mbox; })

To main, I add assert(keep == 123);. The new macro definition uses a C language extension called a statement expression which permits a block to take the place of an expression. The value of the last statement becomes the value of the expression as a whole.

For toolchains that don’t support statement expressions, the same result can be had with the comma operator, or by writing a small function with the macro calling that function.

Reset

To write a thorough unit test, more than the happy path needs to be traversed. Between calls to the function under test, variables like keep need to be set to known values, and the return value of xQueueCreate() should vary at need.

#define UNDEFINED 0xcafecafe

int keep;
void *mbox;
#define xQueueCreate(x, y) ({ keep = (x); mbox; })
#define xqcReset() do { \
    keep = -UNDEFINED;  \
    mbox = &mbox;       \
} while (0)

A simple convention is to set pointers to their own addresses. The macro now returns the value of mbox, which defaults to the address of mbox. The value of mbox can be set to NULL to test failure cases.

int main(void) {
    int err;
    sys_mbox_t foo, fooDflt = { &foo, &foo };

    #define reset() do {    \
        xqcReset();         \
        foo = fooDflt;      \
    } while (0)

    /* ... */

    /* xQueueCreate returns NULL */
    reset();
    mbox = NULL;
    err = sys_mbox_new(&foo, 234);
    assert(err == ERR_MEM);
    assert(keep == 234);
    assert(foo.xTask == &foo); /* unchanged */
    assert(foo.xMbox == &foo); /* unchanged */

    /* ... */
}

Test Anything Protocol

There are two drawback to using assert: there’s no indication that the tests are actually running, and testing stops with the first failure. I’ll provide an assert-like macro that counts successes but does not abort failures, and prints each result. A simple convention is to print “ok” or “not ok” followed by a test number. This convention is embodied as the Test Anything Protocol.

#include <stdio.h>

int ok_cnt_;    /* number of passing tests */
int index_;     /* ordinal of current test */

#define ok(x) do {                      \
    char *msg = "not ok";               \
    index_++;                           \
    if (x) {                            \
        ok_cnt_++;                      \
        msg += 4;                       \
    }                                   \
    printf("%s %d\n", msg, index_);     \
} while (0)

This produces output like:

$ ./test-sys_mbox_new
ok 1
ok 2
ok 3
ok 4

TAP has some other useful ideas. The unit test should indicate how many tests are expected to run by printing 1..N where N is the number of tests. This is called the plan. If a test fails, some diagnostic output can be appended to the line. A summary line can indicate if the plan matches the success count.

$ ./test-sys_mbox_new
1..4
ok 1
ok 2
not ok 3 - test-sys_mbox_new.c:26       foo.xTask == NULL
ok 4
FAILED

A minimally sufficient TAP implementation in a few dozen lines is shown as an appendix. It provides three macros: plan(), ok(), and tally().

The Complete Unit Test

The typedefs and constants are useful for testing other mbox functions, so they are moved to a common header file.

#include "common.h"

int keep;
void *mbox;
#define xQueueCreate(x, y) ({ keep = (x); mbox; })
#define xqcReset() do { \
    keep = -UNDEFINED;  \
    mbox = &mbox;       \
} while (0)

#include "sys_mbox_new.c"

int main(void) {
    int err;
    sys_mbox_t foo, fooDflt = { &foo, &foo };

    #define reset() do {    \
        xqcReset();         \
        foo = fooDflt;      \
    } while (0)

    plan(8);

    /* happy path */
    reset();
    err = sys_mbox_new(&foo, 123);
    ok(err == ERR_OK);
    ok(keep == 123);
    ok(foo.xTask == NULL);
    ok(foo.xMbox == &mbox);

    /* xQueueCreate returns NULL */
    reset();
    mbox = NULL;
    err = sys_mbox_new(&foo, 234);
    ok(err == ERR_MEM);
    ok(keep == 234);
    ok(foo.xTask == &foo); /* unchanged */
    ok(foo.xMbox == &foo); /* unchanged */

    return tally();
}

Scripting to Save Effort

The initial assumption of one function per file is both unlikely and impractical. It’s certainly possible to write unit tests for multiple functions in a single main(), but it’s also reasonable to use a tool to extract a single function into a temporary file. Combine this with a few more tools, and the effort can be automated. I’ll use make and a patched ctags to do the heavy lifting.

Nota bene, the makefile, report script, and tap.h are available in the mock4thewin repo.

make

make is the traditional dependency-based build automation tool. A Makefile contains rules that make uses to generate an output file from one or more input files. My Makefile starts by reading the list of .c files starting with test-. From this list, it derives the list of functions to extract, and the list of executables to build. In $(pathsubst ...) and later in rules, % is a wildcard character.

TEST_SRC:=$(wildcard test-*.c)
TEMP_SRC:=$(patsubst test-%,%,$(TEST_SRC))
TESTS=$(patsubst %.c,%,$(TEST_SRC))

Given these directory contents:

Makefile  README  common.h  report  tap.h  test-sys_arch_mbox_fetch.c  test-sys_mbox_free.c  test-sys_mbox_new.c

the variables have these values.

TEST_SRC := test-sys_arch_mbox_fetch.c test-sys_mbox_new.c test-sys_mbox_free.c
TEMP_SRC := sys_arch_mbox_fetch.c sys_mbox_new.c sys_mbox_free.c
TESTS := test-sys_arch_mbox_fetch test-sys_mbox_new test-sys_mbox_free

Here’s a rule describing how to compile each test.

$(TESTS): test-%: test-%.c %.c common.h tap.h
	$(CC) $(CFLAGS) $< -o $@

The first line captures dependencies. For example, test-sys_mbox_new depends on test-sys_mbox_new.c, sys_mbox_new.c, common.h, and tap.h The second line, which must begin with a real tab character, is the command to run. The $< and $@ are replaced by make with the input and output files respectively.

Exuberant Ctags

While I used to use sed to dump functions from source files, I wanted a more robust approach. As I could find no satisfactory ready-made solution, I adapted the faithful ctags tool. To quote the man page:

The ctags program generates an index file for a variety of language objects found in source files. This tag file allows these items to be quickly and easily located by a text editor or other utility. A “tag” signifies a language object for which an index entry is available.

ctags can record the starting line numbers of functions in the index (the xref format). My patch causes it to also record the ending line numbers.

The code is available in the ctags-xref repo.

It includes a bash script, show-source, that uses the line numbers from the index to dump a given function.

Here are the calls to ctags-xref and show-source used in my Makefile. make replaces the $(pathsubst ...) portion with the output filename minus the .c.

index: ../sys_arch.c
	ctags-xref $< > index

$(TEMP_SRC): index
	show-source $(patsubst %.c,%,$@) > $@

(The earlier section on using sed in now in the appendix.)

Makefile

Here is the complete Makefile. A more general and reusable version may be found in Mock4thewin.mk.

TEST_SRC:=$(wildcard test-*.c)
TEMP_SRC:=$(patsubst test-%,%,$(TEST_SRC))
TESTS=$(patsubst %.c,%,$(TEST_SRC))

CFLAGS+=-std=c99 -Wall -fprofile-arcs -ftest-coverage -O0 -ggdb

build: $(TESTS)

test: build
	@rm -f *.gcov *.gcda
	@unit-test-report $(TESTS)

clean:
	rm -f $(TESTS) $(TEMP_SRC) index *.gc??

.PHONY: build test clean

index: ../sys_arch.c
	ctags-xref $< > index

$(TEMP_SRC): index
	show-source $(patsubst %.c,%,$@) > $@

$(TESTS): test-%: test-%.c %.c common.h tap.h
	$(CC) $(CFLAGS) $< -o $@

unit-test-report above will be described after gcov.

gcov

gcov is a tool to determine how many times each line of source code is executed for a given program run. It combines metadata from the program’s compilation with trace data from the program’s execution. Two args are given to the compiler, -fprofile-arcs and -ftest-coverage. The metadata file has the name of the source file, and ends with .gcno. When the program is run, it outputs a file ending with .gcda. To get the final counts, gcov is run with the program file as argument. It writes the counts to a file that ends with .gcov.

$ gcov -f test-sys_mbox_new
Function 'main'
Lines executed:100.00% of 17

Function 'sys_mbox_new'
Lines executed:100.00% of 8

File 'test-sys_mbox_new.c'
Lines executed:100.00% of 17
Creating 'test-sys_mbox_new.c.gcov'

File 'sys_mbox_new.c'
Lines executed:100.00% of 8
Creating 'sys_mbox_new.c.gcov'

Here are the contents of sys_mbox_new.c.gcov.

        -:    0:Source:sys_mbox_new.c
        -:    0:Graph:test-sys_mbox_new.gcno
        -:    0:Data:test-sys_mbox_new.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        2:    1:err_t sys_mbox_new( sys_mbox_t *pxMailBox, int iSize )
        -:    2:{
        2:    3:    err_t xReturn = ERR_MEM;
        -:    4:    sys_mbox_t pxTempMbox;
        -:    5:
        2:    6:    pxTempMbox.xMbox = xQueueCreate( iSize, sizeof( void * ) );
        -:    7:
        2:    8:    if( pxTempMbox.xMbox != NULL )
        -:    9:    {
        1:   10:        pxTempMbox.xTask = NULL;
        1:   11:        *pxMailBox = pxTempMbox;
        1:   12:        xReturn = ERR_OK;
        -:   13:    }
        -:   14:
        2:   15:    return xReturn;
        -:   16:}

Lines beginning with a dash have no executable code. Any line that was not traversed will have five # characters instead of a count. If I comment out the happy path case from the sys_mbox_new() unit test, the contents of sys_mbox_new.c.gcov are this:

        -:    0:Source:sys_mbox_new.c
        -:    0:Graph:test-sys_mbox_new.gcno
        -:    0:Data:test-sys_mbox_new.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        1:    1:err_t sys_mbox_new( sys_mbox_t *pxMailBox, int iSize )
        -:    2:{
        1:    3:    err_t xReturn = ERR_MEM;
        -:    4:    sys_mbox_t pxTempMbox;
        -:    5:
        1:    6:    pxTempMbox.xMbox = xQueueCreate( iSize, sizeof( void * ) );
        -:    7:
        1:    8:    if( pxTempMbox.xMbox != NULL )
        -:    9:    {
    #####:   10:        pxTempMbox.xTask = NULL;
    #####:   11:        *pxMailBox = pxTempMbox;
    #####:   12:        xReturn = ERR_OK;
        -:   13:    }
        -:   14:
        1:   15:    return xReturn;
        -:   16:}

gcov will also show branch coverage with the -b switch.

$ gcov -b -f ./test-sys_mbox_new
Function 'main'
Lines executed:100.00% of 17
Branches executed:93.33% of 60
Taken at least once:46.67% of 60
Calls executed:56.52% of 23

Function 'sys_mbox_new'
Lines executed:100.00% of 8
Branches executed:100.00% of 2
Taken at least once:100.00% of 2
No calls

File 'test-sys_mbox_new.c'
Lines executed:100.00% of 17
Branches executed:93.33% of 60
Taken at least once:46.67% of 60
Calls executed:56.52% of 23
Creating 'test-sys_mbox_new.c.gcov'

File 'sys_mbox_new.c'
Lines executed:100.00% of 8
Branches executed:100.00% of 2
Taken at least once:100.00% of 2
No calls
Creating 'sys_mbox_new.c.gcov'

Here are the contents of sys_mbox_new.c.gcov with branch coverage.

        -:    0:Source:sys_mbox_new.c
        -:    0:Graph:./test-sys_mbox_new.gcno
        -:    0:Data:./test-sys_mbox_new.gcda
        -:    0:Runs:1
        -:    0:Programs:1
function sys_mbox_new called 2 returned 100% blocks executed 100%
        2:    1:err_t sys_mbox_new( sys_mbox_t *pxMailBox, int iSize )
        -:    2:{
        2:    3:    err_t xReturn = ERR_MEM;
        -:    4:    sys_mbox_t pxTempMbox;
        -:    5:
        2:    6:    pxTempMbox.xMbox = xQueueCreate( iSize, sizeof( void * ) );
        -:    7:
        2:    8:    if( pxTempMbox.xMbox != NULL )
branch  0 taken 50% (fallthrough)
branch  1 taken 50%
        -:    9:    {
        1:   10:        pxTempMbox.xTask = NULL;
        1:   11:        *pxMailBox = pxTempMbox;
        1:   12:        xReturn = ERR_OK;
        -:   13:        #if SYS_STATS
        -:   14:        {
        -:   15:            SYS_STATS_INC_USED( mbox );
        -:   16:        }
        -:   17:        #endif /* SYS_STATS */
        -:   18:    }
        -:   19:
        2:   20:    return xReturn;
        -:   21:}

Summary Report

A small bash script runs a test and prints a summary.

$ unit-test-report test-sys_mbox_new
FUNCTION                 UNIT TESTS     LINES EXECUTED      BRANCHES TAKEN
sys_mbox_new             PASSED         100.00% of 8        100.00% of 2

With the happy path case commented out, this is the report.

$ unit-test-report test-sys_mbox_new
FUNCTION                 UNIT TESTS     LINES EXECUTED      BRANCHES TAKEN
sys_mbox_new             FAILED         62.50% of 8         50.00% of 2
branch  1 taken 100%
        -:    9:    {
    #####:   10:        pxTempMbox.xTask = NULL;
    #####:   11:        *pxMailBox = pxTempMbox;
    #####:   12:        xReturn = ERR_OK;
        -:   13:        #if SYS_STATS
        -:   14:        {

Running make test builds all the tests and prints all the reports.

$ make test
FUNCTION                 UNIT TESTS     LINES EXECUTED      BRANCHES TAKEN
sys_arch_mbox_fetch      PASSED         100.00% of 26       100.00% of 40
sys_mbox_new             PASSED         100.00% of 8        100.00% of 2
sys_mbox_free            PASSED         100.00% of 13       100.00% of 8

The complete script may be seen in unit-test-report.

A More Involved Example

The sys_mbox_free() function releases the resources acquired by sys_mbox_new(). It has one critical section.

void sys_mbox_free( volatile sys_mbox_t *pxMailBox )
{
    unsigned long ulMessagesWaiting;
    QueueHandle_t xMbox;
    TaskHandle_t xTask;

    if( pxMailBox != NULL )
    {
        ulMessagesWaiting = uxQueueMessagesWaiting( pxMailBox->xMbox );
        configASSERT( ( ulMessagesWaiting == 0 ) );

        taskENTER_CRITICAL();
        xMbox = pxMailBox->xMbox;
        xTask = pxMailBox->xTask;
        pxMailBox->xMbox = NULL;
        taskEXIT_CRITICAL();

        if( xTask != NULL )
        {
            xTaskAbortDelay( xTask );
        }

        if( xMbox != NULL )
        {
            vQueueDelete( xMbox );
        }
    }
}

This unit test requires six functions to be mocked. I’ll use counts to ensure each is called the appropriate number of times. sys_mbox_free() has several invariants. Whenever the critical section is reached, the xMbox element is set to NULL. Every entry to a critical section has a matching exit.

#include "common.h"

int critical;
#define taskENTER_CRITICAL()    do { critical++; } while (0)
#define taskEXIT_CRITICAL()     do { critical--; } while (0)
#define critReset()             do { critical = 0; } while (0)

int uqmwRet;
void *uqmwHandle;
#define uxQueueMessagesWaiting(x)  ({ uqmwHandle = (x); uqmwRet; })
#define uqmwReset()         do { uqmwRet = 0; uqmwHandle = &uqmwHandle; } while (0)

void *xtadHandle;
int xtadCallCount;
#define xTaskAbortDelay(x)  do { xtadCallCount++; xtadHandle = (x); } while (0)
#define xtadReset()         do { xtadCallCount = 0; xtadHandle = &xtadHandle; } while (0)

void *vqdHandle;
int vqdCallCount;
#define vQueueDelete(x)     do { vqdCallCount++; vqdHandle = (x); } while (0)
#define vqdReset()          do { vqdCallCount = 0; vqdHandle = &vqdHandle; } while (0)

int assertCount;
#define configASSERT(x)     do { if (!(x)) { assertCount++; return; } } while (0)
#define assertReset()       do { assertCount = 0; } while (0)

#include "sys_mbox_free.c"

int main(void) {
    void *mbox, *task;
    sys_mbox_t foo, fooDflt = { &mbox, &task };

    #define reset() do {    \
        critReset();        \
        uqmwReset();        \
        xtadReset();        \
        vqdReset();         \
        assertReset();      \
        foo = fooDflt;      \
    } while (0)
    reset();

    /* invariant sets are cumulative */
    #define invariants(x) do {      \
        switch (x) {                \
        /* crit section reached */  \
        case 2:                     \
            ok(foo.xMbox == NULL);  \
            ok(assertCount == 0);   \
        /* always */                \
        case 1:                     \
            ok(critical == 0);      \
        }                           \
        reset();                    \
    } while (0)

    plan(38);

    /* NULL arg is NOOP */
    sys_mbox_free(NULL);
    ok(uqmwHandle == &uqmwHandle);
    ok(assertCount == 0);
    ok(xtadCallCount == 0);
    ok(vqdCallCount == 0);
    invariants(1);

    /* trip no-messages assertion */
    uqmwRet = 1;
    sys_mbox_free(&foo);
    ok(uqmwHandle == &mbox);
    ok(assertCount == 1);
    ok(xtadCallCount == 0);
    ok(vqdCallCount == 0);
    invariants(1);

    /* NULL struct members */
    foo = (sys_mbox_t){NULL, NULL};
    sys_mbox_free(&foo);
    ok(uqmwHandle == NULL);
    ok(xtadCallCount == 0);
    ok(vqdCallCount == 0);
    invariants(2);

    /* NULL mbox member */
    foo.xMbox = NULL;
    sys_mbox_free(&foo);
    ok(uqmwHandle == NULL);
    ok(xtadCallCount == 1);
    ok(xtadHandle == &task);
    ok(vqdCallCount == 0);
    invariants(2);

    /* NULL task member */
    foo.xTask = NULL;
    sys_mbox_free(&foo);
    ok(uqmwHandle == &mbox);
    ok(xtadCallCount == 0);
    ok(vqdCallCount == 1);
    ok(vqdHandle == &mbox);
    invariants(2);

    /* happy path */
    sys_mbox_free(&foo);
    ok(uqmwHandle == &mbox);
    ok(xtadCallCount == 1);
    ok(xtadHandle == &task);
    ok(vqdCallCount == 1);
    ok(vqdHandle == &mbox);
    invariants(2);

    return tally();
}

I check invariants after every call to the function under test. I bundle the call to reset() within to minimize boilerplate. The switch separates the invariants which must always hold, i.e., that entry and exit to critical sections are balanced, from those that must hold after the critical section.

Conclusion

The C preprocessor provides everything I need to write thorough unit tests. I can read, write, and reason about tests with the same skills I use for C in general. To this, I add five dozen lines of tooling for automation. The sum is familiar, fast, and flexible.

Appendix A: tap.h

/* minimal TAP: test anything protocol */
/* https://testanything.org/ */

#include <stdio.h>
#include <stdlib.h>

int plan_;		/* number of tests */
int ok_count_;		/* number of passing tests */
int index_;		/* ordinal of current test */
int verbose_;		/* flag: show all expressions */

/* how many tests should run? */
/* set verbose flag if environment var VERBOSE exists */
#define plan(x) do {						\
	plan_ = (x);						\
	if (plan_) {						\
		printf("%d..%d\n", 1, plan_);			\
	}							\
	if (getenv("VERBOSE")) {				\
		verbose_ = 1;					\
	}							\
} while (0)

/* test expression x and keep count of true values */
/* output diagnostic if false or if VERBOSE is set */
#define ok(x) do {						\
	int pass = (x);						\
	char *msg = "not ok";					\
	index_++;						\
	if (pass) {						\
		ok_count_++;					\
		msg += 4;					\
	}							\
	if (verbose_ || !pass) {				\
		printf("%s %d - %s:%d\t%s\n", msg, index_,	\
				__FILE__, __LINE__, #x);	\
	}							\
	else {							\
		printf("%s %d\n", msg, index_);			\
	}							\
} while (0)

/* summarize test run, and return an exit value for main() */
#define grade() ({						\
	int pass;						\
	if (!plan_) {						\
		plan(index_);					\
	}							\
	pass = plan_ == ok_count_;				\
	printf("%s\n", pass ? "PASSED" : "FAILED");		\
	pass ? EXIT_SUCCESS : EXIT_FAILURE;			\
})


/* vim: set ts=8 noet */

Appendix B: sed

Originally, I used sed to dump a function into a temporary file.

sed is a non-interactive editor that uses regular expressions to transform text. For example, I can use sed to replace the earlier calls to assert() with calls to ok.

$ sed 's/assert/ok/' old.c > new.c

The following sed will extract sys_mbox_new() from sys_arch.c.

$ sed -nr '/^err_t sys_mbox_new\(/,/^}/p' sys_arch.c > sys_mbox_new.c

The quoted argument to sed, called a sed script, says to start printing with a line that begins with err_t sys_mbox_new( and stop printing after a line that begins with }. The printed output is redirected to the file, sys_mbox_new.c.

Here’s a slightly more general sed script.

$ sed -nr '/^(void|u32_t|err_t) sys_mbox_.*\(.*[^;]$/,/^}/p' sys_arch.c > tmp.c

This time, the line can begin with one of three types: void, u32_t, or err_t, but it must not end with a semicolon. The .* portions are wildcards.

Here is the call to sed used in my Makefile. In a Makefile, a literal $ character must be quoted as $$. make replaces the $(pathsubst ...) portion with the output filename minus the .c.

$(TEMP_SRC): ../sys_arch.c
	sed -nr '/^(void|u32_t|err_t) $(patsubst %.c,%,$@)\(.*[^;]$$/,/^}/p' $< > $@

This rule causes make to run the following commands.

sed -nr '/^(void|u32_t|err_t) sys_arch_mbox_fetch\(.*[^;]$/,/^}/p' ../sys_arch.c > sys_arch_mbox_fetch.c
sed -nr '/^(void|u32_t|err_t) sys_mbox_new\(.*[^;]$/,/^}/p' ../sys_arch.c > sys_mbox_new.c
sed -nr '/^(void|u32_t|err_t) sys_mbox_free\(.*[^;]$/,/^}/p' ../sys_arch.c > sys_mbox_free.c