diff mbox series

[v3,3/7] unit-tests: add for_test

Message ID c51025cc-26e5-41e2-be56-94e141a64f5d@web.de (mailing list archive)
State New, archived
Headers show
Series add and use for_test to simplify tests | expand

Commit Message

René Scharfe July 24, 2024, 2:51 p.m. UTC
The macro TEST only allows defining a test that consists of a single
expression.  Add a new macro, for_test, which provides a way to define
unit tests that are made up of one or more statements.

for_test allows defining self-contained tests en bloc, a bit like
test_expect_success does for regular tests.  It acts like a for loop
that runs at most once; the test body is executed if test_skip_all()
had not been called before.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 .clang-format               |  2 ++
 t/helper/test-example-tap.c | 33 +++++++++++++++++++++++++++++++++
 t/t0080-unit-test-output.sh | 35 ++++++++++++++++++++++++++++++++++-
 t/unit-tests/test-lib.h     | 20 ++++++++++++++++++++
 4 files changed, 89 insertions(+), 1 deletion(-)

--
2.45.2

Comments

Kyle Lippincott July 24, 2024, 7:24 p.m. UTC | #1
On Wed, Jul 24, 2024 at 7:53 AM René Scharfe <l.s.r@web.de> wrote:
>
> The macro TEST only allows defining a test that consists of a single
> expression.  Add a new macro, for_test, which provides a way to define
> unit tests that are made up of one or more statements.

Perhaps obvious to you and others, but I'm at a loss; can you
elaborate on why this is a significant problem? Why is having the
tests in a separate function a significant downside, and what are the
benefits of this new macro that counteract the "magic" here? I'm not
seeing it.

Macros like this require additional cognitive overhead to understand;
I can't read `for_test ("for_test passing test") check_int(1, ==, 1);`
and understand what's going on without going and reading the comment
above `for_test`. Unlike `TEST(<func>, "<description>")`, I can't even
_guess_ what it's doing. This macro is project-specific, and this
knowledge has to be attained by everyone wishing to even read this
code.

