Skip to content

Commit e137fe3

Browse files
phillipwoodgitster
authored andcommitted
unit tests: add TAP unit test framework
This patch contains an 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); 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 will be built as part of the default "make all" target, to avoid bitrot. If you wish to build just the unit tests, you can run "make build-unit-tests". To run the tests, you can use "make unit-tests" or run the test binaries directly, as in "./t/unit-tests/bin/t-strbuf". Signed-off-by: Phillip Wood <[email protected]> Signed-off-by: Josh Steadmon <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 581790e commit e137fe3

File tree

8 files changed

+792
-4
lines changed

8 files changed

+792
-4
lines changed

Makefile

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,9 @@ TEST_BUILTINS_OBJS =
682682
TEST_OBJS =
683683
TEST_PROGRAMS_NEED_X =
684684
THIRD_PARTY_SOURCES =
685+
UNIT_TEST_PROGRAMS =
686+
UNIT_TEST_DIR = t/unit-tests
687+
UNIT_TEST_BIN = $(UNIT_TEST_DIR)/bin
685688

686689
# Having this variable in your environment would break pipelines because
687690
# you cause "cd" to echo its destination to stdout. It can also take
@@ -1331,6 +1334,12 @@ THIRD_PARTY_SOURCES += compat/regex/%
13311334
THIRD_PARTY_SOURCES += sha1collisiondetection/%
13321335
THIRD_PARTY_SOURCES += sha1dc/%
13331336

1337+
UNIT_TEST_PROGRAMS += t-basic
1338+
UNIT_TEST_PROGRAMS += t-strbuf
1339+
UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))
1340+
UNIT_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TEST_PROGRAMS))
1341+
UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/test-lib.o
1342+
13341343
# xdiff and reftable libs may in turn depend on what is in libgit.a
13351344
GITLIBS = common-main.o $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) $(LIB_FILE)
13361345
EXTLIBS =
@@ -2672,6 +2681,7 @@ OBJECTS += $(TEST_OBJS)
26722681
OBJECTS += $(XDIFF_OBJS)
26732682
OBJECTS += $(FUZZ_OBJS)
26742683
OBJECTS += $(REFTABLE_OBJS) $(REFTABLE_TEST_OBJS)
2684+
OBJECTS += $(UNIT_TEST_OBJS)
26752685

26762686
ifndef NO_CURL
26772687
OBJECTS += http.o http-walker.o remote-curl.o
@@ -3167,7 +3177,7 @@ endif
31673177

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

3170-
all:: $(TEST_PROGRAMS) $(test_bindir_programs)
3180+
all:: $(TEST_PROGRAMS) $(test_bindir_programs) $(UNIT_TEST_PROGS)
31713181

31723182
bin-wrappers/%: wrap-for-bin.sh
31733183
$(call mkdir_p_parent_template)
@@ -3592,7 +3602,7 @@ endif
35923602

35933603
artifacts-tar:: $(ALL_COMMANDS_TO_INSTALL) $(SCRIPT_LIB) $(OTHER_PROGRAMS) \
35943604
GIT-BUILD-OPTIONS $(TEST_PROGRAMS) $(test_bindir_programs) \
3595-
$(MOFILES)
3605+
$(UNIT_TEST_PROGS) $(MOFILES)
35963606
$(QUIET_SUBDIR0)templates $(QUIET_SUBDIR1) \
35973607
SHELL_PATH='$(SHELL_PATH_SQ)' PERL_PATH='$(PERL_PATH_SQ)'
35983608
test -n "$(ARTIFACTS_DIRECTORY)"
@@ -3653,7 +3663,7 @@ clean: profile-clean coverage-clean cocciclean
36533663
$(RM) $(OBJECTS)
36543664
$(RM) $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) $(REFTABLE_TEST_LIB)
36553665
$(RM) $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS) $(OTHER_PROGRAMS)
3656-
$(RM) $(TEST_PROGRAMS)
3666+
$(RM) $(TEST_PROGRAMS) $(UNIT_TEST_PROGS)
36573667
$(RM) $(FUZZ_PROGRAMS)
36583668
$(RM) $(SP_OBJ)
36593669
$(RM) $(HCC)
@@ -3831,3 +3841,15 @@ $(FUZZ_PROGRAMS): all
38313841
$(XDIFF_OBJS) $(EXTLIBS) git.o $@.o $(LIB_FUZZING_ENGINE) -o $@
38323842

