threads / patch / 63370

v2, 10 partst/unit-tests: convert unit-tests to use clar

Subject: [PATCH v2 00/10] t/unit-tests: convert unit-tests to use clar

## tl;dr

31 messages between Apr 29, 2025 and May 26, 2025. Diffs are folded; open one to read it.

replies: 30people: 3as markdown or json

Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · lore
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.

Changes in v2:
 - some code refactoring based on review, which includes 
 temporary relocation of reftable helper functions to 
 t/unit-tests/unit-test.{c,h} files in the initial commit

Thanks Seyi

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
Seyi Kuforiji (10):
  t/unit-tests: implement reftable test helper functions in
    unit-test.{c,h}
  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
  t/unit-tests: adapt lib-reftable{c,h} helper functions to 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 ++
 t/unit-tests/u-reftable-readwrite.c |  870 ++++++++++++++++
 t/unit-tests/u-reftable-record.c    |  565 +++++++++++
 t/unit-tests/u-reftable-stack.c     | 1247 +++++++++++++++++++++++
 20 files changed, 4031 insertions(+), 4463 deletions(-)
 delete mode 100644 t/unit-tests/t-reftable-basics.c
 delete mode 100644 t/unit-tests/t-reftable-block.c
 delete mode 100644 t/unit-tests/t-reftable-merged.c
 delete mode 100644 t/unit-tests/t-reftable-pq.c
 delete mode 100644 t/unit-tests/t-reftable-reader.c
 delete mode 100644 t/unit-tests/t-reftable-readwrite.c
 delete mode 100644 t/unit-tests/t-reftable-record.c
 delete mode 100644 t/unit-tests/t-reftable-stack.c
 create mode 100644 t/unit-tests/u-reftable-basics.c
 create mode 100644 t/unit-tests/u-reftable-block.c
 create mode 100644 t/unit-tests/u-reftable-merged.c
 create mode 100644 t/unit-tests/u-reftable-pq.c
 create mode 100644 t/unit-tests/u-reftable-reader.c
 create mode 100644 t/unit-tests/u-reftable-readwrite.c
 create mode 100644 t/unit-tests/u-reftable-record.c
 create mode 100644 t/unit-tests/u-reftable-stack.c
Range-diff against v1:
 -:  ---------- >  1:  d85c5694ca t/unit-tests: implement reftable test helper functions in unit-test.{c,h}
 2:  4c5fc06603 !  2:  d24faa0176 t/unit-tests: convert reftable basics test to use clar test framework
    @@ Makefile: CLAR_TEST_SUITES += u-oid-array
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -UNIT_TEST_PROGRAMS += t-reftable-basics
      UNIT_TEST_PROGRAMS += t-reftable-block
 3:  c9bdff8290 !  3:  d9eb47645b t/unit-tests: convert reftable block test to use clar
    @@ Makefile: CLAR_TEST_SUITES += u-oidmap
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -UNIT_TEST_PROGRAMS += t-reftable-block
      UNIT_TEST_PROGRAMS += t-reftable-merged
 4:  60e2ff8bca !  4:  6ffc7cdfc6 t/unit-tests: convert reftable merged test to use clar
    @@ Makefile: CLAR_TEST_SUITES += u-oidtree
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -UNIT_TEST_PROGRAMS += t-reftable-merged
      UNIT_TEST_PROGRAMS += t-reftable-pq
 5:  b06520b033 !  5:  19b08968a0 t/unit-tests: convert reftable pq test to use clar
    @@ Makefile: CLAR_TEST_SUITES += u-prio-queue
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -UNIT_TEST_PROGRAMS += t-reftable-pq
      UNIT_TEST_PROGRAMS += t-reftable-reader
 6:  8b8ad57ff5 !  6:  b34d6714ed t/unit-tests: convert reftable reader test to use clar
    @@ Makefile: CLAR_TEST_SUITES += u-reftable-basics
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -UNIT_TEST_PROGRAMS += t-reftable-reader
      UNIT_TEST_PROGRAMS += t-reftable-readwrite
 7:  da73e8c85b !  7:  05c909ea94 t/unit-tests: convert reftable readwrite test to use clar
    @@ Makefile: CLAR_TEST_SUITES += u-reftable-block
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -UNIT_TEST_PROGRAMS += t-reftable-readwrite
      UNIT_TEST_PROGRAMS += t-reftable-record
 8:  ac6fd74321 !  8:  1515cf15b7 t/unit-tests: convert reftable record test to use clar
    @@ Makefile: CLAR_TEST_SUITES += u-reftable-merged
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -UNIT_TEST_PROGRAMS += t-reftable-record
      UNIT_TEST_PROGRAMS += t-reftable-stack
 9:  5bc6a599c6 !  9:  dcbb2b6e56 t/unit-tests: convert reftable stack test to use clar
    @@ Makefile: CLAR_TEST_SUITES += u-reftable-pq
      CLAR_TEST_SUITES += u-reftable-tree
      CLAR_TEST_SUITES += u-strbuf
      CLAR_TEST_SUITES += u-strcmp-offset
    -@@ Makefile: CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-oid.o
    - CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/lib-reftable.o
    +@@ Makefile: 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
      
     -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
     
      ## t/meson.build ##
     @@ t/meson.build: clar_test_suites = [
 1:  304d17fef8 ! 10:  79aa0145aa t/unit-tests: adapt lib-reftable{c,h} helper functions to clar
    @@ Commit message
         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.
    +    framework, which requires them. This change migrates the helper
    +    functions back into `lib-reftable.{c,h}`.
     
         Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
     
    @@ Makefile: CLAR_TEST_SUITES += u-urlmatch-normalization
     +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
    -@@ Makefile: 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
    @@ t/unit-tests/lib-reftable.h
      			     struct reftable_ref_record *refs,
      			     size_t nrecords,
      			     struct reftable_log_record *logs,
    +
    + ## t/unit-tests/unit-test.c ##
    +@@
    + #include "unit-test.h"
    + #include "hex.h"
    + #include "parse-options.h"
    +-#include "reftable/constants.h"
    +-#include "reftable/writer.h"
    + #include "strbuf.h"
    + #include "string-list.h"
    + #include "strvec.h"
    + 
    +-void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id)
    +-{
    +-	memset(p, (uint8_t)i, hash_size(id));
    +-}
    +-
    +-static ssize_t strbuf_writer_write(void *b, const void *data, size_t sz)
    +-{
    +-	strbuf_add(b, data, sz);
    +-	return sz;
    +-}
    +-
    +-static int strbuf_writer_flush(void *arg UNUSED)
    +-{
    +-	return 0;
    +-}
    +-
    +-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);
    +-	cl_assert(ret == 0);
    +-	return writer;
    +-}
    +-
    +-void cl_reftable_write_to_buf(struct reftable_buf *buf,
    +-			     struct reftable_ref_record *refs,
    +-			     size_t nrefs,
    +-			     struct reftable_log_record *logs,
    +-			     size_t nlogs,
    +-			     struct reftable_write_options *_opts)
    +-{
    +-	struct reftable_write_options opts = { 0 };
    +-	const struct reftable_stats *stats;
    +-	struct reftable_writer *writer;
    +-	uint64_t min = 0xffffffff;
    +-	uint64_t max = 0;
    +-	int ret;
    +-
    +-	if (_opts)
    +-		opts = *_opts;
    +-
    +-	for (size_t i = 0; i < nrefs; i++) {
    +-		uint64_t ui = refs[i].update_index;
    +-		if (ui > max)
    +-			max = ui;
    +-		if (ui < min)
    +-			min = ui;
    +-	}
    +-	for (size_t i = 0; i < nlogs; i++) {
    +-		uint64_t ui = logs[i].update_index;
    +-		if (ui > max)
    +-			max = ui;
    +-		if (ui < min)
    +-			min = ui;
    +-	}
    +-
    +-	writer = cl_reftable_strbuf_writer(buf, &opts);
    +-	reftable_writer_set_limits(writer, min, max);
    +-
    +-	if (nrefs) {
    +-		ret = reftable_writer_add_refs(writer, refs, nrefs);
    +-		cl_assert_equal_i(ret, 0);
    +-	}
    +-
    +-	if (nlogs) {
    +-		ret = reftable_writer_add_logs(writer, logs, nlogs);
    +-		cl_assert_equal_i(ret, 0);
    +-	}
    +-
    +-	ret = reftable_writer_close(writer);
    +-	cl_assert_equal_i(ret, 0);
    +-
    +-	stats = reftable_writer_stats(writer);
    +-	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);
    +-		cl_assert(buf->buf[off] == 'r');
    +-	}
    +-
    +-	if (nrefs)
    +-		cl_assert(stats->ref_stats.blocks > 0);
    +-	if (nlogs)
    +-		cl_assert(stats->log_stats.blocks > 0);
    +-
    +-	reftable_writer_free(writer);
    +-}
    +-
    + static const char * const unit_test_usage[] = {
    + 	N_("unit-test [<options>]"),
    + 	NULL,
    +
    + ## t/unit-tests/unit-test.h ##
    +@@
    + #include "git-compat-util.h"
    + #include "clar/clar.h"
    + #include "clar-decls.h"
    +-#include "git-compat-util.h"
    +-#include "reftable/reftable-writer.h"
    + #include "strbuf.h"
    + 
    +-struct reftable_buf;
    +-
    +-void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id);
    +-
    +-struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf,
    +-						 struct reftable_write_options *opts);
    +-
    +-void cl_reftable_write_to_buf(struct reftable_buf *buf,
    +-			     struct reftable_ref_record *refs,
    +-			     size_t nrecords,
    +-			     struct reftable_log_record *logs,
    +-			     size_t nlogs,
    +-			     struct reftable_write_options *opts);
    +-
    + #define cl_failf(fmt, ...) do { \
    + 	char desc[4096]; \
    + 	snprintf(desc, sizeof(desc), fmt, __VA_ARGS__); \
