diff mbox series

[2/6] unit-tests: add TEST_RUN

Message ID 8175f239-8d4e-49f7-ae0d-dba7df8c365d@web.de (mailing list archive)
State New, archived
Headers show
Series unit-tests: add and use TEST_RUN to simplify tests | expand

Commit Message

René Scharfe June 29, 2024, 3:43 p.m. UTC
The macro TEST only allows defining a test that consists of a single
expression.  Add the new macro, TEST_RUN, which provides a way to define
unit tests that are made up of one or more statements.  A test started
with it implicitly ends when the next test is started or test_done() is
called.

TEST_RUN allows defining self-contained tests en bloc, a bit like
test_expect_success does for regular tests.  Unlike TEST it does not
require defining wrapper functions for test statements.

No public method is provided for ending a test explicitly, yet; let's
see if we'll ever need one.

Signed-off-by: René Scharfe <l.s.r@web.de>
---
 t/helper/test-example-tap.c | 33 +++++++++++++++++++++++++++++
 t/t0080/expect              | 35 ++++++++++++++++++++++++++++++-
 t/unit-tests/test-lib.c     | 42 +++++++++++++++++++++++++++++++++++--
 t/unit-tests/test-lib.h     |  8 +++++++
 4 files changed, 115 insertions(+), 3 deletions(-)

--
2.45.2

Comments

Phillip Wood July 2, 2024, 3:13 p.m. UTC | #1
Hi René

On 29/06/2024 16:43, René Scharfe wrote:
> The macro TEST only allows defining a test that consists of a single
> expression.  Add the new macro, TEST_RUN, which provides a way to define
> unit tests that are made up of one or more statements.  A test started
> with it implicitly ends when the next test is started or test_done() is
> called.
> 
> TEST_RUN allows defining self-contained tests en bloc, a bit like
> test_expect_success does for regular tests.  Unlike TEST it does not
> require defining wrapper functions for test statements.

There are pros and cons to not requiring one function per test. It can 
be a pain to have to write separate functions but it keeps each test 
self contained which hopefully makes it harder to have accidental 
dependencies between tests. Having separate functions for each test 
makes it easy to initialize and free resources for every test by writing 
a setup() function that initializes the resources, calls the test 
function and then frees the resources. The changes in patch 6 to use 
TEST_RUN() mean that each test now has more boilerplate to initialize 
and free the strbuf. Having each test in its own function also makes 
main() shorter and which means can quickly get an overview of all the 
test cases from it.

On the other hand having all the tests defined in main() using 
TEST_RUN() means we can just write the test body without having to 
define a separate function and then call it with TEST()

> No public method is provided for ending a test explicitly, yet; let's
> see if we'll ever need one.

This means that we do not error out if there are accidentally nested 
tests. That probably does not matter too much.