38333843
fuzz-all: $(FUZZ_PROGRAMS)
3844+
3845+
$(UNIT_TEST_BIN):
3846+
@mkdir -p $(UNIT_TEST_BIN)
3847+
3848+
$(UNIT_TEST_PROGS): $(UNIT_TEST_BIN)/%$X: $(UNIT_TEST_DIR)/%.o $(UNIT_TEST_DIR)/test-lib.o $(GITLIBS) GIT-LDFLAGS $(UNIT_TEST_BIN)
3849+
$(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \
3850+
$(filter %.o,$^) $(filter %.a,$^) $(LIBS)
3851+
3852+
.PHONY: build-unit-tests unit-tests
3853+
build-unit-tests: $(UNIT_TEST_PROGS)
3854+
unit-tests: $(UNIT_TEST_PROGS)
3855+
$(MAKE) -C t/ unit-tests

t/Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ TAR ?= $(TAR)
1717
RM ?= rm -f
1818
PROVE ?= prove
1919
DEFAULT_TEST_TARGET ?= test
20+
DEFAULT_UNIT_TEST_TARGET ?= unit-tests-raw
2021
TEST_LINT ?= test-lint
2122

2223
ifdef TEST_OUTPUT_DIRECTORY
@@ -41,6 +42,7 @@ TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))
4142
TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))
4243
CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))
4344
CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl
45+
UNIT_TESTS = $(sort $(filter-out unit-tests/bin/t-basic%,$(wildcard unit-tests/bin/t-*)))
4446

4547
# `test-chainlint` (which is a dependency of `test-lint`, `test` and `prove`)
4648
# checks all tests in all scripts via a single invocation, so tell individual
@@ -65,6 +67,17 @@ prove: pre-clean check-chainlint $(TEST_LINT)
6567
$(T):
6668
@echo "*** $@ ***"; '$(TEST_SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS)
6769

70+
$(UNIT_TESTS):
71+
@echo "*** $@ ***"; $@
72+
73+
.PHONY: unit-tests unit-tests-raw unit-tests-prove
74+
unit-tests: $(DEFAULT_UNIT_TEST_TARGET)
75+
76+
unit-tests-raw: $(UNIT_TESTS)
77+
78+
unit-tests-prove:
79+
@echo "*** prove - unit tests ***"; $(PROVE) $(GIT_PROVE_OPTS) $(UNIT_TESTS)
80+
6881
pre-clean:
6982
$(RM) -r '$(TEST_RESULTS_DIRECTORY_SQ)'
7083

@@ -149,4 +162,4 @@ perf:
149162
$(MAKE) -C perf/ all
150163

151164
.PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
152-
check-chainlint clean-chainlint test-chainlint
165+
check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)

t/t0080-unit-test-output.sh

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/bin/sh
2+
3+
test_description='Test the output of the unit test framework'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'TAP output from unit tests' '
8+
cat >expect <<-EOF &&
9+
ok 1 - passing test
10+
ok 2 - passing test and assertion return 1
11+
# check "1 == 2" failed at t/unit-tests/t-basic.c:76
12+
# left: 1
13+
# right: 2
14+
not ok 3 - failing test
15+
ok 4 - failing test and assertion return 0
16+
not ok 5 - passing TEST_TODO() # TODO
17+
ok 6 - passing TEST_TODO() returns 1
18+
# todo check ${SQ}check(x)${SQ} succeeded at t/unit-tests/t-basic.c:25
19+
not ok 7 - failing TEST_TODO()
20+
ok 8 - failing TEST_TODO() returns 0
21+
# check "0" failed at t/unit-tests/t-basic.c:30
22+
# skipping test - missing prerequisite
23+
# skipping check ${SQ}1${SQ} at t/unit-tests/t-basic.c:32
24+
ok 9 - test_skip() # SKIP
25+
ok 10 - skipped test returns 1
26+
# skipping test - missing prerequisite
27+
ok 11 - test_skip() inside TEST_TODO() # SKIP
28+
ok 12 - test_skip() inside TEST_TODO() returns 1
29+
# check "0" failed at t/unit-tests/t-basic.c:48
30+
not ok 13 - TEST_TODO() after failing check
31+
ok 14 - TEST_TODO() after failing check returns 0
32+
# check "0" failed at t/unit-tests/t-basic.c:56
33+
not ok 15 - failing check after TEST_TODO()
34+
ok 16 - failing check after TEST_TODO() returns 0
35+
# check "!strcmp("\thello\\\\", "there\"\n")" failed at t/unit-tests/t-basic.c:61
36+
# left: "\011hello\\\\"
37+
# right: "there\"\012"
38+
# check "!strcmp("NULL", NULL)" failed at t/unit-tests/t-basic.c:62
39+
# left: "NULL"
40+
# right: NULL
41+
# check "${SQ}a${SQ} == ${SQ}\n${SQ}" failed at t/unit-tests/t-basic.c:63
42+
# left: ${SQ}a${SQ}
43+
# right: ${SQ}\012${SQ}
44+
# check "${SQ}\\\\${SQ} == ${SQ}\\${SQ}${SQ}" failed at t/unit-tests/t-basic.c:64
45+
# left: ${SQ}\\\\${SQ}
46+
# right: ${SQ}\\${SQ}${SQ}
47+
not ok 17 - messages from failing string and char comparison
48+
# BUG: test has no checks at t/unit-tests/t-basic.c:91
49+
not ok 18 - test with no checks
50+
ok 19 - test with no checks returns 0
51+
1..19
52+
EOF
53+
54+
! "$GIT_BUILD_DIR"/t/unit-tests/bin/t-basic >actual &&
55+
test_cmp expect actual
56+
'
57+
58+
test_done