-- 
2.43.0
Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · re: Seyi Kuforiji · lore

[PATCH v2 01/10] t/unit-tests: implement reftable test helper functions in unit-test.{c,h}

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++)`, implement equivalent helper functions in unit-test.{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 <kuforiji98@gmail.com>
---
 t/unit-tests/unit-test.c | 93 ++++++++++++++++++++++++++++++++++++++++
 t/unit-tests/unit-test.h | 16 +++++++
 2 files changed, 109 insertions(+)
Show changes to 2 files +109 −0

t/unit-tests/unit-test.c, t/unit-tests/unit-test.h

diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c
index 5af645048a..6c2a4e6aa8 100644
--- a/t/unit-tests/unit-test.c
+++ b/t/unit-tests/unit-test.c
@@ -1,10 +1,103 @@
 #include "unit-test.h"
 #include "hex.h"
 #include "parse-options.h"
+#include "reftable/constants.h"
+#include "reftable/writer.h"
 #include "strbuf.h"
 #include "string-list.h"
 #include "strvec.h"
 
+void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id)
+{
+	memset(p, (uint8_t)i, hash_size(id));
+}
+
+static ssize_t strbuf_writer_write(void *b, const void *data, size_t sz)
+{
+	strbuf_add(b, data, sz);
+	return sz;
+}
+
+static int strbuf_writer_flush(void *arg UNUSED)
+{
+	return 0;
+}
+
+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);
+	cl_assert(ret == 0);
+	return writer;
+}
+
+void cl_reftable_write_to_buf(struct reftable_buf *buf,
+			     struct reftable_ref_record *refs,
+			     size_t nrefs,
+			     struct reftable_log_record *logs,
+			     size_t nlogs,
+			     struct reftable_write_options *_opts)
+{
+	struct reftable_write_options opts = { 0 };
+	const struct reftable_stats *stats;
+	struct reftable_writer *writer;
+	uint64_t min = 0xffffffff;
+	uint64_t max = 0;
+	int ret;
+
+	if (_opts)
+		opts = *_opts;
+
+	for (size_t i = 0; i < nrefs; i++) {
+		uint64_t ui = refs[i].update_index;
+		if (ui > max)
+			max = ui;
+		if (ui < min)
+			min = ui;
+	}
+	for (size_t i = 0; i < nlogs; i++) {
+		uint64_t ui = logs[i].update_index;
+		if (ui > max)
+			max = ui;
+		if (ui < min)
+			min = ui;
+	}
+
+	writer = cl_reftable_strbuf_writer(buf, &opts);
+	reftable_writer_set_limits(writer, min, max);
+
+	if (nrefs) {
+		ret = reftable_writer_add_refs(writer, refs, nrefs);
+		cl_assert_equal_i(ret, 0);
+	}
+
+	if (nlogs) {
+		ret = reftable_writer_add_logs(writer, logs, nlogs);
+		cl_assert_equal_i(ret, 0);
+	}
+
+	ret = reftable_writer_close(writer);
+	cl_assert_equal_i(ret, 0);
+
+	stats = reftable_writer_stats(writer);
+	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);
+		cl_assert(buf->buf[off] == 'r');
+	}
+
+	if (nrefs)
+		cl_assert(stats->ref_stats.blocks > 0);
+	if (nlogs)
+		cl_assert(stats->log_stats.blocks > 0);
+
+	reftable_writer_free(writer);
+}
+
 static const char * const unit_test_usage[] = {
 	N_("unit-test [<options>]"),
 	NULL,
diff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h
index 85e5d6a948..fe0aebd876 100644
--- a/t/unit-tests/unit-test.h
+++ b/t/unit-tests/unit-test.h
@@ -1,8 +1,24 @@
 #include "git-compat-util.h"
 #include "clar/clar.h"
 #include "clar-decls.h"
+#include "git-compat-util.h"
+#include "reftable/reftable-writer.h"
 #include "strbuf.h"
 
+struct reftable_buf;
+
+void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id);
+
+struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf,
+						 struct reftable_write_options *opts);
+
+void cl_reftable_write_to_buf(struct reftable_buf *buf,
+			     struct reftable_ref_record *refs,
+			     size_t nrecords,
+			     struct reftable_log_record *logs,
+			     size_t nlogs,
+			     struct reftable_write_options *opts);
+
 #define cl_failf(fmt, ...) do { \
 	char desc[4096]; \
 	snprintf(desc, sizeof(desc), fmt, __VA_ARGS__); \
-- 
2.43.0
Junio C Hamano· Apr 29, 2025, 23:04 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 01/10] t/unit-tests: implement reftable test helper functions in unit-test.{c,h}

Seyi Kuforiji <kuforiji98@gmail.com> writes:
> Helper functions defined in `t/unit-tests/lib-reftable.{c,h}` are
> required for the reftable-related test files to run efficeintly. In the

efficeintly? effectively? efficiently? correctly? Wouldn't it be sufficient to say "... to run." without anything else?

> 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.
OK.
> type cast `for (size_t i = 0; i < (size_t)stats->ref_stats.blocks;
> i++)`, implement equivalent helper functions in unit-test.{c,h} to use
> clar.
I cannot quite parse this.
> 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.
OK.
Show 5 quoted lines
> Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
> ---
>  t/unit-tests/unit-test.c | 93 ++++++++++++++++++++++++++++++++++++++++
>  t/unit-tests/unit-test.h | 16 +++++++
>  2 files changed, 109 insertions(+)

Hmph, this probably is a question better asked to Patrick, but it somehow feels a bit unsatisfactory that we are duplicating instead of replacing, as we cannot see "ah, this removed thing is now added in a different shape to fit in the other framework" in the patch--- instead what we see in the patch is a bunch of "a completely new thing that honors the convention we are familiar in existing clar based tests is added here".

Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 01/10] t/unit-tests: implement reftable test helper functions in unit-test.{c,h}

On Tue, Apr 29, 2025 at 04:04:22PM -0700, Junio C Hamano wrote:
Show 14 quoted lines
> Seyi Kuforiji <kuforiji98@gmail.com> writes:
> > Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
> > ---
> >  t/unit-tests/unit-test.c | 93 ++++++++++++++++++++++++++++++++++++++++
> >  t/unit-tests/unit-test.h | 16 +++++++
> >  2 files changed, 109 insertions(+)
> 
> Hmph, this probably is a question better asked to Patrick, but it
> somehow feels a bit unsatisfactory that we are duplicating instead
> of replacing, as we cannot see "ah, this removed thing is now added
> in a different shape to fit in the other framework" in the patch---
> instead what we see in the patch is a bunch of "a completely new
> thing that honors the convention we are familiar in existing clar
> based tests is added here".

Well, my expectation is that at the end of this patch series we will remove the old functionality. The steps should roughly be:

  1. Implement equivalents for the reftable-specific test library for
     clar.
  2. Convert all reftable unit tests to use the new functions.
  3. Remove the old functions.

Given that (2) is a bunch of tests it makes perfect sense to first have a separate patch that adds without removing yet.

What I found a bit confusing though is that the new functions aren't added to "t/unit-tests/lib-reftable.{c,h}". This would be very much preferable from my point of view as the test functions aren't globally relevant, but really only relevant to the reftable library.

Patrick
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 01/10] t/unit-tests: implement reftable test helper functions in unit-test.{c,h}

On Tue, Apr 29, 2025 at 06:52:53PM +0100, Seyi Kuforiji wrote:
Show 19 quoted lines
> 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++)`, implement equivalent helper functions in unit-test.{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 <kuforiji98@gmail.com>
> ---
>  t/unit-tests/unit-test.c | 93 ++++++++++++++++++++++++++++++++++++++++
>  t/unit-tests/unit-test.h | 16 +++++++
>  2 files changed, 109 insertions(+)

I think this functionality should be added to "t/unit-tests/lib-reftable.{c,h}" instead of to the generic unit testing library as it is highly specific to reftables.

Show 37 quoted lines
> diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c
> index 5af645048a..6c2a4e6aa8 100644
> --- a/t/unit-tests/unit-test.c
> +++ b/t/unit-tests/unit-test.c
> @@ -1,10 +1,103 @@
>  #include "unit-test.h"
>  #include "hex.h"
>  #include "parse-options.h"
> +#include "reftable/constants.h"
> +#include "reftable/writer.h"
>  #include "strbuf.h"
>  #include "string-list.h"
>  #include "strvec.h"
>  
> +void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id)
> +{
> +	memset(p, (uint8_t)i, hash_size(id));
> +}
> +
> +static ssize_t strbuf_writer_write(void *b, const void *data, size_t sz)
> +{
> +	strbuf_add(b, data, sz);
> +	return sz;
> +}
> +
> +static int strbuf_writer_flush(void *arg UNUSED)
> +{
> +	return 0;
> +}
> +
> +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);
> +	cl_assert(ret == 0);

