git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v7 2/3] unit tests: add TAP unit test framework

From
Junio C Hamano <gitster@pobox.com>
Date
Sep 22, 2023, 20:05 UTC
Message-ID
<xmqqa5te0y9r.fsf@gitster.g>
In-Reply-To
<xmqq350hw6n7.fsf@gitster.g>

It seems this got stuck during Josh's absense and I didn't ping it further, but I should have noticed that you are the author of this patch, and pinged you in the meantime.

Any thought on the "polarity" of the return values from the assertion? I still find it confusing and hard to follow.

Thanks.
Show 267 quoted lines
> Josh Steadmon <steadmon@google.com> writes:
>
>> +test_expect_success 'TAP output from unit tests' '
>> +	cat >expect <<-EOF &&
>> +	ok 1 - passing test
>> +	ok 2 - passing test and assertion return 0
>> +	# check "1 == 2" failed at t/unit-tests/t-basic.c:76
>> +	#    left: 1
>> +	#   right: 2
>> +	not ok 3 - failing test
>> +	ok 4 - failing test and assertion return -1
>> +	not ok 5 - passing TEST_TODO() # TODO
>> +	ok 6 - passing TEST_TODO() returns 0
>> +	# todo check ${SQ}check(x)${SQ} succeeded at t/unit-tests/t-basic.c:25
>> +	not ok 7 - failing TEST_TODO()
>> +	ok 8 - failing TEST_TODO() returns -1
>> +	# check "0" failed at t/unit-tests/t-basic.c:30
>> +	# skipping test - missing prerequisite
>> +	# skipping check ${SQ}1${SQ} at t/unit-tests/t-basic.c:32
>> +	ok 9 - test_skip() # SKIP
>> +	ok 10 - skipped test returns 0
>> +	# skipping test - missing prerequisite
>> +	ok 11 - test_skip() inside TEST_TODO() # SKIP
>> +	ok 12 - test_skip() inside TEST_TODO() returns 0
>> +	# check "0" failed at t/unit-tests/t-basic.c:48
>> +	not ok 13 - TEST_TODO() after failing check
>> +	ok 14 - TEST_TODO() after failing check returns -1
>> +	# check "0" failed at t/unit-tests/t-basic.c:56
>> +	not ok 15 - failing check after TEST_TODO()
>> +	ok 16 - failing check after TEST_TODO() returns -1
>> +	# check "!strcmp("\thello\\\\", "there\"\n")" failed at t/unit-tests/t-basic.c:61
>> +	#    left: "\011hello\\\\"
>> +	#   right: "there\"\012"
>> +	# check "!strcmp("NULL", NULL)" failed at t/unit-tests/t-basic.c:62
>> +	#    left: "NULL"
>> +	#   right: NULL
>> +	# check "${SQ}a${SQ} == ${SQ}\n${SQ}" failed at t/unit-tests/t-basic.c:63
>> +	#    left: ${SQ}a${SQ}
>> +	#   right: ${SQ}\012${SQ}
>> +	# check "${SQ}\\\\${SQ} == ${SQ}\\${SQ}${SQ}" failed at t/unit-tests/t-basic.c:64
>> +	#    left: ${SQ}\\\\${SQ}
>> +	#   right: ${SQ}\\${SQ}${SQ}
>> +	not ok 17 - messages from failing string and char comparison
>> +	# BUG: test has no checks at t/unit-tests/t-basic.c:91
>> +	not ok 18 - test with no checks
>> +	ok 19 - test with no checks returns -1
>> +	1..19
>> +	EOF
>
> Presumably t-basic will serve as a catalog of check_* functions and
> the test binary, together with this test piece, will keep growing as
> we gain features in the unit tests infrastructure.  I wonder how
> maintainable the above is, though.  When we acquire new test, we
> would need to renumber.  What if multiple developers add new
> features to the catalog at the same time?
>
>> diff --git a/t/unit-tests/.gitignore b/t/unit-tests/.gitignore
>> new file mode 100644
>> index 0000000000..e292d58348
>> --- /dev/null
>> +++ b/t/unit-tests/.gitignore
>> @@ -0,0 +1,2 @@
>> +/t-basic
>> +/t-strbuf
>
> Also, can we come up with some naming convention so that we do not
> have to keep adding to this file every time we add a new test
> script?
>
>> diff --git a/t/unit-tests/t-strbuf.c b/t/unit-tests/t-strbuf.c
>> new file mode 100644
>> index 0000000000..561611e242
>> --- /dev/null
>> +++ b/t/unit-tests/t-strbuf.c
>> @@ -0,0 +1,75 @@
>> +#include "test-lib.h"
>> +#include "strbuf.h"
>> +
>> +/* wrapper that supplies tests with an initialized strbuf */
>> +static void setup(void (*f)(struct strbuf*, void*), void *data)
>> +{
>> +	struct strbuf buf = STRBUF_INIT;
>> +
>> +	f(&buf, data);
>> +	strbuf_release(&buf);
>> +	check_uint(buf.len, ==, 0);
>> +	check_uint(buf.alloc, ==, 0);
>> +	check(buf.buf == strbuf_slopbuf);
>> +	check_char(buf.buf[0], ==, '\0');
>> +}
>
> What I am going to utter from here on are not complaints but purely
> meant as questions.  
>
> Would the resulting output and maintainability of the tests change
> (improve, or worsen) if we introduce
>
> 	static void assert_empty_strbuf(struct strbuf *buf)
> 	{
> 		check_uint(buf->len, ==, 0);
>                 check_uint(buf->alloc, ==, 0);
> 		check(buf.buf == strbuf_slopbuf);
> 		check_char(buf.buf[0], ==, '\0');
> 	}
>
> and call it from the setup() function to ensure that
> strbuf_release(&buf) it calls after running customer test f() brings
> the buffer in a reasonably initialized state?  The t_static_init()
> test should be able to say
>
> 	static void t_static_init(void)
> 	{
> 		struct strbuf buf = STRBUF_INIT;
> 		assert_empty_strbuf(&buf);
> 	}
>
> if we did so, but is that a good thing or a bad thing (e.g. it may
> make it harder to figure out where the real error came from, because
> of the "line number" thing will not easily capture the caller of the
> caller, perhaps)?  
>
>> +static void t_static_init(void)
>> +{
>> +	struct strbuf buf = STRBUF_INIT;
>> +
>> +	check_uint(buf.len, ==, 0);
>> +	check_uint(buf.alloc, ==, 0);
>> +	if (check(buf.buf == strbuf_slopbuf))
>> +		return; /* avoid de-referencing buf.buf */
>
> strbuf_slopbuf[0] is designed to be readable.  Do check() assertions
> return their parameter negated?
>
> In other words, if "we expect buf.buf to point at the slopbuf, but
> if that expectation does not hold, check() returns true and we
> refrain from doing check_char() on the next line because we cannot
> trust what buf.buf points at" is what is going on here, I find it
> very confusing.  Perhaps my intuition is failing me, but somehow I
> would have expected that passing check_foo() would return true while
> failing ones would return false.
>
> IOW I would expect
>
> 	if (check(buf.buf == strbuf_slopbuf))
> 		return;
>
> to work very similarly to
>
> 	if (buf.buf == strbuf_slopbuf)
> 		return;
>
> in expressing the control flow, simply because they are visually
> similar.  But of course, if we early-return because buf.buf that
> does not point at strbuf_slopbuf is a sign of trouble, then the
> control flow we want is
>
> 	if (buf.buf != strbuf_slopbuf)
> 		return;
>
> or
>
> 	if (!(buf.buf == strbuf_slopbuf))
> 		return;
>
> The latter is easier to translate to check_foo(), because what is
> inside the inner parentheses is the condition we expect, and we
> would like check_foo() to complain when the condition does not hold.
>
> For the "check_foo()" thing to work in a similar way, while having
> the side effect of reporting any failed expectations, we would want
> to write
>
> 	if (!check(buf.buf == strbuf_slopbuf))
> 		return;
>
> And for that similarity to work, check_foo() must return false when
> its expectation fails, and return true when its expectation holds.
>
> I think that is where my "I find it very confusing" comes from.
>
>> +	check_char(buf.buf[0], ==, '\0');
>> +}
>
>> +static void t_dynamic_init(void)
>> +{
>> +	struct strbuf buf;
>> +
>> +	strbuf_init(&buf, 1024);
>> +	check_uint(buf.len, ==, 0);
>> +	check_uint(buf.alloc, >=, 1024);
>> +	check_char(buf.buf[0], ==, '\0');
>
> Is it sensible to check buf.buf is not slopbuf at this point, or
> does it make the test TOO intimate with the current implementation
> detail?
>
>> +	strbuf_release(&buf);
>> +}
>> +
>> +static void t_addch(struct strbuf *buf, void *data)
>> +{
>> +	const char *p_ch = data;
>> +	const char ch = *p_ch;
>> +
>> +	strbuf_addch(buf, ch);
>> +	if (check_uint(buf->len, ==, 1) ||
>> +	    check_uint(buf->alloc, >, 1))
>> +		return; /* avoid de-referencing buf->buf */
>
> Again, I find the return values from these check_uint() calls highly
> confusing, if this is saying "if len is 1 and alloc is more than 1,
> then we are in an expected state and can further validate that buf[0]
> is ch and buf[1] is NULL, but otherwise we should punt".  The polarity
> looks screwy.  Perhaps it is just me?
>
>> +	check_char(buf->buf[0], ==, ch);
>> +	check_char(buf->buf[1], ==, '\0');
>> +}
>
> In any case, this t_addch() REQUIRES that incoming buf is empty,
> doesn't it?  I do not think it is sensible.  I would have expected
> that it would be more like
>
> 	t_addch(struct strbuf *buf, void *data)
> 	{
> 		char ch = *(char *)data;
> 		size_t orig_alloc = buf->alloc;
> 		size_t orig_len = buf->len;
>
> 		if (!assert_sane_strbuf(buf))
> 			return;
>                 strbuf_addch(buf, ch);
> 		if (!assert_sane_strbuf(buf))
> 			return;
> 		check_uint(buf->len, ==, orig_len + 1);
> 		check_uint(buf->alloc, >=, orig_alloc);
>                 check_char(buf->buf[buf->len - 1], ==, ch);
>                 check_char(buf->buf[buf->len], ==, '\0');
> 	}
>
> to ensure that we can add a ch to a strbuf with any existing
> contents and get a one-byte longer contents than before, with the
> last byte of the buffer becoming 'ch' and still NUL terminated.
>
> And we protect ourselves with a helper that checks if the given
> strbuf looks *sane*.
>
> 	static int assert_sane_strbuf(struct strbuf *buf)
> 	{
>         	/* can use slopbuf only when the length is 0 */
> 		if (buf->buf == strbuf_slopbuf)
>                 	return (buf->len == 0);
> 		/* everybody else must have non-NULL buffer */
> 		if (buf->buf == NULL)
> 			return 0;
>                 /* 
> 		 * alloc must be at least 1 byte larger than len
> 		 * for the terminating NUL at the end.
> 		 */
> 		return ((buf->len + 1 <= buf->alloc) &&
> 		    	(buf->buf[buf->len] == '\0'));
> 	}
>
> You can obviously use your check_foo() for the individual checks
> done in this function to get a more detailed diagnosis, but because
> I have confused myself enough by thinking about their polarity, I
> wrote this in barebones comparison instead.
Previous: Junio C HamanoNext: phillip.wood123@gmail.com
Message 20 of 67 in “unit tests: Add a project plan document”
  1. unit tests: Add a project plan documentJosh Steadmon, Jun 30, 2023
  2. Junio C HamanoJul 1, 2023
  3. Junio C HamanoJul 1, 2023
  4. unit tests: Add a project plan documentJosh Steadmon, Aug 7, 2023
  5. Phillip WoodAug 14, 2023
  6. Josh SteadmonAug 15, 2023
  7. Phillip WoodAug 17, 2023
  8. 0/3 Add unit test framework and project plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bitJosh Steadmon, Aug 16, 2023
  9. 3/3 ci: run unit tests in CIJosh Steadmon, Aug 16, 2023
  10. 1/3 unit tests: Add a project plan documentJosh Steadmon, Aug 16, 2023
  11. 2/3 unit tests: add TAP unit test frameworkJosh Steadmon, Aug 16, 2023
  12. Junio C HamanoAug 17, 2023
  13. Junio C HamanoAug 17, 2023
  14. Josh SteadmonAug 17, 2023
  15. 0/3 Add unit test framework and project planJosh Steadmon, Aug 17, 2023
  16. 3/3 ci: run unit tests in CIJosh Steadmon, Aug 17, 2023
  17. 1/3 unit tests: Add a project plan documentJosh Steadmon, Aug 17, 2023
  18. 2/3 unit tests: add TAP unit test frameworkJosh Steadmon, Aug 17, 2023
  19. Junio C HamanoAug 18, 2023
  20. Junio C HamanoSep 22, 2023
  21. phillip.wood123@gmail.comSep 24, 2023
  22. Junio C HamanoSep 25, 2023
  23. Josh SteadmonOct 6, 2023
  24. Josh SteadmonOct 9, 2023
  25. Junio C HamanoAug 17, 2023
  26. Josh SteadmonAug 24, 2023
  27. Junio C HamanoSep 13, 2023
  28. 0/3 Add unit test framework and project planJosh Steadmon, Oct 9, 2023
  29. 1/3 unit tests: Add a project plan documentJosh Steadmon, Oct 9, 2023
  30. Oswald BuddenhagenOct 10, 2023
  31. Josh SteadmonOct 11, 2023
  32. Oswald BuddenhagenOct 11, 2023
  33. Josh SteadmonNov 1, 2023
  34. Christian CouderOct 27, 2023
  35. Josh SteadmonNov 1, 2023
  36. Junio C HamanoNov 1, 2023
  37. 2/3 unit tests: add TAP unit test frameworkJosh Steadmon, Oct 9, 2023
  38. Junio C HamanoOct 11, 2023
  39. 5/3 fixup! unit tests: add TAP unit test frameworkPhillip Wood, Oct 16, 2023
  40. Junio C HamanoOct 16, 2023
  41. Josh SteadmonNov 1, 2023
  42. Junio C HamanoNov 1, 2023
  43. Josh SteadmonNov 1, 2023
  44. Junio C HamanoNov 1, 2023
  45. Christian CouderOct 27, 2023
  46. Josh SteadmonNov 1, 2023
  47. 3/3 ci: run unit tests in CIJosh Steadmon, Oct 9, 2023
  48. Junio C HamanoOct 9, 2023
  49. 0/3 CMake unit test fixupsPhillip Wood, Oct 19, 2023
  50. 1/3 fixup! cmake: also build unit testsPhillip Wood, Oct 19, 2023
  51. 2/3 fixup! artifacts-tar: when including `.dll` files, don't forget the unit-testsPhillip Wood, Oct 19, 2023
  52. 3/3 fixup! cmake: handle also unit testsPhillip Wood, Oct 19, 2023
  53. Junio C HamanoOct 19, 2023
  54. phillip.wood123@gmail.comOct 16, 2023
  55. Josh SteadmonNov 1, 2023
  56. Christian CouderOct 27, 2023
  57. 0/3 Add unit test framework and project planJosh Steadmon, Nov 1, 2023
  58. 1/3 unit tests: Add a project plan documentJosh Steadmon, Nov 1, 2023
  59. 3/3 ci: run unit tests in CIJosh Steadmon, Nov 1, 2023
  60. 2/3 unit tests: add TAP unit test frameworkJosh Steadmon, Nov 1, 2023
  61. Christian CouderNov 3, 2023
  62. Josh SteadmonNov 9, 2023
  63. 0/3 Add unit test framework and project planJosh Steadmon, Nov 9, 2023
  64. 1/3 unit tests: Add a project plan documentJosh Steadmon, Nov 9, 2023
  65. Junio C HamanoNov 9, 2023
  66. 2/3 unit tests: add TAP unit test frameworkJosh Steadmon, Nov 9, 2023
  67. 3/3 ci: run unit tests in CIJosh Steadmon, Nov 9, 2023

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.