Lance Pollard
Lance Pollard

Reputation: 79360

C Macro for defining test methods with callbacks

So you can dynamically define a method with its own name like this:

#define test(name) void name() { print("#name"); }

Then you can call it like:

test(foo);
foo();

I'm wondering though if you can make a "callback"-style form, like this:

#define test(name, body) void name() { print(#name); body(); }

Where it invokes a body that is defined as sort of a "block" like this:

test(dosomething, {
  int a = add(1, 1);
  assert(a == 2);
})

But more than that, I would like to pass a callback for async functions to say they are complete, like this:

test(dosomething, { (done)
  int a = add(1, 1);
  assert(a == 2);
  done();
})

In addition, I am defining these outside of the main, so it would be defined in the same scope as a normal function. Because of that, the tests aren't going to automatically run. They need to be iterated over. As such, they probably need to be pushed into an array of some sort. So wondering how that could be done, if macros allow you to sort of capture stuff into an array, or to build up an enum one #define at a time.

#define test(name, body) void name() { \
  print(#name); \
} \
\
TESTS[CURRENT_TEST++] = &name \ // push the test into a TESTS array.

So then in main you can iterate over them:

int
main() {
  iterate over TESTS...
}

To summarize, I am wondering how to #define this at the file body level (i.e. not in main, but at the level of functions):

void afunction() { printf("Foo"); }

test(dosomething, { (done)
  int a = add(1, 1);
  assert(a == 2);
  done();
})

void anotherfunction() { printf("Bar"); }

such that I can iterate over the tests in main.

Upvotes: 2

Views: 534

Answers (1)

H Walters
H Walters

Reputation: 2674

Looks like you're building some sort of mini test framework using the c preprocessor.

There's a caveat for the bodies; to the C preprocessor, curly brackets and square brackets are just tokens. Parenthesized expressions are recognized (i.e., parentheses are matched), and commas are recognized as delimiters. So for example, this macro invocation:

test(dosomething, { int a = add(1, 1); assert(a == 2); })

...has two arguments despite having two commas (because the second comma is "hugged" in a parenthesized set), but that's a bit misleading. This invocation:

test(dosomething, { enum { red, green, blue }; assert(red+1==green); })

...has four arguments: 1: dosomething, 2: { enum { red, 3: green, and 4: blue }; assert(red+1==green); }. If you're going to do this, you probably want to cover cases like this... there are basic strategies: (a) hug the body in parentheses (you can unwrap it in expansion), or (b) use variadic macros.

They need to be iterated over.

Sounds like a job for x-macros (below I'll be using the parameterized-macro flavor of x-macros).

But more than that, I would like to pass a callback for async functions to say they are complete, like this:

...you can't add an argument in the middle, but the braces don't have to be part of this (they don't help anyway, since the preprocessor ignores them). So for the above, we probably want to pick the hug option. That leaves your invocations looking like this:

test(dosomething, (int a=add(1,1); assert(a==2);), done)

However, since we're ripping the curly braces out, we can put them in arbitrary places in our expansion and do arbitrary things in between. Since I'm guessing you want the same kind of async thing going on, we could just put that thing in the expansion that generates the definition rather as an argument.

Here's roughly what it would look like, using a parameterized macro version of x-macros, and applying an async on expansion (using semaphores to demonstrate how arbitrary this could be):

#define APPLY_TEST_MACROS(macro) \
    macro(test_add, (int a=add(1,1); assert(a==2);  )) \
    macro(test_sub, (int a=sub(5,2); assert(a==3);  )) \
    macro(test_mul, (int a=mul(3,4); assert(a==12); ))

#define UNWRAP(...) __VA_ARGS__
#define MAKE_ASYNC_SEM(NAME_, BODY_) \
   void NAME_() { \
      sem_wait(&test_sem_ctl); print(#NAME_); sem_post(&test_sem_ctl); \
      UNWRAP BODY_ \
      sem_wait(&test_sem_ctl); \
      if (0==--tests_remaining) sem_post(&test_sem_done); \
      sem_post(&test_sem_ctl); \
   }
#define COUNT_TESTS(NAME_, BODY_) +1
sem_t test_sem_ctl;
sem_t test_sem_done;
void init_semaphores() {
   sem_init(&test_sem_ctl, 0, 1);
   sem_init(&test_sem_done, 0, 0);
}
// iterate over tests to count them
unsigned int tests_remaining = APPLY_TEST_MACROS(COUNT_TESTS);
// define the tests
APPLY_TEST_MACROS(MAKE_ASYNC_SEM)

...and so forth (I'm stopping here because the idea is to convey the idea, not code it for you). The x-macro layout allows you to iterate in the preprocessor, so you can do something like spawn a thread per test; you could also just use this same approach to build an array of test functions if, say, you want to feed your tests to a thread pool.

Upvotes: 2

Related Questions