We typically don't explicitly compare with zero, so this should rather be `cl_assert(!ret)`.

Show 37 quoted lines
> +	return writer;
> +}
> +
> +void cl_reftable_write_to_buf(struct reftable_buf *buf,
> +			     struct reftable_ref_record *refs,
> +			     size_t nrefs,
> +			     struct reftable_log_record *logs,
> +			     size_t nlogs,
> +			     struct reftable_write_options *_opts)
> +{
> +	struct reftable_write_options opts = { 0 };
> +	const struct reftable_stats *stats;
> +	struct reftable_writer *writer;
> +	uint64_t min = 0xffffffff;
> +	uint64_t max = 0;
> +	int ret;
> +
> +	if (_opts)
> +		opts = *_opts;
> +
> +	for (size_t i = 0; i < nrefs; i++) {
> +		uint64_t ui = refs[i].update_index;
> +		if (ui > max)
> +			max = ui;
> +		if (ui < min)
> +			min = ui;
> +	}
> +	for (size_t i = 0; i < nlogs; i++) {
> +		uint64_t ui = logs[i].update_index;
> +		if (ui > max)
> +			max = ui;
> +		if (ui < min)
> +			min = ui;
> +	}
> +
> +	writer = cl_reftable_strbuf_writer(buf, &opts);
> +	reftable_writer_set_limits(writer, min, max);

This function may return an error, as well, so let's verify it while at it.

Patrick
Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · re: Seyi Kuforiji · lore

[PATCH v2 02/10] t/unit-tests: convert reftable basics test to use clar test framework

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 <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +197 −221

Makefile, t/meson.build, t/unit-tests/t-reftable-basics.c, t/unit-tests/u-reftable-basics.c

diff --git a/Makefile b/Makefile
index 13f9062a05..7b12bb078c 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
@@ -1374,7 +1375,6 @@ 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
 
-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 bfb744e886..8a42b595d9 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',
@@ -54,7 +55,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
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 02/10] t/unit-tests: convert reftable basics test to use clar test framework

