[PATCH 0/9] t/unit-tests: convert unit-tests to use clar

Subsystems: kernel build + files below scripts/ (unless maintained elsewhere), the rest · Maintainers: Nathan Chancellor, Nicolas Schier, Linus Torvalds

Series: 9 patches touching 19 files, +2973/-3419

STALE548d SUPERSEDED

Status: Superseded by v5 (posted 2025-07-24). [go to v5]

Revision v1 (5 revisions archived).

Revisions (5)
  1. v1 current re-posted go to this post re-posted go to this post re-posted go to this post re-posted current
  2. v2 [diff against previous] re-posted go to this post re-posted go to this post re-posted go to this post
  3. v3 [diff against previous] re-posted go to this post
  4. v4 [diff against previous]
  5. v5 [diff against previous] re-posted go to this post

10 messages, 1 author, 2025-04-27 · open the first message on its own page

[PATCH 0/9] t/unit-tests: convert unit-tests to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:54:24

Hello,

This small patch series marks the final batch of our existing unit test
files transitioned to the Clar testing framework. It covers all the
reftable-related test files, and is part of our ongoing effort to
standardize our testing framework to enhance its maintainability.

Thanks
Seyi

Signed-off-by: Seyi Kuforiji <redacted>

Seyi Kuforiji (9):
  t/unit-tests: adapt lib-reftable{c,h} helper functions to clar
  t/unit-tests: convert reftable basics test to use clar test framework
  t/unit-tests: convert reftable block test to use clar
  t/unit-tests: convert reftable merged test to use clar
  t/unit-tests: convert reftable pq test to use clar
  t/unit-tests: convert reftable reader test to use clar
  t/unit-tests: convert reftable readwrite test to use clar
  t/unit-tests: convert reftable record test to use clar
  t/unit-tests: convert reftable stack test to use clar

 Makefile                            |   20 +-
 t/meson.build                       |   21 +-
 t/unit-tests/lib-reftable.c         |   26 +-
 t/unit-tests/lib-reftable.h         |    6 +-
 t/unit-tests/t-reftable-basics.c    |  219 ----
 t/unit-tests/t-reftable-block.c     |  383 -------
 t/unit-tests/t-reftable-merged.c    |  546 ----------
 t/unit-tests/t-reftable-pq.c        |  161 ---
 t/unit-tests/t-reftable-reader.c    |   96 --
 t/unit-tests/t-reftable-readwrite.c |  985 ------------------
 t/unit-tests/t-reftable-record.c    |  585 -----------
 t/unit-tests/t-reftable-stack.c     | 1451 ---------------------------
 t/unit-tests/u-reftable-basics.c    |  195 ++++
 t/unit-tests/u-reftable-block.c     |  373 +++++++
 t/unit-tests/u-reftable-merged.c    |  515 ++++++++++
 t/unit-tests/u-reftable-pq.c        |  152 +++
 t/unit-tests/u-reftable-reader.c    |   78 ++

24 more lines

[PATCH 1/9] t/unit-tests: adapt lib-reftable{c,h} helper functions to clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:54:32

Helper functions defined in `t/unit-tests/lib-reftable.{c,h}` are
required for the reftable-related test files to run efficeintly. In the
current implementation these functions are designed to conform with our
homegrown unit-testing structure. So in other to convert the reftable
test files, there is need for a clar specific implementation of these
helper functions.

type cast `for (size_t i = 0; i < (size_t)stats->ref_stats.blocks; i++)`
Adapt functions in lib-reftable.{c,h} to use clar. These functions
conform with the clar testing framework and become available for all
reftable-related test files implemented using the clar testing
framework, which requires them. This will be used by subsequent commits.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                    |  4 ++--
 t/meson.build               |  4 ++--
 t/unit-tests/lib-reftable.c | 26 +++++++++++++-------------
 t/unit-tests/lib-reftable.h |  6 +++---
 4 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/Makefile b/Makefile
index 13f9062a05..1a0f8c30cd 100644
--- a/Makefile
+++ b/Makefile
@@ -1371,8 +1371,9 @@ CLAR_TEST_SUITES += u-urlmatch-normalization
 CLAR_TEST_PROG = $(UNIT_TEST_BIN)/unit-tests$(X)
 CLAR_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(CLAR_TEST_SUITES))
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/clar/clar.o
-CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
+CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
+CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
 UNIT_TEST_PROGRAMS += t-reftable-basics
 UNIT_TEST_PROGRAMS += t-reftable-block
@@ -1384,7 +1385,6 @@ UNIT_TEST_PROGRAMS += t-reftable-record
 UNIT_TEST_PROGRAMS += t-reftable-stack
 UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))
 UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/test-lib.o
-UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 
 # xdiff and reftable libs may in turn depend on what is in libgit.a
 GITLIBS = common-main.o $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) $(LIB_FILE)
diff --git a/t/meson.build b/t/meson.build
index bfb744e886..0102894b6f 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -18,8 +18,9 @@ clar_test_suites = [
 
 clar_sources = [
   'unit-tests/clar/clar.c',
+  'unit-tests/lib-oid.c',
+  'unit-tests/lib-reftable.c',
   'unit-tests/unit-test.c',
-  'unit-tests/lib-oid.c'
 ]
 
 clar_decls_h = custom_target(
@@ -69,7 +70,6 @@ foreach unit_test_program : unit_test_programs
   unit_test = executable(unit_test_name,
     sources: [
       'unit-tests/test-lib.c',
-      'unit-tests/lib-reftable.c',
       unit_test_program,
     ],
     dependencies: [libgit_commonmain],
diff --git a/t/unit-tests/lib-reftable.c b/t/unit-tests/lib-reftable.c
index 8a69612266..414364166f 100644
--- a/t/unit-tests/lib-reftable.c
+++ b/t/unit-tests/lib-reftable.c
@@ -1,12 +1,12 @@
 #define DISABLE_SIGN_COMPARE_WARNINGS
 
 #include "lib-reftable.h"
-#include "test-lib.h"
+#include "unit-test.h"
 #include "reftable/constants.h"
 #include "reftable/writer.h"
 #include "strbuf.h"
 
-void t_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id)
+void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id)
 {
 	memset(p, (uint8_t)i, hash_size(id));
 }
@@ -22,17 +22,17 @@ static int strbuf_writer_flush(void *arg UNUSED)
 	return 0;
 }
 
-struct reftable_writer *t_reftable_strbuf_writer(struct reftable_buf *buf,
+struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf,
 						 struct reftable_write_options *opts)
 {
 	struct reftable_writer *writer;
 	int ret = reftable_writer_new(&writer, &strbuf_writer_write, &strbuf_writer_flush,
 				      buf, opts);
-	check(!ret);
+	cl_assert(ret == 0);
 	return writer;
 }
 
-void t_reftable_write_to_buf(struct reftable_buf *buf,
+void cl_reftable_write_to_buf(struct reftable_buf *buf,
 			     struct reftable_ref_record *refs,
 			     size_t nrefs,
 			     struct reftable_log_record *logs,
@@ -64,35 +64,35 @@ void t_reftable_write_to_buf(struct reftable_buf *buf,
 			min = ui;
 	}
 
-	writer = t_reftable_strbuf_writer(buf, &opts);
+	writer = cl_reftable_strbuf_writer(buf, &opts);
 	reftable_writer_set_limits(writer, min, max);
 
 	if (nrefs) {
 		ret = reftable_writer_add_refs(writer, refs, nrefs);
-		check_int(ret, ==, 0);
+		cl_assert_equal_i(ret, 0);
 	}
 
 	if (nlogs) {
 		ret = reftable_writer_add_logs(writer, logs, nlogs);
-		check_int(ret, ==, 0);
+		cl_assert_equal_i(ret, 0);
 	}
 
 	ret = reftable_writer_close(writer);
-	check_int(ret, ==, 0);
+	cl_assert_equal_i(ret, 0);
 
 	stats = reftable_writer_stats(writer);
-	for (size_t i = 0; i < stats->ref_stats.blocks; i++) {
+	for (size_t i = 0; i < (size_t)stats->ref_stats.blocks; i++) {
 		size_t off = i * (opts.block_size ? opts.block_size
 						  : DEFAULT_BLOCK_SIZE);
 		if (!off)
 			off = header_size(opts.hash_id == REFTABLE_HASH_SHA256 ? 2 : 1);
-		check_char(buf->buf[off], ==, 'r');
+		cl_assert(buf->buf[off] == 'r');
 	}
 
 	if (nrefs)
-		check_int(stats->ref_stats.blocks, >, 0);
+		cl_assert(stats->ref_stats.blocks > 0);
 	if (nlogs)
-		check_int(stats->log_stats.blocks, >, 0);
+		cl_assert(stats->log_stats.blocks > 0);
 
 	reftable_writer_free(writer);
 }
diff --git a/t/unit-tests/lib-reftable.h b/t/unit-tests/lib-reftable.h
index e4c360fa7e..2958db5dc0 100644
--- a/t/unit-tests/lib-reftable.h
+++ b/t/unit-tests/lib-reftable.h
@@ -6,12 +6,12 @@
 
 struct reftable_buf;
 
-void t_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id);
+void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id);
 
-struct reftable_writer *t_reftable_strbuf_writer(struct reftable_buf *buf,
+struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf,
 						 struct reftable_write_options *opts);
 
-void t_reftable_write_to_buf(struct reftable_buf *buf,
+void cl_reftable_write_to_buf(struct reftable_buf *buf,
 			     struct reftable_ref_record *refs,
 			     size_t nrecords,
 			     struct reftable_log_record *logs,
-- 
2.43.0

[PATCH 2/9] t/unit-tests: convert reftable basics test to use clar test framework

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:54:37

Adapt reftable basics test file to clar by using clar assertions
where necessary.Break up test edge case to improve modularity and
clarity.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                         |   2 +-
 t/meson.build                    |   2 +-
 t/unit-tests/t-reftable-basics.c | 219 -------------------------------
 t/unit-tests/u-reftable-basics.c | 195 +++++++++++++++++++++++++++
 4 files changed, 197 insertions(+), 221 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-basics.c
 create mode 100644 t/unit-tests/u-reftable-basics.c
diff --git a/Makefile b/Makefile
index 1a0f8c30cd..07e37bd1f3 100644
--- a/Makefile
+++ b/Makefile
@@ -1362,6 +1362,7 @@ CLAR_TEST_SUITES += u-oid-array
 CLAR_TEST_SUITES += u-oidmap
 CLAR_TEST_SUITES += u-oidtree
 CLAR_TEST_SUITES += u-prio-queue
+CLAR_TEST_SUITES += u-reftable-basics
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1375,7 +1376,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-basics
 UNIT_TEST_PROGRAMS += t-reftable-block
 UNIT_TEST_PROGRAMS += t-reftable-merged
 UNIT_TEST_PROGRAMS += t-reftable-pq
diff --git a/t/meson.build b/t/meson.build
index 0102894b6f..9add1b3d8b 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -8,6 +8,7 @@ clar_test_suites = [
   'unit-tests/u-oidmap.c',
   'unit-tests/u-oidtree.c',
   'unit-tests/u-prio-queue.c',
+  'unit-tests/u-reftable-basics.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -55,7 +56,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-basics.c',
   'unit-tests/t-reftable-block.c',
   'unit-tests/t-reftable-merged.c',
   'unit-tests/t-reftable-pq.c',
diff --git a/t/unit-tests/t-reftable-basics.c b/t/unit-tests/t-reftable-basics.c
deleted file mode 100644
index c9e751e49e..0000000000
--- a/t/unit-tests/t-reftable-basics.c
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
-Copyright 2020 Google LLC
-
-Use of this source code is governed by a BSD-style
-license that can be found in the LICENSE file or at
-https://developers.google.com/open-source/licenses/bsd
-*/
-
-#include "test-lib.h"
-#include "reftable/basics.h"
-
-struct integer_needle_lesseq_args {
-	int needle;
-	int *haystack;
-};
-
-static int integer_needle_lesseq(size_t i, void *_args)
-{
-	struct integer_needle_lesseq_args *args = _args;
-	return args->needle <= args->haystack[i];
-}
-
-static void *realloc_stub(void *p UNUSED, size_t size UNUSED)
-{
-	return NULL;
-}
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	if_test ("binary search with binsearch works") {
-		int haystack[] = { 2, 4, 6, 8, 10 };
-		struct {
-			int needle;
-			size_t expected_idx;
-		} testcases[] = {
-			{-9000, 0},
-			{-1, 0},
-			{0, 0},
-			{2, 0},
-			{3, 1},
-			{4, 1},
-			{7, 3},
-			{9, 4},
-			{10, 4},
-			{11, 5},
-			{9000, 5},
-		};
-
-		for (size_t i = 0; i < ARRAY_SIZE(testcases); i++) {
-			struct integer_needle_lesseq_args args = {
-				.haystack = haystack,
-				.needle = testcases[i].needle,
-			};
-			size_t idx;
-
-			idx = binsearch(ARRAY_SIZE(haystack),
-					&integer_needle_lesseq, &args);
-			check_int(idx, ==, testcases[i].expected_idx);
-		}
-	}
-
-	if_test ("names_length returns size of a NULL-terminated string array") {
-		const char *a[] = { "a", "b", NULL };
-		check_int(names_length(a), ==, 2);
-	}
-
-	if_test ("names_equal compares NULL-terminated string arrays") {
-		const char *a[] = { "a", "b", "c", NULL };
-		const char *b[] = { "a", "b", "d", NULL };
-		const char *c[] = { "a", "b", NULL };
-
-		check(names_equal(a, a));
-		check(!names_equal(a, b));
-		check(!names_equal(a, c));
-	}
-
-	if_test ("parse_names works for basic input") {
-		char in1[] = "line\n";
-		char in2[] = "a\nb\nc";
-		char **out = parse_names(in1, strlen(in1));
-		check(out != NULL);
-		check_str(out[0], "line");
-		check(!out[1]);
-		free_names(out);
-
-		out = parse_names(in2, strlen(in2));
-		check(out != NULL);
-		check_str(out[0], "a");
-		check_str(out[1], "b");
-		check_str(out[2], "c");
-		check(!out[3]);
-		free_names(out);
-	}
-
-	if_test ("parse_names drops empty string") {
-		char in[] = "a\n\nb\n";
-		char **out = parse_names(in, strlen(in));
-		check(out != NULL);
-		check_str(out[0], "a");
-		/* simply '\n' should be dropped as empty string */
-		check_str(out[1], "b");
-		check(!out[2]);
-		free_names(out);
-	}
-
-	if_test ("common_prefix_size works") {
-		struct reftable_buf a = REFTABLE_BUF_INIT;
-		struct reftable_buf b = REFTABLE_BUF_INIT;
-		struct {
-			const char *a, *b;
-			int want;
-		} cases[] = {
-			{"abcdef", "abc", 3},
-			{ "abc", "ab", 2 },
-			{ "", "abc", 0 },
-			{ "abc", "abd", 2 },
-			{ "abc", "pqr", 0 },
-		};
-
-		for (size_t i = 0; i < ARRAY_SIZE(cases); i++) {
-			check(!reftable_buf_addstr(&a, cases[i].a));
-			check(!reftable_buf_addstr(&b, cases[i].b));
-			check_uint(common_prefix_size(&a, &b), ==, cases[i].want);
-			reftable_buf_reset(&a);
-			reftable_buf_reset(&b);
-		}
-		reftable_buf_release(&a);
-		reftable_buf_release(&b);
-	}
-
-	if_test ("reftable_put_be64 and reftable_get_be64 work") {
-		uint64_t in = 0x1122334455667788;
-		uint8_t dest[8];
-		uint64_t out;
-		reftable_put_be64(dest, in);
-		out = reftable_get_be64(dest);
-		check_int(in, ==, out);
-	}
-
-	if_test ("reftable_put_be32 and reftable_get_be32 work") {
-		uint32_t in = 0x11223344;
-		uint8_t dest[4];
-		uint32_t out;
-		reftable_put_be32(dest, in);
-		out = reftable_get_be32(dest);
-		check_int(in, ==, out);
-	}
-
-	if_test ("reftable_put_be24 and reftable_get_be24 work") {
-		uint32_t in = 0x112233;
-		uint8_t dest[3];
-		uint32_t out;
-		reftable_put_be24(dest, in);
-		out = reftable_get_be24(dest);
-		check_int(in, ==, out);
-	}
-
-	if_test ("put_be16 and get_be16 work") {
-		uint32_t in = 0xfef1;
-		uint8_t dest[3];
-		uint32_t out;
-		reftable_put_be16(dest, in);
-		out = reftable_get_be16(dest);
-		check_int(in, ==, out);
-	}
-
-	if_test ("REFTABLE_ALLOC_GROW works") {
-		int *arr = NULL, *old_arr;
-		size_t alloc = 0, old_alloc;
-
-		check(!REFTABLE_ALLOC_GROW(arr, 1, alloc));
-		check(arr != NULL);
-		check_uint(alloc, >=, 1);
-		arr[0] = 42;
-
-		old_alloc = alloc;
-		old_arr = arr;
-		reftable_set_alloc(NULL, realloc_stub, NULL);
-		check(REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc));
-		check(arr == old_arr);
-		check_uint(alloc, ==, old_alloc);
-
-		old_alloc = alloc;
-		reftable_set_alloc(NULL, NULL, NULL);
-		check(!REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc));
-		check(arr != NULL);
-		check_uint(alloc, >, old_alloc);
-		arr[alloc - 1] = 42;
-
-		reftable_free(arr);
-	}
-
-	if_test ("REFTABLE_ALLOC_GROW_OR_NULL works") {
-		int *arr = NULL;
-		size_t alloc = 0, old_alloc;
-
-		REFTABLE_ALLOC_GROW_OR_NULL(arr, 1, alloc);
-		check(arr != NULL);
-		check_uint(alloc, >=, 1);
-		arr[0] = 42;
-
-		old_alloc = alloc;
-		REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
-		check(arr != NULL);
-		check_uint(alloc, >, old_alloc);
-		arr[alloc - 1] = 42;
-
-		old_alloc = alloc;
-		reftable_set_alloc(NULL, realloc_stub, NULL);
-		REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
-		check(arr == NULL);
-		check_uint(alloc, ==, 0);
-		reftable_set_alloc(NULL, NULL, NULL);
-
-		reftable_free(arr);
-	}
-
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-basics.c b/t/unit-tests/u-reftable-basics.c
new file mode 100644
index 0000000000..63dd568faf
--- /dev/null
+++ b/t/unit-tests/u-reftable-basics.c
@@ -0,0 +1,195 @@
+/*
+Copyright 2020 Google LLC
+
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file or at
+https://developers.google.com/open-source/licenses/bsd
+*/
+
+#include "unit-test.h"
+#include "reftable/basics.h"
+
+struct integer_needle_lesseq_args {
+	int needle;
+	int *haystack;
+};
+
+static int integer_needle_lesseq(size_t i, void *_args)
+{
+	struct integer_needle_lesseq_args *args = _args;
+	return args->needle <= args->haystack[i];
+}
+
+static void *realloc_stub(void *p UNUSED, size_t size UNUSED)
+{
+	return NULL;
+}
+
+void test_reftable_basics__binsearch(void)
+{
+	int haystack[] = { 2, 4, 6, 8, 10 };
+	struct {
+		int needle;
+		size_t expected_idx;
+	} testcases[] = {
+		{-9000, 0},
+		{-1, 0},
+		{0, 0},
+		{2, 0},
+		{3, 1},
+		{4, 1},
+		{7, 3},
+		{9, 4},
+		{10, 4},
+		{11, 5},
+		{9000, 5},
+	};
+
+	for (size_t i = 0; i < ARRAY_SIZE(testcases); i++) {
+		struct integer_needle_lesseq_args args = {
+			.haystack = haystack,
+			.needle = testcases[i].needle,
+		};
+		size_t idx;
+
+		idx = binsearch(ARRAY_SIZE(haystack),
+				&integer_needle_lesseq, &args);
+		cl_assert_equal_i(idx, testcases[i].expected_idx);
+	}
+
+}
+
+void test_reftable_basics__names_length(void)
+{
+	const char *a[] = { "a", "b", NULL };
+	cl_assert_equal_i(names_length(a), 2);
+}
+
+void test_reftable_basics__names_equal(void)
+{
+	const char *a[] = { "a", "b", "c", NULL };
+	const char *b[] = { "a", "b", "d", NULL };
+	const char *c[] = { "a", "b", NULL };
+
+	cl_assert(names_equal(a, a));
+	cl_assert(!names_equal(a, b));
+	cl_assert(!names_equal(a, c));
+}
+
+void test_reftable_basics__parse_names(void)
+{
+	char in1[] = "line\n";
+	char in2[] = "a\nb\nc";
+	char **out = parse_names(in1, strlen(in1));
+	cl_assert(out != NULL);
+	cl_assert_equal_s(out[0], "line");
+	cl_assert(!out[1]);
+	free_names(out);
+
+	out = parse_names(in2, strlen(in2));
+	cl_assert(out != NULL);
+	cl_assert_equal_s(out[0], "a");
+	cl_assert_equal_s(out[1], "b");
+	cl_assert_equal_s(out[2], "c");
+	cl_assert(!out[3]);
+	free_names(out);
+}
+
+void test_reftable_basics__common_prefix_size(void)
+{
+	struct reftable_buf a = REFTABLE_BUF_INIT;
+	struct reftable_buf b = REFTABLE_BUF_INIT;
+	struct {
+		const char *a, *b;
+		int want;
+	} cases[] = {
+		{"abcdef", "abc", 3},
+		{ "abc", "ab", 2 },
+		{ "", "abc", 0 },
+		{ "abc", "abd", 2 },
+		{ "abc", "pqr", 0 },
+	};
+
+	for (size_t i = 0; i < ARRAY_SIZE(cases); i++) {
+		reftable_buf_reset(&a);
+		reftable_buf_reset(&b);
+		cl_assert_equal_i(reftable_buf_addstr(&a, cases[i].a), 0);
+		cl_assert_equal_i(reftable_buf_addstr(&b, cases[i].b), 0);
+		cl_assert_equal_i(common_prefix_size(&a, &b), cases[i].want);
+	}
+	reftable_buf_release(&a);
+	reftable_buf_release(&b);
+}
+
+void test_reftable_basics__put_get_be24(void)
+{
+	uint32_t in = 0x112233;
+	uint8_t dest[3];
+	uint32_t out;
+	reftable_put_be24(dest, in);
+	out = reftable_get_be24(dest);
+	cl_assert_equal_i(in, out);
+}
+
+void test_reftable_basics__put_get_be16(void)
+{
+	uint32_t in = 0xfef1;
+	uint8_t dest[3];
+	uint32_t out;
+	reftable_put_be16(dest, in);
+	out = reftable_get_be16(dest);
+	cl_assert_equal_i(in, out);
+}
+
+void test_reftable_basics__grow_alloc(void)
+{
+	int *arr = NULL, *old_arr;
+	size_t alloc = 0, old_alloc;
+
+	cl_assert_equal_i(REFTABLE_ALLOC_GROW(arr, 1, alloc), 0);
+	cl_assert(arr != NULL);
+	cl_assert(alloc >= 1);
+	arr[0] = 42;
+
+	old_alloc = alloc;
+	old_arr = arr;
+	reftable_set_alloc(NULL, realloc_stub, NULL);
+	cl_assert(REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc));
+	cl_assert(arr == old_arr);
+	cl_assert_equal_i(alloc, old_alloc);
+
+	old_alloc = alloc;
+	reftable_set_alloc(NULL, NULL, NULL);
+	cl_assert_equal_i(REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc), 0);
+	cl_assert(arr != NULL);
+	cl_assert(alloc > old_alloc);
+	arr[alloc - 1] = 42;
+
+	reftable_free(arr);
+}
+
+void test_reftable_basics__grow_alloc_or_null(void)
+{
+	int *arr = NULL;
+	size_t alloc = 0, old_alloc;
+
+	REFTABLE_ALLOC_GROW_OR_NULL(arr, 1, alloc);
+	cl_assert(arr != NULL);
+	cl_assert(alloc >= 1);
+	arr[0] = 42;
+
+	old_alloc = alloc;
+	REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
+	cl_assert(arr != NULL);
+	cl_assert(alloc > old_alloc);
+	arr[alloc - 1] = 42;
+
+	old_alloc = alloc;
+	reftable_set_alloc(NULL, realloc_stub, NULL);
+	REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
+	cl_assert(arr == NULL);
+	cl_assert_equal_i(alloc, 0);
+	reftable_set_alloc(NULL, NULL, NULL);
+
+	reftable_free(arr);
+}
-- 
2.43.0

[PATCH 3/9] t/unit-tests: convert reftable block test to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:54:42

Adapt reftable block test file to use clar testing framework by using
clar assertions where necessary.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                        |   2 +-
 t/meson.build                   |   2 +-
 t/unit-tests/t-reftable-block.c | 383 --------------------------------
 t/unit-tests/u-reftable-block.c | 373 +++++++++++++++++++++++++++++++
 4 files changed, 375 insertions(+), 385 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-block.c
 create mode 100644 t/unit-tests/u-reftable-block.c
diff --git a/Makefile b/Makefile
index 07e37bd1f3..3fc012f86a 100644
--- a/Makefile
+++ b/Makefile
@@ -1363,6 +1363,7 @@ CLAR_TEST_SUITES += u-oidmap
 CLAR_TEST_SUITES += u-oidtree
 CLAR_TEST_SUITES += u-prio-queue
 CLAR_TEST_SUITES += u-reftable-basics
+CLAR_TEST_SUITES += u-reftable-block
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1376,7 +1377,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-block
 UNIT_TEST_PROGRAMS += t-reftable-merged
 UNIT_TEST_PROGRAMS += t-reftable-pq
 UNIT_TEST_PROGRAMS += t-reftable-reader