I personally consider the patches that use this to be regressions in
my ability to read and understand the tests. As an example, one of the
diff hunks in the strbuf patch (patch #6 in the series) does this:
-       TEST(setup_populated(t_addch, "initial value", "a"),
-            "strbuf_addch appends to initial value");
+       for_test ("strbuf_addch appends to initial value") {
+               struct strbuf sb = STRBUF_INIT;
+               t_addstr(&sb, "initial value");
+               t_addch(&sb, 'a');
+               t_release(&sb);
+       }

But the main simplification in that patch (not using
`setup_populated`) can be achieved without `for_test`:

+ static void t_addch_appends(void)
+ {
+     struct strbuf sb = STRBUF_INIT;
+     t_addstr(&sb, "initial value");
+     t_addch(&sb, 'a');
+     t_release(&sb);
+ }

-       TEST(setup_populated(t_addch, "initial value", "a"),
-            "strbuf_addch appends to initial value");
+       TEST(t_addch_appends(), "strbuf_addch appends to initial value");

It seems to me that all `for_test` is getting us there is an inlining
of the test logic, which seems like it's optimizing for vertical space
in the file. Maybe it's because I'm coming from a C++ environment at
$JOB that's using Google's gunit and gmock frameworks, where every
test is in its own function and we usually don't even write the main
function ourselves, but I have a preference for the separate
functions.

Maybe I'm in the minority, so only consider this at somewhere like a
-0.5 on this series (fine with deferring to a reviewer who is
significantly in favor of it, but if there aren't any, I'd lean
towards not landing this).
Phillip Wood July 25, 2024, 9:45 a.m. UTC | #2
On 24/07/2024 20:24, Kyle Lippincott wrote:
> On Wed, Jul 24, 2024 at 7:53 AM René Scharfe <l.s.r@web.de> wrote:
>>
> Macros like this require additional cognitive overhead to understand;
> I can't read `for_test ("for_test passing test") check_int(1, ==, 1);`
> and understand what's going on without going and reading the comment
> above `for_test`. Unlike `TEST(<func>, "<description>")`, I can't even
> _guess_ what it's doing. This macro is project-specific, and this
> knowledge has to be attained by everyone wishing to even read this
> code.

As well as the obscure name and oddness of using "continue" to exit a 
test early I think having two ways of defining tests also adds to the 
cognitive overhead of writing tests as one now has to decide which to use.

As you say below it is not clear to me that the changes in the last 
three patches that start using for_test() in our existing tests are an 
improvement.

This will be the last comment from me for a while as I'm about to go off 
the list for a couple of weeks

Best Wishes

Phillip

> I personally consider the patches that use this to be regressions in
> my ability to read and understand the tests. As an example, one of the
> diff hunks in the strbuf patch (patch #6 in the series) does this:
> -       TEST(setup_populated(t_addch, "initial value", "a"),
> -            "strbuf_addch appends to initial value");
> +       for_test ("strbuf_addch appends to initial value") {
> +               struct strbuf sb = STRBUF_INIT;
> +               t_addstr(&sb, "initial value");
> +               t_addch(&sb, 'a');
> +               t_release(&sb);
> +       }
> 
> But the main simplification in that patch (not using
> `setup_populated`) can be achieved without `for_test`:
> 
> + static void t_addch_appends(void)
> + {
> +     struct strbuf sb = STRBUF_INIT;
> +     t_addstr(&sb, "initial value");
> +     t_addch(&sb, 'a');
> +     t_release(&sb);
> + }
> 
> -       TEST(setup_populated(t_addch, "initial value", "a"),
> -            "strbuf_addch appends to initial value");
> +       TEST(t_addch_appends(), "strbuf_addch appends to initial value");
> 
> It seems to me that all `for_test` is getting us there is an inlining
> of the test logic, which seems like it's optimizing for vertical space
> in the file. Maybe it's because I'm coming from a C++ environment at
> $JOB that's using Google's gunit and gmock frameworks, where every
> test is in its own function and we usually don't even write the main
> function ourselves, but I have a preference for the separate
> functions.
> 
> Maybe I'm in the minority, so only consider this at somewhere like a
> -0.5 on this series (fine with deferring to a reviewer who is
> significantly in favor of it, but if there aren't any, I'd lean
> towards not landing this).
>
Junio C Hamano July 25, 2024, 4:02 p.m. UTC | #3
Kyle Lippincott <spectral@google.com> writes:

> But the main simplification in that patch (not using
> `setup_populated`) can be achieved without `for_test`:
>
> + static void t_addch_appends(void)
> + {
> +     struct strbuf sb = STRBUF_INIT;
> +     t_addstr(&sb, "initial value");
> +     t_addch(&sb, 'a');
> +     t_release(&sb);
> + }
>
> -       TEST(setup_populated(t_addch, "initial value", "a"),
> -            "strbuf_addch appends to initial value");
> +       TEST(t_addch_appends(), "strbuf_addch appends to initial value");

Yup.  I like that better when we are using TEST(), whether the other
thing exists or not.

> It seems to me that all `for_test` is getting us there is an inlining
> of the test logic, which seems like it's optimizing for vertical space
> in the file.

I would consider that it optimizes for human readers who have to
scan the file.  Having to jump around from caller to callee to
understand the whole thing (rather, the description being away from
the callee, which is where the most of the logic is anyway) is one
gripe I have with the approach taken by the TEST() thing.

> Maybe it's because I'm coming from a C++ environment at
> $JOB that's using Google's gunit and gmock frameworks, where every
> test is in its own function and we usually don't even write the main
> function ourselves, but I have a preference for the separate
> functions.

If we do not have to write the main at all, then it would make the
separate function that implements a single test a lot more palatable,
and we do not even have to implement and call TEST() macro ;-).

Thanks.
Kyle Lippincott July 25, 2024, 9:31 p.m. UTC | #4
On Thu, Jul 25, 2024 at 9:02 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> Kyle Lippincott <spectral@google.com> writes:
>
> > Maybe it's because I'm coming from a C++ environment at
> > $JOB that's using Google's gunit and gmock frameworks, where every
> > test is in its own function and we usually don't even write the main
> > function ourselves, but I have a preference for the separate
> > functions.
>
> If we do not have to write the main at all, then it would make the
> separate function that implements a single test a lot more palatable,
> and we do not even have to implement and call TEST() macro ;-).