t/unit-tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/bin

t/unit-tests/t-basic.c

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#include "test-lib.h"
2+
3+
/*
4+
* The purpose of this "unit test" is to verify a few invariants of the unit
5+
* test framework itself, as well as to provide examples of output from actually
6+
* failing tests. As such, it is intended that this test fails, and thus it
7+
* should not be run as part of `make unit-tests`. Instead, we verify it behaves
8+
* as expected in the integration test t0080-unit-test-output.sh
9+
*/
10+
11+
/* Used to store the return value of check_int(). */
12+
static int check_res;
13+
14+
/* Used to store the return value of TEST(). */
15+
static int test_res;
16+
17+
static void t_res(int expect)
18+
{
19+
check_int(check_res, ==, expect);
20+
check_int(test_res, ==, expect);
21+
}
22+
23+
static void t_todo(int x)
24+
{
25+
check_res = TEST_TODO(check(x));
26+
}
27+
28+
static void t_skip(void)
29+
{
30+
check(0);
31+
test_skip("missing prerequisite");
32+
check(1);
33+
}
34+
35+
static int do_skip(void)
36+
{
37+
test_skip("missing prerequisite");
38+
return 1;
39+
}
40+
41+
static void t_skip_todo(void)
42+
{
43+
check_res = TEST_TODO(do_skip());
44+
}
45+
46+
static void t_todo_after_fail(void)
47+
{
48+
check(0);
49+
TEST_TODO(check(0));
50+
}
51+
52+
static void t_fail_after_todo(void)
53+
{
54+
check(1);
55+
TEST_TODO(check(0));
56+
check(0);
57+
}
58+
59+
static void t_messages(void)
60+
{
61+
check_str("\thello\\", "there\"\n");
62+
check_str("NULL", NULL);
63+
check_char('a', ==, '\n');
64+
check_char('\\', ==, '\'');
65+
}
66+
67+
static void t_empty(void)
68+
{
69+
; /* empty */
70+
}
71+
72+
int cmd_main(int argc, const char **argv)
73+
{
74+
test_res = TEST(check_res = check_int(1, ==, 1), "passing test");
75+
TEST(t_res(1), "passing test and assertion return 1");
76+
test_res = TEST(check_res = check_int(1, ==, 2), "failing test");
77+
TEST(t_res(0), "failing test and assertion return 0");
78+
test_res = TEST(t_todo(0), "passing TEST_TODO()");
79+
TEST(t_res(1), "passing TEST_TODO() returns 1");
80+
test_res = TEST(t_todo(1), "failing TEST_TODO()");
81+
TEST(t_res(0), "failing TEST_TODO() returns 0");
82+
test_res = TEST(t_skip(), "test_skip()");
83+
TEST(check_int(test_res, ==, 1), "skipped test returns 1");
84+
test_res = TEST(t_skip_todo(), "test_skip() inside TEST_TODO()");
85+
TEST(t_res(1), "test_skip() inside TEST_TODO() returns 1");
86+
test_res = TEST(t_todo_after_fail(), "TEST_TODO() after failing check");
87+
TEST(check_int(test_res, ==, 0), "TEST_TODO() after failing check returns 0");
88+
test_res = TEST(t_fail_after_todo(), "failing check after TEST_TODO()");
89+
TEST(check_int(test_res, ==, 0), "failing check after TEST_TODO() returns 0");
90+
TEST(t_messages(), "messages from failing string and char comparison");
91+
test_res = TEST(t_empty(), "test with no checks");
92+
TEST(check_int(test_res, ==, 0), "test with no checks returns 0");
93+
94+
return test_done();
95+
}

