Skip to content

Commit

Permalink
unit-tests: proof of concept test framework
Browse files Browse the repository at this point in the history
This patch contains a proof of concept implementation for writing unit
tests with TAP output. Each test is a function that contains one or more
checks. The test is run with the TEST() macro and if any of the checks
fail then the test will fail. A complete program that tests STRBUF_INIT
would look like

    #include "test-lib.h"
    #include "strbuf.h"

    static void t_static_init(void)
    {
            struct strbuf buf = STRBUF_INIT;

            check_uint(buf.len, ==, 0);
            check_uint(buf.alloc, ==, 0);
            if (check(buf.buf == strbuf_slopbuf))
		    return; /* avoid SIGSEV */
            check_char(buf.buf[0], ==, '\0');
    }

    int main(void)
    {
            TEST(t_static_init(), "static initialization works);

            return test_done();
    }

The output of this program would be

    ok 1 - static initialization works
    1..1

If any of the checks in a test fail then they print a diagnostic message
to aid debugging and the test will be reported as failing. For example a
failing integer check would look like

    # check "x >= 3" failed at my-test.c:102
    #    left: 2
    #   right: 3
    not ok 1 - x is greater than or equal to three

There are a number of check functions implemented so far. check() checks
a boolean condition, check_int(), check_uint() and check_char() take two
values to compare and a comparison operator. check_str() will check if
two strings are equal. Custom checks are simple to implement as shown in
the comments above test_assert() in test-lib.h.

Tests can be skipped with test_skip() which can be supplied with a
reason for skipping which it will print. Tests can print diagnostic
messages with test_msg().  Checks that are known to fail can be wrapped
in TEST_TODO().

There are a couple of example test programs included in this
patch. t-basic.c implements some self-tests and demonstrates the
diagnostic output for failing test. The output of this program is
checked by t0080-unit-test-output.sh. t-strbuf.c shows some example
unit tests for strbuf.c

The unit tests can be built with "make unit-tests" (this works but the
Makefile changes need some further work). Once they have been built they
can be run manually (e.g t/unit-tests/t-strbuf) or with prove.

Signed-off-by: Phillip Wood <[email protected]>
  • Loading branch information
phillipwood committed May 12, 2023
1 parent 69c7866 commit 2d29258
Show file tree
Hide file tree
Showing 7 changed files with 716 additions and 2 deletions.
23 changes: 21 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ SCRIPT_LIB =
TEST_BUILTINS_OBJS =
TEST_OBJS =
TEST_PROGRAMS_NEED_X =
UNIT_TEST_PROGRAMS =
THIRD_PARTY_SOURCES =

# Having this variable in your environment would break pipelines because
Expand Down Expand Up @@ -2665,13 +2666,20 @@ SCALAR_OBJS += scalar.o
.PHONY: scalar-objs
scalar-objs: $(SCALAR_OBJS)

UNIT_TEST_PROGRAMS += t-basic
UNIT_TEST_PROGRAMS += t-strbuf
UNIT_TEST_PROGS = $(patsubst %,t/unit-tests/%$X,$(UNIT_TEST_PROGRAMS))
UNIT_TEST_OBJS = $(patsubst %,t/unit-tests/%.o,$(UNIT_TEST_PROGRAMS))
UNIT_TEST_OBJS += t/unit-tests/test-lib.o

OBJECTS += $(GIT_OBJS)
OBJECTS += $(SCALAR_OBJS)
OBJECTS += $(PROGRAM_OBJS)
OBJECTS += $(TEST_OBJS)
OBJECTS += $(XDIFF_OBJS)
OBJECTS += $(FUZZ_OBJS)
OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)
OBJECTS += $(UNIT_TEST_OBJS)

ifndef NO_CURL
OBJECTS += http.o http-walker.o remote-curl.o
Expand Down Expand Up @@ -3167,7 +3175,7 @@ endif

test_bindir_programs := $(patsubst %,bin-wrappers/%,$(BINDIR_PROGRAMS_NEED_X) $(BINDIR_PROGRAMS_NO_X) $(TEST_PROGRAMS_NEED_X))

all:: $(TEST_PROGRAMS) $(test_bindir_programs)
all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)

bin-wrappers/%: wrap-for-bin.sh
$(call mkdir_p_parent_template)
Expand Down Expand Up @@ -3592,7 +3600,7 @@ endif

artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \
GIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \
$(MOFILES)
$(UNIT_TEST_PROGS) $(MOFILES)
$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \
SHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'
test -n "$(ARTIFACTS_DIRECTORY)"
Expand Down Expand Up @@ -3831,3 +3839,14 @@ $(FUZZ_PROGRAMS): all
$(XDIFF_OBJS) $(EXTLIBS) git.o $@.o $(LIB_FUZZING_ENGINE) -o $@

fuzz-all: $(FUZZ_PROGRAMS)

t/unit-tests/t-basic$X: t/unit-tests/t-basic.o t/unit-tests/test-lib.o $(GITLIBS) GIT-LDFLAGS
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
$(filter %.o,$^) $(filter %.a,$^) $(LIBS)

t/unit-tests/t-strbuf$X: t/unit-tests/t-strbuf.o t/unit-tests/test-lib.o $(GITLIBS) GIT-LDFLAGS
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
$(filter %.o,$^) $(filter %.a,$^) $(LIBS)

.PHONY: unit-tests
unit-tests: $(UNIT_TEST_PROGS)
58 changes: 58 additions & 0 deletions t/t0080-unit-test-output.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/sh

test_description='Test the output of the unit test framework'

. ./test-lib.sh

test_expect_success 'TAP output from unit tests' '
cat >expect <<-EOF &&
ok 1 - passing test
ok 2 - passing test and assertion return 0
# check "1 == 2" failed at t/unit-tests/t-basic.c:68
# left: 1
# right: 2
not ok 3 - failing test
ok 4 - failing test and assertion return -1
not ok 5 - passing TEST_TODO() # TODO
ok 6 - passing TEST_TODO() returns 0
# todo check ${SQ}check(x)${SQ} succeeded at t/unit-tests/t-basic.c:17
not ok 7 - failing TEST_TODO()
ok 8 - failing TEST_TODO() returns -1
# check "0" failed at t/unit-tests/t-basic.c:22
# skipping test - missing prerequisite
# skipping check ${SQ}1${SQ} at t/unit-tests/t-basic.c:24
ok 9 - test_skip() # SKIP
ok 10 - skipped test returns 0
# skipping test - missing prerequisite
ok 11 - test_skip() inside TEST_TODO() # SKIP
ok 12 - test_skip() inside TEST_TODO() returns 0
# check "0" failed at t/unit-tests/t-basic.c:40
not ok 13 - TEST_TODO() after failing check
ok 14 - TEST_TODO() after failing check returns -1
# check "0" failed at t/unit-tests/t-basic.c:48
not ok 15 - failing check after TEST_TODO()
ok 16 - failing check after TEST_TODO() returns -1
# check "!strcmp("\thello\\\\", "there\"\n")" failed at t/unit-tests/t-basic.c:53
# left: "\011hello\\\\"
# right: "there\"\012"
# check "!strcmp("NULL", NULL)" failed at t/unit-tests/t-basic.c:54
# left: "NULL"
# right: NULL
# check "${SQ}a${SQ} == ${SQ}\n${SQ}" failed at t/unit-tests/t-basic.c:55
# left: ${SQ}a${SQ}
# right: ${SQ}\012${SQ}
# check "${SQ}\\\\${SQ} == ${SQ}\\${SQ}${SQ}" failed at t/unit-tests/t-basic.c:56
# left: ${SQ}\\\\${SQ}
# right: ${SQ}\\${SQ}${SQ}
not ok 17 - messages from failing string and char comparison
# BUG: test has no checks at t/unit-tests/t-basic.c:83
not ok 18 - test with no checks
ok 19 - test with no checks returns -1
1..19
EOF
! "$GIT_BUILD_DIR"/t/unit-tests/t-basic >actual &&
test_cmp expect actual
'

test_done
2 changes: 2 additions & 0 deletions t/unit-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/t-basic
/t-strbuf
87 changes: 87 additions & 0 deletions t/unit-tests/t-basic.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#include "test-lib.h"

/* Used to store the return value of check_int(). */
static int check_res;

/* Used to store the return value of TEST(). */
static int test_res;

static void t_res(int expect)
{
check_int(check_res, ==, expect);
check_int(test_res, ==, expect);
}

static void t_todo(int x)
{
check_res = TEST_TODO(check(x));
}

static void t_skip(void)
{
check(0);
test_skip("missing prerequisite");
check(1);
}

static int do_skip(void)
{
test_skip("missing prerequisite");
return 0;
}

static void t_skip_todo(void)
{
check_res = TEST_TODO(do_skip());
}