diff --git a/t/meson.build b/t/meson.build
index 9add1b3d8b..eb9e69eb27 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -9,6 +9,7 @@ clar_test_suites = [
   'unit-tests/u-oidtree.c',
   'unit-tests/u-prio-queue.c',
   'unit-tests/u-reftable-basics.c',
+  'unit-tests/u-reftable-block.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -56,7 +57,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-block.c',
   'unit-tests/t-reftable-merged.c',
   'unit-tests/t-reftable-pq.c',
   'unit-tests/t-reftable-reader.c',
diff --git a/t/unit-tests/t-reftable-block.c b/t/unit-tests/t-reftable-block.c
deleted file mode 100644
index 22040aeefa..0000000000
--- a/t/unit-tests/t-reftable-block.c
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
-Copyright 2020 Google LLC
-
-Use of this source code is governed by a BSD-style
-license that can be found in the LICENSE file or at
-https://developers.google.com/open-source/licenses/bsd
-*/
-
-#include "test-lib.h"
-#include "reftable/block.h"
-#include "reftable/blocksource.h"
-#include "reftable/constants.h"
-#include "reftable/reftable-error.h"
-#include "strbuf.h"
-
-static void t_ref_block_read_write(void)
-{
-	const int header_off = 21; /* random */
-	struct reftable_record recs[30];
-	const size_t N = ARRAY_SIZE(recs);
-	const size_t block_size = 1024;
-	struct reftable_block block = { 0 };
-	struct block_writer bw = {
-		.last_key = REFTABLE_BUF_INIT,
-	};
-	struct reftable_record rec = {
-		.type = BLOCK_TYPE_REF,
-	};
-	size_t i = 0;
-	int ret;
-	struct block_reader br = { 0 };
-	struct block_iter it = BLOCK_ITER_INIT;
-	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
-
-	REFTABLE_CALLOC_ARRAY(block.data, block_size);
-	check(block.data != NULL);
-	block.len = block_size;
-	block_source_from_buf(&block.source ,&buf);
-	ret = block_writer_init(&bw, BLOCK_TYPE_REF, block.data, block_size,
-				header_off, hash_size(REFTABLE_HASH_SHA1));
-	check(!ret);
-
-	rec.u.ref.refname = (char *) "";
-	rec.u.ref.value_type = REFTABLE_REF_DELETION;
-	ret = block_writer_add(&bw, &rec);
-	check_int(ret, ==, REFTABLE_API_ERROR);
-
-	for (i = 0; i < N; i++) {
-		rec.u.ref.refname = xstrfmt("branch%02"PRIuMAX, (uintmax_t)i);
-		rec.u.ref.value_type = REFTABLE_REF_VAL1;
-		memset(rec.u.ref.value.val1, i, REFTABLE_HASH_SIZE_SHA1);
-
-		recs[i] = rec;
-		ret = block_writer_add(&bw, &rec);
-		rec.u.ref.refname = NULL;
-		rec.u.ref.value_type = REFTABLE_REF_DELETION;
-		check_int(ret, ==, 0);
-	}
-
-	ret = block_writer_finish(&bw);
-	check_int(ret, >, 0);
-
-	block_writer_release(&bw);
-
-	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
-
-	block_iter_seek_start(&it, &br);
-
-	for (i = 0; ; i++) {
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, >=, 0);
-		if (ret > 0) {
-			check_int(i, ==, N);
-			break;
-		}
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	for (i = 0; i < N; i++) {
-		block_iter_reset(&it);
-		reftable_record_key(&recs[i], &want);
-
-		ret = block_iter_seek_key(&it, &br, &want);
-		check_int(ret, ==, 0);
-
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, ==, 0);
-
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-
-		want.len--;
-		ret = block_iter_seek_key(&it, &br, &want);
-		check_int(ret, ==, 0);
-
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, ==, 0);
-		check(reftable_record_equal(&recs[10 * (i / 10)], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	block_reader_release(&br);
-	block_iter_close(&it);
-	reftable_record_release(&rec);
-	reftable_block_done(&br.block);
-	reftable_buf_release(&want);
-	reftable_buf_release(&buf);
-	for (i = 0; i < N; i++)
-		reftable_record_release(&recs[i]);
-}
-
-static void t_log_block_read_write(void)
-{
-	const int header_off = 21;
-	struct reftable_record recs[30];
-	const size_t N = ARRAY_SIZE(recs);
-	const size_t block_size = 2048;
-	struct reftable_block block = { 0 };
-	struct block_writer bw = {
-		.last_key = REFTABLE_BUF_INIT,
-	};
-	struct reftable_record rec = {
-		.type = BLOCK_TYPE_LOG,
-	};
-	size_t i = 0;
-	int ret;
-	struct block_reader br = { 0 };
-	struct block_iter it = BLOCK_ITER_INIT;
-	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
-
-	REFTABLE_CALLOC_ARRAY(block.data, block_size);
-	check(block.data != NULL);
-	block.len = block_size;
-	block_source_from_buf(&block.source ,&buf);
-	ret = block_writer_init(&bw, BLOCK_TYPE_LOG, block.data, block_size,
-				header_off, hash_size(REFTABLE_HASH_SHA1));
-	check(!ret);
-
-	for (i = 0; i < N; i++) {
-		rec.u.log.refname = xstrfmt("branch%02"PRIuMAX , (uintmax_t)i);
-		rec.u.log.update_index = i;
-		rec.u.log.value_type = REFTABLE_LOG_UPDATE;
-
-		recs[i] = rec;
-		ret = block_writer_add(&bw, &rec);
-		rec.u.log.refname = NULL;
-		rec.u.log.value_type = REFTABLE_LOG_DELETION;
-		check_int(ret, ==, 0);
-	}
-
-	ret = block_writer_finish(&bw);
-	check_int(ret, >, 0);
-
-	block_writer_release(&bw);
-
-	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
-
-	block_iter_seek_start(&it, &br);
-
-	for (i = 0; ; i++) {
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, >=, 0);
-		if (ret > 0) {
-			check_int(i, ==, N);
-			break;
-		}
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	for (i = 0; i < N; i++) {
-		block_iter_reset(&it);
-		reftable_buf_reset(&want);
-		check(!reftable_buf_addstr(&want, recs[i].u.log.refname));
-
-		ret = block_iter_seek_key(&it, &br, &want);
-		check_int(ret, ==, 0);
-
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, ==, 0);
-
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-
-		want.len--;
-		ret = block_iter_seek_key(&it, &br, &want);
-		check_int(ret, ==, 0);
-
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, ==, 0);
-		check(reftable_record_equal(&recs[10 * (i / 10)], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	block_reader_release(&br);
-	block_iter_close(&it);
-	reftable_record_release(&rec);
-	reftable_block_done(&br.block);
-	reftable_buf_release(&want);
-	reftable_buf_release(&buf);
-	for (i = 0; i < N; i++)
-		reftable_record_release(&recs[i]);
-}
-
-static void t_obj_block_read_write(void)
-{
-	const int header_off = 21;
-	struct reftable_record recs[30];
-	const size_t N = ARRAY_SIZE(recs);
-	const size_t block_size = 1024;
-	struct reftable_block block = { 0 };
-	struct block_writer bw = {
-		.last_key = REFTABLE_BUF_INIT,
-	};
-	struct reftable_record rec = {
-		.type = BLOCK_TYPE_OBJ,
-	};
-	size_t i = 0;
-	int ret;
-	struct block_reader br = { 0 };
-	struct block_iter it = BLOCK_ITER_INIT;
-	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
-
-	REFTABLE_CALLOC_ARRAY(block.data, block_size);
-	check(block.data != NULL);
-	block.len = block_size;
-	block_source_from_buf(&block.source, &buf);
-	ret = block_writer_init(&bw, BLOCK_TYPE_OBJ, block.data, block_size,
-				header_off, hash_size(REFTABLE_HASH_SHA1));
-	check(!ret);
-
-	for (i = 0; i < N; i++) {
-		uint8_t bytes[] = { i, i + 1, i + 2, i + 3, i + 5 }, *allocated;
-		DUP_ARRAY(allocated, bytes, ARRAY_SIZE(bytes));
-
-		rec.u.obj.hash_prefix = allocated;
-		rec.u.obj.hash_prefix_len = 5;
-
-		recs[i] = rec;
-		ret = block_writer_add(&bw, &rec);
-		rec.u.obj.hash_prefix = NULL;
-		rec.u.obj.hash_prefix_len = 0;
-		check_int(ret, ==, 0);
-	}
-
-	ret = block_writer_finish(&bw);
-	check_int(ret, >, 0);
-
-	block_writer_release(&bw);
-
-	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
-
-	block_iter_seek_start(&it, &br);
-
-	for (i = 0; ; i++) {
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, >=, 0);
-		if (ret > 0) {
-			check_int(i, ==, N);
-			break;
-		}
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	for (i = 0; i < N; i++) {
-		block_iter_reset(&it);
-		reftable_record_key(&recs[i], &want);
-
-		ret = block_iter_seek_key(&it, &br, &want);
-		check_int(ret, ==, 0);
-
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, ==, 0);
-
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	block_reader_release(&br);
-	block_iter_close(&it);
-	reftable_record_release(&rec);
-	reftable_block_done(&br.block);
-	reftable_buf_release(&want);
-	reftable_buf_release(&buf);
-	for (i = 0; i < N; i++)
-		reftable_record_release(&recs[i]);
-}
-
-static void t_index_block_read_write(void)
-{
-	const int header_off = 21;
-	struct reftable_record recs[30];
-	const size_t N = ARRAY_SIZE(recs);
-	const size_t block_size = 1024;
-	struct reftable_block block = { 0 };
-	struct block_writer bw = {
-		.last_key = REFTABLE_BUF_INIT,
-	};
-	struct reftable_record rec = {
-		.type = BLOCK_TYPE_INDEX,
-		.u.idx.last_key = REFTABLE_BUF_INIT,
-	};
-	size_t i = 0;
-	int ret;
-	struct block_reader br = { 0 };
-	struct block_iter it = BLOCK_ITER_INIT;
-	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
-
-	REFTABLE_CALLOC_ARRAY(block.data, block_size);
-	check(block.data != NULL);
-	block.len = block_size;
-	block_source_from_buf(&block.source, &buf);
-	ret = block_writer_init(&bw, BLOCK_TYPE_INDEX, block.data, block_size,
-				header_off, hash_size(REFTABLE_HASH_SHA1));
-	check(!ret);
-
-	for (i = 0; i < N; i++) {
-		char buf[128];
-
-		snprintf(buf, sizeof(buf), "branch%02"PRIuMAX, (uintmax_t)i);
-
-		reftable_buf_init(&recs[i].u.idx.last_key);
-		recs[i].type = BLOCK_TYPE_INDEX;
-		check(!reftable_buf_addstr(&recs[i].u.idx.last_key, buf));
-		recs[i].u.idx.offset = i;
-
-		ret = block_writer_add(&bw, &recs[i]);
-		check_int(ret, ==, 0);
-	}
-
-	ret = block_writer_finish(&bw);
-	check_int(ret, >, 0);
-
-	block_writer_release(&bw);
-
-	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
-
-	block_iter_seek_start(&it, &br);
-
-	for (i = 0; ; i++) {
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, >=, 0);
-		if (ret > 0) {
-			check_int(i, ==, N);
-			break;
-		}
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	for (i = 0; i < N; i++) {
-		block_iter_reset(&it);
-		reftable_record_key(&recs[i], &want);
-
-		ret = block_iter_seek_key(&it, &br, &want);
-		check_int(ret, ==, 0);
-
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, ==, 0);
-
-		check(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1));
-
-		want.len--;
-		ret = block_iter_seek_key(&it, &br, &want);
-		check_int(ret, ==, 0);
-
-		ret = block_iter_next(&it, &rec);
-		check_int(ret, ==, 0);
-		check(reftable_record_equal(&recs[10 * (i / 10)], &rec, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	block_reader_release(&br);
-	block_iter_close(&it);
-	reftable_record_release(&rec);
-	reftable_block_done(&br.block);
-	reftable_buf_release(&want);
-	reftable_buf_release(&buf);
-	for (i = 0; i < N; i++)
-		reftable_record_release(&recs[i]);
-}
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	TEST(t_index_block_read_write(), "read-write operations on index blocks work");
-	TEST(t_log_block_read_write(), "read-write operations on log blocks work");
-	TEST(t_obj_block_read_write(), "read-write operations on obj blocks work");
-	TEST(t_ref_block_read_write(), "read-write operations on ref blocks work");
-
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-block.c b/t/unit-tests/u-reftable-block.c
new file mode 100644
index 0000000000..af24901230
--- /dev/null
+++ b/t/unit-tests/u-reftable-block.c
@@ -0,0 +1,373 @@
+/*
+Copyright 2020 Google LLC
+
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file or at
+https://developers.google.com/open-source/licenses/bsd
+*/
+
+#include "unit-test.h"
+#include "reftable/block.h"
+#include "reftable/blocksource.h"
+#include "reftable/constants.h"
+#include "reftable/reftable-error.h"
+#include "strbuf.h"
+
+void test_reftable_block__index_read_write(void)
+{
+	const int header_off = 21; /* random */
+	struct reftable_record recs[30];
+	const size_t N = ARRAY_SIZE(recs);
+	const size_t block_size = 1024;
+	struct reftable_block block = { 0 };
+	struct block_writer bw = {
+		.last_key = REFTABLE_BUF_INIT,
+	};
+	struct reftable_record rec = {
+		.type = BLOCK_TYPE_REF,
+	};
+	size_t i = 0;
+	int ret;
+	struct block_reader br = { 0 };
+	struct block_iter it = BLOCK_ITER_INIT;
+	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
+
+	REFTABLE_CALLOC_ARRAY(block.data, block_size);
+	cl_assert(block.data != NULL);
+	block.len = block_size;
+	block_source_from_buf(&block.source ,&buf);
+	ret = block_writer_init(&bw, BLOCK_TYPE_REF, block.data, block_size,
+				header_off, hash_size(REFTABLE_HASH_SHA1));
+	cl_assert(ret == 0);
+
+	rec.u.ref.refname = (char *) "";
+	rec.u.ref.value_type = REFTABLE_REF_DELETION;
+	ret = block_writer_add(&bw, &rec);
+	cl_assert_equal_i(ret, REFTABLE_API_ERROR);
+
+	for (i = 0; i < N; i++) {
+		rec.u.ref.refname = xstrfmt("branch%02"PRIuMAX, (uintmax_t)i);
+		rec.u.ref.value_type = REFTABLE_REF_VAL1;
+		memset(rec.u.ref.value.val1, i, REFTABLE_HASH_SIZE_SHA1);
+
+		recs[i] = rec;
+		ret = block_writer_add(&bw, &rec);
+		rec.u.ref.refname = NULL;
+		rec.u.ref.value_type = REFTABLE_REF_DELETION;
+		cl_assert_equal_i(ret, 0);
+	}
+
+	ret = block_writer_finish(&bw);
+	cl_assert(ret > 0);
+
+	block_writer_release(&bw);
+
+	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
+
+	block_iter_seek_start(&it, &br);
+
+	for (i = 0; ; i++) {
+		ret = block_iter_next(&it, &rec);
+		cl_assert(ret >= 0);
+		if (ret > 0) {
+			cl_assert_equal_i(i, N);
+			break;
+		}
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	for (i = 0; i < N; i++) {
+		block_iter_reset(&it);
+		reftable_record_key(&recs[i], &want);
+
+		ret = block_iter_seek_key(&it, &br, &want);
+		cl_assert_equal_i(ret, 0);
+
+		ret = block_iter_next(&it, &rec);
+		cl_assert_equal_i(ret, 0);
+
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+
+		want.len--;
+		ret = block_iter_seek_key(&it, &br, &want);
+		cl_assert_equal_i(ret, 0);
+
+		ret = block_iter_next(&it, &rec);
+		cl_assert_equal_i(ret, 0);
+		cl_assert_equal_i(reftable_record_equal(&recs[10 * (i / 10)], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	block_reader_release(&br);
+	block_iter_close(&it);
+	reftable_record_release(&rec);
+	reftable_block_done(&br.block);
+	reftable_buf_release(&want);
+	reftable_buf_release(&buf);
+	for (i = 0; i < N; i++)
+		reftable_record_release(&recs[i]);
+}
+
+void test_reftable_block__log_read_write(void)
+{
+	const int header_off = 21;
+	struct reftable_record recs[30];
+	const size_t N = ARRAY_SIZE(recs);
+	const size_t block_size = 2048;
+	struct reftable_block block = { 0 };
+	struct block_writer bw = {
+		.last_key = REFTABLE_BUF_INIT,
+	};
+	struct reftable_record rec = {
+		.type = BLOCK_TYPE_LOG,
+	};
+	size_t i = 0;
+	int ret;
+	struct block_reader br = { 0 };
+	struct block_iter it = BLOCK_ITER_INIT;
+	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
+
+	REFTABLE_CALLOC_ARRAY(block.data, block_size);
+	cl_assert(block.data != NULL);
+	block.len = block_size;
+	block_source_from_buf(&block.source ,&buf);
+	ret = block_writer_init(&bw, BLOCK_TYPE_LOG, block.data, block_size,
+				header_off, hash_size(REFTABLE_HASH_SHA1));
+	cl_assert(ret == 0);
+
+	for (i = 0; i < N; i++) {
+		rec.u.log.refname = xstrfmt("branch%02"PRIuMAX , (uintmax_t)i);
+		rec.u.log.update_index = i;
+		rec.u.log.value_type = REFTABLE_LOG_UPDATE;
+
+		recs[i] = rec;
+		ret = block_writer_add(&bw, &rec);
+		rec.u.log.refname = NULL;
+		rec.u.log.value_type = REFTABLE_LOG_DELETION;
+		cl_assert_equal_i(ret, 0);
+	}
+
+	ret = block_writer_finish(&bw);
+	cl_assert(ret > 0);
+
+	block_writer_release(&bw);
+
+	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
+
+	block_iter_seek_start(&it, &br);
+
+	for (i = 0; ; i++) {
+		ret = block_iter_next(&it, &rec);
+		cl_assert(ret >= 0);
+		if (ret > 0) {
+			cl_assert_equal_i(i, N);
+			break;
+		}
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	for (i = 0; i < N; i++) {
+		block_iter_reset(&it);
+		reftable_buf_reset(&want);
+		cl_assert(reftable_buf_addstr(&want, recs[i].u.log.refname) == 0);
+
+		ret = block_iter_seek_key(&it, &br, &want);
+		cl_assert_equal_i(ret, 0);
+
+		ret = block_iter_next(&it, &rec);
+		cl_assert_equal_i(ret, 0);
+
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+
+		want.len--;
+		ret = block_iter_seek_key(&it, &br, &want);
+		cl_assert_equal_i(ret, 0);
+
+		ret = block_iter_next(&it, &rec);
+		cl_assert_equal_i(ret, 0);
+		cl_assert_equal_i(reftable_record_equal(&recs[10 * (i / 10)], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	block_reader_release(&br);
+	block_iter_close(&it);
+	reftable_record_release(&rec);
+	reftable_block_done(&br.block);
+	reftable_buf_release(&want);
+	reftable_buf_release(&buf);
+	for (i = 0; i < N; i++)
+		reftable_record_release(&recs[i]);
+}
+
+void test_reftable_block__obj_read_write(void)
+{
+	const int header_off = 21;
+	struct reftable_record recs[30];
+	const size_t N = ARRAY_SIZE(recs);
+	const size_t block_size = 1024;
+	struct reftable_block block = { 0 };
+	struct block_writer bw = {
+		.last_key = REFTABLE_BUF_INIT,
+	};
+	struct reftable_record rec = {
+		.type = BLOCK_TYPE_OBJ,
+	};
+	size_t i = 0;
+	int ret;
+	struct block_reader br = { 0 };
+	struct block_iter it = BLOCK_ITER_INIT;
+	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
+
+	REFTABLE_CALLOC_ARRAY(block.data, block_size);
+	cl_assert(block.data != NULL);
+	block.len = block_size;
+	block_source_from_buf(&block.source, &buf);
+	ret = block_writer_init(&bw, BLOCK_TYPE_OBJ, block.data, block_size,
+				header_off, hash_size(REFTABLE_HASH_SHA1));
+	cl_assert(ret == 0);
+
+	for (i = 0; i < N; i++) {
+		uint8_t bytes[] = { i, i + 1, i + 2, i + 3, i + 5 }, *allocated;
+		DUP_ARRAY(allocated, bytes, ARRAY_SIZE(bytes));
+
+		rec.u.obj.hash_prefix = allocated;
+		rec.u.obj.hash_prefix_len = 5;
+
+		recs[i] = rec;
+		ret = block_writer_add(&bw, &rec);
+		rec.u.obj.hash_prefix = NULL;
+		rec.u.obj.hash_prefix_len = 0;
+		cl_assert_equal_i(ret, 0);
+	}
+
+	ret = block_writer_finish(&bw);
+	cl_assert(ret > 0);
+
+	block_writer_release(&bw);
+
+	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
+
+	block_iter_seek_start(&it, &br);
+
+	for (i = 0; ; i++) {
+		ret = block_iter_next(&it, &rec);
+		cl_assert(ret >= 0);
+		if (ret > 0) {
+			cl_assert_equal_i(i, N);
+			break;
+		}
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	for (i = 0; i < N; i++) {
+		block_iter_reset(&it);
+		reftable_record_key(&recs[i], &want);
+
+		ret = block_iter_seek_key(&it, &br, &want);
+		cl_assert_equal_i(ret, 0);
+
+		ret = block_iter_next(&it, &rec);
+		cl_assert_equal_i(ret, 0);
+
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	block_reader_release(&br);
+	block_iter_close(&it);
+	reftable_record_release(&rec);
+	reftable_block_done(&br.block);
+	reftable_buf_release(&want);
+	reftable_buf_release(&buf);
+	for (i = 0; i < N; i++)
+		reftable_record_release(&recs[i]);
+}
+
+void test_reftable_block__ref_read_write(void)
+{
+	const int header_off = 21;
+	struct reftable_record recs[30];
+	const size_t N = ARRAY_SIZE(recs);
+	const size_t block_size = 1024;
+	struct reftable_block block = { 0 };
+	struct block_writer bw = {
+		.last_key = REFTABLE_BUF_INIT,
+	};
+	struct reftable_record rec = {
+		.type = BLOCK_TYPE_INDEX,
+		.u.idx.last_key = REFTABLE_BUF_INIT,
+	};
+	size_t i = 0;
+	int ret;
+	struct block_reader br = { 0 };
+	struct block_iter it = BLOCK_ITER_INIT;
+	struct reftable_buf want = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
+
+	REFTABLE_CALLOC_ARRAY(block.data, block_size);
+	cl_assert(block.data != NULL);
+	block.len = block_size;
+	block_source_from_buf(&block.source, &buf);
+	ret = block_writer_init(&bw, BLOCK_TYPE_INDEX, block.data, block_size,
+				header_off, hash_size(REFTABLE_HASH_SHA1));
+	cl_assert(ret == 0);
+
+	for (i = 0; i < N; i++) {
+		char buf[128];
+
+		snprintf(buf, sizeof(buf), "branch%02"PRIuMAX, (uintmax_t)i);
+
+		reftable_buf_init(&recs[i].u.idx.last_key);
+		recs[i].type = BLOCK_TYPE_INDEX;
+		cl_assert(reftable_buf_addstr(&recs[i].u.idx.last_key, buf) == 0);
+		recs[i].u.idx.offset = i;
+
+		ret = block_writer_add(&bw, &recs[i]);
+		cl_assert_equal_i(ret, 0);
+	}
+
+	ret = block_writer_finish(&bw);
+	cl_assert(ret > 0);
+
+	block_writer_release(&bw);
+
+	block_reader_init(&br, &block, header_off, block_size, REFTABLE_HASH_SIZE_SHA1);
+
+	block_iter_seek_start(&it, &br);
+
+	for (i = 0; ; i++) {
+		ret = block_iter_next(&it, &rec);
+		cl_assert(ret >= 0);
+		if (ret > 0) {
+			cl_assert_equal_i(i, N);
+			break;
+		}
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	for (i = 0; i < N; i++) {
+		block_iter_reset(&it);
+		reftable_record_key(&recs[i], &want);
+
+		ret = block_iter_seek_key(&it, &br, &want);
+		cl_assert_equal_i(ret, 0);
+
+		ret = block_iter_next(&it, &rec);
+		cl_assert_equal_i(ret, 0);
+
+		cl_assert_equal_i(reftable_record_equal(&recs[i], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+
+		want.len--;
+		ret = block_iter_seek_key(&it, &br, &want);
+		cl_assert_equal_i(ret, 0);
+
+		ret = block_iter_next(&it, &rec);
+		cl_assert_equal_i(ret, 0);
+		cl_assert_equal_i(reftable_record_equal(&recs[10 * (i / 10)], &rec, REFTABLE_HASH_SIZE_SHA1), 1);
+	}
+
+	block_reader_release(&br);
+	block_iter_close(&it);
+	reftable_record_release(&rec);
+	reftable_block_done(&br.block);
+	reftable_buf_release(&want);
+	reftable_buf_release(&buf);
+	for (i = 0; i < N; i++)
+		reftable_record_release(&recs[i]);
+}
-- 
2.43.0

[PATCH 4/9] t/unit-tests: convert reftable merged test to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:54:49

Adapt reftable merged test file to use clar testing framework by using
clar assertions where necessary.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                         |   2 +-
 t/meson.build                    |   2 +-
 t/unit-tests/t-reftable-merged.c | 546 -------------------------------
 t/unit-tests/u-reftable-merged.c | 515 +++++++++++++++++++++++++++++
 4 files changed, 517 insertions(+), 548 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-merged.c
 create mode 100644 t/unit-tests/u-reftable-merged.c
diff --git a/Makefile b/Makefile
index 3fc012f86a..a7a519ac81 100644
--- a/Makefile
+++ b/Makefile
@@ -1364,6 +1364,7 @@ CLAR_TEST_SUITES += u-oidtree
 CLAR_TEST_SUITES += u-prio-queue
 CLAR_TEST_SUITES += u-reftable-basics
 CLAR_TEST_SUITES += u-reftable-block
+CLAR_TEST_SUITES += u-reftable-merged
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1377,7 +1378,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-merged
 UNIT_TEST_PROGRAMS += t-reftable-pq
 UNIT_TEST_PROGRAMS += t-reftable-reader
 UNIT_TEST_PROGRAMS += t-reftable-readwrite
diff --git a/t/meson.build b/t/meson.build
index eb9e69eb27..a4210dc561 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -10,6 +10,7 @@ clar_test_suites = [
   'unit-tests/u-prio-queue.c',
   'unit-tests/u-reftable-basics.c',
   'unit-tests/u-reftable-block.c',
+  'unit-tests/u-reftable-merged.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -57,7 +58,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-merged.c',
   'unit-tests/t-reftable-pq.c',
   'unit-tests/t-reftable-reader.c',
   'unit-tests/t-reftable-readwrite.c',
diff --git a/t/unit-tests/t-reftable-merged.c b/t/unit-tests/t-reftable-merged.c
deleted file mode 100644
index 60836f80d6..0000000000
--- a/t/unit-tests/t-reftable-merged.c
+++ /dev/null
@@ -1,546 +0,0 @@
-/*
-Copyright 2020 Google LLC
-
-Use of this source code is governed by a BSD-style
-license that can be found in the LICENSE file or at
-https://developers.google.com/open-source/licenses/bsd
-*/
-
-#include "test-lib.h"
-#include "lib-reftable.h"
-#include "reftable/blocksource.h"
-#include "reftable/constants.h"
-#include "reftable/merged.h"
-#include "reftable/reader.h"
-#include "reftable/reftable-error.h"
-#include "reftable/reftable-merged.h"
-#include "reftable/reftable-writer.h"
-
-static struct reftable_merged_table *
-merged_table_from_records(struct reftable_ref_record **refs,
-			  struct reftable_block_source **source,
-			  struct reftable_reader ***readers, const size_t *sizes,
-			  struct reftable_buf *buf, const size_t n)
-{
-	struct reftable_merged_table *mt = NULL;
-	struct reftable_write_options opts = {
-		.block_size = 256,
-	};
-	int err;
-
-	REFTABLE_CALLOC_ARRAY(*readers, n);
-	check(*readers != NULL);
-	REFTABLE_CALLOC_ARRAY(*source, n);
-	check(*source != NULL);
-
-	for (size_t i = 0; i < n; i++) {
-		t_reftable_write_to_buf(&buf[i], refs[i], sizes[i], NULL, 0, &opts);
-		block_source_from_buf(&(*source)[i], &buf[i]);
-
-		err = reftable_reader_new(&(*readers)[i], &(*source)[i],
-					  "name");
-		check(!err);
-	}
-
-	err = reftable_merged_table_new(&mt, *readers, n, REFTABLE_HASH_SHA1);
-	check(!err);
-	return mt;
-}
-
-static void readers_destroy(struct reftable_reader **readers, const size_t n)
-{
-	for (size_t i = 0; i < n; i++)
-		reftable_reader_decref(readers[i]);
-	reftable_free(readers);
-}
-
-static void t_merged_single_record(void)
-{
-	struct reftable_ref_record r1[] = { {
-		.refname = (char *) "b",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_VAL1,
-		.value.val1 = { 1, 2, 3, 0 },
-	} };
-	struct reftable_ref_record r2[] = { {
-		.refname = (char *) "a",
-		.update_index = 2,
-		.value_type = REFTABLE_REF_DELETION,
-	} };
-	struct reftable_ref_record r3[] = { {
-		.refname = (char *) "c",
-		.update_index = 3,
-		.value_type = REFTABLE_REF_DELETION,
-	} };
-
-	struct reftable_ref_record *refs[] = { r1, r2, r3 };
-	size_t sizes[] = { ARRAY_SIZE(r1), ARRAY_SIZE(r2), ARRAY_SIZE(r3) };
-	struct reftable_buf bufs[3] = { REFTABLE_BUF_INIT, REFTABLE_BUF_INIT, REFTABLE_BUF_INIT };
-	struct reftable_block_source *bs = NULL;
-	struct reftable_reader **readers = NULL;
-	struct reftable_merged_table *mt =
-		merged_table_from_records(refs, &bs, &readers, sizes, bufs, 3);
-	struct reftable_ref_record ref = { 0 };
-	struct reftable_iterator it = { 0 };
-	int err;
-
-	err = merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, "a");
-	check(!err);
-
-	err = reftable_iterator_next_ref(&it, &ref);
-	check(!err);
-	check(reftable_ref_record_equal(&r2[0], &ref, REFTABLE_HASH_SIZE_SHA1));
-	reftable_ref_record_release(&ref);
-	reftable_iterator_destroy(&it);
-	readers_destroy(readers, 3);
-	reftable_merged_table_free(mt);
-	for (size_t i = 0; i < ARRAY_SIZE(bufs); i++)
-		reftable_buf_release(&bufs[i]);
-	reftable_free(bs);
-}
-
-static void t_merged_refs(void)
-{
-	struct reftable_ref_record r1[] = {
-		{
-			.refname = (char *) "a",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 1 },
-		},
-		{
-			.refname = (char *) "b",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 1 },
-		},
-		{
-			.refname = (char *) "c",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 1 },
-		}
-	};
-	struct reftable_ref_record r2[] = { {
-		.refname = (char *) "a",
-		.update_index = 2,
-		.value_type = REFTABLE_REF_DELETION,
-	} };
-	struct reftable_ref_record r3[] = {
-		{
-			.refname = (char *) "c",
-			.update_index = 3,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 2 },
-		},
-		{
-			.refname = (char *) "d",
-			.update_index = 3,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 1 },
-		},
-	};
-
-	struct reftable_ref_record *want[] = {
-		&r2[0],
-		&r1[1],
-		&r3[0],
-		&r3[1],
-	};
-
-	struct reftable_ref_record *refs[] = { r1, r2, r3 };
-	size_t sizes[3] = { ARRAY_SIZE(r1), ARRAY_SIZE(r2), ARRAY_SIZE(r3) };
-	struct reftable_buf bufs[3] = { REFTABLE_BUF_INIT, REFTABLE_BUF_INIT, REFTABLE_BUF_INIT };
-	struct reftable_block_source *bs = NULL;
-	struct reftable_reader **readers = NULL;
-	struct reftable_merged_table *mt =
-		merged_table_from_records(refs, &bs, &readers, sizes, bufs, 3);
-	struct reftable_iterator it = { 0 };
-	int err;
-	struct reftable_ref_record *out = NULL;
-	size_t len = 0;
-	size_t cap = 0;
-	size_t i;
-
-	err = merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, "a");
-	check(!err);
-	check_int(reftable_merged_table_hash_id(mt), ==, REFTABLE_HASH_SHA1);
-	check_int(reftable_merged_table_min_update_index(mt), ==, 1);
-	check_int(reftable_merged_table_max_update_index(mt), ==, 3);
-
-	while (len < 100) { /* cap loops/recursion. */
-		struct reftable_ref_record ref = { 0 };
-		int err = reftable_iterator_next_ref(&it, &ref);
-		if (err > 0)
-			break;
-
-		check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
-		out[len++] = ref;
-	}
-	reftable_iterator_destroy(&it);
-
-	check_int(ARRAY_SIZE(want), ==, len);
-	for (i = 0; i < len; i++)
-		check(reftable_ref_record_equal(want[i], &out[i],
-						 REFTABLE_HASH_SIZE_SHA1));
-	for (i = 0; i < len; i++)
-		reftable_ref_record_release(&out[i]);
-	reftable_free(out);
-
-	for (i = 0; i < 3; i++)
-		reftable_buf_release(&bufs[i]);
-	readers_destroy(readers, 3);
-	reftable_merged_table_free(mt);
-	reftable_free(bs);
-}
-
-static void t_merged_seek_multiple_times(void)
-{
-	struct reftable_ref_record r1[] = {
-		{
-			.refname = (char *) "a",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 1 },
-		},
-		{
-			.refname = (char *) "c",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 2 },
-		}
-	};
-	struct reftable_ref_record r2[] = {
-		{
-			.refname = (char *) "b",
-			.update_index = 2,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 3 },
-		},
-		{
-			.refname = (char *) "d",
-			.update_index = 2,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 4 },
-		},
-	};
-	struct reftable_ref_record *refs[] = {
-		r1, r2,
-	};
-	size_t sizes[] = {
-		ARRAY_SIZE(r1), ARRAY_SIZE(r2),
-	};
-	struct reftable_buf bufs[] = {
-		REFTABLE_BUF_INIT, REFTABLE_BUF_INIT,
-	};
-	struct reftable_block_source *sources = NULL;
-	struct reftable_reader **readers = NULL;
-	struct reftable_ref_record rec = { 0 };
-	struct reftable_iterator it = { 0 };
-	struct reftable_merged_table *mt;
-
-	mt = merged_table_from_records(refs, &sources, &readers, sizes, bufs, 2);
-	merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
-
-	for (size_t i = 0; i < 5; i++) {
-		int err = reftable_iterator_seek_ref(&it, "c");
-		check(!err);
-
-		err = reftable_iterator_next_ref(&it, &rec);
-		check(!err);
-		err = reftable_ref_record_equal(&rec, &r1[1], REFTABLE_HASH_SIZE_SHA1);
-		check(err == 1);
-
-		err = reftable_iterator_next_ref(&it, &rec);
-		check(!err);
-		err = reftable_ref_record_equal(&rec, &r2[1], REFTABLE_HASH_SIZE_SHA1);
-		check(err == 1);
-
-		err = reftable_iterator_next_ref(&it, &rec);
-		check(err > 0);
-	}
-
-	for (size_t i = 0; i < ARRAY_SIZE(bufs); i++)
-		reftable_buf_release(&bufs[i]);
-	readers_destroy(readers, ARRAY_SIZE(refs));
-	reftable_ref_record_release(&rec);
-	reftable_iterator_destroy(&it);
-	reftable_merged_table_free(mt);
-	reftable_free(sources);
-}
-
-static void t_merged_seek_multiple_times_without_draining(void)
-{
-	struct reftable_ref_record r1[] = {
-		{
-			.refname = (char *) "a",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 1 },
-		},
-		{
-			.refname = (char *) "c",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 2 },
-		}
-	};
-	struct reftable_ref_record r2[] = {
-		{
-			.refname = (char *) "b",
-			.update_index = 2,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 3 },
-		},
-		{
-			.refname = (char *) "d",
-			.update_index = 2,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 4 },
-		},
-	};
-	struct reftable_ref_record *refs[] = {
-		r1, r2,
-	};
-	size_t sizes[] = {
-		ARRAY_SIZE(r1), ARRAY_SIZE(r2),
-	};
-	struct reftable_buf bufs[] = {
-		REFTABLE_BUF_INIT, REFTABLE_BUF_INIT,
-	};
-	struct reftable_block_source *sources = NULL;
-	struct reftable_reader **readers = NULL;
-	struct reftable_ref_record rec = { 0 };
-	struct reftable_iterator it = { 0 };
-	struct reftable_merged_table *mt;
-	int err;
-
-	mt = merged_table_from_records(refs, &sources, &readers, sizes, bufs, 2);
-	merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
-
-	err = reftable_iterator_seek_ref(&it, "b");
-	check(!err);
-	err = reftable_iterator_next_ref(&it, &rec);
-	check(!err);
-	err = reftable_ref_record_equal(&rec, &r2[0], REFTABLE_HASH_SIZE_SHA1);
-	check(err == 1);
-
-	err = reftable_iterator_seek_ref(&it, "a");
-	check(!err);
-	err = reftable_iterator_next_ref(&it, &rec);
-	check(!err);
-	err = reftable_ref_record_equal(&rec, &r1[0], REFTABLE_HASH_SIZE_SHA1);
-	check(err == 1);
-
-	for (size_t i = 0; i < ARRAY_SIZE(bufs); i++)
-		reftable_buf_release(&bufs[i]);
-	readers_destroy(readers, ARRAY_SIZE(refs));
-	reftable_ref_record_release(&rec);
-	reftable_iterator_destroy(&it);
-	reftable_merged_table_free(mt);
-	reftable_free(sources);
-}
-
-static struct reftable_merged_table *
-merged_table_from_log_records(struct reftable_log_record **logs,
-			      struct reftable_block_source **source,
-			      struct reftable_reader ***readers, const size_t *sizes,
-			      struct reftable_buf *buf, const size_t n)
-{
-	struct reftable_merged_table *mt = NULL;
-	struct reftable_write_options opts = {
-		.block_size = 256,
-		.exact_log_message = 1,
-	};
-	int err;
-
-	REFTABLE_CALLOC_ARRAY(*readers, n);
-	check(*readers != NULL);
-	REFTABLE_CALLOC_ARRAY(*source, n);
-	check(*source != NULL);
-
-	for (size_t i = 0; i < n; i++) {
-		t_reftable_write_to_buf(&buf[i], NULL, 0, logs[i], sizes[i], &opts);
-		block_source_from_buf(&(*source)[i], &buf[i]);
-
-		err = reftable_reader_new(&(*readers)[i], &(*source)[i],
-					  "name");
-		check(!err);
-	}
-
-	err = reftable_merged_table_new(&mt, *readers, n, REFTABLE_HASH_SHA1);
-	check(!err);
-	return mt;
-}
-
-static void t_merged_logs(void)
-{
-	struct reftable_log_record r1[] = {
-		{
-			.refname = (char *) "a",
-			.update_index = 2,
-			.value_type = REFTABLE_LOG_UPDATE,
-			.value.update = {
-				.old_hash = { 2 },
-				/* deletion */
-				.name = (char *) "jane doe",
-				.email = (char *) "jane@invalid",
-				.message = (char *) "message2",
-			}
-		},
-		{
-			.refname = (char *) "a",
-			.update_index = 1,
-			.value_type = REFTABLE_LOG_UPDATE,
-			.value.update = {
-				.old_hash = { 1 },
-				.new_hash = { 2 },
-				.name = (char *) "jane doe",
-				.email = (char *) "jane@invalid",
-				.message = (char *) "message1",
-			}
-		},
-	};
-	struct reftable_log_record r2[] = {
-		{
-			.refname = (char *) "a",
-			.update_index = 3,
-			.value_type = REFTABLE_LOG_UPDATE,
-			.value.update = {
-				.new_hash = { 3 },
-				.name = (char *) "jane doe",
-				.email = (char *) "jane@invalid",
-				.message = (char *) "message3",
-			}
-		},
-	};
-	struct reftable_log_record r3[] = {
-		{
-			.refname = (char *) "a",
-			.update_index = 2,
-			.value_type = REFTABLE_LOG_DELETION,
-		},
-	};
-	struct reftable_log_record *want[] = {
-		&r2[0],
-		&r3[0],
-		&r1[1],
-	};
-
-	struct reftable_log_record *logs[] = { r1, r2, r3 };
-	size_t sizes[3] = { ARRAY_SIZE(r1), ARRAY_SIZE(r2), ARRAY_SIZE(r3) };
-	struct reftable_buf bufs[3] = { REFTABLE_BUF_INIT, REFTABLE_BUF_INIT, REFTABLE_BUF_INIT };
-	struct reftable_block_source *bs = NULL;
-	struct reftable_reader **readers = NULL;
-	struct reftable_merged_table *mt = merged_table_from_log_records(
-		logs, &bs, &readers, sizes, bufs, 3);
-	struct reftable_iterator it = { 0 };
-	int err;
-	struct reftable_log_record *out = NULL;
-	size_t len = 0;
-	size_t cap = 0;
-	size_t i;
-
-	err = merged_table_init_iter(mt, &it, BLOCK_TYPE_LOG);
-	check(!err);
-	err = reftable_iterator_seek_log(&it, "a");
-	check(!err);
-	check_int(reftable_merged_table_hash_id(mt), ==, REFTABLE_HASH_SHA1);
-	check_int(reftable_merged_table_min_update_index(mt), ==, 1);
-	check_int(reftable_merged_table_max_update_index(mt), ==, 3);
-
-	while (len < 100) { /* cap loops/recursion. */
-		struct reftable_log_record log = { 0 };
-		int err = reftable_iterator_next_log(&it, &log);
-		if (err > 0)
-			break;
-
-		check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
-		out[len++] = log;
-	}
-	reftable_iterator_destroy(&it);
-
-	check_int(ARRAY_SIZE(want), ==, len);
-	for (i = 0; i < len; i++)
-		check(reftable_log_record_equal(want[i], &out[i],
-						 REFTABLE_HASH_SIZE_SHA1));
-
-	err = merged_table_init_iter(mt, &it, BLOCK_TYPE_LOG);
-	check(!err);
-	err = reftable_iterator_seek_log_at(&it, "a", 2);
-	check(!err);
-	reftable_log_record_release(&out[0]);
-	err = reftable_iterator_next_log(&it, &out[0]);
-	check(!err);
-	check(reftable_log_record_equal(&out[0], &r3[0], REFTABLE_HASH_SIZE_SHA1));
-	reftable_iterator_destroy(&it);
-
-	for (i = 0; i < len; i++)
-		reftable_log_record_release(&out[i]);
-	reftable_free(out);
-
-	for (i = 0; i < 3; i++)
-		reftable_buf_release(&bufs[i]);
-	readers_destroy(readers, 3);
-	reftable_merged_table_free(mt);
-	reftable_free(bs);
-}
-
-static void t_default_write_opts(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_ref_record rec = {
-		.refname = (char *) "master",
-		.update_index = 1,
-	};
-	int err;
-	struct reftable_block_source source = { 0 };
-	uint32_t hash_id;
-	struct reftable_reader *rd = NULL;
-	struct reftable_merged_table *merged = NULL;
-
-	reftable_writer_set_limits(w, 1, 1);
-
-	err = reftable_writer_add_ref(w, &rec);
-	check(!err);
-
-	err = reftable_writer_close(w);
-	check(!err);
-	reftable_writer_free(w);
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&rd, &source, "filename");
-	check(!err);
-
-	hash_id = reftable_reader_hash_id(rd);
-	check_int(hash_id, ==, REFTABLE_HASH_SHA1);
-
-	err = reftable_merged_table_new(&merged, &rd, 1, REFTABLE_HASH_SHA256);
-	check_int(err, ==, REFTABLE_FORMAT_ERROR);
-	err = reftable_merged_table_new(&merged, &rd, 1, REFTABLE_HASH_SHA1);
-	check(!err);
-
-	reftable_reader_decref(rd);
-	reftable_merged_table_free(merged);
-	reftable_buf_release(&buf);
-}
-
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	TEST(t_default_write_opts(), "merged table with default write opts");
-	TEST(t_merged_logs(), "merged table with multiple log updates for same ref");
-	TEST(t_merged_refs(), "merged table with multiple updates to same ref");
-	TEST(t_merged_seek_multiple_times(), "merged table can seek multiple times");
-	TEST(t_merged_seek_multiple_times_without_draining(), "merged table can seek multiple times without draining");
-	TEST(t_merged_single_record(), "ref occurring in only one record can be fetched");
-
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-merged.c b/t/unit-tests/u-reftable-merged.c
new file mode 100644
index 0000000000..48c8f9f6b5
--- /dev/null
+++ b/t/unit-tests/u-reftable-merged.c
@@ -0,0 +1,515 @@
+/*
+Copyright 2020 Google LLC
+
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file or at
+https://developers.google.com/open-source/licenses/bsd
+*/
+
+#include "unit-test.h"
+#include "lib-reftable.h"
+#include "reftable/blocksource.h"
+#include "reftable/constants.h"
+#include "reftable/merged.h"
+#include "reftable/reader.h"
+#include "reftable/reftable-error.h"
+#include "reftable/reftable-merged.h"
+#include "reftable/reftable-writer.h"
+
+static struct reftable_merged_table *
+merged_table_from_records(struct reftable_ref_record **refs,
+			  struct reftable_block_source **source,
+			  struct reftable_reader ***readers, const size_t *sizes,
+			  struct reftable_buf *buf, const size_t n)
+{
+	struct reftable_merged_table *mt = NULL;
+	struct reftable_write_options opts = {
+		.block_size = 256,
+	};
+	int err;
+
+	REFTABLE_CALLOC_ARRAY(*readers, n);
+	cl_assert(*readers != NULL);
+	REFTABLE_CALLOC_ARRAY(*source, n);
+	cl_assert(*source != NULL);
+
+	for (size_t i = 0; i < n; i++) {
+		cl_reftable_write_to_buf(&buf[i], refs[i], sizes[i], NULL, 0, &opts);
+		block_source_from_buf(&(*source)[i], &buf[i]);
+
+		err = reftable_reader_new(&(*readers)[i], &(*source)[i],
+					  "name");
+		cl_assert(err == 0);
+	}
+
+	err = reftable_merged_table_new(&mt, *readers, n, REFTABLE_HASH_SHA1);
+	cl_assert(err == 0);
+	return mt;
+}
+
+static void readers_destroy(struct reftable_reader **readers, const size_t n)
+{
+	for (size_t i = 0; i < n; i++)
+		reftable_reader_decref(readers[i]);
+	reftable_free(readers);
+}
+
+void test_reftable_merged__merged_single_record(void)
+{
+	struct reftable_ref_record r1[] = { {
+		.refname = (char *) "b",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_VAL1,
+		.value.val1 = { 1, 2, 3, 0 },
+	} };
+	struct reftable_ref_record r2[] = { {
+		.refname = (char *) "a",
+		.update_index = 2,
+		.value_type = REFTABLE_REF_DELETION,
+	} };
+	struct reftable_ref_record r3[] = { {
+		.refname = (char *) "c",
+		.update_index = 3,
+		.value_type = REFTABLE_REF_DELETION,
+	} };
+
+	struct reftable_ref_record *refs[] = { r1, r2, r3 };
+	size_t sizes[] = { ARRAY_SIZE(r1), ARRAY_SIZE(r2), ARRAY_SIZE(r3) };
+	struct reftable_buf bufs[3] = { REFTABLE_BUF_INIT, REFTABLE_BUF_INIT, REFTABLE_BUF_INIT };
+	struct reftable_block_source *bs = NULL;
+	struct reftable_reader **readers = NULL;
+	struct reftable_merged_table *mt =
+		merged_table_from_records(refs, &bs, &readers, sizes, bufs, 3);
+	struct reftable_ref_record ref = { 0 };
+	struct reftable_iterator it = { 0 };
+	int err;
+
+	err = merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
+	cl_assert(err == 0);
+	err = reftable_iterator_seek_ref(&it, "a");
+	cl_assert(err == 0);
+
+	err = reftable_iterator_next_ref(&it, &ref);
+	cl_assert(err == 0);
+	cl_assert(reftable_ref_record_equal(&r2[0], &ref, REFTABLE_HASH_SIZE_SHA1) != 0);
+	reftable_ref_record_release(&ref);
+	reftable_iterator_destroy(&it);
+	readers_destroy(readers, 3);
+	reftable_merged_table_free(mt);
+	for (size_t i = 0; i < ARRAY_SIZE(bufs); i++)
+		reftable_buf_release(&bufs[i]);
+	reftable_free(bs);
+}
+
+void test_reftable_merged__merged_refs(void)
+{
+	struct reftable_ref_record r1[] = {
+		{
+			.refname = (char *) "a",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 1 },
+		},
+		{
+			.refname = (char *) "b",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 1 },
+		},
+		{
+			.refname = (char *) "c",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 1 },
+		}
+	};
+	struct reftable_ref_record r2[] = { {
+		.refname = (char *) "a",
+		.update_index = 2,
+		.value_type = REFTABLE_REF_DELETION,
+	} };
+	struct reftable_ref_record r3[] = {
+		{
+			.refname = (char *) "c",
+			.update_index = 3,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 2 },
+		},
+		{
+			.refname = (char *) "d",
+			.update_index = 3,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 1 },
+		},
+	};
+
+	struct reftable_ref_record *want[] = {
+		&r2[0],
+		&r1[1],
+		&r3[0],
+		&r3[1],
+	};
+
+	struct reftable_ref_record *refs[] = { r1, r2, r3 };
+	size_t sizes[3] = { ARRAY_SIZE(r1), ARRAY_SIZE(r2), ARRAY_SIZE(r3) };
+	struct reftable_buf bufs[3] = { REFTABLE_BUF_INIT, REFTABLE_BUF_INIT, REFTABLE_BUF_INIT };
+	struct reftable_block_source *bs = NULL;
+	struct reftable_reader **readers = NULL;
+	struct reftable_merged_table *mt =
+		merged_table_from_records(refs, &bs, &readers, sizes, bufs, 3);
+	struct reftable_iterator it = { 0 };
+	int err;
+	struct reftable_ref_record *out = NULL;
+	size_t len = 0;
+	size_t cap = 0;
+	size_t i;
+
+	err = merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
+	cl_assert(err == 0);
+	err = reftable_iterator_seek_ref(&it, "a");
+	cl_assert(err == 0);
+	cl_assert_equal_i(reftable_merged_table_hash_id(mt), REFTABLE_HASH_SHA1);
+	cl_assert_equal_i(reftable_merged_table_min_update_index(mt), 1);
+	cl_assert_equal_i(reftable_merged_table_max_update_index(mt), 3);
+
+	while (len < 100) { /* cap loops/recursion. */
+		struct reftable_ref_record ref = { 0 };
+		int err = reftable_iterator_next_ref(&it, &ref);
+		if (err > 0)
+			break;
+
+		cl_assert(REFTABLE_ALLOC_GROW(out, len + 1, cap) == 0);
+		out[len++] = ref;
+	}
+	reftable_iterator_destroy(&it);
+
+	cl_assert_equal_i(ARRAY_SIZE(want), len);
+	for (i = 0; i < len; i++)
+		cl_assert(reftable_ref_record_equal(want[i], &out[i],
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+	for (i = 0; i < len; i++)
+		reftable_ref_record_release(&out[i]);
+	reftable_free(out);
+
+	for (i = 0; i < 3; i++)
+		reftable_buf_release(&bufs[i]);
+	readers_destroy(readers, 3);
+	reftable_merged_table_free(mt);
+	reftable_free(bs);
+}
+
+void test_reftable_merged__merged_seek_multiple_times(void)
+{
+	struct reftable_ref_record r1[] = {
+		{
+			.refname = (char *) "a",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 1 },
+		},
+		{
+			.refname = (char *) "c",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 2 },
+		}
+	};
+	struct reftable_ref_record r2[] = {
+		{
+			.refname = (char *) "b",
+			.update_index = 2,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 3 },
+		},
+		{
+			.refname = (char *) "d",
+			.update_index = 2,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 4 },
+		},
+	};
+	struct reftable_ref_record *refs[] = {
+		r1, r2,
+	};
+	size_t sizes[] = {
+		ARRAY_SIZE(r1), ARRAY_SIZE(r2),
+	};
+	struct reftable_buf bufs[] = {
+		REFTABLE_BUF_INIT, REFTABLE_BUF_INIT,
+	};
+	struct reftable_block_source *sources = NULL;
+	struct reftable_reader **readers = NULL;
+	struct reftable_ref_record rec = { 0 };
+	struct reftable_iterator it = { 0 };
+	struct reftable_merged_table *mt;
+
+	mt = merged_table_from_records(refs, &sources, &readers, sizes, bufs, 2);
+	merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
+
+	for (size_t i = 0; i < 5; i++) {
+		int err = reftable_iterator_seek_ref(&it, "c");
+		cl_assert(err == 0);
+
+		cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+		cl_assert_equal_i(reftable_ref_record_equal(&rec, &r1[1],
+													REFTABLE_HASH_SIZE_SHA1), 1);
+
+		cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+		cl_assert_equal_i(reftable_ref_record_equal(&rec, &r2[1],
+													REFTABLE_HASH_SIZE_SHA1), 1);
+
+		cl_assert(reftable_iterator_next_ref(&it, &rec) > 0);
+	}
+
+	for (size_t i = 0; i < ARRAY_SIZE(bufs); i++)
+		reftable_buf_release(&bufs[i]);
+	readers_destroy(readers, ARRAY_SIZE(refs));
+	reftable_ref_record_release(&rec);
+	reftable_iterator_destroy(&it);
+	reftable_merged_table_free(mt);
+	reftable_free(sources);
+}
+
+void test_reftable_merged__merged_seek_multiple_times_no_drain(void)
+{
+	struct reftable_ref_record r1[] = {
+		{
+			.refname = (char *) "a",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 1 },
+		},
+		{
+			.refname = (char *) "c",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 2 },
+		}
+	};
+	struct reftable_ref_record r2[] = {
+		{
+			.refname = (char *) "b",
+			.update_index = 2,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 3 },
+		},
+		{
+			.refname = (char *) "d",
+			.update_index = 2,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 4 },
+		},
+	};
+	struct reftable_ref_record *refs[] = {
+		r1, r2,
+	};
+	size_t sizes[] = {
+		ARRAY_SIZE(r1), ARRAY_SIZE(r2),
+	};
+	struct reftable_buf bufs[] = {
+		REFTABLE_BUF_INIT, REFTABLE_BUF_INIT,
+	};
+	struct reftable_block_source *sources = NULL;
+	struct reftable_reader **readers = NULL;
+	struct reftable_ref_record rec = { 0 };
+	struct reftable_iterator it = { 0 };
+	struct reftable_merged_table *mt;
+
+	mt = merged_table_from_records(refs, &sources, &readers, sizes, bufs, 2);
+	merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
+
+	cl_assert(reftable_iterator_seek_ref(&it, "b") == 0);
+	cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+	cl_assert_equal_i(reftable_ref_record_equal(&rec, &r2[0],
+												REFTABLE_HASH_SIZE_SHA1), 1);
+
+	cl_assert(reftable_iterator_seek_ref(&it, "a") == 0);
+	cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+	cl_assert_equal_i(reftable_ref_record_equal(&rec, &r1[0],
+												REFTABLE_HASH_SIZE_SHA1), 1);
+
+	for (size_t i = 0; i < ARRAY_SIZE(bufs); i++)
+		reftable_buf_release(&bufs[i]);
+	readers_destroy(readers, ARRAY_SIZE(refs));
+	reftable_ref_record_release(&rec);
+	reftable_iterator_destroy(&it);
+	reftable_merged_table_free(mt);
+	reftable_free(sources);
+}
+
+static struct reftable_merged_table *
+merged_table_from_log_records(struct reftable_log_record **logs,
+			      struct reftable_block_source **source,
+			      struct reftable_reader ***readers, const size_t *sizes,
+			      struct reftable_buf *buf, const size_t n)
+{
+	struct reftable_merged_table *mt = NULL;
+	struct reftable_write_options opts = {
+		.block_size = 256,
+		.exact_log_message = 1,
+	};
+	int err;
+
+	REFTABLE_CALLOC_ARRAY(*readers, n);
+	cl_assert(*readers != NULL);
+	REFTABLE_CALLOC_ARRAY(*source, n);
+	cl_assert(*source != NULL);
+
+	for (size_t i = 0; i < n; i++) {
+		cl_reftable_write_to_buf(&buf[i], NULL, 0, logs[i], sizes[i], &opts);
+		block_source_from_buf(&(*source)[i], &buf[i]);
+
+		err = reftable_reader_new(&(*readers)[i], &(*source)[i],
+					  "name");
+		cl_assert(err == 0);
+	}
+
+	cl_assert(reftable_merged_table_new(&mt, *readers, n, REFTABLE_HASH_SHA1) == 0);
+	return mt;
+}
+
+void test_reftable_merged__merged_logs(void)
+{
+	struct reftable_log_record r1[] = {
+		{
+			.refname = (char *) "a",
+			.update_index = 2,
+			.value_type = REFTABLE_LOG_UPDATE,
+			.value.update = {
+				.old_hash = { 2 },
+				/* deletion */
+				.name = (char *) "jane doe",
+				.email = (char *) "jane@invalid",
+				.message = (char *) "message2",
+			}
+		},
+		{
+			.refname = (char *) "a",
+			.update_index = 1,
+			.value_type = REFTABLE_LOG_UPDATE,
+			.value.update = {
+				.old_hash = { 1 },
+				.new_hash = { 2 },
+				.name = (char *) "jane doe",
+				.email = (char *) "jane@invalid",
+				.message = (char *) "message1",
+			}
+		},
+	};
+	struct reftable_log_record r2[] = {
+		{
+			.refname = (char *) "a",
+			.update_index = 3,
+			.value_type = REFTABLE_LOG_UPDATE,
+			.value.update = {
+				.new_hash = { 3 },
+				.name = (char *) "jane doe",
+				.email = (char *) "jane@invalid",
+				.message = (char *) "message3",
+			}
+		},
+	};
+	struct reftable_log_record r3[] = {
+		{
+			.refname = (char *) "a",
+			.update_index = 2,
+			.value_type = REFTABLE_LOG_DELETION,
+		},
+	};
+	struct reftable_log_record *want[] = {
+		&r2[0],
+		&r3[0],
+		&r1[1],
+	};
+
+	struct reftable_log_record *logs[] = { r1, r2, r3 };
+	size_t sizes[3] = { ARRAY_SIZE(r1), ARRAY_SIZE(r2), ARRAY_SIZE(r3) };
+	struct reftable_buf bufs[3] = { REFTABLE_BUF_INIT, REFTABLE_BUF_INIT, REFTABLE_BUF_INIT };
+	struct reftable_block_source *bs = NULL;
+	struct reftable_reader **readers = NULL;
+	struct reftable_merged_table *mt = merged_table_from_log_records(
+		logs, &bs, &readers, sizes, bufs, 3);
+	struct reftable_iterator it = { 0 };
+	struct reftable_log_record *out = NULL;
+	size_t len = 0;
+	size_t cap = 0;
+	size_t i;
+
+	cl_assert(merged_table_init_iter(mt, &it, BLOCK_TYPE_LOG) == 0);
+	cl_assert(reftable_iterator_seek_log(&it, "a") == 0);
+	cl_assert_equal_i(reftable_merged_table_hash_id(mt), REFTABLE_HASH_SHA1);
+	cl_assert_equal_i(reftable_merged_table_min_update_index(mt), 1);
+	cl_assert_equal_i(reftable_merged_table_max_update_index(mt), 3);
+
+	while (len < 100) { /* cap loops/recursion. */
+		struct reftable_log_record log = { 0 };
+		int err = reftable_iterator_next_log(&it, &log);
+		if (err > 0)
+			break;
+
+		cl_assert(REFTABLE_ALLOC_GROW(out, len + 1, cap) == 0);
+		out[len++] = log;
+	}
+	reftable_iterator_destroy(&it);
+
+	cl_assert_equal_i(ARRAY_SIZE(want), len);
+	for (i = 0; i < len; i++)
+		cl_assert(reftable_log_record_equal(want[i], &out[i],
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+
+	cl_assert(merged_table_init_iter(mt, &it, BLOCK_TYPE_LOG) == 0);
+	cl_assert(reftable_iterator_seek_log_at(&it, "a", 2) == 0);
+	reftable_log_record_release(&out[0]);
+	cl_assert(reftable_iterator_next_log(&it, &out[0]) == 0);
+	cl_assert(reftable_log_record_equal(&out[0], &r3[0],
+										REFTABLE_HASH_SIZE_SHA1) != 0);
+	reftable_iterator_destroy(&it);
+
+	for (i = 0; i < len; i++)
+		reftable_log_record_release(&out[i]);
+	reftable_free(out);
+
+	for (i = 0; i < 3; i++)
+		reftable_buf_release(&bufs[i]);
+	readers_destroy(readers, 3);
+	reftable_merged_table_free(mt);
+	reftable_free(bs);
+}
+
+void test_reftable_merged__default_write_opts(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_ref_record rec = {
+		.refname = (char *) "master",
+		.update_index = 1,
+	};
+	int err;
+	struct reftable_block_source source = { 0 };
+	uint32_t hash_id;
+	struct reftable_reader *rd = NULL;
+	struct reftable_merged_table *merged = NULL;
+
+	reftable_writer_set_limits(w, 1, 1);
+
+	cl_assert(reftable_writer_add_ref(w, &rec) == 0);
+
+	cl_assert(reftable_writer_close(w) == 0);
+	reftable_writer_free(w);
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&rd, &source, "filename") == 0);
+
+	hash_id = reftable_reader_hash_id(rd);
+	cl_assert_equal_i(hash_id, REFTABLE_HASH_SHA1);
+
+	err = reftable_merged_table_new(&merged, &rd, 1, REFTABLE_HASH_SHA256);
+	cl_assert_equal_i(err, REFTABLE_FORMAT_ERROR);
+	cl_assert(reftable_merged_table_new(&merged, &rd, 1, REFTABLE_HASH_SHA1) == 0);
+
+	reftable_reader_decref(rd);
+	reftable_merged_table_free(merged);
+	reftable_buf_release(&buf);
+}
-- 
2.43.0

