summaryrefslogtreecommitdiff
path: root/t/unit-tests
diff options
context:
space:
mode:
Diffstat (limited to 't/unit-tests')
-rw-r--r--t/unit-tests/lib-oid.c2
-rw-r--r--t/unit-tests/lib-oid.h8
-rw-r--r--t/unit-tests/t-ctype.c6
-rw-r--r--t/unit-tests/t-hash.c2
-rw-r--r--t/unit-tests/t-hashmap.c361
-rw-r--r--t/unit-tests/t-mem-pool.c2
-rw-r--r--t/unit-tests/t-oid-array.c126
-rw-r--r--t/unit-tests/t-prio-queue.c2
-rw-r--r--t/unit-tests/t-reftable-basics.c228
-rw-r--r--t/unit-tests/t-reftable-block.c371
-rw-r--r--t/unit-tests/t-reftable-merged.c454
-rw-r--r--t/unit-tests/t-reftable-pq.c152
-rw-r--r--t/unit-tests/t-reftable-readwrite.c976
-rw-r--r--t/unit-tests/t-reftable-record.c2
-rw-r--r--t/unit-tests/t-reftable-stack.c1324
-rw-r--r--t/unit-tests/t-reftable-tree.c84
-rw-r--r--t/unit-tests/t-strbuf.c2
-rw-r--r--t/unit-tests/t-strcmp-offset.c2
-rw-r--r--t/unit-tests/t-strvec.c405
-rw-r--r--t/unit-tests/t-trailer.c2
-rw-r--r--t/unit-tests/t-urlmatch-normalization.c271
-rw-r--r--t/unit-tests/test-lib.c36
-rw-r--r--t/unit-tests/test-lib.h20
23 files changed, 4470 insertions, 368 deletions
diff --git a/t/unit-tests/lib-oid.c b/t/unit-tests/lib-oid.c
index 37105f0a8f..8f0ccac532 100644
--- a/t/unit-tests/lib-oid.c
+++ b/t/unit-tests/lib-oid.c
@@ -3,7 +3,7 @@
#include "strbuf.h"
#include "hex.h"
-static int init_hash_algo(void)
+int init_hash_algo(void)
{
static int algo = -1;
diff --git a/t/unit-tests/lib-oid.h b/t/unit-tests/lib-oid.h
index 8d2acca768..4e77c04bd2 100644
--- a/t/unit-tests/lib-oid.h
+++ b/t/unit-tests/lib-oid.h
@@ -13,5 +13,13 @@
* environment variable.
*/
int get_oid_arbitrary_hex(const char *s, struct object_id *oid);
+/*
+ * Returns one of GIT_HASH_{SHA1, SHA256, UNKNOWN} based on the value of
+ * GIT_TEST_DEFAULT_HASH environment variable. The fallback value in the
+ * absence of GIT_TEST_DEFAULT_HASH is GIT_HASH_SHA1. It also uses
+ * check(algo != GIT_HASH_UNKNOWN) before returning to verify if the
+ * GIT_TEST_DEFAULT_HASH's value is valid or not.
+ */
+int init_hash_algo(void);
#endif /* LIB_OID_H */
diff --git a/t/unit-tests/t-ctype.c b/t/unit-tests/t-ctype.c
index d6ac1fe678..6043f0d9bc 100644
--- a/t/unit-tests/t-ctype.c
+++ b/t/unit-tests/t-ctype.c
@@ -4,15 +4,13 @@
size_t len = ARRAY_SIZE(string) - 1 + \
BUILD_ASSERT_OR_ZERO(ARRAY_SIZE(string) > 0) + \
BUILD_ASSERT_OR_ZERO(sizeof(string[0]) == sizeof(char)); \
- int skip = test__run_begin(); \
- if (!skip) { \
+ if_test (#class " works") { \
for (int i = 0; i < 256; i++) { \
if (!check_int(class(i), ==, !!memchr(string, i, len)))\
test_msg(" i: 0x%02x", i); \
} \
check(!class(EOF)); \
} \
- test__run_end(!skip, TEST_LOCATION(), #class " works"); \
} while (0)
#define DIGIT "0123456789"
@@ -33,7 +31,7 @@
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" \
"\x7f"
-int cmd_main(int argc, const char **argv) {
+int cmd_main(int argc UNUSED, const char **argv UNUSED) {
TEST_CHAR_CLASS(isspace, " \n\r\t");
TEST_CHAR_CLASS(isdigit, DIGIT);
TEST_CHAR_CLASS(isalpha, LOWER UPPER);
diff --git a/t/unit-tests/t-hash.c b/t/unit-tests/t-hash.c
index e9a78bf2c0..e62647019b 100644
--- a/t/unit-tests/t-hash.c
+++ b/t/unit-tests/t-hash.c
@@ -38,7 +38,7 @@ static void check_hash_data(const void *data, size_t data_length,
"SHA1 and SHA256 (%s) works", #literal); \
} while (0)
-int cmd_main(int argc, const char **argv)
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
{
struct strbuf aaaaaaaaaa_100000 = STRBUF_INIT;
struct strbuf alphabet_100000 = STRBUF_INIT;
diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c
new file mode 100644
index 0000000000..83b79dff39
--- /dev/null
+++ b/t/unit-tests/t-hashmap.c
@@ -0,0 +1,361 @@
+#include "test-lib.h"
+#include "hashmap.h"
+#include "strbuf.h"
+
+struct test_entry {
+ int padding; /* hashmap entry no longer needs to be the first member */
+ struct hashmap_entry ent;
+ /* key and value as two \0-terminated strings */
+ char key[FLEX_ARRAY];
+};
+
+static int test_entry_cmp(const void *cmp_data,
+ const struct hashmap_entry *eptr,
+ const struct hashmap_entry *entry_or_key,
+ const void *keydata)
+{
+ const unsigned int ignore_case = cmp_data ? *((int *)cmp_data) : 0;
+ const struct test_entry *e1, *e2;
+ const char *key = keydata;
+
+ e1 = container_of(eptr, const struct test_entry, ent);
+ e2 = container_of(entry_or_key, const struct test_entry, ent);
+
+ if (ignore_case)
+ return strcasecmp(e1->key, key ? key : e2->key);
+ else
+ return strcmp(e1->key, key ? key : e2->key);
+}
+
+static const char *get_value(const struct test_entry *e)
+{
+ return e->key + strlen(e->key) + 1;
+}
+
+static struct test_entry *alloc_test_entry(const char *key, const char *value,
+ unsigned int ignore_case)
+{
+ size_t klen = strlen(key);
+ size_t vlen = strlen(value);
+ unsigned int hash = ignore_case ? strihash(key) : strhash(key);
+ struct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));
+
+ hashmap_entry_init(&entry->ent, hash);
+ memcpy(entry->key, key, klen + 1);
+ memcpy(entry->key + klen + 1, value, vlen + 1);
+ return entry;
+}
+
+static struct test_entry *get_test_entry(struct hashmap *map, const char *key,
+ unsigned int ignore_case)
+{
+ return hashmap_get_entry_from_hash(
+ map, ignore_case ? strihash(key) : strhash(key), key,
+ struct test_entry, ent);
+}
+
+static int key_val_contains(const char *key_val[][2], char seen[], size_t n,
+ struct test_entry *entry)
+{
+ for (size_t i = 0; i < n; i++) {
+ if (!strcmp(entry->key, key_val[i][0]) &&
+ !strcmp(get_value(entry), key_val[i][1])) {
+ if (seen[i])
+ return 2;
+ seen[i] = 1;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static void setup(void (*f)(struct hashmap *map, unsigned int ignore_case),
+ unsigned int ignore_case)
+{
+ struct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);
+
+ f(&map, ignore_case);
+ hashmap_clear_and_free(&map, struct test_entry, ent);
+}
+
+static void t_replace(struct hashmap *map, unsigned int ignore_case)
+{
+ struct test_entry *entry;
+
+ entry = alloc_test_entry("key1", "value1", ignore_case);
+ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+
+ entry = alloc_test_entry(ignore_case ? "Key1" : "key1", "value2",
+ ignore_case);
+ entry = hashmap_put_entry(map, entry, ent);
+ if (check(entry != NULL))
+ check_str(get_value(entry), "value1");
+ free(entry);
+
+ entry = alloc_test_entry("fooBarFrotz", "value3", ignore_case);
+ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+
+ entry = alloc_test_entry(ignore_case ? "FOObarFrotz" : "fooBarFrotz",
+ "value4", ignore_case);
+ entry = hashmap_put_entry(map, entry, ent);
+ if (check(entry != NULL))
+ check_str(get_value(entry), "value3");
+ free(entry);
+}
+
+static void t_get(struct hashmap *map, unsigned int ignore_case)
+{
+ struct test_entry *entry;
+ const char *key_val[][2] = { { "key1", "value1" },
+ { "key2", "value2" },
+ { "fooBarFrotz", "value3" },
+ { ignore_case ? "key4" : "foobarfrotz",
+ "value4" } };
+ const char *query[][2] = {
+ { ignore_case ? "Key1" : "key1", "value1" },
+ { ignore_case ? "keY2" : "key2", "value2" },
+ { ignore_case ? "FOObarFrotz" : "fooBarFrotz", "value3" },
+ { ignore_case ? "FOObarFrotz" : "foobarfrotz",
+ ignore_case ? "value3" : "value4" }
+ };
+
+ for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+ entry = alloc_test_entry(key_val[i][0], key_val[i][1],
+ ignore_case);
+ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(query); i++) {
+ entry = get_test_entry(map, query[i][0], ignore_case);
+ if (check(entry != NULL))
+ check_str(get_value(entry), query[i][1]);
+ else
+ test_msg("query key: %s", query[i][0]);
+ }
+
+ check_pointer_eq(get_test_entry(map, "notInMap", ignore_case), NULL);
+ check_int(map->tablesize, ==, 64);
+ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
+}
+
+static void t_add(struct hashmap *map, unsigned int ignore_case)
+{
+ struct test_entry *entry;
+ const char *key_val[][2] = {
+ { "key1", "value1" },
+ { ignore_case ? "Key1" : "key1", "value2" },
+ { "fooBarFrotz", "value3" },
+ { ignore_case ? "FOObarFrotz" : "fooBarFrotz", "value4" }
+ };
+ const char *query_keys[] = { "key1", ignore_case ? "FOObarFrotz" :
+ "fooBarFrotz" };
+ char seen[ARRAY_SIZE(key_val)] = { 0 };
+
+ for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+ entry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);
+ hashmap_add(map, &entry->ent);
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(query_keys); i++) {
+ int count = 0;
+ entry = hashmap_get_entry_from_hash(map,
+ ignore_case ? strihash(query_keys[i]) :
+ strhash(query_keys[i]),
+ query_keys[i], struct test_entry, ent);
+
+ hashmap_for_each_entry_from(map, entry, ent)
+ {
+ int ret;
+ if (!check_int((ret = key_val_contains(
+ key_val, seen,
+ ARRAY_SIZE(key_val), entry)),
+ ==, 0)) {
+ switch (ret) {
+ case 1:
+ test_msg("found entry was not given in the input\n"
+ " key: %s\n value: %s",
+ entry->key, get_value(entry));
+ break;
+ case 2:
+ test_msg("duplicate entry detected\n"
+ " key: %s\n value: %s",
+ entry->key, get_value(entry));
+ break;
+ }
+ } else {
+ count++;
+ }
+ }
+ check_int(count, ==, 2);
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
+ if (!check_int(seen[i], ==, 1))
+ test_msg("following key-val pair was not iterated over:\n"
+ " key: %s\n value: %s",
+ key_val[i][0], key_val[i][1]);
+ }
+
+ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
+ check_pointer_eq(get_test_entry(map, "notInMap", ignore_case), NULL);
+}
+
+static void t_remove(struct hashmap *map, unsigned int ignore_case)
+{
+ struct test_entry *entry, *removed;
+ const char *key_val[][2] = { { "key1", "value1" },
+ { "key2", "value2" },
+ { "fooBarFrotz", "value3" } };
+ const char *remove[][2] = { { ignore_case ? "Key1" : "key1", "value1" },
+ { ignore_case ? "keY2" : "key2", "value2" } };
+
+ for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+ entry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);
+ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(remove); i++) {
+ entry = alloc_test_entry(remove[i][0], "", ignore_case);
+ removed = hashmap_remove_entry(map, entry, ent, remove[i][0]);
+ if (check(removed != NULL))
+ check_str(get_value(removed), remove[i][1]);
+ free(entry);
+ free(removed);
+ }
+
+ entry = alloc_test_entry("notInMap", "", ignore_case);
+ check_pointer_eq(hashmap_remove_entry(map, entry, ent, "notInMap"), NULL);
+ free(entry);
+
+ check_int(map->tablesize, ==, 64);
+ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));
+}
+
+static void t_iterate(struct hashmap *map, unsigned int ignore_case)
+{
+ struct test_entry *entry;
+ struct hashmap_iter iter;
+ const char *key_val[][2] = { { "key1", "value1" },
+ { "key2", "value2" },
+ { "fooBarFrotz", "value3" } };
+ char seen[ARRAY_SIZE(key_val)] = { 0 };
+
+ for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+ entry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);
+ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+ }
+
+ hashmap_for_each_entry(map, &iter, entry, ent /* member name */)
+ {
+ int ret;
+ if (!check_int((ret = key_val_contains(key_val, seen,
+ ARRAY_SIZE(key_val),
+ entry)), ==, 0)) {
+ switch (ret) {
+ case 1:
+ test_msg("found entry was not given in the input\n"
+ " key: %s\n value: %s",
+ entry->key, get_value(entry));
+ break;
+ case 2:
+ test_msg("duplicate entry detected\n"
+ " key: %s\n value: %s",
+ entry->key, get_value(entry));
+ break;
+ }
+ }
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
+ if (!check_int(seen[i], ==, 1))
+ test_msg("following key-val pair was not iterated over:\n"
+ " key: %s\n value: %s",
+ key_val[i][0], key_val[i][1]);
+ }
+
+ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
+}
+
+static void t_alloc(struct hashmap *map, unsigned int ignore_case)
+{
+ struct test_entry *entry, *removed;
+
+ for (int i = 1; i <= 51; i++) {
+ char *key = xstrfmt("key%d", i);
+ char *value = xstrfmt("value%d", i);
+ entry = alloc_test_entry(key, value, ignore_case);
+ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+ free(key);
+ free(value);
+ }
+ check_int(map->tablesize, ==, 64);
+ check_int(hashmap_get_size(map), ==, 51);
+
+ entry = alloc_test_entry("key52", "value52", ignore_case);
+ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+ check_int(map->tablesize, ==, 256);
+ check_int(hashmap_get_size(map), ==, 52);
+
+ for (int i = 1; i <= 12; i++) {
+ char *key = xstrfmt("key%d", i);
+ char *value = xstrfmt("value%d", i);
+
+ entry = alloc_test_entry(key, "", ignore_case);
+ removed = hashmap_remove_entry(map, entry, ent, key);
+ if (check(removed != NULL))
+ check_str(value, get_value(removed));
+ free(key);
+ free(value);
+ free(entry);
+ free(removed);
+ }
+ check_int(map->tablesize, ==, 256);
+ check_int(hashmap_get_size(map), ==, 40);
+
+ entry = alloc_test_entry("key40", "", ignore_case);
+ removed = hashmap_remove_entry(map, entry, ent, "key40");
+ if (check(removed != NULL))
+ check_str("value40", get_value(removed));
+ check_int(map->tablesize, ==, 64);
+ check_int(hashmap_get_size(map), ==, 39);
+ free(entry);
+ free(removed);
+}
+
+static void t_intern(void)
+{
+ const char *values[] = { "value1", "Value1", "value2", "value2" };
+
+ for (size_t i = 0; i < ARRAY_SIZE(values); i++) {
+ const char *i1 = strintern(values[i]);
+ const char *i2 = strintern(values[i]);
+
+ if (!check(!strcmp(i1, values[i])))
+ test_msg("strintern(%s) returns %s\n", values[i], i1);
+ else if (!check(i1 != values[i]))
+ test_msg("strintern(%s) returns input pointer\n",
+ values[i]);
+ else if (!check_pointer_eq(i1, i2))
+ test_msg("address('%s') != address('%s'), so strintern('%s') != strintern('%s')",
+ i1, i2, values[i], values[i]);
+ else
+ check_str(i1, values[i]);
+ }
+}
+
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
+{
+ TEST(setup(t_replace, 0), "replace works");
+ TEST(setup(t_replace, 1), "replace (case insensitive) works");
+ TEST(setup(t_get, 0), "get works");
+ TEST(setup(t_get, 1), "get (case insensitive) works");
+ TEST(setup(t_add, 0), "add works");
+ TEST(setup(t_add, 1), "add (case insensitive) works");
+ TEST(setup(t_remove, 0), "remove works");
+ TEST(setup(t_remove, 1), "remove (case insensitive) works");
+ TEST(setup(t_iterate, 0), "iterate works");
+ TEST(setup(t_iterate, 1), "iterate (case insensitive) works");
+ TEST(setup(t_alloc, 0), "grow / shrink works");
+ TEST(t_intern(), "string interning works");
+ return test_done();
+}
diff --git a/t/unit-tests/t-mem-pool.c b/t/unit-tests/t-mem-pool.c
index a0d57df761..fe500c704b 100644
--- a/t/unit-tests/t-mem-pool.c
+++ b/t/unit-tests/t-mem-pool.c
@@ -20,7 +20,7 @@ static void t_calloc_100(struct mem_pool *pool)
check(pool->mp_block->end != NULL);
}
-int cmd_main(int argc, const char **argv)
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
{
TEST(setup_static(t_calloc_100, 1024 * 1024),
"mem_pool_calloc returns 100 zeroed bytes with big block");
diff --git a/t/unit-tests/t-oid-array.c b/t/unit-tests/t-oid-array.c
new file mode 100644
index 0000000000..45b59a2a51
--- /dev/null
+++ b/t/unit-tests/t-oid-array.c
@@ -0,0 +1,126 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "test-lib.h"
+#include "lib-oid.h"
+#include "oid-array.h"
+#include "hex.h"
+
+static int fill_array(struct oid_array *array, const char *hexes[], size_t n)
+{
+ for (size_t i = 0; i < n; i++) {
+ struct object_id oid;
+
+ if (!check_int(get_oid_arbitrary_hex(hexes[i], &oid), ==, 0))
+ return -1;
+ oid_array_append(array, &oid);
+ }
+ if (!check_uint(array->nr, ==, n))
+ return -1;
+ return 0;
+}
+
+static int add_to_oid_array(const struct object_id *oid, void *data)
+{
+ struct oid_array *array = data;
+
+ oid_array_append(array, oid);
+ return 0;
+}
+
+static void t_enumeration(const char **input_args, size_t input_sz,
+ const char **expect_args, size_t expect_sz)
+{
+ struct oid_array input = OID_ARRAY_INIT, expect = OID_ARRAY_INIT,
+ actual = OID_ARRAY_INIT;
+ size_t i;
+
+ if (fill_array(&input, input_args, input_sz))
+ return;
+ if (fill_array(&expect, expect_args, expect_sz))
+ return;
+
+ oid_array_for_each_unique(&input, add_to_oid_array, &actual);
+ if (!check_uint(actual.nr, ==, expect.nr))
+ return;
+
+ for (i = 0; i < actual.nr; i++) {
+ if (!check(oideq(&actual.oid[i], &expect.oid[i])))
+ test_msg("expected: %s\n got: %s\n index: %" PRIuMAX,
+ oid_to_hex(&expect.oid[i]), oid_to_hex(&actual.oid[i]),
+ (uintmax_t)i);
+ }
+
+ oid_array_clear(&actual);
+ oid_array_clear(&input);
+ oid_array_clear(&expect);
+}
+
+#define TEST_ENUMERATION(input, expect, desc) \
+ TEST(t_enumeration(input, ARRAY_SIZE(input), expect, ARRAY_SIZE(expect)), \
+ desc " works")
+
+static void t_lookup(const char **input_hexes, size_t n, const char *query_hex,
+ int lower_bound, int upper_bound)
+{
+ struct oid_array array = OID_ARRAY_INIT;
+ struct object_id oid_query;
+ int ret;
+
+ if (!check_int(get_oid_arbitrary_hex(query_hex, &oid_query), ==, 0))
+ return;
+ if (fill_array(&array, input_hexes, n))
+ return;
+ ret = oid_array_lookup(&array, &oid_query);
+
+ if (!check_int(ret, <=, upper_bound) ||
+ !check_int(ret, >=, lower_bound))
+ test_msg("oid query for lookup: %s", oid_to_hex(&oid_query));
+
+ oid_array_clear(&array);
+}
+
+#define TEST_LOOKUP(input_hexes, query, lower_bound, upper_bound, desc) \
+ TEST(t_lookup(input_hexes, ARRAY_SIZE(input_hexes), query, \
+ lower_bound, upper_bound), \
+ desc " works")
+
+static void setup(void)
+{
+ /* The hash algo is used by oid_array_lookup() internally */
+ int algo = init_hash_algo();
+ if (check_int(algo, !=, GIT_HASH_UNKNOWN))
+ repo_set_hash_algo(the_repository, algo);
+}
+
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
+{
+ const char *arr_input[] = { "88", "44", "aa", "55" };
+ const char *arr_input_dup[] = { "88", "44", "aa", "55",
+ "88", "44", "aa", "55",
+ "88", "44", "aa", "55" };
+ const char *res_sorted[] = { "44", "55", "88", "aa" };
+ const char *nearly_55;
+
+ if (!TEST(setup(), "setup"))
+ test_skip_all("hash algo initialization failed");
+
+ TEST_ENUMERATION(arr_input, res_sorted, "ordered enumeration");
+ TEST_ENUMERATION(arr_input_dup, res_sorted,
+ "ordered enumeration with duplicate suppression");
+
+ TEST_LOOKUP(arr_input, "55", 1, 1, "lookup");
+ TEST_LOOKUP(arr_input, "33", INT_MIN, -1, "lookup non-existent entry");
+ TEST_LOOKUP(arr_input_dup, "55", 3, 5, "lookup with duplicates");
+ TEST_LOOKUP(arr_input_dup, "66", INT_MIN, -1,
+ "lookup non-existent entry with duplicates");
+
+ nearly_55 = init_hash_algo() == GIT_HASH_SHA1 ?
+ "5500000000000000000000000000000000000001" :
+ "5500000000000000000000000000000000000000000000000000000000000001";
+ TEST_LOOKUP(((const char *[]){ "55", nearly_55 }), "55", 0, 0,
+ "lookup with almost duplicate values");
+ TEST_LOOKUP(((const char *[]){ "55", "55" }), "55", 0, 1,
+ "lookup with single duplicate value");
+
+ return test_done();
+}
diff --git a/t/unit-tests/t-prio-queue.c b/t/unit-tests/t-prio-queue.c
index 7a4e5780e1..fe6ae37935 100644
--- a/t/unit-tests/t-prio-queue.c
+++ b/t/unit-tests/t-prio-queue.c
@@ -69,7 +69,7 @@ static void test_prio_queue(int *input, size_t input_size,
#define TEST_INPUT(input, result) \
test_prio_queue(input, ARRAY_SIZE(input), result, ARRAY_SIZE(result))
-int cmd_main(int argc, const char **argv)
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
{
TEST(TEST_INPUT(((int []){ 2, 6, 3, 10, 9, 5, 7, 4, 5, 8, 1, DUMP }),
((int []){ 1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10 })),
diff --git a/t/unit-tests/t-reftable-basics.c b/t/unit-tests/t-reftable-basics.c
index 4e80bdf16d..e5556ebf52 100644
--- a/t/unit-tests/t-reftable-basics.c
+++ b/t/unit-tests/t-reftable-basics.c
@@ -20,141 +20,125 @@ static int integer_needle_lesseq(size_t i, void *_args)
return args->needle <= args->haystack[i];
}
-static void test_binsearch(void)
+int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
{
- 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,
+ 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},
};
- size_t idx;
- idx = binsearch(ARRAY_SIZE(haystack), &integer_needle_lesseq, &args);
- check_int(idx, ==, testcases[i].expected_idx);
+ 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);
+ }
}
-}
-
-static void test_names_length(void)
-{
- const char *a[] = { "a", "b", NULL };
- check_int(names_length(a), ==, 2);
-}
-static void test_names_equal(void)
-{
- const char *a[] = { "a", "b", "c", NULL };
- const char *b[] = { "a", "b", "d", NULL };
- const char *c[] = { "a", "b", NULL };
+ if_test ("names_length retuns size of a NULL-terminated string array") {
+ const char *a[] = { "a", "b", NULL };
+ check_int(names_length(a), ==, 2);
+ }
- check(names_equal(a, a));
- check(!names_equal(a, b));
- check(!names_equal(a, c));
-}
+ 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 };
-static void test_parse_names_normal(void)
-{
- char in1[] = "line\n";
- char in2[] = "a\nb\nc";
- char **out = NULL;
- parse_names(in1, strlen(in1), &out);
- check_str(out[0], "line");
- check(!out[1]);
- free_names(out);
-
- parse_names(in2, strlen(in2), &out);
- check_str(out[0], "a");
- check_str(out[1], "b");
- check_str(out[2], "c");
- check(!out[3]);
- free_names(out);
-}
+ check(names_equal(a, a));
+ check(!names_equal(a, b));
+ check(!names_equal(a, c));
+ }
-static void test_parse_names_drop_empty(void)
-{
- char in[] = "a\n\nb\n";
- char **out = NULL;
- parse_names(in, strlen(in), &out);
- 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 ("parse_names works for basic input") {
+ char in1[] = "line\n";
+ char in2[] = "a\nb\nc";
+ char **out = NULL;
+ parse_names(in1, strlen(in1), &out);
+ check_str(out[0], "line");
+ check(!out[1]);
+ free_names(out);
+
+ parse_names(in2, strlen(in2), &out);
+ check_str(out[0], "a");
+ check_str(out[1], "b");
+ check_str(out[2], "c");
+ check(!out[3]);
+ free_names(out);
+ }
-static void test_common_prefix(void)
-{
- struct strbuf a = STRBUF_INIT;
- struct strbuf b = STRBUF_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++) {
- strbuf_addstr(&a, cases[i].a);
- strbuf_addstr(&b, cases[i].b);
- check_int(common_prefix_size(&a, &b), ==, cases[i].want);
- strbuf_reset(&a);
- strbuf_reset(&b);
+ if_test ("parse_names drops empty string") {
+ char in[] = "a\n\nb\n";
+ char **out = NULL;
+ parse_names(in, strlen(in), &out);
+ check_str(out[0], "a");
+ /* simply '\n' should be dropped as empty string */
+ check_str(out[1], "b");
+ check(!out[2]);
+ free_names(out);
}
- strbuf_release(&a);
- strbuf_release(&b);
-}
-static void test_u24_roundtrip(void)
-{
- uint32_t in = 0x112233;
- uint8_t dest[3];
- uint32_t out;
- put_be24(dest, in);
- out = get_be24(dest);
- check_int(in, ==, out);
-}
+ if_test ("common_prefix_size works") {
+ struct strbuf a = STRBUF_INIT;
+ struct strbuf b = STRBUF_INIT;
+ struct {
+ const char *a, *b;
+ int want;
+ } cases[] = {
+ {"abcdef", "abc", 3},
+ { "abc", "ab", 2 },
+ { "", "abc", 0 },
+ { "abc", "abd", 2 },
+ { "abc", "pqr", 0 },
+ };
-static void test_u16_roundtrip(void)
-{
- uint32_t in = 0xfef1;
- uint8_t dest[3];
- uint32_t out;
- put_be16(dest, in);
- out = get_be16(dest);
- check_int(in, ==, out);
-}
+ for (size_t i = 0; i < ARRAY_SIZE(cases); i++) {
+ strbuf_addstr(&a, cases[i].a);
+ strbuf_addstr(&b, cases[i].b);
+ check_int(common_prefix_size(&a, &b), ==, cases[i].want);
+ strbuf_reset(&a);
+ strbuf_reset(&b);
+ }
+ strbuf_release(&a);
+ strbuf_release(&b);
+ }
-int cmd_main(int argc, const char *argv[])
-{
- TEST(test_common_prefix(), "common_prefix_size works");
- TEST(test_parse_names_normal(), "parse_names works for basic input");
- TEST(test_parse_names_drop_empty(), "parse_names drops empty string");
- TEST(test_binsearch(), "binary search with binsearch works");
- TEST(test_names_length(), "names_length retuns size of a NULL-terminated string array");
- TEST(test_names_equal(), "names_equal compares NULL-terminated string arrays");
- TEST(test_u24_roundtrip(), "put_be24 and get_be24 work");
- TEST(test_u16_roundtrip(), "put_be16 and get_be16 work");
+ if_test ("put_be24 and get_be24 work") {
+ uint32_t in = 0x112233;
+ uint8_t dest[3];
+ uint32_t out;
+ put_be24(dest, in);
+ out = 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;
+ put_be16(dest, in);
+ out = get_be16(dest);
+ check_int(in, ==, out);
+ }
return test_done();
}
diff --git a/t/unit-tests/t-reftable-block.c b/t/unit-tests/t-reftable-block.c
new file mode 100644
index 0000000000..f1a49485e2
--- /dev/null
+++ b/t/unit-tests/t-reftable-block.c
@@ -0,0 +1,371 @@
+/*
+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"
+
+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 = STRBUF_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 strbuf want = STRBUF_INIT, buf = STRBUF_INIT;
+
+ REFTABLE_CALLOC_ARRAY(block.data, block_size);
+ block.len = block_size;
+ block_source_from_strbuf(&block.source ,&buf);
+ block_writer_init(&bw, BLOCK_TYPE_REF, block.data, block_size,
+ header_off, hash_size(GIT_SHA1_FORMAT_ID));
+
+ 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, GIT_SHA1_RAWSZ);
+
+ 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, GIT_SHA1_RAWSZ);
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ 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, GIT_SHA1_RAWSZ));
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ block_reader_release(&br);
+ block_iter_close(&it);
+ reftable_record_release(&rec);
+ reftable_block_done(&br.block);
+ strbuf_release(&want);
+ strbuf_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 = STRBUF_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 strbuf want = STRBUF_INIT, buf = STRBUF_INIT;
+
+ REFTABLE_CALLOC_ARRAY(block.data, block_size);
+ block.len = block_size;
+ block_source_from_strbuf(&block.source ,&buf);
+ block_writer_init(&bw, BLOCK_TYPE_LOG, block.data, block_size,
+ header_off, hash_size(GIT_SHA1_FORMAT_ID));
+
+ 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, GIT_SHA1_RAWSZ);
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ for (i = 0; i < N; i++) {
+ block_iter_reset(&it);
+ strbuf_reset(&want);
+ strbuf_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, GIT_SHA1_RAWSZ));
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ block_reader_release(&br);
+ block_iter_close(&it);
+ reftable_record_release(&rec);
+ reftable_block_done(&br.block);
+ strbuf_release(&want);
+ strbuf_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 = STRBUF_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 strbuf want = STRBUF_INIT, buf = STRBUF_INIT;
+
+ REFTABLE_CALLOC_ARRAY(block.data, block_size);
+ block.len = block_size;
+ block_source_from_strbuf(&block.source, &buf);
+ block_writer_init(&bw, BLOCK_TYPE_OBJ, block.data, block_size,
+ header_off, hash_size(GIT_SHA1_FORMAT_ID));
+
+ 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, GIT_SHA1_RAWSZ);
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ block_reader_release(&br);
+ block_iter_close(&it);
+ reftable_record_release(&rec);
+ reftable_block_done(&br.block);
+ strbuf_release(&want);
+ strbuf_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 = STRBUF_INIT,
+ };
+ struct reftable_record rec = {
+ .type = BLOCK_TYPE_INDEX,
+ .u.idx.last_key = STRBUF_INIT,
+ };
+ size_t i = 0;
+ int ret;
+ struct block_reader br = { 0 };
+ struct block_iter it = BLOCK_ITER_INIT;
+ struct strbuf want = STRBUF_INIT, buf = STRBUF_INIT;
+
+ REFTABLE_CALLOC_ARRAY(block.data, block_size);
+ block.len = block_size;
+ block_source_from_strbuf(&block.source, &buf);
+ block_writer_init(&bw, BLOCK_TYPE_INDEX, block.data, block_size,
+ header_off, hash_size(GIT_SHA1_FORMAT_ID));
+
+ for (i = 0; i < N; i++) {
+ strbuf_init(&recs[i].u.idx.last_key, 9);
+
+ recs[i].type = BLOCK_TYPE_INDEX;
+ strbuf_addf(&recs[i].u.idx.last_key, "branch%02"PRIuMAX, (uintmax_t)i);
+ 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, GIT_SHA1_RAWSZ);
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ 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, GIT_SHA1_RAWSZ));
+
+ 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, GIT_SHA1_RAWSZ));
+ }
+
+ block_reader_release(&br);
+ block_iter_close(&it);
+ reftable_record_release(&rec);
+ reftable_block_done(&br.block);
+ strbuf_release(&want);
+ strbuf_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/t-reftable-merged.c b/t/unit-tests/t-reftable-merged.c
new file mode 100644
index 0000000000..e9d100a01e
--- /dev/null
+++ b/t/unit-tests/t-reftable-merged.c
@@ -0,0 +1,454 @@
+/*
+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/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 ssize_t strbuf_add_void(void *b, const void *data, const size_t sz)
+{
+ strbuf_add(b, data, sz);
+ return sz;
+}
+
+static int noop_flush(void *arg UNUSED)
+{
+ return 0;
+}
+
+static void write_test_table(struct strbuf *buf,
+ struct reftable_ref_record refs[], const size_t n)
+{
+ uint64_t min = 0xffffffff;
+ uint64_t max = 0;
+ size_t i;
+ int err;
+
+ struct reftable_write_options opts = {
+ .block_size = 256,
+ };
+ struct reftable_writer *w = NULL;
+ for (i = 0; i < n; i++) {
+ uint64_t ui = refs[i].update_index;
+ if (ui > max)
+ max = ui;
+ if (ui < min)
+ min = ui;
+ }
+
+ w = reftable_new_writer(&strbuf_add_void, &noop_flush, buf, &opts);
+ reftable_writer_set_limits(w, min, max);
+
+ for (i = 0; i < n; i++) {
+ uint64_t before = refs[i].update_index;
+ int n = reftable_writer_add_ref(w, &refs[i]);
+ check_int(n, ==, 0);
+ check_int(before, ==, refs[i].update_index);
+ }
+
+ err = reftable_writer_close(w);
+ check(!err);
+
+ reftable_writer_free(w);
+}
+
+static void write_test_log_table(struct strbuf *buf, struct reftable_log_record logs[],
+ const size_t n, const uint64_t update_index)
+{
+ int err;
+
+ struct reftable_write_options opts = {
+ .block_size = 256,
+ .exact_log_message = 1,
+ };
+ struct reftable_writer *w = NULL;
+ w = reftable_new_writer(&strbuf_add_void, &noop_flush, buf, &opts);
+ reftable_writer_set_limits(w, update_index, update_index);
+
+ for (size_t i = 0; i < n; i++) {
+ int err = reftable_writer_add_log(w, &logs[i]);
+ check(!err);
+ }
+
+ err = reftable_writer_close(w);
+ check(!err);
+
+ reftable_writer_free(w);
+}
+
+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 strbuf *buf, const size_t n)
+{
+ struct reftable_merged_table *mt = NULL;
+ int err;
+
+ REFTABLE_CALLOC_ARRAY(*readers, n);
+ REFTABLE_CALLOC_ARRAY(*source, n);
+
+ for (size_t i = 0; i < n; i++) {
+ write_test_table(&buf[i], refs[i], sizes[i]);
+ block_source_from_strbuf(&(*source)[i], &buf[i]);
+
+ err = reftable_reader_new(&(*readers)[i], &(*source)[i],
+ "name");
+ check(!err);
+ }
+
+ err = reftable_merged_table_new(&mt, *readers, n, GIT_SHA1_FORMAT_ID);
+ 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 strbuf bufs[3] = { STRBUF_INIT, STRBUF_INIT, STRBUF_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;
+
+ merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
+ 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, GIT_SHA1_RAWSZ));
+ 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++)
+ strbuf_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 strbuf bufs[3] = { STRBUF_INIT, STRBUF_INIT, STRBUF_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;
+
+ merged_table_init_iter(mt, &it, BLOCK_TYPE_REF);
+ err = reftable_iterator_seek_ref(&it, "a");
+ check(!err);
+ check_int(reftable_merged_table_hash_id(mt), ==, GIT_SHA1_FORMAT_ID);
+ 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;
+
+ 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],
+ GIT_SHA1_RAWSZ));
+ for (i = 0; i < len; i++)
+ reftable_ref_record_release(&out[i]);
+ reftable_free(out);
+
+ for (i = 0; i < 3; i++)
+ strbuf_release(&bufs[i]);
+ readers_destroy(readers, 3);
+ reftable_merged_table_free(mt);
+ reftable_free(bs);
+}
+
+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 strbuf *buf, const size_t n)
+{
+ struct reftable_merged_table *mt = NULL;
+ int err;
+
+ REFTABLE_CALLOC_ARRAY(*readers, n);
+ REFTABLE_CALLOC_ARRAY(*source, n);
+
+ for (size_t i = 0; i < n; i++) {
+ write_test_log_table(&buf[i], logs[i], sizes[i], i + 1);
+ block_source_from_strbuf(&(*source)[i], &buf[i]);
+
+ err = reftable_reader_new(&(*readers)[i], &(*source)[i],
+ "name");
+ check(!err);
+ }
+
+ err = reftable_merged_table_new(&mt, *readers, n, GIT_SHA1_FORMAT_ID);
+ 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 strbuf bufs[3] = { STRBUF_INIT, STRBUF_INIT, STRBUF_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;
+
+ merged_table_init_iter(mt, &it, BLOCK_TYPE_LOG);
+ err = reftable_iterator_seek_log(&it, "a");
+ check(!err);
+ check_int(reftable_merged_table_hash_id(mt), ==, GIT_SHA1_FORMAT_ID);
+ 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;
+
+ 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],
+ GIT_SHA1_RAWSZ));
+
+ merged_table_init_iter(mt, &it, BLOCK_TYPE_LOG);
+ 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], GIT_SHA1_RAWSZ));
+ 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++)
+ strbuf_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 strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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_strbuf(&source, &buf);
+
+ err = reftable_reader_new(&rd, &source, "filename");
+ check(!err);
+
+ hash_id = reftable_reader_hash_id(rd);
+ check_int(hash_id, ==, GIT_SHA1_FORMAT_ID);
+
+ err = reftable_merged_table_new(&merged, &rd, 1, GIT_SHA256_FORMAT_ID);
+ check_int(err, ==, REFTABLE_FORMAT_ERROR);
+ err = reftable_merged_table_new(&merged, &rd, 1, GIT_SHA1_FORMAT_ID);
+ check(!err);
+
+ reftable_reader_decref(rd);
+ reftable_merged_table_free(merged);
+ strbuf_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_single_record(), "ref ocurring in only one record can be fetched");
+
+ return test_done();
+}
diff --git a/t/unit-tests/t-reftable-pq.c b/t/unit-tests/t-reftable-pq.c
new file mode 100644
index 0000000000..ada4c19f18
--- /dev/null
+++ b/t/unit-tests/t-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 "test-lib.h"
+#include "reftable/constants.h"
+#include "reftable/pq.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)
+{
+ return !reftable_record_cmp(a->rec, b->rec) && (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++) {
+ 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 = merged_iter_pqueue_remove(&pq);
+ 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++) {
+ 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 = merged_iter_pqueue_remove(&pq);
+ 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++) {
+ 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 = merged_iter_pqueue_remove(&pq);
+
+ merged_iter_pqueue_check(&pq);
+ check(pq_entry_equal(&top, &e));
+ check(reftable_record_equal(top.rec, &recs[i], GIT_SHA1_RAWSZ));
+ 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/t-reftable-readwrite.c b/t/unit-tests/t-reftable-readwrite.c
new file mode 100644
index 0000000000..82bfaf3287
--- /dev/null
+++ b/t/unit-tests/t-reftable-readwrite.c
@@ -0,0 +1,976 @@
+/*
+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/blocksource.h"
+#include "reftable/reader.h"
+#include "reftable/reftable-error.h"
+#include "reftable/reftable-writer.h"
+
+static const int update_index = 5;
+
+static void set_test_hash(uint8_t *p, int i)
+{
+ memset(p, (uint8_t)i, hash_size(GIT_SHA1_FORMAT_ID));
+}
+
+static ssize_t strbuf_add_void(void *b, const void *data, size_t sz)
+{
+ strbuf_add(b, data, sz);
+ return sz;
+}
+
+static int noop_flush(void *arg UNUSED)
+{
+ return 0;
+}
+
+static void t_buffer(void)
+{
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_block_source source = { 0 };
+ struct reftable_block out = { 0 };
+ int n;
+ uint8_t in[] = "hello";
+ strbuf_add(&buf, in, sizeof(in));
+ block_source_from_strbuf(&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);
+ strbuf_release(&buf);
+}
+
+static void write_table(char ***names, struct strbuf *buf, int N,
+ int block_size, uint32_t hash_id)
+{
+ struct reftable_write_options opts = {
+ .block_size = block_size,
+ .hash_id = hash_id,
+ };
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, buf, &opts);
+ struct reftable_ref_record ref = { 0 };
+ int i = 0, n;
+ struct reftable_log_record log = { 0 };
+ const struct reftable_stats *stats = NULL;
+
+ REFTABLE_CALLOC_ARRAY(*names, N + 1);
+
+ reftable_writer_set_limits(w, update_index, update_index);
+ for (i = 0; i < N; i++) {
+ char name[100];
+ int n;
+
+ snprintf(name, sizeof(name), "refs/heads/branch%02d", i);
+
+ ref.refname = name;
+ ref.update_index = update_index;
+ ref.value_type = REFTABLE_REF_VAL1;
+ set_test_hash(ref.value.val1, i);
+ (*names)[i] = xstrdup(name);
+
+ n = reftable_writer_add_ref(w, &ref);
+ check_int(n, ==, 0);
+ }
+
+ for (i = 0; i < N; i++) {
+ char name[100];
+ int n;
+
+ snprintf(name, sizeof(name), "refs/heads/branch%02d", i);
+
+ log.refname = name;
+ log.update_index = update_index;
+ log.value_type = REFTABLE_LOG_UPDATE;
+ set_test_hash(log.value.update.new_hash, i);
+ log.value.update.message = (char *) "message";
+
+ n = reftable_writer_add_log(w, &log);
+ check_int(n, ==, 0);
+ }
+
+ n = reftable_writer_close(w);
+ check_int(n, ==, 0);
+
+ stats = reftable_writer_stats(w);
+ for (i = 0; i < stats->ref_stats.blocks; i++) {
+ int off = i * opts.block_size;
+ if (!off)
+ off = header_size((hash_id == GIT_SHA256_FORMAT_ID) ? 2 : 1);
+ check_char(buf->buf[off], ==, 'r');
+ }
+
+ check_int(stats->log_stats.blocks, >, 0);
+ reftable_writer_free(w);
+}
+
+static void t_log_buffer_size(void)
+{
+ struct strbuf buf = STRBUF_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 = 0xa,
+ .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 =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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 < GIT_SHA1_RAWSZ; i++) {
+ log.value.update.old_hash[i] = (uint8_t)(git_rand() % 256);
+ log.value.update.new_hash[i] = (uint8_t)(git_rand() % 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);
+ strbuf_release(&buf);
+}
+
+static void t_log_overflow(void)
+{
+ struct strbuf buf = STRBUF_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 = 0xa,
+ .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 =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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);
+ strbuf_release(&buf);
+}
+
+static void t_log_write_read(void)
+{
+ int N = 2;
+ char **names = reftable_calloc(N + 1, sizeof(*names));
+ int err;
+ struct reftable_write_options opts = {
+ .block_size = 256,
+ };
+ struct reftable_ref_record ref = { 0 };
+ int i = 0;
+ struct reftable_log_record log = { 0 };
+ int n;
+ struct reftable_iterator it = { 0 };
+ struct reftable_reader *reader;
+ struct reftable_block_source source = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &buf, &opts);
+ const struct reftable_stats *stats = 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;
+ set_test_hash(log.value.update.old_hash, i);
+ set_test_hash(log.value.update.new_hash, i + 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;
+
+ block_source_from_strbuf(&source, &buf);
+
+ err = reftable_reader_new(&reader, &source, "file.log");
+ check(!err);
+
+ reftable_reader_init_ref_iterator(reader, &it);
+
+ 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);
+
+ reftable_reader_init_log_iterator(reader, &it);
+
+ 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. */
+ strbuf_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 strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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() % 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_strbuf(&source, &buf);
+
+ err = reftable_reader_new(&reader, &source, "file.log");
+ check(!err);
+
+ reftable_reader_init_log_iterator(reader, &it);
+ err = reftable_iterator_seek_log(&it, "refname");
+ check_int(err, ==, REFTABLE_ZLIB_ERROR);
+
+ reftable_iterator_destroy(&it);
+
+ /* cleanup. */
+ reftable_reader_decref(reader);
+ strbuf_release(&buf);
+}
+
+static void t_table_read_write_sequential(void)
+{
+ char **names;
+ struct strbuf buf = STRBUF_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, GIT_SHA1_FORMAT_ID);
+
+ block_source_from_strbuf(&source, &buf);
+
+ err = reftable_reader_new(&reader, &source, "file.ref");
+ check(!err);
+
+ reftable_reader_init_ref_iterator(reader, &it);
+ 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);
+ strbuf_release(&buf);
+ free_names(names);
+}
+
+static void t_table_write_small_table(void)
+{
+ char **names;
+ struct strbuf buf = STRBUF_INIT;
+ int N = 1;
+ write_table(&names, &buf, N, 4096, GIT_SHA1_FORMAT_ID);
+ check_int(buf.len, <, 200);
+ strbuf_release(&buf);
+ free_names(names);
+}
+
+static void t_table_read_api(void)
+{
+ char **names;
+ struct strbuf buf = STRBUF_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, GIT_SHA1_FORMAT_ID);
+
+ block_source_from_strbuf(&source, &buf);
+
+ err = reftable_reader_new(&reader, &source, "file.ref");
+ check(!err);
+
+ reftable_reader_init_ref_iterator(reader, &it);
+ err = reftable_iterator_seek_ref(&it, names[0]);
+ check(!err);
+
+ err = reftable_iterator_next_log(&it, &log);
+ check_int(err, ==, REFTABLE_API_ERROR);
+
+ strbuf_release(&buf);
+ free_names(names);
+ reftable_iterator_destroy(&it);
+ reftable_reader_decref(reader);
+ strbuf_release(&buf);
+}
+
+static void t_table_read_write_seek(int index, int hash_id)
+{
+ char **names;
+ struct strbuf buf = STRBUF_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 strbuf pastLast = STRBUF_INIT;
+ struct reftable_ref_record ref = { 0 };
+
+ write_table(&names, &buf, N, 256, hash_id);
+
+ block_source_from_strbuf(&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++) {
+ reftable_reader_init_ref_iterator(reader, &it);
+ 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);
+ }
+
+ strbuf_addstr(&pastLast, names[N - 1]);
+ strbuf_addstr(&pastLast, "/");
+
+ reftable_reader_init_ref_iterator(reader, &it);
+ 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);
+ }
+
+ strbuf_release(&pastLast);
+ reftable_iterator_destroy(&it);
+
+ strbuf_release(&buf);
+ free_names(names);
+ reftable_reader_decref(reader);
+}
+
+static void t_table_read_write_seek_linear(void)
+{
+ t_table_read_write_seek(0, GIT_SHA1_FORMAT_ID);
+}
+
+static void t_table_read_write_seek_linear_sha256(void)
+{
+ t_table_read_write_seek(0, GIT_SHA256_FORMAT_ID);
+}
+
+static void t_table_read_write_seek_index(void)
+{
+ t_table_read_write_seek(1, GIT_SHA1_FORMAT_ID);
+}
+
+static void t_table_refs_for(int indexed)
+{
+ int N = 50;
+ char **want_names = reftable_calloc(N + 1, sizeof(*want_names));
+ int want_names_len = 0;
+ uint8_t want_hash[GIT_SHA1_RAWSZ];
+
+ struct reftable_write_options opts = {
+ .block_size = 256,
+ };
+ struct reftable_ref_record ref = { 0 };
+ int i = 0;
+ int n;
+ int err;
+ struct reftable_reader *reader;
+ struct reftable_block_source source = { 0 };
+
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &buf, &opts);
+
+ struct reftable_iterator it = { 0 };
+ int j;
+
+ set_test_hash(want_hash, 4);
+
+ for (i = 0; i < N; i++) {
+ uint8_t hash[GIT_SHA1_RAWSZ];
+ 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;
+ set_test_hash(ref.value.val2.value, i / 4);
+ set_test_hash(ref.value.val2.target_value, 3 + i / 4);
+
+ /* 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, GIT_SHA1_RAWSZ) ||
+ !memcmp(ref.value.val2.target_value, want_hash, GIT_SHA1_RAWSZ))
+ 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_strbuf(&source, &buf);
+
+ err = reftable_reader_new(&reader, &source, "file.ref");
+ check(!err);
+ if (!indexed)
+ reader->obj_offsets.is_present = 0;
+
+ reftable_reader_init_ref_iterator(reader, &it);
+ 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);
+
+ strbuf_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 strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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_int(buf.len, ==, header_size(1) + footer_size(1));
+
+ block_source_from_strbuf(&source, &buf);
+
+ err = reftable_reader_new(&rd, &source, "filename");
+ check(!err);
+
+ reftable_reader_init_ref_iterator(rd, &it);
+ 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);
+ strbuf_release(&buf);
+}
+
+static void t_write_object_id_min_length(void)
+{
+ struct reftable_write_options opts = {
+ .block_size = 75,
+ };
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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);
+ strbuf_release(&buf);
+}
+
+static void t_write_object_id_length(void)
+{
+ struct reftable_write_options opts = {
+ .block_size = 75,
+ };
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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);
+ strbuf_release(&buf);
+}
+
+static void t_write_empty_key(void)
+{
+ struct reftable_write_options opts = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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);
+ strbuf_release(&buf);
+}
+
+static void t_write_key_order(void)
+{
+ struct reftable_write_options opts = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_writer *w =
+ reftable_new_writer(&strbuf_add_void, &noop_flush, &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);
+ strbuf_release(&buf);
+}
+
+static void t_write_multiple_indices(void)
+{
+ struct reftable_write_options opts = {
+ .block_size = 100,
+ };
+ struct strbuf writer_buf = STRBUF_INIT, buf = STRBUF_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, i;
+
+ writer = reftable_new_writer(&strbuf_add_void, &noop_flush, &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},
+ };
+
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "refs/heads/%04d", i);
+ ref.refname = buf.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 },
+ },
+ };
+
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "refs/heads/%04d", i);
+ log.refname = buf.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_strbuf(&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.
+ */
+ reftable_reader_init_log_iterator(reader, &it);
+ err = reftable_iterator_seek_log(&it, "");
+ check(!err);
+
+ reftable_iterator_destroy(&it);
+ reftable_writer_free(writer);
+ reftable_reader_decref(reader);
+ strbuf_release(&writer_buf);
+ strbuf_release(&buf);
+}
+
+static void t_write_multi_level_index(void)
+{
+ struct reftable_write_options opts = {
+ .block_size = 100,
+ };
+ struct strbuf writer_buf = STRBUF_INIT, buf = STRBUF_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 = reftable_new_writer(&strbuf_add_void, &noop_flush, &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},
+ };
+
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "refs/heads/%03" PRIuMAX, (uintmax_t)i);
+ ref.refname = buf.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_strbuf(&source, &writer_buf);
+ err = reftable_reader_new(&reader, &source, "filename");
+ check(!err);
+
+ /*
+ * Seeking the last ref should work as expected.
+ */
+ reftable_reader_init_ref_iterator(reader, &it);
+ err = reftable_iterator_seek_ref(&it, "refs/heads/199");
+ check(!err);
+
+ reftable_iterator_destroy(&it);
+ reftable_writer_free(writer);
+ reftable_reader_decref(reader);
+ strbuf_release(&writer_buf);
+ strbuf_release(&buf);
+}
+
+static void t_corrupt_table_empty(void)
+{
+ struct strbuf buf = STRBUF_INIT;
+ struct reftable_block_source source = { 0 };
+ struct reftable_reader *reader;
+ int err;
+
+ block_source_from_strbuf(&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 strbuf buf = STRBUF_INIT;
+ struct reftable_block_source source = { 0 };
+ struct reftable_reader *reader;
+ int err;
+ strbuf_add(&buf, zeros, sizeof(zeros));
+
+ block_source_from_strbuf(&source, &buf);
+ err = reftable_reader_new(&reader, &source, "file.log");
+ check_int(err, ==, REFTABLE_FORMAT_ERROR);
+
+ strbuf_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_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/t-reftable-record.c b/t/unit-tests/t-reftable-record.c
index cb649ee419..a7f67d4d9f 100644
--- a/t/unit-tests/t-reftable-record.c
+++ b/t/unit-tests/t-reftable-record.c
@@ -532,7 +532,7 @@ static void t_reftable_index_record_roundtrip(void)
strbuf_release(&in.u.idx.last_key);
}
-int cmd_main(int argc, const char *argv[])
+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");
diff --git a/t/unit-tests/t-reftable-stack.c b/t/unit-tests/t-reftable-stack.c
new file mode 100644
index 0000000000..d62a9c1bed
--- /dev/null
+++ b/t/unit-tests/t-reftable-stack.c
@@ -0,0 +1,1324 @@
+/*
+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/merged.h"
+#include "reftable/reader.h"
+#include "reftable/reftable-error.h"
+#include "reftable/stack.h"
+#include <dirent.h>
+
+static void set_test_hash(uint8_t *p, int i)
+{
+ memset(p, (uint8_t)i, hash_size(GIT_SHA1_FORMAT_ID));
+}
+
+static void clear_dir(const char *dirname)
+{
+ struct strbuf path = STRBUF_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;
+ 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)
+{
+ struct strbuf buf = STRBUF_INIT;
+ 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,
+ };
+
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "refs/heads/branch-%04"PRIuMAX, (uintmax_t)i);
+ ref.refname = buf.buf;
+ set_test_hash(ref.value.val1, i);
+
+ err = reftable_stack_add(st, &write_test_ref, &ref);
+ check(!err);
+ }
+
+ st->opts.disable_auto_compact = disable_auto_compact;
+ strbuf_release(&buf);
+}
+
+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;
+
+ 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 strbuf scratch = STRBUF_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, GIT_SHA1_RAWSZ));
+ check_int(st->readers_len, >, 0);
+
+#ifndef GIT_WINDOWS_NATIVE
+ strbuf_addstr(&scratch, dir);
+ strbuf_addstr(&scratch, "/tables.list");
+ err = stat(scratch.buf, &stat_result);
+ check(!err);
+ check_int((stat_result.st_mode & 0777), ==, opts.default_permissions);
+
+ strbuf_reset(&scratch);
+ strbuf_addstr(&scratch, dir);
+ strbuf_addstr(&scratch, "/");
+ /* do not try at home; not an external API for reftable. */
+ strbuf_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);
+ strbuf_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);
+ 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, GIT_SHA1_RAWSZ));
+
+ reftable_ref_record_release(&dest);
+ reftable_stack_destroy(st);
+ 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);
+ 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 strbuf table_path = STRBUF_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.
+ */
+ strbuf_addf(&table_path, "%s/%s.lock", dir, st->readers[0]->name);
+ 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);
+ strbuf_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 strbuf path = STRBUF_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;
+ set_test_hash(refs[i].value.val1, i);
+
+ 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");
+ set_test_hash(logs[i].value.update.new_hash, i);
+ }
+
+ 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,
+ GIT_SHA1_RAWSZ));
+ 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,
+ GIT_SHA1_RAWSZ));
+ reftable_log_record_release(&dest);
+ }
+
+#ifndef GIT_WINDOWS_NATIVE
+ strbuf_addstr(&path, dir);
+ strbuf_addstr(&path, "/tables.list");
+ err = stat(path.buf, &stat_result);
+ check(!err);
+ check_int((stat_result.st_mode & 0777), ==, opts.default_permissions);
+
+ strbuf_reset(&path);
+ strbuf_addstr(&path, dir);
+ strbuf_addstr(&path, "/");
+ /* do not try at home; not an external API for reftable. */
+ strbuf_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]);
+ }
+ strbuf_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;
+ set_test_hash(refs[i].value.val1, i);
+
+ 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");
+ set_test_hash(logs[i].value.update.new_hash, i);
+ }
+
+ 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], GIT_SHA1_RAWSZ));
+ reftable_ref_record_release(&ref);
+ }
+ check_int(i, ==, N);
+
+ reftable_iterator_destroy(&it);
+
+ reftable_stack_init_log_iterator(st, &it);
+ 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], GIT_SHA1_RAWSZ));
+ 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;
+ set_test_hash(refs[i].value.val1, i);
+ }
+
+ logs[i].refname = xstrdup(buf);
+ /* update_index is part of the key. */
+ logs[i].update_index = 42;
+ if (i % 2 == 0) {
+ logs[i].value_type = REFTABLE_LOG_UPDATE;
+ set_test_hash(logs[i].value.update.new_hash, i);
+ 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 = GIT_SHA256_FORMAT_ID };
+ 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, GIT_SHA1_RAWSZ));
+ 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");
+ set_test_hash(logs[i].value.update.new_hash, i);
+ }
+
+ 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)
+{
+ 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 strbuf buf = STRBUF_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.
+ */
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "%s/%s.lock", dir, st->readers[2]->name);
+ 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);
+ strbuf_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;
+ struct strbuf refname = STRBUF_INIT;
+ 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",
+ };
+
+ /*
+ * 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;
+
+ strbuf_reset(&refname);
+ strbuf_addf(&refname, "branch-%04"PRIuMAX, (uintmax_t)i);
+ ref.refname = 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);
+ strbuf_release(&refname);
+ 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 strbuf buf = STRBUF_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. */
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "%s/%s.lock", dir, st->readers[1]->name);
+ 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);
+ strbuf_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;
+ 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 strbuf table_path = STRBUF_INIT, content = STRBUF_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.
+ */
+ strbuf_addf(&content, "%s\n", st->readers[0]->name);
+ strbuf_addf(&content, "%s\n", st->readers[1]->name);
+ strbuf_addstr(&content, "garbage\n");
+ strbuf_addf(&table_path, "%s.lock", st->list_file);
+ 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);
+ strbuf_release(&table_path);
+ strbuf_release(&content);
+ 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_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_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/t-reftable-tree.c b/t/unit-tests/t-reftable-tree.c
new file mode 100644
index 0000000000..700479d34b
--- /dev/null
+++ b/t/unit-tests/t-reftable-tree.c
@@ -0,0 +1,84 @@
+/*
+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/tree.h"
+
+static int t_compare(const void *a, const void *b)
+{
+ return (char *)a - (char *)b;
+}
+
+struct curry {
+ void **arr;
+ size_t len;
+};
+
+static void store(void *arg, void *key)
+{
+ struct curry *c = arg;
+ c->arr[c->len++] = key;
+}
+
+static void t_tree_search(void)
+{
+ struct tree_node *root = NULL;
+ void *values[11] = { 0 };
+ struct tree_node *nodes[11] = { 0 };
+ size_t i = 1;
+
+ /*
+ * Pseudo-randomly insert the pointers for elements between
+ * values[1] and values[10] (inclusive) in the tree.
+ */
+ do {
+ nodes[i] = tree_search(&values[i], &root, &t_compare, 1);
+ i = (i * 7) % 11;
+ } while (i != 1);
+
+ for (i = 1; i < ARRAY_SIZE(nodes); i++) {
+ check_pointer_eq(&values[i], nodes[i]->key);
+ check_pointer_eq(nodes[i], tree_search(&values[i], &root, &t_compare, 0));
+ }
+
+ check(!tree_search(values, &root, t_compare, 0));
+ tree_free(root);
+}
+
+static void t_infix_walk(void)
+{
+ struct tree_node *root = NULL;
+ void *values[11] = { 0 };
+ void *out[11] = { 0 };
+ struct curry c = {
+ .arr = (void **) &out,
+ };
+ size_t i = 1;
+ size_t count = 0;
+
+ do {
+ tree_search(&values[i], &root, t_compare, 1);
+ i = (i * 7) % 11;
+ count++;
+ } while (i != 1);
+
+ infix_walk(root, &store, &c);
+ for (i = 1; i < ARRAY_SIZE(values); i++)
+ check_pointer_eq(&values[i], out[i - 1]);
+ check(!out[i - 1]);
+ check_int(c.len, ==, count);
+ tree_free(root);
+}
+
+int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
+{
+ TEST(t_tree_search(), "tree_search works");
+ TEST(t_infix_walk(), "infix_walk works");
+
+ return test_done();
+}
diff --git a/t/unit-tests/t-strbuf.c b/t/unit-tests/t-strbuf.c
index 6027dafef7..3f4044d697 100644
--- a/t/unit-tests/t-strbuf.c
+++ b/t/unit-tests/t-strbuf.c
@@ -105,7 +105,7 @@ static void t_addstr(struct strbuf *buf, const void *data)
check_str(buf->buf + orig_len, text);
}
-int cmd_main(int argc, const char **argv)
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
{
if (!TEST(t_static_init(), "static initialization works"))
test_skip_all("STRBUF_INIT is broken");
diff --git a/t/unit-tests/t-strcmp-offset.c b/t/unit-tests/t-strcmp-offset.c
index fe4c2706b1..6880f21161 100644
--- a/t/unit-tests/t-strcmp-offset.c
+++ b/t/unit-tests/t-strcmp-offset.c
@@ -24,7 +24,7 @@ static void check_strcmp_offset(const char *string1, const char *string2,
expect_offset), \
"strcmp_offset(%s, %s) works", #string1, #string2)
-int cmd_main(int argc, const char **argv)
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
{
TEST_STRCMP_OFFSET("abc", "abc", 0, 3);
TEST_STRCMP_OFFSET("abc", "def", -1, 0);
diff --git a/t/unit-tests/t-strvec.c b/t/unit-tests/t-strvec.c
index d4615ab06d..5c844cf0c9 100644
--- a/t/unit-tests/t-strvec.c
+++ b/t/unit-tests/t-strvec.c
@@ -3,270 +3,209 @@
#include "strvec.h"
#define check_strvec(vec, ...) \
- check_strvec_loc(TEST_LOCATION(), vec, __VA_ARGS__)
-LAST_ARG_MUST_BE_NULL
-static void check_strvec_loc(const char *loc, struct strvec *vec, ...)
-{
- va_list ap;
- size_t nr = 0;
-
- va_start(ap, vec);
- while (1) {
- const char *str = va_arg(ap, const char *);
- if (!str)
- break;
-
- if (!check_uint(vec->nr, >, nr) ||
- !check_uint(vec->alloc, >, nr) ||
- !check_str(vec->v[nr], str)) {
- struct strbuf msg = STRBUF_INIT;
- strbuf_addf(&msg, "strvec index %"PRIuMAX, (uintmax_t) nr);
- test_assert(loc, msg.buf, 0);
- strbuf_release(&msg);
- va_end(ap);
- return;
- }
-
- nr++;
+ do { \
+ const char *expect[] = { __VA_ARGS__ }; \
+ if (check_uint(ARRAY_SIZE(expect), >, 0) && \
+ check_pointer_eq(expect[ARRAY_SIZE(expect) - 1], NULL) && \
+ check_uint((vec)->nr, ==, ARRAY_SIZE(expect) - 1) && \
+ check_uint((vec)->nr, <=, (vec)->alloc)) { \
+ for (size_t i = 0; i < ARRAY_SIZE(expect); i++) { \
+ if (!check_str((vec)->v[i], expect[i])) { \
+ test_msg(" i: %"PRIuMAX, \
+ (uintmax_t)i); \
+ break; \
+ } \
+ } \
+ } \
+ } while (0)
+
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
+{
+ if_test ("static initialization") {
+ struct strvec vec = STRVEC_INIT;
+ check_pointer_eq(vec.v, empty_strvec);
+ check_uint(vec.nr, ==, 0);
+ check_uint(vec.alloc, ==, 0);
}
- va_end(ap);
-
- check_uint(vec->nr, ==, nr);
- check_uint(vec->alloc, >=, nr);
- check_pointer_eq(vec->v[nr], NULL);
-}
-static void t_static_init(void)
-{
- struct strvec vec = STRVEC_INIT;
- check_pointer_eq(vec.v, empty_strvec);
- check_uint(vec.nr, ==, 0);
- check_uint(vec.alloc, ==, 0);
-}
-
-static void t_dynamic_init(void)
-{
- struct strvec vec;
- strvec_init(&vec);
- check_pointer_eq(vec.v, empty_strvec);
- check_uint(vec.nr, ==, 0);
- check_uint(vec.alloc, ==, 0);
-}
+ if_test ("dynamic initialization") {
+ struct strvec vec;
+ strvec_init(&vec);
+ check_pointer_eq(vec.v, empty_strvec);
+ check_uint(vec.nr, ==, 0);
+ check_uint(vec.alloc, ==, 0);
+ }
-static void t_clear(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_push(&vec, "foo");
- strvec_clear(&vec);
- check_pointer_eq(vec.v, empty_strvec);
- check_uint(vec.nr, ==, 0);
- check_uint(vec.alloc, ==, 0);
-}
+ if_test ("clear") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_push(&vec, "foo");
+ strvec_clear(&vec);
+ check_pointer_eq(vec.v, empty_strvec);
+ check_uint(vec.nr, ==, 0);
+ check_uint(vec.alloc, ==, 0);
+ }
-static void t_push(void)
-{
- struct strvec vec = STRVEC_INIT;
+ if_test ("push") {
+ struct strvec vec = STRVEC_INIT;
- strvec_push(&vec, "foo");
- check_strvec(&vec, "foo", NULL);
+ strvec_push(&vec, "foo");
+ check_strvec(&vec, "foo", NULL);
- strvec_push(&vec, "bar");
- check_strvec(&vec, "foo", "bar", NULL);
+ strvec_push(&vec, "bar");
+ check_strvec(&vec, "foo", "bar", NULL);
- strvec_clear(&vec);
-}
+ strvec_clear(&vec);
+ }
-static void t_pushf(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushf(&vec, "foo: %d", 1);
- check_strvec(&vec, "foo: 1", NULL);
- strvec_clear(&vec);
-}
+ if_test ("pushf") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushf(&vec, "foo: %d", 1);
+ check_strvec(&vec, "foo: 1", NULL);
+ strvec_clear(&vec);
+ }
-static void t_pushl(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- check_strvec(&vec, "foo", "bar", "baz", NULL);
- strvec_clear(&vec);
-}
+ if_test ("pushl") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ check_strvec(&vec, "foo", "bar", "baz", NULL);
+ strvec_clear(&vec);
+ }
-static void t_pushv(void)
-{
- const char *strings[] = {
- "foo", "bar", "baz", NULL,
- };
- struct strvec vec = STRVEC_INIT;
+ if_test ("pushv") {
+ const char *strings[] = {
+ "foo", "bar", "baz", NULL,
+ };
+ struct strvec vec = STRVEC_INIT;
- strvec_pushv(&vec, strings);
- check_strvec(&vec, "foo", "bar", "baz", NULL);
+ strvec_pushv(&vec, strings);
+ check_strvec(&vec, "foo", "bar", "baz", NULL);
- strvec_clear(&vec);
-}
+ strvec_clear(&vec);
+ }
-static void t_replace_at_head(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- strvec_replace(&vec, 0, "replaced");
- check_strvec(&vec, "replaced", "bar", "baz", NULL);
- strvec_clear(&vec);
-}
+ if_test ("replace at head") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ strvec_replace(&vec, 0, "replaced");
+ check_strvec(&vec, "replaced", "bar", "baz", NULL);
+ strvec_clear(&vec);
+ }
-static void t_replace_at_tail(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- strvec_replace(&vec, 2, "replaced");
- check_strvec(&vec, "foo", "bar", "replaced", NULL);
- strvec_clear(&vec);
-}
+ if_test ("replace at tail") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ strvec_replace(&vec, 2, "replaced");
+ check_strvec(&vec, "foo", "bar", "replaced", NULL);
+ strvec_clear(&vec);
+ }
-static void t_replace_in_between(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- strvec_replace(&vec, 1, "replaced");
- check_strvec(&vec, "foo", "replaced", "baz", NULL);
- strvec_clear(&vec);
-}
+ if_test ("replace in between") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ strvec_replace(&vec, 1, "replaced");
+ check_strvec(&vec, "foo", "replaced", "baz", NULL);
+ strvec_clear(&vec);
+ }
-static void t_replace_with_substring(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", NULL);
- strvec_replace(&vec, 0, vec.v[0] + 1);
- check_strvec(&vec, "oo", NULL);
- strvec_clear(&vec);
-}
+ if_test ("replace with substring") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", NULL);
+ strvec_replace(&vec, 0, vec.v[0] + 1);
+ check_strvec(&vec, "oo", NULL);
+ strvec_clear(&vec);
+ }
-static void t_remove_at_head(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- strvec_remove(&vec, 0);
- check_strvec(&vec, "bar", "baz", NULL);
- strvec_clear(&vec);
-}
+ if_test ("remove at head") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ strvec_remove(&vec, 0);
+ check_strvec(&vec, "bar", "baz", NULL);
+ strvec_clear(&vec);
+ }
-static void t_remove_at_tail(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- strvec_remove(&vec, 2);
- check_strvec(&vec, "foo", "bar", NULL);
- strvec_clear(&vec);
-}
+ if_test ("remove at tail") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ strvec_remove(&vec, 2);
+ check_strvec(&vec, "foo", "bar", NULL);
+ strvec_clear(&vec);
+ }
-static void t_remove_in_between(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- strvec_remove(&vec, 1);
- check_strvec(&vec, "foo", "baz", NULL);
- strvec_clear(&vec);
-}
+ if_test ("remove in between") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ strvec_remove(&vec, 1);
+ check_strvec(&vec, "foo", "baz", NULL);
+ strvec_clear(&vec);
+ }
-static void t_pop_empty_array(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pop(&vec);
- check_strvec(&vec, NULL);
- strvec_clear(&vec);
-}
+ if_test ("pop with empty array") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pop(&vec);
+ check_strvec(&vec, NULL);
+ strvec_clear(&vec);
+ }
-static void t_pop_non_empty_array(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_pushl(&vec, "foo", "bar", "baz", NULL);
- strvec_pop(&vec);
- check_strvec(&vec, "foo", "bar", NULL);
- strvec_clear(&vec);
-}
+ if_test ("pop with non-empty array") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_pushl(&vec, "foo", "bar", "baz", NULL);
+ strvec_pop(&vec);
+ check_strvec(&vec, "foo", "bar", NULL);
+ strvec_clear(&vec);
+ }
-static void t_split_empty_string(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_split(&vec, "");
- check_strvec(&vec, NULL);
- strvec_clear(&vec);
-}
+ if_test ("split empty string") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_split(&vec, "");
+ check_strvec(&vec, NULL);
+ strvec_clear(&vec);
+ }
-static void t_split_single_item(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_split(&vec, "foo");
- check_strvec(&vec, "foo", NULL);
- strvec_clear(&vec);
-}
+ if_test ("split single item") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_split(&vec, "foo");
+ check_strvec(&vec, "foo", NULL);
+ strvec_clear(&vec);
+ }
-static void t_split_multiple_items(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_split(&vec, "foo bar baz");
- check_strvec(&vec, "foo", "bar", "baz", NULL);
- strvec_clear(&vec);
-}
+ if_test ("split multiple items") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_split(&vec, "foo bar baz");
+ check_strvec(&vec, "foo", "bar", "baz", NULL);
+ strvec_clear(&vec);
+ }
-static void t_split_whitespace_only(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_split(&vec, " \t\n");
- check_strvec(&vec, NULL);
- strvec_clear(&vec);
-}
+ if_test ("split whitespace only") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_split(&vec, " \t\n");
+ check_strvec(&vec, NULL);
+ strvec_clear(&vec);
+ }
-static void t_split_multiple_consecutive_whitespaces(void)
-{
- struct strvec vec = STRVEC_INIT;
- strvec_split(&vec, "foo\n\t bar");
- check_strvec(&vec, "foo", "bar", NULL);
- strvec_clear(&vec);
-}
+ if_test ("split multiple consecutive whitespaces") {
+ struct strvec vec = STRVEC_INIT;
+ strvec_split(&vec, "foo\n\t bar");
+ check_strvec(&vec, "foo", "bar", NULL);
+ strvec_clear(&vec);
+ }
-static void t_detach(void)
-{
- struct strvec vec = STRVEC_INIT;
- const char **detached;
+ if_test ("detach") {
+ struct strvec vec = STRVEC_INIT;
+ const char **detached;
- strvec_push(&vec, "foo");
+ strvec_push(&vec, "foo");
- detached = strvec_detach(&vec);
- check_str(detached[0], "foo");
- check_pointer_eq(detached[1], NULL);
+ detached = strvec_detach(&vec);
+ check_str(detached[0], "foo");
+ check_pointer_eq(detached[1], NULL);
- check_pointer_eq(vec.v, empty_strvec);
- check_uint(vec.nr, ==, 0);
- check_uint(vec.alloc, ==, 0);
+ check_pointer_eq(vec.v, empty_strvec);
+ check_uint(vec.nr, ==, 0);
+ check_uint(vec.alloc, ==, 0);
- free((char *) detached[0]);
- free(detached);
-}
+ free((char *) detached[0]);
+ free(detached);
+ }
-int cmd_main(int argc, const char **argv)
-{
- TEST(t_static_init(), "static initialization");
- TEST(t_dynamic_init(), "dynamic initialization");
- TEST(t_clear(), "clear");
- TEST(t_push(), "push");
- TEST(t_pushf(), "pushf");
- TEST(t_pushl(), "pushl");
- TEST(t_pushv(), "pushv");
- TEST(t_replace_at_head(), "replace at head");
- TEST(t_replace_in_between(), "replace in between");
- TEST(t_replace_at_tail(), "replace at tail");
- TEST(t_replace_with_substring(), "replace with substring");
- TEST(t_remove_at_head(), "remove at head");
- TEST(t_remove_in_between(), "remove in between");
- TEST(t_remove_at_tail(), "remove at tail");
- TEST(t_pop_empty_array(), "pop with empty array");
- TEST(t_pop_non_empty_array(), "pop with non-empty array");
- TEST(t_split_empty_string(), "split empty string");
- TEST(t_split_single_item(), "split single item");
- TEST(t_split_multiple_items(), "split multiple items");
- TEST(t_split_whitespace_only(), "split whitespace only");
- TEST(t_split_multiple_consecutive_whitespaces(), "split multiple consecutive whitespaces");
- TEST(t_detach(), "detach");
return test_done();
}
diff --git a/t/unit-tests/t-trailer.c b/t/unit-tests/t-trailer.c
index 2ecca359d9..e1c6ad7461 100644
--- a/t/unit-tests/t-trailer.c
+++ b/t/unit-tests/t-trailer.c
@@ -308,7 +308,7 @@ static void run_t_trailer_iterator(void)
}
}
-int cmd_main(int argc, const char **argv)
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
{
run_t_trailer_iterator();
return test_done();
diff --git a/t/unit-tests/t-urlmatch-normalization.c b/t/unit-tests/t-urlmatch-normalization.c
new file mode 100644
index 0000000000..1769c357b9
--- /dev/null
+++ b/t/unit-tests/t-urlmatch-normalization.c
@@ -0,0 +1,271 @@
+#include "test-lib.h"
+#include "urlmatch.h"
+
+static void check_url_normalizable(const char *url, unsigned int normalizable)
+{
+ char *url_norm = url_normalize(url, NULL);
+
+ if (!check_int(normalizable, ==, url_norm ? 1 : 0))
+ test_msg("input url: %s", url);
+ free(url_norm);
+}
+
+static void check_normalized_url(const char *url, const char *expect)
+{
+ char *url_norm = url_normalize(url, NULL);
+
+ if (!check_str(url_norm, expect))
+ test_msg("input url: %s", url);
+ free(url_norm);
+}
+
+static void compare_normalized_urls(const char *url1, const char *url2,
+ unsigned int equal)
+{
+ char *url1_norm = url_normalize(url1, NULL);
+ char *url2_norm = url_normalize(url2, NULL);
+
+ if (equal) {
+ if (!check_str(url1_norm, url2_norm))
+ test_msg("input url1: %s\n input url2: %s", url1,
+ url2);
+ } else if (!check_int(strcmp(url1_norm, url2_norm), !=, 0)) {
+ test_msg(" normalized url1: %s\n normalized url2: %s\n"
+ " input url1: %s\n input url2: %s",
+ url1_norm, url2_norm, url1, url2);
+ }
+ free(url1_norm);
+ free(url2_norm);
+}
+
+static void check_normalized_url_length(const char *url, size_t len)
+{
+ struct url_info info;
+ char *url_norm = url_normalize(url, &info);
+
+ if (!check_int(info.url_len, ==, len))
+ test_msg(" input url: %s\n normalized url: %s", url,
+ url_norm);
+ free(url_norm);
+}
+
+/* Note that only "file:" URLs should be allowed without a host */
+static void t_url_scheme(void)
+{
+ check_url_normalizable("", 0);
+ check_url_normalizable("_", 0);
+ check_url_normalizable("scheme", 0);
+ check_url_normalizable("scheme:", 0);
+ check_url_normalizable("scheme:/", 0);
+ check_url_normalizable("scheme://", 0);
+ check_url_normalizable("file", 0);
+ check_url_normalizable("file:", 0);
+ check_url_normalizable("file:/", 0);
+ check_url_normalizable("file://", 1);
+ check_url_normalizable("://acme.co", 0);
+ check_url_normalizable("x_test://acme.co", 0);
+ check_url_normalizable("-test://acme.co", 0);
+ check_url_normalizable("0test://acme.co", 0);
+ check_url_normalizable("+test://acme.co", 0);
+ check_url_normalizable(".test://acme.co", 0);
+ check_url_normalizable("schem%6e://", 0);
+ check_url_normalizable("x-Test+v1.0://acme.co", 1);
+ check_normalized_url("AbCdeF://x.Y", "abcdef://x.y/");
+}
+
+static void t_url_authority(void)
+{
+ check_url_normalizable("scheme://user:pass@", 0);
+ check_url_normalizable("scheme://?", 0);
+ check_url_normalizable("scheme://#", 0);
+ check_url_normalizable("scheme:///", 0);
+ check_url_normalizable("scheme://:", 0);
+ check_url_normalizable("scheme://:555", 0);
+ check_url_normalizable("file://user:pass@", 1);
+ check_url_normalizable("file://?", 1);
+ check_url_normalizable("file://#", 1);
+ check_url_normalizable("file:///", 1);
+ check_url_normalizable("file://:", 1);
+ check_url_normalizable("file://:555", 0);
+ check_url_normalizable("scheme://user:pass@host", 1);
+ check_url_normalizable("scheme://@host", 1);
+ check_url_normalizable("scheme://%00@host", 1);
+ check_url_normalizable("scheme://%%@host", 0);
+ check_url_normalizable("scheme://host_", 1);
+ check_url_normalizable("scheme://user:pass@host/", 1);
+ check_url_normalizable("scheme://@host/", 1);
+ check_url_normalizable("scheme://host/", 1);
+ check_url_normalizable("scheme://host?x", 1);
+ check_url_normalizable("scheme://host#x", 1);
+ check_url_normalizable("scheme://host/@", 1);
+ check_url_normalizable("scheme://host?@x", 1);
+ check_url_normalizable("scheme://host#@x", 1);
+ check_url_normalizable("scheme://[::1]", 1);
+ check_url_normalizable("scheme://[::1]/", 1);
+ check_url_normalizable("scheme://hos%41/", 0);
+ check_url_normalizable("scheme://[invalid....:/", 1);
+ check_url_normalizable("scheme://invalid....:]/", 1);
+ check_url_normalizable("scheme://invalid....:[/", 0);
+ check_url_normalizable("scheme://invalid....:[", 0);
+}
+
+static void t_url_port(void)
+{
+ check_url_normalizable("xyz://q@some.host:", 1);
+ check_url_normalizable("xyz://q@some.host:456/", 1);
+ check_url_normalizable("xyz://q@some.host:0", 0);
+ check_url_normalizable("xyz://q@some.host:0000000", 0);
+ check_url_normalizable("xyz://q@some.host:0000001?", 1);
+ check_url_normalizable("xyz://q@some.host:065535#", 1);
+ check_url_normalizable("xyz://q@some.host:65535", 1);
+ check_url_normalizable("xyz://q@some.host:65536", 0);
+ check_url_normalizable("xyz://q@some.host:99999", 0);
+ check_url_normalizable("xyz://q@some.host:100000", 0);
+ check_url_normalizable("xyz://q@some.host:100001", 0);
+ check_url_normalizable("http://q@some.host:80", 1);
+ check_url_normalizable("https://q@some.host:443", 1);
+ check_url_normalizable("http://q@some.host:80/", 1);
+ check_url_normalizable("https://q@some.host:443?", 1);
+ check_url_normalizable("http://q@:8008", 0);
+ check_url_normalizable("http://:8080", 0);
+ check_url_normalizable("http://:", 0);
+ check_url_normalizable("xyz://q@some.host:456/", 1);
+ check_url_normalizable("xyz://[::1]:456/", 1);
+ check_url_normalizable("xyz://[::1]:/", 1);
+ check_url_normalizable("xyz://[::1]:000/", 0);
+ check_url_normalizable("xyz://[::1]:0%300/", 0);
+ check_url_normalizable("xyz://[::1]:0x80/", 0);
+ check_url_normalizable("xyz://[::1]:4294967297/", 0);
+ check_url_normalizable("xyz://[::1]:030f/", 0);
+}
+
+static void t_url_port_normalization(void)
+{
+ check_normalized_url("http://x:800", "http://x:800/");
+ check_normalized_url("http://x:0800", "http://x:800/");
+ check_normalized_url("http://x:00000800", "http://x:800/");
+ check_normalized_url("http://x:065535", "http://x:65535/");
+ check_normalized_url("http://x:1", "http://x:1/");
+ check_normalized_url("http://x:80", "http://x/");
+ check_normalized_url("http://x:080", "http://x/");
+ check_normalized_url("http://x:000000080", "http://x/");
+ check_normalized_url("https://x:443", "https://x/");
+ check_normalized_url("https://x:0443", "https://x/");
+ check_normalized_url("https://x:000000443", "https://x/");
+}
+
+static void t_url_general_escape(void)
+{
+ check_url_normalizable("http://x.y?%fg", 0);
+ check_normalized_url("X://W/%7e%41^%3a", "x://w/~A%5E%3A");
+ check_normalized_url("X://W/:/?#[]@", "x://w/:/?#[]@");
+ check_normalized_url("X://W/$&()*+,;=", "x://w/$&()*+,;=");
+ check_normalized_url("X://W/'", "x://w/'");
+ check_normalized_url("X://W?!", "x://w/?!");
+}
+
+static void t_url_high_bit(void)
+{
+ check_normalized_url(
+ "x://q/\x01\x02\x03\x04\x05\x06\x07\x08\x0e\x0f\x10\x11\x12",
+ "x://q/%01%02%03%04%05%06%07%08%0E%0F%10%11%12");
+ check_normalized_url(
+ "x://q/\x13\x14\x15\x16\x17\x18\x19\x1b\x1c\x1d\x1e\x1f\x7f",
+ "x://q/%13%14%15%16%17%18%19%1B%1C%1D%1E%1F%7F");
+ check_normalized_url(
+ "x://q/\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f",
+ "x://q/%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F");
+ check_normalized_url(
+ "x://q/\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f",
+ "x://q/%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F");
+ check_normalized_url(
+ "x://q/\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf",
+ "x://q/%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF");
+ check_normalized_url(
+ "x://q/\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf",
+ "x://q/%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF");
+ check_normalized_url(
+ "x://q/\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf",
+ "x://q/%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF");
+ check_normalized_url(
+ "x://q/\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf",
+ "x://q/%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF");
+ check_normalized_url(
+ "x://q/\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef",
+ "x://q/%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF");
+ check_normalized_url(
+ "x://q/\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff",
+ "x://q/%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF");
+}
+
+static void t_url_utf8_escape(void)
+{
+ check_normalized_url(
+ "x://q/\xc2\x80\xdf\xbf\xe0\xa0\x80\xef\xbf\xbd\xf0\x90\x80\x80\xf0\xaf\xbf\xbd",
+ "x://q/%C2%80%DF%BF%E0%A0%80%EF%BF%BD%F0%90%80%80%F0%AF%BF%BD");
+}
+
+static void t_url_username_pass(void)
+{
+ check_normalized_url("x://%41%62(^):%70+d@foo", "x://Ab(%5E):p+d@foo/");
+}
+
+static void t_url_length(void)
+{
+ check_normalized_url_length("Http://%4d%65:%4d^%70@The.Host", 25);
+ check_normalized_url_length("http://%41:%42@x.y/%61/", 17);
+ check_normalized_url_length("http://@x.y/^", 15);
+}
+
+static void t_url_dots(void)
+{
+ check_normalized_url("x://y/.", "x://y/");
+ check_normalized_url("x://y/./", "x://y/");
+ check_normalized_url("x://y/a/.", "x://y/a");
+ check_normalized_url("x://y/a/./", "x://y/a/");
+ check_normalized_url("x://y/.?", "x://y/?");
+ check_normalized_url("x://y/./?", "x://y/?");
+ check_normalized_url("x://y/a/.?", "x://y/a?");
+ check_normalized_url("x://y/a/./?", "x://y/a/?");
+ check_normalized_url("x://y/a/./b/.././../c", "x://y/c");
+ check_normalized_url("x://y/a/./b/../.././c/", "x://y/c/");
+ check_normalized_url("x://y/a/./b/.././../c/././.././.", "x://y/");
+ check_url_normalizable("x://y/a/./b/.././../c/././.././..", 0);
+ check_normalized_url("x://y/a/./?/././..", "x://y/a/?/././..");
+ check_normalized_url("x://y/%2e/", "x://y/");
+ check_normalized_url("x://y/%2E/", "x://y/");
+ check_normalized_url("x://y/a/%2e./", "x://y/");
+ check_normalized_url("x://y/b/.%2E/", "x://y/");
+ check_normalized_url("x://y/c/%2e%2E/", "x://y/");
+}
+
+/*
+ * "http://@foo" specifies an empty user name but does not specify a password.
+ * "http://foo" specifies neither a user name nor a password.
+ * So they should not be equivalent.
+ */
+static void t_url_equivalents(void)
+{
+ compare_normalized_urls("httP://x", "Http://X/", 1);
+ compare_normalized_urls("Http://%4d%65:%4d^%70@The.Host", "hTTP://Me:%4D^p@the.HOST:80/", 1);
+ compare_normalized_urls("https://@x.y/^", "httpS://x.y:443/^", 0);
+ compare_normalized_urls("https://@x.y/^", "httpS://@x.y:0443/^", 1);
+ compare_normalized_urls("https://@x.y/^/../abc", "httpS://@x.y:0443/abc", 1);
+ compare_normalized_urls("https://@x.y/^/..", "httpS://@x.y:0443/", 1);
+}
+
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
+{
+ TEST(t_url_scheme(), "url scheme");
+ TEST(t_url_authority(), "url authority");
+ TEST(t_url_port(), "url port checks");
+ TEST(t_url_port_normalization(), "url port normalization");
+ TEST(t_url_general_escape(), "url general escapes");
+ TEST(t_url_high_bit(), "url high-bit escapes");
+ TEST(t_url_utf8_escape(), "url utf8 escapes");
+ TEST(t_url_username_pass(), "url username/password escapes");
+ TEST(t_url_length(), "url normalized lengths");
+ TEST(t_url_dots(), "url . and .. segments");
+ TEST(t_url_equivalents(), "url equivalents");
+ return test_done();
+}
diff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c
index 3c513ce59a..fa1f95965c 100644
--- a/t/unit-tests/test-lib.c
+++ b/t/unit-tests/test-lib.c
@@ -16,6 +16,8 @@ static struct {
unsigned running :1;
unsigned skip_all :1;
unsigned todo :1;
+ char location[100];
+ char description[100];
} ctx = {
.lazy_plan = 1,
.result = RESULT_NONE,
@@ -125,6 +127,8 @@ void test_plan(int count)
int test_done(void)
{
+ if (ctx.running && ctx.location[0] && ctx.description[0])
+ test__run_end(1, ctx.location, "%s", ctx.description);
assert(!ctx.running);
if (ctx.lazy_plan)
@@ -167,13 +171,38 @@ void test_skip_all(const char *format, ...)
va_end(ap);
}
+void test__run_describe(const char *location, const char *format, ...)
+{
+ va_list ap;
+ int len;
+
+ assert(ctx.running);
+ assert(!ctx.location[0]);
+ assert(!ctx.description[0]);
+
+ xsnprintf(ctx.location, sizeof(ctx.location), "%s",
+ make_relative(location));
+
+ va_start(ap, format);
+ len = vsnprintf(ctx.description, sizeof(ctx.description), format, ap);
+ va_end(ap);
+ if (len < 0)
+ die("unable to format message: %s", format);
+ if (len >= sizeof(ctx.description))
+ BUG("ctx.description too small to format %s", format);
+}
+
int test__run_begin(void)
{
+ if (ctx.running && ctx.location[0] && ctx.description[0])
+ test__run_end(1, ctx.location, "%s", ctx.description);
assert(!ctx.running);
ctx.count++;
ctx.result = RESULT_NONE;
ctx.running = 1;
+ ctx.location[0] = '\0';
+ ctx.description[0] = '\0';
return ctx.skip_all;
}
@@ -264,7 +293,12 @@ static void test_todo(void)
int test_assert(const char *location, const char *check, int ok)
{
- assert(ctx.running);
+ if (!ctx.running) {
+ test_msg("BUG: check outside of test at %s",
+ make_relative(location));
+ ctx.failed = 1;
+ return 0;
+ }
if (ctx.result == RESULT_SKIP) {
test_msg("skipping check '%s' at %s", check,
diff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h
index c59f646fd9..e4b234697f 100644
--- a/t/unit-tests/test-lib.h
+++ b/t/unit-tests/test-lib.h
@@ -15,6 +15,23 @@
TEST_LOCATION(), __VA_ARGS__)
/*
+ * Run a test unless test_skip_all() has been called. Acts like a
+ * conditional; the test body is expected as a statement or block after
+ * the closing parenthesis. The description for each test should be
+ * unique. E.g.:
+ *
+ * if_test ("something else %d %d", arg1, arg2) {
+ * prepare();
+ * test_something_else(arg1, arg2);
+ * cleanup();
+ * }
+ */
+#define if_test(...) \
+ if (test__run_begin() ? \
+ (test__run_end(0, TEST_LOCATION(), __VA_ARGS__), 0) : \
+ (test__run_describe(TEST_LOCATION(), __VA_ARGS__), 1))
+
+/*
* Print a test plan, should be called before any tests. If the number
* of tests is not known in advance test_done() will automatically
* print a plan at the end of the test program.
@@ -154,6 +171,9 @@ union test__tmp {
extern union test__tmp test__tmp[2];
+__attribute__((format (printf, 2, 3)))
+void test__run_describe(const char *, const char *, ...);
+
int test__run_begin(void);
__attribute__((format (printf, 3, 4)))
int test__run_end(int, const char *, const char *, ...);