{"thread":{"id":"61692","subject":"[GSoC][PATCH] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","startedAt":"2024-06-28T12:42:26Z","lastAt":"2024-08-07T16:20:31Z","messageCount":20,"participants":["Ghanshyam Thakkar","Josh Steadmon","Junio C Hamano","Phillip Wood","Christian Couder","A U Thor"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"497804","messageId":"20240628124149.43688-1-shyamthakkar001@gmail.com","threadId":"61692","inReplyTo":null,"subject":"[GSoC][PATCH] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-06-28T12:41:40Z","receivedAt":"2024-06-28T12:42:26Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\nlibrary. Migrate them to the unit testing framework for better\ndebugging, runtime performance and consice code.\n\nAlong with the migration, make 'add' tests from the shellscript order\nagnostic in unit tests, since they iterate over entries with the same\nkeys and we do not guarantee the order.\n\nThe helper/test-hashmap.c is still not removed because it contains a\nperformance test meant to be run by the user directly (not used in\nt/perf). And it makes sense for such a utility to be a helper.\n\nMentored-by: Christian Couder <chriscool@tuxfamily.org>\nMentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\nSigned-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n---\n Makefile                 |   1 +\n t/helper/test-hashmap.c  | 100 +-----------\n t/t0011-hashmap.sh       | 260 ------------------------------\n t/unit-tests/t-hashmap.c | 339 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 342 insertions(+), 358 deletions(-)\n delete mode 100755 t/t0011-hashmap.sh\n create mode 100644 t/unit-tests/t-hashmap.c\n\ndiff --git a/Makefile b/Makefile\nindex 3eab701b10..74bb026610 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n+UNIT_TEST_PROGRAMS += t-hashmap\n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-oidtree\n UNIT_TEST_PROGRAMS += t-prio-queue\ndiff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c\nindex 2912899558..7b854a7030 100644\n--- a/t/helper/test-hashmap.c\n+++ b/t/helper/test-hashmap.c\n@@ -12,11 +12,6 @@ struct test_entry\n \tchar key[FLEX_ARRAY];\n };\n \n-static const char *get_value(const struct test_entry *e)\n-{\n-\treturn e->key + strlen(e->key) + 1;\n-}\n-\n static int test_entry_cmp(const void *cmp_data,\n \t\t\t  const struct hashmap_entry *eptr,\n \t\t\t  const struct hashmap_entry *entry_or_key,\n@@ -141,30 +136,16 @@ static void perf_hashmap(unsigned int method, unsigned int rounds)\n /*\n  * Read stdin line by line and print result of commands to stdout:\n  *\n- * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)\n- * put key value -> NULL / old value\n- * get key -> NULL / value\n- * remove key -> NULL / old value\n- * iterate -> key1 value1\\nkey2 value2\\n...\n- * size -> tablesize numentries\n- *\n  * perfhashmap method rounds -> test hashmap.[ch] performance\n  */\n int cmd__hashmap(int argc, const char **argv)\n {\n \tstruct string_list parts = STRING_LIST_INIT_NODUP;\n \tstruct strbuf line = STRBUF_INIT;\n-\tint icase;\n-\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);\n-\n-\t/* init hash map */\n-\ticase = argc > 1 && !strcmp(\"ignorecase\", argv[1]);\n \n \t/* process commands from stdin */\n \twhile (strbuf_getline(&line, stdin) != EOF) {\n \t\tchar *cmd, *p1, *p2;\n-\t\tunsigned int hash = 0;\n-\t\tstruct test_entry *entry;\n \n \t\t/* break line into command and up to two parameters */\n \t\tstring_list_setlen(&parts, 0);\n@@ -180,84 +161,8 @@ int cmd__hashmap(int argc, const char **argv)\n \t\tcmd = parts.items[0].string;\n \t\tp1 = parts.nr >= 1 ? parts.items[1].string : NULL;\n \t\tp2 = parts.nr >= 2 ? parts.items[2].string : NULL;\n-\t\tif (p1)\n-\t\t\thash = icase ? strihash(p1) : strhash(p1);\n-\n-\t\tif (!strcmp(\"add\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add to hashmap */\n-\t\t\thashmap_add(&map, &entry->ent);\n-\n-\t\t} else if (!strcmp(\"put\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add / replace entry */\n-\t\t\tentry = hashmap_put_entry(&map, entry, ent);\n-\n-\t\t\t/* print and free replaced entry, if any */\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"get\", cmd) && p1) {\n-\t\t\t/* lookup entry in hashmap */\n-\t\t\tentry = hashmap_get_entry_from_hash(&map, hash, p1,\n-\t\t\t\t\t\t\tstruct test_entry, ent);\n-\n-\t\t\t/* print result */\n-\t\t\tif (!entry)\n-\t\t\t\tputs(\"NULL\");\n-\t\t\thashmap_for_each_entry_from(&map, entry, ent)\n-\t\t\t\tputs(get_value(entry));\n-\n-\t\t} else if (!strcmp(\"remove\", cmd) && p1) {\n-\n-\t\t\t/* setup static key */\n-\t\t\tstruct hashmap_entry key;\n-\t\t\tstruct hashmap_entry *rm;\n-\t\t\thashmap_entry_init(&key, hash);\n-\n-\t\t\t/* remove entry from hashmap */\n-\t\t\trm = hashmap_remove(&map, &key, p1);\n-\t\t\tentry = rm ? container_of(rm, struct test_entry, ent)\n-\t\t\t\t\t: NULL;\n-\n-\t\t\t/* print result and free entry*/\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"iterate\", cmd)) {\n-\t\t\tstruct hashmap_iter iter;\n-\n-\t\t\thashmap_for_each_entry(&map, &iter, entry,\n-\t\t\t\t\t\tent /* member name */)\n-\t\t\t\tprintf(\"%s %s\\n\", entry->key, get_value(entry));\n-\n-\t\t} else if (!strcmp(\"size\", cmd)) {\n-\n-\t\t\t/* print table sizes */\n-\t\t\tprintf(\"%u %u\\n\", map.tablesize,\n-\t\t\t       hashmap_get_size(&map));\n-\n-\t\t} else if (!strcmp(\"intern\", cmd) && p1) {\n-\n-\t\t\t/* test that strintern works */\n-\t\t\tconst char *i1 = strintern(p1);\n-\t\t\tconst char *i2 = strintern(p1);\n-\t\t\tif (strcmp(i1, p1))\n-\t\t\t\tprintf(\"strintern(%s) returns %s\\n\", p1, i1);\n-\t\t\telse if (i1 == p1)\n-\t\t\t\tprintf(\"strintern(%s) returns input pointer\\n\", p1);\n-\t\t\telse if (i1 != i2)\n-\t\t\t\tprintf(\"strintern(%s) != strintern(%s)\", i1, i2);\n-\t\t\telse\n-\t\t\t\tprintf(\"%s\\n\", i1);\n-\n-\t\t} else if (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n+\t\n+\t\tif (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n \n \t\t\tperf_hashmap(atoi(p1), atoi(p2));\n \n@@ -270,6 +175,5 @@ int cmd__hashmap(int argc, const char **argv)\n \n \tstring_list_clear(&parts, 0);\n \tstrbuf_release(&line);\n-\thashmap_clear_and_free(&map, struct test_entry, ent);\n \treturn 0;\n }\ndiff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh\ndeleted file mode 100755\nindex 46e74ad107..0000000000\n--- a/t/t0011-hashmap.sh\n+++ /dev/null\n@@ -1,260 +0,0 @@\n-#!/bin/sh\n-\n-test_description='test hashmap and string hash functions'\n-\n-TEST_PASSES_SANITIZE_LEAK=true\n-. ./test-lib.sh\n-\n-test_hashmap() {\n-\techo \"$1\" | test-tool hashmap $3 > actual &&\n-\techo \"$2\" > expect &&\n-\ttest_cmp expect actual\n-}\n-\n-test_expect_success 'put' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-NULL\n-NULL\n-NULL\n-64 4\"\n-\n-'\n-\n-test_expect_success 'put (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-size\" \"NULL\n-NULL\n-NULL\n-64 3\" ignorecase\n-\n-'\n-\n-test_expect_success 'replace' '\n-\n-test_hashmap \"put key1 value1\n-put key1 value2\n-put fooBarFrotz value3\n-put fooBarFrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\"\n-\n-'\n-\n-test_expect_success 'replace (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put Key1 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\" ignorecase\n-\n-'\n-\n-test_expect_success 'get' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-get key1\n-get key2\n-get fooBarFrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'get (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-get Key1\n-get keY2\n-get foobarfrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'add' '\n-\n-test_hashmap \"add key1 value1\n-add key1 value2\n-add fooBarFrotz value3\n-add fooBarFrotz value4\n-get key1\n-get fooBarFrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'add (case insensitive)' '\n-\n-test_hashmap \"add key1 value1\n-add Key1 value2\n-add fooBarFrotz value3\n-add foobarfrotz value4\n-get key1\n-get Foobarfrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'remove' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove key1\n-remove key2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\"\n-\n-'\n-\n-test_expect_success 'remove (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove Key1\n-remove keY2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\" ignorecase\n-\n-'\n-\n-test_expect_success 'iterate' '\n-\ttest-tool hashmap >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'iterate (case insensitive)' '\n-\ttest-tool hashmap ignorecase >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'grow / shrink' '\n-\n-\trm -f in &&\n-\trm -f expect &&\n-\tfor n in $(test_seq 51)\n-\tdo\n-\t\techo put key$n value$n >> in &&\n-\t\techo NULL >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 64 51 >> expect &&\n-\techo put key52 value52 >> in &&\n-\techo NULL >> expect &&\n-\techo size >> in &&\n-\techo 256 52 >> expect &&\n-\tfor n in $(test_seq 12)\n-\tdo\n-\t\techo remove key$n >> in &&\n-\t\techo value$n >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 256 40 >> expect &&\n-\techo remove key40 >> in &&\n-\techo value40 >> expect &&\n-\techo size >> in &&\n-\techo 64 39 >> expect &&\n-\ttest-tool hashmap <in >out &&\n-\ttest_cmp expect out\n-\n-'\n-\n-test_expect_success 'string interning' '\n-\n-test_hashmap \"intern value1\n-intern Value1\n-intern value2\n-intern value2\n-\" \"value1\n-Value1\n-value2\n-value2\"\n-\n-'\n-\n-test_done\ndiff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\nnew file mode 100644\nindex 0000000000..628aba5231\n--- /dev/null\n+++ b/t/unit-tests/t-hashmap.c\n@@ -0,0 +1,339 @@\n+#include \"test-lib.h\"\n+#include \"hashmap.h\"\n+#include \"strbuf.h\"\n+\n+struct test_entry {\n+\tint padding; /* hashmap entry no longer needs to be the first member */\n+\tstruct hashmap_entry ent;\n+\t/* key and value as two \\0-terminated strings */\n+\tchar key[FLEX_ARRAY];\n+};\n+\n+static int test_entry_cmp(const void *cmp_data,\n+\t\t\t  const struct hashmap_entry *eptr,\n+\t\t\t  const struct hashmap_entry *entry_or_key,\n+\t\t\t  const void *keydata)\n+{\n+\tconst int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n+\tconst struct test_entry *e1, *e2;\n+\tconst char *key = keydata;\n+\n+\te1 = container_of(eptr, const struct test_entry, ent);\n+\te2 = container_of(entry_or_key, const struct test_entry, ent);\n+\n+\tif (ignore_case)\n+\t\treturn strcasecmp(e1->key, key ? key : e2->key);\n+\telse\n+\t\treturn strcmp(e1->key, key ? key : e2->key);\n+}\n+\n+static const char *get_value(const struct test_entry *e)\n+{\n+\treturn e->key + strlen(e->key) + 1;\n+}\n+\n+static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n+\t\t\t\t\t   const char *key, const char *value)\n+{\n+\tsize_t klen = strlen(key);\n+\tsize_t vlen = strlen(value);\n+\tunsigned int hash = ignore_case ? strihash(key) : strhash(key);\n+\tstruct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n+\n+\thashmap_entry_init(&entry->ent, hash);\n+\tmemcpy(entry->key, key, klen + 1);\n+\tmemcpy(entry->key + klen + 1, value, vlen + 1);\n+\treturn entry;\n+}\n+\n+static struct test_entry *get_test_entry(struct hashmap *map,\n+\t\t\t\t\t unsigned int ignore_case, const char *key)\n+{\n+\treturn hashmap_get_entry_from_hash(\n+\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n+\t\tstruct test_entry, ent);\n+}\n+\n+static int key_val_contains(const char *key_val[][3], size_t n,\n+\t\t\t    struct test_entry *entry)\n+{\n+\tfor (size_t i = 0; i < n; i++) {\n+\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n+\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n+\t\t\tif (!strcmp(key_val[i][2], \"USED\"))\n+\t\t\t\treturn 2;\n+\t\t\tkey_val[i][2] = \"USED\";\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n+static void setup(void (*f)(struct hashmap *map, int ignore_case),\n+\t\t  int ignore_case)\n+{\n+\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n+\tf(&map, ignore_case);\n+\thashmap_clear_and_free(&map, struct test_entry, ent);\n+}\n+\n+static void t_put(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t   { \"key2\", \"value2\" },\n+\t\t\t\t   { \"fooBarFrotz\", \"value3\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tcheck(ignore_case ? entry != NULL : entry == NULL);\n+\tfree(entry);\n+\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, ignore_case ? 3 : 4);\n+}\n+\n+static void t_replace(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\n+\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\n+\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n+\t\t\t\t \"value2\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value1\");\n+\tfree(entry);\n+\n+\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\n+\tentry = alloc_test_entry(ignore_case,\n+\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n+\t\t\t\t \"value4\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value3\");\n+\tfree(entry);\n+}\n+\n+static void t_get(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t   { \"key2\", \"value2\" },\n+\t\t\t\t   { \"fooBarFrotz\", \"value3\" },\n+\t\t\t\t   { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n+\tconst char *query[][2] = {\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n+\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n+\t};\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n+\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n+\t\tif (check(entry != NULL))\n+\t\t\tcheck_str(get_value(entry), query[i][1]);\n+\t}\n+\n+\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n+}\n+\n+static void t_add(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][3] = {\n+\t\t{ \"key1\", \"value2\", \"UNUSED\" },\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n+\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n+\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n+\t};\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\thashmap_add(map, &entry->ent);\n+\t}\n+\n+\thashmap_for_each_entry_from(map, entry, ent)\n+\t{\n+\t\tint ret;\n+\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n+\t\t\t\t\t\t       entry)), ==, 0)) {\n+\t\t\tswitch (ret) {\n+\t\t\t\tcase 1:\n+\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 2:\n+\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n+}\n+\n+static void t_remove(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t   { \"key2\", \"value2\" },\n+\t\t\t\t   { \"fooBarFrotz\", \"value3\" } };\n+\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, remove[i][0], \"\");\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(get_value(removed), remove[i][1]);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\n+\tentry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n+\tcheck(hashmap_remove_entry(map, entry, ent, \"notInMap\") == NULL);\n+\tfree(entry);\n+}\n+\n+static void t_iterate(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tstruct hashmap_iter iter;\n+\tconst char *key_val[][3] = { { \"key1\", \"value1\", \"UNUSED\" },\n+\t\t\t\t     { \"key2\", \"value2\", \"UNUSED\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\", \"UNUSED\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n+\t{\n+\t\tint ret;\n+\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n+\t\t\t\t\t      entry)), ==, 0)) {\n+\t\t\tswitch (ret) {\n+\t\t\t\tcase 1:\n+\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 2:\n+\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\tcheck_int(hashmap_get_size(map), ==, 3);\n+}\n+\n+static void t_alloc(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\n+\tfor (int i = 1; i <= 51; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\t\tentry = alloc_test_entry(ignore_case, key, value);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t\tfree(key);\n+\t\tfree(value);\n+\t}\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 51);\n+\n+\tentry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 52);\n+\n+\tfor (int i = 1; i <= 12; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\n+\t\tentry = alloc_test_entry(ignore_case, key, \"\");\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(value, get_value(removed));\n+\t\tfree(key);\n+\t\tfree(value);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 40);\n+\n+\tentry = alloc_test_entry(ignore_case, \"key40\", \"\");\n+\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n+\tif (check(removed != NULL))\n+\t\tcheck_str(\"value40\", get_value(removed));\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 39);\n+\tfree(entry);\n+\tfree(removed);\n+}\n+\n+static void t_intern(struct hashmap *map, int ignore_case)\n+{\n+\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n+\t\tconst char *i1 = strintern(values[i]);\n+\t\tconst char *i2 = strintern(values[i]);\n+\n+\t\tif (!check(!strcmp(i1, values[i])))\n+\t\t\ttest_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n+\t\telse if (!check(i1 != values[i]))\n+\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n+\t\t\t\t values[i]);\n+\t\telse if (!check(i1 == i2))\n+\t\t\ttest_msg(\"address('%s') != address('%s'), so strintern('%s') != strintern('%s')\",\n+\t\t\t\t i1, i2, values[i], values[i]);\n+\t\telse\n+\t\t\tcheck_str(i1, values[i]);\n+\t}\n+}\n+\n+int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+{\n+\tTEST(setup(t_put, 0), \"put works\");\n+\tTEST(setup(t_put, 1), \"put (case insensitive) works\");\n+\tTEST(setup(t_replace, 0), \"replace works\");\n+\tTEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n+\tTEST(setup(t_get, 0), \"get works\");\n+\tTEST(setup(t_get, 1), \"get (case insensitive) works\");\n+\tTEST(setup(t_add, 0), \"add works\");\n+\tTEST(setup(t_add, 1), \"add (case insensitive) works\");\n+\tTEST(setup(t_remove, 0), \"remove works\");\n+\tTEST(setup(t_remove, 1), \"remove (case insensitive) works\");\n+\tTEST(setup(t_iterate, 0), \"iterate works\");\n+\tTEST(setup(t_iterate, 1), \"iterate (case insensitive) works\");\n+\tTEST(setup(t_alloc, 0), \"grow / shrink works\");\n+\tTEST(setup(t_intern, 0), \"string interning works\");\n+\treturn test_done();\n+}\n-- \n2.45.2\n\n"},{"id":"498277","messageId":"20240708161641.10335-2-shyamthakkar001@gmail.com","threadId":"61692","inReplyTo":"20240628124149.43688-1-shyamthakkar001@gmail.com","subject":"[GSoC][PATCH v2] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-07-08T16:15:47Z","receivedAt":"2024-07-08T16:17:30Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\nlibrary. Migrate them to the unit testing framework for better\ndebugging, runtime performance and consice code.\n\nAlong with the migration, make 'add' tests from the shellscript order\nagnostic in unit tests, since they iterate over entries with the same\nkeys and we do not guarantee the order.\n\nThe helper/test-hashmap.c is still not removed because it contains a\nperformance test meant to be run by the user directly (not used in\nt/perf). And it makes sense for such a utility to be a helper.\n\nMentored-by: Christian Couder <chriscool@tuxfamily.org>\nMentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\nSigned-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n---\nThe changes in v2 are inspired from the review of another similar\ntest t-oidmap: https://lore.kernel.org/git/16e06a6d-5fd0-4132-9d82-5c6f13b7f9ed@gmail.com/\n\nThe v2 also includes some formatting corrections and one of the\ntestcases, t_add(), was changed to be more similar to the original.\n\nRange-diff against v1:\n1:  f095025d1b ! 1:  bbb4f2f23e t: port helper/test-hashmap.c to unit-tests/t-hashmap.c\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\t  int ignore_case)\n     +{\n     +\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n    ++\n     +\tf(&map, ignore_case);\n     +\thashmap_clear_and_free(&map, struct test_entry, ent);\n     +}\n    @@ t/unit-tests/t-hashmap.c (new)\n     +{\n     +\tstruct test_entry *entry;\n     +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n    -+\t\t\t\t   { \"key2\", \"value2\" },\n    -+\t\t\t\t   { \"fooBarFrotz\", \"value3\" } };\n    ++\t\t\t\t     { \"key2\", \"value2\" },\n    ++\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n     +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tfree(entry);\n     +\n     +\tcheck_int(map->tablesize, ==, 64);\n    -+\tcheck_int(hashmap_get_size(map), ==, ignore_case ? 3 : 4);\n    ++\tcheck_int(hashmap_get_size(map), ==,\n    ++\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n     +}\n     +\n     +static void t_replace(struct hashmap *map, int ignore_case)\n    @@ t/unit-tests/t-hashmap.c (new)\n     +{\n     +\tstruct test_entry *entry;\n     +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n    -+\t\t\t\t   { \"key2\", \"value2\" },\n    -+\t\t\t\t   { \"fooBarFrotz\", \"value3\" },\n    -+\t\t\t\t   { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n    ++\t\t\t\t     { \"key2\", \"value2\" },\n    ++\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n    ++\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n     +\tconst char *query[][2] = {\n     +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n     +\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n    @@ t/unit-tests/t-hashmap.c (new)\n     +{\n     +\tstruct test_entry *entry;\n     +\tconst char *key_val[][3] = {\n    -+\t\t{ \"key1\", \"value2\", \"UNUSED\" },\n    ++\t\t{ \"key1\", \"value1\", \"UNUSED\" },\n     +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n     +\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n     +\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n     +\t};\n    ++\tconst char *queries[] = { \"key1\",\n    ++\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n     +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n     +\t\thashmap_add(map, &entry->ent);\n     +\t}\n     +\n    -+\thashmap_for_each_entry_from(map, entry, ent)\n    -+\t{\n    -+\t\tint ret;\n    -+\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n    -+\t\t\t\t\t\t       entry)), ==, 0)) {\n    -+\t\t\tswitch (ret) {\n    ++\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n    ++\t\tint count = 0;\n    ++\t\tentry = hashmap_get_entry_from_hash(map,\n    ++\t\t\tignore_case ? strihash(queries[i]) :\n    ++\t\t\t\t      strhash(queries[i]),\n    ++\t\t\tqueries[i], struct test_entry, ent);\n    ++\n    ++\t\thashmap_for_each_entry_from(map, entry, ent)\n    ++\t\t{\n    ++\t\t\tint ret;\n    ++\t\t\tif (!check_int((ret = key_val_contains(\n    ++\t\t\t\t\t\tkey_val, ARRAY_SIZE(key_val),\n    ++\t\t\t\t\t\tentry)), ==, 0)) {\n    ++\t\t\t\tswitch (ret) {\n     +\t\t\t\tcase 1:\n     +\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n     +\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n     +\t\t\t\t\t\t entry->key, get_value(entry));\n     +\t\t\t\t\tbreak;\n    ++\t\t\t\t}\n    ++\t\t\t} else {\n    ++\t\t\t\tcount++;\n     +\t\t\t}\n     +\t\t}\n    ++\t\tcheck_int(count, ==, 2);\n     +\t}\n    -+\n    ++\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n     +\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n     +}\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +{\n     +\tstruct test_entry *entry, *removed;\n     +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n    -+\t\t\t\t   { \"key2\", \"value2\" },\n    -+\t\t\t\t   { \"fooBarFrotz\", \"value3\" } };\n    ++\t\t\t\t     { \"key2\", \"value2\" },\n    ++\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n     +\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n     +\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tconst char *key_val[][3] = { { \"key1\", \"value1\", \"UNUSED\" },\n     +\t\t\t\t     { \"key2\", \"value2\", \"UNUSED\" },\n     +\t\t\t\t     { \"fooBarFrotz\", \"value3\", \"UNUSED\" } };\n    ++\tint count = 0;\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n     +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t{\n     +\t\tint ret;\n     +\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n    -+\t\t\t\t\t      entry)), ==, 0)) {\n    ++\t\t\t\t\t\t       entry)), ==, 0)) {\n     +\t\t\tswitch (ret) {\n    -+\t\t\t\tcase 1:\n    -+\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n    -+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n    -+\t\t\t\t\t\t entry->key, get_value(entry));\n    -+\t\t\t\t\tbreak;\n    -+\t\t\t\tcase 2:\n    -+\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n    -+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n    -+\t\t\t\t\t\t entry->key, get_value(entry));\n    -+\t\t\t\t\tbreak;\n    ++\t\t\tcase 1:\n    ++\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n    ++\t\t\t\t\t \"    key: %s\\n  value: %s\",\n    ++\t\t\t\t\t entry->key, get_value(entry));\n    ++\t\t\t\tbreak;\n    ++\t\t\tcase 2:\n    ++\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n    ++\t\t\t\t\t \"    key: %s\\n  value: %s\",\n    ++\t\t\t\t\t entry->key, get_value(entry));\n    ++\t\t\t\tbreak;\n     +\t\t\t}\n    ++\t\t} else {\n    ++\t\t\tcount++;\n     +\t\t}\n     +\t}\n    -+\tcheck_int(hashmap_get_size(map), ==, 3);\n    ++\tcheck_int(count, ==, ARRAY_SIZE(key_val));\n    ++\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n     +}\n     +\n     +static void t_alloc(struct hashmap *map, int ignore_case)\n\n Makefile                 |   1 +\n t/helper/test-hashmap.c  | 100 +----------\n t/t0011-hashmap.sh       | 260 ----------------------------\n t/unit-tests/t-hashmap.c | 359 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 362 insertions(+), 358 deletions(-)\n delete mode 100755 t/t0011-hashmap.sh\n create mode 100644 t/unit-tests/t-hashmap.c\n\ndiff --git a/Makefile b/Makefile\nindex 3eab701b10..74bb026610 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n+UNIT_TEST_PROGRAMS += t-hashmap\n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-oidtree\n UNIT_TEST_PROGRAMS += t-prio-queue\ndiff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c\nindex 2912899558..7b854a7030 100644\n--- a/t/helper/test-hashmap.c\n+++ b/t/helper/test-hashmap.c\n@@ -12,11 +12,6 @@ struct test_entry\n \tchar key[FLEX_ARRAY];\n };\n \n-static const char *get_value(const struct test_entry *e)\n-{\n-\treturn e->key + strlen(e->key) + 1;\n-}\n-\n static int test_entry_cmp(const void *cmp_data,\n \t\t\t  const struct hashmap_entry *eptr,\n \t\t\t  const struct hashmap_entry *entry_or_key,\n@@ -141,30 +136,16 @@ static void perf_hashmap(unsigned int method, unsigned int rounds)\n /*\n  * Read stdin line by line and print result of commands to stdout:\n  *\n- * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)\n- * put key value -> NULL / old value\n- * get key -> NULL / value\n- * remove key -> NULL / old value\n- * iterate -> key1 value1\\nkey2 value2\\n...\n- * size -> tablesize numentries\n- *\n  * perfhashmap method rounds -> test hashmap.[ch] performance\n  */\n int cmd__hashmap(int argc, const char **argv)\n {\n \tstruct string_list parts = STRING_LIST_INIT_NODUP;\n \tstruct strbuf line = STRBUF_INIT;\n-\tint icase;\n-\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);\n-\n-\t/* init hash map */\n-\ticase = argc > 1 && !strcmp(\"ignorecase\", argv[1]);\n \n \t/* process commands from stdin */\n \twhile (strbuf_getline(&line, stdin) != EOF) {\n \t\tchar *cmd, *p1, *p2;\n-\t\tunsigned int hash = 0;\n-\t\tstruct test_entry *entry;\n \n \t\t/* break line into command and up to two parameters */\n \t\tstring_list_setlen(&parts, 0);\n@@ -180,84 +161,8 @@ int cmd__hashmap(int argc, const char **argv)\n \t\tcmd = parts.items[0].string;\n \t\tp1 = parts.nr >= 1 ? parts.items[1].string : NULL;\n \t\tp2 = parts.nr >= 2 ? parts.items[2].string : NULL;\n-\t\tif (p1)\n-\t\t\thash = icase ? strihash(p1) : strhash(p1);\n-\n-\t\tif (!strcmp(\"add\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add to hashmap */\n-\t\t\thashmap_add(&map, &entry->ent);\n-\n-\t\t} else if (!strcmp(\"put\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add / replace entry */\n-\t\t\tentry = hashmap_put_entry(&map, entry, ent);\n-\n-\t\t\t/* print and free replaced entry, if any */\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"get\", cmd) && p1) {\n-\t\t\t/* lookup entry in hashmap */\n-\t\t\tentry = hashmap_get_entry_from_hash(&map, hash, p1,\n-\t\t\t\t\t\t\tstruct test_entry, ent);\n-\n-\t\t\t/* print result */\n-\t\t\tif (!entry)\n-\t\t\t\tputs(\"NULL\");\n-\t\t\thashmap_for_each_entry_from(&map, entry, ent)\n-\t\t\t\tputs(get_value(entry));\n-\n-\t\t} else if (!strcmp(\"remove\", cmd) && p1) {\n-\n-\t\t\t/* setup static key */\n-\t\t\tstruct hashmap_entry key;\n-\t\t\tstruct hashmap_entry *rm;\n-\t\t\thashmap_entry_init(&key, hash);\n-\n-\t\t\t/* remove entry from hashmap */\n-\t\t\trm = hashmap_remove(&map, &key, p1);\n-\t\t\tentry = rm ? container_of(rm, struct test_entry, ent)\n-\t\t\t\t\t: NULL;\n-\n-\t\t\t/* print result and free entry*/\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"iterate\", cmd)) {\n-\t\t\tstruct hashmap_iter iter;\n-\n-\t\t\thashmap_for_each_entry(&map, &iter, entry,\n-\t\t\t\t\t\tent /* member name */)\n-\t\t\t\tprintf(\"%s %s\\n\", entry->key, get_value(entry));\n-\n-\t\t} else if (!strcmp(\"size\", cmd)) {\n-\n-\t\t\t/* print table sizes */\n-\t\t\tprintf(\"%u %u\\n\", map.tablesize,\n-\t\t\t       hashmap_get_size(&map));\n-\n-\t\t} else if (!strcmp(\"intern\", cmd) && p1) {\n-\n-\t\t\t/* test that strintern works */\n-\t\t\tconst char *i1 = strintern(p1);\n-\t\t\tconst char *i2 = strintern(p1);\n-\t\t\tif (strcmp(i1, p1))\n-\t\t\t\tprintf(\"strintern(%s) returns %s\\n\", p1, i1);\n-\t\t\telse if (i1 == p1)\n-\t\t\t\tprintf(\"strintern(%s) returns input pointer\\n\", p1);\n-\t\t\telse if (i1 != i2)\n-\t\t\t\tprintf(\"strintern(%s) != strintern(%s)\", i1, i2);\n-\t\t\telse\n-\t\t\t\tprintf(\"%s\\n\", i1);\n-\n-\t\t} else if (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n+\t\n+\t\tif (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n \n \t\t\tperf_hashmap(atoi(p1), atoi(p2));\n \n@@ -270,6 +175,5 @@ int cmd__hashmap(int argc, const char **argv)\n \n \tstring_list_clear(&parts, 0);\n \tstrbuf_release(&line);\n-\thashmap_clear_and_free(&map, struct test_entry, ent);\n \treturn 0;\n }\ndiff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh\ndeleted file mode 100755\nindex 46e74ad107..0000000000\n--- a/t/t0011-hashmap.sh\n+++ /dev/null\n@@ -1,260 +0,0 @@\n-#!/bin/sh\n-\n-test_description='test hashmap and string hash functions'\n-\n-TEST_PASSES_SANITIZE_LEAK=true\n-. ./test-lib.sh\n-\n-test_hashmap() {\n-\techo \"$1\" | test-tool hashmap $3 > actual &&\n-\techo \"$2\" > expect &&\n-\ttest_cmp expect actual\n-}\n-\n-test_expect_success 'put' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-NULL\n-NULL\n-NULL\n-64 4\"\n-\n-'\n-\n-test_expect_success 'put (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-size\" \"NULL\n-NULL\n-NULL\n-64 3\" ignorecase\n-\n-'\n-\n-test_expect_success 'replace' '\n-\n-test_hashmap \"put key1 value1\n-put key1 value2\n-put fooBarFrotz value3\n-put fooBarFrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\"\n-\n-'\n-\n-test_expect_success 'replace (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put Key1 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\" ignorecase\n-\n-'\n-\n-test_expect_success 'get' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-get key1\n-get key2\n-get fooBarFrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'get (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-get Key1\n-get keY2\n-get foobarfrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'add' '\n-\n-test_hashmap \"add key1 value1\n-add key1 value2\n-add fooBarFrotz value3\n-add fooBarFrotz value4\n-get key1\n-get fooBarFrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'add (case insensitive)' '\n-\n-test_hashmap \"add key1 value1\n-add Key1 value2\n-add fooBarFrotz value3\n-add foobarfrotz value4\n-get key1\n-get Foobarfrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'remove' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove key1\n-remove key2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\"\n-\n-'\n-\n-test_expect_success 'remove (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove Key1\n-remove keY2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\" ignorecase\n-\n-'\n-\n-test_expect_success 'iterate' '\n-\ttest-tool hashmap >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'iterate (case insensitive)' '\n-\ttest-tool hashmap ignorecase >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'grow / shrink' '\n-\n-\trm -f in &&\n-\trm -f expect &&\n-\tfor n in $(test_seq 51)\n-\tdo\n-\t\techo put key$n value$n >> in &&\n-\t\techo NULL >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 64 51 >> expect &&\n-\techo put key52 value52 >> in &&\n-\techo NULL >> expect &&\n-\techo size >> in &&\n-\techo 256 52 >> expect &&\n-\tfor n in $(test_seq 12)\n-\tdo\n-\t\techo remove key$n >> in &&\n-\t\techo value$n >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 256 40 >> expect &&\n-\techo remove key40 >> in &&\n-\techo value40 >> expect &&\n-\techo size >> in &&\n-\techo 64 39 >> expect &&\n-\ttest-tool hashmap <in >out &&\n-\ttest_cmp expect out\n-\n-'\n-\n-test_expect_success 'string interning' '\n-\n-test_hashmap \"intern value1\n-intern Value1\n-intern value2\n-intern value2\n-\" \"value1\n-Value1\n-value2\n-value2\"\n-\n-'\n-\n-test_done\ndiff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\nnew file mode 100644\nindex 0000000000..1c951fcfd8\n--- /dev/null\n+++ b/t/unit-tests/t-hashmap.c\n@@ -0,0 +1,359 @@\n+#include \"test-lib.h\"\n+#include \"hashmap.h\"\n+#include \"strbuf.h\"\n+\n+struct test_entry {\n+\tint padding; /* hashmap entry no longer needs to be the first member */\n+\tstruct hashmap_entry ent;\n+\t/* key and value as two \\0-terminated strings */\n+\tchar key[FLEX_ARRAY];\n+};\n+\n+static int test_entry_cmp(const void *cmp_data,\n+\t\t\t  const struct hashmap_entry *eptr,\n+\t\t\t  const struct hashmap_entry *entry_or_key,\n+\t\t\t  const void *keydata)\n+{\n+\tconst int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n+\tconst struct test_entry *e1, *e2;\n+\tconst char *key = keydata;\n+\n+\te1 = container_of(eptr, const struct test_entry, ent);\n+\te2 = container_of(entry_or_key, const struct test_entry, ent);\n+\n+\tif (ignore_case)\n+\t\treturn strcasecmp(e1->key, key ? key : e2->key);\n+\telse\n+\t\treturn strcmp(e1->key, key ? key : e2->key);\n+}\n+\n+static const char *get_value(const struct test_entry *e)\n+{\n+\treturn e->key + strlen(e->key) + 1;\n+}\n+\n+static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n+\t\t\t\t\t   const char *key, const char *value)\n+{\n+\tsize_t klen = strlen(key);\n+\tsize_t vlen = strlen(value);\n+\tunsigned int hash = ignore_case ? strihash(key) : strhash(key);\n+\tstruct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n+\n+\thashmap_entry_init(&entry->ent, hash);\n+\tmemcpy(entry->key, key, klen + 1);\n+\tmemcpy(entry->key + klen + 1, value, vlen + 1);\n+\treturn entry;\n+}\n+\n+static struct test_entry *get_test_entry(struct hashmap *map,\n+\t\t\t\t\t unsigned int ignore_case, const char *key)\n+{\n+\treturn hashmap_get_entry_from_hash(\n+\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n+\t\tstruct test_entry, ent);\n+}\n+\n+static int key_val_contains(const char *key_val[][3], size_t n,\n+\t\t\t    struct test_entry *entry)\n+{\n+\tfor (size_t i = 0; i < n; i++) {\n+\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n+\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n+\t\t\tif (!strcmp(key_val[i][2], \"USED\"))\n+\t\t\t\treturn 2;\n+\t\t\tkey_val[i][2] = \"USED\";\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n+static void setup(void (*f)(struct hashmap *map, int ignore_case),\n+\t\t  int ignore_case)\n+{\n+\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n+\n+\tf(&map, ignore_case);\n+\thashmap_clear_and_free(&map, struct test_entry, ent);\n+}\n+\n+static void t_put(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tcheck(ignore_case ? entry != NULL : entry == NULL);\n+\tfree(entry);\n+\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==,\n+\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n+}\n+\n+static void t_replace(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\n+\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\n+\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n+\t\t\t\t \"value2\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value1\");\n+\tfree(entry);\n+\n+\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\n+\tentry = alloc_test_entry(ignore_case,\n+\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n+\t\t\t\t \"value4\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value3\");\n+\tfree(entry);\n+}\n+\n+static void t_get(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n+\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n+\tconst char *query[][2] = {\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n+\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n+\t};\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n+\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n+\t\tif (check(entry != NULL))\n+\t\t\tcheck_str(get_value(entry), query[i][1]);\n+\t}\n+\n+\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n+}\n+\n+static void t_add(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][3] = {\n+\t\t{ \"key1\", \"value1\", \"UNUSED\" },\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n+\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n+\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n+\t};\n+\tconst char *queries[] = { \"key1\",\n+\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\thashmap_add(map, &entry->ent);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n+\t\tint count = 0;\n+\t\tentry = hashmap_get_entry_from_hash(map,\n+\t\t\tignore_case ? strihash(queries[i]) :\n+\t\t\t\t      strhash(queries[i]),\n+\t\t\tqueries[i], struct test_entry, ent);\n+\n+\t\thashmap_for_each_entry_from(map, entry, ent)\n+\t\t{\n+\t\t\tint ret;\n+\t\t\tif (!check_int((ret = key_val_contains(\n+\t\t\t\t\t\tkey_val, ARRAY_SIZE(key_val),\n+\t\t\t\t\t\tentry)), ==, 0)) {\n+\t\t\t\tswitch (ret) {\n+\t\t\t\tcase 1:\n+\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 2:\n+\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tcount++;\n+\t\t\t}\n+\t\t}\n+\t\tcheck_int(count, ==, 2);\n+\t}\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n+}\n+\n+static void t_remove(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, remove[i][0], \"\");\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(get_value(removed), remove[i][1]);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\n+\tentry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n+\tcheck(hashmap_remove_entry(map, entry, ent, \"notInMap\") == NULL);\n+\tfree(entry);\n+}\n+\n+static void t_iterate(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tstruct hashmap_iter iter;\n+\tconst char *key_val[][3] = { { \"key1\", \"value1\", \"UNUSED\" },\n+\t\t\t\t     { \"key2\", \"value2\", \"UNUSED\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\", \"UNUSED\" } };\n+\tint count = 0;\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t}\n+\n+\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n+\t{\n+\t\tint ret;\n+\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n+\t\t\t\t\t\t       entry)), ==, 0)) {\n+\t\t\tswitch (ret) {\n+\t\t\tcase 1:\n+\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\tcase 2:\n+\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tcount++;\n+\t\t}\n+\t}\n+\tcheck_int(count, ==, ARRAY_SIZE(key_val));\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+}\n+\n+static void t_alloc(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\n+\tfor (int i = 1; i <= 51; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\t\tentry = alloc_test_entry(ignore_case, key, value);\n+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\t\tfree(key);\n+\t\tfree(value);\n+\t}\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 51);\n+\n+\tentry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 52);\n+\n+\tfor (int i = 1; i <= 12; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\n+\t\tentry = alloc_test_entry(ignore_case, key, \"\");\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(value, get_value(removed));\n+\t\tfree(key);\n+\t\tfree(value);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 40);\n+\n+\tentry = alloc_test_entry(ignore_case, \"key40\", \"\");\n+\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n+\tif (check(removed != NULL))\n+\t\tcheck_str(\"value40\", get_value(removed));\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 39);\n+\tfree(entry);\n+\tfree(removed);\n+}\n+\n+static void t_intern(struct hashmap *map, int ignore_case)\n+{\n+\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n+\t\tconst char *i1 = strintern(values[i]);\n+\t\tconst char *i2 = strintern(values[i]);\n+\n+\t\tif (!check(!strcmp(i1, values[i])))\n+\t\t\ttest_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n+\t\telse if (!check(i1 != values[i]))\n+\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n+\t\t\t\t values[i]);\n+\t\telse if (!check(i1 == i2))\n+\t\t\ttest_msg(\"address('%s') != address('%s'), so strintern('%s') != strintern('%s')\",\n+\t\t\t\t i1, i2, values[i], values[i]);\n+\t\telse\n+\t\t\tcheck_str(i1, values[i]);\n+\t}\n+}\n+\n+int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+{\n+\tTEST(setup(t_put, 0), \"put works\");\n+\tTEST(setup(t_put, 1), \"put (case insensitive) works\");\n+\tTEST(setup(t_replace, 0), \"replace works\");\n+\tTEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n+\tTEST(setup(t_get, 0), \"get works\");\n+\tTEST(setup(t_get, 1), \"get (case insensitive) works\");\n+\tTEST(setup(t_add, 0), \"add works\");\n+\tTEST(setup(t_add, 1), \"add (case insensitive) works\");\n+\tTEST(setup(t_remove, 0), \"remove works\");\n+\tTEST(setup(t_remove, 1), \"remove (case insensitive) works\");\n+\tTEST(setup(t_iterate, 0), \"iterate works\");\n+\tTEST(setup(t_iterate, 1), \"iterate (case insensitive) works\");\n+\tTEST(setup(t_alloc, 0), \"grow / shrink works\");\n+\tTEST(setup(t_intern, 0), \"string interning works\");\n+\treturn test_done();\n+}\n-- \n2.45.2\n\n"},{"id":"498375","messageId":"mlnerj7j6knamzj3ipnd7rgqd6xm5xrjep35rldhv6sikzipu5@72szgbso6cpo","threadId":"61692","inReplyTo":"20240708161641.10335-2-shyamthakkar001@gmail.com","subject":"Re: [GSoC][PATCH v2] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2024-07-09T19:34:47Z","receivedAt":"2024-07-09T19:34:54Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"\nThis looks like a good conversion to me. There are a few small\nimprovements that could be made, but mostly LGTM. Comments are inline\nbelow.\n\n\nOn 2024.07.08 21:45, Ghanshyam Thakkar wrote:\n> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\n> library. Migrate them to the unit testing framework for better\n> debugging, runtime performance and consice code.\n\nTypo: s/consice/concise/\n\n> Along with the migration, make 'add' tests from the shellscript order\n> agnostic in unit tests, since they iterate over entries with the same\n> keys and we do not guarantee the order.\n> \n> The helper/test-hashmap.c is still not removed because it contains a\n> performance test meant to be run by the user directly (not used in\n> t/perf). And it makes sense for such a utility to be a helper.\n> \n> Mentored-by: Christian Couder <chriscool@tuxfamily.org>\n> Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\n> Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n> ---\n> The changes in v2 are inspired from the review of another similar\n> test t-oidmap: https://lore.kernel.org/git/16e06a6d-5fd0-4132-9d82-5c6f13b7f9ed@gmail.com/\n> \n> The v2 also includes some formatting corrections and one of the\n> testcases, t_add(), was changed to be more similar to the original.\n> \n>  Makefile                 |   1 +\n>  t/helper/test-hashmap.c  | 100 +----------\n>  t/t0011-hashmap.sh       | 260 ----------------------------\n>  t/unit-tests/t-hashmap.c | 359 +++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 362 insertions(+), 358 deletions(-)\n>  delete mode 100755 t/t0011-hashmap.sh\n>  create mode 100644 t/unit-tests/t-hashmap.c\n\n[snip]\n\n> diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\n> new file mode 100644\n> index 0000000000..1c951fcfd8\n> --- /dev/null\n> +++ b/t/unit-tests/t-hashmap.c\n> @@ -0,0 +1,359 @@\n> +#include \"test-lib.h\"\n> +#include \"hashmap.h\"\n> +#include \"strbuf.h\"\n> +\n> +struct test_entry {\n> +\tint padding; /* hashmap entry no longer needs to be the first member */\n> +\tstruct hashmap_entry ent;\n> +\t/* key and value as two \\0-terminated strings */\n> +\tchar key[FLEX_ARRAY];\n> +};\n> +\n> +static int test_entry_cmp(const void *cmp_data,\n> +\t\t\t  const struct hashmap_entry *eptr,\n> +\t\t\t  const struct hashmap_entry *entry_or_key,\n> +\t\t\t  const void *keydata)\n> +{\n> +\tconst int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n> +\tconst struct test_entry *e1, *e2;\n> +\tconst char *key = keydata;\n> +\n> +\te1 = container_of(eptr, const struct test_entry, ent);\n> +\te2 = container_of(entry_or_key, const struct test_entry, ent);\n> +\n> +\tif (ignore_case)\n> +\t\treturn strcasecmp(e1->key, key ? key : e2->key);\n> +\telse\n> +\t\treturn strcmp(e1->key, key ? key : e2->key);\n> +}\n> +\n> +static const char *get_value(const struct test_entry *e)\n> +{\n> +\treturn e->key + strlen(e->key) + 1;\n> +}\n> +\n> +static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n> +\t\t\t\t\t   const char *key, const char *value)\n> +{\n> +\tsize_t klen = strlen(key);\n> +\tsize_t vlen = strlen(value);\n> +\tunsigned int hash = ignore_case ? strihash(key) : strhash(key);\n> +\tstruct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n> +\n> +\thashmap_entry_init(&entry->ent, hash);\n> +\tmemcpy(entry->key, key, klen + 1);\n> +\tmemcpy(entry->key + klen + 1, value, vlen + 1);\n> +\treturn entry;\n> +}\n\nSo we're duplicating `struct test_entry`, `test_entry_cmp()`, and\n`alloc_test_entry()`, which have (almost) identical definitions in\nt/helper/test-hashmap.c. I wonder if it's worth splitting these into a\nseparate .c file. Maybe it's too much of a pain to add Makefile rules to\nshare objects across the test helper and the unit tests. Something to\nkeep in mind I guess, if we find that we want to share more code than\nthis.\n\n\n> +static struct test_entry *get_test_entry(struct hashmap *map,\n> +\t\t\t\t\t unsigned int ignore_case, const char *key)\n> +{\n> +\treturn hashmap_get_entry_from_hash(\n> +\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n> +\t\tstruct test_entry, ent);\n> +}\n> +\n> +static int key_val_contains(const char *key_val[][3], size_t n,\n> +\t\t\t    struct test_entry *entry)\n> +{\n> +\tfor (size_t i = 0; i < n; i++) {\n> +\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n> +\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n> +\t\t\tif (!strcmp(key_val[i][2], \"USED\"))\n> +\t\t\t\treturn 2;\n> +\t\t\tkey_val[i][2] = \"USED\";\n> +\t\t\treturn 0;\n> +\t\t}\n> +\t}\n> +\treturn 1;\n> +}\n> +\n> +static void setup(void (*f)(struct hashmap *map, int ignore_case),\n> +\t\t  int ignore_case)\n> +{\n> +\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n> +\n> +\tf(&map, ignore_case);\n> +\thashmap_clear_and_free(&map, struct test_entry, ent);\n> +}\n\nAs I mentioned in my review [1] on René's TEST_RUN series, I don't think\nwe get much value out of having a setup + callback approach when the\nsetup is minimal. Would you consider rewriting a v2 using TEST_RUN once\nthat is ready in `next`?\n\n[1] https://lore.kernel.org/git/tswyfparvchgi7qxrjxbx4eb7cohypzekjqzbnkbffsesaiazs@vtewtz7o6twi/\n\n> +static void t_put(struct hashmap *map, int ignore_case)\n> +{\n> +\tstruct test_entry *entry;\n> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> +\t\t\t\t     { \"key2\", \"value2\" },\n> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\t}\n> +\n> +\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n> +\tentry = hashmap_put_entry(map, entry, ent);\n> +\tcheck(ignore_case ? entry != NULL : entry == NULL);\n> +\tfree(entry);\n> +\n> +\tcheck_int(map->tablesize, ==, 64);\n> +\tcheck_int(hashmap_get_size(map), ==,\n> +\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n> +}\n\nAhhh, so you're using the same function for both case-sensitive and\n-insensitive tests. So I guess TEST_RUN isn't useful here after all.\nPersonally I'd still rather get rid of setup(), but I don't feel super\nstrongly about it.\n\n\n> +static void t_replace(struct hashmap *map, int ignore_case)\n> +{\n> +\tstruct test_entry *entry;\n> +\n> +\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n> +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\n> +\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n> +\t\t\t\t \"value2\");\n> +\tentry = hashmap_put_entry(map, entry, ent);\n> +\tif (check(entry != NULL))\n> +\t\tcheck_str(get_value(entry), \"value1\");\n> +\tfree(entry);\n> +\n> +\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n> +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\n> +\tentry = alloc_test_entry(ignore_case,\n> +\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n> +\t\t\t\t \"value4\");\n> +\tentry = hashmap_put_entry(map, entry, ent);\n> +\tif (check(entry != NULL))\n> +\t\tcheck_str(get_value(entry), \"value3\");\n> +\tfree(entry);\n> +}\n> +\n> +static void t_get(struct hashmap *map, int ignore_case)\n> +{\n> +\tstruct test_entry *entry;\n> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> +\t\t\t\t     { \"key2\", \"value2\" },\n> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n> +\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n> +\tconst char *query[][2] = {\n> +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> +\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n> +\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n> +\t};\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\t}\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n> +\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n> +\t\tif (check(entry != NULL))\n> +\t\t\tcheck_str(get_value(entry), query[i][1]);\n> +\t}\n> +\n> +\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n> +}\n> +\n> +static void t_add(struct hashmap *map, int ignore_case)\n> +{\n> +\tstruct test_entry *entry;\n> +\tconst char *key_val[][3] = {\n> +\t\t{ \"key1\", \"value1\", \"UNUSED\" },\n> +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n> +\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n> +\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n> +\t};\n> +\tconst char *queries[] = { \"key1\",\n> +\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> +\t\thashmap_add(map, &entry->ent);\n> +\t}\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n\nSince we only have one query, can we remove the loop and simplify the\nfollowing block of code?\n\nAlso (here and elsewhere), it might be less confusing to say \"UNSEEN\" /\n\"SEEN\" instead of \"UNUSED\" / \"USED\". The latter makes it sound to me\nlike there's some API requirement to have a 3-item array that we don't\nactually need, but in this case those fields are actually used in\nkey_val_contains() to track duplicates.\n\n\n> +\t\tint count = 0;\n> +\t\tentry = hashmap_get_entry_from_hash(map,\n> +\t\t\tignore_case ? strihash(queries[i]) :\n> +\t\t\t\t      strhash(queries[i]),\n> +\t\t\tqueries[i], struct test_entry, ent);\n> +\n> +\t\thashmap_for_each_entry_from(map, entry, ent)\n> +\t\t{\n> +\t\t\tint ret;\n> +\t\t\tif (!check_int((ret = key_val_contains(\n> +\t\t\t\t\t\tkey_val, ARRAY_SIZE(key_val),\n> +\t\t\t\t\t\tentry)), ==, 0)) {\n> +\t\t\t\tswitch (ret) {\n> +\t\t\t\tcase 1:\n> +\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n> +\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> +\t\t\t\t\t\t entry->key, get_value(entry));\n> +\t\t\t\t\tbreak;\n> +\t\t\t\tcase 2:\n> +\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n> +\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> +\t\t\t\t\t\t entry->key, get_value(entry));\n> +\t\t\t\t\tbreak;\n> +\t\t\t\t}\n> +\t\t\t} else {\n> +\t\t\t\tcount++;\n> +\t\t\t}\n> +\t\t}\n> +\t\tcheck_int(count, ==, 2);\n> +\t}\n> +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n> +}\n> +\n> +static void t_remove(struct hashmap *map, int ignore_case)\n> +{\n> +\tstruct test_entry *entry, *removed;\n> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> +\t\t\t\t     { \"key2\", \"value2\" },\n> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n> +\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> +\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\t}\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n> +\t\tentry = alloc_test_entry(ignore_case, remove[i][0], \"\");\n> +\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n> +\t\tif (check(removed != NULL))\n> +\t\t\tcheck_str(get_value(removed), remove[i][1]);\n> +\t\tfree(entry);\n> +\t\tfree(removed);\n> +\t}\n> +\n> +\tentry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n> +\tcheck(hashmap_remove_entry(map, entry, ent, \"notInMap\") == NULL);\n> +\tfree(entry);\n> +}\n\nIs there a reason why you don't check the table size as the shell test\ndid? (similar to what you have in t_put())\n\n> +static void t_iterate(struct hashmap *map, int ignore_case)\n> +{\n> +\tstruct test_entry *entry;\n> +\tstruct hashmap_iter iter;\n> +\tconst char *key_val[][3] = { { \"key1\", \"value1\", \"UNUSED\" },\n> +\t\t\t\t     { \"key2\", \"value2\", \"UNUSED\" },\n> +\t\t\t\t     { \"fooBarFrotz\", \"value3\", \"UNUSED\" } };\n> +\tint count = 0;\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\t}\n> +\n> +\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n> +\t{\n> +\t\tint ret;\n> +\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n> +\t\t\t\t\t\t       entry)), ==, 0)) {\n> +\t\t\tswitch (ret) {\n> +\t\t\tcase 1:\n> +\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n> +\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> +\t\t\t\t\t entry->key, get_value(entry));\n> +\t\t\t\tbreak;\n> +\t\t\tcase 2:\n> +\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n> +\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> +\t\t\t\t\t entry->key, get_value(entry));\n> +\t\t\t\tbreak;\n> +\t\t\t}\n> +\t\t} else {\n> +\t\t\tcount++;\n> +\t\t}\n> +\t}\n> +\tcheck_int(count, ==, ARRAY_SIZE(key_val));\n> +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +}\n> +\n> +static void t_alloc(struct hashmap *map, int ignore_case)\n> +{\n> +\tstruct test_entry *entry, *removed;\n> +\n> +\tfor (int i = 1; i <= 51; i++) {\n> +\t\tchar *key = xstrfmt(\"key%d\", i);\n> +\t\tchar *value = xstrfmt(\"value%d\", i);\n> +\t\tentry = alloc_test_entry(ignore_case, key, value);\n> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\t\tfree(key);\n> +\t\tfree(value);\n> +\t}\n> +\tcheck_int(map->tablesize, ==, 64);\n> +\tcheck_int(hashmap_get_size(map), ==, 51);\n> +\n> +\tentry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n> +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> +\tcheck_int(map->tablesize, ==, 256);\n> +\tcheck_int(hashmap_get_size(map), ==, 52);\n> +\n> +\tfor (int i = 1; i <= 12; i++) {\n> +\t\tchar *key = xstrfmt(\"key%d\", i);\n> +\t\tchar *value = xstrfmt(\"value%d\", i);\n> +\n> +\t\tentry = alloc_test_entry(ignore_case, key, \"\");\n> +\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n> +\t\tif (check(removed != NULL))\n> +\t\t\tcheck_str(value, get_value(removed));\n> +\t\tfree(key);\n> +\t\tfree(value);\n> +\t\tfree(entry);\n> +\t\tfree(removed);\n> +\t}\n> +\tcheck_int(map->tablesize, ==, 256);\n> +\tcheck_int(hashmap_get_size(map), ==, 40);\n> +\n> +\tentry = alloc_test_entry(ignore_case, \"key40\", \"\");\n> +\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n> +\tif (check(removed != NULL))\n> +\t\tcheck_str(\"value40\", get_value(removed));\n> +\tcheck_int(map->tablesize, ==, 64);\n> +\tcheck_int(hashmap_get_size(map), ==, 39);\n> +\tfree(entry);\n> +\tfree(removed);\n> +}\n> +\n> +static void t_intern(struct hashmap *map, int ignore_case)\n> +{\n> +\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n> +\n> +\tfor (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n> +\t\tconst char *i1 = strintern(values[i]);\n> +\t\tconst char *i2 = strintern(values[i]);\n> +\n> +\t\tif (!check(!strcmp(i1, values[i])))\n\nShould we use check_str() here? Or does that add too much extraneous\ndetail when the test_msg() below is what we really care about?\n\n> +\t\t\ttest_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n> +\t\telse if (!check(i1 != values[i]))\n\nSimilarly, check_pointer_eq() here?\n\n\n> +\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n> +\t\t\t\t values[i]);\n> +\t\telse if (!check(i1 == i2))\n\nAnd check_pointer_eq() here as well.\n\n\n> +\t\t\ttest_msg(\"address('%s') != address('%s'), so strintern('%s') != strintern('%s')\",\n> +\t\t\t\t i1, i2, values[i], values[i]);\n> +\t\telse\n> +\t\t\tcheck_str(i1, values[i]);\n> +\t}\n> +}\n> +\n> +int cmd_main(int argc UNUSED, const char **argv UNUSED)\n> +{\n> +\tTEST(setup(t_put, 0), \"put works\");\n> +\tTEST(setup(t_put, 1), \"put (case insensitive) works\");\n> +\tTEST(setup(t_replace, 0), \"replace works\");\n> +\tTEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n> +\tTEST(setup(t_get, 0), \"get works\");\n> +\tTEST(setup(t_get, 1), \"get (case insensitive) works\");\n> +\tTEST(setup(t_add, 0), \"add works\");\n> +\tTEST(setup(t_add, 1), \"add (case insensitive) works\");\n> +\tTEST(setup(t_remove, 0), \"remove works\");\n> +\tTEST(setup(t_remove, 1), \"remove (case insensitive) works\");\n> +\tTEST(setup(t_iterate, 0), \"iterate works\");\n> +\tTEST(setup(t_iterate, 1), \"iterate (case insensitive) works\");\n> +\tTEST(setup(t_alloc, 0), \"grow / shrink works\");\n> +\tTEST(setup(t_intern, 0), \"string interning works\");\n> +\treturn test_done();\n> +}\n> -- \n> 2.45.2\n> \n> \n"},{"id":"498381","messageId":"xmqqcynmdyy2.fsf@gitster.g","threadId":"61692","inReplyTo":"mlnerj7j6knamzj3ipnd7rgqd6xm5xrjep35rldhv6sikzipu5@72szgbso6cpo","subject":"Re: [GSoC][PATCH v2] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-09T21:42:45Z","receivedAt":"2024-07-09T21:42:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n>> +static void t_put(struct hashmap *map, int ignore_case)\n>> +{\n>> +\tstruct test_entry *entry;\n>> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n>> +\t\t\t\t     { \"key2\", \"value2\" },\n>> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n>> +\n>> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n>> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n>> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n>> +\t}\n>> +\n>> +\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n>> +\tentry = hashmap_put_entry(map, entry, ent);\n>> +\tcheck(ignore_case ? entry != NULL : entry == NULL);\n>> +\tfree(entry);\n>> +\n>> +\tcheck_int(map->tablesize, ==, 64);\n>> +\tcheck_int(hashmap_get_size(map), ==,\n>> +\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n>> +}\n>\n> Ahhh, so you're using the same function for both case-sensitive and\n> -insensitive tests. So I guess TEST_RUN isn't useful here after all.\n> Personally I'd still rather get rid of setup(), but I don't feel super\n> strongly about it.\n\nConsulting the table with \"fooBarFrotz\" and checking what gets\nreturned (expect \"value3\" for !icase, or \"value4\" for icase) is one\nof the things that are missing.  In fact, the values stored are not\neven checked with the above test at all.\n\n>> +static void t_replace(struct hashmap *map, int ignore_case)\n>> +{\n>> +\tstruct test_entry *entry;\n>> +\n>> +\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n>> +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n>> +\n>> +\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n>> +\t\t\t\t \"value2\");\n>> +\tentry = hashmap_put_entry(map, entry, ent);\n>> +\tif (check(entry != NULL))\n>> +\t\tcheck_str(get_value(entry), \"value1\");\n>> +\tfree(entry);\n>> +\n>> +\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n>> +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n>> +\n>> +\tentry = alloc_test_entry(ignore_case,\n>> +\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n>> +\t\t\t\t \"value4\");\n\nCurious.  If the hashmap is set up for icase use, do callers still\nneed to downcase the key?  Shouldn't the library take care of that?\nAfter all, test_entry_cmp() when the hashmap is being used in icase\nmode does strcasecmp() anyway.\n\n>> +\tentry = hashmap_put_entry(map, entry, ent);\n>> +\tif (check(entry != NULL))\n>> +\t\tcheck_str(get_value(entry), \"value3\");\n\nHere the stored value is checked, which is good.\n\n>> +\tfree(entry);\n>> +}\n>> +\n>> +static void t_get(struct hashmap *map, int ignore_case)\n>> +{\n>> +\tstruct test_entry *entry;\n>> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n>> +\t\t\t\t     { \"key2\", \"value2\" },\n>> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n>> +\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n>> +\tconst char *query[][2] = {\n>> +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n>> +\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n>> +\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n>> +\t};\n>> +\n>> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n>> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n>> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n>> +\t}\n>> +\n>> +\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n>> +\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n>> +\t\tif (check(entry != NULL))\n>> +\t\t\tcheck_str(get_value(entry), query[i][1]);\n>> +\t}\n>> +\n>> +\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n>> +}\n\nIt is getting dubious if it is worth having t_put, when t_get does\nso similar things and more.\n\nSame comment applies wrt icase, and not limited to t_get() but all\nthe remaining test functions (elided).\n\nThanks.\n\n"},{"id":"498391","messageId":"D2LEBVI9SZXC.3D4M7X4SN99BX@gmail.com","threadId":"61692","inReplyTo":"mlnerj7j6knamzj3ipnd7rgqd6xm5xrjep35rldhv6sikzipu5@72szgbso6cpo","subject":"Re: [GSoC][PATCH v2] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-07-09T23:27:32Z","receivedAt":"2024-07-09T23:27:38Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"Josh Steadmon <steadmon@google.com> wrote:\n>\n> This looks like a good conversion to me. There are a few small\n> improvements that could be made, but mostly LGTM. Comments are inline\n> below.\n>\n>\n> On 2024.07.08 21:45, Ghanshyam Thakkar wrote:\n> > helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\n> > library. Migrate them to the unit testing framework for better\n> > debugging, runtime performance and consice code.\n>\n> Typo: s/consice/concise/\n\nWill update.\n\n>\n> > Along with the migration, make 'add' tests from the shellscript order\n> > agnostic in unit tests, since they iterate over entries with the same\n> > keys and we do not guarantee the order.\n> > \n> > The helper/test-hashmap.c is still not removed because it contains a\n> > performance test meant to be run by the user directly (not used in\n> > t/perf). And it makes sense for such a utility to be a helper.\n> > \n> > Mentored-by: Christian Couder <chriscool@tuxfamily.org>\n> > Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\n> > Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n> > ---\n> > The changes in v2 are inspired from the review of another similar\n> > test t-oidmap: https://lore.kernel.org/git/16e06a6d-5fd0-4132-9d82-5c6f13b7f9ed@gmail.com/\n> > \n> > The v2 also includes some formatting corrections and one of the\n> > testcases, t_add(), was changed to be more similar to the original.\n> > \n> >  Makefile                 |   1 +\n> >  t/helper/test-hashmap.c  | 100 +----------\n> >  t/t0011-hashmap.sh       | 260 ----------------------------\n> >  t/unit-tests/t-hashmap.c | 359 +++++++++++++++++++++++++++++++++++++++\n> >  4 files changed, 362 insertions(+), 358 deletions(-)\n> >  delete mode 100755 t/t0011-hashmap.sh\n> >  create mode 100644 t/unit-tests/t-hashmap.c\n>\n> [snip]\n>\n> > diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\n> > new file mode 100644\n> > index 0000000000..1c951fcfd8\n> > --- /dev/null\n> > +++ b/t/unit-tests/t-hashmap.c\n> > @@ -0,0 +1,359 @@\n> > +#include \"test-lib.h\"\n> > +#include \"hashmap.h\"\n> > +#include \"strbuf.h\"\n> > +\n> > +struct test_entry {\n> > +\tint padding; /* hashmap entry no longer needs to be the first member */\n> > +\tstruct hashmap_entry ent;\n> > +\t/* key and value as two \\0-terminated strings */\n> > +\tchar key[FLEX_ARRAY];\n> > +};\n> > +\n> > +static int test_entry_cmp(const void *cmp_data,\n> > +\t\t\t  const struct hashmap_entry *eptr,\n> > +\t\t\t  const struct hashmap_entry *entry_or_key,\n> > +\t\t\t  const void *keydata)\n> > +{\n> > +\tconst int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n> > +\tconst struct test_entry *e1, *e2;\n> > +\tconst char *key = keydata;\n> > +\n> > +\te1 = container_of(eptr, const struct test_entry, ent);\n> > +\te2 = container_of(entry_or_key, const struct test_entry, ent);\n> > +\n> > +\tif (ignore_case)\n> > +\t\treturn strcasecmp(e1->key, key ? key : e2->key);\n> > +\telse\n> > +\t\treturn strcmp(e1->key, key ? key : e2->key);\n> > +}\n> > +\n> > +static const char *get_value(const struct test_entry *e)\n> > +{\n> > +\treturn e->key + strlen(e->key) + 1;\n> > +}\n> > +\n> > +static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n> > +\t\t\t\t\t   const char *key, const char *value)\n> > +{\n> > +\tsize_t klen = strlen(key);\n> > +\tsize_t vlen = strlen(value);\n> > +\tunsigned int hash = ignore_case ? strihash(key) : strhash(key);\n> > +\tstruct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n> > +\n> > +\thashmap_entry_init(&entry->ent, hash);\n> > +\tmemcpy(entry->key, key, klen + 1);\n> > +\tmemcpy(entry->key + klen + 1, value, vlen + 1);\n> > +\treturn entry;\n> > +}\n>\n> So we're duplicating `struct test_entry`, `test_entry_cmp()`, and\n> `alloc_test_entry()`, which have (almost) identical definitions in\n> t/helper/test-hashmap.c. I wonder if it's worth splitting these into a\n> separate .c file. Maybe it's too much of a pain to add Makefile rules to\n> share objects across the test helper and the unit tests. Something to\n> keep in mind I guess, if we find that we want to share more code than\n> this.\n>\n>\n> > +static struct test_entry *get_test_entry(struct hashmap *map,\n> > +\t\t\t\t\t unsigned int ignore_case, const char *key)\n> > +{\n> > +\treturn hashmap_get_entry_from_hash(\n> > +\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n> > +\t\tstruct test_entry, ent);\n> > +}\n> > +\n> > +static int key_val_contains(const char *key_val[][3], size_t n,\n> > +\t\t\t    struct test_entry *entry)\n> > +{\n> > +\tfor (size_t i = 0; i < n; i++) {\n> > +\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n> > +\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n> > +\t\t\tif (!strcmp(key_val[i][2], \"USED\"))\n> > +\t\t\t\treturn 2;\n> > +\t\t\tkey_val[i][2] = \"USED\";\n> > +\t\t\treturn 0;\n> > +\t\t}\n> > +\t}\n> > +\treturn 1;\n> > +}\n> > +\n> > +static void setup(void (*f)(struct hashmap *map, int ignore_case),\n> > +\t\t  int ignore_case)\n> > +{\n> > +\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n> > +\n> > +\tf(&map, ignore_case);\n> > +\thashmap_clear_and_free(&map, struct test_entry, ent);\n> > +}\n>\n> As I mentioned in my review [1] on René's TEST_RUN series, I don't\n> think\n> we get much value out of having a setup + callback approach when the\n> setup is minimal. Would you consider rewriting a v2 using TEST_RUN once\n> that is ready in `next`?\n>\n> [1]\n> https://lore.kernel.org/git/tswyfparvchgi7qxrjxbx4eb7cohypzekjqzbnkbffsesaiazs@vtewtz7o6twi/\n>\n> > +static void t_put(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tstruct test_entry *entry;\n> > +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> > +\t\t\t\t     { \"key2\", \"value2\" },\n> > +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> > +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> > +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\t}\n> > +\n> > +\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n> > +\tentry = hashmap_put_entry(map, entry, ent);\n> > +\tcheck(ignore_case ? entry != NULL : entry == NULL);\n> > +\tfree(entry);\n> > +\n> > +\tcheck_int(map->tablesize, ==, 64);\n> > +\tcheck_int(hashmap_get_size(map), ==,\n> > +\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n> > +}\n>\n> Ahhh, so you're using the same function for both case-sensitive and\n> -insensitive tests. So I guess TEST_RUN isn't useful here after all.\n> Personally I'd still rather get rid of setup(), but I don't feel super\n> strongly about it.\n\nSure, I'll get rid of setup().\n\n>\n> > +static void t_replace(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tstruct test_entry *entry;\n> > +\n> > +\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n> > +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\n> > +\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n> > +\t\t\t\t \"value2\");\n> > +\tentry = hashmap_put_entry(map, entry, ent);\n> > +\tif (check(entry != NULL))\n> > +\t\tcheck_str(get_value(entry), \"value1\");\n> > +\tfree(entry);\n> > +\n> > +\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n> > +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\n> > +\tentry = alloc_test_entry(ignore_case,\n> > +\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n> > +\t\t\t\t \"value4\");\n> > +\tentry = hashmap_put_entry(map, entry, ent);\n> > +\tif (check(entry != NULL))\n> > +\t\tcheck_str(get_value(entry), \"value3\");\n> > +\tfree(entry);\n> > +}\n> > +\n> > +static void t_get(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tstruct test_entry *entry;\n> > +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> > +\t\t\t\t     { \"key2\", \"value2\" },\n> > +\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n> > +\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n> > +\tconst char *query[][2] = {\n> > +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> > +\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n> > +\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n> > +\t};\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> > +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> > +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\t}\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n> > +\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n> > +\t\tif (check(entry != NULL))\n> > +\t\t\tcheck_str(get_value(entry), query[i][1]);\n> > +\t}\n> > +\n> > +\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n> > +}\n> > +\n> > +static void t_add(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tstruct test_entry *entry;\n> > +\tconst char *key_val[][3] = {\n> > +\t\t{ \"key1\", \"value1\", \"UNUSED\" },\n> > +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n> > +\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n> > +\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n> > +\t};\n> > +\tconst char *queries[] = { \"key1\",\n> > +\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> > +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> > +\t\thashmap_add(map, &entry->ent);\n> > +\t}\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n>\n> Since we only have one query, can we remove the loop and simplify the\n> following block of code?\n\nWe have two queries as can be seen above. One is \"key1\" and the other is\n\"Foobarfrotz\"/\"fooBarFrotz\".\n\n> Also (here and elsewhere), it might be less confusing to say \"UNSEEN\" /\n> \"SEEN\" instead of \"UNUSED\" / \"USED\". The latter makes it sound to me\n> like there's some API requirement to have a 3-item array that we don't\n> actually need, but in this case those fields are actually used in\n> key_val_contains() to track duplicates.\n\nYeah, \"UNSEEN\" is definitely less confusing. Will change.\n\n>\n> > +\t\tint count = 0;\n> > +\t\tentry = hashmap_get_entry_from_hash(map,\n> > +\t\t\tignore_case ? strihash(queries[i]) :\n> > +\t\t\t\t      strhash(queries[i]),\n> > +\t\t\tqueries[i], struct test_entry, ent);\n> > +\n> > +\t\thashmap_for_each_entry_from(map, entry, ent)\n> > +\t\t{\n> > +\t\t\tint ret;\n> > +\t\t\tif (!check_int((ret = key_val_contains(\n> > +\t\t\t\t\t\tkey_val, ARRAY_SIZE(key_val),\n> > +\t\t\t\t\t\tentry)), ==, 0)) {\n> > +\t\t\t\tswitch (ret) {\n> > +\t\t\t\tcase 1:\n> > +\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n> > +\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> > +\t\t\t\t\t\t entry->key, get_value(entry));\n> > +\t\t\t\t\tbreak;\n> > +\t\t\t\tcase 2:\n> > +\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n> > +\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> > +\t\t\t\t\t\t entry->key, get_value(entry));\n> > +\t\t\t\t\tbreak;\n> > +\t\t\t\t}\n> > +\t\t\t} else {\n> > +\t\t\t\tcount++;\n> > +\t\t\t}\n> > +\t\t}\n> > +\t\tcheck_int(count, ==, 2);\n> > +\t}\n> > +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> > +\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n> > +}\n> > +\n> > +static void t_remove(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tstruct test_entry *entry, *removed;\n> > +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> > +\t\t\t\t     { \"key2\", \"value2\" },\n> > +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n> > +\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> > +\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> > +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> > +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\t}\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n> > +\t\tentry = alloc_test_entry(ignore_case, remove[i][0], \"\");\n> > +\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n> > +\t\tif (check(removed != NULL))\n> > +\t\t\tcheck_str(get_value(removed), remove[i][1]);\n> > +\t\tfree(entry);\n> > +\t\tfree(removed);\n> > +\t}\n> > +\n> > +\tentry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n> > +\tcheck(hashmap_remove_entry(map, entry, ent, \"notInMap\") == NULL);\n> > +\tfree(entry);\n> > +}\n>\n> Is there a reason why you don't check the table size as the shell test\n> did? (similar to what you have in t_put())\n\nHuh, I seem to have forgotten to put this snippet here:\n  \n\tcheck_int(map->tablesize, ==, 64);\n\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));\n\t\nI'll add it in v3.\n\n>\n> > +static void t_iterate(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tstruct test_entry *entry;\n> > +\tstruct hashmap_iter iter;\n> > +\tconst char *key_val[][3] = { { \"key1\", \"value1\", \"UNUSED\" },\n> > +\t\t\t\t     { \"key2\", \"value2\", \"UNUSED\" },\n> > +\t\t\t\t     { \"fooBarFrotz\", \"value3\", \"UNUSED\" } };\n> > +\tint count = 0;\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> > +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> > +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\t}\n> > +\n> > +\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n> > +\t{\n> > +\t\tint ret;\n> > +\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n> > +\t\t\t\t\t\t       entry)), ==, 0)) {\n> > +\t\t\tswitch (ret) {\n> > +\t\t\tcase 1:\n> > +\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n> > +\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> > +\t\t\t\t\t entry->key, get_value(entry));\n> > +\t\t\t\tbreak;\n> > +\t\t\tcase 2:\n> > +\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n> > +\t\t\t\t\t \"    key: %s\\n  value: %s\",\n> > +\t\t\t\t\t entry->key, get_value(entry));\n> > +\t\t\t\tbreak;\n> > +\t\t\t}\n> > +\t\t} else {\n> > +\t\t\tcount++;\n> > +\t\t}\n> > +\t}\n> > +\tcheck_int(count, ==, ARRAY_SIZE(key_val));\n> > +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> > +}\n> > +\n> > +static void t_alloc(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tstruct test_entry *entry, *removed;\n> > +\n> > +\tfor (int i = 1; i <= 51; i++) {\n> > +\t\tchar *key = xstrfmt(\"key%d\", i);\n> > +\t\tchar *value = xstrfmt(\"value%d\", i);\n> > +\t\tentry = alloc_test_entry(ignore_case, key, value);\n> > +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\t\tfree(key);\n> > +\t\tfree(value);\n> > +\t}\n> > +\tcheck_int(map->tablesize, ==, 64);\n> > +\tcheck_int(hashmap_get_size(map), ==, 51);\n> > +\n> > +\tentry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n> > +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> > +\tcheck_int(map->tablesize, ==, 256);\n> > +\tcheck_int(hashmap_get_size(map), ==, 52);\n> > +\n> > +\tfor (int i = 1; i <= 12; i++) {\n> > +\t\tchar *key = xstrfmt(\"key%d\", i);\n> > +\t\tchar *value = xstrfmt(\"value%d\", i);\n> > +\n> > +\t\tentry = alloc_test_entry(ignore_case, key, \"\");\n> > +\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n> > +\t\tif (check(removed != NULL))\n> > +\t\t\tcheck_str(value, get_value(removed));\n> > +\t\tfree(key);\n> > +\t\tfree(value);\n> > +\t\tfree(entry);\n> > +\t\tfree(removed);\n> > +\t}\n> > +\tcheck_int(map->tablesize, ==, 256);\n> > +\tcheck_int(hashmap_get_size(map), ==, 40);\n> > +\n> > +\tentry = alloc_test_entry(ignore_case, \"key40\", \"\");\n> > +\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n> > +\tif (check(removed != NULL))\n> > +\t\tcheck_str(\"value40\", get_value(removed));\n> > +\tcheck_int(map->tablesize, ==, 64);\n> > +\tcheck_int(hashmap_get_size(map), ==, 39);\n> > +\tfree(entry);\n> > +\tfree(removed);\n> > +}\n> > +\n> > +static void t_intern(struct hashmap *map, int ignore_case)\n> > +{\n> > +\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n> > +\n> > +\tfor (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n> > +\t\tconst char *i1 = strintern(values[i]);\n> > +\t\tconst char *i2 = strintern(values[i]);\n> > +\n> > +\t\tif (!check(!strcmp(i1, values[i])))\n>\n> Should we use check_str() here? Or does that add too much extraneous\n> detail when the test_msg() below is what we really care about?\n\nIt would repetitive as both print the same thing but test_msg() has more\ncontext.\n\n> > +\t\t\ttest_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n> > +\t\telse if (!check(i1 != values[i]))\n>\n> Similarly, check_pointer_eq() here?\n\nHow would we use check_pointer_eq() here as we are checking to make\nsure they are not equal?\n\n>\n> > +\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n> > +\t\t\t\t values[i]);\n> > +\t\telse if (!check(i1 == i2))\n>\n> And check_pointer_eq() here as well.\n\nWill update here.\n\nThanks for the review.\n\n"},{"id":"498435","messageId":"2d7fd61d-0b2c-465f-9f24-47c99ab5b56c@gmail.com","threadId":"61692","inReplyTo":"mlnerj7j6knamzj3ipnd7rgqd6xm5xrjep35rldhv6sikzipu5@72szgbso6cpo","subject":"Re: [GSoC][PATCH v2] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2024-07-10T10:19:31Z","receivedAt":"2024-07-10T10:19:34Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 09/07/2024 20:34, Josh Steadmon wrote:\n> On 2024.07.08 21:45, Ghanshyam Thakkar wrote:\n\n>> +static void t_put(struct hashmap *map, int ignore_case)\n>> +{\n>> +\tstruct test_entry *entry;\n>> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n>> +\t\t\t\t     { \"key2\", \"value2\" },\n>> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n>> +\n>> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n>> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n>> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n>> +\t}\n>> +\n>> +\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n>> +\tentry = hashmap_put_entry(map, entry, ent);\n>> +\tcheck(ignore_case ? entry != NULL : entry == NULL);\n>> +\tfree(entry);\n>> +\n>> +\tcheck_int(map->tablesize, ==, 64);\n>> +\tcheck_int(hashmap_get_size(map), ==,\n>> +\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n>> +}\n> \n> Ahhh, so you're using the same function for both case-sensitive and\n> -insensitive tests. So I guess TEST_RUN isn't useful here after all.\n> Personally I'd still rather get rid of setup(), but I don't feel super\n> strongly about it.\n\nI'm not sure - we have to pass ignore_case to HASHMAP_INIT and the test \nfunction so using setup() means we cannot pass different values to the \ntwo different functions.\n\nFor parameterized tests where we calling the same function with \ndifferent inputs using a setup function allows us to\n  - write more concise test code\n  - easily change the setup of all the tests if the api changes in the\n    future.\n  - consistently free resources at the end of a test making it easier to\n    write leak-free tests\n  - assert pre- and post- conditions on all tests\n\nUsing TEST_RUN is useful to declare the different inputs for each test. \nFor example in the oid-array tests it allows us to write\n\n     TEST_RUN(\"ordered enumeration\") {\n         const char *input[] = { \"88\", \"44\", \"aa\", \"55\" };\n         const char *expected[] = { \"44\", \"55\", \"88\", \"aa\" };\n\n         TEST_ENUMERATION(input, expected)\n     }\n\nrather than declaring a bunch of variables up front a long way from \nwhere they are used.\n\n>> +static void t_add(struct hashmap *map, int ignore_case)\n>> +{\n>> +\tstruct test_entry *entry;\n>> +\tconst char *key_val[][3] = {\n>> +\t\t{ \"key1\", \"value1\", \"UNUSED\" },\n>> +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n>> +\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n>> +\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n>> +\t};\n>> +\tconst char *queries[] = { \"key1\",\n>> +\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n>> +\n>> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n>> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n>> +\t\thashmap_add(map, &entry->ent);\n>> +\t}\n>> +\n>> +\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n> \n> Since we only have one query, can we remove the loop and simplify the\n> following block of code?\n> \n> Also (here and elsewhere), it might be less confusing to say \"UNSEEN\" /\n> \"SEEN\" instead of \"UNUSED\" / \"USED\". The latter makes it sound to me\n> like there's some API requirement to have a 3-item array that we don't\n> actually need, but in this case those fields are actually used in\n> key_val_contains() to track duplicates.\n\nThe third element just needs to be a boolean flag so it might be better \nto use a struct\n\n\tconst struct {\n\t\tchar *key;\n\t\tchar *val;\n\t\tchar seen;\n\t} key_val[] = {\n\t\t{ .key = \"key1\", .val = \"value1\" },\n\t\t{ .key = ignore_case ? \"Key1\" : \"key1\" .val = \"value2\" },\t\t{ .key = \n\"fooBarFrotz\" .val = \"value3\" },\n\t\t{ .key = ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", .value = \"value4\" }\n\t};\n\nBest Wishes\n\nPhillip\n\n"},{"id":"498476","messageId":"D2M45OWGEGU8.2TWYW1FQ2X3I@gmail.com","threadId":"61692","inReplyTo":"xmqqcynmdyy2.fsf@gitster.g","subject":"Re: [GSoC][PATCH v2] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-07-10T19:41:56Z","receivedAt":"2024-07-10T19:42:03Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"Junio C Hamano <gitster@pobox.com> wrote:\n> Josh Steadmon <steadmon@google.com> writes:\n>\n> >> +static void t_put(struct hashmap *map, int ignore_case)\n> >> +{\n> >> +\tstruct test_entry *entry;\n> >> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> >> +\t\t\t\t     { \"key2\", \"value2\" },\n> >> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n> >> +\n> >> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> >> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> >> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> >> +\t}\n> >> +\n> >> +\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n> >> +\tentry = hashmap_put_entry(map, entry, ent);\n> >> +\tcheck(ignore_case ? entry != NULL : entry == NULL);\n> >> +\tfree(entry);\n> >> +\n> >> +\tcheck_int(map->tablesize, ==, 64);\n> >> +\tcheck_int(hashmap_get_size(map), ==,\n> >> +\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n> >> +}\n> >\n> > Ahhh, so you're using the same function for both case-sensitive and\n> > -insensitive tests. So I guess TEST_RUN isn't useful here after all.\n> > Personally I'd still rather get rid of setup(), but I don't feel super\n> > strongly about it.\n>\n> Consulting the table with \"fooBarFrotz\" and checking what gets\n> returned (expect \"value3\" for !icase, or \"value4\" for icase) is one\n> of the things that are missing. In fact, the values stored are not\n> even checked with the above test at all.\n\nYeah, I tried to replicate the 'put' tests from the shellscript.\nHowever, t_get() does the same thing as you mentioned below, so I'll\njust remove it and rename t_get() to t_put_get(), and add some\nmissing checks from t_put() to t_put_get().\n\n>\n> >> +static void t_replace(struct hashmap *map, int ignore_case)\n> >> +{\n> >> +\tstruct test_entry *entry;\n> >> +\n> >> +\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n> >> +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> >> +\n> >> +\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n> >> +\t\t\t\t \"value2\");\n> >> +\tentry = hashmap_put_entry(map, entry, ent);\n> >> +\tif (check(entry != NULL))\n> >> +\t\tcheck_str(get_value(entry), \"value1\");\n> >> +\tfree(entry);\n> >> +\n> >> +\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n> >> +\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> >> +\n> >> +\tentry = alloc_test_entry(ignore_case,\n> >> +\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n> >> +\t\t\t\t \"value4\");\n>\n> Curious. If the hashmap is set up for icase use, do callers still\n> need to downcase the key? Shouldn't the library take care of that?\n> After all, test_entry_cmp() when the hashmap is being used in icase\n> mode does strcasecmp() anyway.\n\nOf course we don't need to downcase. But the idea is to insert\n\"fooBarFrotz\" then insert \"foobarfrotz\" to check if it is considered the\nsame or not. In a way, it is also testing test_entry_cmp() and\nalloc_test_entry(). If we pass \"fooBarFrotz\" both the times\nwe can't be sure if it downcases or not, as both are same, we'd\npass even if it didn't downcase. After all, we are testing the\nlibrary rather than using it.\n\nThanks.\n\n>\n> >> +\tentry = hashmap_put_entry(map, entry, ent);\n> >> +\tif (check(entry != NULL))\n> >> +\t\tcheck_str(get_value(entry), \"value3\");\n>\n> Here the stored value is checked, which is good.\n>\n> >> +\tfree(entry);\n> >> +}\n> >> +\n> >> +static void t_get(struct hashmap *map, int ignore_case)\n> >> +{\n> >> +\tstruct test_entry *entry;\n> >> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> >> +\t\t\t\t     { \"key2\", \"value2\" },\n> >> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n> >> +\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n> >> +\tconst char *query[][2] = {\n> >> +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> >> +\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n> >> +\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n> >> +\t};\n> >> +\n> >> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> >> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> >> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> >> +\t}\n> >> +\n> >> +\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n> >> +\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n> >> +\t\tif (check(entry != NULL))\n> >> +\t\t\tcheck_str(get_value(entry), query[i][1]);\n> >> +\t}\n> >> +\n> >> +\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n> >> +}\n>\n> It is getting dubious if it is worth having t_put, when t_get does\n> so similar things and more.\n>\n> Same comment applies wrt icase, and not limited to t_get() but all\n> the remaining test functions (elided).\n>\n> Thanks.\n\n"},{"id":"498531","messageId":"D2MXS5A9M8RF.Z7ACS9BB2NOC@gmail.com","threadId":"61692","inReplyTo":"2d7fd61d-0b2c-465f-9f24-47c99ab5b56c@gmail.com","subject":"Re: [GSoC][PATCH v2] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-07-11T18:54:48Z","receivedAt":"2024-07-11T18:54:53Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> wrote:\n> On 09/07/2024 20:34, Josh Steadmon wrote:\n> > On 2024.07.08 21:45, Ghanshyam Thakkar wrote:\n>\n> >> +static void t_put(struct hashmap *map, int ignore_case)\n> >> +{\n> >> +\tstruct test_entry *entry;\n> >> +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n> >> +\t\t\t\t     { \"key2\", \"value2\" },\n> >> +\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n> >> +\n> >> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> >> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> >> +\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n> >> +\t}\n> >> +\n> >> +\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n> >> +\tentry = hashmap_put_entry(map, entry, ent);\n> >> +\tcheck(ignore_case ? entry != NULL : entry == NULL);\n> >> +\tfree(entry);\n> >> +\n> >> +\tcheck_int(map->tablesize, ==, 64);\n> >> +\tcheck_int(hashmap_get_size(map), ==,\n> >> +\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n> >> +}\n> > \n> > Ahhh, so you're using the same function for both case-sensitive and\n> > -insensitive tests. So I guess TEST_RUN isn't useful here after all.\n> > Personally I'd still rather get rid of setup(), but I don't feel super\n> > strongly about it.\n>\n> I'm not sure - we have to pass ignore_case to HASHMAP_INIT and the test\n> function so using setup() means we cannot pass different values to the\n> two different functions.\n>\n> For parameterized tests where we calling the same function with\n> different inputs using a setup function allows us to\n> - write more concise test code\n> - easily change the setup of all the tests if the api changes in the\n> future.\n> - consistently free resources at the end of a test making it easier to\n> write leak-free tests\n> - assert pre- and post- conditions on all tests\n>\n> Using TEST_RUN is useful to declare the different inputs for each test.\n> For example in the oid-array tests it allows us to write\n>\n> TEST_RUN(\"ordered enumeration\") {\n> const char *input[] = { \"88\", \"44\", \"aa\", \"55\" };\n> const char *expected[] = { \"44\", \"55\", \"88\", \"aa\" };\n>\n> TEST_ENUMERATION(input, expected)\n> }\n>\n> rather than declaring a bunch of variables up front a long way from\n> where they are used.\n\nYeah, I changed my mind from removing setup(). I'll keep it as it does\nnot have anything complex to make manual verifying harder. Also, since\nthis test does not have any variable inputs, we can just use TEST(),\nalthough TEST_RUN() with variable inputs is very useful in other tests.\n\n> >> +static void t_add(struct hashmap *map, int ignore_case)\n> >> +{\n> >> +\tstruct test_entry *entry;\n> >> +\tconst char *key_val[][3] = {\n> >> +\t\t{ \"key1\", \"value1\", \"UNUSED\" },\n> >> +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n> >> +\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n> >> +\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n> >> +\t};\n> >> +\tconst char *queries[] = { \"key1\",\n> >> +\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n> >> +\n> >> +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> >> +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> >> +\t\thashmap_add(map, &entry->ent);\n> >> +\t}\n> >> +\n> >> +\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n> > \n> > Since we only have one query, can we remove the loop and simplify the\n> > following block of code?\n> > \n> > Also (here and elsewhere), it might be less confusing to say \"UNSEEN\" /\n> > \"SEEN\" instead of \"UNUSED\" / \"USED\". The latter makes it sound to me\n> > like there's some API requirement to have a 3-item array that we don't\n> > actually need, but in this case those fields are actually used in\n> > key_val_contains() to track duplicates.\n>\n> The third element just needs to be a boolean flag so it might be better\n> to use a struct\n>\n> const struct {\n> char *key;\n> char *val;\n> char seen;\n> } key_val[] = {\n> { .key = \"key1\", .val = \"value1\" },\n> { .key = ignore_case ? \"Key1\" : \"key1\" .val = \"value2\" }, { .key =\n> \"fooBarFrotz\" .val = \"value3\" },\n> { .key = ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", .value = \"value4\"\n> }\n> };\n\nI think we can just use an extra 'char seen[]' for markings and modify\nkey_val_contains() to have this parameter, instead of having it as a\nstruct.\n\nThanks.\n\n"},{"id":"498554","messageId":"20240711235159.5320-1-shyamthakkar001@gmail.com","threadId":"61692","inReplyTo":"20240708161641.10335-2-shyamthakkar001@gmail.com","subject":"[GSoC][PATCH v3] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-07-11T23:51:42Z","receivedAt":"2024-07-11T23:52:29Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\nlibrary. Migrate them to the unit testing framework for better\ndebugging, runtime performance and concise code.\n\nAlong with the migration, make 'add' tests from the shellscript order\nagnostic in unit tests, since they iterate over entries with the same\nkeys and we do not guarantee the order.\n\nThe helper/test-hashmap.c is still not removed because it contains a\nperformance test meant to be run by the user directly (not used in\nt/perf). And it makes sense for such a utility to be a helper.\n\nMentored-by: Christian Couder <chriscool@tuxfamily.org>\nMentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\nHelped-by: Josh Steadmon <steadmon@google.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n---\nRange-diff against v2:\n1:  bbb4f2f23e ! 1:  03ba77665e t: port helper/test-hashmap.c to unit-tests/t-hashmap.c\n    @@ Commit message\n     \n         helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\n         library. Migrate them to the unit testing framework for better\n    -    debugging, runtime performance and consice code.\n    +    debugging, runtime performance and concise code.\n     \n         Along with the migration, make 'add' tests from the shellscript order\n         agnostic in unit tests, since they iterate over entries with the same\n    @@ Commit message\n     \n         Mentored-by: Christian Couder <chriscool@tuxfamily.org>\n         Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\n    +    Helped-by: Josh Steadmon <steadmon@google.com>\n    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n         Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n     \n      ## Makefile ##\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\tstruct test_entry, ent);\n     +}\n     +\n    -+static int key_val_contains(const char *key_val[][3], size_t n,\n    ++static int key_val_contains(const char *key_val[][2], char seen[], size_t n,\n     +\t\t\t    struct test_entry *entry)\n     +{\n     +\tfor (size_t i = 0; i < n; i++) {\n     +\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n     +\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n    -+\t\t\tif (!strcmp(key_val[i][2], \"USED\"))\n    ++\t\t\tif (seen[i])\n     +\t\t\t\treturn 2;\n    -+\t\t\tkey_val[i][2] = \"USED\";\n    ++\t\t\tseen[i] = 1;\n     +\t\t\treturn 0;\n     +\t\t}\n     +\t}\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\thashmap_clear_and_free(&map, struct test_entry, ent);\n     +}\n     +\n    -+static void t_put(struct hashmap *map, int ignore_case)\n    -+{\n    -+\tstruct test_entry *entry;\n    -+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n    -+\t\t\t\t     { \"key2\", \"value2\" },\n    -+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n    -+\n    -+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n    -+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    -+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    -+\t}\n    -+\n    -+\tentry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n    -+\tentry = hashmap_put_entry(map, entry, ent);\n    -+\tcheck(ignore_case ? entry != NULL : entry == NULL);\n    -+\tfree(entry);\n    -+\n    -+\tcheck_int(map->tablesize, ==, 64);\n    -+\tcheck_int(hashmap_get_size(map), ==,\n    -+\t\t  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n    -+}\n    -+\n     +static void t_replace(struct hashmap *map, int ignore_case)\n     +{\n     +\tstruct test_entry *entry;\n     +\n     +\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n    -+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    ++\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\n     +\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n     +\t\t\t\t \"value2\");\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tfree(entry);\n     +\n     +\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n    -+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    ++\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\n     +\tentry = alloc_test_entry(ignore_case,\n     +\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n     +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    -+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    ++\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t}\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n     +\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n     +\t\tif (check(entry != NULL))\n     +\t\t\tcheck_str(get_value(entry), query[i][1]);\n    ++\t\telse\n    ++\t\t\ttest_msg(\"query key: %s\", query[i][0]);\n     +\t}\n     +\n    -+\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n    ++\tcheck_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n    ++\tcheck_int(map->tablesize, ==, 64);\n    ++\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n     +}\n     +\n     +static void t_add(struct hashmap *map, int ignore_case)\n     +{\n     +\tstruct test_entry *entry;\n    -+\tconst char *key_val[][3] = {\n    -+\t\t{ \"key1\", \"value1\", \"UNUSED\" },\n    -+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n    -+\t\t{ \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n    -+\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n    ++\tconst char *key_val[][2] = {\n    ++\t\t{ \"key1\", \"value1\" },\n    ++\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n    ++\t\t{ \"fooBarFrotz\", \"value3\" },\n    ++\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\" }\n     +\t};\n     +\tconst char *queries[] = { \"key1\",\n     +\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n    ++\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n     +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\t{\n     +\t\t\tint ret;\n     +\t\t\tif (!check_int((ret = key_val_contains(\n    -+\t\t\t\t\t\tkey_val, ARRAY_SIZE(key_val),\n    -+\t\t\t\t\t\tentry)), ==, 0)) {\n    ++\t\t\t\t\t\tkey_val, seen,\n    ++\t\t\t\t\t\tARRAY_SIZE(key_val), entry)),\n    ++\t\t\t\t       ==, 0)) {\n     +\t\t\t\tswitch (ret) {\n     +\t\t\t\tcase 1:\n     +\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\t}\n     +\t\tcheck_int(count, ==, 2);\n     +\t}\n    ++\n    ++\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n    ++\t\tif (!check_int(seen[i], ==, 1))\n    ++\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n    ++\t\t\t\t \"    key: %s\\n  value: %s\",\n    ++\t\t\t\t key_val[i][0], key_val[i][1]);\n    ++\t}\n    ++\n     +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n    -+\tcheck(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n    ++\tcheck_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n     +}\n     +\n     +static void t_remove(struct hashmap *map, int ignore_case)\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n     +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    -+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    ++\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t}\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t}\n     +\n     +\tentry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n    -+\tcheck(hashmap_remove_entry(map, entry, ent, \"notInMap\") == NULL);\n    ++\tcheck_pointer_eq(hashmap_remove_entry(map, entry, ent, \"notInMap\"), NULL);\n     +\tfree(entry);\n    ++\n    ++\tcheck_int(map->tablesize, ==, 64);\n    ++\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));\n     +}\n     +\n     +static void t_iterate(struct hashmap *map, int ignore_case)\n     +{\n     +\tstruct test_entry *entry;\n     +\tstruct hashmap_iter iter;\n    -+\tconst char *key_val[][3] = { { \"key1\", \"value1\", \"UNUSED\" },\n    -+\t\t\t\t     { \"key2\", \"value2\", \"UNUSED\" },\n    -+\t\t\t\t     { \"fooBarFrotz\", \"value3\", \"UNUSED\" } };\n    -+\tint count = 0;\n    ++\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n    ++\t\t\t\t     { \"key2\", \"value2\" },\n    ++\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n    ++\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n     +\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    -+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    ++\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t}\n     +\n     +\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n     +\t{\n     +\t\tint ret;\n    -+\t\tif (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n    ++\t\tif (!check_int((ret = key_val_contains(key_val, seen,\n    ++\t\t\t\t\t\t       ARRAY_SIZE(key_val),\n     +\t\t\t\t\t\t       entry)), ==, 0)) {\n     +\t\t\tswitch (ret) {\n     +\t\t\tcase 1:\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\t\t\t\t entry->key, get_value(entry));\n     +\t\t\t\tbreak;\n     +\t\t\t}\n    -+\t\t} else {\n    -+\t\t\tcount++;\n     +\t\t}\n     +\t}\n    -+\tcheck_int(count, ==, ARRAY_SIZE(key_val));\n    ++\n    ++\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n    ++\t\tif (!check_int(seen[i], ==, 1))\n    ++\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n    ++\t\t\t\t \"    key: %s\\n  value: %s\",\n    ++\t\t\t\t key_val[i][0], key_val[i][1]);\n    ++\t}\n    ++\n     +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n     +}\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\tchar *key = xstrfmt(\"key%d\", i);\n     +\t\tchar *value = xstrfmt(\"value%d\", i);\n     +\t\tentry = alloc_test_entry(ignore_case, key, value);\n    -+\t\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    ++\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t\tfree(key);\n     +\t\tfree(value);\n     +\t}\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tcheck_int(hashmap_get_size(map), ==, 51);\n     +\n     +\tentry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n    -+\tcheck(hashmap_put_entry(map, entry, ent) == NULL);\n    ++\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\tcheck_int(map->tablesize, ==, 256);\n     +\tcheck_int(hashmap_get_size(map), ==, 52);\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\telse if (!check(i1 != values[i]))\n     +\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n     +\t\t\t\t values[i]);\n    -+\t\telse if (!check(i1 == i2))\n    ++\t\telse if (!check_pointer_eq(i1, i2))\n     +\t\t\ttest_msg(\"address('%s') != address('%s'), so strintern('%s') != strintern('%s')\",\n     +\t\t\t\t i1, i2, values[i], values[i]);\n     +\t\telse\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\n     +int cmd_main(int argc UNUSED, const char **argv UNUSED)\n     +{\n    -+\tTEST(setup(t_put, 0), \"put works\");\n    -+\tTEST(setup(t_put, 1), \"put (case insensitive) works\");\n     +\tTEST(setup(t_replace, 0), \"replace works\");\n     +\tTEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n     +\tTEST(setup(t_get, 0), \"get works\");\n\n Makefile                 |   1 +\n t/helper/test-hashmap.c  | 100 +----------\n t/t0011-hashmap.sh       | 260 ----------------------------\n t/unit-tests/t-hashmap.c | 358 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 361 insertions(+), 358 deletions(-)\n delete mode 100755 t/t0011-hashmap.sh\n create mode 100644 t/unit-tests/t-hashmap.c\n\ndiff --git a/Makefile b/Makefile\nindex 3eab701b10..74bb026610 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n+UNIT_TEST_PROGRAMS += t-hashmap\n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-oidtree\n UNIT_TEST_PROGRAMS += t-prio-queue\ndiff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c\nindex 2912899558..7b854a7030 100644\n--- a/t/helper/test-hashmap.c\n+++ b/t/helper/test-hashmap.c\n@@ -12,11 +12,6 @@ struct test_entry\n \tchar key[FLEX_ARRAY];\n };\n \n-static const char *get_value(const struct test_entry *e)\n-{\n-\treturn e->key + strlen(e->key) + 1;\n-}\n-\n static int test_entry_cmp(const void *cmp_data,\n \t\t\t  const struct hashmap_entry *eptr,\n \t\t\t  const struct hashmap_entry *entry_or_key,\n@@ -141,30 +136,16 @@ static void perf_hashmap(unsigned int method, unsigned int rounds)\n /*\n  * Read stdin line by line and print result of commands to stdout:\n  *\n- * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)\n- * put key value -> NULL / old value\n- * get key -> NULL / value\n- * remove key -> NULL / old value\n- * iterate -> key1 value1\\nkey2 value2\\n...\n- * size -> tablesize numentries\n- *\n  * perfhashmap method rounds -> test hashmap.[ch] performance\n  */\n int cmd__hashmap(int argc, const char **argv)\n {\n \tstruct string_list parts = STRING_LIST_INIT_NODUP;\n \tstruct strbuf line = STRBUF_INIT;\n-\tint icase;\n-\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);\n-\n-\t/* init hash map */\n-\ticase = argc > 1 && !strcmp(\"ignorecase\", argv[1]);\n \n \t/* process commands from stdin */\n \twhile (strbuf_getline(&line, stdin) != EOF) {\n \t\tchar *cmd, *p1, *p2;\n-\t\tunsigned int hash = 0;\n-\t\tstruct test_entry *entry;\n \n \t\t/* break line into command and up to two parameters */\n \t\tstring_list_setlen(&parts, 0);\n@@ -180,84 +161,8 @@ int cmd__hashmap(int argc, const char **argv)\n \t\tcmd = parts.items[0].string;\n \t\tp1 = parts.nr >= 1 ? parts.items[1].string : NULL;\n \t\tp2 = parts.nr >= 2 ? parts.items[2].string : NULL;\n-\t\tif (p1)\n-\t\t\thash = icase ? strihash(p1) : strhash(p1);\n-\n-\t\tif (!strcmp(\"add\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add to hashmap */\n-\t\t\thashmap_add(&map, &entry->ent);\n-\n-\t\t} else if (!strcmp(\"put\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add / replace entry */\n-\t\t\tentry = hashmap_put_entry(&map, entry, ent);\n-\n-\t\t\t/* print and free replaced entry, if any */\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"get\", cmd) && p1) {\n-\t\t\t/* lookup entry in hashmap */\n-\t\t\tentry = hashmap_get_entry_from_hash(&map, hash, p1,\n-\t\t\t\t\t\t\tstruct test_entry, ent);\n-\n-\t\t\t/* print result */\n-\t\t\tif (!entry)\n-\t\t\t\tputs(\"NULL\");\n-\t\t\thashmap_for_each_entry_from(&map, entry, ent)\n-\t\t\t\tputs(get_value(entry));\n-\n-\t\t} else if (!strcmp(\"remove\", cmd) && p1) {\n-\n-\t\t\t/* setup static key */\n-\t\t\tstruct hashmap_entry key;\n-\t\t\tstruct hashmap_entry *rm;\n-\t\t\thashmap_entry_init(&key, hash);\n-\n-\t\t\t/* remove entry from hashmap */\n-\t\t\trm = hashmap_remove(&map, &key, p1);\n-\t\t\tentry = rm ? container_of(rm, struct test_entry, ent)\n-\t\t\t\t\t: NULL;\n-\n-\t\t\t/* print result and free entry*/\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"iterate\", cmd)) {\n-\t\t\tstruct hashmap_iter iter;\n-\n-\t\t\thashmap_for_each_entry(&map, &iter, entry,\n-\t\t\t\t\t\tent /* member name */)\n-\t\t\t\tprintf(\"%s %s\\n\", entry->key, get_value(entry));\n-\n-\t\t} else if (!strcmp(\"size\", cmd)) {\n-\n-\t\t\t/* print table sizes */\n-\t\t\tprintf(\"%u %u\\n\", map.tablesize,\n-\t\t\t       hashmap_get_size(&map));\n-\n-\t\t} else if (!strcmp(\"intern\", cmd) && p1) {\n-\n-\t\t\t/* test that strintern works */\n-\t\t\tconst char *i1 = strintern(p1);\n-\t\t\tconst char *i2 = strintern(p1);\n-\t\t\tif (strcmp(i1, p1))\n-\t\t\t\tprintf(\"strintern(%s) returns %s\\n\", p1, i1);\n-\t\t\telse if (i1 == p1)\n-\t\t\t\tprintf(\"strintern(%s) returns input pointer\\n\", p1);\n-\t\t\telse if (i1 != i2)\n-\t\t\t\tprintf(\"strintern(%s) != strintern(%s)\", i1, i2);\n-\t\t\telse\n-\t\t\t\tprintf(\"%s\\n\", i1);\n-\n-\t\t} else if (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n+\t\n+\t\tif (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n \n \t\t\tperf_hashmap(atoi(p1), atoi(p2));\n \n@@ -270,6 +175,5 @@ int cmd__hashmap(int argc, const char **argv)\n \n \tstring_list_clear(&parts, 0);\n \tstrbuf_release(&line);\n-\thashmap_clear_and_free(&map, struct test_entry, ent);\n \treturn 0;\n }\ndiff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh\ndeleted file mode 100755\nindex 46e74ad107..0000000000\n--- a/t/t0011-hashmap.sh\n+++ /dev/null\n@@ -1,260 +0,0 @@\n-#!/bin/sh\n-\n-test_description='test hashmap and string hash functions'\n-\n-TEST_PASSES_SANITIZE_LEAK=true\n-. ./test-lib.sh\n-\n-test_hashmap() {\n-\techo \"$1\" | test-tool hashmap $3 > actual &&\n-\techo \"$2\" > expect &&\n-\ttest_cmp expect actual\n-}\n-\n-test_expect_success 'put' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-NULL\n-NULL\n-NULL\n-64 4\"\n-\n-'\n-\n-test_expect_success 'put (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-size\" \"NULL\n-NULL\n-NULL\n-64 3\" ignorecase\n-\n-'\n-\n-test_expect_success 'replace' '\n-\n-test_hashmap \"put key1 value1\n-put key1 value2\n-put fooBarFrotz value3\n-put fooBarFrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\"\n-\n-'\n-\n-test_expect_success 'replace (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put Key1 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\" ignorecase\n-\n-'\n-\n-test_expect_success 'get' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-get key1\n-get key2\n-get fooBarFrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'get (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-get Key1\n-get keY2\n-get foobarfrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'add' '\n-\n-test_hashmap \"add key1 value1\n-add key1 value2\n-add fooBarFrotz value3\n-add fooBarFrotz value4\n-get key1\n-get fooBarFrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'add (case insensitive)' '\n-\n-test_hashmap \"add key1 value1\n-add Key1 value2\n-add fooBarFrotz value3\n-add foobarfrotz value4\n-get key1\n-get Foobarfrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'remove' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove key1\n-remove key2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\"\n-\n-'\n-\n-test_expect_success 'remove (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove Key1\n-remove keY2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\" ignorecase\n-\n-'\n-\n-test_expect_success 'iterate' '\n-\ttest-tool hashmap >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'iterate (case insensitive)' '\n-\ttest-tool hashmap ignorecase >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'grow / shrink' '\n-\n-\trm -f in &&\n-\trm -f expect &&\n-\tfor n in $(test_seq 51)\n-\tdo\n-\t\techo put key$n value$n >> in &&\n-\t\techo NULL >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 64 51 >> expect &&\n-\techo put key52 value52 >> in &&\n-\techo NULL >> expect &&\n-\techo size >> in &&\n-\techo 256 52 >> expect &&\n-\tfor n in $(test_seq 12)\n-\tdo\n-\t\techo remove key$n >> in &&\n-\t\techo value$n >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 256 40 >> expect &&\n-\techo remove key40 >> in &&\n-\techo value40 >> expect &&\n-\techo size >> in &&\n-\techo 64 39 >> expect &&\n-\ttest-tool hashmap <in >out &&\n-\ttest_cmp expect out\n-\n-'\n-\n-test_expect_success 'string interning' '\n-\n-test_hashmap \"intern value1\n-intern Value1\n-intern value2\n-intern value2\n-\" \"value1\n-Value1\n-value2\n-value2\"\n-\n-'\n-\n-test_done\ndiff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\nnew file mode 100644\nindex 0000000000..3112b10b33\n--- /dev/null\n+++ b/t/unit-tests/t-hashmap.c\n@@ -0,0 +1,358 @@\n+#include \"test-lib.h\"\n+#include \"hashmap.h\"\n+#include \"strbuf.h\"\n+\n+struct test_entry {\n+\tint padding; /* hashmap entry no longer needs to be the first member */\n+\tstruct hashmap_entry ent;\n+\t/* key and value as two \\0-terminated strings */\n+\tchar key[FLEX_ARRAY];\n+};\n+\n+static int test_entry_cmp(const void *cmp_data,\n+\t\t\t  const struct hashmap_entry *eptr,\n+\t\t\t  const struct hashmap_entry *entry_or_key,\n+\t\t\t  const void *keydata)\n+{\n+\tconst int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n+\tconst struct test_entry *e1, *e2;\n+\tconst char *key = keydata;\n+\n+\te1 = container_of(eptr, const struct test_entry, ent);\n+\te2 = container_of(entry_or_key, const struct test_entry, ent);\n+\n+\tif (ignore_case)\n+\t\treturn strcasecmp(e1->key, key ? key : e2->key);\n+\telse\n+\t\treturn strcmp(e1->key, key ? key : e2->key);\n+}\n+\n+static const char *get_value(const struct test_entry *e)\n+{\n+\treturn e->key + strlen(e->key) + 1;\n+}\n+\n+static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n+\t\t\t\t\t   const char *key, const char *value)\n+{\n+\tsize_t klen = strlen(key);\n+\tsize_t vlen = strlen(value);\n+\tunsigned int hash = ignore_case ? strihash(key) : strhash(key);\n+\tstruct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n+\n+\thashmap_entry_init(&entry->ent, hash);\n+\tmemcpy(entry->key, key, klen + 1);\n+\tmemcpy(entry->key + klen + 1, value, vlen + 1);\n+\treturn entry;\n+}\n+\n+static struct test_entry *get_test_entry(struct hashmap *map,\n+\t\t\t\t\t unsigned int ignore_case, const char *key)\n+{\n+\treturn hashmap_get_entry_from_hash(\n+\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n+\t\tstruct test_entry, ent);\n+}\n+\n+static int key_val_contains(const char *key_val[][2], char seen[], size_t n,\n+\t\t\t    struct test_entry *entry)\n+{\n+\tfor (size_t i = 0; i < n; i++) {\n+\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n+\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n+\t\t\tif (seen[i])\n+\t\t\t\treturn 2;\n+\t\t\tseen[i] = 1;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n+static void setup(void (*f)(struct hashmap *map, int ignore_case),\n+\t\t  int ignore_case)\n+{\n+\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n+\n+\tf(&map, ignore_case);\n+\thashmap_clear_and_free(&map, struct test_entry, ent);\n+}\n+\n+static void t_replace(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\n+\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\n+\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n+\t\t\t\t \"value2\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value1\");\n+\tfree(entry);\n+\n+\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\n+\tentry = alloc_test_entry(ignore_case,\n+\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n+\t\t\t\t \"value4\");\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value3\");\n+\tfree(entry);\n+}\n+\n+static void t_get(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n+\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n+\tconst char *query[][2] = {\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n+\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n+\t};\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n+\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n+\t\tif (check(entry != NULL))\n+\t\t\tcheck_str(get_value(entry), query[i][1]);\n+\t\telse\n+\t\t\ttest_msg(\"query key: %s\", query[i][0]);\n+\t}\n+\n+\tcheck_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+}\n+\n+static void t_add(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = {\n+\t\t{ \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n+\t\t{ \"fooBarFrotz\", \"value3\" },\n+\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\" }\n+\t};\n+\tconst char *queries[] = { \"key1\",\n+\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n+\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\thashmap_add(map, &entry->ent);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n+\t\tint count = 0;\n+\t\tentry = hashmap_get_entry_from_hash(map,\n+\t\t\tignore_case ? strihash(queries[i]) :\n+\t\t\t\t      strhash(queries[i]),\n+\t\t\tqueries[i], struct test_entry, ent);\n+\n+\t\thashmap_for_each_entry_from(map, entry, ent)\n+\t\t{\n+\t\t\tint ret;\n+\t\t\tif (!check_int((ret = key_val_contains(\n+\t\t\t\t\t\tkey_val, seen,\n+\t\t\t\t\t\tARRAY_SIZE(key_val), entry)),\n+\t\t\t\t       ==, 0)) {\n+\t\t\t\tswitch (ret) {\n+\t\t\t\tcase 1:\n+\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 2:\n+\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tcount++;\n+\t\t\t}\n+\t\t}\n+\t\tcheck_int(count, ==, 2);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n+\t\tif (!check_int(seen[i], ==, 1))\n+\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n+\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t key_val[i][0], key_val[i][1]);\n+\t}\n+\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+\tcheck_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n+}\n+\n+static void t_remove(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, remove[i][0], \"\");\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(get_value(removed), remove[i][1]);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\n+\tentry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n+\tcheck_pointer_eq(hashmap_remove_entry(map, entry, ent, \"notInMap\"), NULL);\n+\tfree(entry);\n+\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));\n+}\n+\n+static void t_iterate(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tstruct hashmap_iter iter;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n+\t{\n+\t\tint ret;\n+\t\tif (!check_int((ret = key_val_contains(key_val, seen,\n+\t\t\t\t\t\t       ARRAY_SIZE(key_val),\n+\t\t\t\t\t\t       entry)), ==, 0)) {\n+\t\t\tswitch (ret) {\n+\t\t\tcase 1:\n+\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\tcase 2:\n+\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n+\t\tif (!check_int(seen[i], ==, 1))\n+\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n+\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t key_val[i][0], key_val[i][1]);\n+\t}\n+\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+}\n+\n+static void t_alloc(struct hashmap *map, int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\n+\tfor (int i = 1; i <= 51; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\t\tentry = alloc_test_entry(ignore_case, key, value);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t\tfree(key);\n+\t\tfree(value);\n+\t}\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 51);\n+\n+\tentry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 52);\n+\n+\tfor (int i = 1; i <= 12; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\n+\t\tentry = alloc_test_entry(ignore_case, key, \"\");\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(value, get_value(removed));\n+\t\tfree(key);\n+\t\tfree(value);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 40);\n+\n+\tentry = alloc_test_entry(ignore_case, \"key40\", \"\");\n+\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n+\tif (check(removed != NULL))\n+\t\tcheck_str(\"value40\", get_value(removed));\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 39);\n+\tfree(entry);\n+\tfree(removed);\n+}\n+\n+static void t_intern(struct hashmap *map, int ignore_case)\n+{\n+\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n+\t\tconst char *i1 = strintern(values[i]);\n+\t\tconst char *i2 = strintern(values[i]);\n+\n+\t\tif (!check(!strcmp(i1, values[i])))\n+\t\t\ttest_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n+\t\telse if (!check(i1 != values[i]))\n+\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n+\t\t\t\t values[i]);\n+\t\telse if (!check_pointer_eq(i1, i2))\n+\t\t\ttest_msg(\"address('%s') != address('%s'), so strintern('%s') != strintern('%s')\",\n+\t\t\t\t i1, i2, values[i], values[i]);\n+\t\telse\n+\t\t\tcheck_str(i1, values[i]);\n+\t}\n+}\n+\n+int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+{\n+\tTEST(setup(t_replace, 0), \"replace works\");\n+\tTEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n+\tTEST(setup(t_get, 0), \"get works\");\n+\tTEST(setup(t_get, 1), \"get (case insensitive) works\");\n+\tTEST(setup(t_add, 0), \"add works\");\n+\tTEST(setup(t_add, 1), \"add (case insensitive) works\");\n+\tTEST(setup(t_remove, 0), \"remove works\");\n+\tTEST(setup(t_remove, 1), \"remove (case insensitive) works\");\n+\tTEST(setup(t_iterate, 0), \"iterate works\");\n+\tTEST(setup(t_iterate, 1), \"iterate (case insensitive) works\");\n+\tTEST(setup(t_alloc, 0), \"grow / shrink works\");\n+\tTEST(setup(t_intern, 0), \"string interning works\");\n+\treturn test_done();\n+}\n-- \n2.45.2\n\n"},{"id":"498555","messageId":"D2N4AG0P10QT.2EMBZAPNDBSP0@gmail.com","threadId":"61692","inReplyTo":"20240711235159.5320-1-shyamthakkar001@gmail.com","subject":"Re: [GSoC][PATCH v3] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-07-12T00:00:48Z","receivedAt":"2024-07-12T00:00:55Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"Ghanshyam Thakkar <shyamthakkar001@gmail.com> wrote:\n> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\n> library. Migrate them to the unit testing framework for better\n> debugging, runtime performance and concise code.\n>\n> Along with the migration, make 'add' tests from the shellscript order\n> agnostic in unit tests, since they iterate over entries with the same\n> keys and we do not guarantee the order.\n>\n> The helper/test-hashmap.c is still not removed because it contains a\n> performance test meant to be run by the user directly (not used in\n> t/perf). And it makes sense for such a utility to be a helper.\n>\n> Mentored-by: Christian Couder <chriscool@tuxfamily.org>\n> Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\n> Helped-by: Josh Steadmon <steadmon@google.com>\n> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n> Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n> ---\nChanges in v3:\n\n- I have removed the t_put() tests as they were identical to\n  t_get() minus the size checks at the end. So, I've added those size\n  checks to t_get().\n\n- Besides that I've replaced check() with check_pointer_eq() where ever\n  applicable.\n  \n- And replaced the third elements of key_val[] which recorded\n  the presence of certain key-val pair with 'char seen[]'.\n\n- Added some test_msg() statements for better debugging.\n\n- Fixed typo in commit message.\n\nThanks.\n\n> Range-diff against v2:\n> 1: bbb4f2f23e ! 1: 03ba77665e t: port helper/test-hashmap.c to\n> unit-tests/t-hashmap.c\n> @@ Commit message\n>      \n> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\n> library. Migrate them to the unit testing framework for better\n> - debugging, runtime performance and consice code.\n> + debugging, runtime performance and concise code.\n>      \n> Along with the migration, make 'add' tests from the shellscript order\n> agnostic in unit tests, since they iterate over entries with the same\n> @@ Commit message\n>      \n> Mentored-by: Christian Couder <chriscool@tuxfamily.org>\n> Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\n> + Helped-by: Josh Steadmon <steadmon@google.com>\n> + Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n> Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n>      \n> ## Makefile ##\n> @@ t/unit-tests/t-hashmap.c (new)\n> + struct test_entry, ent);\n> +}\n> +\n> -+static int key_val_contains(const char *key_val[][3], size_t n,\n> ++static int key_val_contains(const char *key_val[][2], char seen[],\n> size_t n,\n> + struct test_entry *entry)\n> +{\n> + for (size_t i = 0; i < n; i++) {\n> + if (!strcmp(entry->key, key_val[i][0]) &&\n> + !strcmp(get_value(entry), key_val[i][1])) {\n> -+ if (!strcmp(key_val[i][2], \"USED\"))\n> ++ if (seen[i])\n> + return 2;\n> -+ key_val[i][2] = \"USED\";\n> ++ seen[i] = 1;\n> + return 0;\n> + }\n> + }\n> @@ t/unit-tests/t-hashmap.c (new)\n> + hashmap_clear_and_free(&map, struct test_entry, ent);\n> +}\n> +\n> -+static void t_put(struct hashmap *map, int ignore_case)\n> -+{\n> -+ struct test_entry *entry;\n> -+ const char *key_val[][2] = { { \"key1\", \"value1\" },\n> -+ { \"key2\", \"value2\" },\n> -+ { \"fooBarFrotz\", \"value3\" } };\n> -+\n> -+ for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> -+ entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> -+ }\n> -+\n> -+ entry = alloc_test_entry(ignore_case, \"foobarfrotz\", \"value4\");\n> -+ entry = hashmap_put_entry(map, entry, ent);\n> -+ check(ignore_case ? entry != NULL : entry == NULL);\n> -+ free(entry);\n> -+\n> -+ check_int(map->tablesize, ==, 64);\n> -+ check_int(hashmap_get_size(map), ==,\n> -+ ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);\n> -+}\n> -+\n> +static void t_replace(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry;\n> +\n> + entry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> +\n> + entry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n> + \"value2\");\n> @@ t/unit-tests/t-hashmap.c (new)\n> + free(entry);\n> +\n> + entry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> +\n> + entry = alloc_test_entry(ignore_case,\n> + ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n> @@ t/unit-tests/t-hashmap.c (new)\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + }\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n> + entry = get_test_entry(map, ignore_case, query[i][0]);\n> + if (check(entry != NULL))\n> + check_str(get_value(entry), query[i][1]);\n> ++ else\n> ++ test_msg(\"query key: %s\", query[i][0]);\n> + }\n> +\n> -+ check(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n> ++ check_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n> ++ check_int(map->tablesize, ==, 64);\n> ++ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +}\n> +\n> +static void t_add(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry;\n> -+ const char *key_val[][3] = {\n> -+ { \"key1\", \"value1\", \"UNUSED\" },\n> -+ { ignore_case ? \"Key1\" : \"key1\", \"value2\", \"UNUSED\" },\n> -+ { \"fooBarFrotz\", \"value3\", \"UNUSED\" },\n> -+ { ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\", \"UNUSED\" }\n> ++ const char *key_val[][2] = {\n> ++ { \"key1\", \"value1\" },\n> ++ { ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n> ++ { \"fooBarFrotz\", \"value3\" },\n> ++ { ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\" }\n> + };\n> + const char *queries[] = { \"key1\",\n> + ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n> ++ char seen[ARRAY_SIZE(key_val)] = { 0 };\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> @@ t/unit-tests/t-hashmap.c (new)\n> + {\n> + int ret;\n> + if (!check_int((ret = key_val_contains(\n> -+ key_val, ARRAY_SIZE(key_val),\n> -+ entry)), ==, 0)) {\n> ++ key_val, seen,\n> ++ ARRAY_SIZE(key_val), entry)),\n> ++ ==, 0)) {\n> + switch (ret) {\n> + case 1:\n> + test_msg(\"found entry was not given in the input\\n\"\n> @@ t/unit-tests/t-hashmap.c (new)\n> + }\n> + check_int(count, ==, 2);\n> + }\n> ++\n> ++ for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n> ++ if (!check_int(seen[i], ==, 1))\n> ++ test_msg(\"following key-val pair was not iterated over:\\n\"\n> ++ \" key: %s\\n value: %s\",\n> ++ key_val[i][0], key_val[i][1]);\n> ++ }\n> ++\n> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> -+ check(get_test_entry(map, ignore_case, \"notInMap\") == NULL);\n> ++ check_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n> +}\n> +\n> +static void t_remove(struct hashmap *map, int ignore_case)\n> @@ t/unit-tests/t-hashmap.c (new)\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + }\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n> @@ t/unit-tests/t-hashmap.c (new)\n> + }\n> +\n> + entry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n> -+ check(hashmap_remove_entry(map, entry, ent, \"notInMap\") == NULL);\n> ++ check_pointer_eq(hashmap_remove_entry(map, entry, ent, \"notInMap\"),\n> NULL);\n> + free(entry);\n> ++\n> ++ check_int(map->tablesize, ==, 64);\n> ++ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) -\n> ARRAY_SIZE(remove));\n> +}\n> +\n> +static void t_iterate(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry;\n> + struct hashmap_iter iter;\n> -+ const char *key_val[][3] = { { \"key1\", \"value1\", \"UNUSED\" },\n> -+ { \"key2\", \"value2\", \"UNUSED\" },\n> -+ { \"fooBarFrotz\", \"value3\", \"UNUSED\" } };\n> -+ int count = 0;\n> ++ const char *key_val[][2] = { { \"key1\", \"value1\" },\n> ++ { \"key2\", \"value2\" },\n> ++ { \"fooBarFrotz\", \"value3\" } };\n> ++ char seen[ARRAY_SIZE(key_val)] = { 0 };\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + }\n> +\n> + hashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n> + {\n> + int ret;\n> -+ if (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),\n> ++ if (!check_int((ret = key_val_contains(key_val, seen,\n> ++ ARRAY_SIZE(key_val),\n> + entry)), ==, 0)) {\n> + switch (ret) {\n> + case 1:\n> @@ t/unit-tests/t-hashmap.c (new)\n> + entry->key, get_value(entry));\n> + break;\n> + }\n> -+ } else {\n> -+ count++;\n> + }\n> + }\n> -+ check_int(count, ==, ARRAY_SIZE(key_val));\n> ++\n> ++ for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n> ++ if (!check_int(seen[i], ==, 1))\n> ++ test_msg(\"following key-val pair was not iterated over:\\n\"\n> ++ \" key: %s\\n value: %s\",\n> ++ key_val[i][0], key_val[i][1]);\n> ++ }\n> ++\n> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +}\n> +\n> @@ t/unit-tests/t-hashmap.c (new)\n> + char *key = xstrfmt(\"key%d\", i);\n> + char *value = xstrfmt(\"value%d\", i);\n> + entry = alloc_test_entry(ignore_case, key, value);\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + free(key);\n> + free(value);\n> + }\n> @@ t/unit-tests/t-hashmap.c (new)\n> + check_int(hashmap_get_size(map), ==, 51);\n> +\n> + entry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n> -+ check(hashmap_put_entry(map, entry, ent) == NULL);\n> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + check_int(map->tablesize, ==, 256);\n> + check_int(hashmap_get_size(map), ==, 52);\n> +\n> @@ t/unit-tests/t-hashmap.c (new)\n> + else if (!check(i1 != values[i]))\n> + test_msg(\"strintern(%s) returns input pointer\\n\",\n> + values[i]);\n> -+ else if (!check(i1 == i2))\n> ++ else if (!check_pointer_eq(i1, i2))\n> + test_msg(\"address('%s') != address('%s'), so strintern('%s') !=\n> strintern('%s')\",\n> + i1, i2, values[i], values[i]);\n> + else\n> @@ t/unit-tests/t-hashmap.c (new)\n> +\n> +int cmd_main(int argc UNUSED, const char **argv UNUSED)\n> +{\n> -+ TEST(setup(t_put, 0), \"put works\");\n> -+ TEST(setup(t_put, 1), \"put (case insensitive) works\");\n> + TEST(setup(t_replace, 0), \"replace works\");\n> + TEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n> + TEST(setup(t_get, 0), \"get works\");\n>\n> Makefile | 1 +\n> t/helper/test-hashmap.c | 100 +----------\n> t/t0011-hashmap.sh | 260 ----------------------------\n> t/unit-tests/t-hashmap.c | 358 +++++++++++++++++++++++++++++++++++++++\n> 4 files changed, 361 insertions(+), 358 deletions(-)\n> delete mode 100755 t/t0011-hashmap.sh\n> create mode 100644 t/unit-tests/t-hashmap.c\n>\n> diff --git a/Makefile b/Makefile\n> index 3eab701b10..74bb026610 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n> UNIT_TEST_PROGRAMS += t-ctype\n> UNIT_TEST_PROGRAMS += t-example-decorate\n> UNIT_TEST_PROGRAMS += t-hash\n> +UNIT_TEST_PROGRAMS += t-hashmap\n> UNIT_TEST_PROGRAMS += t-mem-pool\n> UNIT_TEST_PROGRAMS += t-oidtree\n> UNIT_TEST_PROGRAMS += t-prio-queue\n> diff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c\n> index 2912899558..7b854a7030 100644\n> --- a/t/helper/test-hashmap.c\n> +++ b/t/helper/test-hashmap.c\n> @@ -12,11 +12,6 @@ struct test_entry\n> char key[FLEX_ARRAY];\n> };\n>  \n> -static const char *get_value(const struct test_entry *e)\n> -{\n> - return e->key + strlen(e->key) + 1;\n> -}\n> -\n> static int test_entry_cmp(const void *cmp_data,\n> const struct hashmap_entry *eptr,\n> const struct hashmap_entry *entry_or_key,\n> @@ -141,30 +136,16 @@ static void perf_hashmap(unsigned int method,\n> unsigned int rounds)\n> /*\n> * Read stdin line by line and print result of commands to stdout:\n> *\n> - * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)\n> - * put key value -> NULL / old value\n> - * get key -> NULL / value\n> - * remove key -> NULL / old value\n> - * iterate -> key1 value1\\nkey2 value2\\n...\n> - * size -> tablesize numentries\n> - *\n> * perfhashmap method rounds -> test hashmap.[ch] performance\n> */\n> int cmd__hashmap(int argc, const char **argv)\n> {\n> struct string_list parts = STRING_LIST_INIT_NODUP;\n> struct strbuf line = STRBUF_INIT;\n> - int icase;\n> - struct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);\n> -\n> - /* init hash map */\n> - icase = argc > 1 && !strcmp(\"ignorecase\", argv[1]);\n>  \n> /* process commands from stdin */\n> while (strbuf_getline(&line, stdin) != EOF) {\n> char *cmd, *p1, *p2;\n> - unsigned int hash = 0;\n> - struct test_entry *entry;\n>  \n> /* break line into command and up to two parameters */\n> string_list_setlen(&parts, 0);\n> @@ -180,84 +161,8 @@ int cmd__hashmap(int argc, const char **argv)\n> cmd = parts.items[0].string;\n> p1 = parts.nr >= 1 ? parts.items[1].string : NULL;\n> p2 = parts.nr >= 2 ? parts.items[2].string : NULL;\n> - if (p1)\n> - hash = icase ? strihash(p1) : strhash(p1);\n> -\n> - if (!strcmp(\"add\", cmd) && p1 && p2) {\n> -\n> - /* create entry with key = p1, value = p2 */\n> - entry = alloc_test_entry(hash, p1, p2);\n> -\n> - /* add to hashmap */\n> - hashmap_add(&map, &entry->ent);\n> -\n> - } else if (!strcmp(\"put\", cmd) && p1 && p2) {\n> -\n> - /* create entry with key = p1, value = p2 */\n> - entry = alloc_test_entry(hash, p1, p2);\n> -\n> - /* add / replace entry */\n> - entry = hashmap_put_entry(&map, entry, ent);\n> -\n> - /* print and free replaced entry, if any */\n> - puts(entry ? get_value(entry) : \"NULL\");\n> - free(entry);\n> -\n> - } else if (!strcmp(\"get\", cmd) && p1) {\n> - /* lookup entry in hashmap */\n> - entry = hashmap_get_entry_from_hash(&map, hash, p1,\n> - struct test_entry, ent);\n> -\n> - /* print result */\n> - if (!entry)\n> - puts(\"NULL\");\n> - hashmap_for_each_entry_from(&map, entry, ent)\n> - puts(get_value(entry));\n> -\n> - } else if (!strcmp(\"remove\", cmd) && p1) {\n> -\n> - /* setup static key */\n> - struct hashmap_entry key;\n> - struct hashmap_entry *rm;\n> - hashmap_entry_init(&key, hash);\n> -\n> - /* remove entry from hashmap */\n> - rm = hashmap_remove(&map, &key, p1);\n> - entry = rm ? container_of(rm, struct test_entry, ent)\n> - : NULL;\n> -\n> - /* print result and free entry*/\n> - puts(entry ? get_value(entry) : \"NULL\");\n> - free(entry);\n> -\n> - } else if (!strcmp(\"iterate\", cmd)) {\n> - struct hashmap_iter iter;\n> -\n> - hashmap_for_each_entry(&map, &iter, entry,\n> - ent /* member name */)\n> - printf(\"%s %s\\n\", entry->key, get_value(entry));\n> -\n> - } else if (!strcmp(\"size\", cmd)) {\n> -\n> - /* print table sizes */\n> - printf(\"%u %u\\n\", map.tablesize,\n> - hashmap_get_size(&map));\n> -\n> - } else if (!strcmp(\"intern\", cmd) && p1) {\n> -\n> - /* test that strintern works */\n> - const char *i1 = strintern(p1);\n> - const char *i2 = strintern(p1);\n> - if (strcmp(i1, p1))\n> - printf(\"strintern(%s) returns %s\\n\", p1, i1);\n> - else if (i1 == p1)\n> - printf(\"strintern(%s) returns input pointer\\n\", p1);\n> - else if (i1 != i2)\n> - printf(\"strintern(%s) != strintern(%s)\", i1, i2);\n> - else\n> - printf(\"%s\\n\", i1);\n> -\n> - } else if (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n> +\n> + if (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n>  \n> perf_hashmap(atoi(p1), atoi(p2));\n>  \n> @@ -270,6 +175,5 @@ int cmd__hashmap(int argc, const char **argv)\n>  \n> string_list_clear(&parts, 0);\n> strbuf_release(&line);\n> - hashmap_clear_and_free(&map, struct test_entry, ent);\n> return 0;\n> }\n> diff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh\n> deleted file mode 100755\n> index 46e74ad107..0000000000\n> --- a/t/t0011-hashmap.sh\n> +++ /dev/null\n> @@ -1,260 +0,0 @@\n> -#!/bin/sh\n> -\n> -test_description='test hashmap and string hash functions'\n> -\n> -TEST_PASSES_SANITIZE_LEAK=true\n> -. ./test-lib.sh\n> -\n> -test_hashmap() {\n> - echo \"$1\" | test-tool hashmap $3 > actual &&\n> - echo \"$2\" > expect &&\n> - test_cmp expect actual\n> -}\n> -\n> -test_expect_success 'put' '\n> -\n> -test_hashmap \"put key1 value1\n> -put key2 value2\n> -put fooBarFrotz value3\n> -put foobarfrotz value4\n> -size\" \"NULL\n> -NULL\n> -NULL\n> -NULL\n> -64 4\"\n> -\n> -'\n> -\n> -test_expect_success 'put (case insensitive)' '\n> -\n> -test_hashmap \"put key1 value1\n> -put key2 value2\n> -put fooBarFrotz value3\n> -size\" \"NULL\n> -NULL\n> -NULL\n> -64 3\" ignorecase\n> -\n> -'\n> -\n> -test_expect_success 'replace' '\n> -\n> -test_hashmap \"put key1 value1\n> -put key1 value2\n> -put fooBarFrotz value3\n> -put fooBarFrotz value4\n> -size\" \"NULL\n> -value1\n> -NULL\n> -value3\n> -64 2\"\n> -\n> -'\n> -\n> -test_expect_success 'replace (case insensitive)' '\n> -\n> -test_hashmap \"put key1 value1\n> -put Key1 value2\n> -put fooBarFrotz value3\n> -put foobarfrotz value4\n> -size\" \"NULL\n> -value1\n> -NULL\n> -value3\n> -64 2\" ignorecase\n> -\n> -'\n> -\n> -test_expect_success 'get' '\n> -\n> -test_hashmap \"put key1 value1\n> -put key2 value2\n> -put fooBarFrotz value3\n> -put foobarfrotz value4\n> -get key1\n> -get key2\n> -get fooBarFrotz\n> -get notInMap\" \"NULL\n> -NULL\n> -NULL\n> -NULL\n> -value1\n> -value2\n> -value3\n> -NULL\"\n> -\n> -'\n> -\n> -test_expect_success 'get (case insensitive)' '\n> -\n> -test_hashmap \"put key1 value1\n> -put key2 value2\n> -put fooBarFrotz value3\n> -get Key1\n> -get keY2\n> -get foobarfrotz\n> -get notInMap\" \"NULL\n> -NULL\n> -NULL\n> -value1\n> -value2\n> -value3\n> -NULL\" ignorecase\n> -\n> -'\n> -\n> -test_expect_success 'add' '\n> -\n> -test_hashmap \"add key1 value1\n> -add key1 value2\n> -add fooBarFrotz value3\n> -add fooBarFrotz value4\n> -get key1\n> -get fooBarFrotz\n> -get notInMap\" \"value2\n> -value1\n> -value4\n> -value3\n> -NULL\"\n> -\n> -'\n> -\n> -test_expect_success 'add (case insensitive)' '\n> -\n> -test_hashmap \"add key1 value1\n> -add Key1 value2\n> -add fooBarFrotz value3\n> -add foobarfrotz value4\n> -get key1\n> -get Foobarfrotz\n> -get notInMap\" \"value2\n> -value1\n> -value4\n> -value3\n> -NULL\" ignorecase\n> -\n> -'\n> -\n> -test_expect_success 'remove' '\n> -\n> -test_hashmap \"put key1 value1\n> -put key2 value2\n> -put fooBarFrotz value3\n> -remove key1\n> -remove key2\n> -remove notInMap\n> -size\" \"NULL\n> -NULL\n> -NULL\n> -value1\n> -value2\n> -NULL\n> -64 1\"\n> -\n> -'\n> -\n> -test_expect_success 'remove (case insensitive)' '\n> -\n> -test_hashmap \"put key1 value1\n> -put key2 value2\n> -put fooBarFrotz value3\n> -remove Key1\n> -remove keY2\n> -remove notInMap\n> -size\" \"NULL\n> -NULL\n> -NULL\n> -value1\n> -value2\n> -NULL\n> -64 1\" ignorecase\n> -\n> -'\n> -\n> -test_expect_success 'iterate' '\n> - test-tool hashmap >actual.raw <<-\\EOF &&\n> - put key1 value1\n> - put key2 value2\n> - put fooBarFrotz value3\n> - iterate\n> - EOF\n> -\n> - cat >expect <<-\\EOF &&\n> - NULL\n> - NULL\n> - NULL\n> - fooBarFrotz value3\n> - key1 value1\n> - key2 value2\n> - EOF\n> -\n> - sort <actual.raw >actual &&\n> - test_cmp expect actual\n> -'\n> -\n> -test_expect_success 'iterate (case insensitive)' '\n> - test-tool hashmap ignorecase >actual.raw <<-\\EOF &&\n> - put key1 value1\n> - put key2 value2\n> - put fooBarFrotz value3\n> - iterate\n> - EOF\n> -\n> - cat >expect <<-\\EOF &&\n> - NULL\n> - NULL\n> - NULL\n> - fooBarFrotz value3\n> - key1 value1\n> - key2 value2\n> - EOF\n> -\n> - sort <actual.raw >actual &&\n> - test_cmp expect actual\n> -'\n> -\n> -test_expect_success 'grow / shrink' '\n> -\n> - rm -f in &&\n> - rm -f expect &&\n> - for n in $(test_seq 51)\n> - do\n> - echo put key$n value$n >> in &&\n> - echo NULL >> expect || return 1\n> - done &&\n> - echo size >> in &&\n> - echo 64 51 >> expect &&\n> - echo put key52 value52 >> in &&\n> - echo NULL >> expect &&\n> - echo size >> in &&\n> - echo 256 52 >> expect &&\n> - for n in $(test_seq 12)\n> - do\n> - echo remove key$n >> in &&\n> - echo value$n >> expect || return 1\n> - done &&\n> - echo size >> in &&\n> - echo 256 40 >> expect &&\n> - echo remove key40 >> in &&\n> - echo value40 >> expect &&\n> - echo size >> in &&\n> - echo 64 39 >> expect &&\n> - test-tool hashmap <in >out &&\n> - test_cmp expect out\n> -\n> -'\n> -\n> -test_expect_success 'string interning' '\n> -\n> -test_hashmap \"intern value1\n> -intern Value1\n> -intern value2\n> -intern value2\n> -\" \"value1\n> -Value1\n> -value2\n> -value2\"\n> -\n> -'\n> -\n> -test_done\n> diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\n> new file mode 100644\n> index 0000000000..3112b10b33\n> --- /dev/null\n> +++ b/t/unit-tests/t-hashmap.c\n> @@ -0,0 +1,358 @@\n> +#include \"test-lib.h\"\n> +#include \"hashmap.h\"\n> +#include \"strbuf.h\"\n> +\n> +struct test_entry {\n> + int padding; /* hashmap entry no longer needs to be the first member\n> */\n> + struct hashmap_entry ent;\n> + /* key and value as two \\0-terminated strings */\n> + char key[FLEX_ARRAY];\n> +};\n> +\n> +static int test_entry_cmp(const void *cmp_data,\n> + const struct hashmap_entry *eptr,\n> + const struct hashmap_entry *entry_or_key,\n> + const void *keydata)\n> +{\n> + const int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n> + const struct test_entry *e1, *e2;\n> + const char *key = keydata;\n> +\n> + e1 = container_of(eptr, const struct test_entry, ent);\n> + e2 = container_of(entry_or_key, const struct test_entry, ent);\n> +\n> + if (ignore_case)\n> + return strcasecmp(e1->key, key ? key : e2->key);\n> + else\n> + return strcmp(e1->key, key ? key : e2->key);\n> +}\n> +\n> +static const char *get_value(const struct test_entry *e)\n> +{\n> + return e->key + strlen(e->key) + 1;\n> +}\n> +\n> +static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n> + const char *key, const char *value)\n> +{\n> + size_t klen = strlen(key);\n> + size_t vlen = strlen(value);\n> + unsigned int hash = ignore_case ? strihash(key) : strhash(key);\n> + struct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen,\n> 2));\n> +\n> + hashmap_entry_init(&entry->ent, hash);\n> + memcpy(entry->key, key, klen + 1);\n> + memcpy(entry->key + klen + 1, value, vlen + 1);\n> + return entry;\n> +}\n> +\n> +static struct test_entry *get_test_entry(struct hashmap *map,\n> + unsigned int ignore_case, const char *key)\n> +{\n> + return hashmap_get_entry_from_hash(\n> + map, ignore_case ? strihash(key) : strhash(key), key,\n> + struct test_entry, ent);\n> +}\n> +\n> +static int key_val_contains(const char *key_val[][2], char seen[],\n> size_t n,\n> + struct test_entry *entry)\n> +{\n> + for (size_t i = 0; i < n; i++) {\n> + if (!strcmp(entry->key, key_val[i][0]) &&\n> + !strcmp(get_value(entry), key_val[i][1])) {\n> + if (seen[i])\n> + return 2;\n> + seen[i] = 1;\n> + return 0;\n> + }\n> + }\n> + return 1;\n> +}\n> +\n> +static void setup(void (*f)(struct hashmap *map, int ignore_case),\n> + int ignore_case)\n> +{\n> + struct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n> +\n> + f(&map, ignore_case);\n> + hashmap_clear_and_free(&map, struct test_entry, ent);\n> +}\n> +\n> +static void t_replace(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry;\n> +\n> + entry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> +\n> + entry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n> + \"value2\");\n> + entry = hashmap_put_entry(map, entry, ent);\n> + if (check(entry != NULL))\n> + check_str(get_value(entry), \"value1\");\n> + free(entry);\n> +\n> + entry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> +\n> + entry = alloc_test_entry(ignore_case,\n> + ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n> + \"value4\");\n> + entry = hashmap_put_entry(map, entry, ent);\n> + if (check(entry != NULL))\n> + check_str(get_value(entry), \"value3\");\n> + free(entry);\n> +}\n> +\n> +static void t_get(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry;\n> + const char *key_val[][2] = { { \"key1\", \"value1\" },\n> + { \"key2\", \"value2\" },\n> + { \"fooBarFrotz\", \"value3\" },\n> + { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n> + const char *query[][2] = {\n> + { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> + { ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n> + { ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n> + };\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + }\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n> + entry = get_test_entry(map, ignore_case, query[i][0]);\n> + if (check(entry != NULL))\n> + check_str(get_value(entry), query[i][1]);\n> + else\n> + test_msg(\"query key: %s\", query[i][0]);\n> + }\n> +\n> + check_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n> + check_int(map->tablesize, ==, 64);\n> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +}\n> +\n> +static void t_add(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry;\n> + const char *key_val[][2] = {\n> + { \"key1\", \"value1\" },\n> + { ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n> + { \"fooBarFrotz\", \"value3\" },\n> + { ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\" }\n> + };\n> + const char *queries[] = { \"key1\",\n> + ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n> + char seen[ARRAY_SIZE(key_val)] = { 0 };\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> + hashmap_add(map, &entry->ent);\n> + }\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n> + int count = 0;\n> + entry = hashmap_get_entry_from_hash(map,\n> + ignore_case ? strihash(queries[i]) :\n> + strhash(queries[i]),\n> + queries[i], struct test_entry, ent);\n> +\n> + hashmap_for_each_entry_from(map, entry, ent)\n> + {\n> + int ret;\n> + if (!check_int((ret = key_val_contains(\n> + key_val, seen,\n> + ARRAY_SIZE(key_val), entry)),\n> + ==, 0)) {\n> + switch (ret) {\n> + case 1:\n> + test_msg(\"found entry was not given in the input\\n\"\n> + \" key: %s\\n value: %s\",\n> + entry->key, get_value(entry));\n> + break;\n> + case 2:\n> + test_msg(\"duplicate entry detected\\n\"\n> + \" key: %s\\n value: %s\",\n> + entry->key, get_value(entry));\n> + break;\n> + }\n> + } else {\n> + count++;\n> + }\n> + }\n> + check_int(count, ==, 2);\n> + }\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n> + if (!check_int(seen[i], ==, 1))\n> + test_msg(\"following key-val pair was not iterated over:\\n\"\n> + \" key: %s\\n value: %s\",\n> + key_val[i][0], key_val[i][1]);\n> + }\n> +\n> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> + check_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n> +}\n> +\n> +static void t_remove(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry, *removed;\n> + const char *key_val[][2] = { { \"key1\", \"value1\" },\n> + { \"key2\", \"value2\" },\n> + { \"fooBarFrotz\", \"value3\" } };\n> + const char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\"\n> },\n> + { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + }\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n> + entry = alloc_test_entry(ignore_case, remove[i][0], \"\");\n> + removed = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n> + if (check(removed != NULL))\n> + check_str(get_value(removed), remove[i][1]);\n> + free(entry);\n> + free(removed);\n> + }\n> +\n> + entry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n> + check_pointer_eq(hashmap_remove_entry(map, entry, ent, \"notInMap\"),\n> NULL);\n> + free(entry);\n> +\n> + check_int(map->tablesize, ==, 64);\n> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) -\n> ARRAY_SIZE(remove));\n> +}\n> +\n> +static void t_iterate(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry;\n> + struct hashmap_iter iter;\n> + const char *key_val[][2] = { { \"key1\", \"value1\" },\n> + { \"key2\", \"value2\" },\n> + { \"fooBarFrotz\", \"value3\" } };\n> + char seen[ARRAY_SIZE(key_val)] = { 0 };\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + }\n> +\n> + hashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n> + {\n> + int ret;\n> + if (!check_int((ret = key_val_contains(key_val, seen,\n> + ARRAY_SIZE(key_val),\n> + entry)), ==, 0)) {\n> + switch (ret) {\n> + case 1:\n> + test_msg(\"found entry was not given in the input\\n\"\n> + \" key: %s\\n value: %s\",\n> + entry->key, get_value(entry));\n> + break;\n> + case 2:\n> + test_msg(\"duplicate entry detected\\n\"\n> + \" key: %s\\n value: %s\",\n> + entry->key, get_value(entry));\n> + break;\n> + }\n> + }\n> + }\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n> + if (!check_int(seen[i], ==, 1))\n> + test_msg(\"following key-val pair was not iterated over:\\n\"\n> + \" key: %s\\n value: %s\",\n> + key_val[i][0], key_val[i][1]);\n> + }\n> +\n> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +}\n> +\n> +static void t_alloc(struct hashmap *map, int ignore_case)\n> +{\n> + struct test_entry *entry, *removed;\n> +\n> + for (int i = 1; i <= 51; i++) {\n> + char *key = xstrfmt(\"key%d\", i);\n> + char *value = xstrfmt(\"value%d\", i);\n> + entry = alloc_test_entry(ignore_case, key, value);\n> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + free(key);\n> + free(value);\n> + }\n> + check_int(map->tablesize, ==, 64);\n> + check_int(hashmap_get_size(map), ==, 51);\n> +\n> + entry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> + check_int(map->tablesize, ==, 256);\n> + check_int(hashmap_get_size(map), ==, 52);\n> +\n> + for (int i = 1; i <= 12; i++) {\n> + char *key = xstrfmt(\"key%d\", i);\n> + char *value = xstrfmt(\"value%d\", i);\n> +\n> + entry = alloc_test_entry(ignore_case, key, \"\");\n> + removed = hashmap_remove_entry(map, entry, ent, key);\n> + if (check(removed != NULL))\n> + check_str(value, get_value(removed));\n> + free(key);\n> + free(value);\n> + free(entry);\n> + free(removed);\n> + }\n> + check_int(map->tablesize, ==, 256);\n> + check_int(hashmap_get_size(map), ==, 40);\n> +\n> + entry = alloc_test_entry(ignore_case, \"key40\", \"\");\n> + removed = hashmap_remove_entry(map, entry, ent, \"key40\");\n> + if (check(removed != NULL))\n> + check_str(\"value40\", get_value(removed));\n> + check_int(map->tablesize, ==, 64);\n> + check_int(hashmap_get_size(map), ==, 39);\n> + free(entry);\n> + free(removed);\n> +}\n> +\n> +static void t_intern(struct hashmap *map, int ignore_case)\n> +{\n> + const char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n> +\n> + for (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n> + const char *i1 = strintern(values[i]);\n> + const char *i2 = strintern(values[i]);\n> +\n> + if (!check(!strcmp(i1, values[i])))\n> + test_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n> + else if (!check(i1 != values[i]))\n> + test_msg(\"strintern(%s) returns input pointer\\n\",\n> + values[i]);\n> + else if (!check_pointer_eq(i1, i2))\n> + test_msg(\"address('%s') != address('%s'), so strintern('%s') !=\n> strintern('%s')\",\n> + i1, i2, values[i], values[i]);\n> + else\n> + check_str(i1, values[i]);\n> + }\n> +}\n> +\n> +int cmd_main(int argc UNUSED, const char **argv UNUSED)\n> +{\n> + TEST(setup(t_replace, 0), \"replace works\");\n> + TEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n> + TEST(setup(t_get, 0), \"get works\");\n> + TEST(setup(t_get, 1), \"get (case insensitive) works\");\n> + TEST(setup(t_add, 0), \"add works\");\n> + TEST(setup(t_add, 1), \"add (case insensitive) works\");\n> + TEST(setup(t_remove, 0), \"remove works\");\n> + TEST(setup(t_remove, 1), \"remove (case insensitive) works\");\n> + TEST(setup(t_iterate, 0), \"iterate works\");\n> + TEST(setup(t_iterate, 1), \"iterate (case insensitive) works\");\n> + TEST(setup(t_alloc, 0), \"grow / shrink works\");\n> + TEST(setup(t_intern, 0), \"string interning works\");\n> + return test_done();\n> +}\n> --\n> 2.45.2\n\n"},{"id":"498590","messageId":"xmqqcynimt1v.fsf@gitster.g","threadId":"61692","inReplyTo":"D2N4AG0P10QT.2EMBZAPNDBSP0@gmail.com","subject":"Re: [GSoC][PATCH v3] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-12T17:14:20Z","receivedAt":"2024-07-12T17:14:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Ghanshyam Thakkar\" <shyamthakkar001@gmail.com> writes:\n\n> - And replaced the third elements of key_val[] which recorded\n>   the presence of certain key-val pair with 'char seen[]'.\n\nIt makes it in-line with the oidmap test, which is good.\n"},{"id":"499333","messageId":"CAP8UFD0apMhWA_F4La13=P3Mq433_Kq2qXzkVp9FNC+cWi_U9A@mail.gmail.com","threadId":"61692","inReplyTo":"20240711235159.5320-1-shyamthakkar001@gmail.com","subject":"Re: [GSoC][PATCH v3] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2024-07-25T12:48:50Z","receivedAt":"2024-07-25T12:49:04Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Fri, Jul 12, 2024 at 1:52 AM Ghanshyam Thakkar\n<shyamthakkar001@gmail.com> wrote:\n>\n> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\n> library. Migrate them to the unit testing framework for better\n> debugging, runtime performance and concise code.\n>\n> Along with the migration, make 'add' tests from the shellscript order\n\nNit: s/shellscript/shell script/\n\n> agnostic in unit tests, since they iterate over entries with the same\n> keys and we do not guarantee the order.\n\nThe above is not very clear. Maybe rephrasing a bit and adding an\nexample could help.\n\n> The helper/test-hashmap.c is still not removed because it contains a\n> performance test meant to be run by the user directly (not used in\n> t/perf). And it makes sense for such a utility to be a helper.\n\n\n> diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\n> new file mode 100644\n> index 0000000000..3112b10b33\n> --- /dev/null\n> +++ b/t/unit-tests/t-hashmap.c\n> @@ -0,0 +1,358 @@\n> +#include \"test-lib.h\"\n> +#include \"hashmap.h\"\n> +#include \"strbuf.h\"\n> +\n> +struct test_entry {\n> +       int padding; /* hashmap entry no longer needs to be the first member */\n> +       struct hashmap_entry ent;\n> +       /* key and value as two \\0-terminated strings */\n> +       char key[FLEX_ARRAY];\n> +};\n> +\n> +static int test_entry_cmp(const void *cmp_data,\n> +                         const struct hashmap_entry *eptr,\n> +                         const struct hashmap_entry *entry_or_key,\n> +                         const void *keydata)\n> +{\n> +       const int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n> +       const struct test_entry *e1, *e2;\n> +       const char *key = keydata;\n> +\n> +       e1 = container_of(eptr, const struct test_entry, ent);\n> +       e2 = container_of(entry_or_key, const struct test_entry, ent);\n> +\n> +       if (ignore_case)\n> +               return strcasecmp(e1->key, key ? key : e2->key);\n> +       else\n> +               return strcmp(e1->key, key ? key : e2->key);\n> +}\n> +\n> +static const char *get_value(const struct test_entry *e)\n> +{\n> +       return e->key + strlen(e->key) + 1;\n> +}\n> +\n> +static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n> +                                          const char *key, const char *value)\n\nNit: `unsigned int ignore_case` is a bool flag argument which we often\nput at the end of function arguments, so perhaps:\n\nstatic struct test_entry *alloc_test_entry(const char *key, const char *value,\n                                          unsigned int ignore_case)\n\n> +{\n> +       size_t klen = strlen(key);\n> +       size_t vlen = strlen(value);\n> +       unsigned int hash = ignore_case ? strihash(key) : strhash(key);\n> +       struct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n> +\n> +       hashmap_entry_init(&entry->ent, hash);\n> +       memcpy(entry->key, key, klen + 1);\n> +       memcpy(entry->key + klen + 1, value, vlen + 1);\n> +       return entry;\n> +}\n> +\n> +static struct test_entry *get_test_entry(struct hashmap *map,\n> +                                        unsigned int ignore_case, const char *key)\n\nHere also `unsigned int ignore_case` might want to be the last argument.\n\n> +{\n> +       return hashmap_get_entry_from_hash(\n> +               map, ignore_case ? strihash(key) : strhash(key), key,\n> +               struct test_entry, ent);\n> +}\n> +\n> +static int key_val_contains(const char *key_val[][2], char seen[], size_t n,\n> +                           struct test_entry *entry)\n> +{\n> +       for (size_t i = 0; i < n; i++) {\n> +               if (!strcmp(entry->key, key_val[i][0]) &&\n> +                   !strcmp(get_value(entry), key_val[i][1])) {\n> +                       if (seen[i])\n> +                               return 2;\n> +                       seen[i] = 1;\n> +                       return 0;\n> +               }\n> +       }\n> +       return 1;\n> +}\n> +\n> +static void setup(void (*f)(struct hashmap *map, int ignore_case),\n> +                 int ignore_case)\n\nWhy `int ignore_case` here, instead of `unsigned int ignore_case` above?\n\nIt's nice that the `ignore_case` argument is the last argument though.\n\n> +{\n> +       struct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n> +\n> +       f(&map, ignore_case);\n> +       hashmap_clear_and_free(&map, struct test_entry, ent);\n> +}\n> +\n> +static void t_replace(struct hashmap *map, int ignore_case)\n\nHere also, why `int ignore_case` here, instead of `unsigned int\nignore_case` towards the top of this file?\n\nI won't mention it anymore below, but I think it might be better to\nhave `unsigned int ignore_case` everywhere.\n\n> +{\n> +       struct test_entry *entry;\n> +\n> +       entry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n> +       check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> +\n> +       entry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n> +                                \"value2\");\n> +       entry = hashmap_put_entry(map, entry, ent);\n> +       if (check(entry != NULL))\n> +               check_str(get_value(entry), \"value1\");\n> +       free(entry);\n> +\n> +       entry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n> +       check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> +\n> +       entry = alloc_test_entry(ignore_case,\n> +                                ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n> +                                \"value4\");\n\nMaybe using something like \"FOObarFrotZ\" instead of \"foobarfrotz\"\nwould make it clear that we don't need to downcase in case we ignore\ncase.\n\n> +       entry = hashmap_put_entry(map, entry, ent);\n> +       if (check(entry != NULL))\n> +               check_str(get_value(entry), \"value3\");\n> +       free(entry);\n> +}\n> +\n> +static void t_get(struct hashmap *map, int ignore_case)\n> +{\n> +       struct test_entry *entry;\n> +       const char *key_val[][2] = { { \"key1\", \"value1\" },\n> +                                    { \"key2\", \"value2\" },\n> +                                    { \"fooBarFrotz\", \"value3\" },\n> +                                    { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n> +       const char *query[][2] = {\n> +               { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> +               { ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n> +               { ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n> +       };\n\nI think adding a test case like:\n\n               { ignore_case ? \"FOOBarFrotZ\" : \"foobarfrotz\",\n                 ignore_case ? : \"value3\" : \"value4\" }\n\nwhich is a bit similar to what Junio suggested, could help a bit check\nthat things work well especially when not ignoring the case.\n\n> +       for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> +               entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> +               check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n> +       }\n> +\n> +       for (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n> +               entry = get_test_entry(map, ignore_case, query[i][0]);\n> +               if (check(entry != NULL))\n> +                       check_str(get_value(entry), query[i][1]);\n> +               else\n> +                       test_msg(\"query key: %s\", query[i][0]);\n> +       }\n> +\n> +       check_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n> +       check_int(map->tablesize, ==, 64);\n> +       check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +}\n> +\n> +static void t_add(struct hashmap *map, int ignore_case)\n> +{\n> +       struct test_entry *entry;\n> +       const char *key_val[][2] = {\n> +               { \"key1\", \"value1\" },\n> +               { ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n> +               { \"fooBarFrotz\", \"value3\" },\n> +               { ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\" }\n> +       };\n> +       const char *queries[] = { \"key1\",\n> +                                 ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n\nIt is tricky that here `queries` is not defined and doesn't contain\nthe same things as in the other functions above:\n\nconst char *queries[][2] = ...\n\nMaybe changing the name to something like `query_keys` would help make\npeople aware that here it contains only keys.\n\n> +       char seen[ARRAY_SIZE(key_val)] = { 0 };\n> +\n> +       for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n> +               entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n> +               hashmap_add(map, &entry->ent);\n> +       }\n> +\n> +       for (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n> +               int count = 0;\n> +               entry = hashmap_get_entry_from_hash(map,\n> +                       ignore_case ? strihash(queries[i]) :\n> +                                     strhash(queries[i]),\n> +                       queries[i], struct test_entry, ent);\n> +\n> +               hashmap_for_each_entry_from(map, entry, ent)\n> +               {\n> +                       int ret;\n> +                       if (!check_int((ret = key_val_contains(\n> +                                               key_val, seen,\n> +                                               ARRAY_SIZE(key_val), entry)),\n> +                                      ==, 0)) {\n> +                               switch (ret) {\n> +                               case 1:\n> +                                       test_msg(\"found entry was not given in the input\\n\"\n> +                                                \"    key: %s\\n  value: %s\",\n> +                                                entry->key, get_value(entry));\n> +                                       break;\n> +                               case 2:\n> +                                       test_msg(\"duplicate entry detected\\n\"\n> +                                                \"    key: %s\\n  value: %s\",\n> +                                                entry->key, get_value(entry));\n> +                                       break;\n> +                               }\n> +                       } else {\n> +                               count++;\n> +                       }\n> +               }\n> +               check_int(count, ==, 2);\n> +       }\n> +\n> +       for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n> +               if (!check_int(seen[i], ==, 1))\n> +                       test_msg(\"following key-val pair was not iterated over:\\n\"\n> +                                \"    key: %s\\n  value: %s\",\n> +                                key_val[i][0], key_val[i][1]);\n> +       }\n> +\n> +       check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n> +       check_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n> +}\n\nThanks!\n"},{"id":"499598","messageId":"20240730115101.188100-1-author@example.com","threadId":"61692","inReplyTo":"20240711235159.5320-1-shyamthakkar001@gmail.com","subject":"[PATCH v4] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"A U Thor","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-07-30T11:50:33Z","receivedAt":"2024-07-30T11:52:20Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"From: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n\nhelper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\nlibrary. Migrate them to the unit testing framework for better\ndebugging, runtime performance and concise code.\n\nAlong with the migration, make 'add' tests from the shell script order\nagnostic in unit tests, since they iterate over entries with the same\nkeys and we do not guarantee the order. This was already done for the\n'iterate' tests[1].\n\nThe helper/test-hashmap.c is still not removed because it contains a\nperformance test meant to be run by the user directly (not used in\nt/perf). And it makes sense for such a utility to be a helper.\n\n[1]: e1e7a77141 (t: sort output of hashmap iteration, 2019-07-30)\n\nMentored-by: Christian Couder <chriscool@tuxfamily.org>\nMentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\nHelped-by: Josh Steadmon <steadmon@google.com>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n---\nChanges in v4:\n- update commit message to add a reference and fix typo\n- change 'int ignore_case' to 'unsigned int ignore_case'\n- make 'ignore_case' the last parameter in all the functions\n- update t_get() to add a testcase query for better ignore-case\n  checking\n\nRange-diff against v3:\n1:  03ba77665e ! 1:  04022c4cb9 t: port helper/test-hashmap.c to unit-tests/t-hashmap.c\n    @@ Commit message\n         library. Migrate them to the unit testing framework for better\n         debugging, runtime performance and concise code.\n     \n    -    Along with the migration, make 'add' tests from the shellscript order\n    +    Along with the migration, make 'add' tests from the shell script order\n         agnostic in unit tests, since they iterate over entries with the same\n    -    keys and we do not guarantee the order.\n    +    keys and we do not guarantee the order. This was already done for the\n    +    'iterate' tests[1].\n     \n         The helper/test-hashmap.c is still not removed because it contains a\n         performance test meant to be run by the user directly (not used in\n         t/perf). And it makes sense for such a utility to be a helper.\n     \n    +    [1]: e1e7a77141 (t: sort output of hashmap iteration, 2019-07-30)\n    +\n         Mentored-by: Christian Couder <chriscool@tuxfamily.org>\n         Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\n         Helped-by: Josh Steadmon <steadmon@google.com>\n    +    Helped-by: Junio C Hamano <gitster@pobox.com>\n         Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n         Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n     \n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\t\t  const struct hashmap_entry *entry_or_key,\n     +\t\t\t  const void *keydata)\n     +{\n    -+\tconst int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n    ++\tconst unsigned int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n     +\tconst struct test_entry *e1, *e2;\n     +\tconst char *key = keydata;\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\treturn e->key + strlen(e->key) + 1;\n     +}\n     +\n    -+static struct test_entry *alloc_test_entry(unsigned int ignore_case,\n    -+\t\t\t\t\t   const char *key, const char *value)\n    ++static struct test_entry *alloc_test_entry(const char *key, const char *value,\n    ++\t\t\t\t\t   unsigned int ignore_case)\n     +{\n     +\tsize_t klen = strlen(key);\n     +\tsize_t vlen = strlen(value);\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\treturn entry;\n     +}\n     +\n    -+static struct test_entry *get_test_entry(struct hashmap *map,\n    -+\t\t\t\t\t unsigned int ignore_case, const char *key)\n    ++static struct test_entry *get_test_entry(struct hashmap *map, const char *key,\n    ++\t\t\t\t\t unsigned int ignore_case)\n     +{\n     +\treturn hashmap_get_entry_from_hash(\n     +\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\treturn 1;\n     +}\n     +\n    -+static void setup(void (*f)(struct hashmap *map, int ignore_case),\n    -+\t\t  int ignore_case)\n    ++static void setup(void (*f)(struct hashmap *map, unsigned int ignore_case),\n    ++\t\t  unsigned int ignore_case)\n     +{\n     +\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\thashmap_clear_and_free(&map, struct test_entry, ent);\n     +}\n     +\n    -+static void t_replace(struct hashmap *map, int ignore_case)\n    ++static void t_replace(struct hashmap *map, unsigned int ignore_case)\n     +{\n     +\tstruct test_entry *entry;\n     +\n    -+\tentry = alloc_test_entry(ignore_case, \"key1\", \"value1\");\n    ++\tentry = alloc_test_entry(\"key1\", \"value1\", ignore_case);\n     +\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\n    -+\tentry = alloc_test_entry(ignore_case, ignore_case ? \"Key1\" : \"key1\",\n    -+\t\t\t\t \"value2\");\n    ++\tentry = alloc_test_entry(ignore_case ? \"Key1\" : \"key1\", \"value2\",\n    ++\t\t\t\t ignore_case);\n     +\tentry = hashmap_put_entry(map, entry, ent);\n     +\tif (check(entry != NULL))\n     +\t\tcheck_str(get_value(entry), \"value1\");\n     +\tfree(entry);\n     +\n    -+\tentry = alloc_test_entry(ignore_case, \"fooBarFrotz\", \"value3\");\n    ++\tentry = alloc_test_entry(\"fooBarFrotz\", \"value3\", ignore_case);\n     +\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\n    -+\tentry = alloc_test_entry(ignore_case,\n    -+\t\t\t\t ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\",\n    -+\t\t\t\t \"value4\");\n    ++\tentry = alloc_test_entry(ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\",\n    ++\t\t\t\t \"value4\", ignore_case);\n     +\tentry = hashmap_put_entry(map, entry, ent);\n     +\tif (check(entry != NULL))\n     +\t\tcheck_str(get_value(entry), \"value3\");\n     +\tfree(entry);\n     +}\n     +\n    -+static void t_get(struct hashmap *map, int ignore_case)\n    ++static void t_get(struct hashmap *map, unsigned int ignore_case)\n     +{\n     +\tstruct test_entry *entry;\n     +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n     +\t\t\t\t     { \"key2\", \"value2\" },\n     +\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n    -+\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n    ++\t\t\t\t     { ignore_case ? \"TeNor\" : \"tenor\",\n    ++\t\t\t\t       ignore_case ? \"value4\" : \"value5\" } };\n     +\tconst char *query[][2] = {\n     +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n     +\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n    -+\t\t{ ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n    ++\t\t{ ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n    ++\t\t{ ignore_case ? \"TENOR\" : \"tenor\",\n    ++\t\t  ignore_case ? \"value4\" : \"value5\" }\n     +\t};\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n    -+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    ++\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1],\n    ++\t\t\t\t\t ignore_case);\n     +\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t}\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n    -+\t\tentry = get_test_entry(map, ignore_case, query[i][0]);\n    ++\t\tentry = get_test_entry(map, query[i][0], ignore_case);\n     +\t\tif (check(entry != NULL))\n     +\t\t\tcheck_str(get_value(entry), query[i][1]);\n     +\t\telse\n     +\t\t\ttest_msg(\"query key: %s\", query[i][0]);\n     +\t}\n     +\n    -+\tcheck_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n    ++\tcheck_pointer_eq(get_test_entry(map, \"notInMap\", ignore_case), NULL);\n     +\tcheck_int(map->tablesize, ==, 64);\n     +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n     +}\n     +\n    -+static void t_add(struct hashmap *map, int ignore_case)\n    ++static void t_add(struct hashmap *map, unsigned int ignore_case)\n     +{\n     +\tstruct test_entry *entry;\n     +\tconst char *key_val[][2] = {\n     +\t\t{ \"key1\", \"value1\" },\n     +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n     +\t\t{ \"fooBarFrotz\", \"value3\" },\n    -+\t\t{ ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\", \"value4\" }\n    ++\t\t{ ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value4\" }\n     +\t};\n    -+\tconst char *queries[] = { \"key1\",\n    -+\t\t\t\t  ignore_case ? \"Foobarfrotz\" : \"fooBarFrotz\" };\n    ++\tconst char *query_keys[] = { \"key1\", ignore_case ? \"FOObarFrotz\" :\n    ++\t\t\t\t\t\t\t   \"fooBarFrotz\" };\n     +\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n    -+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    ++\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n     +\t\thashmap_add(map, &entry->ent);\n     +\t}\n     +\n    -+\tfor (size_t i = 0; i < ARRAY_SIZE(queries); i++) {\n    ++\tfor (size_t i = 0; i < ARRAY_SIZE(query_keys); i++) {\n     +\t\tint count = 0;\n     +\t\tentry = hashmap_get_entry_from_hash(map,\n    -+\t\t\tignore_case ? strihash(queries[i]) :\n    -+\t\t\t\t      strhash(queries[i]),\n    -+\t\t\tqueries[i], struct test_entry, ent);\n    ++\t\t\tignore_case ? strihash(query_keys[i]) :\n    ++\t\t\t\t      strhash(query_keys[i]),\n    ++\t\t\tquery_keys[i], struct test_entry, ent);\n     +\n     +\t\thashmap_for_each_entry_from(map, entry, ent)\n     +\t\t{\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t}\n     +\n     +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n    -+\tcheck_pointer_eq(get_test_entry(map, ignore_case, \"notInMap\"), NULL);\n    ++\tcheck_pointer_eq(get_test_entry(map, \"notInMap\", ignore_case), NULL);\n     +}\n     +\n    -+static void t_remove(struct hashmap *map, int ignore_case)\n    ++static void t_remove(struct hashmap *map, unsigned int ignore_case)\n     +{\n     +\tstruct test_entry *entry, *removed;\n     +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n    -+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    ++\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n     +\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t}\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n    -+\t\tentry = alloc_test_entry(ignore_case, remove[i][0], \"\");\n    ++\t\tentry = alloc_test_entry(remove[i][0], \"\", ignore_case);\n     +\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n     +\t\tif (check(removed != NULL))\n     +\t\t\tcheck_str(get_value(removed), remove[i][1]);\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\tfree(removed);\n     +\t}\n     +\n    -+\tentry = alloc_test_entry(ignore_case, \"notInMap\", \"\");\n    ++\tentry = alloc_test_entry(\"notInMap\", \"\", ignore_case);\n     +\tcheck_pointer_eq(hashmap_remove_entry(map, entry, ent, \"notInMap\"), NULL);\n     +\tfree(entry);\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));\n     +}\n     +\n    -+static void t_iterate(struct hashmap *map, int ignore_case)\n    ++static void t_iterate(struct hashmap *map, unsigned int ignore_case)\n     +{\n     +\tstruct test_entry *entry;\n     +\tstruct hashmap_iter iter;\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n    -+\t\tentry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);\n    ++\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n     +\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t}\n     +\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n     +}\n     +\n    -+static void t_alloc(struct hashmap *map, int ignore_case)\n    ++static void t_alloc(struct hashmap *map, unsigned int ignore_case)\n     +{\n     +\tstruct test_entry *entry, *removed;\n     +\n     +\tfor (int i = 1; i <= 51; i++) {\n     +\t\tchar *key = xstrfmt(\"key%d\", i);\n     +\t\tchar *value = xstrfmt(\"value%d\", i);\n    -+\t\tentry = alloc_test_entry(ignore_case, key, value);\n    ++\t\tentry = alloc_test_entry(key, value, ignore_case);\n     +\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\t\tfree(key);\n     +\t\tfree(value);\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tcheck_int(map->tablesize, ==, 64);\n     +\tcheck_int(hashmap_get_size(map), ==, 51);\n     +\n    -+\tentry = alloc_test_entry(ignore_case, \"key52\", \"value52\");\n    ++\tentry = alloc_test_entry(\"key52\", \"value52\", ignore_case);\n     +\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n     +\tcheck_int(map->tablesize, ==, 256);\n     +\tcheck_int(hashmap_get_size(map), ==, 52);\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\t\tchar *key = xstrfmt(\"key%d\", i);\n     +\t\tchar *value = xstrfmt(\"value%d\", i);\n     +\n    -+\t\tentry = alloc_test_entry(ignore_case, key, \"\");\n    ++\t\tentry = alloc_test_entry(key, \"\", ignore_case);\n     +\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n     +\t\tif (check(removed != NULL))\n     +\t\t\tcheck_str(value, get_value(removed));\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tcheck_int(map->tablesize, ==, 256);\n     +\tcheck_int(hashmap_get_size(map), ==, 40);\n     +\n    -+\tentry = alloc_test_entry(ignore_case, \"key40\", \"\");\n    ++\tentry = alloc_test_entry(\"key40\", \"\", ignore_case);\n     +\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n     +\tif (check(removed != NULL))\n     +\t\tcheck_str(\"value40\", get_value(removed));\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tfree(removed);\n     +}\n     +\n    -+static void t_intern(struct hashmap *map, int ignore_case)\n    ++static void t_intern(struct hashmap *map, unsigned int ignore_case)\n     +{\n     +\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n     +\n\n Makefile                 |   1 +\n t/helper/test-hashmap.c  | 100 +----------\n t/t0011-hashmap.sh       | 260 ----------------------------\n t/unit-tests/t-hashmap.c | 361 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 364 insertions(+), 358 deletions(-)\n delete mode 100755 t/t0011-hashmap.sh\n create mode 100644 t/unit-tests/t-hashmap.c\n\ndiff --git a/Makefile b/Makefile\nindex 3eab701b10..74bb026610 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n+UNIT_TEST_PROGRAMS += t-hashmap\n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-oidtree\n UNIT_TEST_PROGRAMS += t-prio-queue\ndiff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c\nindex 2912899558..7b854a7030 100644\n--- a/t/helper/test-hashmap.c\n+++ b/t/helper/test-hashmap.c\n@@ -12,11 +12,6 @@ struct test_entry\n \tchar key[FLEX_ARRAY];\n };\n \n-static const char *get_value(const struct test_entry *e)\n-{\n-\treturn e->key + strlen(e->key) + 1;\n-}\n-\n static int test_entry_cmp(const void *cmp_data,\n \t\t\t  const struct hashmap_entry *eptr,\n \t\t\t  const struct hashmap_entry *entry_or_key,\n@@ -141,30 +136,16 @@ static void perf_hashmap(unsigned int method, unsigned int rounds)\n /*\n  * Read stdin line by line and print result of commands to stdout:\n  *\n- * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)\n- * put key value -> NULL / old value\n- * get key -> NULL / value\n- * remove key -> NULL / old value\n- * iterate -> key1 value1\\nkey2 value2\\n...\n- * size -> tablesize numentries\n- *\n  * perfhashmap method rounds -> test hashmap.[ch] performance\n  */\n int cmd__hashmap(int argc, const char **argv)\n {\n \tstruct string_list parts = STRING_LIST_INIT_NODUP;\n \tstruct strbuf line = STRBUF_INIT;\n-\tint icase;\n-\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);\n-\n-\t/* init hash map */\n-\ticase = argc > 1 && !strcmp(\"ignorecase\", argv[1]);\n \n \t/* process commands from stdin */\n \twhile (strbuf_getline(&line, stdin) != EOF) {\n \t\tchar *cmd, *p1, *p2;\n-\t\tunsigned int hash = 0;\n-\t\tstruct test_entry *entry;\n \n \t\t/* break line into command and up to two parameters */\n \t\tstring_list_setlen(&parts, 0);\n@@ -180,84 +161,8 @@ int cmd__hashmap(int argc, const char **argv)\n \t\tcmd = parts.items[0].string;\n \t\tp1 = parts.nr >= 1 ? parts.items[1].string : NULL;\n \t\tp2 = parts.nr >= 2 ? parts.items[2].string : NULL;\n-\t\tif (p1)\n-\t\t\thash = icase ? strihash(p1) : strhash(p1);\n-\n-\t\tif (!strcmp(\"add\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add to hashmap */\n-\t\t\thashmap_add(&map, &entry->ent);\n-\n-\t\t} else if (!strcmp(\"put\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add / replace entry */\n-\t\t\tentry = hashmap_put_entry(&map, entry, ent);\n-\n-\t\t\t/* print and free replaced entry, if any */\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"get\", cmd) && p1) {\n-\t\t\t/* lookup entry in hashmap */\n-\t\t\tentry = hashmap_get_entry_from_hash(&map, hash, p1,\n-\t\t\t\t\t\t\tstruct test_entry, ent);\n-\n-\t\t\t/* print result */\n-\t\t\tif (!entry)\n-\t\t\t\tputs(\"NULL\");\n-\t\t\thashmap_for_each_entry_from(&map, entry, ent)\n-\t\t\t\tputs(get_value(entry));\n-\n-\t\t} else if (!strcmp(\"remove\", cmd) && p1) {\n-\n-\t\t\t/* setup static key */\n-\t\t\tstruct hashmap_entry key;\n-\t\t\tstruct hashmap_entry *rm;\n-\t\t\thashmap_entry_init(&key, hash);\n-\n-\t\t\t/* remove entry from hashmap */\n-\t\t\trm = hashmap_remove(&map, &key, p1);\n-\t\t\tentry = rm ? container_of(rm, struct test_entry, ent)\n-\t\t\t\t\t: NULL;\n-\n-\t\t\t/* print result and free entry*/\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"iterate\", cmd)) {\n-\t\t\tstruct hashmap_iter iter;\n-\n-\t\t\thashmap_for_each_entry(&map, &iter, entry,\n-\t\t\t\t\t\tent /* member name */)\n-\t\t\t\tprintf(\"%s %s\\n\", entry->key, get_value(entry));\n-\n-\t\t} else if (!strcmp(\"size\", cmd)) {\n-\n-\t\t\t/* print table sizes */\n-\t\t\tprintf(\"%u %u\\n\", map.tablesize,\n-\t\t\t       hashmap_get_size(&map));\n-\n-\t\t} else if (!strcmp(\"intern\", cmd) && p1) {\n-\n-\t\t\t/* test that strintern works */\n-\t\t\tconst char *i1 = strintern(p1);\n-\t\t\tconst char *i2 = strintern(p1);\n-\t\t\tif (strcmp(i1, p1))\n-\t\t\t\tprintf(\"strintern(%s) returns %s\\n\", p1, i1);\n-\t\t\telse if (i1 == p1)\n-\t\t\t\tprintf(\"strintern(%s) returns input pointer\\n\", p1);\n-\t\t\telse if (i1 != i2)\n-\t\t\t\tprintf(\"strintern(%s) != strintern(%s)\", i1, i2);\n-\t\t\telse\n-\t\t\t\tprintf(\"%s\\n\", i1);\n-\n-\t\t} else if (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n+\t\n+\t\tif (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n \n \t\t\tperf_hashmap(atoi(p1), atoi(p2));\n \n@@ -270,6 +175,5 @@ int cmd__hashmap(int argc, const char **argv)\n \n \tstring_list_clear(&parts, 0);\n \tstrbuf_release(&line);\n-\thashmap_clear_and_free(&map, struct test_entry, ent);\n \treturn 0;\n }\ndiff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh\ndeleted file mode 100755\nindex 46e74ad107..0000000000\n--- a/t/t0011-hashmap.sh\n+++ /dev/null\n@@ -1,260 +0,0 @@\n-#!/bin/sh\n-\n-test_description='test hashmap and string hash functions'\n-\n-TEST_PASSES_SANITIZE_LEAK=true\n-. ./test-lib.sh\n-\n-test_hashmap() {\n-\techo \"$1\" | test-tool hashmap $3 > actual &&\n-\techo \"$2\" > expect &&\n-\ttest_cmp expect actual\n-}\n-\n-test_expect_success 'put' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-NULL\n-NULL\n-NULL\n-64 4\"\n-\n-'\n-\n-test_expect_success 'put (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-size\" \"NULL\n-NULL\n-NULL\n-64 3\" ignorecase\n-\n-'\n-\n-test_expect_success 'replace' '\n-\n-test_hashmap \"put key1 value1\n-put key1 value2\n-put fooBarFrotz value3\n-put fooBarFrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\"\n-\n-'\n-\n-test_expect_success 'replace (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put Key1 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\" ignorecase\n-\n-'\n-\n-test_expect_success 'get' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-get key1\n-get key2\n-get fooBarFrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'get (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-get Key1\n-get keY2\n-get foobarfrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'add' '\n-\n-test_hashmap \"add key1 value1\n-add key1 value2\n-add fooBarFrotz value3\n-add fooBarFrotz value4\n-get key1\n-get fooBarFrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'add (case insensitive)' '\n-\n-test_hashmap \"add key1 value1\n-add Key1 value2\n-add fooBarFrotz value3\n-add foobarfrotz value4\n-get key1\n-get Foobarfrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'remove' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove key1\n-remove key2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\"\n-\n-'\n-\n-test_expect_success 'remove (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove Key1\n-remove keY2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\" ignorecase\n-\n-'\n-\n-test_expect_success 'iterate' '\n-\ttest-tool hashmap >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'iterate (case insensitive)' '\n-\ttest-tool hashmap ignorecase >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'grow / shrink' '\n-\n-\trm -f in &&\n-\trm -f expect &&\n-\tfor n in $(test_seq 51)\n-\tdo\n-\t\techo put key$n value$n >> in &&\n-\t\techo NULL >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 64 51 >> expect &&\n-\techo put key52 value52 >> in &&\n-\techo NULL >> expect &&\n-\techo size >> in &&\n-\techo 256 52 >> expect &&\n-\tfor n in $(test_seq 12)\n-\tdo\n-\t\techo remove key$n >> in &&\n-\t\techo value$n >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 256 40 >> expect &&\n-\techo remove key40 >> in &&\n-\techo value40 >> expect &&\n-\techo size >> in &&\n-\techo 64 39 >> expect &&\n-\ttest-tool hashmap <in >out &&\n-\ttest_cmp expect out\n-\n-'\n-\n-test_expect_success 'string interning' '\n-\n-test_hashmap \"intern value1\n-intern Value1\n-intern value2\n-intern value2\n-\" \"value1\n-Value1\n-value2\n-value2\"\n-\n-'\n-\n-test_done\ndiff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\nnew file mode 100644\nindex 0000000000..7c9b239eca\n--- /dev/null\n+++ b/t/unit-tests/t-hashmap.c\n@@ -0,0 +1,361 @@\n+#include \"test-lib.h\"\n+#include \"hashmap.h\"\n+#include \"strbuf.h\"\n+\n+struct test_entry {\n+\tint padding; /* hashmap entry no longer needs to be the first member */\n+\tstruct hashmap_entry ent;\n+\t/* key and value as two \\0-terminated strings */\n+\tchar key[FLEX_ARRAY];\n+};\n+\n+static int test_entry_cmp(const void *cmp_data,\n+\t\t\t  const struct hashmap_entry *eptr,\n+\t\t\t  const struct hashmap_entry *entry_or_key,\n+\t\t\t  const void *keydata)\n+{\n+\tconst unsigned int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n+\tconst struct test_entry *e1, *e2;\n+\tconst char *key = keydata;\n+\n+\te1 = container_of(eptr, const struct test_entry, ent);\n+\te2 = container_of(entry_or_key, const struct test_entry, ent);\n+\n+\tif (ignore_case)\n+\t\treturn strcasecmp(e1->key, key ? key : e2->key);\n+\telse\n+\t\treturn strcmp(e1->key, key ? key : e2->key);\n+}\n+\n+static const char *get_value(const struct test_entry *e)\n+{\n+\treturn e->key + strlen(e->key) + 1;\n+}\n+\n+static struct test_entry *alloc_test_entry(const char *key, const char *value,\n+\t\t\t\t\t   unsigned int ignore_case)\n+{\n+\tsize_t klen = strlen(key);\n+\tsize_t vlen = strlen(value);\n+\tunsigned int hash = ignore_case ? strihash(key) : strhash(key);\n+\tstruct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n+\n+\thashmap_entry_init(&entry->ent, hash);\n+\tmemcpy(entry->key, key, klen + 1);\n+\tmemcpy(entry->key + klen + 1, value, vlen + 1);\n+\treturn entry;\n+}\n+\n+static struct test_entry *get_test_entry(struct hashmap *map, const char *key,\n+\t\t\t\t\t unsigned int ignore_case)\n+{\n+\treturn hashmap_get_entry_from_hash(\n+\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n+\t\tstruct test_entry, ent);\n+}\n+\n+static int key_val_contains(const char *key_val[][2], char seen[], size_t n,\n+\t\t\t    struct test_entry *entry)\n+{\n+\tfor (size_t i = 0; i < n; i++) {\n+\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n+\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n+\t\t\tif (seen[i])\n+\t\t\t\treturn 2;\n+\t\t\tseen[i] = 1;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n+static void setup(void (*f)(struct hashmap *map, unsigned int ignore_case),\n+\t\t  unsigned int ignore_case)\n+{\n+\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n+\n+\tf(&map, ignore_case);\n+\thashmap_clear_and_free(&map, struct test_entry, ent);\n+}\n+\n+static void t_replace(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\n+\tentry = alloc_test_entry(\"key1\", \"value1\", ignore_case);\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\n+\tentry = alloc_test_entry(ignore_case ? \"Key1\" : \"key1\", \"value2\",\n+\t\t\t\t ignore_case);\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value1\");\n+\tfree(entry);\n+\n+\tentry = alloc_test_entry(\"fooBarFrotz\", \"value3\", ignore_case);\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\n+\tentry = alloc_test_entry(ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\",\n+\t\t\t\t \"value4\", ignore_case);\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value3\");\n+\tfree(entry);\n+}\n+\n+static void t_get(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n+\t\t\t\t     { ignore_case ? \"TeNor\" : \"tenor\",\n+\t\t\t\t       ignore_case ? \"value4\" : \"value5\" } };\n+\tconst char *query[][2] = {\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n+\t\t{ ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n+\t\t{ ignore_case ? \"TENOR\" : \"tenor\",\n+\t\t  ignore_case ? \"value4\" : \"value5\" }\n+\t};\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1],\n+\t\t\t\t\t ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n+\t\tentry = get_test_entry(map, query[i][0], ignore_case);\n+\t\tif (check(entry != NULL))\n+\t\t\tcheck_str(get_value(entry), query[i][1]);\n+\t\telse\n+\t\t\ttest_msg(\"query key: %s\", query[i][0]);\n+\t}\n+\n+\tcheck_pointer_eq(get_test_entry(map, \"notInMap\", ignore_case), NULL);\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+}\n+\n+static void t_add(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = {\n+\t\t{ \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n+\t\t{ \"fooBarFrotz\", \"value3\" },\n+\t\t{ ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value4\" }\n+\t};\n+\tconst char *query_keys[] = { \"key1\", ignore_case ? \"FOObarFrotz\" :\n+\t\t\t\t\t\t\t   \"fooBarFrotz\" };\n+\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n+\t\thashmap_add(map, &entry->ent);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(query_keys); i++) {\n+\t\tint count = 0;\n+\t\tentry = hashmap_get_entry_from_hash(map,\n+\t\t\tignore_case ? strihash(query_keys[i]) :\n+\t\t\t\t      strhash(query_keys[i]),\n+\t\t\tquery_keys[i], struct test_entry, ent);\n+\n+\t\thashmap_for_each_entry_from(map, entry, ent)\n+\t\t{\n+\t\t\tint ret;\n+\t\t\tif (!check_int((ret = key_val_contains(\n+\t\t\t\t\t\tkey_val, seen,\n+\t\t\t\t\t\tARRAY_SIZE(key_val), entry)),\n+\t\t\t\t       ==, 0)) {\n+\t\t\t\tswitch (ret) {\n+\t\t\t\tcase 1:\n+\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 2:\n+\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tcount++;\n+\t\t\t}\n+\t\t}\n+\t\tcheck_int(count, ==, 2);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n+\t\tif (!check_int(seen[i], ==, 1))\n+\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n+\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t key_val[i][0], key_val[i][1]);\n+\t}\n+\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+\tcheck_pointer_eq(get_test_entry(map, \"notInMap\", ignore_case), NULL);\n+}\n+\n+static void t_remove(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n+\t\tentry = alloc_test_entry(remove[i][0], \"\", ignore_case);\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(get_value(removed), remove[i][1]);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\n+\tentry = alloc_test_entry(\"notInMap\", \"\", ignore_case);\n+\tcheck_pointer_eq(hashmap_remove_entry(map, entry, ent, \"notInMap\"), NULL);\n+\tfree(entry);\n+\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));\n+}\n+\n+static void t_iterate(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tstruct hashmap_iter iter;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n+\t{\n+\t\tint ret;\n+\t\tif (!check_int((ret = key_val_contains(key_val, seen,\n+\t\t\t\t\t\t       ARRAY_SIZE(key_val),\n+\t\t\t\t\t\t       entry)), ==, 0)) {\n+\t\t\tswitch (ret) {\n+\t\t\tcase 1:\n+\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\tcase 2:\n+\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n+\t\tif (!check_int(seen[i], ==, 1))\n+\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n+\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t key_val[i][0], key_val[i][1]);\n+\t}\n+\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+}\n+\n+static void t_alloc(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\n+\tfor (int i = 1; i <= 51; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\t\tentry = alloc_test_entry(key, value, ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t\tfree(key);\n+\t\tfree(value);\n+\t}\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 51);\n+\n+\tentry = alloc_test_entry(\"key52\", \"value52\", ignore_case);\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 52);\n+\n+\tfor (int i = 1; i <= 12; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\n+\t\tentry = alloc_test_entry(key, \"\", ignore_case);\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(value, get_value(removed));\n+\t\tfree(key);\n+\t\tfree(value);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 40);\n+\n+\tentry = alloc_test_entry(\"key40\", \"\", ignore_case);\n+\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n+\tif (check(removed != NULL))\n+\t\tcheck_str(\"value40\", get_value(removed));\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 39);\n+\tfree(entry);\n+\tfree(removed);\n+}\n+\n+static void t_intern(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n+\t\tconst char *i1 = strintern(values[i]);\n+\t\tconst char *i2 = strintern(values[i]);\n+\n+\t\tif (!check(!strcmp(i1, values[i])))\n+\t\t\ttest_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n+\t\telse if (!check(i1 != values[i]))\n+\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n+\t\t\t\t values[i]);\n+\t\telse if (!check_pointer_eq(i1, i2))\n+\t\t\ttest_msg(\"address('%s') != address('%s'), so strintern('%s') != strintern('%s')\",\n+\t\t\t\t i1, i2, values[i], values[i]);\n+\t\telse\n+\t\t\tcheck_str(i1, values[i]);\n+\t}\n+}\n+\n+int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+{\n+\tTEST(setup(t_replace, 0), \"replace works\");\n+\tTEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n+\tTEST(setup(t_get, 0), \"get works\");\n+\tTEST(setup(t_get, 1), \"get (case insensitive) works\");\n+\tTEST(setup(t_add, 0), \"add works\");\n+\tTEST(setup(t_add, 1), \"add (case insensitive) works\");\n+\tTEST(setup(t_remove, 0), \"remove works\");\n+\tTEST(setup(t_remove, 1), \"remove (case insensitive) works\");\n+\tTEST(setup(t_iterate, 0), \"iterate works\");\n+\tTEST(setup(t_iterate, 1), \"iterate (case insensitive) works\");\n+\tTEST(setup(t_alloc, 0), \"grow / shrink works\");\n+\tTEST(setup(t_intern, 0), \"string interning works\");\n+\treturn test_done();\n+}\n-- \n2.45.2\n\n"},{"id":"499727","messageId":"CAP8UFD04uQbWhY0QDp+nC8VyBvpUs=tBDm=w4CzKK2GcwFZdGQ@mail.gmail.com","threadId":"61692","inReplyTo":"20240730115101.188100-1-author@example.com","subject":"Re: [PATCH v4] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2024-07-31T17:18:08Z","receivedAt":"2024-07-31T17:18:23Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"Hi Ghanshyam,\n\nNot sure why the \"From:\" header of this email says:\n\n\"From: A U Thor <shyamthakkar001@gmail.com>\"\n\nDid you make changes to your config or something?\n\nOn Tue, Jul 30, 2024 at 1:52 PM A U Thor <shyamthakkar001@gmail.com> wrote:\n>\n> From: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n\n\n> Changes in v4:\n> - update commit message to add a reference and fix typo\n> - change 'int ignore_case' to 'unsigned int ignore_case'\n> - make 'ignore_case' the last parameter in all the functions\n> - update t_get() to add a testcase query for better ignore-case\n>   checking\n>\n> Range-diff against v3:\n\n\n>     -+static void t_get(struct hashmap *map, int ignore_case)\n>     ++static void t_get(struct hashmap *map, unsigned int ignore_case)\n>      +{\n>      +  struct test_entry *entry;\n>      +  const char *key_val[][2] = { { \"key1\", \"value1\" },\n>      +                               { \"key2\", \"value2\" },\n>      +                               { \"fooBarFrotz\", \"value3\" },\n>     -+                               { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n>     ++                               { ignore_case ? \"TeNor\" : \"tenor\",\n>     ++                                 ignore_case ? \"value4\" : \"value5\" } };\n>      +  const char *query[][2] = {\n>      +          { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n>      +          { ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n>     -+          { ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n>     ++          { ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n>     ++          { ignore_case ? \"TENOR\" : \"tenor\",\n>     ++            ignore_case ? \"value4\" : \"value5\" }\n>      +  };\n\nI suggested adding the following test case:\n\n               { ignore_case ? \"FOOBarFrotZ\" : \"foobarfrotz\",\n                 ignore_case ? : \"value3\" : \"value4\" }\n\nto better check that things work well especially when not ignoring the case.\n\nThis is because, when not ignoring the case, there used to be a choice\nbetween { \"fooBarFrotz\", \"value3\" } and { \"foobarfrotz\", \"value4\" }\nthat can be decided only by the case of the key in 'query'. But\ninstead you removed that choice from 'key_val'.\n\nAnd it seems to me that the new test case you added doesn't bring much value.\n\nMaybe there was something wrong in what I suggested, but then please explain it.\n\nThe other changes look good to me and seem to address the suggestions\nmade by others and me.\n\nThanks!\n"},{"id":"499735","messageId":"xmqq7cd12y5r.fsf@gitster.g","threadId":"61692","inReplyTo":"CAP8UFD04uQbWhY0QDp+nC8VyBvpUs=tBDm=w4CzKK2GcwFZdGQ@mail.gmail.com","subject":"Re: [PATCH v4] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-07-31T18:50:56Z","receivedAt":"2024-07-31T18:51:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n>>      +  const char *query[][2] = {\n>>      +          { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n>>      +          { ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n>>     -+          { ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n>>     ++          { ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n>>     ++          { ignore_case ? \"TENOR\" : \"tenor\",\n>>     ++            ignore_case ? \"value4\" : \"value5\" }\n>>      +  };\n>\n> I suggested adding the following test case:\n>\n>                { ignore_case ? \"FOOBarFrotZ\" : \"foobarfrotz\",\n>                  ignore_case ? : \"value3\" : \"value4\" }\n>\n> to better check that things work well especially when not ignoring the case.\n>\n> This is because, when not ignoring the case, there used to be a choice\n> between { \"fooBarFrotz\", \"value3\" } and { \"foobarfrotz\", \"value4\" }\n> that can be decided only by the case of the key in 'query'. But\n> instead you removed that choice from 'key_val'.\n>\n> And it seems to me that the new test case you added doesn't bring much value.\n>\n> Maybe there was something wrong in what I suggested, but then please explain it.\n>\n> The other changes look good to me and seem to address the suggestions\n> made by others and me.\n>\n> Thanks!\n\nThanks for a careful review, and of course thanks for working on the\npatch, A U Thor ;-)\n"},{"id":"499922","messageId":"D352L1G9SF8Z.1NMPOJB2YOLRT@gmail.com","threadId":"61692","inReplyTo":"CAP8UFD04uQbWhY0QDp+nC8VyBvpUs=tBDm=w4CzKK2GcwFZdGQ@mail.gmail.com","subject":"Re: [PATCH v4] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-08-02T02:28:36Z","receivedAt":"2024-08-02T02:28:41Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"Christian Couder <christian.couder@gmail.com> wrote:\n> Hi Ghanshyam,\n>\n> Not sure why the \"From:\" header of this email says:\n>\n> \"From: A U Thor <shyamthakkar001@gmail.com>\"\n>\n> Did you make changes to your config or something?\n\nI think I must have changed the user to \"A U Thor <author@exmaple.com>\"\nin the repo config while testing something, but don't consciously remember\ndoing so.\n\n>\n> On Tue, Jul 30, 2024 at 1:52 PM A U Thor <shyamthakkar001@gmail.com>\n> wrote:\n> >\n> > From: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n>\n>\n> > Changes in v4:\n> > - update commit message to add a reference and fix typo\n> > - change 'int ignore_case' to 'unsigned int ignore_case'\n> > - make 'ignore_case' the last parameter in all the functions\n> > - update t_get() to add a testcase query for better ignore-case\n> >   checking\n> >\n> > Range-diff against v3:\n>\n>\n> >     -+static void t_get(struct hashmap *map, int ignore_case)\n> >     ++static void t_get(struct hashmap *map, unsigned int ignore_case)\n> >      +{\n> >      +  struct test_entry *entry;\n> >      +  const char *key_val[][2] = { { \"key1\", \"value1\" },\n> >      +                               { \"key2\", \"value2\" },\n> >      +                               { \"fooBarFrotz\", \"value3\" },\n> >     -+                               { ignore_case ? \"key4\" : \"foobarfrotz\", \"value4\" } };\n> >     ++                               { ignore_case ? \"TeNor\" : \"tenor\",\n> >     ++                                 ignore_case ? \"value4\" : \"value5\" } };\n> >      +  const char *query[][2] = {\n> >      +          { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n> >      +          { ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n> >     -+          { ignore_case ? \"foobarfrotz\" : \"fooBarFrotz\", \"value3\" }\n> >     ++          { ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n> >     ++          { ignore_case ? \"TENOR\" : \"tenor\",\n> >     ++            ignore_case ? \"value4\" : \"value5\" }\n> >      +  };\n>\n> I suggested adding the following test case:\n>\n> { ignore_case ? \"FOOBarFrotZ\" : \"foobarfrotz\",\n> ignore_case ? : \"value3\" : \"value4\" }\n>\n> to better check that things work well especially when not ignoring the\n> case.\n>\n> This is because, when not ignoring the case, there used to be a choice\n> between { \"fooBarFrotz\", \"value3\" } and { \"foobarfrotz\", \"value4\" }\n> that can be decided only by the case of the key in 'query'. But\n> instead you removed that choice from 'key_val'.\n\nYeah, you're correct about the choice thing. I think I misunderstood\nyour comments from the previous version. Will update.\n\nThanks.\n\n"},{"id":"500002","messageId":"20240803133517.73308-2-shyamthakkar001@gmail.com","threadId":"61692","inReplyTo":"20240730115101.188100-1-author@example.com","subject":"[GSoC][PATCH v5] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-08-03T13:34:49Z","receivedAt":"2024-08-03T13:35:59Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\nlibrary. Migrate them to the unit testing framework for better\ndebugging, runtime performance and concise code.\n\nAlong with the migration, make 'add' tests from the shell script order\nagnostic in unit tests, since they iterate over entries with the same\nkeys and we do not guarantee the order. This was already done for the\n'iterate' tests[1].\n\nThe helper/test-hashmap.c is still not removed because it contains a\nperformance test meant to be run by the user directly (not used in\nt/perf). And it makes sense for such a utility to be a helper.\n\n[1]: e1e7a77141 (t: sort output of hashmap iteration, 2019-07-30)\n\nMentored-by: Christian Couder <chriscool@tuxfamily.org>\nMentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\nHelped-by: Josh Steadmon <steadmon@google.com>\nHelped-by: Junio C Hamano <gitster@pobox.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n---\nChanges in v5:\n- modify one of the inputs and queries inside t_get() to better check\n  ignore-case scenario. This should address the comments from\n  Christian in v4.\n\n Makefile                 |   1 +\n t/helper/test-hashmap.c  | 100 +----------\n t/t0011-hashmap.sh       | 260 ----------------------------\n t/unit-tests/t-hashmap.c | 361 +++++++++++++++++++++++++++++++++++++++\n 4 files changed, 364 insertions(+), 358 deletions(-)\n delete mode 100755 t/t0011-hashmap.sh\n create mode 100644 t/unit-tests/t-hashmap.c\n\ndiff --git a/Makefile b/Makefile\nindex 3eab701b10..74bb026610 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%\n UNIT_TEST_PROGRAMS += t-ctype\n UNIT_TEST_PROGRAMS += t-example-decorate\n UNIT_TEST_PROGRAMS += t-hash\n+UNIT_TEST_PROGRAMS += t-hashmap\n UNIT_TEST_PROGRAMS += t-mem-pool\n UNIT_TEST_PROGRAMS += t-oidtree\n UNIT_TEST_PROGRAMS += t-prio-queue\ndiff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c\nindex 2912899558..7b854a7030 100644\n--- a/t/helper/test-hashmap.c\n+++ b/t/helper/test-hashmap.c\n@@ -12,11 +12,6 @@ struct test_entry\n \tchar key[FLEX_ARRAY];\n };\n \n-static const char *get_value(const struct test_entry *e)\n-{\n-\treturn e->key + strlen(e->key) + 1;\n-}\n-\n static int test_entry_cmp(const void *cmp_data,\n \t\t\t  const struct hashmap_entry *eptr,\n \t\t\t  const struct hashmap_entry *entry_or_key,\n@@ -141,30 +136,16 @@ static void perf_hashmap(unsigned int method, unsigned int rounds)\n /*\n  * Read stdin line by line and print result of commands to stdout:\n  *\n- * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)\n- * put key value -> NULL / old value\n- * get key -> NULL / value\n- * remove key -> NULL / old value\n- * iterate -> key1 value1\\nkey2 value2\\n...\n- * size -> tablesize numentries\n- *\n  * perfhashmap method rounds -> test hashmap.[ch] performance\n  */\n int cmd__hashmap(int argc, const char **argv)\n {\n \tstruct string_list parts = STRING_LIST_INIT_NODUP;\n \tstruct strbuf line = STRBUF_INIT;\n-\tint icase;\n-\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);\n-\n-\t/* init hash map */\n-\ticase = argc > 1 && !strcmp(\"ignorecase\", argv[1]);\n \n \t/* process commands from stdin */\n \twhile (strbuf_getline(&line, stdin) != EOF) {\n \t\tchar *cmd, *p1, *p2;\n-\t\tunsigned int hash = 0;\n-\t\tstruct test_entry *entry;\n \n \t\t/* break line into command and up to two parameters */\n \t\tstring_list_setlen(&parts, 0);\n@@ -180,84 +161,8 @@ int cmd__hashmap(int argc, const char **argv)\n \t\tcmd = parts.items[0].string;\n \t\tp1 = parts.nr >= 1 ? parts.items[1].string : NULL;\n \t\tp2 = parts.nr >= 2 ? parts.items[2].string : NULL;\n-\t\tif (p1)\n-\t\t\thash = icase ? strihash(p1) : strhash(p1);\n-\n-\t\tif (!strcmp(\"add\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add to hashmap */\n-\t\t\thashmap_add(&map, &entry->ent);\n-\n-\t\t} else if (!strcmp(\"put\", cmd) && p1 && p2) {\n-\n-\t\t\t/* create entry with key = p1, value = p2 */\n-\t\t\tentry = alloc_test_entry(hash, p1, p2);\n-\n-\t\t\t/* add / replace entry */\n-\t\t\tentry = hashmap_put_entry(&map, entry, ent);\n-\n-\t\t\t/* print and free replaced entry, if any */\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"get\", cmd) && p1) {\n-\t\t\t/* lookup entry in hashmap */\n-\t\t\tentry = hashmap_get_entry_from_hash(&map, hash, p1,\n-\t\t\t\t\t\t\tstruct test_entry, ent);\n-\n-\t\t\t/* print result */\n-\t\t\tif (!entry)\n-\t\t\t\tputs(\"NULL\");\n-\t\t\thashmap_for_each_entry_from(&map, entry, ent)\n-\t\t\t\tputs(get_value(entry));\n-\n-\t\t} else if (!strcmp(\"remove\", cmd) && p1) {\n-\n-\t\t\t/* setup static key */\n-\t\t\tstruct hashmap_entry key;\n-\t\t\tstruct hashmap_entry *rm;\n-\t\t\thashmap_entry_init(&key, hash);\n-\n-\t\t\t/* remove entry from hashmap */\n-\t\t\trm = hashmap_remove(&map, &key, p1);\n-\t\t\tentry = rm ? container_of(rm, struct test_entry, ent)\n-\t\t\t\t\t: NULL;\n-\n-\t\t\t/* print result and free entry*/\n-\t\t\tputs(entry ? get_value(entry) : \"NULL\");\n-\t\t\tfree(entry);\n-\n-\t\t} else if (!strcmp(\"iterate\", cmd)) {\n-\t\t\tstruct hashmap_iter iter;\n-\n-\t\t\thashmap_for_each_entry(&map, &iter, entry,\n-\t\t\t\t\t\tent /* member name */)\n-\t\t\t\tprintf(\"%s %s\\n\", entry->key, get_value(entry));\n-\n-\t\t} else if (!strcmp(\"size\", cmd)) {\n-\n-\t\t\t/* print table sizes */\n-\t\t\tprintf(\"%u %u\\n\", map.tablesize,\n-\t\t\t       hashmap_get_size(&map));\n-\n-\t\t} else if (!strcmp(\"intern\", cmd) && p1) {\n-\n-\t\t\t/* test that strintern works */\n-\t\t\tconst char *i1 = strintern(p1);\n-\t\t\tconst char *i2 = strintern(p1);\n-\t\t\tif (strcmp(i1, p1))\n-\t\t\t\tprintf(\"strintern(%s) returns %s\\n\", p1, i1);\n-\t\t\telse if (i1 == p1)\n-\t\t\t\tprintf(\"strintern(%s) returns input pointer\\n\", p1);\n-\t\t\telse if (i1 != i2)\n-\t\t\t\tprintf(\"strintern(%s) != strintern(%s)\", i1, i2);\n-\t\t\telse\n-\t\t\t\tprintf(\"%s\\n\", i1);\n-\n-\t\t} else if (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n+\t\n+\t\tif (!strcmp(\"perfhashmap\", cmd) && p1 && p2) {\n \n \t\t\tperf_hashmap(atoi(p1), atoi(p2));\n \n@@ -270,6 +175,5 @@ int cmd__hashmap(int argc, const char **argv)\n \n \tstring_list_clear(&parts, 0);\n \tstrbuf_release(&line);\n-\thashmap_clear_and_free(&map, struct test_entry, ent);\n \treturn 0;\n }\ndiff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh\ndeleted file mode 100755\nindex 46e74ad107..0000000000\n--- a/t/t0011-hashmap.sh\n+++ /dev/null\n@@ -1,260 +0,0 @@\n-#!/bin/sh\n-\n-test_description='test hashmap and string hash functions'\n-\n-TEST_PASSES_SANITIZE_LEAK=true\n-. ./test-lib.sh\n-\n-test_hashmap() {\n-\techo \"$1\" | test-tool hashmap $3 > actual &&\n-\techo \"$2\" > expect &&\n-\ttest_cmp expect actual\n-}\n-\n-test_expect_success 'put' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-NULL\n-NULL\n-NULL\n-64 4\"\n-\n-'\n-\n-test_expect_success 'put (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-size\" \"NULL\n-NULL\n-NULL\n-64 3\" ignorecase\n-\n-'\n-\n-test_expect_success 'replace' '\n-\n-test_hashmap \"put key1 value1\n-put key1 value2\n-put fooBarFrotz value3\n-put fooBarFrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\"\n-\n-'\n-\n-test_expect_success 'replace (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put Key1 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-size\" \"NULL\n-value1\n-NULL\n-value3\n-64 2\" ignorecase\n-\n-'\n-\n-test_expect_success 'get' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-put foobarfrotz value4\n-get key1\n-get key2\n-get fooBarFrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'get (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-get Key1\n-get keY2\n-get foobarfrotz\n-get notInMap\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'add' '\n-\n-test_hashmap \"add key1 value1\n-add key1 value2\n-add fooBarFrotz value3\n-add fooBarFrotz value4\n-get key1\n-get fooBarFrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\"\n-\n-'\n-\n-test_expect_success 'add (case insensitive)' '\n-\n-test_hashmap \"add key1 value1\n-add Key1 value2\n-add fooBarFrotz value3\n-add foobarfrotz value4\n-get key1\n-get Foobarfrotz\n-get notInMap\" \"value2\n-value1\n-value4\n-value3\n-NULL\" ignorecase\n-\n-'\n-\n-test_expect_success 'remove' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove key1\n-remove key2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\"\n-\n-'\n-\n-test_expect_success 'remove (case insensitive)' '\n-\n-test_hashmap \"put key1 value1\n-put key2 value2\n-put fooBarFrotz value3\n-remove Key1\n-remove keY2\n-remove notInMap\n-size\" \"NULL\n-NULL\n-NULL\n-value1\n-value2\n-NULL\n-64 1\" ignorecase\n-\n-'\n-\n-test_expect_success 'iterate' '\n-\ttest-tool hashmap >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'iterate (case insensitive)' '\n-\ttest-tool hashmap ignorecase >actual.raw <<-\\EOF &&\n-\tput key1 value1\n-\tput key2 value2\n-\tput fooBarFrotz value3\n-\titerate\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tNULL\n-\tNULL\n-\tNULL\n-\tfooBarFrotz value3\n-\tkey1 value1\n-\tkey2 value2\n-\tEOF\n-\n-\tsort <actual.raw >actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'grow / shrink' '\n-\n-\trm -f in &&\n-\trm -f expect &&\n-\tfor n in $(test_seq 51)\n-\tdo\n-\t\techo put key$n value$n >> in &&\n-\t\techo NULL >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 64 51 >> expect &&\n-\techo put key52 value52 >> in &&\n-\techo NULL >> expect &&\n-\techo size >> in &&\n-\techo 256 52 >> expect &&\n-\tfor n in $(test_seq 12)\n-\tdo\n-\t\techo remove key$n >> in &&\n-\t\techo value$n >> expect || return 1\n-\tdone &&\n-\techo size >> in &&\n-\techo 256 40 >> expect &&\n-\techo remove key40 >> in &&\n-\techo value40 >> expect &&\n-\techo size >> in &&\n-\techo 64 39 >> expect &&\n-\ttest-tool hashmap <in >out &&\n-\ttest_cmp expect out\n-\n-'\n-\n-test_expect_success 'string interning' '\n-\n-test_hashmap \"intern value1\n-intern Value1\n-intern value2\n-intern value2\n-\" \"value1\n-Value1\n-value2\n-value2\"\n-\n-'\n-\n-test_done\ndiff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c\nnew file mode 100644\nindex 0000000000..09a48c2c4e\n--- /dev/null\n+++ b/t/unit-tests/t-hashmap.c\n@@ -0,0 +1,361 @@\n+#include \"test-lib.h\"\n+#include \"hashmap.h\"\n+#include \"strbuf.h\"\n+\n+struct test_entry {\n+\tint padding; /* hashmap entry no longer needs to be the first member */\n+\tstruct hashmap_entry ent;\n+\t/* key and value as two \\0-terminated strings */\n+\tchar key[FLEX_ARRAY];\n+};\n+\n+static int test_entry_cmp(const void *cmp_data,\n+\t\t\t  const struct hashmap_entry *eptr,\n+\t\t\t  const struct hashmap_entry *entry_or_key,\n+\t\t\t  const void *keydata)\n+{\n+\tconst unsigned int ignore_case = cmp_data ? *((int *)cmp_data) : 0;\n+\tconst struct test_entry *e1, *e2;\n+\tconst char *key = keydata;\n+\n+\te1 = container_of(eptr, const struct test_entry, ent);\n+\te2 = container_of(entry_or_key, const struct test_entry, ent);\n+\n+\tif (ignore_case)\n+\t\treturn strcasecmp(e1->key, key ? key : e2->key);\n+\telse\n+\t\treturn strcmp(e1->key, key ? key : e2->key);\n+}\n+\n+static const char *get_value(const struct test_entry *e)\n+{\n+\treturn e->key + strlen(e->key) + 1;\n+}\n+\n+static struct test_entry *alloc_test_entry(const char *key, const char *value,\n+\t\t\t\t\t   unsigned int ignore_case)\n+{\n+\tsize_t klen = strlen(key);\n+\tsize_t vlen = strlen(value);\n+\tunsigned int hash = ignore_case ? strihash(key) : strhash(key);\n+\tstruct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));\n+\n+\thashmap_entry_init(&entry->ent, hash);\n+\tmemcpy(entry->key, key, klen + 1);\n+\tmemcpy(entry->key + klen + 1, value, vlen + 1);\n+\treturn entry;\n+}\n+\n+static struct test_entry *get_test_entry(struct hashmap *map, const char *key,\n+\t\t\t\t\t unsigned int ignore_case)\n+{\n+\treturn hashmap_get_entry_from_hash(\n+\t\tmap, ignore_case ? strihash(key) : strhash(key), key,\n+\t\tstruct test_entry, ent);\n+}\n+\n+static int key_val_contains(const char *key_val[][2], char seen[], size_t n,\n+\t\t\t    struct test_entry *entry)\n+{\n+\tfor (size_t i = 0; i < n; i++) {\n+\t\tif (!strcmp(entry->key, key_val[i][0]) &&\n+\t\t    !strcmp(get_value(entry), key_val[i][1])) {\n+\t\t\tif (seen[i])\n+\t\t\t\treturn 2;\n+\t\t\tseen[i] = 1;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 1;\n+}\n+\n+static void setup(void (*f)(struct hashmap *map, unsigned int ignore_case),\n+\t\t  unsigned int ignore_case)\n+{\n+\tstruct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);\n+\n+\tf(&map, ignore_case);\n+\thashmap_clear_and_free(&map, struct test_entry, ent);\n+}\n+\n+static void t_replace(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\n+\tentry = alloc_test_entry(\"key1\", \"value1\", ignore_case);\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\n+\tentry = alloc_test_entry(ignore_case ? \"Key1\" : \"key1\", \"value2\",\n+\t\t\t\t ignore_case);\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value1\");\n+\tfree(entry);\n+\n+\tentry = alloc_test_entry(\"fooBarFrotz\", \"value3\", ignore_case);\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\n+\tentry = alloc_test_entry(ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\",\n+\t\t\t\t \"value4\", ignore_case);\n+\tentry = hashmap_put_entry(map, entry, ent);\n+\tif (check(entry != NULL))\n+\t\tcheck_str(get_value(entry), \"value3\");\n+\tfree(entry);\n+}\n+\n+static void t_get(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n+\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\",\n+\t\t\t\t       \"value4\" } };\n+\tconst char *query[][2] = {\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n+\t\t{ ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n+\t\t{ ignore_case ? \"FOObarFrotz\" : \"foobarfrotz\",\n+\t\t  ignore_case ? \"value3\" : \"value4\" }\n+\t};\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1],\n+\t\t\t\t\t ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(query); i++) {\n+\t\tentry = get_test_entry(map, query[i][0], ignore_case);\n+\t\tif (check(entry != NULL))\n+\t\t\tcheck_str(get_value(entry), query[i][1]);\n+\t\telse\n+\t\t\ttest_msg(\"query key: %s\", query[i][0]);\n+\t}\n+\n+\tcheck_pointer_eq(get_test_entry(map, \"notInMap\", ignore_case), NULL);\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+}\n+\n+static void t_add(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tconst char *key_val[][2] = {\n+\t\t{ \"key1\", \"value1\" },\n+\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value2\" },\n+\t\t{ \"fooBarFrotz\", \"value3\" },\n+\t\t{ ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value4\" }\n+\t};\n+\tconst char *query_keys[] = { \"key1\", ignore_case ? \"FOObarFrotz\" :\n+\t\t\t\t\t\t\t   \"fooBarFrotz\" };\n+\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n+\t\thashmap_add(map, &entry->ent);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(query_keys); i++) {\n+\t\tint count = 0;\n+\t\tentry = hashmap_get_entry_from_hash(map,\n+\t\t\tignore_case ? strihash(query_keys[i]) :\n+\t\t\t\t      strhash(query_keys[i]),\n+\t\t\tquery_keys[i], struct test_entry, ent);\n+\n+\t\thashmap_for_each_entry_from(map, entry, ent)\n+\t\t{\n+\t\t\tint ret;\n+\t\t\tif (!check_int((ret = key_val_contains(\n+\t\t\t\t\t\tkey_val, seen,\n+\t\t\t\t\t\tARRAY_SIZE(key_val), entry)),\n+\t\t\t\t       ==, 0)) {\n+\t\t\t\tswitch (ret) {\n+\t\t\t\tcase 1:\n+\t\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase 2:\n+\t\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tcount++;\n+\t\t\t}\n+\t\t}\n+\t\tcheck_int(count, ==, 2);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n+\t\tif (!check_int(seen[i], ==, 1))\n+\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n+\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t key_val[i][0], key_val[i][1]);\n+\t}\n+\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+\tcheck_pointer_eq(get_test_entry(map, \"notInMap\", ignore_case), NULL);\n+}\n+\n+static void t_remove(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\tconst char *remove[][2] = { { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n+\t\t\t\t    { ignore_case ? \"keY2\" : \"key2\", \"value2\" } };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(remove); i++) {\n+\t\tentry = alloc_test_entry(remove[i][0], \"\", ignore_case);\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, remove[i][0]);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(get_value(removed), remove[i][1]);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\n+\tentry = alloc_test_entry(\"notInMap\", \"\", ignore_case);\n+\tcheck_pointer_eq(hashmap_remove_entry(map, entry, ent, \"notInMap\"), NULL);\n+\tfree(entry);\n+\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));\n+}\n+\n+static void t_iterate(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry;\n+\tstruct hashmap_iter iter;\n+\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n+\t\t\t\t     { \"key2\", \"value2\" },\n+\t\t\t\t     { \"fooBarFrotz\", \"value3\" } };\n+\tchar seen[ARRAY_SIZE(key_val)] = { 0 };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n+\t\tentry = alloc_test_entry(key_val[i][0], key_val[i][1], ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t}\n+\n+\thashmap_for_each_entry(map, &iter, entry, ent /* member name */)\n+\t{\n+\t\tint ret;\n+\t\tif (!check_int((ret = key_val_contains(key_val, seen,\n+\t\t\t\t\t\t       ARRAY_SIZE(key_val),\n+\t\t\t\t\t\t       entry)), ==, 0)) {\n+\t\t\tswitch (ret) {\n+\t\t\tcase 1:\n+\t\t\t\ttest_msg(\"found entry was not given in the input\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\tcase 2:\n+\t\t\t\ttest_msg(\"duplicate entry detected\\n\"\n+\t\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t\t entry->key, get_value(entry));\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(seen); i++) {\n+\t\tif (!check_int(seen[i], ==, 1))\n+\t\t\ttest_msg(\"following key-val pair was not iterated over:\\n\"\n+\t\t\t\t \"    key: %s\\n  value: %s\",\n+\t\t\t\t key_val[i][0], key_val[i][1]);\n+\t}\n+\n+\tcheck_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));\n+}\n+\n+static void t_alloc(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tstruct test_entry *entry, *removed;\n+\n+\tfor (int i = 1; i <= 51; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\t\tentry = alloc_test_entry(key, value, ignore_case);\n+\t\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\t\tfree(key);\n+\t\tfree(value);\n+\t}\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 51);\n+\n+\tentry = alloc_test_entry(\"key52\", \"value52\", ignore_case);\n+\tcheck_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 52);\n+\n+\tfor (int i = 1; i <= 12; i++) {\n+\t\tchar *key = xstrfmt(\"key%d\", i);\n+\t\tchar *value = xstrfmt(\"value%d\", i);\n+\n+\t\tentry = alloc_test_entry(key, \"\", ignore_case);\n+\t\tremoved = hashmap_remove_entry(map, entry, ent, key);\n+\t\tif (check(removed != NULL))\n+\t\t\tcheck_str(value, get_value(removed));\n+\t\tfree(key);\n+\t\tfree(value);\n+\t\tfree(entry);\n+\t\tfree(removed);\n+\t}\n+\tcheck_int(map->tablesize, ==, 256);\n+\tcheck_int(hashmap_get_size(map), ==, 40);\n+\n+\tentry = alloc_test_entry(\"key40\", \"\", ignore_case);\n+\tremoved = hashmap_remove_entry(map, entry, ent, \"key40\");\n+\tif (check(removed != NULL))\n+\t\tcheck_str(\"value40\", get_value(removed));\n+\tcheck_int(map->tablesize, ==, 64);\n+\tcheck_int(hashmap_get_size(map), ==, 39);\n+\tfree(entry);\n+\tfree(removed);\n+}\n+\n+static void t_intern(struct hashmap *map, unsigned int ignore_case)\n+{\n+\tconst char *values[] = { \"value1\", \"Value1\", \"value2\", \"value2\" };\n+\n+\tfor (size_t i = 0; i < ARRAY_SIZE(values); i++) {\n+\t\tconst char *i1 = strintern(values[i]);\n+\t\tconst char *i2 = strintern(values[i]);\n+\n+\t\tif (!check(!strcmp(i1, values[i])))\n+\t\t\ttest_msg(\"strintern(%s) returns %s\\n\", values[i], i1);\n+\t\telse if (!check(i1 != values[i]))\n+\t\t\ttest_msg(\"strintern(%s) returns input pointer\\n\",\n+\t\t\t\t values[i]);\n+\t\telse if (!check_pointer_eq(i1, i2))\n+\t\t\ttest_msg(\"address('%s') != address('%s'), so strintern('%s') != strintern('%s')\",\n+\t\t\t\t i1, i2, values[i], values[i]);\n+\t\telse\n+\t\t\tcheck_str(i1, values[i]);\n+\t}\n+}\n+\n+int cmd_main(int argc UNUSED, const char **argv UNUSED)\n+{\n+\tTEST(setup(t_replace, 0), \"replace works\");\n+\tTEST(setup(t_replace, 1), \"replace (case insensitive) works\");\n+\tTEST(setup(t_get, 0), \"get works\");\n+\tTEST(setup(t_get, 1), \"get (case insensitive) works\");\n+\tTEST(setup(t_add, 0), \"add works\");\n+\tTEST(setup(t_add, 1), \"add (case insensitive) works\");\n+\tTEST(setup(t_remove, 0), \"remove works\");\n+\tTEST(setup(t_remove, 1), \"remove (case insensitive) works\");\n+\tTEST(setup(t_iterate, 0), \"iterate works\");\n+\tTEST(setup(t_iterate, 1), \"iterate (case insensitive) works\");\n+\tTEST(setup(t_alloc, 0), \"grow / shrink works\");\n+\tTEST(setup(t_intern, 0), \"string interning works\");\n+\treturn test_done();\n+}\n\nRange-diff against v4:\n1:  04022c4cb9 ! 1:  49cb2db9c8 t: port helper/test-hashmap.c to unit-tests/t-hashmap.c\n    @@ t/unit-tests/t-hashmap.c (new)\n     +\tconst char *key_val[][2] = { { \"key1\", \"value1\" },\n     +\t\t\t\t     { \"key2\", \"value2\" },\n     +\t\t\t\t     { \"fooBarFrotz\", \"value3\" },\n    -+\t\t\t\t     { ignore_case ? \"TeNor\" : \"tenor\",\n    -+\t\t\t\t       ignore_case ? \"value4\" : \"value5\" } };\n    ++\t\t\t\t     { ignore_case ? \"key4\" : \"foobarfrotz\",\n    ++\t\t\t\t       \"value4\" } };\n     +\tconst char *query[][2] = {\n     +\t\t{ ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n     +\t\t{ ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n     +\t\t{ ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n    -+\t\t{ ignore_case ? \"TENOR\" : \"tenor\",\n    -+\t\t  ignore_case ? \"value4\" : \"value5\" }\n    ++\t\t{ ignore_case ? \"FOObarFrotz\" : \"foobarfrotz\",\n    ++\t\t  ignore_case ? \"value3\" : \"value4\" }\n     +\t};\n     +\n     +\tfor (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {\n-- \n2.46.0\n\n"},{"id":"500329","messageId":"CAP8UFD3VwiAbGvzgi14EcEQLX6Gs7pNy+dvZcXkPOueKpJS7HA@mail.gmail.com","threadId":"61692","inReplyTo":"20240803133517.73308-2-shyamthakkar001@gmail.com","subject":"Re: [GSoC][PATCH v5] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2024-08-07T14:33:43Z","receivedAt":"2024-08-07T14:33:57Z","isPatch":true,"sender":{"key":"christian.couder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/208954?v=4"},"body":"On Sat, Aug 3, 2024 at 3:35 PM Ghanshyam Thakkar\n<shyamthakkar001@gmail.com> wrote:\n>\n> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h\n> library. Migrate them to the unit testing framework for better\n> debugging, runtime performance and concise code.\n>\n> Along with the migration, make 'add' tests from the shell script order\n> agnostic in unit tests, since they iterate over entries with the same\n> keys and we do not guarantee the order. This was already done for the\n> 'iterate' tests[1].\n>\n> The helper/test-hashmap.c is still not removed because it contains a\n> performance test meant to be run by the user directly (not used in\n> t/perf). And it makes sense for such a utility to be a helper.\n>\n> [1]: e1e7a77141 (t: sort output of hashmap iteration, 2019-07-30)\n>\n> Mentored-by: Christian Couder <chriscool@tuxfamily.org>\n> Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>\n> Helped-by: Josh Steadmon <steadmon@google.com>\n> Helped-by: Junio C Hamano <gitster@pobox.com>\n> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n> Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>\n> ---\n> Changes in v5:\n> - modify one of the inputs and queries inside t_get() to better check\n>   ignore-case scenario. This should address the comments from\n>   Christian in v4.\n\nYes, the only comment I had about the patch itself is addressed. There\nwere also comments about the email author, but that is fixed too.\n\n> Range-diff against v4:\n\nNit (not worth a reroll): the range-diff usually goes before the\nactual patch in the section starting with a line containing only 3\ndashes\n\n> 1:  04022c4cb9 ! 1:  49cb2db9c8 t: port helper/test-hashmap.c to unit-tests/t-hashmap.c\n>     @@ t/unit-tests/t-hashmap.c (new)\n>      +  const char *key_val[][2] = { { \"key1\", \"value1\" },\n>      +                               { \"key2\", \"value2\" },\n>      +                               { \"fooBarFrotz\", \"value3\" },\n>     -+                               { ignore_case ? \"TeNor\" : \"tenor\",\n>     -+                                 ignore_case ? \"value4\" : \"value5\" } };\n>     ++                               { ignore_case ? \"key4\" : \"foobarfrotz\",\n>     ++                                 \"value4\" } };\n>      +  const char *query[][2] = {\n>      +          { ignore_case ? \"Key1\" : \"key1\", \"value1\" },\n>      +          { ignore_case ? \"keY2\" : \"key2\", \"value2\" },\n>      +          { ignore_case ? \"FOObarFrotz\" : \"fooBarFrotz\", \"value3\" },\n>     -+          { ignore_case ? \"TENOR\" : \"tenor\",\n>     -+            ignore_case ? \"value4\" : \"value5\" }\n>     ++          { ignore_case ? \"FOObarFrotz\" : \"foobarfrotz\",\n>     ++            ignore_case ? \"value3\" : \"value4\" }\n>      +  };\n\nYeah, the above are the changes I suggested.\n\nWith this all the suggestions made by others and me have been taken\ninto account and this patch should be good to go.\n\nThanks.\n"},{"id":"500331","messageId":"D39RDAEXKU68.10JTFKUKJ6YHH@gmail.com","threadId":"61692","inReplyTo":"CAP8UFD3VwiAbGvzgi14EcEQLX6Gs7pNy+dvZcXkPOueKpJS7HA@mail.gmail.com","subject":"Re: [GSoC][PATCH v5] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Ghanshyam Thakkar","fromEmail":"shyamthakkar001@gmail.com","sentAt":"2024-08-07T14:44:36Z","receivedAt":"2024-08-07T14:44:42Z","isPatch":true,"sender":{"key":"shyamthakkar001@gmail.com","avatar":"https://avatars.githubusercontent.com/u/72698233?v=4"},"body":"Christian Couder <christian.couder@gmail.com> wrote:\n> On Sat, Aug 3, 2024 at 3:35 PM Ghanshyam Thakkar\n> <shyamthakkar001@gmail.com> wrote:\n>\n> > Range-diff against v4:\n>\n> Nit (not worth a reroll): the range-diff usually goes before the\n> actual patch in the section starting with a line containing only 3\n> dashes\n\n'format-patch' placed it here by itself. Looking at the release notes for\n2.46 I found this:\n\n * The inter/range-diff output has been moved to the end of the patch\n   when format-patch adds it to a single patch, instead of writing it\n   before the patch text, to be consistent with what is done for a\n   cover letter for a multi-patch series.\n\nThanks.\n"},{"id":"500335","messageId":"xmqq34ngpann.fsf@gitster.g","threadId":"61692","inReplyTo":"CAP8UFD3VwiAbGvzgi14EcEQLX6Gs7pNy+dvZcXkPOueKpJS7HA@mail.gmail.com","subject":"Re: [GSoC][PATCH v5] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-08-07T16:20:28Z","receivedAt":"2024-08-07T16:20:31Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n>> Range-diff against v4:\n>\n> Nit (not worth a reroll): the range-diff usually goes before the\n> actual patch in the section starting with a line containing only 3\n> dashes\n\nThis comes from https://lore.kernel.org/git/Zk7UsJjhY_FV2z8C@tanuki/\nthat eventually became 2fa04ceb (format-patch: move range/inter diff\nat the end of a single patch output, 2024-05-24).\n"}]}