[PATCH 5/9] t/unit-tests: convert reftable pq test to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:54:55

Adapt reftable priority queue test file to use clar by using clar
assertions where necessary.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                     |   2 +-
 t/meson.build                |   2 +-
 t/unit-tests/t-reftable-pq.c | 161 -----------------------------------
 t/unit-tests/u-reftable-pq.c | 152 +++++++++++++++++++++++++++++++++
 4 files changed, 154 insertions(+), 163 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-pq.c
 create mode 100644 t/unit-tests/u-reftable-pq.c
diff --git a/Makefile b/Makefile
index a7a519ac81..060d89df7f 100644
--- a/Makefile
+++ b/Makefile
@@ -1365,6 +1365,7 @@ CLAR_TEST_SUITES += u-prio-queue
 CLAR_TEST_SUITES += u-reftable-basics
 CLAR_TEST_SUITES += u-reftable-block
 CLAR_TEST_SUITES += u-reftable-merged
+CLAR_TEST_SUITES += u-reftable-pq
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1378,7 +1379,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-pq
 UNIT_TEST_PROGRAMS += t-reftable-reader
 UNIT_TEST_PROGRAMS += t-reftable-readwrite
 UNIT_TEST_PROGRAMS += t-reftable-record
diff --git a/t/meson.build b/t/meson.build
index a4210dc561..1e12bd1b96 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -11,6 +11,7 @@ clar_test_suites = [
   'unit-tests/u-reftable-basics.c',
   'unit-tests/u-reftable-block.c',
   'unit-tests/u-reftable-merged.c',
+  'unit-tests/u-reftable-pq.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -58,7 +59,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-pq.c',
   'unit-tests/t-reftable-reader.c',
   'unit-tests/t-reftable-readwrite.c',
   'unit-tests/t-reftable-record.c',
diff --git a/t/unit-tests/t-reftable-pq.c b/t/unit-tests/t-reftable-pq.c
deleted file mode 100644
index c128fe8616..0000000000
--- a/t/unit-tests/t-reftable-pq.c
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
-Copyright 2020 Google LLC
-
-Use of this source code is governed by a BSD-style
-license that can be found in the LICENSE file or at
-https://developers.google.com/open-source/licenses/bsd
-*/
-
-#include "test-lib.h"
-#include "reftable/constants.h"
-#include "reftable/pq.h"
-#include "strbuf.h"
-
-static void merged_iter_pqueue_check(const struct merged_iter_pqueue *pq)
-{
-	for (size_t i = 1; i < pq->len; i++) {
-		size_t parent = (i - 1) / 2;
-		check(pq_less(&pq->heap[parent], &pq->heap[i]));
-	}
-}
-
-static int pq_entry_equal(struct pq_entry *a, struct pq_entry *b)
-{
-	int cmp;
-	check(!reftable_record_cmp(a->rec, b->rec, &cmp));
-	return !cmp && (a->index == b->index);
-}
-
-static void t_pq_record(void)
-{
-	struct merged_iter_pqueue pq = { 0 };
-	struct reftable_record recs[54];
-	size_t N = ARRAY_SIZE(recs) - 1, i;
-	char *last = NULL;
-
-	for (i = 0; i < N; i++) {
-		check(!reftable_record_init(&recs[i], BLOCK_TYPE_REF));
-		recs[i].u.ref.refname = xstrfmt("%02"PRIuMAX, (uintmax_t)i);
-	}
-
-	i = 1;
-	do {
-		struct pq_entry e = {
-			.rec = &recs[i],
-		};
-
-		merged_iter_pqueue_add(&pq, &e);
-		merged_iter_pqueue_check(&pq);
-		i = (i * 7) % N;
-	} while (i != 1);
-
-	while (!merged_iter_pqueue_is_empty(pq)) {
-		struct pq_entry top = merged_iter_pqueue_top(pq);
-		struct pq_entry e;
-
-		check(!merged_iter_pqueue_remove(&pq, &e));
-		merged_iter_pqueue_check(&pq);
-
-		check(pq_entry_equal(&top, &e));
-		check(reftable_record_type(e.rec) == BLOCK_TYPE_REF);
-		if (last)
-			check_int(strcmp(last, e.rec->u.ref.refname), <, 0);
-		last = e.rec->u.ref.refname;
-	}
-
-	for (i = 0; i < N; i++)
-		reftable_record_release(&recs[i]);
-	merged_iter_pqueue_release(&pq);
-}
-
-static void t_pq_index(void)
-{
-	struct merged_iter_pqueue pq = { 0 };
-	struct reftable_record recs[13];
-	char *last = NULL;
-	size_t N = ARRAY_SIZE(recs), i;
-
-	for (i = 0; i < N; i++) {
-		check(!reftable_record_init(&recs[i], BLOCK_TYPE_REF));
-		recs[i].u.ref.refname = (char *) "refs/heads/master";
-	}
-
-	i = 1;
-	do {
-		struct pq_entry e = {
-			.rec = &recs[i],
-			.index = i,
-		};
-
-		merged_iter_pqueue_add(&pq, &e);
-		merged_iter_pqueue_check(&pq);
-		i = (i * 7) % N;
-	} while (i != 1);
-
-	for (i = N - 1; i > 0; i--) {
-		struct pq_entry top = merged_iter_pqueue_top(pq);
-		struct pq_entry e;
-
-		check(!merged_iter_pqueue_remove(&pq, &e));
-		merged_iter_pqueue_check(&pq);
-
-		check(pq_entry_equal(&top, &e));
-		check(reftable_record_type(e.rec) == BLOCK_TYPE_REF);
-		check_int(e.index, ==, i);
-		if (last)
-			check_str(last, e.rec->u.ref.refname);
-		last = e.rec->u.ref.refname;
-	}
-
-	merged_iter_pqueue_release(&pq);
-}
-
-static void t_merged_iter_pqueue_top(void)
-{
-	struct merged_iter_pqueue pq = { 0 };
-	struct reftable_record recs[13];
-	size_t N = ARRAY_SIZE(recs), i;
-
-	for (i = 0; i < N; i++) {
-		check(!reftable_record_init(&recs[i], BLOCK_TYPE_REF));
-		recs[i].u.ref.refname = (char *) "refs/heads/master";
-	}
-
-	i = 1;
-	do {
-		struct pq_entry e = {
-			.rec = &recs[i],
-			.index = i,
-		};
-
-		merged_iter_pqueue_add(&pq, &e);
-		merged_iter_pqueue_check(&pq);
-		i = (i * 7) % N;
-	} while (i != 1);
-
-	for (i = N - 1; i > 0; i--) {
-		struct pq_entry top = merged_iter_pqueue_top(pq);
-		struct pq_entry e;
-
-		check(!merged_iter_pqueue_remove(&pq, &e));
-
-		merged_iter_pqueue_check(&pq);
-		check(pq_entry_equal(&top, &e));
-		check(reftable_record_equal(top.rec, &recs[i], REFTABLE_HASH_SIZE_SHA1));
-		for (size_t j = 0; i < pq.len; j++) {
-			check(pq_less(&top, &pq.heap[j]));
-			check_int(top.index, >, j);
-		}
-	}
-
-	merged_iter_pqueue_release(&pq);
-}
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	TEST(t_pq_record(), "pq works with record-based comparison");
-	TEST(t_pq_index(), "pq works with index-based comparison");
-	TEST(t_merged_iter_pqueue_top(), "merged_iter_pqueue_top works");
-
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-pq.c b/t/unit-tests/u-reftable-pq.c
new file mode 100644
index 0000000000..ecbf08586c
--- /dev/null
+++ b/t/unit-tests/u-reftable-pq.c
@@ -0,0 +1,152 @@
+/*
+Copyright 2020 Google LLC
+
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file or at
+https://developers.google.com/open-source/licenses/bsd
+*/
+
+#include "unit-test.h"
+#include "reftable/constants.h"
+#include "reftable/pq.h"
+#include "strbuf.h"
+
+static void merged_iter_pqueue_check(const struct merged_iter_pqueue *pq)
+{
+	for (size_t i = 1; i < pq->len; i++) {
+		size_t parent = (i - 1) / 2;
+		cl_assert(pq_less(&pq->heap[parent], &pq->heap[i]) != 0);
+	}
+}
+
+static int pq_entry_equal(struct pq_entry *a, struct pq_entry *b)
+{
+	int cmp;
+	cl_assert(reftable_record_cmp(a->rec, b->rec, &cmp) == 0);
+	return !cmp && (a->index == b->index);
+}
+
+void test_reftable_pq__record(void)
+{
+	struct merged_iter_pqueue pq = { 0 };
+	struct reftable_record recs[54];
+	size_t N = ARRAY_SIZE(recs) - 1, i;
+	char *last = NULL;
+
+	for (i = 0; i < N; i++) {
+		cl_assert(reftable_record_init(&recs[i], BLOCK_TYPE_REF) == 0);
+		recs[i].u.ref.refname = xstrfmt("%02"PRIuMAX, (uintmax_t)i);
+	}
+
+	i = 1;
+	do {
+		struct pq_entry e = {
+			.rec = &recs[i],
+		};
+
+		merged_iter_pqueue_add(&pq, &e);
+		merged_iter_pqueue_check(&pq);
+		i = (i * 7) % N;
+	} while (i != 1);
+
+	while (!merged_iter_pqueue_is_empty(pq)) {
+		struct pq_entry top = merged_iter_pqueue_top(pq);
+		struct pq_entry e;
+
+		cl_assert(merged_iter_pqueue_remove(&pq, &e) == 0);
+		merged_iter_pqueue_check(&pq);
+
+		cl_assert(pq_entry_equal(&top, &e) != 0);
+		cl_assert(reftable_record_type(e.rec) == BLOCK_TYPE_REF);
+		if (last)
+			cl_assert(strcmp(last, e.rec->u.ref.refname) < 0);
+		last = e.rec->u.ref.refname;
+	}
+
+	for (i = 0; i < N; i++)
+		reftable_record_release(&recs[i]);
+	merged_iter_pqueue_release(&pq);
+}
+
+void test_reftable_pq__index(void)
+{
+	struct merged_iter_pqueue pq = { 0 };
+	struct reftable_record recs[13];
+	char *last = NULL;
+	size_t N = ARRAY_SIZE(recs), i;
+
+	for (i = 0; i < N; i++) {
+		cl_assert(reftable_record_init(&recs[i], BLOCK_TYPE_REF) == 0);
+		recs[i].u.ref.refname = (char *) "refs/heads/master";
+	}
+
+	i = 1;
+	do {
+		struct pq_entry e = {
+			.rec = &recs[i],
+			.index = i,
+		};
+
+		merged_iter_pqueue_add(&pq, &e);
+		merged_iter_pqueue_check(&pq);
+		i = (i * 7) % N;
+	} while (i != 1);
+
+	for (i = N - 1; i > 0; i--) {
+		struct pq_entry top = merged_iter_pqueue_top(pq);
+		struct pq_entry e;
+
+		cl_assert(merged_iter_pqueue_remove(&pq, &e) == 0);
+		merged_iter_pqueue_check(&pq);
+
+		cl_assert(pq_entry_equal(&top, &e) != 0);
+		cl_assert(reftable_record_type(e.rec) == BLOCK_TYPE_REF);
+		cl_assert_equal_i(e.index, i);
+		if (last)
+			cl_assert_equal_s(last, e.rec->u.ref.refname);
+		last = e.rec->u.ref.refname;
+	}
+
+	merged_iter_pqueue_release(&pq);
+}
+
+void test_reftable_pq__merged_iter_pqueue_top(void)
+{
+	struct merged_iter_pqueue pq = { 0 };
+	struct reftable_record recs[13];
+	size_t N = ARRAY_SIZE(recs), i;
+
+	for (i = 0; i < N; i++) {
+		cl_assert(reftable_record_init(&recs[i], BLOCK_TYPE_REF) == 0);
+		recs[i].u.ref.refname = (char *) "refs/heads/master";
+	}
+
+	i = 1;
+	do {
+		struct pq_entry e = {
+			.rec = &recs[i],
+			.index = i,
+		};
+
+		merged_iter_pqueue_add(&pq, &e);
+		merged_iter_pqueue_check(&pq);
+		i = (i * 7) % N;
+	} while (i != 1);
+
+	for (i = N - 1; i > 0; i--) {
+		struct pq_entry top = merged_iter_pqueue_top(pq);
+		struct pq_entry e;
+
+		cl_assert(merged_iter_pqueue_remove(&pq, &e) == 0);
+
+		merged_iter_pqueue_check(&pq);
+		cl_assert(pq_entry_equal(&top, &e) != 0);
+		cl_assert(reftable_record_equal(top.rec, &recs[i], REFTABLE_HASH_SIZE_SHA1) != 0);
+		for (size_t j = 0; i < pq.len; j++) {
+			cl_assert(pq_less(&top, &pq.heap[j]) != 0);
+			cl_assert(top.index > j);
+		}
+	}
+
+	merged_iter_pqueue_release(&pq);
+}
-- 
2.43.0

[PATCH 6/9] t/unit-tests: convert reftable reader test to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:55:00

Adapt reftable reader test file to use clar by using clar assertions
where necessary.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                         |  2 +-
 t/meson.build                    |  2 +-
 t/unit-tests/t-reftable-reader.c | 96 --------------------------------
 t/unit-tests/u-reftable-reader.c | 78 ++++++++++++++++++++++++++
 4 files changed, 80 insertions(+), 98 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-reader.c
 create mode 100644 t/unit-tests/u-reftable-reader.c
diff --git a/Makefile b/Makefile
index 060d89df7f..948eff1c5c 100644
--- a/Makefile
+++ b/Makefile
@@ -1366,6 +1366,7 @@ CLAR_TEST_SUITES += u-reftable-basics
 CLAR_TEST_SUITES += u-reftable-block
 CLAR_TEST_SUITES += u-reftable-merged
 CLAR_TEST_SUITES += u-reftable-pq
+CLAR_TEST_SUITES += u-reftable-reader
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1379,7 +1380,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-reader
 UNIT_TEST_PROGRAMS += t-reftable-readwrite
 UNIT_TEST_PROGRAMS += t-reftable-record
 UNIT_TEST_PROGRAMS += t-reftable-stack
diff --git a/t/meson.build b/t/meson.build
index 1e12bd1b96..43d52c4753 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -12,6 +12,7 @@ clar_test_suites = [
   'unit-tests/u-reftable-block.c',
   'unit-tests/u-reftable-merged.c',
   'unit-tests/u-reftable-pq.c',
+  'unit-tests/u-reftable-reader.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -59,7 +60,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-reader.c',
   'unit-tests/t-reftable-readwrite.c',
   'unit-tests/t-reftable-record.c',
   'unit-tests/t-reftable-stack.c',
diff --git a/t/unit-tests/t-reftable-reader.c b/t/unit-tests/t-reftable-reader.c
deleted file mode 100644
index 546df6005e..0000000000
--- a/t/unit-tests/t-reftable-reader.c
+++ /dev/null
@@ -1,96 +0,0 @@
-#include "test-lib.h"
-#include "lib-reftable.h"
-#include "reftable/blocksource.h"
-#include "reftable/reader.h"
-
-static int t_reader_seek_once(void)
-{
-	struct reftable_ref_record records[] = {
-		{
-			.refname = (char *) "refs/heads/main",
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 42 },
-		},
-	};
-	struct reftable_block_source source = { 0 };
-	struct reftable_ref_record ref = { 0 };
-	struct reftable_iterator it = { 0 };
-	struct reftable_reader *reader;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	int ret;
-
-	t_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0, NULL);
-	block_source_from_buf(&source, &buf);
-
-	ret = reftable_reader_new(&reader, &source, "name");
-	check(!ret);
-
-	reftable_reader_init_ref_iterator(reader, &it);
-	ret = reftable_iterator_seek_ref(&it, "");
-	check(!ret);
-	ret = reftable_iterator_next_ref(&it, &ref);
-	check(!ret);
-
-	ret = reftable_ref_record_equal(&ref, &records[0], REFTABLE_HASH_SIZE_SHA1);
-	check_int(ret, ==, 1);
-
-	ret = reftable_iterator_next_ref(&it, &ref);
-	check_int(ret, ==, 1);
-
-	reftable_ref_record_release(&ref);
-	reftable_iterator_destroy(&it);
-	reftable_reader_decref(reader);
-	reftable_buf_release(&buf);
-	return 0;
-}
-
-static int t_reader_reseek(void)
-{
-	struct reftable_ref_record records[] = {
-		{
-			.refname = (char *) "refs/heads/main",
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { 42 },
-		},
-	};
-	struct reftable_block_source source = { 0 };
-	struct reftable_ref_record ref = { 0 };
-	struct reftable_iterator it = { 0 };
-	struct reftable_reader *reader;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	int ret;
-
-	t_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0, NULL);
-	block_source_from_buf(&source, &buf);
-
-	ret = reftable_reader_new(&reader, &source, "name");
-	check(!ret);
-
-	reftable_reader_init_ref_iterator(reader, &it);
-
-	for (size_t i = 0; i < 5; i++) {
-		ret = reftable_iterator_seek_ref(&it, "");
-		check(!ret);
-		ret = reftable_iterator_next_ref(&it, &ref);
-		check(!ret);
-
-		ret = reftable_ref_record_equal(&ref, &records[0], REFTABLE_HASH_SIZE_SHA1);
-		check_int(ret, ==, 1);
-
-		ret = reftable_iterator_next_ref(&it, &ref);
-		check_int(ret, ==, 1);
-	}
-
-	reftable_ref_record_release(&ref);
-	reftable_iterator_destroy(&it);
-	reftable_reader_decref(reader);
-	reftable_buf_release(&buf);
-	return 0;
-}
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	TEST(t_reader_seek_once(), "reader can seek once");
-	TEST(t_reader_reseek(), "reader can reseek multiple times");
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-reader.c b/t/unit-tests/u-reftable-reader.c
new file mode 100644
index 0000000000..6c35063105
--- /dev/null
+++ b/t/unit-tests/u-reftable-reader.c
@@ -0,0 +1,78 @@
+#include "unit-test.h"
+#include "lib-reftable.h"
+#include "reftable/blocksource.h"
+#include "reftable/reader.h"
+
+void test_reftable_reader__seek_once(void)
+{
+	struct reftable_ref_record records[] = {
+		{
+			.refname = (char *) "refs/heads/main",
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 42 },
+		},
+	};
+	struct reftable_block_source source = { 0 };
+	struct reftable_ref_record ref = { 0 };
+	struct reftable_iterator it = { 0 };
+	struct reftable_reader *reader;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+
+	cl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0, NULL);
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "name") == 0);
+
+
+	reftable_reader_init_ref_iterator(reader, &it);
+	cl_assert(reftable_iterator_seek_ref(&it, "") == 0);
+	cl_assert(reftable_iterator_next_ref(&it, &ref) == 0);
+
+	cl_assert_equal_i(reftable_ref_record_equal(&ref, &records[0],
+												REFTABLE_HASH_SIZE_SHA1), 1);
+
+	cl_assert_equal_i(reftable_iterator_next_ref(&it, &ref), 1);
+
+	reftable_ref_record_release(&ref);
+	reftable_iterator_destroy(&it);
+	reftable_reader_decref(reader);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_reader__reseek(void)
+{
+	struct reftable_ref_record records[] = {
+		{
+			.refname = (char *) "refs/heads/main",
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { 42 },
+		},
+	};
+	struct reftable_block_source source = { 0 };
+	struct reftable_ref_record ref = { 0 };
+	struct reftable_iterator it = { 0 };
+	struct reftable_reader *reader;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+
+	cl_reftable_write_to_buf(&buf, records, ARRAY_SIZE(records), NULL, 0, NULL);
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "name") == 0);
+
+	reftable_reader_init_ref_iterator(reader, &it);
+
+	for (size_t i = 0; i < 5; i++) {
+		cl_assert(reftable_iterator_seek_ref(&it, "") == 0);
+		cl_assert(reftable_iterator_next_ref(&it, &ref) == 0);
+
+		cl_assert_equal_i(reftable_ref_record_equal(&ref, &records[0],
+													REFTABLE_HASH_SIZE_SHA1), 1);
+
+		cl_assert_equal_i(reftable_iterator_next_ref(&it, &ref), 1);
+	}
+
+	reftable_ref_record_release(&ref);
+	reftable_iterator_destroy(&it);
+	reftable_reader_decref(reader);
+	reftable_buf_release(&buf);
+}
-- 
2.43.0

[PATCH 7/9] t/unit-tests: convert reftable readwrite test to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:55:06

Adapt reftable readwrite test file to use clar by using clar assertions
where necessary.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                            |   2 +-
 t/meson.build                       |   2 +-
 t/unit-tests/t-reftable-readwrite.c | 985 ----------------------------
 t/unit-tests/u-reftable-readwrite.c | 870 ++++++++++++++++++++++++
 4 files changed, 872 insertions(+), 987 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-readwrite.c
 create mode 100644 t/unit-tests/u-reftable-readwrite.c
diff --git a/Makefile b/Makefile
index 948eff1c5c..7511959baf 100644
--- a/Makefile
+++ b/Makefile
@@ -1367,6 +1367,7 @@ CLAR_TEST_SUITES += u-reftable-block
 CLAR_TEST_SUITES += u-reftable-merged
 CLAR_TEST_SUITES += u-reftable-pq
 CLAR_TEST_SUITES += u-reftable-reader
+CLAR_TEST_SUITES += u-reftable-readwrite
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1380,7 +1381,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-readwrite
 UNIT_TEST_PROGRAMS += t-reftable-record
 UNIT_TEST_PROGRAMS += t-reftable-stack
 UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))
diff --git a/t/meson.build b/t/meson.build
index 43d52c4753..b9e9648f7c 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -13,6 +13,7 @@ clar_test_suites = [
   'unit-tests/u-reftable-merged.c',
   'unit-tests/u-reftable-pq.c',
   'unit-tests/u-reftable-reader.c',
+  'unit-tests/u-reftable-readwrite.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -60,7 +61,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-readwrite.c',
   'unit-tests/t-reftable-record.c',
   'unit-tests/t-reftable-stack.c',
 ]
