[Tarantool-patches] [PATCH luajit 1/3] test: add unit tests for the write buffer

Sergey Kaplun skaplun at tarantool.org
Thu Nov 5 21:39:46 MSK 2026


Hi, Sergey!
Thanks for the patch and painstaking work with coverage of wbuf module!
Generally LGTM with minor suggestions below.

On 02.11.26, Sergey Bronnikov wrote:
> From: Sergey Bronnikov <sergeyb at tarantool.org>
> 
> The write buffer accumulates the profilers' output and passes it
> to the writer on flush. It has no unit tests, so cover writing of
> the data of all kinds, flushing of a full buffer, the explicit
> flush and the error reporting.
> 
> Part of tarantool/tarantool#11620

Minor: I'm not sure that this is the part of this issue. Maybe
| Relates to
is more appropriate.

> ---
>  test/tarantool-c-tests/unit-wbuf.test.c | 388 ++++++++++++++++++++++++
>  1 file changed, 388 insertions(+)
>  create mode 100644 test/tarantool-c-tests/unit-wbuf.test.c
> 
> diff --git a/test/tarantool-c-tests/unit-wbuf.test.c b/test/tarantool-c-tests/unit-wbuf.test.c
> new file mode 100644
> index 00000000..4bee6243
> --- /dev/null
> +++ b/test/tarantool-c-tests/unit-wbuf.test.c
> @@ -0,0 +1,388 @@
> +/*
> +** Testing of the write buffer.
> +**
> +** The write buffer accumulates the profilers' output and passes
> +** it to the writer on flush. The tests below cover writing of the
> +** data of all kinds, flushing on overflow, the explicit flush and
> +** the error reporting.

Minor: It is better to use
| /*
|  *
|  */

Comments here as in the most C/C++ code in the tests.
The inconsistent comment style in the <unit-leb128.test.c> is my fault
and should be fixed.

> +*/
> +
> +#include <errno.h>
> +
> +#include <stddef.h>
> +#include <stdint.h>
> +#include <string.h>
> +
> +#include "lj_wbuf.h"
> +#include "test.h"
> +
> +/* Size of the write buffer used in the tests. */
> +#define BUFFER_SIZE 10
> +
> +/* Size of the payload that does not fit into the write buffer. */
> +#define PAYLOAD_SIZE (2 * BUFFER_SIZE + 5)
> +
> +/* What the writer does on flush. */
> +enum writer_mode {
> +	/* Report all the data as written. */
> +	WMODE_OK,
> +	/* Report a half of the data as written and keep the buffer. */

Nit: Comment length is more than 66 symbols.
Here and in:
* `test_flush_partial_write()`
* `test_addbyte_failed_flush()`
* `test_addu64_failed_flush()`
* `test_addn_failed_flush()`
* `test_addn_failed_flush_in_loop()`

> +	WMODE_PARTIAL,
> +	/* Imitate an unrecoverable write error, e.g. ENOSPC. */
> +	WMODE_FAIL,
> +};
> +
> +/* Writer context for simulating different write outcomes. */
> +struct writer_ctx {
> +	/* What the writer does on flush. */
> +	enum writer_mode mode;
> +	/* Number of performed flushes. */
> +	size_t flushes;
> +	/* Number of bytes reported as written. */
> +	size_t written;
> +	/* Everything reported as written, in order of writing. */
> +	uint8_t out[2 * PAYLOAD_SIZE];

Why do we need 2x size from the PAYLOAD here? Please add the comment.

> +};
> +
> +static size_t test_writer(const void **buf_addr, size_t len, void *opt)
> +{
> +	struct writer_ctx *ctx = opt;
> +	size_t n = len;
> +
> +	ctx->flushes++;

Minor: Please add the comment that writer function is called only by
`lj_wbuf_flush()` before this increment.

> +	if (ctx->mode == WMODE_FAIL) {
> +		errno = ENOSPC;
> +		*buf_addr = NULL;
> +		return 0;
> +	}
> +	if (ctx->mode == WMODE_PARTIAL) {

Minor: s/if/else if/

> +		errno = EIO;
> +		n = len / 2;
> +	}
> +	memcpy(ctx->out + ctx->written, *buf_addr, n);
> +	ctx->written += n;
> +	return n;
> +}
> +

<snipped>

> +
> +static void assert_stream_stopped_with_error(const struct lj_wbuf *buf)

I suggest using macro instead for such helpers to preserve the line of
original assertion check:

So instead of:
| not ok 9 - test_flush_stopped_stream
|   ---
|   location:     test/tarantool-c-tests/unit-wbuf.test.c:80
|   condition_failed:     'lj_wbuf_test_flag(buf, STREAM_STOP)'