I feel like you're trying to bait me into coming up with a way of
avoiding the main function ;) But I don't think it can really be done
in portable/vanilla C, unfortunately. I tried to think of a way to do
this, and they all involved some other system coming along and
identifying the tests and code-generating a main function, which also
seems like too much magic to me.
Junio C Hamano July 26, 2024, 2:41 a.m. UTC | #5
Kyle Lippincott <spectral@google.com> writes:

>> > Maybe it's because I'm coming from a C++ environment at
>> > $JOB that's using Google's gunit and gmock frameworks, where every
>> > test is in its own function and we usually don't even write the main
>> > function ourselves, but I have a preference for the separate
>> > functions.
>>
>> If we do not have to write the main at all, then it would make the
>> separate function that implements a single test a lot more palatable,
>> and we do not even have to implement and call TEST() macro ;-).
> ...
> I tried to think of a way to do
> this, and they all involved some other system coming along and
> identifying the tests and code-generating a main function, which also
> seems like too much magic to me.

I thought that automatically generating the boilerplates from the
visible list of test functions and piecing them together with a
synthetic main was what was brought up as how libgit2 project does
this?  It does not sound all that involved and I do not find it a
rocket science.

In any case, a well written framework soon becomes "magic" anyway.
Our scripted test suite, back when Pasky and I invented it, was
unknown to many people, would have looked very far away from any
day-to-day shell scripts, and are full of magic.  Modern versions
gained even more magic than the what we had in the olden days.  But
I hear these days other projects imitate the pattern, so it may no
longer so much "magic" to many audiences.

If we use a well-written framework for C unit tests, I suspect that
the same thing would happen eventually.  An important point is the
"well-written" part, which needs to hide the ugly implementation
details well without revealing it in corners.  As Phillip said, a
loop construct similar to for() that you are not allowed to break or
continue may not qualify exactly for that reason.
Patrick Steinhardt July 26, 2024, 12:56 p.m. UTC | #6
On Thu, Jul 25, 2024 at 07:41:19PM -0700, Junio C Hamano wrote:
> Kyle Lippincott <spectral@google.com> writes:
> 
> >> > Maybe it's because I'm coming from a C++ environment at
> >> > $JOB that's using Google's gunit and gmock frameworks, where every
> >> > test is in its own function and we usually don't even write the main
> >> > function ourselves, but I have a preference for the separate
> >> > functions.
> >>
> >> If we do not have to write the main at all, then it would make the
> >> separate function that implements a single test a lot more palatable,
> >> and we do not even have to implement and call TEST() macro ;-).
> > ...
> > I tried to think of a way to do
> > this, and they all involved some other system coming along and
> > identifying the tests and code-generating a main function, which also
> > seems like too much magic to me.
> 
> I thought that automatically generating the boilerplates from the
> visible list of test functions and piecing them together with a
> synthetic main was what was brought up as how libgit2 project does
> this?  It does not sound all that involved and I do not find it a
> rocket science.

As I said when mentioning how libgit2 does it, I'd be happy to present a
working prototype for this if others think it was potentially useful.
Until now I didn't hear any positive feedback thoguh, you're the first
one saying that this might be something that is worth doing.

I just want to avoid wasting time on something that has no chance of
landing in the first place.

Patrick
Junio C Hamano July 26, 2024, 3:59 p.m. UTC | #7
Patrick Steinhardt <ps@pks.im> writes:

> As I said when mentioning how libgit2 does it, I'd be happy to present a
> working prototype for this if others think it was potentially useful.
> Until now I didn't hear any positive feedback thoguh, you're the first
> one saying that this might be something that is worth doing.
>
> I just want to avoid wasting time on something that has no chance of
> landing in the first place.

It takes such a sizeable effort to make it pleasant to use the
TEST() thing, it becomes somewhat questionable that it is worth the
effort.  What I was hoping to see was hinted in the part you did not
quote ;-)

If there is an existing well-written framework for C unit tests we
can apply to our project, it may not have to be so much "magic".  If
you think theirs is not so good and worth copying, then let's not.

Thanks.
Patrick Steinhardt July 29, 2024, 9:48 a.m. UTC | #8
On Fri, Jul 26, 2024 at 08:59:00AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> If there is an existing well-written framework for C unit tests we
> can apply to our project, it may not have to be so much "magic".  If
> you think theirs is not so good and worth copying, then let's not.