diff --git a/t/unit-tests/t-reftable-readwrite.c b/t/unit-tests/t-reftable-readwrite.c
deleted file mode 100644
index c9626831da..0000000000
--- a/t/unit-tests/t-reftable-readwrite.c
+++ /dev/null
@@ -1,985 +0,0 @@
-/*
-Copyright 2020 Google LLC
-
-Use of this source code is governed by a BSD-style
-license that can be found in the LICENSE file or at
-https://developers.google.com/open-source/licenses/bsd
-*/
-
-#define DISABLE_SIGN_COMPARE_WARNINGS
-
-#include "test-lib.h"
-#include "lib-reftable.h"
-#include "reftable/basics.h"
-#include "reftable/blocksource.h"
-#include "reftable/reader.h"
-#include "reftable/reftable-error.h"
-#include "reftable/reftable-writer.h"
-#include "strbuf.h"
-
-static const int update_index = 5;
-
-static void t_buffer(void)
-{
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_block_source source = { 0 };
-	struct reftable_block out = { 0 };
-	int n;
-	uint8_t in[] = "hello";
-	check(!reftable_buf_add(&buf, in, sizeof(in)));
-	block_source_from_buf(&source, &buf);
-	check_int(block_source_size(&source), ==, 6);
-	n = block_source_read_block(&source, &out, 0, sizeof(in));
-	check_int(n, ==, sizeof(in));
-	check(!memcmp(in, out.data, n));
-	reftable_block_done(&out);
-
-	n = block_source_read_block(&source, &out, 1, 2);
-	check_int(n, ==, 2);
-	check(!memcmp(out.data, "el", 2));
-
-	reftable_block_done(&out);
-	block_source_close(&source);
-	reftable_buf_release(&buf);
-}
-
-static void write_table(char ***names, struct reftable_buf *buf, int N,
-			int block_size, enum reftable_hash hash_id)
-{
-	struct reftable_write_options opts = {
-		.block_size = block_size,
-		.hash_id = hash_id,
-	};
-	struct reftable_ref_record *refs;
-	struct reftable_log_record *logs;
-	int i;
-
-	REFTABLE_CALLOC_ARRAY(*names, N + 1);
-	check(*names != NULL);
-	REFTABLE_CALLOC_ARRAY(refs, N);
-	check(refs != NULL);
-	REFTABLE_CALLOC_ARRAY(logs, N);
-	check(logs != NULL);
-
-	for (i = 0; i < N; i++) {
-		refs[i].refname = (*names)[i] = xstrfmt("refs/heads/branch%02d", i);
-		refs[i].update_index = update_index;
-		refs[i].value_type = REFTABLE_REF_VAL1;
-		t_reftable_set_hash(refs[i].value.val1, i, REFTABLE_HASH_SHA1);
-	}
-
-	for (i = 0; i < N; i++) {
-		logs[i].refname = (*names)[i];
-		logs[i].update_index = update_index;
-		logs[i].value_type = REFTABLE_LOG_UPDATE;
-		t_reftable_set_hash(logs[i].value.update.new_hash, i,
-				    REFTABLE_HASH_SHA1);
-		logs[i].value.update.message = (char *) "message";
-	}
-
-	t_reftable_write_to_buf(buf, refs, N, logs, N, &opts);
-
-	reftable_free(refs);
-	reftable_free(logs);
-}
-
-static void t_log_buffer_size(void)
-{
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_write_options opts = {
-		.block_size = 4096,
-	};
-	int err;
-	int i;
-	struct reftable_log_record
-		log = { .refname = (char *) "refs/heads/master",
-			.update_index = update_index,
-			.value_type = REFTABLE_LOG_UPDATE,
-			.value = { .update = {
-					   .name = (char *) "Han-Wen Nienhuys",
-					   .email = (char *) "hanwen@google.com",
-					   .tz_offset = 100,
-					   .time = 0x5e430672,
-					   .message = (char *) "commit: 9\n",
-				   } } };
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-
-	/* This tests buffer extension for log compression. Must use a random
-	   hash, to ensure that the compressed part is larger than the original.
-	*/
-	for (i = 0; i < REFTABLE_HASH_SIZE_SHA1; i++) {
-		log.value.update.old_hash[i] = (uint8_t)(git_rand(0) % 256);
-		log.value.update.new_hash[i] = (uint8_t)(git_rand(0) % 256);
-	}
-	reftable_writer_set_limits(w, update_index, update_index);
-	err = reftable_writer_add_log(w, &log);
-	check(!err);
-	err = reftable_writer_close(w);
-	check(!err);
-	reftable_writer_free(w);
-	reftable_buf_release(&buf);
-}
-
-static void t_log_overflow(void)
-{
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	char msg[256] = { 0 };
-	struct reftable_write_options opts = {
-		.block_size = ARRAY_SIZE(msg),
-	};
-	int err;
-	struct reftable_log_record log = {
-		.refname = (char *) "refs/heads/master",
-		.update_index = update_index,
-		.value_type = REFTABLE_LOG_UPDATE,
-		.value = {
-			.update = {
-				.old_hash = { 1 },
-				.new_hash = { 2 },
-				.name = (char *) "Han-Wen Nienhuys",
-				.email = (char *) "hanwen@google.com",
-				.tz_offset = 100,
-				.time = 0x5e430672,
-				.message = msg,
-			},
-		},
-	};
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-
-	memset(msg, 'x', sizeof(msg) - 1);
-	reftable_writer_set_limits(w, update_index, update_index);
-	err = reftable_writer_add_log(w, &log);
-	check_int(err, ==, REFTABLE_ENTRY_TOO_BIG_ERROR);
-	reftable_writer_free(w);
-	reftable_buf_release(&buf);
-}
-
-static void t_log_write_limits(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_log_record log = {
-		.refname = (char *)"refs/head/master",
-		.update_index = 0,
-		.value_type = REFTABLE_LOG_UPDATE,
-		.value = {
-			.update = {
-				.old_hash = { 1 },
-				.new_hash = { 2 },
-				.name = (char *)"Han-Wen Nienhuys",
-				.email = (char *)"hanwen@google.com",
-				.tz_offset = 100,
-				.time = 0x5e430672,
-			},
-		},
-	};
-	int err;
-
-	reftable_writer_set_limits(w, 1, 1);
-
-	/* write with update_index (0) below set limits (1, 1) */
-	err = reftable_writer_add_log(w, &log);
-	check_int(err, ==, 0);
-
-	/* write with update_index (1) in the set limits (1, 1) */
-	log.update_index = 1;
-	err = reftable_writer_add_log(w, &log);
-	check_int(err, ==, 0);
-
-	/* write with update_index (3) above set limits (1, 1) */
-	log.update_index = 3;
-	err = reftable_writer_add_log(w, &log);
-	check_int(err, ==, REFTABLE_API_ERROR);
-
-	reftable_writer_free(w);
-	reftable_buf_release(&buf);
-}
-
-static void t_log_write_read(void)
-{
-	struct reftable_write_options opts = {
-		.block_size = 256,
-	};
-	struct reftable_ref_record ref = { 0 };
-	struct reftable_log_record log = { 0 };
-	struct reftable_iterator it = { 0 };
-	struct reftable_reader *reader;
-	struct reftable_block_source source = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	const struct reftable_stats *stats = NULL;
-	int N = 2, err, i, n;
-	char **names;
-
-	names = reftable_calloc(N + 1, sizeof(*names));
-	check(names != NULL);
-
-	reftable_writer_set_limits(w, 0, N);
-
-	for (i = 0; i < N; i++) {
-		char name[256];
-		struct reftable_ref_record ref = { 0 };
-		snprintf(name, sizeof(name), "b%02d%0*d", i, 130, 7);
-		names[i] = xstrdup(name);
-		ref.refname = name;
-		ref.update_index = i;
-
-		err = reftable_writer_add_ref(w, &ref);
-		check(!err);
-	}
-
-	for (i = 0; i < N; i++) {
-		struct reftable_log_record log = { 0 };
-
-		log.refname = names[i];
-		log.update_index = i;
-		log.value_type = REFTABLE_LOG_UPDATE;
-		t_reftable_set_hash(log.value.update.old_hash, i,
-				    REFTABLE_HASH_SHA1);
-		t_reftable_set_hash(log.value.update.new_hash, i + 1,
-				    REFTABLE_HASH_SHA1);
-
-		err = reftable_writer_add_log(w, &log);
-		check(!err);
-	}
-
-	n = reftable_writer_close(w);
-	check_int(n, ==, 0);
-
-	stats = reftable_writer_stats(w);
-	check_int(stats->log_stats.blocks, >, 0);
-	reftable_writer_free(w);
-	w = NULL;
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&reader, &source, "file.log");
-	check(!err);
-
-	err = reftable_reader_init_ref_iterator(reader, &it);
-	check(!err);
-
-	err = reftable_iterator_seek_ref(&it, names[N - 1]);
-	check(!err);
-
-	err = reftable_iterator_next_ref(&it, &ref);
-	check(!err);
-
-	/* end of iteration. */
-	err = reftable_iterator_next_ref(&it, &ref);
-	check_int(err, >, 0);
-
-	reftable_iterator_destroy(&it);
-	reftable_ref_record_release(&ref);
-
-	err = reftable_reader_init_log_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_log(&it, "");
-	check(!err);
-
-	for (i = 0; ; i++) {
-		int err = reftable_iterator_next_log(&it, &log);
-		if (err > 0)
-			break;
-		check(!err);
-		check_str(names[i], log.refname);
-		check_int(i, ==, log.update_index);
-		reftable_log_record_release(&log);
-	}
-
-	check_int(i, ==, N);
-	reftable_iterator_destroy(&it);
-
-	/* cleanup. */
-	reftable_buf_release(&buf);
-	free_names(names);
-	reftable_reader_decref(reader);
-}
-
-static void t_log_zlib_corruption(void)
-{
-	struct reftable_write_options opts = {
-		.block_size = 256,
-	};
-	struct reftable_iterator it = { 0 };
-	struct reftable_reader *reader;
-	struct reftable_block_source source = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	const struct reftable_stats *stats = NULL;
-	char message[100] = { 0 };
-	int err, i, n;
-	struct reftable_log_record log = {
-		.refname = (char *) "refname",
-		.value_type = REFTABLE_LOG_UPDATE,
-		.value = {
-			.update = {
-				.new_hash = { 1 },
-				.old_hash = { 2 },
-				.name = (char *) "My Name",
-				.email = (char *) "myname@invalid",
-				.message = message,
-			},
-		},
-	};
-
-	for (i = 0; i < sizeof(message) - 1; i++)
-		message[i] = (uint8_t)(git_rand(0) % 64 + ' ');
-
-	reftable_writer_set_limits(w, 1, 1);
-
-	err = reftable_writer_add_log(w, &log);
-	check(!err);
-
-	n = reftable_writer_close(w);
-	check_int(n, ==, 0);
-
-	stats = reftable_writer_stats(w);
-	check_int(stats->log_stats.blocks, >, 0);
-	reftable_writer_free(w);
-	w = NULL;
-
-	/* corrupt the data. */
-	buf.buf[50] ^= 0x99;
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&reader, &source, "file.log");
-	check(!err);
-
-	err = reftable_reader_init_log_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_log(&it, "refname");
-	check_int(err, ==, REFTABLE_ZLIB_ERROR);
-
-	reftable_iterator_destroy(&it);
-
-	/* cleanup. */
-	reftable_reader_decref(reader);
-	reftable_buf_release(&buf);
-}
-
-static void t_table_read_write_sequential(void)
-{
-	char **names;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	int N = 50;
-	struct reftable_iterator it = { 0 };
-	struct reftable_block_source source = { 0 };
-	struct reftable_reader *reader;
-	int err = 0;
-	int j = 0;
-
-	write_table(&names, &buf, N, 256, REFTABLE_HASH_SHA1);
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&reader, &source, "file.ref");
-	check(!err);
-
-	err = reftable_reader_init_ref_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, "");
-	check(!err);
-
-	for (j = 0; ; j++) {
-		struct reftable_ref_record ref = { 0 };
-		int r = reftable_iterator_next_ref(&it, &ref);
-		check_int(r, >=, 0);
-		if (r > 0)
-			break;
-		check_str(names[j], ref.refname);
-		check_int(update_index, ==, ref.update_index);
-		reftable_ref_record_release(&ref);
-	}
-	check_int(j, ==, N);
-
-	reftable_iterator_destroy(&it);
-	reftable_reader_decref(reader);
-	reftable_buf_release(&buf);
-	free_names(names);
-}
-
-static void t_table_write_small_table(void)
-{
-	char **names;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	int N = 1;
-	write_table(&names, &buf, N, 4096, REFTABLE_HASH_SHA1);
-	check_int(buf.len, <, 200);
-	reftable_buf_release(&buf);
-	free_names(names);
-}
-
-static void t_table_read_api(void)
-{
-	char **names;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	int N = 50;
-	struct reftable_reader *reader;
-	struct reftable_block_source source = { 0 };
-	int err;
-	struct reftable_log_record log = { 0 };
-	struct reftable_iterator it = { 0 };
-
-	write_table(&names, &buf, N, 256, REFTABLE_HASH_SHA1);
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&reader, &source, "file.ref");
-	check(!err);
-
-	err = reftable_reader_init_ref_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, names[0]);
-	check(!err);
-
-	err = reftable_iterator_next_log(&it, &log);
-	check_int(err, ==, REFTABLE_API_ERROR);
-
-	reftable_buf_release(&buf);
-	free_names(names);
-	reftable_iterator_destroy(&it);
-	reftable_reader_decref(reader);
-	reftable_buf_release(&buf);
-}
-
-static void t_table_read_write_seek(int index, enum reftable_hash hash_id)
-{
-	char **names;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	int N = 50;
-	struct reftable_reader *reader;
-	struct reftable_block_source source = { 0 };
-	int err;
-	int i = 0;
-
-	struct reftable_iterator it = { 0 };
-	struct reftable_buf pastLast = REFTABLE_BUF_INIT;
-	struct reftable_ref_record ref = { 0 };
-
-	write_table(&names, &buf, N, 256, hash_id);
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&reader, &source, "file.ref");
-	check(!err);
-	check_int(hash_id, ==, reftable_reader_hash_id(reader));
-
-	if (!index) {
-		reader->ref_offsets.index_offset = 0;
-	} else {
-		check_int(reader->ref_offsets.index_offset, >, 0);
-	}
-
-	for (i = 1; i < N; i++) {
-		err = reftable_reader_init_ref_iterator(reader, &it);
-		check(!err);
-		err = reftable_iterator_seek_ref(&it, names[i]);
-		check(!err);
-		err = reftable_iterator_next_ref(&it, &ref);
-		check(!err);
-		check_str(names[i], ref.refname);
-		check_int(REFTABLE_REF_VAL1, ==, ref.value_type);
-		check_int(i, ==, ref.value.val1[0]);
-
-		reftable_ref_record_release(&ref);
-		reftable_iterator_destroy(&it);
-	}
-
-	check(!reftable_buf_addstr(&pastLast, names[N - 1]));
-	check(!reftable_buf_addstr(&pastLast, "/"));
-
-	err = reftable_reader_init_ref_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, pastLast.buf);
-	if (err == 0) {
-		struct reftable_ref_record ref = { 0 };
-		int err = reftable_iterator_next_ref(&it, &ref);
-		check_int(err, >, 0);
-	} else {
-		check_int(err, >, 0);
-	}
-
-	reftable_buf_release(&pastLast);
-	reftable_iterator_destroy(&it);
-
-	reftable_buf_release(&buf);
-	free_names(names);
-	reftable_reader_decref(reader);
-}
-
-static void t_table_read_write_seek_linear(void)
-{
-	t_table_read_write_seek(0, REFTABLE_HASH_SHA1);
-}
-
-static void t_table_read_write_seek_linear_sha256(void)
-{
-	t_table_read_write_seek(0, REFTABLE_HASH_SHA256);
-}
-
-static void t_table_read_write_seek_index(void)
-{
-	t_table_read_write_seek(1, REFTABLE_HASH_SHA1);
-}
-
-static void t_table_refs_for(int indexed)
-{
-	char **want_names;
-	int want_names_len = 0;
-	uint8_t want_hash[REFTABLE_HASH_SIZE_SHA1];
-
-	struct reftable_write_options opts = {
-		.block_size = 256,
-	};
-	struct reftable_ref_record ref = { 0 };
-	struct reftable_reader *reader;
-	struct reftable_block_source source = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_iterator it = { 0 };
-	int N = 50, n, j, err, i;
-
-	want_names = reftable_calloc(N + 1, sizeof(*want_names));
-	check(want_names != NULL);
-
-	t_reftable_set_hash(want_hash, 4, REFTABLE_HASH_SHA1);
-
-	for (i = 0; i < N; i++) {
-		uint8_t hash[REFTABLE_HASH_SIZE_SHA1];
-		char fill[51] = { 0 };
-		char name[100];
-		struct reftable_ref_record ref = { 0 };
-
-		memset(hash, i, sizeof(hash));
-		memset(fill, 'x', 50);
-		/* Put the variable part in the start */
-		snprintf(name, sizeof(name), "br%02d%s", i, fill);
-		name[40] = 0;
-		ref.refname = name;
-
-		ref.value_type = REFTABLE_REF_VAL2;
-		t_reftable_set_hash(ref.value.val2.value, i / 4,
-				    REFTABLE_HASH_SHA1);
-		t_reftable_set_hash(ref.value.val2.target_value, 3 + i / 4,
-				    REFTABLE_HASH_SHA1);
-
-		/* 80 bytes / entry, so 3 entries per block. Yields 17
-		 */
-		/* blocks. */
-		n = reftable_writer_add_ref(w, &ref);
-		check_int(n, ==, 0);
-
-		if (!memcmp(ref.value.val2.value, want_hash, REFTABLE_HASH_SIZE_SHA1) ||
-		    !memcmp(ref.value.val2.target_value, want_hash, REFTABLE_HASH_SIZE_SHA1))
-			want_names[want_names_len++] = xstrdup(name);
-	}
-
-	n = reftable_writer_close(w);
-	check_int(n, ==, 0);
-
-	reftable_writer_free(w);
-	w = NULL;
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&reader, &source, "file.ref");
-	check(!err);
-	if (!indexed)
-		reader->obj_offsets.is_present = 0;
-
-	err = reftable_reader_init_ref_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, "");
-	check(!err);
-	reftable_iterator_destroy(&it);
-
-	err = reftable_reader_refs_for(reader, &it, want_hash);
-	check(!err);
-
-	for (j = 0; ; j++) {
-		int err = reftable_iterator_next_ref(&it, &ref);
-		check_int(err, >=, 0);
-		if (err > 0)
-			break;
-		check_int(j, <, want_names_len);
-		check_str(ref.refname, want_names[j]);
-		reftable_ref_record_release(&ref);
-	}
-	check_int(j, ==, want_names_len);
-
-	reftable_buf_release(&buf);
-	free_names(want_names);
-	reftable_iterator_destroy(&it);
-	reftable_reader_decref(reader);
-}
-
-static void t_table_refs_for_no_index(void)
-{
-	t_table_refs_for(0);
-}
-
-static void t_table_refs_for_obj_index(void)
-{
-	t_table_refs_for(1);
-}
-
-static void t_write_empty_table(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_block_source source = { 0 };
-	struct reftable_reader *rd = NULL;
-	struct reftable_ref_record rec = { 0 };
-	struct reftable_iterator it = { 0 };
-	int err;
-
-	reftable_writer_set_limits(w, 1, 1);
-
-	err = reftable_writer_close(w);
-	check_int(err, ==, REFTABLE_EMPTY_TABLE_ERROR);
-	reftable_writer_free(w);
-
-	check_uint(buf.len, ==, header_size(1) + footer_size(1));
-
-	block_source_from_buf(&source, &buf);
-
-	err = reftable_reader_new(&rd, &source, "filename");
-	check(!err);
-
-	err = reftable_reader_init_ref_iterator(rd, &it);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, "");
-	check(!err);
-
-	err = reftable_iterator_next_ref(&it, &rec);
-	check_int(err, >, 0);
-
-	reftable_iterator_destroy(&it);
-	reftable_reader_decref(rd);
-	reftable_buf_release(&buf);
-}
-
-static void t_write_object_id_min_length(void)
-{
-	struct reftable_write_options opts = {
-		.block_size = 75,
-	};
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_ref_record ref = {
-		.update_index = 1,
-		.value_type = REFTABLE_REF_VAL1,
-		.value.val1 = {42},
-	};
-	int err;
-	int i;
-
-	reftable_writer_set_limits(w, 1, 1);
-
-	/* Write the same hash in many refs. If there is only 1 hash, the
-	 * disambiguating prefix is length 0 */
-	for (i = 0; i < 256; i++) {
-		char name[256];
-		snprintf(name, sizeof(name), "ref%05d", i);
-		ref.refname = name;
-		err = reftable_writer_add_ref(w, &ref);
-		check(!err);
-	}
-
-	err = reftable_writer_close(w);
-	check(!err);
-	check_int(reftable_writer_stats(w)->object_id_len, ==, 2);
-	reftable_writer_free(w);
-	reftable_buf_release(&buf);
-}
-
-static void t_write_object_id_length(void)
-{
-	struct reftable_write_options opts = {
-		.block_size = 75,
-	};
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_ref_record ref = {
-		.update_index = 1,
-		.value_type = REFTABLE_REF_VAL1,
-		.value.val1 = {42},
-	};
-	int err;
-	int i;
-
-	reftable_writer_set_limits(w, 1, 1);
-
-	/* Write the same hash in many refs. If there is only 1 hash, the
-	 * disambiguating prefix is length 0 */
-	for (i = 0; i < 256; i++) {
-		char name[256];
-		snprintf(name, sizeof(name), "ref%05d", i);
-		ref.refname = name;
-		ref.value.val1[15] = i;
-		err = reftable_writer_add_ref(w, &ref);
-		check(!err);
-	}
-
-	err = reftable_writer_close(w);
-	check(!err);
-	check_int(reftable_writer_stats(w)->object_id_len, ==, 16);
-	reftable_writer_free(w);
-	reftable_buf_release(&buf);
-}
-
-static void t_write_empty_key(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_ref_record ref = {
-		.refname = (char *) "",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_DELETION,
-	};
-	int err;
-
-	reftable_writer_set_limits(w, 1, 1);
-	err = reftable_writer_add_ref(w, &ref);
-	check_int(err, ==, REFTABLE_API_ERROR);
-
-	err = reftable_writer_close(w);
-	check_int(err, ==, REFTABLE_EMPTY_TABLE_ERROR);
-	reftable_writer_free(w);
-	reftable_buf_release(&buf);
-}
-
-static void t_write_key_order(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_writer *w = t_reftable_strbuf_writer(&buf, &opts);
-	struct reftable_ref_record refs[2] = {
-		{
-			.refname = (char *) "b",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_SYMREF,
-			.value = {
-				.symref = (char *) "target",
-			},
-		}, {
-			.refname = (char *) "a",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_SYMREF,
-			.value = {
-				.symref = (char *) "target",
-			},
-		}
-	};
-	int err;
-
-	reftable_writer_set_limits(w, 1, 1);
-	err = reftable_writer_add_ref(w, &refs[0]);
-	check(!err);
-	err = reftable_writer_add_ref(w, &refs[1]);
-	check_int(err, ==, REFTABLE_API_ERROR);
-
-	refs[0].update_index = 2;
-	err = reftable_writer_add_ref(w, &refs[0]);
-	check_int(err, ==, REFTABLE_API_ERROR);
-
-	reftable_writer_close(w);
-	reftable_writer_free(w);
-	reftable_buf_release(&buf);
-}
-
-static void t_write_multiple_indices(void)
-{
-	struct reftable_write_options opts = {
-		.block_size = 100,
-	};
-	struct reftable_buf writer_buf = REFTABLE_BUF_INIT;
-	struct reftable_block_source source = { 0 };
-	struct reftable_iterator it = { 0 };
-	const struct reftable_stats *stats;
-	struct reftable_writer *writer;
-	struct reftable_reader *reader;
-	char buf[128];
-	int err, i;
-
-	writer = t_reftable_strbuf_writer(&writer_buf, &opts);
-	reftable_writer_set_limits(writer, 1, 1);
-	for (i = 0; i < 100; i++) {
-		struct reftable_ref_record ref = {
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = {i},
-		};
-
-		snprintf(buf, sizeof(buf), "refs/heads/%04d", i);
-		ref.refname = buf;
-
-		err = reftable_writer_add_ref(writer, &ref);
-		check(!err);
-	}
-
-	for (i = 0; i < 100; i++) {
-		struct reftable_log_record log = {
-			.update_index = 1,
-			.value_type = REFTABLE_LOG_UPDATE,
-			.value.update = {
-				.old_hash = { i },
-				.new_hash = { i },
-			},
-		};
-
-		snprintf(buf, sizeof(buf), "refs/heads/%04d", i);
-		log.refname = buf;
-
-		err = reftable_writer_add_log(writer, &log);
-		check(!err);
-	}
-
-	reftable_writer_close(writer);
-
-	/*
-	 * The written data should be sufficiently large to result in indices
-	 * for each of the block types.
-	 */
-	stats = reftable_writer_stats(writer);
-	check_int(stats->ref_stats.index_offset, >, 0);
-	check_int(stats->obj_stats.index_offset, >, 0);
-	check_int(stats->log_stats.index_offset, >, 0);
-
-	block_source_from_buf(&source, &writer_buf);
-	err = reftable_reader_new(&reader, &source, "filename");
-	check(!err);
-
-	/*
-	 * Seeking the log uses the log index now. In case there is any
-	 * confusion regarding indices we would notice here.
-	 */
-	err = reftable_reader_init_log_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_log(&it, "");
-	check(!err);
-
-	reftable_iterator_destroy(&it);
-	reftable_writer_free(writer);
-	reftable_reader_decref(reader);
-	reftable_buf_release(&writer_buf);
-}
-
-static void t_write_multi_level_index(void)
-{
-	struct reftable_write_options opts = {
-		.block_size = 100,
-	};
-	struct reftable_buf writer_buf = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
-	struct reftable_block_source source = { 0 };
-	struct reftable_iterator it = { 0 };
-	const struct reftable_stats *stats;
-	struct reftable_writer *writer;
-	struct reftable_reader *reader;
-	int err;
-
-	writer = t_reftable_strbuf_writer(&writer_buf, &opts);
-	reftable_writer_set_limits(writer, 1, 1);
-	for (size_t i = 0; i < 200; i++) {
-		struct reftable_ref_record ref = {
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = {i},
-		};
-		char buf[128];
-
-		snprintf(buf, sizeof(buf), "refs/heads/%03" PRIuMAX, (uintmax_t)i);
-		ref.refname = buf;
-
-		err = reftable_writer_add_ref(writer, &ref);
-		check(!err);
-	}
-	reftable_writer_close(writer);
-
-	/*
-	 * The written refs should be sufficiently large to result in a
-	 * multi-level index.
-	 */
-	stats = reftable_writer_stats(writer);
-	check_int(stats->ref_stats.max_index_level, ==, 2);
-
-	block_source_from_buf(&source, &writer_buf);
-	err = reftable_reader_new(&reader, &source, "filename");
-	check(!err);
-
-	/*
-	 * Seeking the last ref should work as expected.
-	 */
-	err = reftable_reader_init_ref_iterator(reader, &it);
-	check(!err);
-	err = reftable_iterator_seek_ref(&it, "refs/heads/199");
-	check(!err);
-
-	reftable_iterator_destroy(&it);
-	reftable_writer_free(writer);
-	reftable_reader_decref(reader);
-	reftable_buf_release(&writer_buf);
-	reftable_buf_release(&buf);
-}
-
-static void t_corrupt_table_empty(void)
-{
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_block_source source = { 0 };
-	struct reftable_reader *reader;
-	int err;
-
-	block_source_from_buf(&source, &buf);
-	err = reftable_reader_new(&reader, &source, "file.log");
-	check_int(err, ==, REFTABLE_FORMAT_ERROR);
-}
-
-static void t_corrupt_table(void)
-{
-	uint8_t zeros[1024] = { 0 };
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	struct reftable_block_source source = { 0 };
-	struct reftable_reader *reader;
-	int err;
-	check(!reftable_buf_add(&buf, zeros, sizeof(zeros)));
-
-	block_source_from_buf(&source, &buf);
-	err = reftable_reader_new(&reader, &source, "file.log");
-	check_int(err, ==, REFTABLE_FORMAT_ERROR);
-
-	reftable_buf_release(&buf);
-}
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	TEST(t_buffer(), "strbuf works as blocksource");
-	TEST(t_corrupt_table(), "read-write on corrupted table");
-	TEST(t_corrupt_table_empty(), "read-write on an empty table");
-	TEST(t_log_buffer_size(), "buffer extension for log compression");
-	TEST(t_log_overflow(), "log overflow returns expected error");
-	TEST(t_log_write_limits(), "writer limits for writing log records");
-	TEST(t_log_write_read(), "read-write on log records");
-	TEST(t_log_zlib_corruption(), "reading corrupted log record returns expected error");
-	TEST(t_table_read_api(), "read on a table");
-	TEST(t_table_read_write_seek_index(), "read-write on a table with index");
-	TEST(t_table_read_write_seek_linear(), "read-write on a table without index (SHA1)");
-	TEST(t_table_read_write_seek_linear_sha256(), "read-write on a table without index (SHA256)");
-	TEST(t_table_read_write_sequential(), "sequential read-write on a table");
-	TEST(t_table_refs_for_no_index(), "refs-only table with no index");
-	TEST(t_table_refs_for_obj_index(), "refs-only table with index");
-	TEST(t_table_write_small_table(), "write_table works");
-	TEST(t_write_empty_key(), "write on refs with empty keys");
-	TEST(t_write_empty_table(), "read-write on empty tables");
-	TEST(t_write_key_order(), "refs must be written in increasing order");
-	TEST(t_write_multi_level_index(), "table with multi-level index");
-	TEST(t_write_multiple_indices(), "table with indices for multiple block types");
-	TEST(t_write_object_id_length(), "prefix compression on writing refs");
-	TEST(t_write_object_id_min_length(), "prefix compression on writing refs");
-
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-readwrite.c b/t/unit-tests/u-reftable-readwrite.c
new file mode 100644
index 0000000000..3d6bdcfceb
--- /dev/null
+++ b/t/unit-tests/u-reftable-readwrite.c
@@ -0,0 +1,870 @@
+/*
+Copyright 2020 Google LLC
+
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file or at
+https://developers.google.com/open-source/licenses/bsd
+*/
+
+#define DISABLE_SIGN_COMPARE_WARNINGS
+
+#include "unit-test.h"
+#include "lib-reftable.h"
+#include "reftable/basics.h"
+#include "reftable/blocksource.h"
+#include "reftable/reader.h"
+#include "reftable/reftable-error.h"
+#include "reftable/reftable-writer.h"
+#include "strbuf.h"
+
+static const int update_index = 5;
+
+void test_reftable_readwrite__buffer(void)
+{
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_block_source source = { 0 };
+	struct reftable_block out = { 0 };
+	int n;
+	uint8_t in[] = "hello";
+	cl_assert(reftable_buf_add(&buf, in, sizeof(in)) == 0);
+	block_source_from_buf(&source, &buf);
+	cl_assert_equal_i(block_source_size(&source), 6);
+	n = block_source_read_block(&source, &out, 0, sizeof(in));
+	cl_assert_equal_i(n, sizeof(in));
+	cl_assert(memcmp(in, out.data, n) == 0);
+	reftable_block_done(&out);
+
+	cl_assert_equal_i(block_source_read_block(&source, &out, 1, 2), 2);
+	cl_assert(memcmp(out.data, "el", 2) == 0);
+
+	reftable_block_done(&out);
+	block_source_close(&source);
+	reftable_buf_release(&buf);
+}
+
+static void write_table(char ***names, struct reftable_buf *buf, int N,
+			int block_size, enum reftable_hash hash_id)
+{
+	struct reftable_write_options opts = {
+		.block_size = block_size,
+		.hash_id = hash_id,
+	};
+	struct reftable_ref_record *refs;
+	struct reftable_log_record *logs;
+	int i;
+
+	REFTABLE_CALLOC_ARRAY(*names, N + 1);
+	cl_assert(*names != NULL);
+	REFTABLE_CALLOC_ARRAY(refs, N);
+	cl_assert(refs != NULL);
+	REFTABLE_CALLOC_ARRAY(logs, N);
+	cl_assert(logs != NULL);
+
+	for (i = 0; i < N; i++) {
+		refs[i].refname = (*names)[i] = xstrfmt("refs/heads/branch%02d", i);
+		refs[i].update_index = update_index;
+		refs[i].value_type = REFTABLE_REF_VAL1;
+		cl_reftable_set_hash(refs[i].value.val1, i, REFTABLE_HASH_SHA1);
+	}
+
+	for (i = 0; i < N; i++) {
+		logs[i].refname = (*names)[i];
+		logs[i].update_index = update_index;
+		logs[i].value_type = REFTABLE_LOG_UPDATE;
+		cl_reftable_set_hash(logs[i].value.update.new_hash, i,
+				    REFTABLE_HASH_SHA1);
+		logs[i].value.update.message = (char *) "message";
+	}
+
+	cl_reftable_write_to_buf(buf, refs, N, logs, N, &opts);
+
+	reftable_free(refs);
+	reftable_free(logs);
+}
+
+void test_reftable_readwrite__log_buffer_size(void)
+{
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_write_options opts = {
+		.block_size = 4096,
+	};
+	int i;
+	struct reftable_log_record
+		log = { .refname = (char *) "refs/heads/master",
+			.update_index = update_index,
+			.value_type = REFTABLE_LOG_UPDATE,
+			.value = { .update = {
+					   .name = (char *) "Han-Wen Nienhuys",
+					   .email = (char *) "hanwen@google.com",
+					   .tz_offset = 100,
+					   .time = 0x5e430672,
+					   .message = (char *) "commit: 9\n",
+				   } } };
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+
+	/* This tests buffer extension for log compression. Must use a random
+	   hash, to ensure that the compressed part is larger than the original.
+	*/
+	for (i = 0; i < REFTABLE_HASH_SIZE_SHA1; i++) {
+		log.value.update.old_hash[i] = (uint8_t)(git_rand(0) % 256);
+		log.value.update.new_hash[i] = (uint8_t)(git_rand(0) % 256);
+	}
+	reftable_writer_set_limits(w, update_index, update_index);
+	cl_assert(reftable_writer_add_log(w, &log) == 0);
+	cl_assert(reftable_writer_close(w) == 0);
+	reftable_writer_free(w);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__log_overflow(void)
+{
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	char msg[256] = { 0 };
+	struct reftable_write_options opts = {
+		.block_size = ARRAY_SIZE(msg),
+	};
+	struct reftable_log_record log = {
+		.refname = (char *) "refs/heads/master",
+		.update_index = update_index,
+		.value_type = REFTABLE_LOG_UPDATE,
+		.value = {
+			.update = {
+				.old_hash = { 1 },
+				.new_hash = { 2 },
+				.name = (char *) "Han-Wen Nienhuys",
+				.email = (char *) "hanwen@google.com",
+				.tz_offset = 100,
+				.time = 0x5e430672,
+				.message = msg,
+			},
+		},
+	};
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+
+	memset(msg, 'x', sizeof(msg) - 1);
+	reftable_writer_set_limits(w, update_index, update_index);
+	cl_assert_equal_i(reftable_writer_add_log(w, &log), REFTABLE_ENTRY_TOO_BIG_ERROR);
+	reftable_writer_free(w);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__log_write_limits(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_log_record log = {
+		.refname = (char *)"refs/head/master",
+		.update_index = 0,
+		.value_type = REFTABLE_LOG_UPDATE,
+		.value = {
+			.update = {
+				.old_hash = { 1 },
+				.new_hash = { 2 },
+				.name = (char *)"Han-Wen Nienhuys",
+				.email = (char *)"hanwen@google.com",
+				.tz_offset = 100,
+				.time = 0x5e430672,
+			},
+		},
+	};
+
+	reftable_writer_set_limits(w, 1, 1);
+
+	/* write with update_index (0) below set limits (1, 1) */
+	cl_assert_equal_i(reftable_writer_add_log(w, &log), 0);
+
+	/* write with update_index (1) in the set limits (1, 1) */
+	log.update_index = 1;
+	cl_assert_equal_i(reftable_writer_add_log(w, &log), 0);
+
+	/* write with update_index (3) above set limits (1, 1) */
+	log.update_index = 3;
+	cl_assert_equal_i(reftable_writer_add_log(w, &log), REFTABLE_API_ERROR);
+
+	reftable_writer_free(w);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__log_write_read(void)
+{
+	struct reftable_write_options opts = {
+		.block_size = 256,
+	};
+	struct reftable_ref_record ref = { 0 };
+	struct reftable_log_record log = { 0 };
+	struct reftable_iterator it = { 0 };
+	struct reftable_reader *reader;
+	struct reftable_block_source source = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	const struct reftable_stats *stats = NULL;
+	int N = 2, i;
+	char **names;
+
+	names = reftable_calloc(N + 1, sizeof(*names));
+	cl_assert(names != NULL);
+
+	reftable_writer_set_limits(w, 0, N);
+
+	for (i = 0; i < N; i++) {
+		char name[256];
+		struct reftable_ref_record ref = { 0 };
+		snprintf(name, sizeof(name), "b%02d%0*d", i, 130, 7);
+		names[i] = xstrdup(name);
+		ref.refname = name;
+		ref.update_index = i;
+
+		cl_assert(reftable_writer_add_ref(w, &ref) == 0);
+	}
+
+	for (i = 0; i < N; i++) {
+		struct reftable_log_record log = { 0 };
+
+		log.refname = names[i];
+		log.update_index = i;
+		log.value_type = REFTABLE_LOG_UPDATE;
+		cl_reftable_set_hash(log.value.update.old_hash, i,
+				    REFTABLE_HASH_SHA1);
+		cl_reftable_set_hash(log.value.update.new_hash, i + 1,
+				    REFTABLE_HASH_SHA1);
+
+		cl_assert(reftable_writer_add_log(w, &log) == 0);
+	}
+
+	cl_assert_equal_i(reftable_writer_close(w), 0);
+
+	stats = reftable_writer_stats(w);
+	cl_assert(stats->log_stats.blocks > 0);
+	reftable_writer_free(w);
+	w = NULL;
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "file.log") == 0);
+	cl_assert(reftable_reader_init_ref_iterator(reader, &it) == 0);
+	cl_assert(reftable_iterator_seek_ref(&it, names[N - 1]) == 0);
+	cl_assert(reftable_iterator_next_ref(&it, &ref) == 0);
+
+	/* end of iteration. */
+	cl_assert(reftable_iterator_next_ref(&it, &ref) > 0);
+
+	reftable_iterator_destroy(&it);
+	reftable_ref_record_release(&ref);
+
+	cl_assert(reftable_reader_init_log_iterator(reader, &it) == 0);
+	cl_assert(reftable_iterator_seek_log(&it, "") == 0);
+
+	for (i = 0; ; i++) {
+		int err = reftable_iterator_next_log(&it, &log);
+		if (err > 0)
+			break;
+		cl_assert(err == 0);
+		cl_assert_equal_s(names[i], log.refname);
+		cl_assert_equal_i(i, log.update_index);
+		reftable_log_record_release(&log);
+	}
+
+	cl_assert_equal_i(i, N);
+	reftable_iterator_destroy(&it);
+
+	/* cleanup. */
+	reftable_buf_release(&buf);
+	free_names(names);
+	reftable_reader_decref(reader);
+}
+
+void test_reftable_readwrite__log_zlib_corruption(void)
+{
+	struct reftable_write_options opts = {
+		.block_size = 256,
+	};
+	struct reftable_iterator it = { 0 };
+	struct reftable_reader *reader;
+	struct reftable_block_source source = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	const struct reftable_stats *stats = NULL;
+	char message[100] = { 0 };
+	int i;
+	struct reftable_log_record log = {
+		.refname = (char *) "refname",
+		.value_type = REFTABLE_LOG_UPDATE,
+		.value = {
+			.update = {
+				.new_hash = { 1 },
+				.old_hash = { 2 },
+				.name = (char *) "My Name",
+				.email = (char *) "myname@invalid",
+				.message = message,
+			},
+		},
+	};
+
+	for (i = 0; i < sizeof(message) - 1; i++)
+		message[i] = (uint8_t)(git_rand(0) % 64 + ' ');
+
+	reftable_writer_set_limits(w, 1, 1);
+
+	cl_assert(reftable_writer_add_log(w, &log) == 0);
+	cl_assert_equal_i(reftable_writer_close(w), 0);
+
+	stats = reftable_writer_stats(w);
+	cl_assert(stats->log_stats.blocks > 0);
+	reftable_writer_free(w);
+	w = NULL;
+
+	/* corrupt the data. */
+	buf.buf[50] ^= 0x99;
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "file.log") == 0);
+	cl_assert(reftable_reader_init_log_iterator(reader, &it) == 0);
+	cl_assert_equal_i(reftable_iterator_seek_log(&it, "refname"), REFTABLE_ZLIB_ERROR);
+
+	reftable_iterator_destroy(&it);
+
+	/* cleanup. */
+	reftable_reader_decref(reader);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__table_read_write_sequential(void)
+{
+	char **names;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	int N = 50;
+	struct reftable_iterator it = { 0 };
+	struct reftable_block_source source = { 0 };
+	struct reftable_reader *reader;
+	int j = 0;
+
+	write_table(&names, &buf, N, 256, REFTABLE_HASH_SHA1);
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "file.ref") == 0);
+	cl_assert(reftable_reader_init_ref_iterator(reader, &it) == 0);
+	cl_assert(reftable_iterator_seek_ref(&it, "") == 0);
+
+	for (j = 0; ; j++) {
+		struct reftable_ref_record ref = { 0 };
+		int r = reftable_iterator_next_ref(&it, &ref);
+		cl_assert(r >= 0);
+		if (r > 0)
+			break;
+		cl_assert_equal_s(names[j], ref.refname);
+		cl_assert_equal_i(update_index, ref.update_index);
+		reftable_ref_record_release(&ref);
+	}
+	cl_assert_equal_i(j, N);
+
+	reftable_iterator_destroy(&it);
+	reftable_reader_decref(reader);
+	reftable_buf_release(&buf);
+	free_names(names);
+}
+
+void test_reftable_readwrite__table_write_small_table(void)
+{
+	char **names;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	int N = 1;
+	write_table(&names, &buf, N, 4096, REFTABLE_HASH_SHA1);
+	cl_assert(buf.len < 200);
+	reftable_buf_release(&buf);
+	free_names(names);
+}
+
+void test_reftable_readwrite__table_read_api(void)
+{
+	char **names;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	int N = 50;
+	struct reftable_reader *reader;
+	struct reftable_block_source source = { 0 };
+	struct reftable_log_record log = { 0 };
+	struct reftable_iterator it = { 0 };
+
+	write_table(&names, &buf, N, 256, REFTABLE_HASH_SHA1);
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "file.ref") == 0);
+	cl_assert(reftable_reader_init_ref_iterator(reader, &it) == 0);
+	cl_assert(reftable_iterator_seek_ref(&it, names[0]) == 0);
+
+	cl_assert_equal_i(reftable_iterator_next_log(&it, &log), REFTABLE_API_ERROR);
+
+	reftable_buf_release(&buf);
+	free_names(names);
+	reftable_iterator_destroy(&it);
+	reftable_reader_decref(reader);
+	reftable_buf_release(&buf);
+}
+
+static void t_table_read_write_seek(int index, enum reftable_hash hash_id)
+{
+	char **names;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	int N = 50;
+	struct reftable_reader *reader;
+	struct reftable_block_source source = { 0 };
+	int err;
+	int i = 0;
+
+	struct reftable_iterator it = { 0 };
+	struct reftable_buf pastLast = REFTABLE_BUF_INIT;
+	struct reftable_ref_record ref = { 0 };
+
+	write_table(&names, &buf, N, 256, hash_id);
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "file.ref") == 0);
+	cl_assert_equal_i(hash_id, reftable_reader_hash_id(reader));
+
+	if (!index) {
+		reader->ref_offsets.index_offset = 0;
+	} else {
+		cl_assert(reader->ref_offsets.index_offset > 0);
+	}
+
+	for (i = 1; i < N; i++) {
+		cl_assert(reftable_reader_init_ref_iterator(reader, &it) == 0);
+		cl_assert(reftable_iterator_seek_ref(&it, names[i]) == 0);
+		cl_assert(reftable_iterator_next_ref(&it, &ref) == 0);
+		cl_assert_equal_s(names[i], ref.refname);
+		cl_assert_equal_i(REFTABLE_REF_VAL1, ref.value_type);
+		cl_assert_equal_i(i, ref.value.val1[0]);
+
+		reftable_ref_record_release(&ref);
+		reftable_iterator_destroy(&it);
+	}
+
+	cl_assert(reftable_buf_addstr(&pastLast, names[N - 1]) == 0);
+	cl_assert(reftable_buf_addstr(&pastLast, "/") == 0);
+
+	cl_assert(reftable_reader_init_ref_iterator(reader, &it) == 0);
+	err = reftable_iterator_seek_ref(&it, pastLast.buf);
+	if (err == 0) {
+		struct reftable_ref_record ref = { 0 };
+		int err = reftable_iterator_next_ref(&it, &ref);
+		cl_assert(err > 0);
+	} else {
+		cl_assert(err > 0);
+	}
+
+	reftable_buf_release(&pastLast);
+	reftable_iterator_destroy(&it);
+
+	reftable_buf_release(&buf);
+	free_names(names);
+	reftable_reader_decref(reader);
+}
+
+void test_reftable_readwrite__table_read_write_seek_linear(void)
+{
+	t_table_read_write_seek(0, REFTABLE_HASH_SHA1);
+}
+
+void test_reftable_readwrite__table_read_write_seek_linear_sha256(void)
+{
+	t_table_read_write_seek(0, REFTABLE_HASH_SHA256);
+}
+
+void test_reftable_readwrite__table_read_write_seek_index(void)
+{
+	t_table_read_write_seek(1, REFTABLE_HASH_SHA1);
+}
+
+static void t_table_refs_for(int indexed)
+{
+	char **want_names;
+	int want_names_len = 0;
+	uint8_t want_hash[REFTABLE_HASH_SIZE_SHA1];
+
+	struct reftable_write_options opts = {
+		.block_size = 256,
+	};
+	struct reftable_ref_record ref = { 0 };
+	struct reftable_reader *reader;
+	struct reftable_block_source source = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_iterator it = { 0 };
+	int N = 50, j, i;
+
+	want_names = reftable_calloc(N + 1, sizeof(*want_names));
+	cl_assert(want_names != NULL);
+
+	cl_reftable_set_hash(want_hash, 4, REFTABLE_HASH_SHA1);
+
+	for (i = 0; i < N; i++) {
+		uint8_t hash[REFTABLE_HASH_SIZE_SHA1];
+		char fill[51] = { 0 };
+		char name[100];
+		struct reftable_ref_record ref = { 0 };
+
+		memset(hash, i, sizeof(hash));
+		memset(fill, 'x', 50);
+		/* Put the variable part in the start */
+		snprintf(name, sizeof(name), "br%02d%s", i, fill);
+		name[40] = 0;
+		ref.refname = name;
+
+		ref.value_type = REFTABLE_REF_VAL2;
+		cl_reftable_set_hash(ref.value.val2.value, i / 4,
+				    REFTABLE_HASH_SHA1);
+		cl_reftable_set_hash(ref.value.val2.target_value, 3 + i / 4,
+				    REFTABLE_HASH_SHA1);
+
+		/* 80 bytes / entry, so 3 entries per block. Yields 17
+		 */
+		/* blocks. */
+		cl_assert_equal_i(reftable_writer_add_ref(w, &ref), 0);
+
+		if (!memcmp(ref.value.val2.value, want_hash, REFTABLE_HASH_SIZE_SHA1) ||
+		    !memcmp(ref.value.val2.target_value, want_hash, REFTABLE_HASH_SIZE_SHA1))
+			want_names[want_names_len++] = xstrdup(name);
+	}
+
+	cl_assert_equal_i(reftable_writer_close(w), 0);
+
+	reftable_writer_free(w);
+	w = NULL;
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&reader, &source, "file.ref") == 0);
+	if (!indexed)
+		reader->obj_offsets.is_present = 0;
+
+	cl_assert(reftable_reader_init_ref_iterator(reader, &it) == 0);
+	cl_assert(reftable_iterator_seek_ref(&it, "") == 0);
+	reftable_iterator_destroy(&it);
+
+	cl_assert(reftable_reader_refs_for(reader, &it, want_hash) == 0);
+
+	for (j = 0; ; j++) {
+		int err = reftable_iterator_next_ref(&it, &ref);
+		cl_assert(err >= 0);
+		if (err > 0)
+			break;
+		cl_assert(j < want_names_len);
+		cl_assert_equal_s(ref.refname, want_names[j]);
+		reftable_ref_record_release(&ref);
+	}
+	cl_assert_equal_i(j, want_names_len);
+
+	reftable_buf_release(&buf);
+	free_names(want_names);
+	reftable_iterator_destroy(&it);
+	reftable_reader_decref(reader);
+}
+
+void test_reftable_readwrite__table_refs_for_no_index(void)
+{
+	t_table_refs_for(0);
+}
+
+void test_reftable_readwrite__table_refs_for_obj_index(void)
+{
+	t_table_refs_for(1);
+}
+
+void test_reftable_readwrite__write_empty_table(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_block_source source = { 0 };
+	struct reftable_reader *rd = NULL;
+	struct reftable_ref_record rec = { 0 };
+	struct reftable_iterator it = { 0 };
+
+	reftable_writer_set_limits(w, 1, 1);
+
+	cl_assert_equal_i(reftable_writer_close(w), REFTABLE_EMPTY_TABLE_ERROR);
+	reftable_writer_free(w);
+
+	cl_assert_equal_i(buf.len, header_size(1) + footer_size(1));
+
+	block_source_from_buf(&source, &buf);
+
+	cl_assert(reftable_reader_new(&rd, &source, "filename") == 0);
+	cl_assert(reftable_reader_init_ref_iterator(rd, &it) == 0);
+	cl_assert(reftable_iterator_seek_ref(&it, "") == 0);
+	cl_assert(reftable_iterator_next_ref(&it, &rec) > 0);
+
+	reftable_iterator_destroy(&it);
+	reftable_reader_decref(rd);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__write_object_id_min_length(void)
+{
+	struct reftable_write_options opts = {
+		.block_size = 75,
+	};
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_ref_record ref = {
+		.update_index = 1,
+		.value_type = REFTABLE_REF_VAL1,
+		.value.val1 = {42},
+	};
+	int i;
+
+	reftable_writer_set_limits(w, 1, 1);
+
+	/* Write the same hash in many refs. If there is only 1 hash, the
+	 * disambiguating prefix is length 0 */
+	for (i = 0; i < 256; i++) {
+		char name[256];
+		snprintf(name, sizeof(name), "ref%05d", i);
+		ref.refname = name;
+		cl_assert(reftable_writer_add_ref(w, &ref) == 0);
+	}
+
+	cl_assert(reftable_writer_close(w) == 0);
+	cl_assert_equal_i(reftable_writer_stats(w)->object_id_len, 2);
+	reftable_writer_free(w);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__write_object_id_length(void)
+{
+	struct reftable_write_options opts = {
+		.block_size = 75,
+	};
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_ref_record ref = {
+		.update_index = 1,
+		.value_type = REFTABLE_REF_VAL1,
+		.value.val1 = {42},
+	};
+	int i;
+
+	reftable_writer_set_limits(w, 1, 1);
+
+	/* Write the same hash in many refs. If there is only 1 hash, the
+	 * disambiguating prefix is length 0 */
+	for (i = 0; i < 256; i++) {
+		char name[256];
+		snprintf(name, sizeof(name), "ref%05d", i);
+		ref.refname = name;
+		ref.value.val1[15] = i;
+		cl_assert(reftable_writer_add_ref(w, &ref) == 0);
+	}
+
+	cl_assert(reftable_writer_close(w) == 0);
+	cl_assert_equal_i(reftable_writer_stats(w)->object_id_len, 16);
+	reftable_writer_free(w);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__write_empty_key(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_ref_record ref = {
+		.refname = (char *) "",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_DELETION,
+	};
+
+	reftable_writer_set_limits(w, 1, 1);
+	cl_assert_equal_i(reftable_writer_add_ref(w, &ref), REFTABLE_API_ERROR);
+	cl_assert_equal_i(reftable_writer_close(w), REFTABLE_EMPTY_TABLE_ERROR);
+	reftable_writer_free(w);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__write_key_order(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_writer *w = cl_reftable_strbuf_writer(&buf, &opts);
+	struct reftable_ref_record refs[2] = {
+		{
+			.refname = (char *) "b",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_SYMREF,
+			.value = {
+				.symref = (char *) "target",
+			},
+		}, {
+			.refname = (char *) "a",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_SYMREF,
+			.value = {
+				.symref = (char *) "target",
+			},
+		}
+	};
+
+	reftable_writer_set_limits(w, 1, 1);
+	cl_assert(reftable_writer_add_ref(w, &refs[0]) == 0);
+	cl_assert_equal_i(reftable_writer_add_ref(w, &refs[1]), REFTABLE_API_ERROR);
+
+	refs[0].update_index = 2;
+	cl_assert_equal_i(reftable_writer_add_ref(w, &refs[0]), REFTABLE_API_ERROR);
+
+	reftable_writer_close(w);
+	reftable_writer_free(w);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__write_multiple_indices(void)
+{
+	struct reftable_write_options opts = {
+		.block_size = 100,
+	};
+	struct reftable_buf writer_buf = REFTABLE_BUF_INIT;
+	struct reftable_block_source source = { 0 };
+	struct reftable_iterator it = { 0 };
+	const struct reftable_stats *stats;
+	struct reftable_writer *writer;
+	struct reftable_reader *reader;
+	char buf[128];
+	int i;
+
+	writer = cl_reftable_strbuf_writer(&writer_buf, &opts);
+	reftable_writer_set_limits(writer, 1, 1);
+	for (i = 0; i < 100; i++) {
+		struct reftable_ref_record ref = {
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = {i},
+		};
+
+		snprintf(buf, sizeof(buf), "refs/heads/%04d", i);
+		ref.refname = buf;
+
+		cl_assert(reftable_writer_add_ref(writer, &ref) == 0);
+	}
+
+	for (i = 0; i < 100; i++) {
+		struct reftable_log_record log = {
+			.update_index = 1,
+			.value_type = REFTABLE_LOG_UPDATE,
+			.value.update = {
+				.old_hash = { i },
+				.new_hash = { i },
+			},
+		};
+
+		snprintf(buf, sizeof(buf), "refs/heads/%04d", i);
+		log.refname = buf;
+
+		cl_assert(reftable_writer_add_log(writer, &log) == 0);
+	}
+
+	reftable_writer_close(writer);
+
+	/*
+	 * The written data should be sufficiently large to result in indices
+	 * for each of the block types.
+	 */
+	stats = reftable_writer_stats(writer);
+	cl_assert(stats->ref_stats.index_offset > 0);
+	cl_assert(stats->obj_stats.index_offset > 0);
+	cl_assert(stats->log_stats.index_offset > 0);
+
+	block_source_from_buf(&source, &writer_buf);
+	cl_assert(reftable_reader_new(&reader, &source, "filename") == 0);
+
+	/*
+	 * Seeking the log uses the log index now. In case there is any
+	 * confusion regarding indices we would notice here.
+	 */
+	cl_assert(reftable_reader_init_log_iterator(reader, &it) == 0);
+	cl_assert(reftable_iterator_seek_log(&it, "") == 0);
+
+	reftable_iterator_destroy(&it);
+	reftable_writer_free(writer);
+	reftable_reader_decref(reader);
+	reftable_buf_release(&writer_buf);
+}
+
+void test_reftable_readwrite__write_multi_level_index(void)
+{
+	struct reftable_write_options opts = {
+		.block_size = 100,
+	};
+	struct reftable_buf writer_buf = REFTABLE_BUF_INIT, buf = REFTABLE_BUF_INIT;
+	struct reftable_block_source source = { 0 };
+	struct reftable_iterator it = { 0 };
+	const struct reftable_stats *stats;
+	struct reftable_writer *writer;
+	struct reftable_reader *reader;
+
+	writer = cl_reftable_strbuf_writer(&writer_buf, &opts);
+	reftable_writer_set_limits(writer, 1, 1);
+	for (size_t i = 0; i < 200; i++) {
+		struct reftable_ref_record ref = {
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = {i},
+		};
+		char buf[128];
+
+		snprintf(buf, sizeof(buf), "refs/heads/%03" PRIuMAX, (uintmax_t)i);
+		ref.refname = buf;
+
+		cl_assert(reftable_writer_add_ref(writer, &ref) == 0);
+	}
+	reftable_writer_close(writer);
+
+	/*
+	 * The written refs should be sufficiently large to result in a
+	 * multi-level index.
+	 */
+	stats = reftable_writer_stats(writer);
+	cl_assert_equal_i(stats->ref_stats.max_index_level, 2);
+
+	block_source_from_buf(&source, &writer_buf);
+	cl_assert(reftable_reader_new(&reader, &source, "filename") == 0);
+
+	/*
+	 * Seeking the last ref should work as expected.
+	 */
+	cl_assert(reftable_reader_init_ref_iterator(reader, &it) == 0);
+	cl_assert(reftable_iterator_seek_ref(&it, "refs/heads/199") == 0);
+
+	reftable_iterator_destroy(&it);
+	reftable_writer_free(writer);
+	reftable_reader_decref(reader);
+	reftable_buf_release(&writer_buf);
+	reftable_buf_release(&buf);
+}
+
+void test_reftable_readwrite__corrupt_table_empty(void)
+{
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_block_source source = { 0 };
+	struct reftable_reader *reader;
+
+	block_source_from_buf(&source, &buf);
+	cl_assert_equal_i(reftable_reader_new(&reader, &source,
+										  "file.log"),REFTABLE_FORMAT_ERROR);
+}
+
+void test_reftable_readwrite__corrupt_table(void)
+{
+	uint8_t zeros[1024] = { 0 };
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	struct reftable_block_source source = { 0 };
+	struct reftable_reader *reader;
+	cl_assert(reftable_buf_add(&buf, zeros, sizeof(zeros)) == 0);
+
+	block_source_from_buf(&source, &buf);
+	cl_assert_equal_i(reftable_reader_new(&reader, &source,
+										  "file.log"), REFTABLE_FORMAT_ERROR);
+
+	reftable_buf_release(&buf);
+}
-- 
2.43.0

[PATCH 8/9] t/unit-tests: convert reftable record test to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:55:12

Adapt reftable record test file to use clar by using clar assertions
where necessary.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                         |   2 +-
 t/meson.build                    |   2 +-
 t/unit-tests/t-reftable-record.c | 585 -------------------------------
 t/unit-tests/u-reftable-record.c | 565 +++++++++++++++++++++++++++++
 4 files changed, 567 insertions(+), 587 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-record.c
 create mode 100644 t/unit-tests/u-reftable-record.c
diff --git a/Makefile b/Makefile
index 7511959baf..1af0eb8aa2 100644
--- a/Makefile
+++ b/Makefile
@@ -1368,6 +1368,7 @@ CLAR_TEST_SUITES += u-reftable-merged
 CLAR_TEST_SUITES += u-reftable-pq
 CLAR_TEST_SUITES += u-reftable-reader
 CLAR_TEST_SUITES += u-reftable-readwrite
+CLAR_TEST_SUITES += u-reftable-record
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1381,7 +1382,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-record
 UNIT_TEST_PROGRAMS += t-reftable-stack
 UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))
 UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/test-lib.o
diff --git a/t/meson.build b/t/meson.build
index b9e9648f7c..bd0292f0c7 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -14,6 +14,7 @@ clar_test_suites = [
   'unit-tests/u-reftable-pq.c',
   'unit-tests/u-reftable-reader.c',
   'unit-tests/u-reftable-readwrite.c',
+  'unit-tests/u-reftable-record.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -61,7 +62,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-record.c',
   'unit-tests/t-reftable-stack.c',
 ]
 
diff --git a/t/unit-tests/t-reftable-record.c b/t/unit-tests/t-reftable-record.c
deleted file mode 100644
index 5954966373..0000000000
--- a/t/unit-tests/t-reftable-record.c
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
-  Copyright 2020 Google LLC
-
-  Use of this source code is governed by a BSD-style
-  license that can be found in the LICENSE file or at
-  https://developers.google.com/open-source/licenses/bsd
-*/
-
-#include "test-lib.h"
-#include "reftable/basics.h"
-#include "reftable/constants.h"
-#include "reftable/record.h"
-
-static void t_copy(struct reftable_record *rec)
-{
-	struct reftable_record copy;
-	uint8_t typ;
-
-	typ = reftable_record_type(rec);
-	check(!reftable_record_init(&copy, typ));
-	reftable_record_copy_from(&copy, rec, REFTABLE_HASH_SIZE_SHA1);
-	/* do it twice to catch memory leaks */
-	reftable_record_copy_from(&copy, rec, REFTABLE_HASH_SIZE_SHA1);
-	check(reftable_record_equal(rec, &copy, REFTABLE_HASH_SIZE_SHA1));
-
-	reftable_record_release(&copy);
-}
-
-static void t_varint_roundtrip(void)
-{
-	uint64_t inputs[] = { 0,
-			      1,
-			      27,
-			      127,
-			      128,
-			      257,
-			      4096,
-			      ((uint64_t)1 << 63),
-			      ((uint64_t)1 << 63) + ((uint64_t)1 << 63) - 1 };
-
-	for (size_t i = 0; i < ARRAY_SIZE(inputs); i++) {
-		uint8_t dest[10];
-
-		struct string_view out = {
-			.buf = dest,
-			.len = sizeof(dest),
-		};
-		uint64_t in = inputs[i];
-		int n = put_var_int(&out, in);
-		uint64_t got = 0;
-
-		check_int(n, >, 0);
-		out.len = n;
-		n = get_var_int(&got, &out);
-		check_int(n, >, 0);
-
-		check_int(got, ==, in);
-	}
-}
-
-static void t_varint_overflow(void)
-{
-	unsigned char buf[] = {
-		0xFF, 0xFF, 0xFF, 0xFF,
-		0xFF, 0xFF, 0xFF, 0xFF,
-		0xFF, 0x00,
-	};
-	struct string_view view = {
-		.buf = buf,
-		.len = sizeof(buf),
-	};
-	uint64_t value;
-	int err = get_var_int(&value, &view);
-	check_int(err, ==, -1);
-}
-
-static void set_hash(uint8_t *h, int j)
-{
-	for (size_t i = 0; i < hash_size(REFTABLE_HASH_SHA1); i++)
-		h[i] = (j >> i) & 0xff;
-}
-
-static void t_reftable_ref_record_comparison(void)
-{
-	struct reftable_record in[3] = {
-		{
-			.type = BLOCK_TYPE_REF,
-			.u.ref.refname = (char *) "refs/heads/master",
-			.u.ref.value_type = REFTABLE_REF_VAL1,
-		},
-		{
-			.type = BLOCK_TYPE_REF,
-			.u.ref.refname = (char *) "refs/heads/master",
-			.u.ref.value_type = REFTABLE_REF_DELETION,
-		},
-		{
-			.type = BLOCK_TYPE_REF,
-			.u.ref.refname = (char *) "HEAD",
-			.u.ref.value_type = REFTABLE_REF_SYMREF,
-			.u.ref.value.symref = (char *) "refs/heads/master",
-		},
-	};
-	int cmp;
-
-	check(!reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-	check(!cmp);
-
-	check(!reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[1], &in[2], &cmp));
-	check_int(cmp, >, 0);
-
-	in[1].u.ref.value_type = in[0].u.ref.value_type;
-	check(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-	check(!cmp);
-}
-
-static void t_reftable_ref_record_compare_name(void)
-{
-	struct reftable_ref_record recs[3] = {
-		{
-			.refname = (char *) "refs/heads/a"
-		},
-		{
-			.refname = (char *) "refs/heads/b"
-		},
-		{
-			.refname = (char *) "refs/heads/a"
-		},
-	};
-
-	check_int(reftable_ref_record_compare_name(&recs[0], &recs[1]), <, 0);
-	check_int(reftable_ref_record_compare_name(&recs[1], &recs[0]), >, 0);
-	check_int(reftable_ref_record_compare_name(&recs[0], &recs[2]), ==, 0);
-}
-
-static void t_reftable_ref_record_roundtrip(void)
-{
-	struct reftable_buf scratch = REFTABLE_BUF_INIT;
-
-	for (int i = REFTABLE_REF_DELETION; i < REFTABLE_NR_REF_VALUETYPES; i++) {
-		struct reftable_record in = {
-			.type = BLOCK_TYPE_REF,
-			.u.ref.value_type = i,
-		};
-		struct reftable_record out = { .type = BLOCK_TYPE_REF };
-		struct reftable_buf key = REFTABLE_BUF_INIT;
-		uint8_t buffer[1024] = { 0 };
-		struct string_view dest = {
-			.buf = buffer,
-			.len = sizeof(buffer),
-		};
-		int n, m;
-
-		in.u.ref.value_type = i;
-		switch (i) {
-		case REFTABLE_REF_DELETION:
-			break;
-		case REFTABLE_REF_VAL1:
-			set_hash(in.u.ref.value.val1, 1);
-			break;
-		case REFTABLE_REF_VAL2:
-			set_hash(in.u.ref.value.val2.value, 1);
-			set_hash(in.u.ref.value.val2.target_value, 2);
-			break;
-		case REFTABLE_REF_SYMREF:
-			in.u.ref.value.symref = xstrdup("target");
-			break;
-		}
-		in.u.ref.refname = xstrdup("refs/heads/master");
-
-		t_copy(&in);
-
-		check_int(reftable_record_val_type(&in), ==, i);
-		check_int(reftable_record_is_deletion(&in), ==, i == REFTABLE_REF_DELETION);
-
-		reftable_record_key(&in, &key);
-		n = reftable_record_encode(&in, dest, REFTABLE_HASH_SIZE_SHA1);
-		check_int(n, >, 0);
-
-		/* decode into a non-zero reftable_record to test for leaks. */
-		m = reftable_record_decode(&out, key, i, dest, REFTABLE_HASH_SIZE_SHA1, &scratch);
-		check_int(n, ==, m);
-
-		check(reftable_ref_record_equal(&in.u.ref, &out.u.ref,
-						 REFTABLE_HASH_SIZE_SHA1));
-		reftable_record_release(&in);
-
-		reftable_buf_release(&key);
-		reftable_record_release(&out);
-	}
-
-	reftable_buf_release(&scratch);
-}
-
-static void t_reftable_log_record_comparison(void)
-{
-	struct reftable_record in[3] = {
-		{
-			.type = BLOCK_TYPE_LOG,
-			.u.log.refname = (char *) "refs/heads/master",
-			.u.log.update_index = 42,
-		},
-		{
-			.type = BLOCK_TYPE_LOG,
-			.u.log.refname = (char *) "refs/heads/master",
-			.u.log.update_index = 22,
-		},
-		{
-			.type = BLOCK_TYPE_LOG,
-			.u.log.refname = (char *) "refs/heads/main",
-			.u.log.update_index = 22,
-		},
-	};
-	int cmp;
-
-	check(!reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[1], &in[2], &cmp));
-	check_int(cmp, >, 0);
-	/* comparison should be reversed for equal keys, because
-	 * comparison is now performed on the basis of update indices */
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-	check_int(cmp, <, 0);
-
-	in[1].u.log.update_index = in[0].u.log.update_index;
-	check(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-}
-
-static void t_reftable_log_record_compare_key(void)
-{
-	struct reftable_log_record logs[3] = {
-		{
-			.refname = (char *) "refs/heads/a",
-			.update_index = 1,
-		},
-		{
-			.refname = (char *) "refs/heads/b",
-			.update_index = 2,
-		},
-		{
-			.refname = (char *) "refs/heads/a",
-			.update_index = 3,
-		},
-	};
-
-	check_int(reftable_log_record_compare_key(&logs[0], &logs[1]), <, 0);
-	check_int(reftable_log_record_compare_key(&logs[1], &logs[0]), >, 0);
-
-	logs[1].update_index = logs[0].update_index;
-	check_int(reftable_log_record_compare_key(&logs[0], &logs[1]), <, 0);
-
-	check_int(reftable_log_record_compare_key(&logs[0], &logs[2]), >, 0);
-	check_int(reftable_log_record_compare_key(&logs[2], &logs[0]), <, 0);
-	logs[2].update_index = logs[0].update_index;
-	check_int(reftable_log_record_compare_key(&logs[0], &logs[2]), ==, 0);
-}
-
-static void t_reftable_log_record_roundtrip(void)
-{
-	struct reftable_log_record in[] = {
-		{
-			.refname = xstrdup("refs/heads/master"),
-			.update_index = 42,
-			.value_type = REFTABLE_LOG_UPDATE,
-			.value = {
-				.update = {
-					.name = xstrdup("han-wen"),
-					.email = xstrdup("hanwen@google.com"),
-					.message = xstrdup("test"),
-					.time = 1577123507,
-					.tz_offset = 100,
-				},
-			}
-		},
-		{
-			.refname = xstrdup("refs/heads/master"),
-			.update_index = 22,
-			.value_type = REFTABLE_LOG_DELETION,
-		},
-		{
-			.refname = xstrdup("branch"),
-			.update_index = 33,
-			.value_type = REFTABLE_LOG_UPDATE,
-		}
-	};
-	struct reftable_buf scratch = REFTABLE_BUF_INIT;
-	set_hash(in[0].value.update.new_hash, 1);
-	set_hash(in[0].value.update.old_hash, 2);
-	set_hash(in[2].value.update.new_hash, 3);
-	set_hash(in[2].value.update.old_hash, 4);
-
-	check(!reftable_log_record_is_deletion(&in[0]));
-	check(reftable_log_record_is_deletion(&in[1]));
-	check(!reftable_log_record_is_deletion(&in[2]));
-
-	for (size_t i = 0; i < ARRAY_SIZE(in); i++) {
-		struct reftable_record rec = { .type = BLOCK_TYPE_LOG };
-		struct reftable_buf key = REFTABLE_BUF_INIT;
-		uint8_t buffer[1024] = { 0 };
-		struct string_view dest = {
-			.buf = buffer,
-			.len = sizeof(buffer),
-		};
-		/* populate out, to check for leaks. */
-		struct reftable_record out = {
-			.type = BLOCK_TYPE_LOG,
-			.u.log = {
-				.refname = xstrdup("old name"),
-				.value_type = REFTABLE_LOG_UPDATE,
-				.value = {
-					.update = {
-						.name = xstrdup("old name"),
-						.email = xstrdup("old@email"),
-						.message = xstrdup("old message"),
-					},
-				},
-			},
-		};
-		int n, m, valtype;
-
-		rec.u.log = in[i];
-
-		t_copy(&rec);
-
-		reftable_record_key(&rec, &key);
-
-		n = reftable_record_encode(&rec, dest, REFTABLE_HASH_SIZE_SHA1);
-		check_int(n, >=, 0);
-		valtype = reftable_record_val_type(&rec);
-		m = reftable_record_decode(&out, key, valtype, dest,
-					   REFTABLE_HASH_SIZE_SHA1, &scratch);
-		check_int(n, ==, m);
-
-		check(reftable_log_record_equal(&in[i], &out.u.log,
-						 REFTABLE_HASH_SIZE_SHA1));
-		reftable_log_record_release(&in[i]);
-		reftable_buf_release(&key);
-		reftable_record_release(&out);
-	}
-
-	reftable_buf_release(&scratch);
-}
-
-static void t_key_roundtrip(void)
-{
-	uint8_t buffer[1024] = { 0 };
-	struct string_view dest = {
-		.buf = buffer,
-		.len = sizeof(buffer),
-	};
-	struct reftable_buf last_key = REFTABLE_BUF_INIT;
-	struct reftable_buf key = REFTABLE_BUF_INIT;
-	struct reftable_buf roundtrip = REFTABLE_BUF_INIT;
-	int restart;
-	uint8_t extra;
-	int n, m;
-	uint8_t rt_extra;
-
-	check(!reftable_buf_addstr(&last_key, "refs/heads/master"));
-	check(!reftable_buf_addstr(&key, "refs/tags/bla"));
-	extra = 6;
-	n = reftable_encode_key(&restart, dest, last_key, key, extra);
-	check(!restart);
-	check_int(n, >, 0);
-
-	check(!reftable_buf_addstr(&roundtrip, "refs/heads/master"));
-	m = reftable_decode_key(&roundtrip, &rt_extra, dest);
-	check_int(n, ==, m);
-	check(!reftable_buf_cmp(&key, &roundtrip));
-	check_int(rt_extra, ==, extra);
-
-	reftable_buf_release(&last_key);
-	reftable_buf_release(&key);
-	reftable_buf_release(&roundtrip);
-}
-
-static void t_reftable_obj_record_comparison(void)
-{
-
-	uint8_t id_bytes[] = { 0, 1, 2, 3, 4, 5, 6 };
-	uint64_t offsets[] = { 0, 16, 32, 48, 64, 80, 96, 112};
-	struct reftable_record in[3] = {
-		{
-			.type = BLOCK_TYPE_OBJ,
-			.u.obj.hash_prefix = id_bytes,
-			.u.obj.hash_prefix_len = 7,
-			.u.obj.offsets = offsets,
-			.u.obj.offset_len = 8,
-		},
-		{
-			.type = BLOCK_TYPE_OBJ,
-			.u.obj.hash_prefix = id_bytes,
-			.u.obj.hash_prefix_len = 7,
-			.u.obj.offsets = offsets,
-			.u.obj.offset_len = 5,
-		},
-		{
-			.type = BLOCK_TYPE_OBJ,
-			.u.obj.hash_prefix = id_bytes,
-			.u.obj.hash_prefix_len = 5,
-		},
-	};
-	int cmp;
-
-	check(!reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-	check(!cmp);
-
-	check(!reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[1], &in[2], &cmp));
-	check_int(cmp, >, 0);
-
-	in[1].u.obj.offset_len = in[0].u.obj.offset_len;
-	check(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-	check(!cmp);
-}
-
-static void t_reftable_obj_record_roundtrip(void)
-{
-	uint8_t testHash1[REFTABLE_HASH_SIZE_SHA1] = { 1, 2, 3, 4, 0 };
-	uint64_t till9[] = { 1, 2, 3, 4, 500, 600, 700, 800, 9000 };
-	struct reftable_obj_record recs[3] = {
-		{
-			.hash_prefix = testHash1,
-			.hash_prefix_len = 5,
-			.offsets = till9,
-			.offset_len = 3,
-		},
-		{
-			.hash_prefix = testHash1,
-			.hash_prefix_len = 5,
-			.offsets = till9,
-			.offset_len = 9,
-		},
-		{
-			.hash_prefix = testHash1,
-			.hash_prefix_len = 5,
-		},
-	};
-	struct reftable_buf scratch = REFTABLE_BUF_INIT;
-
-	for (size_t i = 0; i < ARRAY_SIZE(recs); i++) {
-		uint8_t buffer[1024] = { 0 };
-		struct string_view dest = {
-			.buf = buffer,
-			.len = sizeof(buffer),
-		};
-		struct reftable_record in = {
-			.type = BLOCK_TYPE_OBJ,
-			.u = {
-				.obj = recs[i],
-			},
-		};
-		struct reftable_buf key = REFTABLE_BUF_INIT;
-		struct reftable_record out = { .type = BLOCK_TYPE_OBJ };
-		int n, m;
-		uint8_t extra;
-
-		check(!reftable_record_is_deletion(&in));
-		t_copy(&in);
-		reftable_record_key(&in, &key);
-		n = reftable_record_encode(&in, dest, REFTABLE_HASH_SIZE_SHA1);
-		check_int(n, >, 0);
-		extra = reftable_record_val_type(&in);
-		m = reftable_record_decode(&out, key, extra, dest,
-					   REFTABLE_HASH_SIZE_SHA1, &scratch);
-		check_int(n, ==, m);
-
-		check(reftable_record_equal(&in, &out, REFTABLE_HASH_SIZE_SHA1));
-		reftable_buf_release(&key);
-		reftable_record_release(&out);
-	}
-
-	reftable_buf_release(&scratch);
-}
-
-static void t_reftable_index_record_comparison(void)
-{
-	struct reftable_record in[3] = {
-		{
-			.type = BLOCK_TYPE_INDEX,
-			.u.idx.offset = 22,
-			.u.idx.last_key = REFTABLE_BUF_INIT,
-		},
-		{
-			.type = BLOCK_TYPE_INDEX,
-			.u.idx.offset = 32,
-			.u.idx.last_key = REFTABLE_BUF_INIT,
-		},
-		{
-			.type = BLOCK_TYPE_INDEX,
-			.u.idx.offset = 32,
-			.u.idx.last_key = REFTABLE_BUF_INIT,
-		},
-	};
-	int cmp;
-
-	check(!reftable_buf_addstr(&in[0].u.idx.last_key, "refs/heads/master"));
-	check(!reftable_buf_addstr(&in[1].u.idx.last_key, "refs/heads/master"));
-	check(!reftable_buf_addstr(&in[2].u.idx.last_key, "refs/heads/branch"));
-
-	check(!reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-	check(!cmp);
-
-	check(!reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[1], &in[2], &cmp));
-	check_int(cmp, >, 0);
-
-	in[1].u.idx.offset = in[0].u.idx.offset;
-	check(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1));
-	check(!reftable_record_cmp(&in[0], &in[1], &cmp));
-	check(!cmp);
-
-	for (size_t i = 0; i < ARRAY_SIZE(in); i++)
-		reftable_record_release(&in[i]);
-}
-
-static void t_reftable_index_record_roundtrip(void)
-{
-	struct reftable_record in = {
-		.type = BLOCK_TYPE_INDEX,
-		.u.idx = {
-			.offset = 42,
-			.last_key = REFTABLE_BUF_INIT,
-		},
-	};
-	uint8_t buffer[1024] = { 0 };
-	struct string_view dest = {
-		.buf = buffer,
-		.len = sizeof(buffer),
-	};
-	struct reftable_buf scratch = REFTABLE_BUF_INIT;
-	struct reftable_buf key = REFTABLE_BUF_INIT;
-	struct reftable_record out = {
-		.type = BLOCK_TYPE_INDEX,
-		.u.idx = { .last_key = REFTABLE_BUF_INIT },
-	};
-	int n, m;
-	uint8_t extra;
-
-	check(!reftable_buf_addstr(&in.u.idx.last_key, "refs/heads/master"));
-	reftable_record_key(&in, &key);
-	t_copy(&in);
-
-	check(!reftable_record_is_deletion(&in));
-	check(!reftable_buf_cmp(&key, &in.u.idx.last_key));
-	n = reftable_record_encode(&in, dest, REFTABLE_HASH_SIZE_SHA1);
-	check_int(n, >, 0);
-
-	extra = reftable_record_val_type(&in);
-	m = reftable_record_decode(&out, key, extra, dest, REFTABLE_HASH_SIZE_SHA1,
-				   &scratch);
-	check_int(m, ==, n);
-
-	check(reftable_record_equal(&in, &out, REFTABLE_HASH_SIZE_SHA1));
-
-	reftable_record_release(&out);
-	reftable_buf_release(&key);
-	reftable_buf_release(&scratch);
-	reftable_buf_release(&in.u.idx.last_key);
-}
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	TEST(t_reftable_ref_record_comparison(), "comparison operations work on ref record");
-	TEST(t_reftable_log_record_comparison(), "comparison operations work on log record");
-	TEST(t_reftable_index_record_comparison(), "comparison operations work on index record");
-	TEST(t_reftable_obj_record_comparison(), "comparison operations work on obj record");
-	TEST(t_reftable_ref_record_compare_name(), "reftable_ref_record_compare_name works");
-	TEST(t_reftable_log_record_compare_key(), "reftable_log_record_compare_key works");
-	TEST(t_reftable_log_record_roundtrip(), "record operations work on log record");
-	TEST(t_reftable_ref_record_roundtrip(), "record operations work on ref record");
-	TEST(t_varint_roundtrip(), "put_var_int and get_var_int work");
-	TEST(t_varint_overflow(), "get_var_int notices an integer overflow");
-	TEST(t_key_roundtrip(), "reftable_encode_key and reftable_decode_key work");
-	TEST(t_reftable_obj_record_roundtrip(), "record operations work on obj record");
-	TEST(t_reftable_index_record_roundtrip(), "record operations work on index record");
-
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-record.c b/t/unit-tests/u-reftable-record.c
new file mode 100644
index 0000000000..ac2e33584c
--- /dev/null
+++ b/t/unit-tests/u-reftable-record.c
@@ -0,0 +1,565 @@
+/*
+  Copyright 2020 Google LLC
+
+  Use of this source code is governed by a BSD-style
+  license that can be found in the LICENSE file or at
+  https://developers.google.com/open-source/licenses/bsd
+*/
+
+#include "unit-test.h"
+#include "reftable/basics.h"
+#include "reftable/constants.h"
+#include "reftable/record.h"
+
+static void t_copy(struct reftable_record *rec)
+{
+	struct reftable_record copy;
+	uint8_t typ;
+
+	typ = reftable_record_type(rec);
+	cl_assert(reftable_record_init(&copy, typ) == 0);
+	reftable_record_copy_from(&copy, rec, REFTABLE_HASH_SIZE_SHA1);
+	/* do it twice to catch memory leaks */
+	reftable_record_copy_from(&copy, rec, REFTABLE_HASH_SIZE_SHA1);
+	cl_assert(reftable_record_equal(rec, &copy, REFTABLE_HASH_SIZE_SHA1) != 0);
+
+	reftable_record_release(&copy);
+}
+
+void test_reftable_record__varint_roundtrip(void)
+{
+	uint64_t inputs[] = { 0,
+			      1,
+			      27,
+			      127,
+			      128,
+			      257,
+			      4096,
+			      ((uint64_t)1 << 63),
+			      ((uint64_t)1 << 63) + ((uint64_t)1 << 63) - 1 };
+
+	for (size_t i = 0; i < ARRAY_SIZE(inputs); i++) {
+		uint8_t dest[10];
+
+		struct string_view out = {
+			.buf = dest,
+			.len = sizeof(dest),
+		};
+		uint64_t in = inputs[i];
+		int n = put_var_int(&out, in);
+		uint64_t got = 0;
+
+		cl_assert(n > 0);
+		out.len = n;
+		n = get_var_int(&got, &out);
+		cl_assert(n > 0);
+
+		cl_assert_equal_i(got, in);
+	}
+}
+
+void test_reftable_record__varint_overflow(void)
+{
+	unsigned char buf[] = {
+		0xFF, 0xFF, 0xFF, 0xFF,
+		0xFF, 0xFF, 0xFF, 0xFF,
+		0xFF, 0x00,
+	};
+	struct string_view view = {
+		.buf = buf,
+		.len = sizeof(buf),
+	};
+	uint64_t value;
+	cl_assert_equal_i(get_var_int(&value, &view), -1);
+}
+
+static void set_hash(uint8_t *h, int j)
+{
+	for (size_t i = 0; i < hash_size(REFTABLE_HASH_SHA1); i++)
+		h[i] = (j >> i) & 0xff;
+}
+
+void test_reftable_record__ref_record_comparison(void)
+{
+	struct reftable_record in[3] = {
+		{
+			.type = BLOCK_TYPE_REF,
+			.u.ref.refname = (char *) "refs/heads/master",
+			.u.ref.value_type = REFTABLE_REF_VAL1,
+		},
+		{
+			.type = BLOCK_TYPE_REF,
+			.u.ref.refname = (char *) "refs/heads/master",
+			.u.ref.value_type = REFTABLE_REF_DELETION,
+		},
+		{
+			.type = BLOCK_TYPE_REF,
+			.u.ref.refname = (char *) "HEAD",
+			.u.ref.value_type = REFTABLE_REF_SYMREF,
+			.u.ref.value.symref = (char *) "refs/heads/master",
+		},
+	};
+	int cmp;
+
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+	cl_assert(cmp == 0);
+
+	cl_assert(reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_cmp(&in[1], &in[2], &cmp) == 0);
+	cl_assert(cmp > 0);
+
+	in[1].u.ref.value_type = in[0].u.ref.value_type;
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) != 0);
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+	cl_assert(cmp == 0);
+}
+
+void test_reftable_record__ref_record_compare_name(void)
+{
+	struct reftable_ref_record recs[3] = {
+		{
+			.refname = (char *) "refs/heads/a"
+		},
+		{
+			.refname = (char *) "refs/heads/b"
+		},
+		{
+			.refname = (char *) "refs/heads/a"
+		},
+	};
+
+	cl_assert(reftable_ref_record_compare_name(&recs[0], &recs[1]) < 0);
+	cl_assert(reftable_ref_record_compare_name(&recs[1], &recs[0]) > 0);
+	cl_assert_equal_i(reftable_ref_record_compare_name(&recs[0], &recs[2]), 0);
+}
+
+void test_reftable_record__ref_record_roundtrip(void)
+{
+	struct reftable_buf scratch = REFTABLE_BUF_INIT;
+
+	for (int i = REFTABLE_REF_DELETION; i < REFTABLE_NR_REF_VALUETYPES; i++) {
+		struct reftable_record in = {
+			.type = BLOCK_TYPE_REF,
+			.u.ref.value_type = i,
+		};
+		struct reftable_record out = { .type = BLOCK_TYPE_REF };
+		struct reftable_buf key = REFTABLE_BUF_INIT;
+		uint8_t buffer[1024] = { 0 };
+		struct string_view dest = {
+			.buf = buffer,
+			.len = sizeof(buffer),
+		};
+		int n, m;
+
+		in.u.ref.value_type = i;
+		switch (i) {
+		case REFTABLE_REF_DELETION:
+			break;
+		case REFTABLE_REF_VAL1:
+			set_hash(in.u.ref.value.val1, 1);
+			break;
+		case REFTABLE_REF_VAL2:
+			set_hash(in.u.ref.value.val2.value, 1);
+			set_hash(in.u.ref.value.val2.target_value, 2);
+			break;
+		case REFTABLE_REF_SYMREF:
+			in.u.ref.value.symref = xstrdup("target");
+			break;
+		}
+		in.u.ref.refname = xstrdup("refs/heads/master");
+
+		t_copy(&in);
+
+		cl_assert_equal_i(reftable_record_val_type(&in), i);
+		cl_assert_equal_i(reftable_record_is_deletion(&in),  i == REFTABLE_REF_DELETION);
+
+		reftable_record_key(&in, &key);
+		n = reftable_record_encode(&in, dest, REFTABLE_HASH_SIZE_SHA1);
+		cl_assert(n > 0);
+
+		/* decode into a non-zero reftable_record to test for leaks. */
+		m = reftable_record_decode(&out, key, i, dest, REFTABLE_HASH_SIZE_SHA1, &scratch);
+		cl_assert_equal_i(n, m);
+
+		cl_assert(reftable_ref_record_equal(&in.u.ref, &out.u.ref,
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+		reftable_record_release(&in);
+
+		reftable_buf_release(&key);
+		reftable_record_release(&out);
+	}
+
+	reftable_buf_release(&scratch);
+}
+
+void test_reftable_record__log_record_comparison(void)
+{
+	struct reftable_record in[3] = {
+		{
+			.type = BLOCK_TYPE_LOG,
+			.u.log.refname = (char *) "refs/heads/master",
+			.u.log.update_index = 42,
+		},
+		{
+			.type = BLOCK_TYPE_LOG,
+			.u.log.refname = (char *) "refs/heads/master",
+			.u.log.update_index = 22,
+		},
+		{
+			.type = BLOCK_TYPE_LOG,
+			.u.log.refname = (char *) "refs/heads/main",
+			.u.log.update_index = 22,
+		},
+	};
+	int cmp;
+
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_cmp(&in[1], &in[2], &cmp) == 0);
+	cl_assert(cmp > 0);
+	/* comparison should be reversed for equal keys, because
+	 * comparison is now performed on the basis of update indices */
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+	cl_assert(cmp < 0);
+
+	in[1].u.log.update_index = in[0].u.log.update_index;
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) != 0);
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+}
+
+void test_reftable_record__log_record_compare_key(void)
+{
+	struct reftable_log_record logs[3] = {
+		{
+			.refname = (char *) "refs/heads/a",
+			.update_index = 1,
+		},
+		{
+			.refname = (char *) "refs/heads/b",
+			.update_index = 2,
+		},
+		{
+			.refname = (char *) "refs/heads/a",
+			.update_index = 3,
+		},
+	};
+
+	cl_assert(reftable_log_record_compare_key(&logs[0], &logs[1]) < 0);
+	cl_assert(reftable_log_record_compare_key(&logs[1], &logs[0]) > 0);
+
+	logs[1].update_index = logs[0].update_index;
+	cl_assert(reftable_log_record_compare_key(&logs[0], &logs[1]) < 0);
+
+	cl_assert(reftable_log_record_compare_key(&logs[0], &logs[2]) > 0);
+	cl_assert(reftable_log_record_compare_key(&logs[2], &logs[0]) < 0);
+	logs[2].update_index = logs[0].update_index;
+	cl_assert_equal_i(reftable_log_record_compare_key(&logs[0], &logs[2]), 0);
+}
+
+void test_reftable_record__log_record_roundtrip(void)
+{
+	struct reftable_log_record in[] = {
+		{
+			.refname = xstrdup("refs/heads/master"),
+			.update_index = 42,
+			.value_type = REFTABLE_LOG_UPDATE,
+			.value = {
+				.update = {
+					.name = xstrdup("han-wen"),
+					.email = xstrdup("hanwen@google.com"),
+					.message = xstrdup("test"),
+					.time = 1577123507,
+					.tz_offset = 100,
+				},
+			}
+		},
+		{
+			.refname = xstrdup("refs/heads/master"),
+			.update_index = 22,
+			.value_type = REFTABLE_LOG_DELETION,
+		},
+		{
+			.refname = xstrdup("branch"),
+			.update_index = 33,
+			.value_type = REFTABLE_LOG_UPDATE,
+		}
+	};
+	struct reftable_buf scratch = REFTABLE_BUF_INIT;
+	set_hash(in[0].value.update.new_hash, 1);
+	set_hash(in[0].value.update.old_hash, 2);
+	set_hash(in[2].value.update.new_hash, 3);
+	set_hash(in[2].value.update.old_hash, 4);
+
+	cl_assert(reftable_log_record_is_deletion(&in[0]) == 0);
+	cl_assert(reftable_log_record_is_deletion(&in[1]) != 0);
+	cl_assert(reftable_log_record_is_deletion(&in[2]) == 0);
+
+	for (size_t i = 0; i < ARRAY_SIZE(in); i++) {
+		struct reftable_record rec = { .type = BLOCK_TYPE_LOG };
+		struct reftable_buf key = REFTABLE_BUF_INIT;
+		uint8_t buffer[1024] = { 0 };
+		struct string_view dest = {
+			.buf = buffer,
+			.len = sizeof(buffer),
+		};
+		/* populate out, to check for leaks. */
+		struct reftable_record out = {
+			.type = BLOCK_TYPE_LOG,
+			.u.log = {
+				.refname = xstrdup("old name"),
+				.value_type = REFTABLE_LOG_UPDATE,
+				.value = {
+					.update = {
+						.name = xstrdup("old name"),
+						.email = xstrdup("old@email"),
+						.message = xstrdup("old message"),
+					},
+				},
+			},
+		};
+		int n, m, valtype;
+
+		rec.u.log = in[i];
+
+		t_copy(&rec);
+
+		reftable_record_key(&rec, &key);
+
+		n = reftable_record_encode(&rec, dest, REFTABLE_HASH_SIZE_SHA1);
+		cl_assert(n >= 0);
+		valtype = reftable_record_val_type(&rec);
+		m = reftable_record_decode(&out, key, valtype, dest,
+					   REFTABLE_HASH_SIZE_SHA1, &scratch);
+		cl_assert_equal_i(n, m);
+
+		cl_assert(reftable_log_record_equal(&in[i], &out.u.log,
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+		reftable_log_record_release(&in[i]);
+		reftable_buf_release(&key);
+		reftable_record_release(&out);
+	}
+
+	reftable_buf_release(&scratch);
+}
+
+void test_reftable_record__key_roundtrip(void)
+{
+	uint8_t buffer[1024] = { 0 };
+	struct string_view dest = {
+		.buf = buffer,
+		.len = sizeof(buffer),
+	};
+	struct reftable_buf last_key = REFTABLE_BUF_INIT;
+	struct reftable_buf key = REFTABLE_BUF_INIT;
+	struct reftable_buf roundtrip = REFTABLE_BUF_INIT;
+	int restart;
+	uint8_t extra;
+	int n, m;
+	uint8_t rt_extra;
+
+	cl_assert(reftable_buf_addstr(&last_key, "refs/heads/master") == 0);
+	cl_assert(reftable_buf_addstr(&key, "refs/tags/bla") == 0);
+	extra = 6;
+	n = reftable_encode_key(&restart, dest, last_key, key, extra);
+	cl_assert(restart == 0);
+	cl_assert(n > 0);
+
+	cl_assert(reftable_buf_addstr(&roundtrip, "refs/heads/master") == 0);
+	m = reftable_decode_key(&roundtrip, &rt_extra, dest);
+	cl_assert_equal_i(n, m);
+	cl_assert(reftable_buf_cmp(&key, &roundtrip) == 0);
+	cl_assert_equal_i(rt_extra, extra);
+
+	reftable_buf_release(&last_key);
+	reftable_buf_release(&key);
+	reftable_buf_release(&roundtrip);
+}
+
+void test_reftable_record__obj_record_comparison(void)
+{
+
+	uint8_t id_bytes[] = { 0, 1, 2, 3, 4, 5, 6 };
+	uint64_t offsets[] = { 0, 16, 32, 48, 64, 80, 96, 112};
+	struct reftable_record in[3] = {
+		{
+			.type = BLOCK_TYPE_OBJ,
+			.u.obj.hash_prefix = id_bytes,
+			.u.obj.hash_prefix_len = 7,
+			.u.obj.offsets = offsets,
+			.u.obj.offset_len = 8,
+		},
+		{
+			.type = BLOCK_TYPE_OBJ,
+			.u.obj.hash_prefix = id_bytes,
+			.u.obj.hash_prefix_len = 7,
+			.u.obj.offsets = offsets,
+			.u.obj.offset_len = 5,
+		},
+		{
+			.type = BLOCK_TYPE_OBJ,
+			.u.obj.hash_prefix = id_bytes,
+			.u.obj.hash_prefix_len = 5,
+		},
+	};
+	int cmp;
+
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+	cl_assert(cmp == 0);
+
+	cl_assert(reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_cmp(&in[1], &in[2], &cmp) == 0);
+	cl_assert(cmp > 0);
+
+	in[1].u.obj.offset_len = in[0].u.obj.offset_len;
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) != 0);
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+	cl_assert(cmp == 0);
+}
+
+void test_reftable_record__obj_record_roundtrip(void)
+{
+	uint8_t testHash1[REFTABLE_HASH_SIZE_SHA1] = { 1, 2, 3, 4, 0 };
+	uint64_t till9[] = { 1, 2, 3, 4, 500, 600, 700, 800, 9000 };
+	struct reftable_obj_record recs[3] = {
+		{
+			.hash_prefix = testHash1,
+			.hash_prefix_len = 5,
+			.offsets = till9,
+			.offset_len = 3,
+		},
+		{
+			.hash_prefix = testHash1,
+			.hash_prefix_len = 5,
+			.offsets = till9,
+			.offset_len = 9,
+		},
+		{
+			.hash_prefix = testHash1,
+			.hash_prefix_len = 5,
+		},
+	};
+	struct reftable_buf scratch = REFTABLE_BUF_INIT;
+
+	for (size_t i = 0; i < ARRAY_SIZE(recs); i++) {
+		uint8_t buffer[1024] = { 0 };
+		struct string_view dest = {
+			.buf = buffer,
+			.len = sizeof(buffer),
+		};
+		struct reftable_record in = {
+			.type = BLOCK_TYPE_OBJ,
+			.u = {
+				.obj = recs[i],
+			},
+		};
+		struct reftable_buf key = REFTABLE_BUF_INIT;
+		struct reftable_record out = { .type = BLOCK_TYPE_OBJ };
+		int n, m;
+		uint8_t extra;
+
+		cl_assert(reftable_record_is_deletion(&in) == 0);
+		t_copy(&in);
+		reftable_record_key(&in, &key);
+		n = reftable_record_encode(&in, dest, REFTABLE_HASH_SIZE_SHA1);
+		cl_assert(n > 0);
+		extra = reftable_record_val_type(&in);
+		m = reftable_record_decode(&out, key, extra, dest,
+					   REFTABLE_HASH_SIZE_SHA1, &scratch);
+		cl_assert_equal_i(n, m);
+
+		cl_assert(reftable_record_equal(&in, &out, REFTABLE_HASH_SIZE_SHA1) != 0);
+		reftable_buf_release(&key);
+		reftable_record_release(&out);
+	}
+
+	reftable_buf_release(&scratch);
+}
+
+void test_reftable_record__index_record_comparison(void)
+{
+	struct reftable_record in[3] = {
+		{
+			.type = BLOCK_TYPE_INDEX,
+			.u.idx.offset = 22,
+			.u.idx.last_key = REFTABLE_BUF_INIT,
+		},
+		{
+			.type = BLOCK_TYPE_INDEX,
+			.u.idx.offset = 32,
+			.u.idx.last_key = REFTABLE_BUF_INIT,
+		},
+		{
+			.type = BLOCK_TYPE_INDEX,
+			.u.idx.offset = 32,
+			.u.idx.last_key = REFTABLE_BUF_INIT,
+		},
+	};
+	int cmp;
+
+	cl_assert(reftable_buf_addstr(&in[0].u.idx.last_key, "refs/heads/master") == 0);
+	cl_assert(reftable_buf_addstr(&in[1].u.idx.last_key, "refs/heads/master") == 0);
+	cl_assert(reftable_buf_addstr(&in[2].u.idx.last_key, "refs/heads/branch") == 0);
+
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+	cl_assert(cmp == 0);
+
+	cl_assert(reftable_record_equal(&in[1], &in[2], REFTABLE_HASH_SIZE_SHA1) == 0);
+	cl_assert(reftable_record_cmp(&in[1], &in[2], &cmp) == 0);
+	cl_assert(cmp > 0);
+
+	in[1].u.idx.offset = in[0].u.idx.offset;
+	cl_assert(reftable_record_equal(&in[0], &in[1], REFTABLE_HASH_SIZE_SHA1) != 0);
+	cl_assert(reftable_record_cmp(&in[0], &in[1], &cmp) == 0);
+	cl_assert(cmp == 0);
+
+	for (size_t i = 0; i < ARRAY_SIZE(in); i++)
+		reftable_record_release(&in[i]);
+}
+
+void test_reftable_record__index_record_roundtrip(void)
+{
+	struct reftable_record in = {
+		.type = BLOCK_TYPE_INDEX,
+		.u.idx = {
+			.offset = 42,
+			.last_key = REFTABLE_BUF_INIT,
+		},
+	};
+	uint8_t buffer[1024] = { 0 };
+	struct string_view dest = {
+		.buf = buffer,
+		.len = sizeof(buffer),
+	};
+	struct reftable_buf scratch = REFTABLE_BUF_INIT;
+	struct reftable_buf key = REFTABLE_BUF_INIT;
+	struct reftable_record out = {
+		.type = BLOCK_TYPE_INDEX,
+		.u.idx = { .last_key = REFTABLE_BUF_INIT },
+	};
+	int n, m;
+	uint8_t extra;
+
+	cl_assert(reftable_buf_addstr(&in.u.idx.last_key, "refs/heads/master") == 0);
+	reftable_record_key(&in, &key);
+	t_copy(&in);
+
+	cl_assert(reftable_record_is_deletion(&in) == 0);
+	cl_assert(reftable_buf_cmp(&key, &in.u.idx.last_key) == 0);
+	n = reftable_record_encode(&in, dest, REFTABLE_HASH_SIZE_SHA1);
+	cl_assert(n > 0);
+
+	extra = reftable_record_val_type(&in);
+	m = reftable_record_decode(&out, key, extra, dest, REFTABLE_HASH_SIZE_SHA1,
+				   &scratch);
+	cl_assert_equal_i(m, n);
+
+	cl_assert(reftable_record_equal(&in, &out, REFTABLE_HASH_SIZE_SHA1) != 0);
+
+	reftable_record_release(&out);
+	reftable_buf_release(&key);
+	reftable_buf_release(&scratch);
+	reftable_buf_release(&in.u.idx.last_key);
+}
-- 
2.43.0

[PATCH 9/9] t/unit-tests: convert reftable stack test to use clar

From: Seyi Kuforiji <hidden>
Date: 2025-04-27 18:55:18

Adapt reftable stack test file to use clar by using clar assertions
where necessary.

This marks the end of all unit tests migrated away from the
`unit-tests/t-*.c` pattern, there are no longer any files matching that
glob. Remove the sanity check for `t-*.c` files to prevent Meson
configuration errors during CI and local builds.

Signed-off-by: Seyi Kuforiji <redacted>
---
 Makefile                        |    2 +-
 t/meson.build                   |    3 +-
 t/unit-tests/t-reftable-stack.c | 1451 -------------------------------
 t/unit-tests/u-reftable-stack.c | 1247 ++++++++++++++++++++++++++
 4 files changed, 1249 insertions(+), 1454 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-stack.c
 create mode 100644 t/unit-tests/u-reftable-stack.c
diff --git a/Makefile b/Makefile
index 1af0eb8aa2..7e646e16ee 100644
--- a/Makefile
+++ b/Makefile
@@ -1369,6 +1369,7 @@ CLAR_TEST_SUITES += u-reftable-pq
 CLAR_TEST_SUITES += u-reftable-reader
 CLAR_TEST_SUITES += u-reftable-readwrite
 CLAR_TEST_SUITES += u-reftable-record
+CLAR_TEST_SUITES += u-reftable-stack
 CLAR_TEST_SUITES += u-reftable-tree
 CLAR_TEST_SUITES += u-strbuf
 CLAR_TEST_SUITES += u-strcmp-offset
@@ -1382,7 +1383,6 @@ CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
 CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/unit-test.o
 
-UNIT_TEST_PROGRAMS += t-reftable-stack
 UNIT_TEST_PROGS = $(patsubst %,$(UNIT_TEST_BIN)/%$X,$(UNIT_TEST_PROGRAMS))
 UNIT_TEST_OBJS += $(UNIT_TEST_DIR)/test-lib.o
 
diff --git a/t/meson.build b/t/meson.build
index bd0292f0c7..7c305a90b5 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -15,6 +15,7 @@ clar_test_suites = [
   'unit-tests/u-reftable-reader.c',
   'unit-tests/u-reftable-readwrite.c',
   'unit-tests/u-reftable-record.c',
+  'unit-tests/u-reftable-stack.c',
   'unit-tests/u-reftable-tree.c',
   'unit-tests/u-strbuf.c',
   'unit-tests/u-strcmp-offset.c',
@@ -62,7 +63,6 @@ clar_unit_tests = executable('unit-tests',
 test('unit-tests', clar_unit_tests)
 
 unit_test_programs = [
-  'unit-tests/t-reftable-stack.c',
 ]
 
 foreach unit_test_program : unit_test_programs
@@ -1102,7 +1102,6 @@ integration_tests = [
 # sufficient to catch missing test suites in our CI though.
 foreach glob, tests : {
   't[0-9][0-9][0-9][0-9]-*.sh': integration_tests,
-  'unit-tests/t-*.c': unit_test_programs,
   'unit-tests/u-*.c': clar_test_suites,
 }
   actual_tests = run_command(shell, '-c', 'ls ' + glob,
diff --git a/t/unit-tests/t-reftable-stack.c b/t/unit-tests/t-reftable-stack.c
deleted file mode 100644
index c3f0059c34..0000000000
--- a/t/unit-tests/t-reftable-stack.c
+++ /dev/null
@@ -1,1451 +0,0 @@
-/*
-Copyright 2020 Google LLC
-
-Use of this source code is governed by a BSD-style
-license that can be found in the LICENSE file or at
-https://developers.google.com/open-source/licenses/bsd
-*/
-
-#define DISABLE_SIGN_COMPARE_WARNINGS
-
-#include "test-lib.h"
-#include "lib-reftable.h"
-#include "dir.h"
-#include "reftable/merged.h"
-#include "reftable/reader.h"
-#include "reftable/reftable-error.h"
-#include "reftable/stack.h"
-#include "strbuf.h"
-#include "tempfile.h"
-#include <dirent.h>
-
-static void clear_dir(const char *dirname)
-{
-	struct strbuf path = REFTABLE_BUF_INIT;
-	strbuf_addstr(&path, dirname);
-	remove_dir_recursively(&path, 0);
-	strbuf_release(&path);
-}
-
-static int count_dir_entries(const char *dirname)
-{
-	DIR *dir = opendir(dirname);
-	int len = 0;
-	struct dirent *d;
-	if (!dir)
-		return 0;
-
-	while ((d = readdir(dir))) {
-		/*
-		 * Besides skipping over "." and "..", we also need to
-		 * skip over other files that have a leading ".". This
-		 * is due to behaviour of NFS, which will rename files
-		 * to ".nfs*" to emulate delete-on-last-close.
-		 *
-		 * In any case this should be fine as the reftable
-		 * library will never write files with leading dots
-		 * anyway.
-		 */
-		if (starts_with(d->d_name, "."))
-			continue;
-		len++;
-	}
-	closedir(dir);
-	return len;
-}
-
-/*
- * Work linenumber into the tempdir, so we can see which tests forget to
- * cleanup.
- */
-static char *get_tmp_template(int linenumber)
-{
-	const char *tmp = getenv("TMPDIR");
-	static char template[1024];
-	snprintf(template, sizeof(template) - 1, "%s/stack_test-%d.XXXXXX",
-		 tmp ? tmp : "/tmp", linenumber);
-	return template;
-}
-
-static char *get_tmp_dir(int linenumber)
-{
-	char *dir = get_tmp_template(linenumber);
-	check(mkdtemp(dir) != NULL);
-	return dir;
-}
-
-static void t_read_file(void)
-{
-	char *fn = get_tmp_template(__LINE__);
-	struct tempfile *tmp = mks_tempfile(fn);
-	int fd = get_tempfile_fd(tmp);
-	char out[1024] = "line1\n\nline2\nline3";
-	int n, err;
-	char **names = NULL;
-	const char *want[] = { "line1", "line2", "line3" };
-
-	check_int(fd, >, 0);
-	n = write_in_full(fd, out, strlen(out));
-	check_int(n, ==, strlen(out));
-	err = close(fd);
-	check_int(err, >=, 0);
-
-	err = read_lines(fn, &names);
-	check(!err);
-
-	for (size_t i = 0; names[i]; i++)
-		check_str(want[i], names[i]);
-	free_names(names);
-	(void) remove(fn);
-	delete_tempfile(&tmp);
-}
-
-static int write_test_ref(struct reftable_writer *wr, void *arg)
-{
-	struct reftable_ref_record *ref = arg;
-	check(!reftable_writer_set_limits(wr, ref->update_index,
-					  ref->update_index));
-	return reftable_writer_add_ref(wr, ref);
-}
-
-static void write_n_ref_tables(struct reftable_stack *st,
-			       size_t n)
-{
-	int disable_auto_compact;
-	int err;
-
-	disable_auto_compact = st->opts.disable_auto_compact;
-	st->opts.disable_auto_compact = 1;
-
-	for (size_t i = 0; i < n; i++) {
-		struct reftable_ref_record ref = {
-			.update_index = reftable_stack_next_update_index(st),
-			.value_type = REFTABLE_REF_VAL1,
-		};
-		char buf[128];
-
-		snprintf(buf, sizeof(buf), "refs/heads/branch-%04"PRIuMAX, (uintmax_t)i);
-		ref.refname = buf;
-		t_reftable_set_hash(ref.value.val1, i, REFTABLE_HASH_SHA1);
-
-		err = reftable_stack_add(st, &write_test_ref, &ref);
-		check(!err);
-	}
-
-	st->opts.disable_auto_compact = disable_auto_compact;
-}
-
-struct write_log_arg {
-	struct reftable_log_record *log;
-	uint64_t update_index;
-};
-
-static int write_test_log(struct reftable_writer *wr, void *arg)
-{
-	struct write_log_arg *wla = arg;
-
-	check(!reftable_writer_set_limits(wr, wla->update_index,
-					  wla->update_index));
-	return reftable_writer_add_log(wr, wla->log);
-}
-
-static void t_reftable_stack_add_one(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_buf scratch = REFTABLE_BUF_INIT;
-	int mask = umask(002);
-	struct reftable_write_options opts = {
-		.default_permissions = 0660,
-	};
-	struct reftable_stack *st = NULL;
-	int err;
-	struct reftable_ref_record ref = {
-		.refname = (char *) "HEAD",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "master",
-	};
-	struct reftable_ref_record dest = { 0 };
-	struct stat stat_result = { 0 };
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_add(st, write_test_ref, &ref);
-	check(!err);
-
-	err = reftable_stack_read_ref(st, ref.refname, &dest);
-	check(!err);
-	check(reftable_ref_record_equal(&ref, &dest, REFTABLE_HASH_SIZE_SHA1));
-	check_int(st->readers_len, >, 0);
-
-#ifndef GIT_WINDOWS_NATIVE
-	check(!reftable_buf_addstr(&scratch, dir));
-	check(!reftable_buf_addstr(&scratch, "/tables.list"));
-	err = stat(scratch.buf, &stat_result);
-	check(!err);
-	check_int((stat_result.st_mode & 0777), ==, opts.default_permissions);
-
-	reftable_buf_reset(&scratch);
-	check(!reftable_buf_addstr(&scratch, dir));
-	check(!reftable_buf_addstr(&scratch, "/"));
-	/* do not try at home; not an external API for reftable. */
-	check(!reftable_buf_addstr(&scratch, st->readers[0]->name));
-	err = stat(scratch.buf, &stat_result);
-	check(!err);
-	check_int((stat_result.st_mode & 0777), ==, opts.default_permissions);
-#else
-	(void) stat_result;
-#endif
-
-	reftable_ref_record_release(&dest);
-	reftable_stack_destroy(st);
-	reftable_buf_release(&scratch);
-	clear_dir(dir);
-	umask(mask);
-}
-
-static void t_reftable_stack_uptodate(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st1 = NULL;
-	struct reftable_stack *st2 = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-
-	int err;
-	struct reftable_ref_record ref1 = {
-		.refname = (char *) "HEAD",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "master",
-	};
-	struct reftable_ref_record ref2 = {
-		.refname = (char *) "branch2",
-		.update_index = 2,
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "master",
-	};
-
-
-	/* simulate multi-process access to the same stack
-	   by creating two stacks for the same directory.
-	 */
-	err = reftable_new_stack(&st1, dir, &opts);
-	check(!err);
-
-	err = reftable_new_stack(&st2, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_add(st1, write_test_ref, &ref1);
-	check(!err);
-
-	err = reftable_stack_add(st2, write_test_ref, &ref2);
-	check_int(err, ==, REFTABLE_OUTDATED_ERROR);
-
-	err = reftable_stack_reload(st2);
-	check(!err);
-
-	err = reftable_stack_add(st2, write_test_ref, &ref2);
-	check(!err);
-	reftable_stack_destroy(st1);
-	reftable_stack_destroy(st2);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_transaction_api(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	int err;
-	struct reftable_addition *add = NULL;
-
-	struct reftable_ref_record ref = {
-		.refname = (char *) "HEAD",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "master",
-	};
-	struct reftable_ref_record dest = { 0 };
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	reftable_addition_destroy(add);
-
-	err = reftable_stack_new_addition(&add, st, 0);
-	check(!err);
-
-	err = reftable_addition_add(add, write_test_ref, &ref);
-	check(!err);
-
-	err = reftable_addition_commit(add);
-	check(!err);
-
-	reftable_addition_destroy(add);
-
-	err = reftable_stack_read_ref(st, ref.refname, &dest);
-	check(!err);
-	check_int(REFTABLE_REF_SYMREF, ==, dest.value_type);
-	check(reftable_ref_record_equal(&ref, &dest, REFTABLE_HASH_SIZE_SHA1));
-
-	reftable_ref_record_release(&dest);
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_transaction_with_reload(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_stack *st1 = NULL, *st2 = NULL;
-	int err;
-	struct reftable_addition *add = NULL;
-	struct reftable_ref_record refs[2] = {
-		{
-			.refname = (char *) "refs/heads/a",
-			.update_index = 1,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { '1' },
-		},
-		{
-			.refname = (char *) "refs/heads/b",
-			.update_index = 2,
-			.value_type = REFTABLE_REF_VAL1,
-			.value.val1 = { '1' },
-		},
-	};
-	struct reftable_ref_record ref = { 0 };
-
-	err = reftable_new_stack(&st1, dir, NULL);
-	check(!err);
-	err = reftable_new_stack(&st2, dir, NULL);
-	check(!err);
-
-	err = reftable_stack_new_addition(&add, st1, 0);
-	check(!err);
-	err = reftable_addition_add(add, write_test_ref, &refs[0]);
-	check(!err);
-	err = reftable_addition_commit(add);
-	check(!err);
-	reftable_addition_destroy(add);
-
-	/*
-	 * The second stack is now outdated, which we should notice. We do not
-	 * create the addition and lock the stack by default, but allow the
-	 * reload to happen when REFTABLE_STACK_NEW_ADDITION_RELOAD is set.
-	 */
-	err = reftable_stack_new_addition(&add, st2, 0);
-	check_int(err, ==, REFTABLE_OUTDATED_ERROR);
-	err = reftable_stack_new_addition(&add, st2, REFTABLE_STACK_NEW_ADDITION_RELOAD);
-	check(!err);
-	err = reftable_addition_add(add, write_test_ref, &refs[1]);
-	check(!err);
-	err = reftable_addition_commit(add);
-	check(!err);
-	reftable_addition_destroy(add);
-
-	for (size_t i = 0; i < ARRAY_SIZE(refs); i++) {
-		err = reftable_stack_read_ref(st2, refs[i].refname, &ref);
-		check(!err);
-		check(reftable_ref_record_equal(&refs[i], &ref, REFTABLE_HASH_SIZE_SHA1));
-	}
-
-	reftable_ref_record_release(&ref);
-	reftable_stack_destroy(st1);
-	reftable_stack_destroy(st2);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_transaction_api_performs_auto_compaction(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_write_options opts = {0};
-	struct reftable_addition *add = NULL;
-	struct reftable_stack *st = NULL;
-	size_t n = 20;
-	int err;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	for (size_t i = 0; i <= n; i++) {
-		struct reftable_ref_record ref = {
-			.update_index = reftable_stack_next_update_index(st),
-			.value_type = REFTABLE_REF_SYMREF,
-			.value.symref = (char *) "master",
-		};
-		char name[100];
-
-		snprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i);
-		ref.refname = name;
-
-		/*
-		 * Disable auto-compaction for all but the last runs. Like this
-		 * we can ensure that we indeed honor this setting and have
-		 * better control over when exactly auto compaction runs.
-		 */
-		st->opts.disable_auto_compact = i != n;
-
-		err = reftable_stack_new_addition(&add, st, 0);
-		check(!err);
-
-		err = reftable_addition_add(add, write_test_ref, &ref);
-		check(!err);
-
-		err = reftable_addition_commit(add);
-		check(!err);
-
-		reftable_addition_destroy(add);
-
-		/*
-		 * The stack length should grow continuously for all runs where
-		 * auto compaction is disabled. When enabled, we should merge
-		 * all tables in the stack.
-		 */
-		if (i != n)
-			check_int(st->merged->readers_len, ==, i + 1);
-		else
-			check_int(st->merged->readers_len, ==, 1);
-	}
-
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_auto_compaction_fails_gracefully(void)
-{
-	struct reftable_ref_record ref = {
-		.refname = (char *) "refs/heads/master",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_VAL1,
-		.value.val1 = {0x01},
-	};
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st;
-	struct reftable_buf table_path = REFTABLE_BUF_INIT;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_add(st, write_test_ref, &ref);
-	check(!err);
-	check_int(st->merged->readers_len, ==, 1);
-	check_int(st->stats.attempts, ==, 0);
-	check_int(st->stats.failures, ==, 0);
-
-	/*
-	 * Lock the newly written table such that it cannot be compacted.
-	 * Adding a new table to the stack should not be impacted by this, even
-	 * though auto-compaction will now fail.
-	 */
-	check(!reftable_buf_addstr(&table_path, dir));
-	check(!reftable_buf_addstr(&table_path, "/"));
-	check(!reftable_buf_addstr(&table_path, st->readers[0]->name));
-	check(!reftable_buf_addstr(&table_path, ".lock"));
-	write_file_buf(table_path.buf, "", 0);
-
-	ref.update_index = 2;
-	err = reftable_stack_add(st, write_test_ref, &ref);
-	check(!err);
-	check_int(st->merged->readers_len, ==, 2);
-	check_int(st->stats.attempts, ==, 1);
-	check_int(st->stats.failures, ==, 1);
-
-	reftable_stack_destroy(st);
-	reftable_buf_release(&table_path);
-	clear_dir(dir);
-}
-
-static int write_error(struct reftable_writer *wr UNUSED, void *arg)
-{
-	return *((int *)arg);
-}
-
-static void t_reftable_stack_update_index_check(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	int err;
-	struct reftable_ref_record ref1 = {
-		.refname = (char *) "name1",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "master",
-	};
-	struct reftable_ref_record ref2 = {
-		.refname = (char *) "name2",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "master",
-	};
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_add(st, write_test_ref, &ref1);
-	check(!err);
-
-	err = reftable_stack_add(st, write_test_ref, &ref2);
-	check_int(err, ==, REFTABLE_API_ERROR);
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_lock_failure(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	int err, i;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-	for (i = -1; i != REFTABLE_EMPTY_TABLE_ERROR; i--) {
-		err = reftable_stack_add(st, write_error, &i);
-		check_int(err, ==, i);
-	}
-
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_add(void)
-{
-	int err = 0;
-	struct reftable_write_options opts = {
-		.exact_log_message = 1,
-		.default_permissions = 0660,
-		.disable_auto_compact = 1,
-	};
-	struct reftable_stack *st = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_ref_record refs[2] = { 0 };
-	struct reftable_log_record logs[2] = { 0 };
-	struct reftable_buf path = REFTABLE_BUF_INIT;
-	struct stat stat_result;
-	size_t i, N = ARRAY_SIZE(refs);
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	for (i = 0; i < N; i++) {
-		char buf[256];
-		snprintf(buf, sizeof(buf), "branch%02"PRIuMAX, (uintmax_t)i);
-		refs[i].refname = xstrdup(buf);
-		refs[i].update_index = i + 1;
-		refs[i].value_type = REFTABLE_REF_VAL1;
-		t_reftable_set_hash(refs[i].value.val1, i, REFTABLE_HASH_SHA1);
-
-		logs[i].refname = xstrdup(buf);
-		logs[i].update_index = N + i + 1;
-		logs[i].value_type = REFTABLE_LOG_UPDATE;
-		logs[i].value.update.email = xstrdup("identity@invalid");
-		t_reftable_set_hash(logs[i].value.update.new_hash, i, REFTABLE_HASH_SHA1);
-	}
-
-	for (i = 0; i < N; i++) {
-		int err = reftable_stack_add(st, write_test_ref, &refs[i]);
-		check(!err);
-	}
-
-	for (i = 0; i < N; i++) {
-		struct write_log_arg arg = {
-			.log = &logs[i],
-			.update_index = reftable_stack_next_update_index(st),
-		};
-		int err = reftable_stack_add(st, write_test_log, &arg);
-		check(!err);
-	}
-
-	err = reftable_stack_compact_all(st, NULL);
-	check(!err);
-
-	for (i = 0; i < N; i++) {
-		struct reftable_ref_record dest = { 0 };
-
-		int err = reftable_stack_read_ref(st, refs[i].refname, &dest);
-		check(!err);
-		check(reftable_ref_record_equal(&dest, refs + i,
-						 REFTABLE_HASH_SIZE_SHA1));
-		reftable_ref_record_release(&dest);
-	}
-
-	for (i = 0; i < N; i++) {
-		struct reftable_log_record dest = { 0 };
-		int err = reftable_stack_read_log(st, refs[i].refname, &dest);
-		check(!err);
-		check(reftable_log_record_equal(&dest, logs + i,
-						 REFTABLE_HASH_SIZE_SHA1));
-		reftable_log_record_release(&dest);
-	}
-
-#ifndef GIT_WINDOWS_NATIVE
-	check(!reftable_buf_addstr(&path, dir));
-	check(!reftable_buf_addstr(&path, "/tables.list"));
-	err = stat(path.buf, &stat_result);
-	check(!err);
-	check_int((stat_result.st_mode & 0777), ==, opts.default_permissions);
-
-	reftable_buf_reset(&path);
-	check(!reftable_buf_addstr(&path, dir));
-	check(!reftable_buf_addstr(&path, "/"));
-	/* do not try at home; not an external API for reftable. */
-	check(!reftable_buf_addstr(&path, st->readers[0]->name));
-	err = stat(path.buf, &stat_result);
-	check(!err);
-	check_int((stat_result.st_mode & 0777), ==, opts.default_permissions);
-#else
-	(void) stat_result;
-#endif
-
-	/* cleanup */
-	reftable_stack_destroy(st);
-	for (i = 0; i < N; i++) {
-		reftable_ref_record_release(&refs[i]);
-		reftable_log_record_release(&logs[i]);
-	}
-	reftable_buf_release(&path);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_iterator(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_ref_record refs[10] = { 0 };
-	struct reftable_log_record logs[10] = { 0 };
-	struct reftable_iterator it = { 0 };
-	size_t N = ARRAY_SIZE(refs), i;
-	int err;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	for (i = 0; i < N; i++) {
-		refs[i].refname = xstrfmt("branch%02"PRIuMAX, (uintmax_t)i);
-		refs[i].update_index = i + 1;
-		refs[i].value_type = REFTABLE_REF_VAL1;
-		t_reftable_set_hash(refs[i].value.val1, i, REFTABLE_HASH_SHA1);
-
-		logs[i].refname = xstrfmt("branch%02"PRIuMAX, (uintmax_t)i);
-		logs[i].update_index = i + 1;
-		logs[i].value_type = REFTABLE_LOG_UPDATE;
-		logs[i].value.update.email = xstrdup("johndoe@invalid");
-		logs[i].value.update.message = xstrdup("commit\n");
-		t_reftable_set_hash(logs[i].value.update.new_hash, i, REFTABLE_HASH_SHA1);
-	}
-
-	for (i = 0; i < N; i++) {
-		err = reftable_stack_add(st, write_test_ref, &refs[i]);
-		check(!err);
-	}
-
-	for (i = 0; i < N; i++) {
-		struct write_log_arg arg = {
-			.log = &logs[i],
-			.update_index = reftable_stack_next_update_index(st),
-		};
-
-		err = reftable_stack_add(st, write_test_log, &arg);
-		check(!err);
-	}
-
-	reftable_stack_init_ref_iterator(st, &it);
-	reftable_iterator_seek_ref(&it, refs[0].refname);
-	for (i = 0; ; i++) {
-		struct reftable_ref_record ref = { 0 };
-
-		err = reftable_iterator_next_ref(&it, &ref);
-		if (err > 0)
-			break;
-		check(!err);
-		check(reftable_ref_record_equal(&ref, &refs[i], REFTABLE_HASH_SIZE_SHA1));
-		reftable_ref_record_release(&ref);
-	}
-	check_int(i, ==, N);
-
-	reftable_iterator_destroy(&it);
-
-	err = reftable_stack_init_log_iterator(st, &it);
-	check(!err);
-
-	reftable_iterator_seek_log(&it, logs[0].refname);
-	for (i = 0; ; i++) {
-		struct reftable_log_record log = { 0 };
-
-		err = reftable_iterator_next_log(&it, &log);
-		if (err > 0)
-			break;
-		check(!err);
-		check(reftable_log_record_equal(&log, &logs[i], REFTABLE_HASH_SIZE_SHA1));
-		reftable_log_record_release(&log);
-	}
-	check_int(i, ==, N);
-
-	reftable_stack_destroy(st);
-	reftable_iterator_destroy(&it);
-	for (i = 0; i < N; i++) {
-		reftable_ref_record_release(&refs[i]);
-		reftable_log_record_release(&logs[i]);
-	}
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_log_normalize(void)
-{
-	int err = 0;
-	struct reftable_write_options opts = {
-		0,
-	};
-	struct reftable_stack *st = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_log_record input = {
-		.refname = (char *) "branch",
-		.update_index = 1,
-		.value_type = REFTABLE_LOG_UPDATE,
-		.value = {
-			.update = {
-				.new_hash = { 1 },
-				.old_hash = { 2 },
-			},
-		},
-	};
-	struct reftable_log_record dest = {
-		.update_index = 0,
-	};
-	struct write_log_arg arg = {
-		.log = &input,
-		.update_index = 1,
-	};
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	input.value.update.message = (char *) "one\ntwo";
-	err = reftable_stack_add(st, write_test_log, &arg);
-	check_int(err, ==, REFTABLE_API_ERROR);
-
-	input.value.update.message = (char *) "one";
-	err = reftable_stack_add(st, write_test_log, &arg);
-	check(!err);
-
-	err = reftable_stack_read_log(st, input.refname, &dest);
-	check(!err);
-	check_str(dest.value.update.message, "one\n");
-
-	input.value.update.message = (char *) "two\n";
-	arg.update_index = 2;
-	err = reftable_stack_add(st, write_test_log, &arg);
-	check(!err);
-	err = reftable_stack_read_log(st, input.refname, &dest);
-	check(!err);
-	check_str(dest.value.update.message, "two\n");
-
-	/* cleanup */
-	reftable_stack_destroy(st);
-	reftable_log_record_release(&dest);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_tombstone(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	int err;
-	struct reftable_ref_record refs[2] = { 0 };
-	struct reftable_log_record logs[2] = { 0 };
-	size_t i, N = ARRAY_SIZE(refs);
-	struct reftable_ref_record dest = { 0 };
-	struct reftable_log_record log_dest = { 0 };
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	/* even entries add the refs, odd entries delete them. */
-	for (i = 0; i < N; i++) {
-		const char *buf = "branch";
-		refs[i].refname = xstrdup(buf);
-		refs[i].update_index = i + 1;
-		if (i % 2 == 0) {
-			refs[i].value_type = REFTABLE_REF_VAL1;
-			t_reftable_set_hash(refs[i].value.val1, i,
-					    REFTABLE_HASH_SHA1);
-		}
-
-		logs[i].refname = xstrdup(buf);
-		/*
-		 * update_index is part of the key so should be constant.
-		 * The value itself should be less than the writer's upper
-		 * limit.
-		 */
-		logs[i].update_index = 1;
-		if (i % 2 == 0) {
-			logs[i].value_type = REFTABLE_LOG_UPDATE;
-			t_reftable_set_hash(logs[i].value.update.new_hash, i,
-					    REFTABLE_HASH_SHA1);
-			logs[i].value.update.email =
-				xstrdup("identity@invalid");
-		}
-	}
-	for (i = 0; i < N; i++) {
-		int err = reftable_stack_add(st, write_test_ref, &refs[i]);
-		check(!err);
-	}
-
-	for (i = 0; i < N; i++) {
-		struct write_log_arg arg = {
-			.log = &logs[i],
-			.update_index = reftable_stack_next_update_index(st),
-		};
-		int err = reftable_stack_add(st, write_test_log, &arg);
-		check(!err);
-	}
-
-	err = reftable_stack_read_ref(st, "branch", &dest);
-	check_int(err, ==, 1);
-	reftable_ref_record_release(&dest);
-
-	err = reftable_stack_read_log(st, "branch", &log_dest);
-	check_int(err, ==, 1);
-	reftable_log_record_release(&log_dest);
-
-	err = reftable_stack_compact_all(st, NULL);
-	check(!err);
-
-	err = reftable_stack_read_ref(st, "branch", &dest);
-	check_int(err, ==, 1);
-
-	err = reftable_stack_read_log(st, "branch", &log_dest);
-	check_int(err, ==, 1);
-	reftable_ref_record_release(&dest);
-	reftable_log_record_release(&log_dest);
-
-	/* cleanup */
-	reftable_stack_destroy(st);
-	for (i = 0; i < N; i++) {
-		reftable_ref_record_release(&refs[i]);
-		reftable_log_record_release(&logs[i]);
-	}
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_hash_id(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	int err;
-
-	struct reftable_ref_record ref = {
-		.refname = (char *) "master",
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "target",
-		.update_index = 1,
-	};
-	struct reftable_write_options opts32 = { .hash_id = REFTABLE_HASH_SHA256 };
-	struct reftable_stack *st32 = NULL;
-	struct reftable_write_options opts_default = { 0 };
-	struct reftable_stack *st_default = NULL;
-	struct reftable_ref_record dest = { 0 };
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_add(st, write_test_ref, &ref);
-	check(!err);
-
-	/* can't read it with the wrong hash ID. */
-	err = reftable_new_stack(&st32, dir, &opts32);
-	check_int(err, ==, REFTABLE_FORMAT_ERROR);
-
-	/* check that we can read it back with default opts too. */
-	err = reftable_new_stack(&st_default, dir, &opts_default);
-	check(!err);
-
-	err = reftable_stack_read_ref(st_default, "master", &dest);
-	check(!err);
-
-	check(reftable_ref_record_equal(&ref, &dest, REFTABLE_HASH_SIZE_SHA1));
-	reftable_ref_record_release(&dest);
-	reftable_stack_destroy(st);
-	reftable_stack_destroy(st_default);
-	clear_dir(dir);
-}
-
-static void t_suggest_compaction_segment(void)
-{
-	uint64_t sizes[] = { 512, 64, 17, 16, 9, 9, 9, 16, 2, 16 };
-	struct segment min =
-		suggest_compaction_segment(sizes, ARRAY_SIZE(sizes), 2);
-	check_int(min.start, ==, 1);
-	check_int(min.end, ==, 10);
-}
-
-static void t_suggest_compaction_segment_nothing(void)
-{
-	uint64_t sizes[] = { 64, 32, 16, 8, 4, 2 };
-	struct segment result =
-		suggest_compaction_segment(sizes, ARRAY_SIZE(sizes), 2);
-	check_int(result.start, ==, result.end);
-}
-
-static void t_reflog_expire(void)
-{
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	struct reftable_log_record logs[20] = { 0 };
-	size_t i, N = ARRAY_SIZE(logs) - 1;
-	int err;
-	struct reftable_log_expiry_config expiry = {
-		.time = 10,
-	};
-	struct reftable_log_record log = { 0 };
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	for (i = 1; i <= N; i++) {
-		char buf[256];
-		snprintf(buf, sizeof(buf), "branch%02"PRIuMAX, (uintmax_t)i);
-
-		logs[i].refname = xstrdup(buf);
-		logs[i].update_index = i;
-		logs[i].value_type = REFTABLE_LOG_UPDATE;
-		logs[i].value.update.time = i;
-		logs[i].value.update.email = xstrdup("identity@invalid");
-		t_reftable_set_hash(logs[i].value.update.new_hash, i,
-				    REFTABLE_HASH_SHA1);
-	}
-
-	for (i = 1; i <= N; i++) {
-		struct write_log_arg arg = {
-			.log = &logs[i],
-			.update_index = reftable_stack_next_update_index(st),
-		};
-		int err = reftable_stack_add(st, write_test_log, &arg);
-		check(!err);
-	}
-
-	err = reftable_stack_compact_all(st, NULL);
-	check(!err);
-
-	err = reftable_stack_compact_all(st, &expiry);
-	check(!err);
-
-	err = reftable_stack_read_log(st, logs[9].refname, &log);
-	check_int(err, ==, 1);
-
-	err = reftable_stack_read_log(st, logs[11].refname, &log);
-	check(!err);
-
-	expiry.min_update_index = 15;
-	err = reftable_stack_compact_all(st, &expiry);
-	check(!err);
-
-	err = reftable_stack_read_log(st, logs[14].refname, &log);
-	check_int(err, ==, 1);
-
-	err = reftable_stack_read_log(st, logs[16].refname, &log);
-	check(!err);
-
-	/* cleanup */
-	reftable_stack_destroy(st);
-	for (i = 0; i <= N; i++)
-		reftable_log_record_release(&logs[i]);
-	clear_dir(dir);
-	reftable_log_record_release(&log);
-}
-
-static int write_nothing(struct reftable_writer *wr, void *arg UNUSED)
-{
-	check(!reftable_writer_set_limits(wr, 1, 1));
-	return 0;
-}
-
-static void t_empty_add(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	int err;
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_stack *st2 = NULL;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_add(st, write_nothing, NULL);
-	check(!err);
-
-	err = reftable_new_stack(&st2, dir, &opts);
-	check(!err);
-	clear_dir(dir);
-	reftable_stack_destroy(st);
-	reftable_stack_destroy(st2);
-}
-
-static int fastlogN(uint64_t sz, uint64_t N)
-{
-	int l = 0;
-	if (sz == 0)
-		return 0;
-	for (; sz; sz /= N)
-		l++;
-	return l - 1;
-}
-
-static void t_reftable_stack_auto_compaction(void)
-{
-	struct reftable_write_options opts = {
-		.disable_auto_compact = 1,
-	};
-	struct reftable_stack *st = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-	size_t i, N = 100;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	for (i = 0; i < N; i++) {
-		char name[100];
-		struct reftable_ref_record ref = {
-			.refname = name,
-			.update_index = reftable_stack_next_update_index(st),
-			.value_type = REFTABLE_REF_SYMREF,
-			.value.symref = (char *) "master",
-		};
-		snprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i);
-
-		err = reftable_stack_add(st, write_test_ref, &ref);
-		check(!err);
-
-		err = reftable_stack_auto_compact(st);
-		check(!err);
-		check(i < 2 || st->merged->readers_len < 2 * fastlogN(i, 2));
-	}
-
-	check_int(reftable_stack_compaction_stats(st)->entries_written, <,
-	       (uint64_t)(N * fastlogN(N, 2)));
-
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_auto_compaction_factor(void)
-{
-	struct reftable_write_options opts = {
-		.auto_compaction_factor = 5,
-	};
-	struct reftable_stack *st = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-	size_t N = 100;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	for (size_t i = 0; i < N; i++) {
-		char name[20];
-		struct reftable_ref_record ref = {
-			.refname = name,
-			.update_index = reftable_stack_next_update_index(st),
-			.value_type = REFTABLE_REF_VAL1,
-		};
-		xsnprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i);
-
-		err = reftable_stack_add(st, &write_test_ref, &ref);
-		check(!err);
-
-		check(i < 5 || st->merged->readers_len < 5 * fastlogN(i, 5));
-	}
-
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_auto_compaction_with_locked_tables(void)
-{
-	struct reftable_write_options opts = {
-		.disable_auto_compact = 1,
-	};
-	struct reftable_stack *st = NULL;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	write_n_ref_tables(st, 5);
-	check_int(st->merged->readers_len, ==, 5);
-
-	/*
-	 * Given that all tables we have written should be roughly the same
-	 * size, we expect that auto-compaction will want to compact all of the
-	 * tables. Locking any of the tables will keep it from doing so.
-	 */
-	check(!reftable_buf_addstr(&buf, dir));
-	check(!reftable_buf_addstr(&buf, "/"));
-	check(!reftable_buf_addstr(&buf, st->readers[2]->name));
-	check(!reftable_buf_addstr(&buf, ".lock"));
-	write_file_buf(buf.buf, "", 0);
-
-	/*
-	 * When parts of the stack are locked, then auto-compaction does a best
-	 * effort compaction of those tables which aren't locked. So while this
-	 * would in theory compact all tables, due to the preexisting lock we
-	 * only compact the newest two tables.
-	 */
-	err = reftable_stack_auto_compact(st);
-	check(!err);
-	check_int(st->stats.failures, ==, 0);
-	check_int(st->merged->readers_len, ==, 4);
-
-	reftable_stack_destroy(st);
-	reftable_buf_release(&buf);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_add_performs_auto_compaction(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-	size_t i, n = 20;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	for (i = 0; i <= n; i++) {
-		struct reftable_ref_record ref = {
-			.update_index = reftable_stack_next_update_index(st),
-			.value_type = REFTABLE_REF_SYMREF,
-			.value.symref = (char *) "master",
-		};
-		char buf[128];
-
-		/*
-		 * Disable auto-compaction for all but the last runs. Like this
-		 * we can ensure that we indeed honor this setting and have
-		 * better control over when exactly auto compaction runs.
-		 */
-		st->opts.disable_auto_compact = i != n;
-
-		snprintf(buf, sizeof(buf), "branch-%04"PRIuMAX, (uintmax_t)i);
-		ref.refname = buf;
-
-		err = reftable_stack_add(st, write_test_ref, &ref);
-		check(!err);
-
-		/*
-		 * The stack length should grow continuously for all runs where
-		 * auto compaction is disabled. When enabled, we should merge
-		 * all tables in the stack.
-		 */
-		if (i != n)
-			check_int(st->merged->readers_len, ==, i + 1);
-		else
-			check_int(st->merged->readers_len, ==, 1);
-	}
-
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_compaction_with_locked_tables(void)
-{
-	struct reftable_write_options opts = {
-		.disable_auto_compact = 1,
-	};
-	struct reftable_stack *st = NULL;
-	struct reftable_buf buf = REFTABLE_BUF_INIT;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	write_n_ref_tables(st, 3);
-	check_int(st->merged->readers_len, ==, 3);
-
-	/* Lock one of the tables that we're about to compact. */
-	check(!reftable_buf_addstr(&buf, dir));
-	check(!reftable_buf_addstr(&buf, "/"));
-	check(!reftable_buf_addstr(&buf, st->readers[1]->name));
-	check(!reftable_buf_addstr(&buf, ".lock"));
-	write_file_buf(buf.buf, "", 0);
-
-	/*
-	 * Compaction is expected to fail given that we were not able to
-	 * compact all tables.
-	 */
-	err = reftable_stack_compact_all(st, NULL);
-	check_int(err, ==, REFTABLE_LOCK_ERROR);
-	check_int(st->stats.failures, ==, 1);
-	check_int(st->merged->readers_len, ==, 3);
-
-	reftable_stack_destroy(st);
-	reftable_buf_release(&buf);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_compaction_concurrent(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st1 = NULL, *st2 = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-
-	err = reftable_new_stack(&st1, dir, &opts);
-	check(!err);
-	write_n_ref_tables(st1, 3);
-
-	err = reftable_new_stack(&st2, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_compact_all(st1, NULL);
-	check(!err);
-
-	reftable_stack_destroy(st1);
-	reftable_stack_destroy(st2);
-
-	check_int(count_dir_entries(dir), ==, 2);
-	clear_dir(dir);
-}
-
-static void unclean_stack_close(struct reftable_stack *st)
-{
-	/* break abstraction boundary to simulate unclean shutdown. */
-	for (size_t i = 0; i < st->readers_len; i++)
-		reftable_reader_decref(st->readers[i]);
-	st->readers_len = 0;
-	REFTABLE_FREE_AND_NULL(st->readers);
-}
-
-static void t_reftable_stack_compaction_concurrent_clean(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st1 = NULL, *st2 = NULL, *st3 = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-
-	err = reftable_new_stack(&st1, dir, &opts);
-	check(!err);
-	write_n_ref_tables(st1, 3);
-
-	err = reftable_new_stack(&st2, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_compact_all(st1, NULL);
-	check(!err);
-
-	unclean_stack_close(st1);
-	unclean_stack_close(st2);
-
-	err = reftable_new_stack(&st3, dir, &opts);
-	check(!err);
-
-	err = reftable_stack_clean(st3);
-	check(!err);
-	check_int(count_dir_entries(dir), ==, 2);
-
-	reftable_stack_destroy(st1);
-	reftable_stack_destroy(st2);
-	reftable_stack_destroy(st3);
-
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_read_across_reload(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st1 = NULL, *st2 = NULL;
-	struct reftable_ref_record rec = { 0 };
-	struct reftable_iterator it = { 0 };
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-
-	/* Create a first stack and set up an iterator for it. */
-	err = reftable_new_stack(&st1, dir, &opts);
-	check(!err);
-	write_n_ref_tables(st1, 2);
-	check_int(st1->merged->readers_len, ==, 2);
-	reftable_stack_init_ref_iterator(st1, &it);
-	err = reftable_iterator_seek_ref(&it, "");
-	check(!err);
-
-	/* Set up a second stack for the same directory and compact it. */
-	err = reftable_new_stack(&st2, dir, &opts);
-	check(!err);
-	check_int(st2->merged->readers_len, ==, 2);
-	err = reftable_stack_compact_all(st2, NULL);
-	check(!err);
-	check_int(st2->merged->readers_len, ==, 1);
-
-	/*
-	 * Verify that we can continue to use the old iterator even after we
-	 * have reloaded its stack.
-	 */
-	err = reftable_stack_reload(st1);
-	check(!err);
-	check_int(st1->merged->readers_len, ==, 1);
-	err = reftable_iterator_next_ref(&it, &rec);
-	check(!err);
-	check_str(rec.refname, "refs/heads/branch-0000");
-	err = reftable_iterator_next_ref(&it, &rec);
-	check(!err);
-	check_str(rec.refname, "refs/heads/branch-0001");
-	err = reftable_iterator_next_ref(&it, &rec);
-	check_int(err, >, 0);
-
-	reftable_ref_record_release(&rec);
-	reftable_iterator_destroy(&it);
-	reftable_stack_destroy(st1);
-	reftable_stack_destroy(st2);
-	clear_dir(dir);
-}
-
-static void t_reftable_stack_reload_with_missing_table(void)
-{
-	struct reftable_write_options opts = { 0 };
-	struct reftable_stack *st = NULL;
-	struct reftable_ref_record rec = { 0 };
-	struct reftable_iterator it = { 0 };
-	struct reftable_buf table_path = REFTABLE_BUF_INIT, content = REFTABLE_BUF_INIT;
-	char *dir = get_tmp_dir(__LINE__);
-	int err;
-
-	/* Create a first stack and set up an iterator for it. */
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-	write_n_ref_tables(st, 2);
-	check_int(st->merged->readers_len, ==, 2);
-	reftable_stack_init_ref_iterator(st, &it);
-	err = reftable_iterator_seek_ref(&it, "");
-	check(!err);
-
-	/*
-	 * Update the tables.list file with some garbage data, while reusing
-	 * our old readers. This should trigger a partial reload of the stack,
-	 * where we try to reuse our old readers.
-	*/
-	check(!reftable_buf_addstr(&content, st->readers[0]->name));
-	check(!reftable_buf_addstr(&content, "\n"));
-	check(!reftable_buf_addstr(&content, st->readers[1]->name));
-	check(!reftable_buf_addstr(&content, "\n"));
-	check(!reftable_buf_addstr(&content, "garbage\n"));
-	check(!reftable_buf_addstr(&table_path, st->list_file));
-	check(!reftable_buf_addstr(&table_path, ".lock"));
-	write_file_buf(table_path.buf, content.buf, content.len);
-	err = rename(table_path.buf, st->list_file);
-	check(!err);
-
-	err = reftable_stack_reload(st);
-	check_int(err, ==, -4);
-	check_int(st->merged->readers_len, ==, 2);
-
-	/*
-	 * Even though the reload has failed, we should be able to continue
-	 * using the iterator.
-	*/
-	err = reftable_iterator_next_ref(&it, &rec);
-	check(!err);
-	check_str(rec.refname, "refs/heads/branch-0000");
-	err = reftable_iterator_next_ref(&it, &rec);
-	check(!err);
-	check_str(rec.refname, "refs/heads/branch-0001");
-	err = reftable_iterator_next_ref(&it, &rec);
-	check_int(err, >, 0);
-
-	reftable_ref_record_release(&rec);
-	reftable_iterator_destroy(&it);
-	reftable_stack_destroy(st);
-	reftable_buf_release(&table_path);
-	reftable_buf_release(&content);
-	clear_dir(dir);
-}
-
-static int write_limits_after_ref(struct reftable_writer *wr, void *arg)
-{
-	struct reftable_ref_record *ref = arg;
-	check(!reftable_writer_set_limits(wr, ref->update_index, ref->update_index));
-	check(!reftable_writer_add_ref(wr, ref));
-	return reftable_writer_set_limits(wr, ref->update_index, ref->update_index);
-}
-
-static void t_reftable_invalid_limit_updates(void)
-{
-	struct reftable_ref_record ref = {
-		.refname = (char *) "HEAD",
-		.update_index = 1,
-		.value_type = REFTABLE_REF_SYMREF,
-		.value.symref = (char *) "master",
-	};
-	struct reftable_write_options opts = {
-		.default_permissions = 0660,
-	};
-	struct reftable_addition *add = NULL;
-	char *dir = get_tmp_dir(__LINE__);
-	struct reftable_stack *st = NULL;
-	int err;
-
-	err = reftable_new_stack(&st, dir, &opts);
-	check(!err);
-
-	reftable_addition_destroy(add);
-
-	err = reftable_stack_new_addition(&add, st, 0);
-	check(!err);
-
-	/*
-	 * write_limits_after_ref also updates the update indexes after adding
-	 * the record. This should cause an err to be returned, since the limits
-	 * must be set at the start.
-	 */
-	err = reftable_addition_add(add, write_limits_after_ref, &ref);
-	check_int(err, ==, REFTABLE_API_ERROR);
-
-	reftable_addition_destroy(add);
-	reftable_stack_destroy(st);
-	clear_dir(dir);
-}
-
-int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
-{
-	TEST(t_empty_add(), "empty addition to stack");
-	TEST(t_read_file(), "read_lines works");
-	TEST(t_reflog_expire(), "expire reflog entries");
-	TEST(t_reftable_invalid_limit_updates(), "prevent limit updates after adding records");
-	TEST(t_reftable_stack_add(), "add multiple refs and logs to stack");
-	TEST(t_reftable_stack_add_one(), "add a single ref record to stack");
-	TEST(t_reftable_stack_add_performs_auto_compaction(), "addition to stack triggers auto-compaction");
-	TEST(t_reftable_stack_auto_compaction(), "stack must form geometric sequence after compaction");
-	TEST(t_reftable_stack_auto_compaction_factor(), "auto-compaction with non-default geometric factor");
-	TEST(t_reftable_stack_auto_compaction_fails_gracefully(), "failure on auto-compaction");
-	TEST(t_reftable_stack_auto_compaction_with_locked_tables(), "auto compaction with locked tables");
-	TEST(t_reftable_stack_compaction_concurrent(), "compaction with concurrent stack");
-	TEST(t_reftable_stack_compaction_concurrent_clean(), "compaction with unclean stack shutdown");
-	TEST(t_reftable_stack_compaction_with_locked_tables(), "compaction with locked tables");
-	TEST(t_reftable_stack_hash_id(), "read stack with wrong hash ID");
-	TEST(t_reftable_stack_iterator(), "log and ref iterator for reftable stack");
-	TEST(t_reftable_stack_lock_failure(), "stack addition with lockfile failure");
-	TEST(t_reftable_stack_log_normalize(), "log messages should be normalized");
-	TEST(t_reftable_stack_read_across_reload(), "stack iterators work across reloads");
-	TEST(t_reftable_stack_reload_with_missing_table(), "stack iteration with garbage tables");
-	TEST(t_reftable_stack_tombstone(), "'tombstone' refs in stack");
-	TEST(t_reftable_stack_transaction_api(), "update transaction to stack");
-	TEST(t_reftable_stack_transaction_with_reload(), "transaction with reload");
-	TEST(t_reftable_stack_transaction_api_performs_auto_compaction(), "update transaction triggers auto-compaction");
-	TEST(t_reftable_stack_update_index_check(), "update transactions with equal update indices");
-	TEST(t_reftable_stack_uptodate(), "stack must be reloaded before ref update");
-	TEST(t_suggest_compaction_segment(), "suggest_compaction_segment with basic input");
-	TEST(t_suggest_compaction_segment_nothing(), "suggest_compaction_segment with pre-compacted input");
-
-	return test_done();
-}
diff --git a/t/unit-tests/u-reftable-stack.c b/t/unit-tests/u-reftable-stack.c
new file mode 100644
index 0000000000..1f41d8c4ca
--- /dev/null
+++ b/t/unit-tests/u-reftable-stack.c
@@ -0,0 +1,1247 @@
+/*
+Copyright 2020 Google LLC
+
+Use of this source code is governed by a BSD-style
+license that can be found in the LICENSE file or at
+https://developers.google.com/open-source/licenses/bsd
+*/
+
+#define DISABLE_SIGN_COMPARE_WARNINGS
+
+#include "unit-test.h"
+#include "lib-reftable.h"
+#include "dir.h"
+#include "reftable/merged.h"
+#include "reftable/reader.h"
+#include "reftable/reftable-error.h"
+#include "reftable/stack.h"
+#include "strbuf.h"
+#include "tempfile.h"
+#include <dirent.h>
+
+static void clear_dir(const char *dirname)
+{
+	struct strbuf path = REFTABLE_BUF_INIT;
+	strbuf_addstr(&path, dirname);
+	remove_dir_recursively(&path, 0);
+	strbuf_release(&path);
+}
+
+static int count_dir_entries(const char *dirname)
+{
+	DIR *dir = opendir(dirname);
+	int len = 0;
+	struct dirent *d;
+	if (!dir)
+		return 0;
+
+	while ((d = readdir(dir))) {
+		/*
+		 * Besides skipping over "." and "..", we also need to
+		 * skip over other files that have a leading ".". This
+		 * is due to behaviour of NFS, which will rename files
+		 * to ".nfs*" to emulate delete-on-last-close.
+		 *
+		 * In any case this should be fine as the reftable
+		 * library will never write files with leading dots
+		 * anyway.
+		 */
+		if (starts_with(d->d_name, "."))
+			continue;
+		len++;
+	}
+	closedir(dir);
+	return len;
+}
+
+/*
+ * Work linenumber into the tempdir, so we can see which tests forget to
+ * cleanup.
+ */
+static char *get_tmp_template(int linenumber)
+{
+	const char *tmp = getenv("TMPDIR");
+	static char template[1024];
+	snprintf(template, sizeof(template) - 1, "%s/stack_test-%d.XXXXXX",
+		 tmp ? tmp : "/tmp", linenumber);
+	return template;
+}
+
+static char *get_tmp_dir(int linenumber)
+{
+	char *dir = get_tmp_template(linenumber);
+	cl_assert(mkdtemp(dir) != NULL);
+	return dir;
+}
+
+void test_reftable_stack__read_file(void)
+{
+	char *fn = get_tmp_template(__LINE__);
+	struct tempfile *tmp = mks_tempfile(fn);
+	int fd = get_tempfile_fd(tmp);
+	char out[1024] = "line1\n\nline2\nline3";
+	int n, err;
+	char **names = NULL;
+	const char *want[] = { "line1", "line2", "line3" };
+
+	cl_assert(fd > 0);
+	n = write_in_full(fd, out, strlen(out));
+	cl_assert_equal_i(n, strlen(out));
+	err = close(fd);
+	cl_assert(err >= 0);
+
+	err = read_lines(fn, &names);
+	cl_assert(err == 0);
+
+	for (size_t i = 0; names[i]; i++)
+		cl_assert_equal_s(want[i], names[i]);
+	free_names(names);
+	(void) remove(fn);
+	delete_tempfile(&tmp);
+}
+
+static int write_test_ref(struct reftable_writer *wr, void *arg)
+{
+	struct reftable_ref_record *ref = arg;
+	cl_assert(reftable_writer_set_limits(wr, ref->update_index,
+										 ref->update_index) == 0);
+	return reftable_writer_add_ref(wr, ref);
+}
+
+static void write_n_ref_tables(struct reftable_stack *st,
+			       size_t n)
+{
+	int disable_auto_compact;
+
+	disable_auto_compact = st->opts.disable_auto_compact;
+	st->opts.disable_auto_compact = 1;
+
+	for (size_t i = 0; i < n; i++) {
+		struct reftable_ref_record ref = {
+			.update_index = reftable_stack_next_update_index(st),
+			.value_type = REFTABLE_REF_VAL1,
+		};
+		char buf[128];
+
+		snprintf(buf, sizeof(buf), "refs/heads/branch-%04"PRIuMAX, (uintmax_t)i);
+		ref.refname = buf;
+		cl_reftable_set_hash(ref.value.val1, i, REFTABLE_HASH_SHA1);
+
+		cl_assert(reftable_stack_add(st, &write_test_ref, &ref) == 0);
+	}
+
+	st->opts.disable_auto_compact = disable_auto_compact;
+}
+
+struct write_log_arg {
+	struct reftable_log_record *log;
+	uint64_t update_index;
+};
+
+static int write_test_log(struct reftable_writer *wr, void *arg)
+{
+	struct write_log_arg *wla = arg;
+
+	cl_assert(reftable_writer_set_limits(wr, wla->update_index,
+										 wla->update_index) == 0);
+	return reftable_writer_add_log(wr, wla->log);
+}
+
+void test_reftable_stack__add_one(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_buf scratch = REFTABLE_BUF_INIT;
+	int mask = umask(002);
+	struct reftable_write_options opts = {
+		.default_permissions = 0660,
+	};
+	struct reftable_stack *st = NULL;
+	struct reftable_ref_record ref = {
+		.refname = (char *) "HEAD",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "master",
+	};
+	struct reftable_ref_record dest = { 0 };
+	struct stat stat_result = { 0 };
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+	cl_assert(reftable_stack_add(st, write_test_ref, &ref) == 0);
+	cl_assert(reftable_stack_read_ref(st, ref.refname, &dest) == 0);
+	cl_assert(reftable_ref_record_equal(&ref, &dest, REFTABLE_HASH_SIZE_SHA1) != 0);
+	cl_assert(st->readers_len > 0);
+
+#ifndef GIT_WINDOWS_NATIVE
+	cl_assert(reftable_buf_addstr(&scratch, dir) == 0);
+	cl_assert(reftable_buf_addstr(&scratch, "/tables.list") == 0);
+	cl_assert(stat(scratch.buf, &stat_result) == 0);
+	cl_assert_equal_i((stat_result.st_mode & 0777), opts.default_permissions);
+
+	reftable_buf_reset(&scratch);
+	cl_assert(reftable_buf_addstr(&scratch, dir) == 0);
+	cl_assert(reftable_buf_addstr(&scratch, "/") == 0);
+	/* do not try at home; not an external API for reftable. */
+	cl_assert(reftable_buf_addstr(&scratch, st->readers[0]->name) == 0);
+	cl_assert(stat(scratch.buf, &stat_result) == 0);
+	cl_assert_equal_i((stat_result.st_mode & 0777), opts.default_permissions);
+#else
+	(void) stat_result;
+#endif
+
+	reftable_ref_record_release(&dest);
+	reftable_stack_destroy(st);
+	reftable_buf_release(&scratch);
+	clear_dir(dir);
+	umask(mask);
+}
+
+void test_reftable_stack__uptodate(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st1 = NULL;
+	struct reftable_stack *st2 = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+
+	struct reftable_ref_record ref1 = {
+		.refname = (char *) "HEAD",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "master",
+	};
+	struct reftable_ref_record ref2 = {
+		.refname = (char *) "branch2",
+		.update_index = 2,
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "master",
+	};
+
+
+	/* simulate multi-process access to the same stack
+	   by creating two stacks for the same directory.
+	 */
+	cl_assert(reftable_new_stack(&st1, dir, &opts) == 0);
+	cl_assert(reftable_new_stack(&st2, dir, &opts) == 0);
+	cl_assert(reftable_stack_add(st1, write_test_ref, &ref1) == 0);
+	cl_assert_equal_i(reftable_stack_add(st2, write_test_ref, &ref2),
+					  REFTABLE_OUTDATED_ERROR);
+	cl_assert(reftable_stack_reload(st2) == 0);
+	cl_assert(reftable_stack_add(st2, write_test_ref, &ref2) == 0);
+	reftable_stack_destroy(st1);
+	reftable_stack_destroy(st2);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__transaction_api(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	struct reftable_addition *add = NULL;
+
+	struct reftable_ref_record ref = {
+		.refname = (char *) "HEAD",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "master",
+	};
+	struct reftable_ref_record dest = { 0 };
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	reftable_addition_destroy(add);
+
+	cl_assert(reftable_stack_new_addition(&add, st, 0) == 0);
+	cl_assert(reftable_addition_add(add, write_test_ref, &ref) == 0);
+	cl_assert(reftable_addition_commit(add) == 0);
+
+	reftable_addition_destroy(add);
+
+	cl_assert(reftable_stack_read_ref(st, ref.refname, &dest) == 0);
+	cl_assert_equal_i(REFTABLE_REF_SYMREF, dest.value_type);
+	cl_assert(reftable_ref_record_equal(&ref, &dest, REFTABLE_HASH_SIZE_SHA1) != 0);
+
+	reftable_ref_record_release(&dest);
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__transaction_with_reload(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_stack *st1 = NULL, *st2 = NULL;
+	struct reftable_addition *add = NULL;
+	struct reftable_ref_record refs[2] = {
+		{
+			.refname = (char *) "refs/heads/a",
+			.update_index = 1,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { '1' },
+		},
+		{
+			.refname = (char *) "refs/heads/b",
+			.update_index = 2,
+			.value_type = REFTABLE_REF_VAL1,
+			.value.val1 = { '1' },
+		},
+	};
+	struct reftable_ref_record ref = { 0 };
+
+	cl_assert(reftable_new_stack(&st1, dir, NULL) == 0);
+	cl_assert(reftable_new_stack(&st2, dir, NULL) == 0);
+	cl_assert(reftable_stack_new_addition(&add, st1, 0) == 0);
+	cl_assert(reftable_addition_add(add, write_test_ref, &refs[0]) == 0);
+	cl_assert(reftable_addition_commit(add) == 0);
+	reftable_addition_destroy(add);
+
+	/*
+	 * The second stack is now outdated, which we should notice. We do not
+	 * create the addition and lock the stack by default, but allow the
+	 * reload to happen when REFTABLE_STACK_NEW_ADDITION_RELOAD is set.
+	 */
+	cl_assert_equal_i(reftable_stack_new_addition(&add, st2, 0),
+					  REFTABLE_OUTDATED_ERROR);
+	cl_assert(reftable_stack_new_addition(&add, st2,
+										  REFTABLE_STACK_NEW_ADDITION_RELOAD) == 0);
+	cl_assert(reftable_addition_add(add, write_test_ref, &refs[1]) == 0);
+	cl_assert(reftable_addition_commit(add) == 0);
+	reftable_addition_destroy(add);
+
+	for (size_t i = 0; i < ARRAY_SIZE(refs); i++) {
+		cl_assert(reftable_stack_read_ref(st2, refs[i].refname, &ref) == 0);
+		cl_assert(reftable_ref_record_equal(&refs[i], &ref,
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+	}
+
+	reftable_ref_record_release(&ref);
+	reftable_stack_destroy(st1);
+	reftable_stack_destroy(st2);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__transaction_api_performs_auto_compaction(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_write_options opts = {0};
+	struct reftable_addition *add = NULL;
+	struct reftable_stack *st = NULL;
+	size_t n = 20;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	for (size_t i = 0; i <= n; i++) {
+		struct reftable_ref_record ref = {
+			.update_index = reftable_stack_next_update_index(st),
+			.value_type = REFTABLE_REF_SYMREF,
+			.value.symref = (char *) "master",
+		};
+		char name[100];
+
+		snprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i);
+		ref.refname = name;
+
+		/*
+		 * Disable auto-compaction for all but the last runs. Like this
+		 * we can ensure that we indeed honor this setting and have
+		 * better control over when exactly auto compaction runs.
+		 */
+		st->opts.disable_auto_compact = i != n;
+
+		cl_assert(reftable_stack_new_addition(&add, st, 0) == 0);
+		cl_assert(reftable_addition_add(add, write_test_ref, &ref) == 0);
+		cl_assert(reftable_addition_commit(add) == 0);
+
+		reftable_addition_destroy(add);
+
+		/*
+		 * The stack length should grow continuously for all runs where
+		 * auto compaction is disabled. When enabled, we should merge
+		 * all tables in the stack.
+		 */
+		if (i != n)
+			cl_assert_equal_i(st->merged->readers_len, i + 1);
+		else
+			cl_assert_equal_i(st->merged->readers_len, 1);
+	}
+
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__auto_compaction_fails_gracefully(void)
+{
+	struct reftable_ref_record ref = {
+		.refname = (char *) "refs/heads/master",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_VAL1,
+		.value.val1 = {0x01},
+	};
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st;
+	struct reftable_buf table_path = REFTABLE_BUF_INIT;
+	char *dir = get_tmp_dir(__LINE__);
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+	cl_assert(reftable_stack_add(st, write_test_ref, &ref) == 0);
+	cl_assert_equal_i(st->merged->readers_len, 1);
+	cl_assert_equal_i(st->stats.attempts, 0);
+	cl_assert_equal_i(st->stats.failures, 0);
+
+	/*
+	 * Lock the newly written table such that it cannot be compacted.
+	 * Adding a new table to the stack should not be impacted by this, even
+	 * though auto-compaction will now fail.
+	 */
+	cl_assert(reftable_buf_addstr(&table_path, dir) == 0);
+	cl_assert(reftable_buf_addstr(&table_path, "/") == 0);
+	cl_assert(reftable_buf_addstr(&table_path, st->readers[0]->name) == 0);
+	cl_assert(reftable_buf_addstr(&table_path, ".lock") == 0);
+	write_file_buf(table_path.buf, "", 0);
+
+	ref.update_index = 2;
+	cl_assert(reftable_stack_add(st, write_test_ref, &ref) == 0);
+	cl_assert_equal_i(st->merged->readers_len, 2);
+	cl_assert_equal_i(st->stats.attempts, 1);
+	cl_assert_equal_i(st->stats.failures, 1);
+
+	reftable_stack_destroy(st);
+	reftable_buf_release(&table_path);
+	clear_dir(dir);
+}
+
+static int write_error(struct reftable_writer *wr UNUSED, void *arg)
+{
+	return *((int *)arg);
+}
+
+void test_reftable_stack__update_index_check(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	struct reftable_ref_record ref1 = {
+		.refname = (char *) "name1",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "master",
+	};
+	struct reftable_ref_record ref2 = {
+		.refname = (char *) "name2",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "master",
+	};
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+	cl_assert(reftable_stack_add(st, write_test_ref, &ref1) == 0);
+	cl_assert_equal_i(reftable_stack_add(st, write_test_ref,
+										 &ref2), REFTABLE_API_ERROR);
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__lock_failure(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	int i;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+	for (i = -1; i != REFTABLE_EMPTY_TABLE_ERROR; i--)
+		cl_assert_equal_i(reftable_stack_add(st, write_error, &i), i);
+
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__add(void)
+{
+	struct reftable_write_options opts = {
+		.exact_log_message = 1,
+		.default_permissions = 0660,
+		.disable_auto_compact = 1,
+	};
+	struct reftable_stack *st = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_ref_record refs[2] = { 0 };
+	struct reftable_log_record logs[2] = { 0 };
+	struct reftable_buf path = REFTABLE_BUF_INIT;
+	struct stat stat_result;
+	size_t i, N = ARRAY_SIZE(refs);
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	for (i = 0; i < N; i++) {
+		char buf[256];
+		snprintf(buf, sizeof(buf), "branch%02"PRIuMAX, (uintmax_t)i);
+		refs[i].refname = xstrdup(buf);
+		refs[i].update_index = i + 1;
+		refs[i].value_type = REFTABLE_REF_VAL1;
+		cl_reftable_set_hash(refs[i].value.val1, i, REFTABLE_HASH_SHA1);
+
+		logs[i].refname = xstrdup(buf);
+		logs[i].update_index = N + i + 1;
+		logs[i].value_type = REFTABLE_LOG_UPDATE;
+		logs[i].value.update.email = xstrdup("identity@invalid");
+		cl_reftable_set_hash(logs[i].value.update.new_hash, i, REFTABLE_HASH_SHA1);
+	}
+
+	for (i = 0; i < N; i++)
+		cl_assert(reftable_stack_add(st, write_test_ref, &refs[i]) == 0);
+
+	for (i = 0; i < N; i++) {
+		struct write_log_arg arg = {
+			.log = &logs[i],
+			.update_index = reftable_stack_next_update_index(st),
+		};
+		cl_assert(reftable_stack_add(st, write_test_log, &arg) == 0);
+	}
+
+	cl_assert(reftable_stack_compact_all(st, NULL) == 0);
+
+	for (i = 0; i < N; i++) {
+		struct reftable_ref_record dest = { 0 };
+
+		cl_assert(reftable_stack_read_ref(st, refs[i].refname, &dest) == 0);
+		cl_assert(reftable_ref_record_equal(&dest, refs + i,
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+		reftable_ref_record_release(&dest);
+	}
+
+	for (i = 0; i < N; i++) {
+		struct reftable_log_record dest = { 0 };
+		cl_assert(reftable_stack_read_log(st, refs[i].refname, &dest) == 0);
+		cl_assert(reftable_log_record_equal(&dest, logs + i,
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+		reftable_log_record_release(&dest);
+	}
+
+#ifndef GIT_WINDOWS_NATIVE
+	cl_assert(reftable_buf_addstr(&path, dir) == 0);
+	cl_assert(reftable_buf_addstr(&path, "/tables.list") == 0);
+	cl_assert(stat(path.buf, &stat_result) == 0);
+	cl_assert_equal_i((stat_result.st_mode & 0777), opts.default_permissions);
+
+	reftable_buf_reset(&path);
+	cl_assert(reftable_buf_addstr(&path, dir) == 0);
+	cl_assert(reftable_buf_addstr(&path, "/") == 0);
+	/* do not try at home; not an external API for reftable. */
+	cl_assert(reftable_buf_addstr(&path, st->readers[0]->name) == 0);
+	cl_assert(stat(path.buf, &stat_result) == 0);
+	cl_assert_equal_i((stat_result.st_mode & 0777), opts.default_permissions);
+#else
+	(void) stat_result;
+#endif
+
+	/* cleanup */
+	reftable_stack_destroy(st);
+	for (i = 0; i < N; i++) {
+		reftable_ref_record_release(&refs[i]);
+		reftable_log_record_release(&logs[i]);
+	}
+	reftable_buf_release(&path);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__iterator(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_ref_record refs[10] = { 0 };
+	struct reftable_log_record logs[10] = { 0 };
+	struct reftable_iterator it = { 0 };
+	size_t N = ARRAY_SIZE(refs), i;
+	int err;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	for (i = 0; i < N; i++) {
+		refs[i].refname = xstrfmt("branch%02"PRIuMAX, (uintmax_t)i);
+		refs[i].update_index = i + 1;
+		refs[i].value_type = REFTABLE_REF_VAL1;
+		cl_reftable_set_hash(refs[i].value.val1, i, REFTABLE_HASH_SHA1);
+
+		logs[i].refname = xstrfmt("branch%02"PRIuMAX, (uintmax_t)i);
+		logs[i].update_index = i + 1;
+		logs[i].value_type = REFTABLE_LOG_UPDATE;
+		logs[i].value.update.email = xstrdup("johndoe@invalid");
+		logs[i].value.update.message = xstrdup("commit\n");
+		cl_reftable_set_hash(logs[i].value.update.new_hash, i, REFTABLE_HASH_SHA1);
+	}
+
+	for (i = 0; i < N; i++)
+		cl_assert(reftable_stack_add(st, write_test_ref, &refs[i]) == 0);
+
+	for (i = 0; i < N; i++) {
+		struct write_log_arg arg = {
+			.log = &logs[i],
+			.update_index = reftable_stack_next_update_index(st),
+		};
+
+		cl_assert(reftable_stack_add(st, write_test_log, &arg) == 0);
+	}
+
+	reftable_stack_init_ref_iterator(st, &it);
+	reftable_iterator_seek_ref(&it, refs[0].refname);
+	for (i = 0; ; i++) {
+		struct reftable_ref_record ref = { 0 };
+
+		err = reftable_iterator_next_ref(&it, &ref);
+		if (err > 0)
+			break;
+		cl_assert(err == 0);
+		cl_assert(reftable_ref_record_equal(&ref, &refs[i],
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+		reftable_ref_record_release(&ref);
+	}
+	cl_assert_equal_i(i, N);
+
+	reftable_iterator_destroy(&it);
+
+	cl_assert(reftable_stack_init_log_iterator(st, &it) == 0);
+
+	reftable_iterator_seek_log(&it, logs[0].refname);
+	for (i = 0; ; i++) {
+		struct reftable_log_record log = { 0 };
+
+		err = reftable_iterator_next_log(&it, &log);
+		if (err > 0)
+			break;
+		cl_assert(err == 0);
+		cl_assert(reftable_log_record_equal(&log, &logs[i],
+											REFTABLE_HASH_SIZE_SHA1) != 0);
+		reftable_log_record_release(&log);
+	}
+	cl_assert_equal_i(i, N);
+
+	reftable_stack_destroy(st);
+	reftable_iterator_destroy(&it);
+	for (i = 0; i < N; i++) {
+		reftable_ref_record_release(&refs[i]);
+		reftable_log_record_release(&logs[i]);
+	}
+	clear_dir(dir);
+}
+
+void test_reftable_stack__log_normalize(void)
+{
+	struct reftable_write_options opts = {
+		0,
+	};
+	struct reftable_stack *st = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_log_record input = {
+		.refname = (char *) "branch",
+		.update_index = 1,
+		.value_type = REFTABLE_LOG_UPDATE,
+		.value = {
+			.update = {
+				.new_hash = { 1 },
+				.old_hash = { 2 },
+			},
+		},
+	};
+	struct reftable_log_record dest = {
+		.update_index = 0,
+	};
+	struct write_log_arg arg = {
+		.log = &input,
+		.update_index = 1,
+	};
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	input.value.update.message = (char *) "one\ntwo";
+	cl_assert_equal_i(reftable_stack_add(st, write_test_log,
+										 &arg), REFTABLE_API_ERROR);
+
+	input.value.update.message = (char *) "one";
+	cl_assert(reftable_stack_add(st, write_test_log, &arg) == 0);
+	cl_assert(reftable_stack_read_log(st, input.refname, &dest) == 0);
+	cl_assert_equal_s(dest.value.update.message, "one\n");
+
+	input.value.update.message = (char *) "two\n";
+	arg.update_index = 2;
+	cl_assert(reftable_stack_add(st, write_test_log, &arg) == 0);
+	cl_assert(reftable_stack_read_log(st, input.refname, &dest) == 0);
+	cl_assert_equal_s(dest.value.update.message, "two\n");
+
+	/* cleanup */
+	reftable_stack_destroy(st);
+	reftable_log_record_release(&dest);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__tombstone(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	struct reftable_ref_record refs[2] = { 0 };
+	struct reftable_log_record logs[2] = { 0 };
+	size_t i, N = ARRAY_SIZE(refs);
+	struct reftable_ref_record dest = { 0 };
+	struct reftable_log_record log_dest = { 0 };
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	/* even entries add the refs, odd entries delete them. */
+	for (i = 0; i < N; i++) {
+		const char *buf = "branch";
+		refs[i].refname = xstrdup(buf);
+		refs[i].update_index = i + 1;
+		if (i % 2 == 0) {
+			refs[i].value_type = REFTABLE_REF_VAL1;
+			cl_reftable_set_hash(refs[i].value.val1, i,
+					    REFTABLE_HASH_SHA1);
+		}
+
+		logs[i].refname = xstrdup(buf);
+		/*
+		 * update_index is part of the key so should be constant.
+		 * The value itself should be less than the writer's upper
+		 * limit.
+		 */
+		logs[i].update_index = 1;
+		if (i % 2 == 0) {
+			logs[i].value_type = REFTABLE_LOG_UPDATE;
+			cl_reftable_set_hash(logs[i].value.update.new_hash, i,
+					    REFTABLE_HASH_SHA1);
+			logs[i].value.update.email =
+				xstrdup("identity@invalid");
+		}
+	}
+	for (i = 0; i < N; i++)
+		cl_assert(reftable_stack_add(st, write_test_ref, &refs[i]) == 0);
+
+	for (i = 0; i < N; i++) {
+		struct write_log_arg arg = {
+			.log = &logs[i],
+			.update_index = reftable_stack_next_update_index(st),
+		};
+		cl_assert(reftable_stack_add(st, write_test_log, &arg) == 0);
+	}
+
+	cl_assert_equal_i(reftable_stack_read_ref(st, "branch", &dest), 1);
+	reftable_ref_record_release(&dest);
+
+	cl_assert_equal_i(reftable_stack_read_log(st, "branch", &log_dest), 1);
+	reftable_log_record_release(&log_dest);
+
+	cl_assert(reftable_stack_compact_all(st, NULL) == 0);
+	cl_assert_equal_i(reftable_stack_read_ref(st, "branch", &dest), 1);
+	cl_assert_equal_i(reftable_stack_read_log(st, "branch", &log_dest), 1);
+	reftable_ref_record_release(&dest);
+	reftable_log_record_release(&log_dest);
+
+	/* cleanup */
+	reftable_stack_destroy(st);
+	for (i = 0; i < N; i++) {
+		reftable_ref_record_release(&refs[i]);
+		reftable_log_record_release(&logs[i]);
+	}
+	clear_dir(dir);
+}
+
+void test_reftable_stack__hash_id(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+
+	struct reftable_ref_record ref = {
+		.refname = (char *) "master",
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "target",
+		.update_index = 1,
+	};
+	struct reftable_write_options opts32 = { .hash_id = REFTABLE_HASH_SHA256 };
+	struct reftable_stack *st32 = NULL;
+	struct reftable_write_options opts_default = { 0 };
+	struct reftable_stack *st_default = NULL;
+	struct reftable_ref_record dest = { 0 };
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+	cl_assert(reftable_stack_add(st, write_test_ref, &ref) == 0);
+
+	/* can't read it with the wrong hash ID. */
+	cl_assert_equal_i(reftable_new_stack(&st32, dir, &opts32), REFTABLE_FORMAT_ERROR);
+
+	/* check that we can read it back with default opts too. */
+	cl_assert(reftable_new_stack(&st_default, dir, &opts_default) == 0);
+	cl_assert(reftable_stack_read_ref(st_default, "master", &dest) == 0);
+	cl_assert(reftable_ref_record_equal(&ref, &dest, REFTABLE_HASH_SIZE_SHA1) != 0);
+	reftable_ref_record_release(&dest);
+	reftable_stack_destroy(st);
+	reftable_stack_destroy(st_default);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__suggest_compaction_segment(void)
+{
+	uint64_t sizes[] = { 512, 64, 17, 16, 9, 9, 9, 16, 2, 16 };
+	struct segment min =
+		suggest_compaction_segment(sizes, ARRAY_SIZE(sizes), 2);
+	cl_assert_equal_i(min.start, 1);
+	cl_assert_equal_i(min.end, 10);
+}
+
+void test_reftable_stack__suggest_compaction_segment_nothing(void)
+{
+	uint64_t sizes[] = { 64, 32, 16, 8, 4, 2 };
+	struct segment result =
+		suggest_compaction_segment(sizes, ARRAY_SIZE(sizes), 2);
+	cl_assert_equal_i(result.start, result.end);
+}
+
+void test_reftable_stack__reflog_expire(void)
+{
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	struct reftable_log_record logs[20] = { 0 };
+	size_t i, N = ARRAY_SIZE(logs) - 1;
+	struct reftable_log_expiry_config expiry = {
+		.time = 10,
+	};
+	struct reftable_log_record log = { 0 };
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	for (i = 1; i <= N; i++) {
+		char buf[256];
+		snprintf(buf, sizeof(buf), "branch%02"PRIuMAX, (uintmax_t)i);
+
+		logs[i].refname = xstrdup(buf);
+		logs[i].update_index = i;
+		logs[i].value_type = REFTABLE_LOG_UPDATE;
+		logs[i].value.update.time = i;
+		logs[i].value.update.email = xstrdup("identity@invalid");
+		cl_reftable_set_hash(logs[i].value.update.new_hash, i,
+				    REFTABLE_HASH_SHA1);
+	}
+
+	for (i = 1; i <= N; i++) {
+		struct write_log_arg arg = {
+			.log = &logs[i],
+			.update_index = reftable_stack_next_update_index(st),
+		};
+		cl_assert(reftable_stack_add(st, write_test_log, &arg) == 0);
+	}
+
+	cl_assert(reftable_stack_compact_all(st, NULL) == 0);
+	cl_assert(reftable_stack_compact_all(st, &expiry) == 0);
+	cl_assert_equal_i(reftable_stack_read_log(st, logs[9].refname, &log), 1);
+	cl_assert(reftable_stack_read_log(st, logs[11].refname, &log) == 0);
+
+	expiry.min_update_index = 15;
+	cl_assert(reftable_stack_compact_all(st, &expiry) == 0);
+	cl_assert_equal_i(reftable_stack_read_log(st, logs[14].refname, &log), 1);
+	cl_assert(reftable_stack_read_log(st, logs[16].refname, &log) == 0);
+
+	/* cleanup */
+	reftable_stack_destroy(st);
+	for (i = 0; i <= N; i++)
+		reftable_log_record_release(&logs[i]);
+	clear_dir(dir);
+	reftable_log_record_release(&log);
+}
+
+static int write_nothing(struct reftable_writer *wr, void *arg UNUSED)
+{
+	cl_assert(reftable_writer_set_limits(wr, 1, 1) == 0);
+	return 0;
+}
+
+void test_reftable_stack__empty_add(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_stack *st2 = NULL;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+	cl_assert(reftable_stack_add(st, write_nothing, NULL) == 0);
+	cl_assert(reftable_new_stack(&st2, dir, &opts) == 0);
+	clear_dir(dir);
+	reftable_stack_destroy(st);
+	reftable_stack_destroy(st2);
+}
+
+static int fastlogN(uint64_t sz, uint64_t N)
+{
+	int l = 0;
+	if (sz == 0)
+		return 0;
+	for (; sz; sz /= N)
+		l++;
+	return l - 1;
+}
+
+void test_reftable_stack__auto_compaction(void)
+{
+	struct reftable_write_options opts = {
+		.disable_auto_compact = 1,
+	};
+	struct reftable_stack *st = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	size_t i, N = 100;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	for (i = 0; i < N; i++) {
+		char name[100];
+		struct reftable_ref_record ref = {
+			.refname = name,
+			.update_index = reftable_stack_next_update_index(st),
+			.value_type = REFTABLE_REF_SYMREF,
+			.value.symref = (char *) "master",
+		};
+		snprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i);
+
+		cl_assert(reftable_stack_add(st, write_test_ref, &ref) == 0);
+		cl_assert(reftable_stack_auto_compact(st) == 0);
+		cl_assert(i < 2 || st->merged->readers_len < 2 * fastlogN(i, 2));
+	}
+
+	cl_assert(reftable_stack_compaction_stats(st)->entries_written <
+			  (uint64_t)(N * fastlogN(N, 2)));
+
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__auto_compaction_factor(void)
+{
+	struct reftable_write_options opts = {
+		.auto_compaction_factor = 5,
+	};
+	struct reftable_stack *st = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	size_t N = 100;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	for (size_t i = 0; i < N; i++) {
+		char name[20];
+		struct reftable_ref_record ref = {
+			.refname = name,
+			.update_index = reftable_stack_next_update_index(st),
+			.value_type = REFTABLE_REF_VAL1,
+		};
+		xsnprintf(name, sizeof(name), "branch%04"PRIuMAX, (uintmax_t)i);
+
+		cl_assert(reftable_stack_add(st, &write_test_ref, &ref) == 0);
+		cl_assert(i < 5 || st->merged->readers_len < 5 * fastlogN(i, 5));
+	}
+
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__auto_compaction_with_locked_tables(void)
+{
+	struct reftable_write_options opts = {
+		.disable_auto_compact = 1,
+	};
+	struct reftable_stack *st = NULL;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	char *dir = get_tmp_dir(__LINE__);
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	write_n_ref_tables(st, 5);
+	cl_assert_equal_i(st->merged->readers_len, 5);
+
+	/*
+	 * Given that all tables we have written should be roughly the same
+	 * size, we expect that auto-compaction will want to compact all of the
+	 * tables. Locking any of the tables will keep it from doing so.
+	 */
+	cl_assert(reftable_buf_addstr(&buf, dir) == 0);
+	cl_assert(reftable_buf_addstr(&buf, "/") == 0);
+	cl_assert(reftable_buf_addstr(&buf, st->readers[2]->name) == 0);
+	cl_assert(reftable_buf_addstr(&buf, ".lock") == 0);
+	write_file_buf(buf.buf, "", 0);
+
+	/*
+	 * When parts of the stack are locked, then auto-compaction does a best
+	 * effort compaction of those tables which aren't locked. So while this
+	 * would in theory compact all tables, due to the preexisting lock we
+	 * only compact the newest two tables.
+	 */
+	cl_assert(reftable_stack_auto_compact(st) == 0);
+	cl_assert_equal_i(st->stats.failures, 0);
+	cl_assert_equal_i(st->merged->readers_len, 4);
+
+	reftable_stack_destroy(st);
+	reftable_buf_release(&buf);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__add_performs_auto_compaction(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	size_t i, n = 20;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	for (i = 0; i <= n; i++) {
+		struct reftable_ref_record ref = {
+			.update_index = reftable_stack_next_update_index(st),
+			.value_type = REFTABLE_REF_SYMREF,
+			.value.symref = (char *) "master",
+		};
+		char buf[128];
+
+		/*
+		 * Disable auto-compaction for all but the last runs. Like this
+		 * we can ensure that we indeed honor this setting and have
+		 * better control over when exactly auto compaction runs.
+		 */
+		st->opts.disable_auto_compact = i != n;
+
+		snprintf(buf, sizeof(buf), "branch-%04"PRIuMAX, (uintmax_t)i);
+		ref.refname = buf;
+
+		cl_assert(reftable_stack_add(st, write_test_ref, &ref) == 0);
+
+		/*
+		 * The stack length should grow continuously for all runs where
+		 * auto compaction is disabled. When enabled, we should merge
+		 * all tables in the stack.
+		 */
+		if (i != n)
+			cl_assert_equal_i(st->merged->readers_len, i + 1);
+		else
+			cl_assert_equal_i(st->merged->readers_len, 1);
+	}
+
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__compaction_with_locked_tables(void)
+{
+	struct reftable_write_options opts = {
+		.disable_auto_compact = 1,
+	};
+	struct reftable_stack *st = NULL;
+	struct reftable_buf buf = REFTABLE_BUF_INIT;
+	char *dir = get_tmp_dir(__LINE__);
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	write_n_ref_tables(st, 3);
+	cl_assert_equal_i(st->merged->readers_len, 3);
+
+	/* Lock one of the tables that we're about to compact. */
+	cl_assert(reftable_buf_addstr(&buf, dir) == 0);
+	cl_assert(reftable_buf_addstr(&buf, "/") == 0);
+	cl_assert(reftable_buf_addstr(&buf, st->readers[1]->name) == 0);
+	cl_assert(reftable_buf_addstr(&buf, ".lock") == 0);
+	write_file_buf(buf.buf, "", 0);
+
+	/*
+	 * Compaction is expected to fail given that we were not able to
+	 * compact all tables.
+	 */
+	cl_assert_equal_i(reftable_stack_compact_all(st, NULL), REFTABLE_LOCK_ERROR);
+	cl_assert_equal_i(st->stats.failures, 1);
+	cl_assert_equal_i(st->merged->readers_len, 3);
+
+	reftable_stack_destroy(st);
+	reftable_buf_release(&buf);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__compaction_concurrent(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st1 = NULL, *st2 = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+
+	cl_assert(reftable_new_stack(&st1, dir, &opts) == 0);
+	write_n_ref_tables(st1, 3);
+
+	cl_assert(reftable_new_stack(&st2, dir, &opts) == 0);
+	cl_assert(reftable_stack_compact_all(st1, NULL) == 0);
+
+	reftable_stack_destroy(st1);
+	reftable_stack_destroy(st2);
+
+	cl_assert_equal_i(count_dir_entries(dir), 2);
+	clear_dir(dir);
+}
+
+static void unclean_stack_close(struct reftable_stack *st)
+{
+	/* break abstraction boundary to simulate unclean shutdown. */
+	for (size_t i = 0; i < st->readers_len; i++)
+		reftable_reader_decref(st->readers[i]);
+	st->readers_len = 0;
+	REFTABLE_FREE_AND_NULL(st->readers);
+}
+
+void test_reftable_stack__compaction_concurrent_clean(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st1 = NULL, *st2 = NULL, *st3 = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+
+	cl_assert(reftable_new_stack(&st1, dir, &opts) == 0);
+	write_n_ref_tables(st1, 3);
+
+	cl_assert(reftable_new_stack(&st2, dir, &opts) == 0);
+	cl_assert(reftable_stack_compact_all(st1, NULL) == 0);
+
+	unclean_stack_close(st1);
+	unclean_stack_close(st2);
+
+	cl_assert(reftable_new_stack(&st3, dir, &opts) == 0);
+	cl_assert(reftable_stack_clean(st3) == 0);
+	cl_assert_equal_i(count_dir_entries(dir), 2);
+
+	reftable_stack_destroy(st1);
+	reftable_stack_destroy(st2);
+	reftable_stack_destroy(st3);
+
+	clear_dir(dir);
+}
+
+void test_reftable_stack__read_across_reload(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st1 = NULL, *st2 = NULL;
+	struct reftable_ref_record rec = { 0 };
+	struct reftable_iterator it = { 0 };
+	char *dir = get_tmp_dir(__LINE__);
+
+	/* Create a first stack and set up an iterator for it. */
+	cl_assert(reftable_new_stack(&st1, dir, &opts) == 0);
+	write_n_ref_tables(st1, 2);
+	cl_assert_equal_i(st1->merged->readers_len, 2);
+	reftable_stack_init_ref_iterator(st1, &it);
+	cl_assert(reftable_iterator_seek_ref(&it, "") == 0);
+
+	/* Set up a second stack for the same directory and compact it. */
+	cl_assert(reftable_new_stack(&st2, dir, &opts) == 0);
+	cl_assert_equal_i(st2->merged->readers_len, 2);
+	cl_assert(reftable_stack_compact_all(st2, NULL) == 0);
+	cl_assert_equal_i(st2->merged->readers_len, 1);
+
+	/*
+	 * Verify that we can continue to use the old iterator even after we
+	 * have reloaded its stack.
+	 */
+	cl_assert(reftable_stack_reload(st1) == 0);
+	cl_assert_equal_i(st1->merged->readers_len, 1);
+	cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+	cl_assert_equal_s(rec.refname, "refs/heads/branch-0000");
+	cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+	cl_assert_equal_s(rec.refname, "refs/heads/branch-0001");
+	cl_assert(reftable_iterator_next_ref(&it, &rec) > 0);
+
+	reftable_ref_record_release(&rec);
+	reftable_iterator_destroy(&it);
+	reftable_stack_destroy(st1);
+	reftable_stack_destroy(st2);
+	clear_dir(dir);
+}
+
+void test_reftable_stack__reload_with_missing_table(void)
+{
+	struct reftable_write_options opts = { 0 };
+	struct reftable_stack *st = NULL;
+	struct reftable_ref_record rec = { 0 };
+	struct reftable_iterator it = { 0 };
+	struct reftable_buf table_path = REFTABLE_BUF_INIT, content = REFTABLE_BUF_INIT;
+	char *dir = get_tmp_dir(__LINE__);
+
+	/* Create a first stack and set up an iterator for it. */
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+	write_n_ref_tables(st, 2);
+	cl_assert_equal_i(st->merged->readers_len, 2);
+	reftable_stack_init_ref_iterator(st, &it);
+	cl_assert(reftable_iterator_seek_ref(&it, "") == 0);
+
+	/*
+	 * Update the tables.list file with some garbage data, while reusing
+	 * our old readers. This should trigger a partial reload of the stack,
+	 * where we try to reuse our old readers.
+	*/
+	cl_assert(reftable_buf_addstr(&content, st->readers[0]->name) == 0);
+	cl_assert(reftable_buf_addstr(&content, "\n") == 0);
+	cl_assert(reftable_buf_addstr(&content, st->readers[1]->name) == 0);
+	cl_assert(reftable_buf_addstr(&content, "\n") == 0);
+	cl_assert(reftable_buf_addstr(&content, "garbage\n") == 0);
+	cl_assert(reftable_buf_addstr(&table_path, st->list_file) == 0);
+	cl_assert(reftable_buf_addstr(&table_path, ".lock") == 0);
+	write_file_buf(table_path.buf, content.buf, content.len);
+	cl_assert(rename(table_path.buf, st->list_file) == 0);
+
+	cl_assert_equal_i(reftable_stack_reload(st), -4);
+	cl_assert_equal_i(st->merged->readers_len, 2);
+
+	/*
+	 * Even though the reload has failed, we should be able to continue
+	 * using the iterator.
+	*/
+	cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+	cl_assert_equal_s(rec.refname, "refs/heads/branch-0000");
+	cl_assert(reftable_iterator_next_ref(&it, &rec) == 0);
+	cl_assert_equal_s(rec.refname, "refs/heads/branch-0001");
+	cl_assert(reftable_iterator_next_ref(&it, &rec) > 0);
+
+	reftable_ref_record_release(&rec);
+	reftable_iterator_destroy(&it);
+	reftable_stack_destroy(st);
+	reftable_buf_release(&table_path);
+	reftable_buf_release(&content);
+	clear_dir(dir);
+}
+
+static int write_limits_after_ref(struct reftable_writer *wr, void *arg)
+{
+	struct reftable_ref_record *ref = arg;
+	cl_assert(reftable_writer_set_limits(wr, ref->update_index,
+										 ref->update_index) == 0);
+	cl_assert(reftable_writer_add_ref(wr, ref) == 0);
+	return reftable_writer_set_limits(wr, ref->update_index, ref->update_index);
+}
+
+void test_reftable_stack__invalid_limit_updates(void)
+{
+	struct reftable_ref_record ref = {
+		.refname = (char *) "HEAD",
+		.update_index = 1,
+		.value_type = REFTABLE_REF_SYMREF,
+		.value.symref = (char *) "master",
+	};
+	struct reftable_write_options opts = {
+		.default_permissions = 0660,
+	};
+	struct reftable_addition *add = NULL;
+	char *dir = get_tmp_dir(__LINE__);
+	struct reftable_stack *st = NULL;
+
+	cl_assert(reftable_new_stack(&st, dir, &opts) == 0);
+
+	reftable_addition_destroy(add);
+
+	cl_assert(reftable_stack_new_addition(&add, st, 0) == 0);
+
+	/*
+	 * write_limits_after_ref also updates the update indexes after adding
+	 * the record. This should cause an err to be returned, since the limits
+	 * must be set at the start.
+	 */
+	cl_assert_equal_i(reftable_addition_add(add, write_limits_after_ref, &ref),
+					  REFTABLE_API_ERROR);
+
+	reftable_addition_destroy(add);
+	reftable_stack_destroy(st);
+	clear_dir(dir);
+}
-- 
2.43.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help