On Tue, Apr 29, 2025 at 06:52:54PM +0100, Seyi Kuforiji wrote:
Show 344 quoted lines
> 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 <kuforiji98@gmail.com>
> ---
>  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 13f9062a05..7b12bb078c 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
> @@ -1374,7 +1375,6 @@ 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
>  
> -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 bfb744e886..8a42b595d9 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',
> @@ -54,7 +55,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);
> +	}
> +
Nit: empty newline can be removed.
Show 38 quoted lines
> +}
> +
> +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);
> +}
> +
I think you missed converting "parse_names drops empty string".
Show 21 quoted lines
> +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);

Why did you change the order of `reftable_buf_reset()` calls? The reordered logic achieves the same result, but I'd recommend to keep things as-is so that reviewers aren't puzzled by this arbitrary change.

> +	}
> +	reftable_buf_release(&a);
> +	reftable_buf_release(&b);
> +}
I miss tests for `put_be64` and `put_be32`.
Show 21 quoted lines
> +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)
Let's rename this to `__alloc_grow()` to match the name of the function.
Show 27 quoted lines
> +{
> +	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)
Same here, let's rename to `alloc_grow_or_null`.
Show 24 quoted lines
> +{
> +	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);
> +}
Other than that this looks well-done to me, thanks!
Patrick
Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · re: Seyi Kuforiji · lore

[PATCH v2 03/10] t/unit-tests: convert reftable block test to use clar

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

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +375 −385

Makefile, t/meson.build, t/unit-tests/t-reftable-block.c, t/unit-tests/u-reftable-block.c

diff --git a/Makefile b/Makefile
index 7b12bb078c..239c575dad 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
@@ -1375,7 +1376,6 @@ 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
 
-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 8a42b595d9..e0d723e74b 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',
@@ -55,7 +56,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
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 03/10] t/unit-tests: convert reftable block test to use clar

On Tue, Apr 29, 2025 at 06:52:55PM +0100, Seyi Kuforiji wrote:
Show 5 quoted lines
> 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

Hm, why is this recorded as a delete and creation? Weird, inspecting the diff locally properly shows it as a rename, which makes it a ton easier to review. It would be great if you could try to play around with the `--find-renames` option in the next iteration of this series and double check that these are shown as a rename.

Show 22 quoted lines
> 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)

This doesn't got to do anything with indices but with refs, so I'd rename this to `__ref_read_write()`.

Show 25 quoted lines
> +{
> +	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);

Same comment here, asserts like this can be retained as `cl_assert(!ret)`.

Show 19 quoted lines
> +	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);

It's a bit unfortunate that we have to use `cl_assert()` here, but that isn't the fault of this series. I do have a pull request pending upstream that introduces integer comparisons. Once we've updated to that version I'll go through our unit tests and adapt callsites accordingly.

[snip]
> +void test_reftable_block__ref_read_write(void)

This one here should be called `__index_read_write()`. I guess you confused the first and and this test name with one another.

Patrick
Seyi Chamber· May 5, 2025, 07:37 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 03/10] t/unit-tests: convert reftable block test to use clar

On Fri, 2 May 2025 at 10:57, Patrick Steinhardt <ps@pks.im> wrote:
Show 14 quoted lines
>
> On Tue, Apr 29, 2025 at 06:52:55PM +0100, Seyi Kuforiji wrote:
> > 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
>
> Hm, why is this recorded as a delete and creation? Weird, inspecting the
> diff locally properly shows it as a rename, which makes it a ton easier
> to review. It would be great if you could try to play around with the
> `--find-renames` option in the next iteration of this series and double
> check that these are shown as a rename.
>

I used `--find-renames=90` for this patch series. Is there any recommended number to set it to? :/

Show 55 quoted lines
> > 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)
>
> This doesn't got to do anything with indices but with refs, so I'd
> rename this to `__ref_read_write()`.
>
> > +{
> > +     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);
>
> Same comment here, asserts like this can be retained as
> `cl_assert(!ret)`.
>
ohh, I didn't know this was possible.
Show 32 quoted lines
> > +     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);
>
> It's a bit unfortunate that we have to use `cl_assert()` here, but that
> isn't the fault of this series. I do have a pull request pending
> upstream that introduces integer comparisons. Once we've updated to that
> version I'll go through our unit tests and adapt callsites accordingly.
>
> [snip]
> > +void test_reftable_block__ref_read_write(void)
>
> This one here should be called `__index_read_write()`. I guess you
> confused the first and and this test name with one another.
>
> Patrick
Ah, okay, Thanks!

Best Seyi

Patrick Steinhardt· May 5, 2025, 09:52 UTC · re: Seyi Chamber · lore

Re: [PATCH v2 03/10] t/unit-tests: convert reftable block test to use clar

On Mon, May 05, 2025 at 08:37:27AM +0100, Seyi Chamber wrote:
Show 18 quoted lines
> On Fri, 2 May 2025 at 10:57, Patrick Steinhardt <ps@pks.im> wrote:
> >
> > On Tue, Apr 29, 2025 at 06:52:55PM +0100, Seyi Kuforiji wrote:
> > > 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
> >
> > Hm, why is this recorded as a delete and creation? Weird, inspecting the
> > diff locally properly shows it as a rename, which makes it a ton easier
> > to review. It would be great if you could try to play around with the
> > `--find-renames` option in the next iteration of this series and double
> > check that these are shown as a rename.
> >
> 
> I used `--find-renames=90` for this patch series. Is there any
> recommended number to set it to? :/

Note that "90" also isn't interpreted the way you think it is, at least going by git-format-patch(1):

    -M[<n>], --find-renames[=<n>]
        Detect renames. If <n> is specified, it is a threshold on the similarity index (i.e. amount
        of addition/deletions compared to the file’s size). For example, -M90% means Git should
        consider a delete/add pair to be a rename if more than 90% of the file hasn’t changed.
        Without a % sign, the number is to be read as a fraction, with a decimal point before it.
        I.e., -M5 becomes 0.5, and is thus the same as -M50%. Similarly, -M05 is the same as -M5%.
        To limit detection to exact renames, use -M100%. The default similarity index is 50%.

