Test-driven C – Autotools, cmocka

Autotools

It seems Autotools is more often than not spoken about in negative terms. I believe this is because approaching the framework can be a bit daunting and that the work flow can seem bureaucratic. As a typical C programmer I’m a control freak that want to understand everything that goes on under the hood, and as soon as I see a cryptic macro hiding a lot of functionality in a way that is not clear, I tend to shy away instinctively.

But Autotools does simplify a lot of the standard routines related to the build/test/distribute/install process, and as long as you overcome the initial impression it will definitely to the job. The general idea behind this post is to give newcomers to Autotools a simple starting point.

The complete project can be found here.

1. Write some tests

We’re going to create a dummy “state” module, so let’s invent some dummy functionality and test it.

[test/state.c]
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>

#include <state.h>

void test1()
{
  state *s;

  s = state_new();
  assert_non_null(s);
  assert_int_equal(state_get(s), 0);
  state_free(s);
}

void test2()
{
  state *s;
  int e;

  s = state_new();
  assert_non_null(s);

  e = state_set(s, 5);
  assert_int_equal(e, 0);
  assert_int_equal(state_get(s), 5);

  e = state_set(s, 42);
  assert_int_equal(e, -1);
  assert_int_equal(state_get(s), 5);
}

int main(void)
{
  const UnitTest tests[] = {
    unit_test(test1),
    unit_test(test2),
  };

  return run_tests(tests);
}

We’re using cmocka here so make sure it is installed.

Compile

$ make
make: *** No targets specified and no makefile found.  Stop.

That failed, no surprise there.

2. Setup Autotools

autogen.sh
#!/bin/sh
mkdir -p autotools m4
autoreconf --force --install
configure.ac
AC_INIT([state], [0.1.0], [fredrik.widlund@gmail.com], [state], [https://github.com/fredrikwidlund/automake-cmocka-example])
AC_CONFIG_AUX_DIR([autotools])
AC_CONFIG_MACRO_DIR([m4])
AM_INIT_AUTOMAKE([1.12 subdir-objects -Wall -Werror no-define])

: ${CFLAGS="-O3 -Wall -Wextra -Wpedantic -std=c11"}
AC_PROG_CC

AC_CONFIG_FILES([Makefile])
AC_OUTPUT
Makefile.am
ACLOCAL_AMFLAGS = -I m4

check_PROGRAMS = test/state
TESTS = test/state

maintainer-clean-local:
        rm -rf aclocal.m4 configure Makefile.in autotools m4

As you can see this is actually not so complex. I won’t comment on every line but instead give some general comments.

  • We are bootstrapping the generation of the Autotools files, with sub-directories for some of the autotools and m4 files being created (autogen.sh)
  • We give some basic definitions of the project (AC_INIT, AC_INIT_AUTOMAKE)
  • We specify that we intend to use a C Compiler (AC_PROG_CC)
  • We tell Autotools to use an underlying make system (AC_CONFIG_FILES([Makefile]))
  • We tell Autotools where our test command located, and supply some extra information about files to clean (Makefile.am)

Compile

So, let’s try another build.

$ ./autogen.sh 
configure.ac:7: installing 'autotools/compile'
configure.ac:4: installing 'autotools/install-sh'
configure.ac:4: installing 'autotools/missing'
Makefile.am: installing './INSTALL'
Makefile.am: error: required file './NEWS' not found
Makefile.am: error: required file './README' not found
Makefile.am: error: required file './AUTHORS' not found
Makefile.am: error: required file './ChangeLog' not found
Makefile.am: installing './COPYING' using GNU General Public License v3 file
Makefile.am:     Consider adding the COPYING file to the version control system
Makefile.am:     for your code, to avoid questions about which license your project uses
Makefile.am: installing 'autotools/depcomp'
autoreconf: automake failed with exit status: 1

Autotools requires by convention the presence of a number of files, including for example a license (“COPYING”). A generic document regarding installation (“INSTALL”) will be created, and a GPL3 added for you. The rest of the files we can just add empty placeholders for ourselves.

Let’s do that, and try another build.

$ touch NEWS README AUTHORS ChangeLog
$ ./autogen.sh 
$ ./configure 
checking for a BSD-compatible install... /usr/bin/install -c
[...]
configure: creating ./config.status
config.status: creating Makefile
config.status: executing depfiles commands
$ make check
[...]
/Applications/Xcode.app/Contents/Developer/usr/bin/make  test/state
depbase=`echo test/state.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\
	gcc -DPACKAGE_NAME=\"state\" -DPACKAGE_TARNAME=\"state\" -DPACKAGE_VERSION=\"0.1.0\" -DPACKAGE_STRING=\"state\ 0.1.0\" -DPACKAGE_BUGREPORT=\"fredrik.widlund@gmail.com\" -DPACKAGE_URL=\"https://github.com/fredrikwidlund/automake-cmocka-example\" -I.     -O3 -Wall -Wextra -Wpedantic -std=c11 -MT test/state.o -MD -MP -MF $depbase.Tpo -c -o test/state.o test/state.c &&\
	mv -f $depbase.Tpo $depbase.Po
test/state.c:6:10: fatal error: 'state.h' file not found
#include 
         ^
1 error generated.
make[1]: *** [test/state.o] Error 1
make: *** [check-am] Error 2

Great, the build steps work, and we naturally get an error since we depend on a module that is not implemented yet.

3. Write some code

src/state.h
#ifndef STATE_H_INCLUDED
#define STATE_H_INCLUDED

typedef struct state state;

struct state
{
  int value;
};

state *state_new();
int    state_set(state *, int);
int    state_get(state *);
void   state_free(state *);

#endif /* STATE_H_INCLUDED */
src/state.c
#include <stdlib.h>
#include "state.h"

state *state_new()
{
  state *s;

  s = malloc(sizeof *s);
  if (!s)
    return NULL;

  s->value = 0;

  return s;
}

int state_set(state *s, int value)
{
  if (value < 1 || value > 10)
    return -1;

  s->value = value;

  return 0;
}

int state_get(state *s)
{
  return s->value;
}

void state_free(state *s)
{
  free(s);
}

In configure.ac we need to add the following lines

AM_PROG_AR
AC_PROG_LIBTOOL

These enable the capabilities in Autotools to build libraries.

In Makefile.am we need to add the build rules for the library.

lib_LTLIBRARIES = libstate.la
libstate_la_SOURCES = src/state.c src/state.h

We also need to add some hints for the tests, in order for it to find the library.

test_state_CFLAGS =  -I$(top_srcdir)/src
test_state_LDFLAGS = -lstate -lcmocka

“$(top_srcdir)” is required instead of “..” since some functionality like “make distcheck” will use a different directory structure.

Let’s clean everything up, and build from scratch.

$ make maintainer-clean
[...]
$ ./autogen.sh 
[...]
$ ./configure
[...]
$ make check
[...]
PASS: test/state
make[3]: Nothing to be done for `all'.
============================================================================
Testsuite summary for state 0.1.0
============================================================================
# TOTAL: 1
# PASS:  1
# SKIP:  0
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================

There. See, it’s not that hard!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s