We have:
| not ok 9 - test_flush_stopped_stream
|   ---
|   location:     test/tarantool-c-tests/unit-wbuf.test.c:304
|   condition_failed:     'lj_wbuf_test_flag((&buf), STREAM_STOP)'

> +{
> +	assert_true(lj_wbuf_test_flag(buf, STREAM_STOP));
> +	assert_true(lj_wbuf_test_flag(buf, STREAM_ERRIO));
> +	assert_int_equal(lj_wbuf_errno(buf), ENOSPC);

Since this precise assertion, I suggest naming the helper function/macro
something like `assert_wbuf_stopped_enospc()`.


> +	assert_ptr_equal(buf->buf, NULL);
> +	assert_ptr_equal(buf->pos, NULL);
> +}
> +
> +static int test_addbyte(void *state)

<snipped>


> +static int test_addstring(void *state)
> +{

<snipped>

> +}
> +
> +static int test_addn_pumped_through_buffer(void *state)
> +{
> +	uint8_t mem[BUFFER_SIZE];
> +	uint8_t payload[PAYLOAD_SIZE];
> +	struct lj_wbuf buf;
> +	struct writer_ctx ctx;
> +
> +	UNUSED(state);
> +
> +	memset(payload, 'a', sizeof(payload));
> +	writer_ctx_init(&ctx, WMODE_OK);
> +	lj_wbuf_init(&buf, test_writer, &ctx, mem, sizeof(mem));
> +
> +	/*
> +	 * The payload does not fit into the buffer, so it is
> +	 * pumped through it. Two full buffers are flushed, the
> +	 * rest stays in the buffer.
> +	 */
> +	lj_wbuf_addn(&buf, payload, sizeof(payload));
> +	assert_sizet_equal(ctx.flushes, 2);

This `2` may be not very obvious.
We may declare constant in this function before the comment above:

| const int writes = PAYLOAD_SIZE / BUFFER_SIZE;

> +	assert_sizet_equal(ctx.written, 2 * BUFFER_SIZE);
> +	assert_true(memcmp(ctx.out, payload, 2 * BUFFER_SIZE) == 0);
> +	assert_sizet_equal(buf_len(&buf), PAYLOAD_SIZE - 2 * BUFFER_SIZE);

It is better to add the variable:

| const int remains = PAYLOAD_SIZE - writes * BUFFER_SIZE;

> +	assert_true(memcmp(mem, payload + 2 * BUFFER_SIZE,
> +			   PAYLOAD_SIZE - 2 * BUFFER_SIZE) == 0);
> +
> +	return TEST_EXIT_SUCCESS;
> +}
> +
> +static int test_flush_on_overflow(void *state)
> +{
> +	uint8_t mem[BUFFER_SIZE];
> +	uint8_t payload[BUFFER_SIZE];
> +	struct lj_wbuf buf;
> +	struct writer_ctx ctx;
> +
> +	UNUSED(state);
> +
> +	memset(payload, 'a', sizeof(payload));
> +	writer_ctx_init(&ctx, WMODE_OK);
> +	lj_wbuf_init(&buf, test_writer, &ctx, mem, sizeof(mem));
> +
> +	/* A byte written to a full buffer is preceded by a flush. */
> +	lj_wbuf_addn(&buf, payload, sizeof(payload));
> +	assert_sizet_equal(buf_len(&buf), sizeof(mem));
> +	lj_wbuf_addbyte(&buf, 0xaa);
> +	assert_sizet_equal(ctx.flushes, 1);
> +	assert_sizet_equal(ctx.written, sizeof(mem));
> +	assert_sizet_equal(buf_len(&buf), 1);
> +	assert_int_equal(mem[0], 0xaa);

I suggest splitting this 1 test into 3 subtests, since they have the
similar logic:
1) Init buffer.
2) Write something without flushes.
3) Write something with a flush.

It helps to avoid the specific `ctx.out` size and makes checks for
`flushes`, `written` and `buf_len()` similar.

> +
> +	/* The same for an ULEB128 value written to a full buffer. */
> +	lj_wbuf_addn(&buf, payload, sizeof(payload) - 1);
> +	assert_sizet_equal(buf_len(&buf), sizeof(mem));
> +	lj_wbuf_addu64(&buf, 0);
> +	assert_sizet_equal(ctx.flushes, 2);
> +	assert_sizet_equal(ctx.written, 2 * sizeof(mem));
> +	assert_sizet_equal(buf_len(&buf), 1);

Looks like there should be a check for the content as for the 0xaa
byte.

