Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic C++ support #43

Merged
merged 3 commits into from
Sep 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.o
tags
test
test++
23 changes: 15 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
UNAME=$(shell uname)

CCFLAGS=-Wall -Wextra -Wconversion -Wredundant-decls -Wshadow -Wno-unused-parameter -O3
CC=clang
CXX=clang++

all: test
all: test test++

remake: clean all

%.o: %.c ctest.h
%.cpp: %.c
ln -fs $< $@

%.c.o: %.c ctest.h
$(CC) $(CCFLAGS) -c -o $@ $<

test: main.o ctest.h mytests.o
$(CC) $(LDFLAGS) main.o mytests.o -o test
%.cpp.o: %.cpp ctest.h
$(CXX) $(CCFLAGS) -c -o $@ $<

clean:
rm -f test *.o
test: main.c.o ctest.h mytests.c.o
$(CC) $(LDFLAGS) main.c.o mytests.c.o -o test

test++: main.cpp.o ctest.h mytests.cpp.o
$(CXX) $(LDFLAGS) main.cpp.o mytests.cpp.o -o test++

clean:
rm -f test test++ *.o
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CTEST

ctest is a unit test framework for software written in C.
ctest is a unit test framework for software written in C/C++.

Features:
* adding tests with minimal hassle (no manual adding to suites or testlists!)
Expand Down
75 changes: 59 additions & 16 deletions ctest.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
#ifndef CTEST_H
#define CTEST_H

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __GNUC__
#define CTEST_IMPL_FORMAT_PRINTF(a, b) __attribute__ ((format(printf, a, b)))
#else
Expand All @@ -25,9 +29,16 @@
#include <inttypes.h> /* intmax_t, uintmax_t, PRI* */
#include <stddef.h> /* size_t */

typedef void (*ctest_nullary_run_func)(void);
typedef void (*ctest_unary_run_func)(void*);
typedef void (*ctest_setup_func)(void*);
typedef void (*ctest_teardown_func)(void*);

union ctest_run_func_union {
ctest_nullary_run_func nullary;
ctest_unary_run_func unary;
};

#define CTEST_IMPL_PRAGMA(x) _Pragma (#x)

#if defined(__GNUC__)
Expand All @@ -50,12 +61,10 @@ typedef void (*ctest_teardown_func)(void*);
#define CTEST_IMPL_DIAG_POP()
#endif

CTEST_IMPL_DIAG_PUSH_IGNORED(strict-prototypes)

struct ctest {
const char* ssname; // suite name
const char* ttname; // test name
void (*run)();
union ctest_run_func_union run;

void* data;
ctest_setup_func* setup;
Expand All @@ -66,17 +75,17 @@ struct ctest {
unsigned int magic;
};

CTEST_IMPL_DIAG_POP()

#define CTEST_IMPL_NAME(name) ctest_##name
#define CTEST_IMPL_FNAME(sname, tname) CTEST_IMPL_NAME(sname##_##tname##_run)
#define CTEST_IMPL_TNAME(sname, tname) CTEST_IMPL_NAME(sname##_##tname)
#define CTEST_IMPL_DATA_SNAME(sname) CTEST_IMPL_NAME(sname##_data)
#define CTEST_IMPL_DATA_TNAME(sname, tname) CTEST_IMPL_NAME(sname##_##tname##_data)
#define CTEST_IMPL_SETUP_FNAME(sname) CTEST_IMPL_NAME(sname##_setup)
#define CTEST_IMPL_SETUP_FPNAME(sname) CTEST_IMPL_NAME(sname##_setup_ptr)
#define CTEST_IMPL_SETUP_TPNAME(sname, tname) CTEST_IMPL_NAME(sname##_##tname##_setup_ptr)
#define CTEST_IMPL_TEARDOWN_FNAME(sname) CTEST_IMPL_NAME(sname##_teardown)
#define CTEST_IMPL_TEARDOWN_FPNAME(sname) CTEST_IMPL_NAME(sname##_teardown_ptr)
#define CTEST_IMPL_TEARDOWN_TPNAME(sname, tname) CTEST_IMPL_NAME(sname##_##tname##_teardown_ptr)

#define CTEST_IMPL_MAGIC (0xdeadbeef)
#ifdef __APPLE__
Expand All @@ -87,14 +96,43 @@ CTEST_IMPL_DIAG_POP()

#define CTEST_IMPL_STRUCT(sname, tname, tskip, tdata, tsetup, tteardown) \
static struct ctest CTEST_IMPL_TNAME(sname, tname) CTEST_IMPL_SECTION = { \
.ssname=#sname, \
.ttname=#tname, \
.run = CTEST_IMPL_FNAME(sname, tname), \
.data = tdata, \
.setup = (ctest_setup_func*) tsetup, \
.teardown = (ctest_teardown_func*) tteardown, \
.skip = tskip, \
.magic = CTEST_IMPL_MAGIC }
#sname, \
#tname, \
{ (ctest_nullary_run_func) CTEST_IMPL_FNAME(sname, tname) }, \
tdata, \
(ctest_setup_func*) tsetup, \
(ctest_teardown_func*) tteardown, \
tskip, \
CTEST_IMPL_MAGIC, \
}