What you probably wanted to say is `--find-renames=90%`, but without the percentage sign it is read as a fraction, where 1 means "exact copy" and 0 means "all different".

I'd recommend to just play around with this option and inspect the outcome until things look reviewable. :)

Patrick
Junio C Hamano· May 5, 2025, 21:14 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 03/10] t/unit-tests: convert reftable block test to use clar

Patrick Steinhardt <ps@pks.im> writes:
Show 11 quoted lines
>     -M[<n>], --find-renames[=<n>]
>         Detect renames. If <n> is specified, it is a threshold on the similarity index (i.e. amount
>         of addition/deletions compared to the file’s size). For example, -M90% means Git should
>         consider a delete/add pair to be a rename if more than 90% of the file hasn’t changed.
>         Without a % sign, the number is to be read as a fraction, with a decimal point before it.
>         I.e., -M5 becomes 0.5, and is thus the same as -M50%. Similarly, -M05 is the same as -M5%.
>         To limit detection to exact renames, use -M100%. The default similarity index is 50%.
>
> What you probably wanted to say is `--find-renames=90%`, but without the
> percentage sign it is read as a fraction, where 1 means "exact copy" and
> 0 means "all different".

I am confused. -M<number> without trailing %-sign is taken as fraction against 1 followed by the same number of '0' has the <number> has digits. -M5 is 5 over 10, -M50 is 50 over 100. -M90 is 90 over 100, so -M90 and -M90% should mean the same thing.

But you are right. When you want to claim your pre- and post- image files still correspond with each other in a meaningful way, even after making extensive change, you would want to _lower_, not raise, your similarity threshold. If the default is -M50, then -M90 would be a useful option to reject what Git (mistakenly) thinks are renames and tell it to instead consider they are removals and creations.

THanks.
Patrick Steinhardt· May 6, 2025, 05:10 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 03/10] t/unit-tests: convert reftable block test to use clar

On Mon, May 05, 2025 at 02:14:39PM -0700, Junio C Hamano wrote:
Show 18 quoted lines
> Patrick Steinhardt <ps@pks.im> writes:
> 
> >     -M[<n>], --find-renames[=<n>]
> >         Detect renames. If <n> is specified, it is a threshold on the similarity index (i.e. amount
> >         of addition/deletions compared to the file’s size). For example, -M90% means Git should
> >         consider a delete/add pair to be a rename if more than 90% of the file hasn’t changed.
> >         Without a % sign, the number is to be read as a fraction, with a decimal point before it.
> >         I.e., -M5 becomes 0.5, and is thus the same as -M50%. Similarly, -M05 is the same as -M5%.
> >         To limit detection to exact renames, use -M100%. The default similarity index is 50%.
> >
> > What you probably wanted to say is `--find-renames=90%`, but without the
> > percentage sign it is read as a fraction, where 1 means "exact copy" and
> > 0 means "all different".
> 
> I am confused.  -M<number> without trailing %-sign is taken as
> fraction against 1 followed by the same number of '0' has the
> <number> has digits.  -M5 is 5 over 10, -M50 is 50 over 100.  -M90
> is 90 over 100, so -M90 and -M90% should mean the same thing.

Hm, I guess _I_ was confused then. It doesn't feel natural to me that -M90 would mean 90% whereas -M5 means 50%, and the documentation isn't quite clear about this, either.

Show 6 quoted lines
> But you are right.  When you want to claim your pre- and post- image
> files still correspond with each other in a meaningful way, even
> after making extensive change, you would want to _lower_, not raise,
> your similarity threshold.  If the default is -M50, then -M90 would
> be a useful option to reject what Git (mistakenly) thinks are renames
> and tell it to instead consider they are removals and creations.
Yeah, at least that point stands :)
Patrick
Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · re: Seyi Kuforiji · lore

[PATCH v2 04/10] t/unit-tests: convert reftable merged test to use clar

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

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +517 −548

Makefile, t/meson.build, t/unit-tests/t-reftable-merged.c, t/unit-tests/u-reftable-merged.c

diff --git a/Makefile b/Makefile
index 239c575dad..2b1642465a 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
@@ -1376,7 +1377,6 @@ 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
 
-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 e0d723e74b..70a783ba80 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',
@@ -56,7 +57,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
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 04/10] t/unit-tests: convert reftable merged test to use clar

On Tue, Apr 29, 2025 at 06:52:56PM +0100, Seyi Kuforiji wrote:
Show 63 quoted lines
> 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)

This should be `__single_record(void)`. Same for the others, the `__merged` prefix is somewhat duplicate as you already have it in the test suite name.

> +void test_reftable_merged__merged_seek_multiple_times(void)
So, same here, let's rename to `__seek_multiple_times()`.
Show 54 quoted lines
> +{
> +	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);

Indentation is wrong. Our tabs are 8 spaces, so this is indented too deep now.

> +		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);
Same here. There are also other instances in this file.
Patrick
Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · re: Seyi Kuforiji · lore

[PATCH v2 05/10] t/unit-tests: convert reftable pq test to use clar

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

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +154 −163

Makefile, t/meson.build, t/unit-tests/t-reftable-pq.c, t/unit-tests/u-reftable-pq.c

diff --git a/Makefile b/Makefile
index 2b1642465a..4142927d0a 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
@@ -1377,7 +1378,6 @@ 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
 
-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 70a783ba80..9bded2d15c 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',
@@ -57,7 +58,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
Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · re: Seyi Kuforiji · lore

[PATCH v2 06/10] t/unit-tests: convert reftable reader test to use clar

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

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +80 −98

Makefile, t/meson.build, t/unit-tests/t-reftable-reader.c, t/unit-tests/u-reftable-reader.c

diff --git a/Makefile b/Makefile
index 4142927d0a..d3e8677653 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
@@ -1378,7 +1379,6 @@ 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
 
-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 9bded2d15c..6a22bd2790 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',
@@ -58,7 +59,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
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 06/10] t/unit-tests: convert reftable reader test to use clar

On Tue, Apr 29, 2025 at 06:52:58PM +0100, Seyi Kuforiji wrote:
Show 32 quoted lines
> 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);
> +
> +
Nit: empty newline.
Show 7 quoted lines
> +	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);
> +
Indentation is wrong again.
Show 36 quoted lines
> +	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);
Here, as well.
Patrick
Seyi Kuforiji· Apr 29, 2025, 17:52 UTC · re: Seyi Kuforiji · lore

[PATCH v2 07/10] t/unit-tests: convert reftable readwrite test to use clar

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

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +872 −987

Makefile, t/meson.build, t/unit-tests/t-reftable-readwrite.c, t/unit-tests/u-reftable-readwrite.c

diff --git a/Makefile b/Makefile
index d3e8677653..88b6851b37 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
@@ -1379,7 +1380,6 @@ 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
 