static void t_todo_after_fail(void)
{
check(0);
TEST_TODO(check(0));
}

static void t_fail_after_todo(void)
{
check(1);
TEST_TODO(check(0));
check(0);
}

static void t_messages(void)
{
check_str("\thello\\", "there\"\n");
check_str("NULL", NULL);
check_char('a', ==, '\n');
check_char('\\', ==, '\'');
}

static void t_empty(void)
{
; /* empty */
}

int cmd_main(int argc, const char **argv)
{
test_res = TEST(check_res = check_int(1, ==, 1), "passing test");
TEST(t_res(0), "passing test and assertion return 0");
test_res = TEST(check_res = check_int(1, ==, 2), "failing test");
TEST(t_res(-1), "failing test and assertion return -1");
test_res = TEST(t_todo(0), "passing TEST_TODO()");
TEST(t_res(0), "passing TEST_TODO() returns 0");
test_res = TEST(t_todo(1), "failing TEST_TODO()");
TEST(t_res(-1), "failing TEST_TODO() returns -1");
test_res = TEST(t_skip(), "test_skip()");
TEST(check_int(test_res, ==, 0), "skipped test returns 0");
test_res = TEST(t_skip_todo(), "test_skip() inside TEST_TODO()");
TEST(t_res(0), "test_skip() inside TEST_TODO() returns 0");
test_res = TEST(t_todo_after_fail(), "TEST_TODO() after failing check");
TEST(check_int(test_res, ==, -1), "TEST_TODO() after failing check returns -1");
test_res = TEST(t_fail_after_todo(), "failing check after TEST_TODO()");
TEST(check_int(test_res, ==, -1), "failing check after TEST_TODO() returns -1");
TEST(t_messages(), "messages from failing string and char comparison");
test_res = TEST(t_empty(), "test with no checks");
TEST(check_int(test_res, ==, -1), "test with no checks returns -1");

return test_done();
}
76 changes: 76 additions & 0 deletions t/unit-tests/t-strbuf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include "test-lib.h"
#include "strbuf.h"

/* wrapper that supplies tests with an initialized strbuf */
static void setup(void (*f)(struct strbuf*, void*), void *data)
{
struct strbuf buf = STRBUF_INIT;

f(&buf, data);
strbuf_release(&buf);
check_uint(buf.len, ==, 0);
check_uint(buf.alloc, ==, 0);
check(buf.buf == strbuf_slopbuf);
check_char(buf.buf[0], ==, '\0');
}

static void t_static_init(void)
{
struct strbuf buf = STRBUF_INIT;

check_uint(buf.len, ==, 0);
check_uint(buf.alloc, ==, 0);
if (check(buf.buf == strbuf_slopbuf))
return; /* avoid de-referencing buf.buf */
check_char(buf.buf[0], ==, '\0');
}

static void t_dynamic_init(void)
{
struct strbuf buf;

strbuf_init(&buf, 1024);
check_uint(buf.len, ==, 0);
check_uint(buf.alloc, >=, 1024);
if (!check(buf.buf != NULL))
check_char(buf.buf[0], ==, '\0');
strbuf_release(&buf);
}

static void t_addch(struct strbuf *buf, void *data)
{
const char *p_ch = data;
const char ch = *p_ch;

strbuf_addch(buf, ch);
if (check_uint(buf->len, ==, 1) ||
check_uint(buf->alloc, >, 1))
return; /* avoid de-referencing buf->buf */
check_char(buf->buf[0], ==, ch);
check_char(buf->buf[1], ==, '\0');
}

static void t_addstr(struct strbuf *buf, void *data)
{
const char *text = data;
size_t len = strlen(text);

strbuf_addstr(buf, text);
if (check_uint(buf->len, ==, len) ||
check_uint(buf->alloc, >, len) ||
check_char(buf->buf[len], ==, '\0'))
return;
check_str(buf->buf, text);
}

int cmd_main(int argc, const char **argv)
{
if (TEST(t_static_init(), "static initialization works"))
test_skip_all("STRBUF_INIT is broken");
TEST(t_dynamic_init(), "dynamic initialization works");
TEST(setup(t_addch, "a"), "strbuf_addch adds char");
TEST(setup(t_addch, ""), "strbuf_addch adds NUL char");
TEST(setup(t_addstr, "hello there"), "strbuf_addstr adds string");

return test_done();
}
Loading

0 comments on commit 2d29258

Please sign in to comment.