> +
> +	/* And for a data block written to a full buffer. */
> +	lj_wbuf_addn(&buf, payload, sizeof(payload) - 1);
> +	assert_sizet_equal(buf_len(&buf), sizeof(mem));
> +	lj_wbuf_addn(&buf, payload, 1);
> +	assert_sizet_equal(ctx.flushes, 3);
> +	assert_sizet_equal(ctx.written, 3 * sizeof(mem));
> +	assert_sizet_equal(buf_len(&buf), 1);

Looks like there should be a check for the content as for the 0xaa
byte.

> +
> +	return TEST_EXIT_SUCCESS;
> +}
> +
> +static int test_flush(void *state)

I suggest renaming this as `test_flush_and_write()` or something similar.
Otherwise, the difference between this test and the test above is
unclear.

> +{
> +	uint8_t mem[BUFFER_SIZE];
> +	struct lj_wbuf buf;
> +	struct writer_ctx ctx;
> +
> +	UNUSED(state);
> +
> +	writer_ctx_init(&ctx, WMODE_OK);
> +	lj_wbuf_init(&buf, test_writer, &ctx, mem, sizeof(mem));
> +
> +	lj_wbuf_addn(&buf, "xyz", 3);
> +	lj_wbuf_flush(&buf);
> +	assert_sizet_equal(ctx.flushes, 1);
> +	assert_sizet_equal(ctx.written, 3);
> +	assert_true(memcmp(ctx.out, "xyz", 3) == 0);
> +	/* The buffer is empty after a successful flush. */
> +	assert_sizet_equal(buf_len(&buf), 0);
> +	assert_false(lj_wbuf_test_flag(&buf, STREAM_ERRIO | STREAM_STOP));
> +
> +	/* The buffer is usable after the flush. */
> +	lj_wbuf_addbyte(&buf, 0x7f);
> +	assert_sizet_equal(buf_len(&buf), 1);
> +	assert_int_equal(mem[0], 0x7f);
> +
> +	return TEST_EXIT_SUCCESS;
> +}
> +
> +static int test_flush_partial_write(void *state)
> +{
> +	uint8_t mem[BUFFER_SIZE];
> +	struct lj_wbuf buf;
> +	struct writer_ctx ctx;
> +
> +	UNUSED(state);
> +
> +	writer_ctx_init(&ctx, WMODE_PARTIAL);
> +	lj_wbuf_init(&buf, test_writer, &ctx, mem, sizeof(mem));
> +
> +	lj_wbuf_addn(&buf, "xyz", 3);
> +	lj_wbuf_flush(&buf);
> +	/* A partial write is reported as an error. */
> +	assert_true(lj_wbuf_test_flag(&buf, STREAM_ERRIO));
> +	assert_int_equal(lj_wbuf_errno(&buf), EIO);
> +	/* The writer keeps the buffer, so the stream is not stopped. */
> +	assert_false(lj_wbuf_test_flag(&buf, STREAM_STOP));
> +	assert_ptr_equal(buf.buf, mem);
> +	/* The unwritten data is dropped. */
> +	assert_sizet_equal(buf_len(&buf), 0);

I'm not sure that we should fixate this policy. Generally there is no
guarantee what has happened with data. Except that it is not written
successfully.
Feel free to ignore.

> +
> +	return TEST_EXIT_SUCCESS;
> +}

<snipped>

> +/*
> +** A writer may release the buffer and set *data to NULL on an
> +** unrecoverable error (e.g. ENOSPC). The stream is stopped in
> +** this case, so the write functions must not touch the released
> +** buffer.
> +*/

Minor: It is better to use
| /*
|  *
|  */

Comments here as in the most C/C++ code in the tests.
The inconsistent comment style in the <unit-leb128.test.c> is my fault
and should be fixed.

> +int main(void)
> +{
> +	const struct test_unit tgroup[] = {
> +		test_unit_def(test_addbyte),
> +		test_unit_def(test_addu64),
> +		test_unit_def(test_addn),
> +		test_unit_def(test_addstring),
> +		test_unit_def(test_addn_pumped_through_buffer),
> +		test_unit_def(test_flush_on_overflow),
> +		test_unit_def(test_flush),
> +		test_unit_def(test_flush_partial_write),
> +		test_unit_def(test_flush_stopped_stream),
> +		test_unit_def(test_write_to_stopped_stream),
> +		test_unit_def(test_terminate),
> +	};
> +	const int test_result = test_run_group(tgroup, NULL);

Minor: Why do we need this additional variable?

> +
> +	return test_result;
> +}
> -- 
> 2.56.0
> 

-- 
Best regards,
Sergey Kaplun


More information about the Tarantool-patches mailing list