With "theirs" being the libgit2 one? I always enjoyed using it, and it
is standalone nowadays and called "clar" [1]. The biggest downside of it
is that it depends on Python to auto-generate the test "main" function,
which is not really a good fit for the Git project. So ultimately, we'd
have to adapt the heart of clar anyway. It's not all that involved, but
something to keep in mind if we wanted to use it.

Patrick

[1]: https://github.com/clar-test/clar
Junio C Hamano July 29, 2024, 6:55 p.m. UTC | #9
Patrick Steinhardt <ps@pks.im> writes:

> is standalone nowadays and called "clar" [1]. The biggest downside of it
> is that it depends on Python to auto-generate the test "main" function,
> which is not really a good fit for the Git project.

Is that because Python is optional (like, we only use it for
optional Perforce thing and import-zip in contrib), or are there
other concerns?

Unlike these components, unit tests are not even for the end-user
consumers, so if it is Python that you find it a blocker, I do not
see a reason to reject it.  The thing looked like a simple script
that does not use any tricky language construct or modules.
Patrick Steinhardt July 30, 2024, 4:49 a.m. UTC | #10
On Mon, Jul 29, 2024 at 11:55:52AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > is standalone nowadays and called "clar" [1]. The biggest downside of it
> > is that it depends on Python to auto-generate the test "main" function,
> > which is not really a good fit for the Git project.
> 
> Is that because Python is optional (like, we only use it for
> optional Perforce thing and import-zip in contrib), or are there
> other concerns?
> 
> Unlike these components, unit tests are not even for the end-user
> consumers, so if it is Python that you find it a blocker, I do not
> see a reason to reject it.  The thing looked like a simple script
> that does not use any tricky language construct or modules.

No concerns other than adding another mandatory dependency for devs. We
already depend on Perl and Shell, so adding Python to the stack feels
suboptimal to me.

As you say though, the script isn't all that involved. So it would also
be possible to port it to Perl if we want to do that, I guess.

Patrick
René Scharfe July 30, 2024, 2 p.m. UTC | #11
Am 30.07.24 um 06:49 schrieb Patrick Steinhardt:
> On Mon, Jul 29, 2024 at 11:55:52AM -0700, Junio C Hamano wrote:
>> Patrick Steinhardt <ps@pks.im> writes:
>>
>>> is standalone nowadays and called "clar" [1]. The biggest downside of it
>>> is that it depends on Python to auto-generate the test "main" function,
>>> which is not really a good fit for the Git project.
>>
>> Is that because Python is optional (like, we only use it for
>> optional Perforce thing and import-zip in contrib), or are there
>> other concerns?
>>
>> Unlike these components, unit tests are not even for the end-user
>> consumers, so if it is Python that you find it a blocker, I do not
>> see a reason to reject it.  The thing looked like a simple script
>> that does not use any tricky language construct or modules.
>
> No concerns other than adding another mandatory dependency for devs. We
> already depend on Perl and Shell, so adding Python to the stack feels
> suboptimal to me.
>
> As you say though, the script isn't all that involved. So it would also
> be possible to port it to Perl if we want to do that, I guess.

From the point of view of a "minimal C unit testing framework" surely
the implementation language with the best dependency story would be C.
Perhaps it could then also test itself.  On the other hand, writing a
string handling program in 2024 in C is probably not the smartest idea.

Then again, generate.py uses a non-greedy regex to remove single-line
comments, which seems wrong, and doesn't seem to support preprocessor
directives like #if and #ifdef, so whole tests can't be disabled this
way.  And it uses pickle to cache results; does that mean it would be
slow without it?  Anyway, point being that parsing C isn't so easy
using Python either.