-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 6a22bd2790..7722d177e2 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',
@@ -59,7 +60,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
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 07/10] t/unit-tests: convert reftable readwrite test to use clar

On Tue, Apr 29, 2025 at 06:52:59PM +0100, Seyi Kuforiji wrote:
Show 40 quoted lines
> 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);

It feels inconsisetnt that we use `cl_assert_equal_i()` to check for `n` but `cl_assert(... == 0)` here.

Patrick
Seyi Kuforiji· Apr 29, 2025, 17:53 UTC · re: Seyi Kuforiji · lore

[PATCH v2 08/10] t/unit-tests: convert reftable record test to use clar

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

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +567 −587

Makefile, t/meson.build, t/unit-tests/t-reftable-record.c, t/unit-tests/u-reftable-record.c

diff --git a/Makefile b/Makefile
index 88b6851b37..cd9db9adf1 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
@@ -1380,7 +1381,6 @@ 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
 
-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 7722d177e2..756cb2a2dd 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',
@@ -60,7 +61,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
Seyi Kuforiji· Apr 29, 2025, 17:53 UTC · re: Seyi Kuforiji · lore

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

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 <kuforiji98@gmail.com>
---
 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
Show changes to 4 files +1249 −1454

Makefile, t/meson.build, t/unit-tests/t-reftable-stack.c, t/unit-tests/u-reftable-stack.c

diff --git a/Makefile b/Makefile
index cd9db9adf1..0b42893611 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
@@ -1381,7 +1382,6 @@ 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
 
-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
diff --git a/t/meson.build b/t/meson.build
index 756cb2a2dd..8fa00fc9ef 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',
@@ -61,7 +62,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
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

Re: [PATCH v2 09/10] t/unit-tests: convert reftable stack test to use clar

On Tue, Apr 29, 2025 at 06:53:01PM +0100, Seyi Kuforiji wrote:
Show 12 quoted lines
> diff --git a/t/meson.build b/t/meson.build
> index 756cb2a2dd..8fa00fc9ef 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -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,

Okay. Ideally this would be moved into the next commit where we remove the infrastructure for our old-style unit tests, but we can't because the glob matches nothing anymore and thus causes an error.

Also, we have a "check-meson" target in "t/Makefile". Don't we have to remove unit tests from there, too?

Patrick
Seyi Chamber· May 5, 2025, 09:11 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 09/10] t/unit-tests: convert reftable stack test to use clar

