Message ID | 20240303101330.20187-5-l.s.r@web.de (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | t-ctype: simplify unit test definitions | expand |
Hi René On 03/03/2024 10:13, René Scharfe wrote: > TEST_CTYPE_FUNC defines a function for testing a character classifier, > TEST_CHAR_CLASS calls it, causing the class name to be mentioned twice. > > Avoid the need to define a class-specific function by letting > TEST_CHAR_CLASS do all the work. This is done by using the internal > functions test__run_begin() and test__run_end(), but they do exist to be > used in test macros after all. Those internal functions exist to implement the TEST() macro, they are not really intended for use outside that (which is why they are marked as private in the header file). If we ever want to update the implementation of TEST() it will be a lot harder if we're using the internal implementation directly in test files. Unit tests should be wrapping TEST() if it is appropriate but not the internal implementation directly. Ideally we wouldn't need TEST_CTYPE_FUNC as there would only be a single function that was passed a ctype predicate, an input array and an array of expected results. Unfortunately I don't think that is possible due the the way the ctype predicates are implemented. Having separate macros to define the test function and to run the test is annoying but I don't think it is really worth exposing the internal implementation just to avoid it. The other patches here look like useful improvements - thanks. Best Wishes Phillip > Alternatively we could unroll the loop to provide a very long expression > that tests all 256 characters and EOF and hand that to TEST, but that > seems awkward and hard to read. > > No change of behavior or output intended. > > Signed-off-by: René Scharfe <l.s.r@web.de> > --- > t/unit-tests/t-ctype.c | 64 ++++++++++++++++-------------------------- > 1 file changed, 24 insertions(+), 40 deletions(-) > > diff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/t-ctype.c > index 02d8569aa3..d6ac1fe678 100644 > --- a/t/unit-tests/t-ctype.c > +++ b/t/unit-tests/t-ctype.c > @@ -1,19 +1,19 @@ > #include "test-lib.h" > > -/* Macro to test a character type */ > -#define TEST_CTYPE_FUNC(func, string) \ > -static void test_ctype_##func(void) { \ > +#define TEST_CHAR_CLASS(class, string) do { \ > size_t len = ARRAY_SIZE(string) - 1 + \ > BUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \ > BUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \ > - for (int i = 0; i < 256; i++) { \ > - if (!check_int(func(i), ==, !!memchr(string, i, len))) \ > - test_msg(" i: 0x%02x", i); \ > + int skip = test__run_begin(); \ > + if (!skip) { \ > + for (int i = 0; i < 256; i++) { \ > + if (!check_int(class(i), ==, !!memchr(string, i, len)))\ > + test_msg(" i: 0x%02x", i); \ > + } \ > + check(!class(EOF)); \ > } \ > - check(!func(EOF)); \ > -} > - > -#define TEST_CHAR_CLASS(class) TEST(test_ctype_##class(), #class " works") > + test__run_end(!skip, TEST_LOCATION(), #class " works"); \ > +} while (0) > > #define DIGIT "0123456789" > #define LOWER "abcdefghijklmnopqrstuvwxyz" > @@ -33,37 +33,21 @@ static void test_ctype_##func(void) { \ > "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" \ > "\x7f" > > -TEST_CTYPE_FUNC(isdigit, DIGIT) > -TEST_CTYPE_FUNC(isspace, " \n\r\t") > -TEST_CTYPE_FUNC(isalpha, LOWER UPPER) > -TEST_CTYPE_FUNC(isalnum, LOWER UPPER DIGIT) > -TEST_CTYPE_FUNC(is_glob_special, "*?[\\") > -TEST_CTYPE_FUNC(is_regex_special, "$()*+.?[\\^{|") > -TEST_CTYPE_FUNC(is_pathspec_magic, "!\"#%&',-/:;<=>@_`~") > -TEST_CTYPE_FUNC(isascii, ASCII) > -TEST_CTYPE_FUNC(islower, LOWER) > -TEST_CTYPE_FUNC(isupper, UPPER) > -TEST_CTYPE_FUNC(iscntrl, CNTRL) > -TEST_CTYPE_FUNC(ispunct, PUNCT) > -TEST_CTYPE_FUNC(isxdigit, DIGIT "abcdefABCDEF") > -TEST_CTYPE_FUNC(isprint, LOWER UPPER DIGIT PUNCT " ") > - > int cmd_main(int argc, const char **argv) { > - /* Run all character type tests */ > - TEST_CHAR_CLASS(isspace); > - TEST_CHAR_CLASS(isdigit); > - TEST_CHAR_CLASS(isalpha); > - TEST_CHAR_CLASS(isalnum); > - TEST_CHAR_CLASS(is_glob_special); > - TEST_CHAR_CLASS(is_regex_special); > - TEST_CHAR_CLASS(is_pathspec_magic); > - TEST_CHAR_CLASS(isascii); > - TEST_CHAR_CLASS(islower); > - TEST_CHAR_CLASS(isupper); > - TEST_CHAR_CLASS(iscntrl); > - TEST_CHAR_CLASS(ispunct); > - TEST_CHAR_CLASS(isxdigit); > - TEST_CHAR_CLASS(isprint); > + TEST_CHAR_CLASS(isspace, " \n\r\t"); > + TEST_CHAR_CLASS(isdigit, DIGIT); > + TEST_CHAR_CLASS(isalpha, LOWER UPPER); > + TEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT); > + TEST_CHAR_CLASS(is_glob_special, "*?[\\"); > + TEST_CHAR_CLASS(is_regex_special, "$()*+.?[\\^{|"); > + TEST_CHAR_CLASS(is_pathspec_magic, "!\"#%&',-/:;<=>@_`~"); > + TEST_CHAR_CLASS(isascii, ASCII); > + TEST_CHAR_CLASS(islower, LOWER); > + TEST_CHAR_CLASS(isupper, UPPER); > + TEST_CHAR_CLASS(iscntrl, CNTRL); > + TEST_CHAR_CLASS(ispunct, PUNCT); > + TEST_CHAR_CLASS(isxdigit, DIGIT "abcdefABCDEF"); > + TEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT " "); > > return test_done(); > } > -- > 2.44.0 >
Hello Phillip, Am 04.03.24 um 10:51 schrieb Phillip Wood: > On 03/03/2024 10:13, René Scharfe wrote: >> TEST_CTYPE_FUNC defines a function for testing a character classifier, >> TEST_CHAR_CLASS calls it, causing the class name to be mentioned twice. >> >> Avoid the need to define a class-specific function by letting >> TEST_CHAR_CLASS do all the work. This is done by using the internal >> functions test__run_begin() and test__run_end(), but they do exist to be >> used in test macros after all. > > Those internal functions exist to implement the TEST() macro, they > are not really intended for use outside that (which is why they are > marked as private in the header file). If we ever want to update the > implementation of TEST() it will be a lot harder if we're using the > internal implementation directly in test files. Unit tests should be > wrapping TEST() if it is appropriate but not the internal > implementation directly. forcing tests to be expressions and not allow them to use statements is an unusual requirement. I don't see how the added friction would make tests any better. It just requires more boilerplate code and annoying repetition. What kind of changes do you envision that would be hindered by allowing statements? > Ideally we wouldn't need TEST_CTYPE_FUNC as there would only be a > single function that was passed a ctype predicate, an input array and > an array of expected results. Unfortunately I don't think that is > possible due the the way the ctype predicates are implemented. Having > separate macros to define the test function and to run the test is > annoying but I don't think it is really worth exposing the internal > implementation just to avoid it. The classifiers are currently implemented as macros. We could turn them into inline functions and would then be able to pass them to a test function. Improving testability is a good idea, but also somehow feels like the tail wagging the dog. It would be easy, though, I think. And less gross than: >> Alternatively we could unroll the loop to provide a very long expression >> that tests all 256 characters and EOF and hand that to TEST, but that >> seems awkward and hard to read. ... which would yield unsightly test macros and huge test binaries. But it would certainly be possible, and keep the definitions of the actual tests clean. René
On 06/03/2024 18:16, René Scharfe wrote: Sorry for the delay I somehow missed your reply > Hello Phillip, > > Am 04.03.24 um 10:51 schrieb Phillip Wood: >> On 03/03/2024 10:13, René Scharfe wrote: >>> TEST_CTYPE_FUNC defines a function for testing a character classifier, >>> TEST_CHAR_CLASS calls it, causing the class name to be mentioned twice. >>> >>> Avoid the need to define a class-specific function by letting >>> TEST_CHAR_CLASS do all the work. This is done by using the internal >>> functions test__run_begin() and test__run_end(), but they do exist to be >>> used in test macros after all. >> >> Those internal functions exist to implement the TEST() macro, they >> are not really intended for use outside that (which is why they are >> marked as private in the header file). If we ever want to update the >> implementation of TEST() it will be a lot harder if we're using the >> internal implementation directly in test files. Unit tests should be >> wrapping TEST() if it is appropriate but not the internal >> implementation directly. > > forcing tests to be expressions and not allow them to use statements is > an unusual requirement. I don't think it is that unusual to require tests to be implemented as functions which more or less amounts to the same thing. > I don't see how the added friction would make > tests any better. It just requires more boilerplate code and annoying > repetition. What kind of changes do you envision that would be > hindered by allowing statements? I'm worried about bugs being introduced by the internal functions being used incorrectly - it is not a user friendly API because it is designed around the limitations of implementing TEST(), not for general consumption. The unit test framework is very new so I don't think we can be sure that we wont need to change it and that will be more difficult if unit tests do not use TEST(). Maybe one of the changes we need is a better way of allowing statements? >> Ideally we wouldn't need TEST_CTYPE_FUNC as there would only be a >> single function that was passed a ctype predicate, an input array and >> an array of expected results. Unfortunately I don't think that is >> possible due the the way the ctype predicates are implemented. Having >> separate macros to define the test function and to run the test is >> annoying but I don't think it is really worth exposing the internal >> implementation just to avoid it. > > The classifiers are currently implemented as macros. We could turn them > into inline functions and would then be able to pass them to a test > function. Improving testability is a good idea, but also somehow feels > like the tail wagging the dog. It would be easy, though, I think. And > less gross than: Making them functions would allow them to be passed as function arguments in our code as well, though I don't know if we have much use for that. I certainly agree it would be better than the alternative below. Best Wishes Phillip >>> Alternatively we could unroll the loop to provide a very long expression >>> that tests all 256 characters and EOF and hand that to TEST, but that >>> seems awkward and hard to read. > > ... which would yield unsightly test macros and huge test binaries. But > it would certainly be possible, and keep the definitions of the actual > tests clean. > > René >
Hi René On 06/03/2024 18:16, René Scharfe wrote: > Hello Phillip, > > Am 04.03.24 um 10:51 schrieb Phillip Wood: >> On 03/03/2024 10:13, René Scharfe wrote: >>> TEST_CTYPE_FUNC defines a function for testing a character classifier, >>> TEST_CHAR_CLASS calls it, causing the class name to be mentioned twice. >>> >>> Avoid the need to define a class-specific function by letting >>> TEST_CHAR_CLASS do all the work. This is done by using the internal >>> functions test__run_begin() and test__run_end(), but they do exist to be >>> used in test macros after all. >> >> Those internal functions exist to implement the TEST() macro, they >> are not really intended for use outside that (which is why they are >> marked as private in the header file). If we ever want to update the >> implementation of TEST() it will be a lot harder if we're using the >> internal implementation directly in test files. Unit tests should be >> wrapping TEST() if it is appropriate but not the internal >> implementation directly. > > forcing tests to be expressions and not allow them to use statements is > an unusual requirement. I don't see how the added friction would make > tests any better. It just requires more boilerplate code and annoying > repetition. What kind of changes do you envision that would be > hindered by allowing statements? On reflection I don't think I'm objecting to allowing statements, only the use of the private functions to do so. If we tweak test__run_begin() and test__run_end() so that the description is passed to test__run_begin() and we invert the return value of that function to match what test__run_end() is expecting then we can have #define TEST_BEGIN(...) \ do { \ int run__ = test__run_begin(__VA_ARGS__); \ if (run__) #define TEST_END \ test_run_end(run__); \ } while (0) Which allow test authors to write TEST_BEGIN("my test") { /* test body here */ } TEST_END; The macros insulate the test code from having to worry about test_skip_all() and the "do { ... } while (0)" means that the compiler will complain if the author forgets TEST_END. I'm slightly on the fence about including the braces in the macros instead as that would make them harder to misuse but it would be less obvious that the test body is run in its own block. The compiler will allow the test author to accidentally nest two calls to TEST_BEGIN() but there is an assertion in test__run_begin() which will catch that at run time. The slight downside compared to TEST() is that it is harder to return early if a check fails - we'd need something like TEST_BEGIN("my test") { if (!check(0)) goto fail /* more checks */ fail: ; } TEST_END; Also unlike TEST(), TEST_END does not indicate to the caller whether the test failed or not but I'm not sure that matters in practice. What do you think? Best Wishes Phillip >> Ideally we wouldn't need TEST_CTYPE_FUNC as there would only be a >> single function that was passed a ctype predicate, an input array and >> an array of expected results. Unfortunately I don't think that is >> possible due the the way the ctype predicates are implemented. Having >> separate macros to define the test function and to run the test is >> annoying but I don't think it is really worth exposing the internal >> implementation just to avoid it. > > The classifiers are currently implemented as macros. We could turn them > into inline functions and would then be able to pass them to a test > function. Improving testability is a good idea, but also somehow feels > like the tail wagging the dog. It would be easy, though, I think. And > less gross than: > >>> Alternatively we could unroll the loop to provide a very long expression >>> that tests all 256 characters and EOF and hand that to TEST, but that >>> seems awkward and hard to read. > > ... which would yield unsightly test macros and huge test binaries. But > it would certainly be possible, and keep the definitions of the actual > tests clean. > > René >
Hi Phillip, Am 09.03.24 um 12:28 schrieb Phillip Wood: > On reflection I don't think I'm objecting to allowing statements, > only the use of the private functions to do so. If we tweak > test__run_begin() and test__run_end() so that the description is > passed to test__run_begin() and we invert the return value of that > function to match what test__run_end() is expecting then we can have > > #define TEST_BEGIN(...) \ > do { \ > int run__ = test__run_begin(__VA_ARGS__); \ > if (run__) > > #define TEST_END \ > test_run_end(run__); \ > } while (0) > > Which allow test authors to write > > TEST_BEGIN("my test") { > /* test body here */ > } TEST_END; > > The macros insulate the test code from having to worry about > test_skip_all() and the "do { ... } while (0)" means that the > compiler will complain if the author forgets TEST_END. the location information is missing, but I get the idea. That would certainly work for t-ctype. > I'm slightly on the fence about including the braces in the macros > instead as that would make them harder to misuse but it would be less > obvious that the test body is run in its own block. The compiler will > allow the test author to accidentally nest two calls to TEST_BEGIN() > but there is an assertion in test__run_begin() which will catch that > at run time. Thought about that as well, and I'd also be wary of including any of the control statements. Custom syntax requires learning, can have weird side-effects and may be misunderstood by editors. Below is a patch that adds the function test_start() and its companion macro TEST_START, which allow defining tests with minimal ceremony, similarly as in the shell-based test suite: #include "test-lib.h" int cmd_main(int argc, const char **argv) { if (TEST_START("something works")) check(something()); if (TEST_START("something else works")) check(something_else()); return test_done(); } It requires storing string copies and sits between the states of the original test machinery, so it's a bit complicated. The biggest downside so far, though, is that I couldn't find an example in the other unit tests that it would simplify significantly. At least it would allow getting rid of the void pointers in t-strbuf, however. > The slight downside compared to TEST() is that it is harder to return > early if a check fails - we'd need something like > > TEST_BEGIN("my test") { > if (!check(0)) > goto fail > /* more checks */ > fail: > ; > } TEST_END; TEST is worse in this regard, as it doesn't allow "if" directly at all. You can use short-circuiting, of course: TEST(check(!!ptr) && check(*ptr == value), "ptr points to value"); But you can do that with TEST_BEGIN as well. In a function you can return early, but you can use functions with both, too. In your example you can use "continue" instead of "goto fail". And with "break" you can skip the test_run_end() call. I consider both to be downsides, though -- the abstraction is leaky. > Also unlike TEST(), TEST_END does not indicate to the caller whether > the test failed or not but I'm not sure that matters in practice. Most TEST invocations out of t-basic ignore its return value so far. ctx.result is left unchanged by test__run_end(), so we could still access it if really needed. Perhaps through a sanctioned function, last_test_result() or similar. René diff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/t-ctype.c index d6ac1fe678..e7c846814e 100644 --- a/t/unit-tests/t-ctype.c +++ b/t/unit-tests/t-ctype.c @@ -4,15 +4,13 @@ size_t len = ARRAY_SIZE(string) - 1 + \ BUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \ BUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \ - int skip = test__run_begin(); \ - if (!skip) { \ + if (TEST_START(#class " works")) { \ for (int i = 0; i < 256; i++) { \ if (!check_int(class(i), ==, !!memchr(string, i, len)))\ test_msg(" i: 0x%02x", i); \ } \ check(!class(EOF)); \ } \ - test__run_end(!skip, TEST_LOCATION(), #class " works"); \ } while (0) #define DIGIT "0123456789" diff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c index 66d6980ffb..bc484c92da 100644 --- a/t/unit-tests/test-lib.c +++ b/t/unit-tests/test-lib.c @@ -16,6 +16,8 @@ static struct { unsigned running :1; unsigned skip_all :1; unsigned todo :1; + char *desc; + char *location; } ctx = { .lazy_plan = 1, .result = RESULT_NONE, @@ -123,9 +125,45 @@ void test_plan(int count) ctx.lazy_plan = 0; } +static void test_run_maybe_end(void) +{ + if (ctx.running) { + assert(ctx.location); + assert(ctx.desc); + test__run_end(0, ctx.location, "%s", ctx.desc); + FREE_AND_NULL(ctx.location); + FREE_AND_NULL(ctx.desc); + } + assert(!ctx.running); + assert(!ctx.location); + assert(!ctx.desc); +} + +int test_start(const char *location, const char *format, ...) +{ + va_list ap; + char *desc; + + test_run_maybe_end(); + + va_start(ap, format); + desc = xstrvfmt(format, ap); + va_end(ap); + + if (test__run_begin()) { + test__run_end(1, location, "%s", desc); + free(desc); + return 0; + } else { + ctx.location = xstrdup(location); + ctx.desc = desc; + return 1; + } +} + int test_done(void) { - assert(!ctx.running); + test_run_maybe_end(); if (ctx.lazy_plan) test_plan(ctx.count); diff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h index a8f07ae0b7..2be95b3ab8 100644 --- a/t/unit-tests/test-lib.h +++ b/t/unit-tests/test-lib.h @@ -21,6 +21,15 @@ */ void test_plan(int count); +/* + * Start a test. It ends when the next test starts or test_done() + * is called. Returns 1 if the test was actually started, 0 if it was + * skipped because test_skip_all() had been called. + */ +int test_start(const char *location, const char *format, ...); + +#define TEST_START(...) test_start(TEST_LOCATION(), __VA_ARGS__) + /* * test_done() must be called at the end of main(). It will print the * plan if plan() was not called at the beginning of the test program
diff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/t-ctype.c index 02d8569aa3..d6ac1fe678 100644 --- a/t/unit-tests/t-ctype.c +++ b/t/unit-tests/t-ctype.c @@ -1,19 +1,19 @@ #include "test-lib.h" -/* Macro to test a character type */ -#define TEST_CTYPE_FUNC(func, string) \ -static void test_ctype_##func(void) { \ +#define TEST_CHAR_CLASS(class, string) do { \ size_t len = ARRAY_SIZE(string) - 1 + \ BUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \ BUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \ - for (int i = 0; i < 256; i++) { \ - if (!check_int(func(i), ==, !!memchr(string, i, len))) \ - test_msg(" i: 0x%02x", i); \ + int skip = test__run_begin(); \ + if (!skip) { \ + for (int i = 0; i < 256; i++) { \ + if (!check_int(class(i), ==, !!memchr(string, i, len)))\ + test_msg(" i: 0x%02x", i); \ + } \ + check(!class(EOF)); \ } \ - check(!func(EOF)); \ -} - -#define TEST_CHAR_CLASS(class) TEST(test_ctype_##class(), #class " works") + test__run_end(!skip, TEST_LOCATION(), #class " works"); \ +} while (0) #define DIGIT "0123456789" #define LOWER "abcdefghijklmnopqrstuvwxyz" @@ -33,37 +33,21 @@ static void test_ctype_##func(void) { \ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" \ "\x7f" -TEST_CTYPE_FUNC(isdigit, DIGIT) -TEST_CTYPE_FUNC(isspace, " \n\r\t") -TEST_CTYPE_FUNC(isalpha, LOWER UPPER) -TEST_CTYPE_FUNC(isalnum, LOWER UPPER DIGIT) -TEST_CTYPE_FUNC(is_glob_special, "*?[\\") -TEST_CTYPE_FUNC(is_regex_special, "$()*+.?[\\^{|") -TEST_CTYPE_FUNC(is_pathspec_magic, "!\"#%&',-/:;<=>@_`~") -TEST_CTYPE_FUNC(isascii, ASCII) -TEST_CTYPE_FUNC(islower, LOWER) -TEST_CTYPE_FUNC(isupper, UPPER) -TEST_CTYPE_FUNC(iscntrl, CNTRL) -TEST_CTYPE_FUNC(ispunct, PUNCT) -TEST_CTYPE_FUNC(isxdigit, DIGIT "abcdefABCDEF") -TEST_CTYPE_FUNC(isprint, LOWER UPPER DIGIT PUNCT " ") - int cmd_main(int argc, const char **argv) { - /* Run all character type tests */ - TEST_CHAR_CLASS(isspace); - TEST_CHAR_CLASS(isdigit); - TEST_CHAR_CLASS(isalpha); - TEST_CHAR_CLASS(isalnum); - TEST_CHAR_CLASS(is_glob_special); - TEST_CHAR_CLASS(is_regex_special); - TEST_CHAR_CLASS(is_pathspec_magic); - TEST_CHAR_CLASS(isascii); - TEST_CHAR_CLASS(islower); - TEST_CHAR_CLASS(isupper); - TEST_CHAR_CLASS(iscntrl); - TEST_CHAR_CLASS(ispunct); - TEST_CHAR_CLASS(isxdigit); - TEST_CHAR_CLASS(isprint); + TEST_CHAR_CLASS(isspace, " \n\r\t"); + TEST_CHAR_CLASS(isdigit, DIGIT); + TEST_CHAR_CLASS(isalpha, LOWER UPPER); + TEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT); + TEST_CHAR_CLASS(is_glob_special, "*?[\\"); + TEST_CHAR_CLASS(is_regex_special, "$()*+.?[\\^{|"); + TEST_CHAR_CLASS(is_pathspec_magic, "!\"#%&',-/:;<=>@_`~"); + TEST_CHAR_CLASS(isascii, ASCII); + TEST_CHAR_CLASS(islower, LOWER); + TEST_CHAR_CLASS(isupper, UPPER); + TEST_CHAR_CLASS(iscntrl, CNTRL); + TEST_CHAR_CLASS(ispunct, PUNCT); + TEST_CHAR_CLASS(isxdigit, DIGIT "abcdefABCDEF"); + TEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT " "); return test_done(); }
TEST_CTYPE_FUNC defines a function for testing a character classifier, TEST_CHAR_CLASS calls it, causing the class name to be mentioned twice. Avoid the need to define a class-specific function by letting TEST_CHAR_CLASS do all the work. This is done by using the internal functions test__run_begin() and test__run_end(), but they do exist to be used in test macros after all. Alternatively we could unroll the loop to provide a very long expression that tests all 256 characters and EOF and hand that to TEST, but that seems awkward and hard to read. No change of behavior or output intended. Signed-off-by: René Scharfe <l.s.r@web.de> --- t/unit-tests/t-ctype.c | 64 ++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 40 deletions(-) -- 2.44.0