René
Patrick Steinhardt July 31, 2024, 5:19 a.m. UTC | #12
On Tue, Jul 30, 2024 at 04:00:07PM +0200, René Scharfe wrote:
> Am 30.07.24 um 06:49 schrieb Patrick Steinhardt:
> > On Mon, Jul 29, 2024 at 11:55:52AM -0700, Junio C Hamano wrote:
> >> Patrick Steinhardt <ps@pks.im> writes:
> >>
> >>> is standalone nowadays and called "clar" [1]. The biggest downside of it
> >>> is that it depends on Python to auto-generate the test "main" function,
> >>> which is not really a good fit for the Git project.
> >>
> >> Is that because Python is optional (like, we only use it for
> >> optional Perforce thing and import-zip in contrib), or are there
> >> other concerns?
> >>
> >> Unlike these components, unit tests are not even for the end-user
> >> consumers, so if it is Python that you find it a blocker, I do not
> >> see a reason to reject it.  The thing looked like a simple script
> >> that does not use any tricky language construct or modules.
> >
> > No concerns other than adding another mandatory dependency for devs. We
> > already depend on Perl and Shell, so adding Python to the stack feels
> > suboptimal to me.
> >
> > As you say though, the script isn't all that involved. So it would also
> > be possible to port it to Perl if we want to do that, I guess.
> 
> From the point of view of a "minimal C unit testing framework" surely
> the implementation language with the best dependency story would be C.
> Perhaps it could then also test itself.  On the other hand, writing a
> string handling program in 2024 in C is probably not the smartest idea.

I certainly wouldn't want to do it ;) I think Perl would be perfectly
fine given that we already rely on it rather extensively in Git.

> Then again, generate.py uses a non-greedy regex to remove single-line
> comments, which seems wrong, and doesn't seem to support preprocessor
> directives like #if and #ifdef, so whole tests can't be disabled this
> way.

I guess it doesn't have to be perfect, just good enough. clar comes with
some niceties like being able to run only some tests, all tests of a
particular suite or to exclude certain tests:

    # Run only submodule::add tests.
    $ ./libgit2_tests -t -ssubmodule::add::
    TAP version 13
    # start of suite 1: submodule::add
    ok 1 - submodule::add::url_absolute
    ok 2 - submodule::add::url_relative
    ok 3 - submodule::add::url_relative_to_origin
    ok 4 - submodule::add::url_relative_to_workdir
    ok 5 - submodule::add::path_exists_in_index
    ok 6 - submodule::add::file_exists_in_index
    ok 7 - submodule::add::submodule_clone
    ok 8 - submodule::add::submodule_clone_into_nonempty_dir_succeeds
    ok 9 - submodule::add::submodule_clone_twice_fails
    1..9

    # Run only a single test in the submodule::add suite.
    $ ./libgit2_tests -t -ssubmodule::add::url_absolute
    TAP version 13
    # start of suite 1: submodule::add
    ok 1 - submodule::add::url_absolute
    1..1

    # Run all submodule tests, but exclude submodule::add.
    $ ./libgit2_tests -t -ssubmodule::  -xsubmodule::add::
    TAP version 13
    # start of suite 1: submodule::escape
    ok 1 - submodule::escape::from_gitdir
    ok 2 - submodule::escape::from_gitdir_windows
    # start of suite 2: submodule::init
    ok 3 - submodule::init::absolute_url
    ...

So I'm not sure whether it's all that important to be able to recognize
things like #if and #ifdef given that you can pick tests to run on the
command line.

The alternative would be of course to hook up LLVM and properly parse
the C sources. But that feels like overthinking it ;)

> And it uses pickle to cache results; does that mean it would be
> slow without it?

Good question. I plan to find some time this week to draft a PoC and
will remember to benchmark this.

Patrick
René Scharfe July 31, 2024, 4:48 p.m. UTC | #13
Am 31.07.24 um 07:19 schrieb Patrick Steinhardt:
> On Tue, Jul 30, 2024 at 04:00:07PM +0200, René Scharfe wrote:
>>
>> From the point of view of a "minimal C unit testing framework" surely
>> the implementation language with the best dependency story would be C.
>> Perhaps it could then also test itself.  On the other hand, writing a
>> string handling program in 2024 in C is probably not the smartest idea.
>
> I certainly wouldn't want to do it ;) I think Perl would be perfectly
> fine given that we already rely on it rather extensively in Git.

Right.