On Fri, 2 May 2025 at 10:57, Patrick Steinhardt <ps@pks.im> wrote:
Show 23 quoted lines
>
> On Tue, Apr 29, 2025 at 06:53:01PM +0100, Seyi Kuforiji wrote:
> > diff --git a/t/meson.build b/t/meson.build
> > index 756cb2a2dd..8fa00fc9ef 100644
> > --- a/t/meson.build
> > +++ b/t/meson.build
> > @@ -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,
>
> Okay. Ideally this would be moved into the next commit where we remove
> the infrastructure for our old-style unit tests, but we can't because
> the glob matches nothing anymore and thus causes an error.
>
> Also, we have a "check-meson" target in "t/Makefile". Don't we have to
> remove unit tests from there, too?
>
> Patrick
You are referring to this, yes?
`@# awk acts up when trying to match single quotes, so we use \047 instead.
    @mkdir -p mesontmp && \
    printf "%s\n" \
        "integration_tests t[0-9][0-9][0-9][0-9]-*.sh" \
        "unit_test_programs unit-tests/t-*.c" \
        "clar_test_suites unit-tests/u-*.c" | \
    while read -r variable pattern; do \`

I've hardly taken a look at the `t/Makefile`, given my very little interaction with the file throughout the test conversions.

Best Seyi

Patrick Steinhardt· May 5, 2025, 09:52 UTC · re: Seyi Chamber · lore

Re: [PATCH v2 09/10] t/unit-tests: convert reftable stack test to use clar

On Mon, May 05, 2025 at 10:11:18AM +0100, Seyi Chamber wrote:
Show 36 quoted lines
> On Fri, 2 May 2025 at 10:57, Patrick Steinhardt <ps@pks.im> wrote:
> >
> > On Tue, Apr 29, 2025 at 06:53:01PM +0100, Seyi Kuforiji wrote:
> > > diff --git a/t/meson.build b/t/meson.build
> > > index 756cb2a2dd..8fa00fc9ef 100644
> > > --- a/t/meson.build
> > > +++ b/t/meson.build
> > > @@ -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,
> >
> > Okay. Ideally this would be moved into the next commit where we remove
> > the infrastructure for our old-style unit tests, but we can't because
> > the glob matches nothing anymore and thus causes an error.
> >
> > Also, we have a "check-meson" target in "t/Makefile". Don't we have to
> > remove unit tests from there, too?
> >
> > Patrick
> 
> You are referring to this, yes?
> `@# awk acts up when trying to match single quotes, so we use \047 instead.
>     @mkdir -p mesontmp && \
>     printf "%s\n" \
>         "integration_tests t[0-9][0-9][0-9][0-9]-*.sh" \
>         "unit_test_programs unit-tests/t-*.c" \
>         "clar_test_suites unit-tests/u-*.c" | \
>     while read -r variable pattern; do \`
> 
> I've hardly taken a look at the `t/Makefile`, given my very little
> interaction with the file throughout the test conversions.
Yes, that's the part I was referring to.
Patrick
Seyi Kuforiji· Apr 29, 2025, 17:53 UTC · re: Seyi Kuforiji · lore

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

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 change migrates the helper functions back into `lib-reftable.{c,h}`.

Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
---
 Makefile                    |  4 +-
 t/meson.build               |  4 +-
 t/unit-tests/lib-reftable.c | 26 +++++------
 t/unit-tests/lib-reftable.h |  6 +--
 t/unit-tests/unit-test.c    | 93 -------------------------------------
 t/unit-tests/unit-test.h    | 16 -------
 6 files changed, 20 insertions(+), 129 deletions(-)
Show changes to 6 files +20 −129

Makefile, t/meson.build, t/unit-tests/lib-reftable.c, t/unit-tests/lib-reftable.h, t/unit-tests/unit-test.c, t/unit-tests/unit-test.h

diff --git a/Makefile b/Makefile
index 0b42893611..7e646e16ee 100644
--- a/Makefile
+++ b/Makefile
@@ -1379,12 +1379,12 @@ 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_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 8fa00fc9ef..7c305a90b5 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -26,8 +26,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,
diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c
index 6c2a4e6aa8..5af645048a 100644
--- a/t/unit-tests/unit-test.c
+++ b/t/unit-tests/unit-test.c
@@ -1,103 +1,10 @@
 #include "unit-test.h"
 #include "hex.h"
 #include "parse-options.h"
-#include "reftable/constants.h"
-#include "reftable/writer.h"
 #include "strbuf.h"
 #include "string-list.h"
 #include "strvec.h"
 
-void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id)
-{
-	memset(p, (uint8_t)i, hash_size(id));
-}
-
-static ssize_t strbuf_writer_write(void *b, const void *data, size_t sz)
-{
-	strbuf_add(b, data, sz);
-	return sz;
-}
-
-static int strbuf_writer_flush(void *arg UNUSED)
-{
-	return 0;
-}
-
-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);
-	cl_assert(ret == 0);
-	return writer;
-}
-
-void cl_reftable_write_to_buf(struct reftable_buf *buf,
-			     struct reftable_ref_record *refs,
-			     size_t nrefs,
-			     struct reftable_log_record *logs,
-			     size_t nlogs,
-			     struct reftable_write_options *_opts)
-{
-	struct reftable_write_options opts = { 0 };
-	const struct reftable_stats *stats;
-	struct reftable_writer *writer;
-	uint64_t min = 0xffffffff;
-	uint64_t max = 0;
-	int ret;
-
-	if (_opts)
-		opts = *_opts;
-
-	for (size_t i = 0; i < nrefs; i++) {
-		uint64_t ui = refs[i].update_index;
-		if (ui > max)
-			max = ui;
-		if (ui < min)
-			min = ui;
-	}
-	for (size_t i = 0; i < nlogs; i++) {
-		uint64_t ui = logs[i].update_index;
-		if (ui > max)
-			max = ui;
-		if (ui < min)
-			min = ui;
-	}
-
-	writer = cl_reftable_strbuf_writer(buf, &opts);
-	reftable_writer_set_limits(writer, min, max);
-
-	if (nrefs) {
-		ret = reftable_writer_add_refs(writer, refs, nrefs);
-		cl_assert_equal_i(ret, 0);
-	}
-
-	if (nlogs) {
-		ret = reftable_writer_add_logs(writer, logs, nlogs);
-		cl_assert_equal_i(ret, 0);
-	}
-
-	ret = reftable_writer_close(writer);
-	cl_assert_equal_i(ret, 0);
-
-	stats = reftable_writer_stats(writer);
-	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);
-		cl_assert(buf->buf[off] == 'r');
-	}
-
-	if (nrefs)
-		cl_assert(stats->ref_stats.blocks > 0);
-	if (nlogs)
-		cl_assert(stats->log_stats.blocks > 0);
-
-	reftable_writer_free(writer);
-}
-
 static const char * const unit_test_usage[] = {
 	N_("unit-test [<options>]"),
 	NULL,
diff --git a/t/unit-tests/unit-test.h b/t/unit-tests/unit-test.h
index fe0aebd876..85e5d6a948 100644
--- a/t/unit-tests/unit-test.h
+++ b/t/unit-tests/unit-test.h
@@ -1,24 +1,8 @@
 #include "git-compat-util.h"
 #include "clar/clar.h"
 #include "clar-decls.h"
-#include "git-compat-util.h"
-#include "reftable/reftable-writer.h"
 #include "strbuf.h"
 
-struct reftable_buf;
-
-void cl_reftable_set_hash(uint8_t *p, int i, enum reftable_hash id);
-
-struct reftable_writer *cl_reftable_strbuf_writer(struct reftable_buf *buf,
-						 struct reftable_write_options *opts);
-
-void cl_reftable_write_to_buf(struct reftable_buf *buf,
-			     struct reftable_ref_record *refs,
-			     size_t nrecords,
-			     struct reftable_log_record *logs,
-			     size_t nlogs,
-			     struct reftable_write_options *opts);
-
 #define cl_failf(fmt, ...) do { \
 	char desc[4096]; \
 	snprintf(desc, sizeof(desc), fmt, __VA_ARGS__); \
-- 
2.43.0
Patrick Steinhardt· May 2, 2025, 09:57 UTC · re: Seyi Kuforiji · lore

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

On Tue, Apr 29, 2025 at 06:53:02PM +0100, Seyi Kuforiji wrote:
Show 41 quoted lines
> 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 change migrates the helper
> functions back into `lib-reftable.{c,h}`.
> 
> Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
> ---
>  Makefile                    |  4 +-
>  t/meson.build               |  4 +-
>  t/unit-tests/lib-reftable.c | 26 +++++------
>  t/unit-tests/lib-reftable.h |  6 +--
>  t/unit-tests/unit-test.c    | 93 -------------------------------------
>  t/unit-tests/unit-test.h    | 16 -------
>  6 files changed, 20 insertions(+), 129 deletions(-)
> 
> diff --git a/Makefile b/Makefile
> index 0b42893611..7e646e16ee 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1379,12 +1379,12 @@ 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_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
>  

`UNIT_TEST_PROGS` only contains the "unit-test" binary now, right? Does this mean that we can simplify build rules in our Makefile now?

Show 23 quoted lines
> diff --git a/t/meson.build b/t/meson.build
> index 8fa00fc9ef..7c305a90b5 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -26,8 +26,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],

Can't we remove this completely? `unit_test_programs` is empty now, so this loop is a no-op now.

Show 20 quoted lines
> 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,

It is quite weird that we declare the replacement functions in "unit-test.h" in the first commit only to remove them at a later point. It would make way more sense if we introduced the functions in "t/unit/lib-reftable.{c,h}" right from the start and then only remove the unused functions in the last step.

Patrick
Seyi Chamber· May 5, 2025, 07:27 UTC · re: Patrick Steinhardt · lore

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

Hi Patrick, On Fri, 2 May 2025 at 10:58, Patrick Steinhardt <ps@pks.im> wrote:

Show 102 quoted lines
>
> On Tue, Apr 29, 2025 at 06:53:02PM +0100, Seyi Kuforiji wrote:
> > 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 change migrates the helper
> > functions back into `lib-reftable.{c,h}`.
> >
> > Signed-off-by: Seyi Kuforiji <kuforiji98@gmail.com>
> > ---
> >  Makefile                    |  4 +-
> >  t/meson.build               |  4 +-
> >  t/unit-tests/lib-reftable.c | 26 +++++------
> >  t/unit-tests/lib-reftable.h |  6 +--
> >  t/unit-tests/unit-test.c    | 93 -------------------------------------
> >  t/unit-tests/unit-test.h    | 16 -------
> >  6 files changed, 20 insertions(+), 129 deletions(-)
> >
> > diff --git a/Makefile b/Makefile
> > index 0b42893611..7e646e16ee 100644
> > --- a/Makefile
> > +++ b/Makefile
> > @@ -1379,12 +1379,12 @@ 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_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
> >
>
> `UNIT_TEST_PROGS` only contains the "unit-test" binary now, right? Does
> this mean that we can simplify build rules in our Makefile now?
>
> > diff --git a/t/meson.build b/t/meson.build
> > index 8fa00fc9ef..7c305a90b5 100644
> > --- a/t/meson.build
> > +++ b/t/meson.build
> > @@ -26,8 +26,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],
>
> Can't we remove this completely? `unit_test_programs` is empty now, so
> this loop is a no-op now.
>
> > 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,
>
> It is quite weird that we declare the replacement functions in
> "unit-test.h" in the first commit only to remove them at a later point.
> It would make way more sense if we introduced the functions in
> "t/unit/lib-reftable.{c,h}" right from the start and then only remove
> the unused functions in the last step.
>
> Patrick

If I get it correctly, you're suggesting I have both the original functions and the clar-based variant in `t/unit/lib-reftable.{c,h}` right from the first commit, right?

Thanks Seyi

Patrick Steinhardt· May 5, 2025, 09:52 UTC · re: Seyi Chamber · lore

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

On Mon, May 05, 2025 at 08:27:16AM +0100, Seyi Chamber wrote:
Show 33 quoted lines
> On Fri, 2 May 2025 at 10:58, Patrick Steinhardt <ps@pks.im> wrote:
> > On Tue, Apr 29, 2025 at 06:53:02PM +0100, Seyi Kuforiji wrote:
> > > 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,
> >
> > It is quite weird that we declare the replacement functions in
> > "unit-test.h" in the first commit only to remove them at a later point.
> > It would make way more sense if we introduced the functions in
> > "t/unit/lib-reftable.{c,h}" right from the start and then only remove
> > the unused functions in the last step.
> >
> > Patrick
> 
> If I get it correctly, you're suggesting I have both the original
> functions and the clar-based variant in `t/unit/lib-reftable.{c,h}`
Yup, exactly.
Patrick
Seyi Chamber· May 26, 2025, 09:04 UTC · re: Patrick Steinhardt · lore

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

On Mon, 5 May 2025 at 10:52, Patrick Steinhardt <ps@pks.im> wrote:
Show 39 quoted lines
>
> On Mon, May 05, 2025 at 08:27:16AM +0100, Seyi Chamber wrote:
> > On Fri, 2 May 2025 at 10:58, Patrick Steinhardt <ps@pks.im> wrote:
> > > On Tue, Apr 29, 2025 at 06:53:02PM +0100, Seyi Kuforiji wrote:
> > > > 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,
> > >
> > > It is quite weird that we declare the replacement functions in
> > > "unit-test.h" in the first commit only to remove them at a later point.
> > > It would make way more sense if we introduced the functions in
> > > "t/unit/lib-reftable.{c,h}" right from the start and then only remove
> > > the unused functions in the last step.
> > >
> > > Patrick
> >
> > If I get it correctly, you're suggesting I have both the original
> > functions and the clar-based variant in `t/unit/lib-reftable.{c,h}`
>
> Yup, exactly.
>
> Patrick
Hi Patrick,

Thanks for the suggestion to move both the original t-helpers and the Clar-based cl_ versions into `t/unit/lib-reftable.{c,h}`.

I’ve tried doing that but ran into some build issues. The Clar-based functions use clar_assert and clar_assert_equal, which aren’t available to non-Clar tests. Since both sets of helpers would live in the same object file, this causes linker errors for binaries that don’t link against Clar.

I also hit Makefile warnings like “target 'lib-reftable.o' given more than once,” due to the object being included in both Clar and non-Clar test builds. And with all declarations in one header, non-Clar tests see prototypes for Clar-only functions they can't link.

Also facing errors like these: `LINK t/unit-tests/bin/t-reftable-basics /usr/bin/ld: t/unit-tests/lib-reftable.o: in function `cl_reftable_strbuf_writer': /home/seyik/git/t/unit-tests/lib-reftable.c:113:(.text+0x520): undefined reference to `clar__assert'`