t/unit-tests/t-strbuf.c

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#include "test-lib.h"
2+
#include "strbuf.h"
3+
4+
/* wrapper that supplies tests with an empty, initialized strbuf */
5+
static void setup(void (*f)(struct strbuf*, void*), void *data)
6+
{
7+
struct strbuf buf = STRBUF_INIT;
8+
9+
f(&buf, data);
10+
strbuf_release(&buf);
11+
check_uint(buf.len, ==, 0);
12+
check_uint(buf.alloc, ==, 0);
13+
}
14+
15+
/* wrapper that supplies tests with a populated, initialized strbuf */
16+
static void setup_populated(void (*f)(struct strbuf*, void*), char *init_str, void *data)
17+
{
18+
struct strbuf buf = STRBUF_INIT;
19+
20+
strbuf_addstr(&buf, init_str);
21+
check_uint(buf.len, ==, strlen(init_str));
22+
f(&buf, data);
23+
strbuf_release(&buf);
24+
check_uint(buf.len, ==, 0);
25+
check_uint(buf.alloc, ==, 0);
26+
}
27+
28+
static int assert_sane_strbuf(struct strbuf *buf)
29+
{
30+
/* Initialized strbufs should always have a non-NULL buffer */
31+
if (!check(!!buf->buf))
32+
return 0;
33+
/* Buffers should always be NUL-terminated */
34+
if (!check_char(buf->buf[buf->len], ==, '\0'))
35+
return 0;
36+
/*
37+
* Freshly-initialized strbufs may not have a dynamically allocated
38+
* buffer
39+
*/
40+
if (buf->len == 0 && buf->alloc == 0)
41+
return 1;
42+
/* alloc must be at least one byte larger than len */
43+
return check_uint(buf->len, <, buf->alloc);
44+
}
45+
46+
static void t_static_init(void)
47+
{
48+
struct strbuf buf = STRBUF_INIT;
49+
50+
check_uint(buf.len, ==, 0);
51+
check_uint(buf.alloc, ==, 0);
52+
check_char(buf.buf[0], ==, '\0');
53+
}
54+
55+
static void t_dynamic_init(void)
56+
{
57+
struct strbuf buf;
58+
59+
strbuf_init(&buf, 1024);
60+
check(assert_sane_strbuf(&buf));
61+
check_uint(buf.len, ==, 0);
62+
check_uint(buf.alloc, >=, 1024);
63+
check_char(buf.buf[0], ==, '\0');
64+
strbuf_release(&buf);
65+
}
66+
67+
static void t_addch(struct strbuf *buf, void *data)
68+
{
69+
const char *p_ch = data;
70+
const char ch = *p_ch;
71+
size_t orig_alloc = buf->alloc;
72+
size_t orig_len = buf->len;
73+
74+
if (!check(assert_sane_strbuf(buf)))
75+
return;
76+
strbuf_addch(buf, ch);
77+
if (!check(assert_sane_strbuf(buf)))
78+
return;
79+
if (!(check_uint(buf->len, ==, orig_len + 1) &&
80+
check_uint(buf->alloc, >=, orig_alloc)))
81+
return; /* avoid de-referencing buf->buf */
82+
check_char(buf->buf[buf->len - 1], ==, ch);
83+
check_char(buf->buf[buf->len], ==, '\0');
84+
}
85+
86+
static void t_addstr(struct strbuf *buf, void *data)
87+
{
88+
const char *text = data;
89+
size_t len = strlen(text);
90+
size_t orig_alloc = buf->alloc;
91+
size_t orig_len = buf->len;
92+
93+
if (!check(assert_sane_strbuf(buf)))
94+
return;
95+
strbuf_addstr(buf, text);
96+
if (!check(assert_sane_strbuf(buf)))
97+
return;
98+
if (!(check_uint(buf->len, ==, orig_len + len) &&
99+
check_uint(buf->alloc, >=, orig_alloc) &&
100+
check_uint(buf->alloc, >, orig_len + len) &&
101+
check_char(buf->buf[orig_len + len], ==, '\0')))
102+
return;
103+
check_str(buf->buf + orig_len, text);
104+
}
105+
106+
int cmd_main(int argc, const char **argv)
107+
{
108+
if (!TEST(t_static_init(), "static initialization works"))
109+
test_skip_all("STRBUF_INIT is broken");
110+
TEST(t_dynamic_init(), "dynamic initialization works");
111+
TEST(setup(t_addch, "a"), "strbuf_addch adds char");
112+
TEST(setup(t_addch, ""), "strbuf_addch adds NUL char");
113+
TEST(setup_populated(t_addch, "initial value", "a"),
114+
"strbuf_addch appends to initial value");
115+
TEST(setup(t_addstr, "hello there"), "strbuf_addstr adds string");
116+
TEST(setup_populated(t_addstr, "initial value", "hello there"),
117+
"strbuf_addstr appends string to initial value");
118+
119+
return test_done();
120+
}

0 commit comments

Comments
 (0)