> clar comes with
> some niceties like being able to run only some tests, all tests of a
> particular suite or to exclude certain tests:
>
>     # Run only submodule::add tests.
>     $ ./libgit2_tests -t -ssubmodule::add::
>     TAP version 13
>     # start of suite 1: submodule::add
>     ok 1 - submodule::add::url_absolute
>     ok 2 - submodule::add::url_relative
>     ok 3 - submodule::add::url_relative_to_origin
>     ok 4 - submodule::add::url_relative_to_workdir
>     ok 5 - submodule::add::path_exists_in_index
>     ok 6 - submodule::add::file_exists_in_index
>     ok 7 - submodule::add::submodule_clone
>     ok 8 - submodule::add::submodule_clone_into_nonempty_dir_succeeds
>     ok 9 - submodule::add::submodule_clone_twice_fails
>     1..9
>
>     # Run only a single test in the submodule::add suite.
>     $ ./libgit2_tests -t -ssubmodule::add::url_absolute
>     TAP version 13
>     # start of suite 1: submodule::add
>     ok 1 - submodule::add::url_absolute
>     1..1
>
>     # Run all submodule tests, but exclude submodule::add.
>     $ ./libgit2_tests -t -ssubmodule::  -xsubmodule::add::
>     TAP version 13
>     # start of suite 1: submodule::escape
>     ok 1 - submodule::escape::from_gitdir
>     ok 2 - submodule::escape::from_gitdir_windows
>     # start of suite 2: submodule::init
>     ok 3 - submodule::init::absolute_url
>     ...
>
> So I'm not sure whether it's all that important to be able to recognize
> things like #if and #ifdef given that you can pick tests to run on the
> command line.

My only reference are the regular (shell-based) tests, and those have
to deal with prerequisites like PTHREADS, PCRE or LIBCURL.  Such tests
wouldn't even compile, and would thus be #if'd out.  We'd have to move
those into separate files instead and skip the whole file, it seems.

> The alternative would be of course to hook up LLVM and properly parse
> the C sources. But that feels like overthinking it ;)

It's as simple as calling clang (or gcc) with the option -E.  We'd need
to disable -Wmissing-prototypes as well, though, as there will be no
declarations before we generate clar-decls.h.  Not sure how to do that
all in a portable way.

Anyway, another consequence is that adding, removing or renaming a
single test function requires rebuilding all unit tests, because
clar-decls.h changes and all of them include it.  Not a problem right
now.  If we ever get to the same number of test files as in the regular
test suite (ca. 1000) then this could become annoying.

Apropos, I wonder how a Clar-style t-ctype would look like, but didn't
dare to even start thinking about it, yet.

René
Patrick Steinhardt Aug. 1, 2024, 6:51 a.m. UTC | #14
On Wed, Jul 31, 2024 at 06:48:33PM +0200, René Scharfe wrote:
> Am 31.07.24 um 07:19 schrieb Patrick Steinhardt:
> > On Tue, Jul 30, 2024 at 04:00:07PM +0200, René Scharfe wrote:
> Apropos, I wonder how a Clar-style t-ctype would look like, but didn't
> dare to even start thinking about it, yet.

Oh, I already had it converted locally, but didn't end up sending it out
to keep things focussed for now. Below diff is how it could look like.

Patrick

diff --git a/Makefile b/Makefile
index cfdce5770b..ed67feb32e 100644
--- a/Makefile
+++ b/Makefile
@@ -1332,13 +1332,13 @@ THIRD_PARTY_SOURCES += compat/regex/%
 THIRD_PARTY_SOURCES += sha1collisiondetection/%
 THIRD_PARTY_SOURCES += sha1dc/%
 
+UNIT_TESTS_SUITES += ctype
 UNIT_TESTS_SUITES += strvec
 UNIT_TESTS_PROG = $(UNIT_TEST_BIN)/unit-tests$X
 UNIT_TESTS_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(UNIT_TESTS_SUITES))
 UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/clar/clar.o
 UNIT_TESTS_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-ctype
 UNIT_TEST_PROGRAMS += t-example-decorate
 UNIT_TEST_PROGRAMS += t-hash
 UNIT_TEST_PROGRAMS += t-mem-pool
diff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/ctype.c
similarity index 70%
rename from t/unit-tests/t-ctype.c
rename to t/unit-tests/ctype.c
index d6ac1fe678..2f6696e828 100644
--- a/t/unit-tests/t-ctype.c
+++ b/t/unit-tests/ctype.c
@@ -1,18 +1,12 @@
-#include "test-lib.h"
+#include "unit-test.h"
 
 #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)); \
-	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)); \
-	} \
-	test__run_end(!skip, TEST_LOCATION(), #class " works"); \
+	for (int i = 0; i < 256; i++) \
+		cl_assert_equal_i(class(i), !!memchr(string, i, len)); \
+	cl_assert(!class(EOF)); \
 } while (0)
 
 #define DIGIT "0123456789"