What would you recommend I do to fix this?

Thanks, Seyi

Patrick Steinhardt· May 26, 2025, 12:56 UTC · re: Seyi Chamber · lore

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

On Mon, May 26, 2025 at 10:04:43AM +0100, Seyi Chamber wrote:
Show 65 quoted lines
> On Mon, 5 May 2025 at 10:52, Patrick Steinhardt <ps@pks.im> wrote:
> >
> > On Mon, May 05, 2025 at 08:27:16AM +0100, Seyi Chamber wrote:
> > > On Fri, 2 May 2025 at 10:58, Patrick Steinhardt <ps@pks.im> wrote:
> > > > On Tue, Apr 29, 2025 at 06:53:02PM +0100, Seyi Kuforiji wrote:
> > > > > 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,
> > > >
> > > > It is quite weird that we declare the replacement functions in
> > > > "unit-test.h" in the first commit only to remove them at a later point.
> > > > It would make way more sense if we introduced the functions in
> > > > "t/unit/lib-reftable.{c,h}" right from the start and then only remove
> > > > the unused functions in the last step.
> > > >
> > > > Patrick
> > >
> > > If I get it correctly, you're suggesting I have both the original
> > > functions and the clar-based variant in `t/unit/lib-reftable.{c,h}`
> >
> > Yup, exactly.
> >
> > Patrick
> 
> Hi Patrick,
> 
> Thanks for the suggestion to move both the original t-helpers and the
> Clar-based cl_ versions into `t/unit/lib-reftable.{c,h}`.
> 
> I’ve tried doing that but ran into some build issues. The Clar-based
> functions use clar_assert and clar_assert_equal, which aren’t
> available to non-Clar tests. Since both sets of helpers would live in
> the same object file, this causes linker errors for binaries that
> don’t link against Clar.
> 
> I also hit Makefile warnings like “target 'lib-reftable.o' given more
> than once,” due to the object being included in both Clar and non-Clar
> test builds. And with all declarations in one header, non-Clar tests
> see prototypes for Clar-only functions they can't link.
> 
> Also facing errors like these:
> `LINK t/unit-tests/bin/t-reftable-basics
> /usr/bin/ld: t/unit-tests/lib-reftable.o: in function
> `cl_reftable_strbuf_writer':
> /home/seyik/git/t/unit-tests/lib-reftable.c:113:(.text+0x520):
> undefined reference to `clar__assert'`
> 
> What would you recommend I do to fix this?

Hm. You could also have a separate "lib-reftable-clar.{c,h}" code unit that contains the new definitions that you introduce early in your series. At the end of the series you could then delete the old "lib-reftable.{c,h}" and rename "lib-reftable-clar.{c,h}" accordingly to have that name. It's requires a bit more shuffling, but given that the final commit would then be a plain rename it shouldn't be too bad, I guess.

Patrick

← back to recent threads