> +int test__run(const char *location, const char *format, ...)
> +{
> +	va_list ap;
> +	char *desc;
> +
> +	test__run_maybe_end();
> +
> +	va_start(ap, format);
> +	desc = xstrvfmt(format, ap);

This uses an strbuf under the hood. So far we've avoided doing that as 
we want to be able to test the strbuf implementation with this test 
framework. We don't need to support arbitrary length strings here so we 
could use a fixed array and xsnprinf() instead.

> +/*
> + * Start a test, returns 1 if the test was actually started or 0 if it
> + * was skipped.  The test ends when the next test starts or test_done()
> + * is called.
> + */
> +#define TEST_RUN(...) test__run(TEST_LOCATION(), __VA_ARGS__)

Looking ahead the plan seems to be to change most instances of TEST() to 
TEST_RUN(). If we are going to go that way perhaps we should steal 
TEST() for this macro and rename the existing TEST() macro.

I'm not very enthusiastic about requiring the test author to wrap 
TEST_RUN() in an if() statement rather than just doing that for them. It 
makes it explicit but from the test author's point of view it just feels 
like pointless boilerplate.

>   /*
>    * 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
> @@ -156,6 +163,7 @@ extern union test__tmp test__tmp[2];
>   int test__run_begin(void);
>   __attribute__((format (printf, 3, 4)))
>   int test__run_end(int, const char *, const char *, ...);

We should add

     __attribute__((format (printf, 2, 3), warn_unused_result))

here to catch any errors in the format string / arguments and to warn if 
TEST_RUN() isn't wrapped in an if() statement.

Best Wishes

Phillip

> +int test__run(const char *location, const char *format, ...);
>   void test__todo_begin(void);
>   int test__todo_end(const char *, const char *, int);
> 
> --
> 2.45.2
Junio C Hamano July 2, 2024, 3:51 p.m. UTC | #2
phillip.wood123@gmail.com writes:

>> No public method is provided for ending a test explicitly, yet; let's
>> see if we'll ever need one.
>
> This means that we do not error out if there are accidentally nested
> tests. That probably does not matter too much.

Isn't it more like it is impossible to create nested tests, since a
"begin" implicitly ends the current one before starting the new one?

>> @@ -156,6 +163,7 @@ extern union test__tmp test__tmp[2];
>>   int test__run_begin(void);
>>   __attribute__((format (printf, 3, 4)))
>>   int test__run_end(int, const char *, const char *, ...);
>
> We should add
>
>     __attribute__((format (printf, 2, 3), warn_unused_result))
>
> here to catch any errors in the format string / arguments and to warn
> if TEST_RUN() isn't wrapped in an if() statement.

Nice.  Especially the "unused" check is valuable.

Thanks.
René Scharfe July 2, 2024, 8:55 p.m. UTC | #3
Am 02.07.24 um 17:51 schrieb Junio C Hamano:
> phillip.wood123@gmail.com writes:
>
>>> @@ -156,6 +163,7 @@ extern union test__tmp test__tmp[2];
>>>   int test__run_begin(void);
>>>   __attribute__((format (printf, 3, 4)))
>>>   int test__run_end(int, const char *, const char *, ...);
>>
>> We should add
>>
>>     __attribute__((format (printf, 2, 3), warn_unused_result))
>>
>> here to catch any errors in the format string / arguments and to warn
>> if TEST_RUN() isn't wrapped in an if() statement.
>
> Nice.  Especially the "unused" check is valuable.

This confused me for a moment.  I assume you both mean the new function
test__run, whose addition was cut from the citation:

   +int test__run(const char *location, const char *format, ...);

For that one the numbers make sense and the idea is good. :)

René
René Scharfe July 2, 2024, 8:55 p.m. UTC | #4
Am 02.07.24 um 17:13 schrieb phillip.wood123@gmail.com:
> Hi René
>
> On 29/06/2024 16:43, René Scharfe wrote:
>> The macro TEST only allows defining a test that consists of a
>> single expression.  Add the new macro, TEST_RUN, which provides a
>> way to define unit tests that are made up of one or more
>> statements.  A test started with it implicitly ends when the next
>> test is started or test_done() is called.
>>
>> TEST_RUN allows defining self-contained tests en bloc, a bit like
>> test_expect_success does for regular tests.  Unlike TEST it does
>> not require defining wrapper functions for test statements.
>
> There are pros and cons to not requiring one function per test. It
> can be a pain to have to write separate functions but it keeps each
> test self contained which hopefully makes it harder to have
> accidental dependencies between tests. Having separate functions for
> each test makes it easy to initialize and free resources for every
> test by writing a setup() function that initializes the resources,
> calls the test function and then frees the resources.

Right.  We should use TEST and TEST_RUN when appropriate.

> The changes in patch 6 to use TEST_RUN() mean that each test now has
> more boilerplate to initialize and free the strbuf.
This makes them more similar to strbuf usage in the wild.  Using
the API idiomatically just makes more sense to me.  Not hiding
initialization and release makes the tests visibly independent.
This is not enforced by TEST_RUN, but made possible by it.

> Having each test in its own function also makes main() shorter and
> which means can quickly get an overview of all the test cases from
> it.

That's true, now you need to grep for TEST_RUN to get such an
overview.

On the other hand I find the start of the description in TEST
invocations somewhat hard to locate, as they are not vertically
aligned due to the preceding variable-length function name.  Just
saying..

>> +int test__run(const char *location, const char *format, ...)
>> +{
>> +    va_list ap;
>> +    char *desc;
>> +
>> +    test__run_maybe_end();
>> +
>> +    va_start(ap, format);
>> +    desc = xstrvfmt(format, ap);
>
> This uses an strbuf under the hood. So far we've avoided doing that
> as we want to be able to test the strbuf implementation with this
> test framework. We don't need to support arbitrary length strings
> here so we could use a fixed array and xsnprinf() instead.

Fair point.  xsnprinf() might be a bit too strict, as it doesn't
handle short buffers gracefully.  Perhaps that's OK; a developer
getting hit by that could simply increase the buffer size.

We could also let xstrvfmt() call vsnprintf(3) directly.  The code
duplication would be a bit grating, but perhaps there's some good
base function hidden in there somewhere.

> Looking ahead the plan seems to be to change most instances of TEST()
> to TEST_RUN(). If we are going to go that way perhaps we should steal
> TEST() for this macro and rename the existing TEST() macro.

Not my plan, at least -- I'm content with just having the *ability*
to keep all parts of a test together.

If we wanted to do that, though, then TEST_RUN would have to be
complemented with a blessed way to check ctx.result in order to handle
the t_static_init test of t-strbuf, which I mentioned in my earlier
reply to Josh.

Err, no, we can simply check the check_* results, like check_strvec_loc
does:

diff --git a/t/unit-tests/t-strbuf.c b/t/unit-tests/t-strbuf.c
index c8e39ddda7..c765fab53a 100644
--- a/t/unit-tests/t-strbuf.c
+++ b/t/unit-tests/t-strbuf.c
@@ -19,15 +19,6 @@ static int assert_sane_strbuf(struct strbuf *buf)
 	return check_uint(buf->len, <, buf->alloc);
 }

-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');
-}
-
 static void t_dynamic_init(void)
 {
 	struct strbuf buf;
@@ -85,8 +76,14 @@ static void t_release(struct strbuf *sb)

 int cmd_main(int argc, const char **argv)
 {
-	if (!TEST(t_static_init(), "static initialization works"))
-		test_skip_all("STRBUF_INIT is broken");
+	if (TEST_RUN("static initialization works")) {
+		struct strbuf buf = STRBUF_INIT;
+		if (!check_uint(buf.len, ==, 0) ||
+		    !check_uint(buf.alloc, ==, 0) ||
+		    !check_char(buf.buf[0], ==, '\0'))
+			test_skip_all("STRBUF_INIT is broken");
+	}
+
 	TEST(t_dynamic_init(), "dynamic initialization works");

 	if (TEST_RUN("strbuf_addch adds char")) {

> I'm not very enthusiastic about requiring the test author to wrap
> TEST_RUN() in an if() statement rather than just doing that for them.
> It makes it explicit but from the test author's point of view it just
> feels like pointless boilerplate.

Hmm.  We can add more magic, but I suspect that it might confuse
developers and editors.

René
Phillip Wood July 5, 2024, 9:42 a.m. UTC | #5
Hi René

On 02/07/2024 21:55, René Scharfe wrote:
> Am 02.07.24 um 17:13 schrieb phillip.wood123@gmail.com:
>> On 29/06/2024 16:43, René Scharfe wrote:
>>> The macro TEST only allows defining a test that consists of a
>>> single expression.  Add the new macro, TEST_RUN, which provides a
>>> way to define unit tests that are made up of one or more
>>> statements.  A test started with it implicitly ends when the next
>>> test is started or test_done() is called.
>>>
>>> TEST_RUN allows defining self-contained tests en bloc, a bit like
>>> test_expect_success does for regular tests.  Unlike TEST it does
>>> not require defining wrapper functions for test statements.
>>
>> There are pros and cons to not requiring one function per test. It
>> can be a pain to have to write separate functions but it keeps each
>> test self contained which hopefully makes it harder to have
>> accidental dependencies between tests. Having separate functions for
>> each test makes it easy to initialize and free resources for every
>> test by writing a setup() function that initializes the resources,
>> calls the test function and then frees the resources.
> 
> Right.  We should use TEST and TEST_RUN when appropriate.
> 
>> The changes in patch 6 to use TEST_RUN() mean that each test now has
>> more boilerplate to initialize and free the strbuf. > This makes them more similar to strbuf usage in the wild.  Using
> the API idiomatically just makes more sense to me.

I see what you mean. I think it only looks idiomatic if you're already 
familiar with the api though as the test bodies call wrappers rather 
than using the strbuf api directly. I think that reduces its value as an 
example of idomatic usage for someone who is not familiar with the 
strbuf api.

>  Not hiding
> initialization and release makes the tests visibly independent.
> This is not enforced by TEST_RUN, but made possible by it.
> 
>> Having each test in its own function also makes main() shorter and
>> which means can quickly get an overview of all the test cases from
>> it.
> 
> That's true, now you need to grep for TEST_RUN to get such an
> overview.
> 
> On the other hand I find the start of the description in TEST
> invocations somewhat hard to locate, as they are not vertically
> aligned due to the preceding variable-length function name.  Just
> saying..

Yes I really wanted the first argument of TEST to be the description but 
that isn't easy to do while supporting printf style format strings.

>>> +int test__run(const char *location, const char *format, ...)
>>> +{
>>> +    va_list ap;
>>> +    char *desc;
>>> +
>>> +    test__run_maybe_end();
>>> +
>>> +    va_start(ap, format);
>>> +    desc = xstrvfmt(format, ap);
>>
>> This uses an strbuf under the hood. So far we've avoided doing that
>> as we want to be able to test the strbuf implementation with this
>> test framework. We don't need to support arbitrary length strings
>> here so we could use a fixed array and xsnprinf() instead.
> 
> Fair point.  xsnprinf() might be a bit too strict, as it doesn't
> handle short buffers gracefully.  Perhaps that's OK; a developer
> getting hit by that could simply increase the buffer size.

I think so.

> We could also let xstrvfmt() call vsnprintf(3) directly.  The code
> duplication would be a bit grating, but perhaps there's some good
> base function hidden in there somewhere.

Oh, interesting - maybe something like

char* xstrvfmt(const char *fmt, ...)
{
	va_list ap, aq;

	va_start(ap, fmt);
	va_copy(aq, ap);
	len = vnsprintf(NULL, 0, fmt, ap);
	if (len < 0)
		BUG(...)
	buf = xmalloc(len + 1);
	if (vnsprintf(buf, len + 1, fmt, aq) != len)
		BUG(...)
	va_end(aq);
	va_end(ap);

	return buf;
}

>> Looking ahead the plan seems to be to change most instances of TEST()
>> to TEST_RUN(). If we are going to go that way perhaps we should steal
>> TEST() for this macro and rename the existing TEST() macro.
> 
> Not my plan, at least -- I'm content with just having the *ability*
> to keep all parts of a test together.

That sounds sensible to me

> +	if (TEST_RUN("static initialization works")) {
> +		struct strbuf buf = STRBUF_INIT;
> +		if (!check_uint(buf.len, ==, 0) ||
> +		    !check_uint(buf.alloc, ==, 0) ||
> +		    !check_char(buf.buf[0], ==, '\0'))
> +			test_skip_all("STRBUF_INIT is broken");
> +	}

that's a nice use of test_skip_all()

>> I'm not very enthusiastic about requiring the test author to wrap
>> TEST_RUN() in an if() statement rather than just doing that for them.
>> It makes it explicit but from the test author's point of view it just
>> feels like pointless boilerplate.
> 
> Hmm.  We can add more magic, but I suspect that it might confuse
> developers and editors.

To me its confusing to have to wrap TEST_RUN() in an if() statement 
until one realizes that the test might be skipped. If we document that 
the test body should be enclosed in braces I don't think it should 
confuse developers or editors and will keep the tests a bit cleaner.

Best Wishes

Phillip
René Scharfe July 5, 2024, 6:01 p.m. UTC | #6
Am 05.07.24 um 11:42 schrieb phillip.wood123@gmail.com:
> On 02/07/2024 21:55, René Scharfe wrote:
>> Am 02.07.24 um 17:13 schrieb phillip.wood123@gmail.com:
>>
>>> The changes in patch 6 to use TEST_RUN() mean that each test now has
>>> more boilerplate to initialize and free the strbuf.
>>
>> This makes them more similar to strbuf usage in the wild.  Using
>> the API idiomatically just makes more sense to me.
>
> I see what you mean. I think it only looks idiomatic if you're
> already familiar with the api though as the test bodies call wrappers
> rather than using the strbuf api directly. I think that reduces its
> value as an example of idomatic usage for someone who is not familiar
> with the strbuf api.

In early versions I used the original names by adding these just before
main():

   #define strbuf_addch(x, y)  t_addch(x, y)
   #define strbuf_addstr(x, y) t_addstr(x, y)
   #define strbuf_release(x)   t_release(x)

This allowed normal looking code to be used in tests, with checks
injected behind the scenes.  Rejected it for v1 because it offers no
structural improvements, just optics.  It does allow to forget the
checked versions when writing tests, though, so perhaps it's still
worth doing.

>> We could also let xstrvfmt() call vsnprintf(3) directly.  The code
>> duplication would be a bit grating, but perhaps there's some good
>> base function hidden in there somewhere.
>
> Oh, interesting - maybe something like
>
> char* xstrvfmt(const char *fmt, ...)
> {
>     va_list ap, aq;
>
>     va_start(ap, fmt);
>     va_copy(aq, ap);
>     len = vnsprintf(NULL, 0, fmt, ap);
>     if (len < 0)
>         BUG(...)
>     buf = xmalloc(len + 1);
>     if (vnsprintf(buf, len + 1, fmt, aq) != len)
>         BUG(...)
>     va_end(aq);
>     va_end(ap);
>
>     return buf;
> }

Yes.  Though the current version allocates 65 bytes in the first try
and only needs to call vnsprintf(3) once if the output fits in.  No
longer doing that might affect the performance of some callers in a
noticeable way.

>>> I'm not very enthusiastic about requiring the test author to wrap
>>> TEST_RUN() in an if() statement rather than just doing that for them.
>>> It makes it explicit but from the test author's point of view it just
>>> feels like pointless boilerplate.
>>
>> Hmm.  We can add more magic, but I suspect that it might confuse
>> developers and editors.
>
> To me its confusing to have to wrap TEST_RUN() in an if() statement
> until one realizes that the test might be skipped. If we document
> that the test body should be enclosed in braces I don't think it
> should confuse developers or editors and will keep the tests a bit
> cleaner.

You don't need braces in either case.  I.e. something like

	if (TEST_RUN("foo"))
		foo();

works fine.  And

	#define IF_TEST_RUN(...) if (TEST_RUN(__VA_ARGS__))
	IF_TEST_RUN("foo")
		foo();

works fine as well.

The confusion that I'm afraid of is that we'd effectively invent a new
control flow keyword here, which is unusual.  There are precedents,
though: foreach macros like for_each_string_list_item.  We tell
clang-format about them using its option ForEachMacros.  I see it also
has an option IfMacros since version 13 (unused by us so far).  Hmm.

René
René Scharfe July 7, 2024, 7:20 a.m. UTC | #7
Am 05.07.24 um 20:01 schrieb René Scharfe:
> Am 05.07.24 um 11:42 schrieb phillip.wood123@gmail.com:
>> On 02/07/2024 21:55, René Scharfe wrote:
>>> Am 02.07.24 um 17:13 schrieb phillip.wood123@gmail.com:
>>>
>>>> I'm not very enthusiastic about requiring the test author to wrap
>>>> TEST_RUN() in an if() statement rather than just doing that for them.
>>>> It makes it explicit but from the test author's point of view it just
>>>> feels like pointless boilerplate.
>>>
>>> Hmm.  We can add more magic, but I suspect that it might confuse
>>> developers and editors.
>>
>> To me its confusing to have to wrap TEST_RUN() in an if() statement
>> until one realizes that the test might be skipped. If we document
>> that the test body should be enclosed in braces I don't think it
>> should confuse developers or editors and will keep the tests a bit
>> cleaner.
>
> You don't need braces in either case.  I.e. something like
>
> 	if (TEST_RUN("foo"))
> 		foo();
>
> works fine.  And
>
> 	#define IF_TEST_RUN(...) if (TEST_RUN(__VA_ARGS__))
> 	IF_TEST_RUN("foo")
> 		foo();
>
> works fine as well.
>
> The confusion that I'm afraid of is that we'd effectively invent a new
> control flow keyword here, which is unusual.  There are precedents,
> though: foreach macros like for_each_string_list_item.  We tell
> clang-format about them using its option ForEachMacros.  I see it also
> has an option IfMacros since version 13 (unused by us so far).  Hmm.

Hmm, again.  I can see the appeal.  How to call it?  Given that the
foreach macros have lower-case names, perhaps to indicate that they act
as control flow statements, not function-like macro calls, would we want
lower case here as well?

	#define test(...) if (TEST_RUN(__VA_ARGS__))

	        test ("passing test")
	                check(1);

		test ("split single item") {
			struct strvec vec = STRVEC_INIT;
			strvec_split(&vec, "foo");
			check_strvec(&vec, "foo", NULL);
			strvec_clear(&vec);
		}

Can't get much cleaner than that.  Requires learning that this macro is
not function-like, but it's probably not too much to ask.

René
Phillip Wood July 8, 2024, 3:12 p.m. UTC | #8
Hi René

On 05/07/2024 19:01, René Scharfe wrote:
> Am 05.07.24 um 11:42 schrieb phillip.wood123@gmail.com:
>> On 02/07/2024 21:55, René Scharfe wrote:
>>> Am 02.07.24 um 17:13 schrieb phillip.wood123@gmail.com:
>>>
>>>> The changes in patch 6 to use TEST_RUN() mean that each test now has
>>>> more boilerplate to initialize and free the strbuf.
>>>
>>> This makes them more similar to strbuf usage in the wild.  Using
>>> the API idiomatically just makes more sense to me.
>>
>> I see what you mean. I think it only looks idiomatic if you're
>> already familiar with the api though as the test bodies call wrappers
>> rather than using the strbuf api directly. I think that reduces its
>> value as an example of idomatic usage for someone who is not familiar
>> with the strbuf api.
> 
> In early versions I used the original names by adding these just before
> main():
> 
>     #define strbuf_addch(x, y)  t_addch(x, y)
>     #define strbuf_addstr(x, y) t_addstr(x, y)
>     #define strbuf_release(x)   t_release(x)
> 
> This allowed normal looking code to be used in tests, with checks
> injected behind the scenes.  Rejected it for v1 because it offers no
> structural improvements, just optics.  It does allow to forget the
> checked versions when writing tests, though, so perhaps it's still
> worth doing.

It also obscures what the test is doing though. It's nice if unit tests 
show how to use an API but I don't think we should let that be our 
overriding concern.

>>> We could also let xstrvfmt() call vsnprintf(3) directly.  The code
>>> duplication would be a bit grating, but perhaps there's some good
>>> base function hidden in there somewhere.
>>
>> Oh, interesting - maybe something like
>>
>> char* xstrvfmt(const char *fmt, ...)
>> {
>>      va_list ap, aq;
>>
>>      va_start(ap, fmt);
>>      va_copy(aq, ap);
>>      len = vnsprintf(NULL, 0, fmt, ap);
>>      if (len < 0)
>>          BUG(...)
>>      buf = xmalloc(len + 1);
>>      if (vnsprintf(buf, len + 1, fmt, aq) != len)
>>          BUG(...)
>>      va_end(aq);
>>      va_end(ap);
>>
>>      return buf;
>> }
> 
> Yes.  Though the current version allocates 65 bytes in the first try
> and only needs to call vnsprintf(3) once if the output fits in.  No
> longer doing that might affect the performance of some callers in a
> noticeable way.

Good point

>>>> I'm not very enthusiastic about requiring the test author to wrap
>>>> TEST_RUN() in an if() statement rather than just doing that for them.
>>>> It makes it explicit but from the test author's point of view it just
>>>> feels like pointless boilerplate.
>>>
>>> Hmm.  We can add more magic, but I suspect that it might confuse
>>> developers and editors.
>>
>> To me its confusing to have to wrap TEST_RUN() in an if() statement
>> until one realizes that the test might be skipped. If we document
>> that the test body should be enclosed in braces I don't think it
>> should confuse developers or editors and will keep the tests a bit
>> cleaner.
> 
> You don't need braces in either case.  I.e. something like
> 
> 	if (TEST_RUN("foo"))
> 		foo();
> 
> works fine.  And
> 
> 	#define IF_TEST_RUN(...) if (TEST_RUN(__VA_ARGS__))
> 	IF_TEST_RUN("foo")
> 		foo();
> 
> works fine as well.

Indeed. The reason I suggested requiring braces was to help editors 
indent the code correctly and because it gives a nice visual cue to see 
what is in each test.

Best Wishes

Phillip

> The confusion that I'm afraid of is that we'd effectively invent a new
> control flow keyword here, which is unusual.  There are precedents,
> though: foreach macros like for_each_string_list_item.  We tell
> clang-format about them using its option ForEachMacros.  I see it also
> has an option IfMacros since version 13 (unused by us so far).  Hmm.
> 
> René
Phillip Wood July 8, 2024, 3:18 p.m. UTC | #9
Hi René

On 07/07/2024 08:20, René Scharfe wrote:
> Hmm, again.  I can see the appeal.  How to call it?  Given that the
> foreach macros have lower-case names, perhaps to indicate that they act
> as control flow statements, not function-like macro calls, would we want
> lower case here as well?

I'd automatically assumed we'd want an uppercase name to signal that it 
was a pre-processor macro but I've not really spent any time thinking 
about it.

> 	#define test(...) if (TEST_RUN(__VA_ARGS__))
> 
> 	        test ("passing test")
> 	                check(1);
> 
> 		test ("split single item") {
> 			struct strvec vec = STRVEC_INIT;
> 			strvec_split(&vec, "foo");
> 			check_strvec(&vec, "foo", NULL);
> 			strvec_clear(&vec);
> 		}
> 
> Can't get much cleaner than that.

Yes, I like it

> Requires learning that this macro is
> not function-like, but it's probably not too much to ask.

For someone new to the project it should hopefully be pretty clear how 
to use it from the existing tests once we have a few more test files 
that use it. Maybe an uppercase name would signal that it is special?

Best Wishes

Phillip
Junio C Hamano July 8, 2024, 3:39 p.m. UTC | #10
phillip.wood123@gmail.com writes:

> Hi René
>
> On 07/07/2024 08:20, René Scharfe wrote:
>> Hmm, again.  I can see the appeal.  How to call it?  Given that the
>> foreach macros have lower-case names, perhaps to indicate that they act
>> as control flow statements, not function-like macro calls, would we want
>> lower case here as well?
>
> I'd automatically assumed we'd want an uppercase name to signal that
> it was a pre-processor macro but I've not really spent any time
> thinking about it.
>
>> 	#define test(...) if (TEST_RUN(__VA_ARGS__))
>> 	        test ("passing test")
>> 	                check(1);
>> 		test ("split single item") {
>> 			struct strvec vec = STRVEC_INIT;
>> 			strvec_split(&vec, "foo");
>> 			check_strvec(&vec, "foo", NULL);
>> 			strvec_clear(&vec);
>> 		}
>> Can't get much cleaner than that.
>
> Yes, I like it

Yeah, if you squint your eyes hard, it starts to look a bit like
test_expect_success we use in the test suite ;-)

Isn't this introducing a new control structure to the language?

A macro that is a conditional switch (aka "if"-like statement),
having "if" in the name somewhere, and a macro that wrapts a loop
around a block (aka "for/while" like statement), having "for" in the
name somewhere, might be less confusing for the uninitiated.

> For someone new to the project it should hopefully be pretty clear how
> to use it from the existing tests once we have a few more test files
> that use it. Maybe an uppercase name would signal that it is special?

As to the cases, I personally prefer lowercase names whose semantics
is very clear, just like e.g., kh_foreach() makes it clear with
"foreach" that it iterates over the set.  But the above "test" may
not qualify---it is not making it sufficiently clear what control
structure it is creating with its name.

Thanks.
Junio C Hamano July 11, 2024, 3:34 p.m. UTC | #11
Junio C Hamano <gitster@pobox.com> writes:

>> I'd automatically assumed we'd want an uppercase name to signal that
>> it was a pre-processor macro but I've not really spent any time
>> thinking about it.
>>
>>> 	#define test(...) if (TEST_RUN(__VA_ARGS__))
>>> 	        test ("passing test")
>>> 	                check(1);
> ...
> Isn't this introducing a new control structure to the language?
>
> A macro that is a conditional switch (aka "if"-like statement),
> having "if" in the name somewhere, and a macro that wrapts a loop
> around a block (aka "for/while" like statement), having "for" in the
> name somewhere, might be less confusing for the uninitiated.

So, perhaps test_if_XXXXXX() but it is not quite clear to me when
TEST_RUN() wants to return true, so I cannot come up with an
appropriate value to fill the XXXXXX part.  If this is about
honoring GIT_SKIP_TESTS or something similar, then I may suggest
test_if_enabled(), but that does not seem like it.  So...
Phillip Wood July 13, 2024, 1:27 p.m. UTC | #12
On 11/07/2024 16:34, Junio C Hamano wrote:
> Junio C Hamano <gitster@pobox.com> writes:
> 
> So, perhaps test_if_XXXXXX() but it is not quite clear to me when
> TEST_RUN() wants to return true, so I cannot come up with an
> appropriate value to fill the XXXXXX part.  If this is about
> honoring GIT_SKIP_TESTS or something similar, then I may suggest
> test_if_enabled(), but that does not seem like it.  So...

TEST_RUN() returns true if the test has not been skipped i.e. the test 
should be run. At the moment the only way to skip a test is to call 
test_skip_all() in a previous test. In the future I expect we'll add 
something like the "--run" option we have for the integration tests.

Best Wishes

Phillip
Junio C Hamano July 13, 2024, 3:48 p.m. UTC | #13
Phillip Wood <phillip.wood123@gmail.com> writes:

> On 11/07/2024 16:34, Junio C Hamano wrote:
>> Junio C Hamano <gitster@pobox.com> writes:
>> So, perhaps test_if_XXXXXX() but it is not quite clear to me when
>> TEST_RUN() wants to return true, so I cannot come up with an
>> appropriate value to fill the XXXXXX part.  If this is about
>> honoring GIT_SKIP_TESTS or something similar, then I may suggest
>> test_if_enabled(), but that does not seem like it.  So...
>
> TEST_RUN() returns true if the test has not been skipped i.e. the test
> should be run. At the moment the only way to skip a test is to call
> test_skip_all() in a previous test. In the future I expect we'll add
> something like the "--run" option we have for the integration tests.

Thanks, so test_if_enabled() is not all that off the mark after all.
diff mbox series

Patch

diff --git a/t/helper/test-example-tap.c b/t/helper/test-example-tap.c
index d072ad559f..7b02177a9f 100644
--- a/t/helper/test-example-tap.c
+++ b/t/helper/test-example-tap.c
@@ -92,5 +92,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");

+	if (TEST_RUN("TEST_RUN passing test"))
+		check_int(1, ==, 1);
+	if (TEST_RUN("TEST_RUN failing test"))
+		check_int(1, ==, 2);
+	if (TEST_RUN("TEST_RUN passing TEST_TODO()"))
+		TEST_TODO(check(0));
+	if (TEST_RUN("TEST_RUN failing TEST_TODO()"))
+		TEST_TODO(check(1));
+	if (TEST_RUN("TEST_RUN test_skip()")) {
+		check(0);
+		test_skip("missing prerequisite");
+		check(1);
+	}
+	if (TEST_RUN("TEST_RUN test_skip() inside TEST_TODO()"))
+		TEST_TODO((test_skip("missing prerequisite"), 1));
+	if (TEST_RUN("TEST_RUN TEST_TODO() after failing check")) {
+		check(0);
+		TEST_TODO(check(0));
+	}
+	if (TEST_RUN("TEST_RUN failing check after TEST_TODO()")) {
+		check(1);
+		TEST_TODO(check(0));
+		check(0);
+	}
+	if (TEST_RUN("TEST_RUN messages from failing string and char comparison")) {
+		check_str("\thello\\", "there\"\n");
+		check_str("NULL", NULL);
+		check_char('a', ==, '\n');
+		check_char('\\', ==, '\'');
+	}
+	if (TEST_RUN("TEST_RUN test with no checks"))
+		; /* nothing */
+
 	return test_done();
 }