@@ -33,21 +27,72 @@
 	"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" \
 	"\x7f"
 
-int cmd_main(int argc, const char **argv) {
+void test_ctype__isspace(void)
+{
 	TEST_CHAR_CLASS(isspace, " \n\r\t");
+}
+
+void test_TEST_CHAR_ctype__CLASSisdigit(void)
+{
 	TEST_CHAR_CLASS(isdigit, DIGIT);
+}
+
+void test_TEST_CHAR_ctype__CLASSisalpha(void)
+{
 	TEST_CHAR_CLASS(isalpha, LOWER UPPER);
+}
+
+void test_ctype__isalnum(void)
+{
 	TEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT);
+}
+
+void test_is_glob_ctype__special(void)
+{
 	TEST_CHAR_CLASS(is_glob_special, "*?[\\");
+}
+
+void test_is_regex_ctype__special(void)
+{
 	TEST_CHAR_CLASS(is_regex_special, "$()*+.?[\\^{|");
+}
+
+void test_is_pathspec_ctype__magic(void)
+{
 	TEST_CHAR_CLASS(is_pathspec_magic, "!\"#%&',-/:;<=>@_`~");
+}
+
+void test_ctype__isascii(void)
+{
 	TEST_CHAR_CLASS(isascii, ASCII);
+}
+
+void test_ctype__islower(void)
+{
 	TEST_CHAR_CLASS(islower, LOWER);
+}
+
+void test_ctype__isupper(void)
+{
 	TEST_CHAR_CLASS(isupper, UPPER);
+}
+
+void test_ctype__iscntrl(void)
+{
 	TEST_CHAR_CLASS(iscntrl, CNTRL);
+}
+
+void test_ctype__ispunct(void)
+{
 	TEST_CHAR_CLASS(ispunct, PUNCT);
+}
+
+void test_ctype__isxdigit(void)
+{
 	TEST_CHAR_CLASS(isxdigit, DIGIT "abcdefABCDEF");
-	TEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT " ");
+}
 
-	return test_done();
+void test_ctype__isprint(void)
+{
+	TEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT " ");
 }
diff mbox series

Patch

diff --git a/.clang-format b/.clang-format
index 6408251577..863dc87dfc 100644
--- a/.clang-format
+++ b/.clang-format
@@ -151,6 +151,7 @@  Cpp11BracedListStyle: false
 # function calls. Taken from:
 #   git grep -h '^#define [^[:space:]]*for_\?each[^[:space:]]*(' |
 #   sed "s/^#define /  - '/; s/(.*$/'/" | sort | uniq
+# Added for_test from t/unit-tests/test-lib.h manually as a special case.
 ForEachMacros:
   - 'for_each_builtin'
   - 'for_each_string_list_item'
@@ -168,6 +169,7 @@  ForEachMacros:
   - 'strintmap_for_each_entry'
   - 'strmap_for_each_entry'
   - 'strset_for_each_entry'
+  - 'for_test'

 # The maximum number of consecutive empty lines to keep.
 MaxEmptyLinesToKeep: 1
diff --git a/t/helper/test-example-tap.c b/t/helper/test-example-tap.c
index 79c12b01cd..5e49fb1e7e 100644
--- a/t/helper/test-example-tap.c
+++ b/t/helper/test-example-tap.c
@@ -94,5 +94,38 @@  int cmd__example_tap(int argc, const char **argv)
 	test_res = TEST(t_empty(), "test with no checks");
 	TEST(check_int(test_res, ==, 0), "test with no checks returns 0");

+	for_test ("for_test passing test")
+		check_int(1, ==, 1);
+	for_test ("for_test failing test")
+		check_int(1, ==, 2);
+	for_test ("for_test passing TEST_TODO()")
+		TEST_TODO(check(0));
+	for_test ("for_test failing TEST_TODO()")
+		TEST_TODO(check(1));
+	for_test ("for_test test_skip()") {
+		check(0);
+		test_skip("missing prerequisite");
+		check(1);
+	}
+	for_test ("for_test test_skip() inside TEST_TODO()")
+		TEST_TODO((test_skip("missing prerequisite"), 1));
+	for_test ("for_test TEST_TODO() after failing check") {
+		check(0);
+		TEST_TODO(check(0));
+	}
+	for_test ("for_test failing check after TEST_TODO()") {
+		check(1);
+		TEST_TODO(check(0));
+		check(0);
+	}
+	for_test ("for_test messages from failing string and char comparison") {
+		check_str("\thello\\", "there\"\n");
+		check_str("NULL", NULL);
+		check_char('a', ==, '\n');
+		check_char('\\', ==, '\'');
+	}
+	for_test ("for_test test with no checks")
+		; /* nothing */
+
 	return test_done();
 }