#ifdef __cplusplus

#define CTEST_SETUP(sname) \
Copy link
Contributor Author

@aceckel aceckel Aug 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handling of the optional setup/teardown functions is the only place where the actual logic must differ between the C and C++ compilation. The problem is that C++ doesn't support tentative definitions the way that C does, which we currently rely on. Having 2 separate approaches to solve the same problem isn't ideal, so it would be nice if we could come up with an approach that both the C and C++ could share.

One potential option is to use a hashtable to store the setup and teardown function pointers, keyed by suite name. Then we could populate this hashtable using __attribute__ ((constructor)) functions that we would install as part of the CTEST_SETUP()/CTEST_TEARDOWN macros. Here is an example implementation (leaving out the hashtable implementation for brevity):

typedef void (*ctest_fixture_func)(void*);

struct ctest_suite {
    const char* name;
    ctest_fixture_func setup;
    ctest_fixture_func teardown;
    struct ctest_slist_head list; // linux kernel style intrusive list head
};

#define CTEST_IMPL_FIXTURE(sname, member, FNAME, CNAME) \
    __attribute__ ((constructor)) static void CNAME(sname)(void) \
    { \
        static struct ctest_suite suite = { \
            #sname, \
            NULL, \
            NULL, \
            CTEST_IMPL_SLIST_HEAD_INIT(suite.list), \
        }; \
        ctest_ensure_suite_hashtable_is_initialized(); \
        struct ctest_suite* const emplaced = ctest_emplace_suite(&suite); \
        emplaced->member = (ctest_fixture_func) &FNAME(sname); \
    }

#define CTEST_SETUP(sname) \
    static void CTEST_IMPL_SETUP_FNAME(sname)(struct CTEST_IMPL_DATA_SNAME(sname)* data); \
    CTEST_IMPL_FIXTURE(sname, setup, CTEST_IMPL_SETUP_FNAME, CTEST_IMPL_SETUP_CNAME) \
    static void CTEST_IMPL_SETUP_FNAME(sname)(struct CTEST_IMPL_DATA_SNAME(sname)* data)

#define CTEST_TEARDOWN(sname) \
    static void CTEST_IMPL_TEARDOWN_FNAME(sname)(struct CTEST_IMPL_DATA_SNAME(sname)* data); \
    CTEST_IMPL_FIXTURE(sname, teardown, CTEST_IMPL_TEARDOWN_FNAME, CTEST_IMPL_TEARDOWN_CNAME) \
    static void CTEST_IMPL_TEARDOWN_FNAME(sname)(struct CTEST_IMPL_DATA_SNAME(sname)* data)

And here is what the main loop might look like, with relevant changes emphasized with >>s.

        if (test == &CTEST_IMPL_TNAME(suite, test)) continue;
        if (filter(test)) {
            ctest_errorbuffer[0] = 0;
            ctest_errorsize = MSG_SIZE-1;
            ctest_errormsg = ctest_errorbuffer;
            printf("TEST %d/%d %s:%s ", idx, total, test->ssname, test->ttname);
            fflush(stdout);
            if (test->skip) {
                color_print(ANSI_BYELLOW, "[SKIPPED]");
                num_skip++;
            } else {
                int result = setjmp(ctest_err);
                if (result == 0) {
>>>>>>>>>>>>>>>>>>  suite = ctest_find_suite(test->ssname);
>>>>>>>>>>>>>>>>>>  if (suite && suite->setup && test->data) suite->setup(test->data);
                    if (test->data)
                        test->run.unary(test->data);
                    else
                        test->run.nullary();
>>>>>>>>>>>>>>>>>>  if (suite && suite->teardown && test->data) suite->teardown(test->data);
                    // if we got here it's ok
#ifdef CTEST_COLOR_OK
                    color_print(ANSI_BGREEN, "[OK]");
#else
                    printf("[OK]\n");
#endif
                    num_ok++;
                } else {
                    color_print(ANSI_BRED, "[FAIL]");
                    num_fail++;
                }
                if (ctest_errorsize != MSG_SIZE-1) printf("%s", ctest_errorbuffer);
            }
            idx++;
        }