diff --git a/t/t0080/expect b/t/t0080/expect
index 0cfa0dc6d8..92526e1b06 100644
--- a/t/t0080/expect
+++ b/t/t0080/expect
@@ -40,4 +40,37 @@  not ok 17 - messages from failing string and char comparison
 # BUG: test has no checks at t/helper/test-example-tap.c:92
 not ok 18 - test with no checks
 ok 19 - test with no checks returns 0
-1..19
+ok 20 - TEST_RUN passing test
+# check "1 == 2" failed at t/helper/test-example-tap.c:98
+#    left: 1
+#   right: 2
+not ok 21 - TEST_RUN failing test
+not ok 22 - TEST_RUN passing TEST_TODO() # TODO
+# todo check 'check(1)' succeeded at t/helper/test-example-tap.c:102
+not ok 23 - TEST_RUN failing TEST_TODO()
+# check "0" failed at t/helper/test-example-tap.c:104
+# skipping test - missing prerequisite
+# skipping check '1' at t/helper/test-example-tap.c:106
+ok 24 - TEST_RUN test_skip() # SKIP
+# skipping test - missing prerequisite
+ok 25 - TEST_RUN test_skip() inside TEST_TODO() # SKIP
+# check "0" failed at t/helper/test-example-tap.c:111
+not ok 26 - TEST_RUN TEST_TODO() after failing check
+# check "0" failed at t/helper/test-example-tap.c:117
+not ok 27 - TEST_RUN failing check after TEST_TODO()
+# check "!strcmp("\thello\\", "there\"\n")" failed at t/helper/test-example-tap.c:120
+#    left: "\011hello\\"
+#   right: "there\"\012"
+# check "!strcmp("NULL", NULL)" failed at t/helper/test-example-tap.c:121
+#    left: "NULL"
+#   right: NULL
+# check "'a' == '\n'" failed at t/helper/test-example-tap.c:122
+#    left: 'a'
+#   right: '\012'
+# check "'\\' == '\''" failed at t/helper/test-example-tap.c:123
+#    left: '\\'
+#   right: '\''
+not ok 28 - TEST_RUN messages from failing string and char comparison
+# BUG: test has no checks at t/helper/test-example-tap.c:125
+not ok 29 - TEST_RUN test with no checks
+1..29
diff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c
index 3c513ce59a..fc50755fae 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;
 }

-int test_done(void)
+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__run(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)
+{
+	test__run_maybe_end();

 	if (ctx.lazy_plan)
 		test_plan(ctx.count);
@@ -169,7 +207,7 @@  void test_skip_all(const char *format, ...)

 int test__run_begin(void)
 {
-	assert(!ctx.running);
+	test__run_maybe_end();

 	ctx.count++;
 	ctx.result = RESULT_NONE;
diff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h
index 2de6d715d5..6df40e3b12 100644
--- a/t/unit-tests/test-lib.h
+++ b/t/unit-tests/test-lib.h
@@ -21,6 +21,13 @@ 
  */
 void test_plan(int count);

+/*
+ * Start a test, returns 1 if the test was actually started or 0 if it
+ * was skipped.  The test ends when the next test starts or test_done()
+ * is called.
+ */
+#define TEST_RUN(...) test__run(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
@@ -156,6 +163,7 @@  extern union test__tmp test__tmp[2];
 int test__run_begin(void);
 __attribute__((format (printf, 3, 4)))
 int test__run_end(int, const char *, const char *, ...);
+int test__run(const char *location, const char *format, ...);
 void test__todo_begin(void);
 int test__todo_end(const char *, const char *, int);