diff --git a/t/t0080-unit-test-output.sh b/t/t0080-unit-test-output.sh
index fe221f3bdb..5185154414 100755
--- a/t/t0080-unit-test-output.sh
+++ b/t/t0080-unit-test-output.sh
@@ -50,7 +50,40 @@  test_expect_success 'TAP output from unit tests' - <<\EOT
 	# BUG: test has no checks at t/helper/test-example-tap.c:94
 	not ok 18 - test with no checks
 	ok 19 - test with no checks returns 0
-	1..19
+	ok 20 - for_test passing test
+	# check "1 == 2" failed at t/helper/test-example-tap.c:100
+	#    left: 1
+	#   right: 2
+	not ok 21 - for_test failing test
+	not ok 22 - for_test passing TEST_TODO() # TODO
+	# todo check 'check(1)' succeeded at t/helper/test-example-tap.c:104
+	not ok 23 - for_test failing TEST_TODO()
+	# check "0" failed at t/helper/test-example-tap.c:106
+	# skipping test - missing prerequisite
+	# skipping check '1' at t/helper/test-example-tap.c:108
+	ok 24 - for_test test_skip() # SKIP
+	# skipping test - missing prerequisite
+	ok 25 - for_test test_skip() inside TEST_TODO() # SKIP
+	# check "0" failed at t/helper/test-example-tap.c:113
+	not ok 26 - for_test TEST_TODO() after failing check
+	# check "0" failed at t/helper/test-example-tap.c:119
+	not ok 27 - for_test failing check after TEST_TODO()
+	# check "!strcmp("\thello\\\\", "there\"\n")" failed at t/helper/test-example-tap.c:122
+	#    left: "\011hello\\\\"
+	#   right: "there\"\012"
+	# check "!strcmp("NULL", NULL)" failed at t/helper/test-example-tap.c:123
+	#    left: "NULL"
+	#   right: NULL
+	# check "'a' == '\n'" failed at t/helper/test-example-tap.c:124
+	#    left: 'a'
+	#   right: '\012'
+	# check "'\\\\' == '\\''" failed at t/helper/test-example-tap.c:125
+	#    left: '\\\\'
+	#   right: '\\''
+	not ok 28 - for_test messages from failing string and char comparison
+	# BUG: test has no checks at t/helper/test-example-tap.c:127
+	not ok 29 - for_test test with no checks
+	1..29
 	EOF

 	! test-tool example-tap >actual &&
diff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h
index 2de6d715d5..598c6ff9f3 100644
--- a/t/unit-tests/test-lib.h
+++ b/t/unit-tests/test-lib.h
@@ -14,6 +14,26 @@ 
 	test__run_end(test__run_begin() ? 0 : (t, 1),	\
 		      TEST_LOCATION(),  __VA_ARGS__)

+/*
+ * Run a test unless test_skip_all() has been called.  Acts like a for
+ * loop that runs at most once, with the test description between the
+ * parentheses and the test body as a statement or block after them.
+ * Supports continue to end the test early, but not break. The
+ * description for each test should be unique.  E.g.:
+ *
+ *  for_test ("something else %d %d", arg1, arg2) {
+ *          prepare();
+ *          test_something_else(arg1, arg2);
+ *          cleanup();
+ *  }
+ */
+#define for_test(...)							\
+	for (int for_test_running_ = test__run_begin() ?		\
+		(test__run_end(0, TEST_LOCATION(), __VA_ARGS__), 0) : 1;\
+	     for_test_running_;						\
+	     test__run_end(1, TEST_LOCATION(), __VA_ARGS__),		\
+		for_test_running_ = 0)
+
 /*
  * Print a test plan, should be called before any tests. If the number
  * of tests is not known in advance test_done() will automatically