This would add some runtime overhead, but the hashtable operations are O(1) so it shouldn't be too bad. It also has the benefit of being less clever and easier to reason about than the C tentative definition approach as well as the C++ template specialization approach.

template <> void CTEST_IMPL_SETUP_FNAME(sname)(struct CTEST_IMPL_DATA_SNAME(sname)* data)

#define CTEST_TEARDOWN(sname) \
template <> void CTEST_IMPL_TEARDOWN_FNAME(sname)(struct CTEST_IMPL_DATA_SNAME(sname)* data)

#define CTEST_DATA(sname) \
template <typename T> void CTEST_IMPL_SETUP_FNAME(sname)(T* data) { } \
template <typename T> void CTEST_IMPL_TEARDOWN_FNAME(sname)(T* data) { } \
struct CTEST_IMPL_DATA_SNAME(sname)

#define CTEST_IMPL_CTEST(sname, tname, tskip) \
static void CTEST_IMPL_FNAME(sname, tname)(void); \
CTEST_IMPL_STRUCT(sname, tname, tskip, NULL, NULL, NULL); \
static void CTEST_IMPL_FNAME(sname, tname)(void)

#define CTEST_IMPL_CTEST2(sname, tname, tskip) \
static struct CTEST_IMPL_DATA_SNAME(sname) CTEST_IMPL_DATA_TNAME(sname, tname); \
static void CTEST_IMPL_FNAME(sname, tname)(struct CTEST_IMPL_DATA_SNAME(sname)* data); \
static void (*CTEST_IMPL_SETUP_TPNAME(sname, tname))(struct CTEST_IMPL_DATA_SNAME(sname)*) = &CTEST_IMPL_SETUP_FNAME(sname)<struct CTEST_IMPL_DATA_SNAME(sname)>; \
static void (*CTEST_IMPL_TEARDOWN_TPNAME(sname, tname))(struct CTEST_IMPL_DATA_SNAME(sname)*) = &CTEST_IMPL_TEARDOWN_FNAME(sname)<struct CTEST_IMPL_DATA_SNAME(sname)>; \
CTEST_IMPL_STRUCT(sname, tname, tskip, &CTEST_IMPL_DATA_TNAME(sname, tname), &CTEST_IMPL_SETUP_TPNAME(sname, tname), &CTEST_IMPL_TEARDOWN_TPNAME(sname, tname)); \
static void CTEST_IMPL_FNAME(sname, tname)(struct CTEST_IMPL_DATA_SNAME(sname)* data)

#else

#define CTEST_SETUP(sname) \
static void CTEST_IMPL_SETUP_FNAME(sname)(struct CTEST_IMPL_DATA_SNAME(sname)* data); \
Expand Down Expand Up @@ -123,6 +161,7 @@ CTEST_IMPL_DIAG_POP()
CTEST_IMPL_STRUCT(sname, tname, tskip, &CTEST_IMPL_DATA_TNAME(sname, tname), &CTEST_IMPL_SETUP_FPNAME(sname), &CTEST_IMPL_TEARDOWN_FPNAME(sname)); \
static void CTEST_IMPL_FNAME(sname, tname)(struct CTEST_IMPL_DATA_SNAME(sname)* data)

#endif

void CTEST_LOG(const char* fmt, ...) CTEST_IMPL_FORMAT_PRINTF(1, 2);
void CTEST_ERR(const char* fmt, ...) CTEST_IMPL_FORMAT_PRINTF(1, 2); // doesn't return
Expand Down Expand Up @@ -427,7 +466,7 @@ static uint64_t getCurrentTime(void) {

static void color_print(const char* color, const char* text) {
if (color_output)
printf("%s%s"ANSI_NORMAL"\n", color, text);
printf("%s%s" ANSI_NORMAL "\n", color, text);
else
printf("%s\n", text);
}
Expand Down Expand Up @@ -512,9 +551,9 @@ __attribute__((no_sanitize_address)) int ctest_main(int argc, const char *argv[]
if (result == 0) {
if (test->setup && *test->setup) (*test->setup)(test->data);
if (test->data)
test->run(test->data);
test->run.unary(test->data);
else
test->run();
test->run.nullary();
if (test->teardown && *test->teardown) (*test->teardown)(test->data);
// if we got here it's ok
#ifdef CTEST_COLOR_OK
Expand Down Expand Up @@ -543,5 +582,9 @@ __attribute__((no_sanitize_address)) int ctest_main(int argc, const char *argv[]

#endif

#ifdef __cplusplus
}
#endif

#endif