diff mbox series

[GSoC,v3] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c

Message ID 20240711235159.5320-1-shyamthakkar001@gmail.com (mailing list archive)
State Superseded
Headers show
Series [GSoC,v3] t: port helper/test-hashmap.c to unit-tests/t-hashmap.c | expand

Commit Message

Ghanshyam Thakkar July 11, 2024, 11:51 p.m. UTC
helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h
library. Migrate them to the unit testing framework for better
debugging, runtime performance and concise code.

Along with the migration, make 'add' tests from the shellscript order
agnostic in unit tests, since they iterate over entries with the same
keys and we do not guarantee the order.

The helper/test-hashmap.c is still not removed because it contains a
performance test meant to be run by the user directly (not used in
t/perf). And it makes sense for such a utility to be a helper.

Mentored-by: Christian Couder <chriscool@tuxfamily.org>
Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
Helped-by: Josh Steadmon <steadmon@google.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>
---
Range-diff against v2:
1:  bbb4f2f23e ! 1:  03ba77665e t: port helper/test-hashmap.c to unit-tests/t-hashmap.c
    @@ Commit message
     
         helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h
         library. Migrate them to the unit testing framework for better
    -    debugging, runtime performance and consice code.
    +    debugging, runtime performance and concise code.
     
         Along with the migration, make 'add' tests from the shellscript order
         agnostic in unit tests, since they iterate over entries with the same
    @@ Commit message
     
         Mentored-by: Christian Couder <chriscool@tuxfamily.org>
         Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
    +    Helped-by: Josh Steadmon <steadmon@google.com>
    +    Helped-by: Phillip Wood <phillip.wood123@gmail.com>
         Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>
     
      ## Makefile ##
    @@ t/unit-tests/t-hashmap.c (new)
     +		struct test_entry, ent);
     +}
     +
    -+static int key_val_contains(const char *key_val[][3], size_t n,
    ++static int key_val_contains(const char *key_val[][2], char seen[], size_t n,
     +			    struct test_entry *entry)
     +{
     +	for (size_t i = 0; i < n; i++) {
     +		if (!strcmp(entry->key, key_val[i][0]) &&
     +		    !strcmp(get_value(entry), key_val[i][1])) {
    -+			if (!strcmp(key_val[i][2], "USED"))
    ++			if (seen[i])
     +				return 2;
    -+			key_val[i][2] = "USED";
    ++			seen[i] = 1;
     +			return 0;
     +		}
     +	}
    @@ t/unit-tests/t-hashmap.c (new)
     +	hashmap_clear_and_free(&map, struct test_entry, ent);
     +}
     +
    -+static void t_put(struct hashmap *map, int ignore_case)
    -+{
    -+	struct test_entry *entry;
    -+	const char *key_val[][2] = { { "key1", "value1" },
    -+				     { "key2", "value2" },
    -+				     { "fooBarFrotz", "value3" } };
    -+
    -+	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
    -+		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
    -+		check(hashmap_put_entry(map, entry, ent) == NULL);
    -+	}
    -+
    -+	entry = alloc_test_entry(ignore_case, "foobarfrotz", "value4");
    -+	entry = hashmap_put_entry(map, entry, ent);
    -+	check(ignore_case ? entry != NULL : entry == NULL);
    -+	free(entry);
    -+
    -+	check_int(map->tablesize, ==, 64);
    -+	check_int(hashmap_get_size(map), ==,
    -+		  ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);
    -+}
    -+
     +static void t_replace(struct hashmap *map, int ignore_case)
     +{
     +	struct test_entry *entry;
     +
     +	entry = alloc_test_entry(ignore_case, "key1", "value1");
    -+	check(hashmap_put_entry(map, entry, ent) == NULL);
    ++	check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
     +
     +	entry = alloc_test_entry(ignore_case, ignore_case ? "Key1" : "key1",
     +				 "value2");
    @@ t/unit-tests/t-hashmap.c (new)
     +	free(entry);
     +
     +	entry = alloc_test_entry(ignore_case, "fooBarFrotz", "value3");
    -+	check(hashmap_put_entry(map, entry, ent) == NULL);
    ++	check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
     +
     +	entry = alloc_test_entry(ignore_case,
     +				 ignore_case ? "foobarfrotz" : "fooBarFrotz",
    @@ t/unit-tests/t-hashmap.c (new)
     +
     +	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
     +		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
    -+		check(hashmap_put_entry(map, entry, ent) == NULL);
    ++		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
     +	}
     +
     +	for (size_t i = 0; i < ARRAY_SIZE(query); i++) {
     +		entry = get_test_entry(map, ignore_case, query[i][0]);
     +		if (check(entry != NULL))
     +			check_str(get_value(entry), query[i][1]);
    ++		else
    ++			test_msg("query key: %s", query[i][0]);
     +	}
     +
    -+	check(get_test_entry(map, ignore_case, "notInMap") == NULL);
    ++	check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
    ++	check_int(map->tablesize, ==, 64);
    ++	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
     +}
     +
     +static void t_add(struct hashmap *map, int ignore_case)
     +{
     +	struct test_entry *entry;
    -+	const char *key_val[][3] = {
    -+		{ "key1", "value1", "UNUSED" },
    -+		{ ignore_case ? "Key1" : "key1", "value2", "UNUSED" },
    -+		{ "fooBarFrotz", "value3", "UNUSED" },
    -+		{ ignore_case ? "Foobarfrotz" : "fooBarFrotz", "value4", "UNUSED" }
    ++	const char *key_val[][2] = {
    ++		{ "key1", "value1" },
    ++		{ ignore_case ? "Key1" : "key1", "value2" },
    ++		{ "fooBarFrotz", "value3" },
    ++		{ ignore_case ? "Foobarfrotz" : "fooBarFrotz", "value4" }
     +	};
     +	const char *queries[] = { "key1",
     +				  ignore_case ? "Foobarfrotz" : "fooBarFrotz" };
    ++	char seen[ARRAY_SIZE(key_val)] = { 0 };
     +
     +	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
     +		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
    @@ t/unit-tests/t-hashmap.c (new)
     +		{
     +			int ret;
     +			if (!check_int((ret = key_val_contains(
    -+						key_val, ARRAY_SIZE(key_val),
    -+						entry)), ==, 0)) {
    ++						key_val, seen,
    ++						ARRAY_SIZE(key_val), entry)),
    ++				       ==, 0)) {
     +				switch (ret) {
     +				case 1:
     +					test_msg("found entry was not given in the input\n"
    @@ t/unit-tests/t-hashmap.c (new)
     +		}
     +		check_int(count, ==, 2);
     +	}
    ++
    ++	for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
    ++		if (!check_int(seen[i], ==, 1))
    ++			test_msg("following key-val pair was not iterated over:\n"
    ++				 "    key: %s\n  value: %s",
    ++				 key_val[i][0], key_val[i][1]);
    ++	}
    ++
     +	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
    -+	check(get_test_entry(map, ignore_case, "notInMap") == NULL);
    ++	check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
     +}
     +
     +static void t_remove(struct hashmap *map, int ignore_case)
    @@ t/unit-tests/t-hashmap.c (new)
     +
     +	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
     +		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
    -+		check(hashmap_put_entry(map, entry, ent) == NULL);
    ++		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
     +	}
     +
     +	for (size_t i = 0; i < ARRAY_SIZE(remove); i++) {
    @@ t/unit-tests/t-hashmap.c (new)
     +	}
     +
     +	entry = alloc_test_entry(ignore_case, "notInMap", "");
    -+	check(hashmap_remove_entry(map, entry, ent, "notInMap") == NULL);
    ++	check_pointer_eq(hashmap_remove_entry(map, entry, ent, "notInMap"), NULL);
     +	free(entry);
    ++
    ++	check_int(map->tablesize, ==, 64);
    ++	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));
     +}
     +
     +static void t_iterate(struct hashmap *map, int ignore_case)
     +{
     +	struct test_entry *entry;
     +	struct hashmap_iter iter;
    -+	const char *key_val[][3] = { { "key1", "value1", "UNUSED" },
    -+				     { "key2", "value2", "UNUSED" },
    -+				     { "fooBarFrotz", "value3", "UNUSED" } };
    -+	int count = 0;
    ++	const char *key_val[][2] = { { "key1", "value1" },
    ++				     { "key2", "value2" },
    ++				     { "fooBarFrotz", "value3" } };
    ++	char seen[ARRAY_SIZE(key_val)] = { 0 };
     +
     +	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
     +		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
    -+		check(hashmap_put_entry(map, entry, ent) == NULL);
    ++		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
     +	}
     +
     +	hashmap_for_each_entry(map, &iter, entry, ent /* member name */)
     +	{
     +		int ret;
    -+		if (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),
    ++		if (!check_int((ret = key_val_contains(key_val, seen,
    ++						       ARRAY_SIZE(key_val),
     +						       entry)), ==, 0)) {
     +			switch (ret) {
     +			case 1:
    @@ t/unit-tests/t-hashmap.c (new)
     +					 entry->key, get_value(entry));
     +				break;
     +			}
    -+		} else {
    -+			count++;
     +		}
     +	}
    -+	check_int(count, ==, ARRAY_SIZE(key_val));
    ++
    ++	for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
    ++		if (!check_int(seen[i], ==, 1))
    ++			test_msg("following key-val pair was not iterated over:\n"
    ++				 "    key: %s\n  value: %s",
    ++				 key_val[i][0], key_val[i][1]);
    ++	}
    ++
     +	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
     +}
     +
    @@ t/unit-tests/t-hashmap.c (new)
     +		char *key = xstrfmt("key%d", i);
     +		char *value = xstrfmt("value%d", i);
     +		entry = alloc_test_entry(ignore_case, key, value);
    -+		check(hashmap_put_entry(map, entry, ent) == NULL);
    ++		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
     +		free(key);
     +		free(value);
     +	}
    @@ t/unit-tests/t-hashmap.c (new)
     +	check_int(hashmap_get_size(map), ==, 51);
     +
     +	entry = alloc_test_entry(ignore_case, "key52", "value52");
    -+	check(hashmap_put_entry(map, entry, ent) == NULL);
    ++	check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
     +	check_int(map->tablesize, ==, 256);
     +	check_int(hashmap_get_size(map), ==, 52);
     +
    @@ t/unit-tests/t-hashmap.c (new)
     +		else if (!check(i1 != values[i]))
     +			test_msg("strintern(%s) returns input pointer\n",
     +				 values[i]);
    -+		else if (!check(i1 == i2))
    ++		else if (!check_pointer_eq(i1, i2))
     +			test_msg("address('%s') != address('%s'), so strintern('%s') != strintern('%s')",
     +				 i1, i2, values[i], values[i]);
     +		else
    @@ t/unit-tests/t-hashmap.c (new)
     +
     +int cmd_main(int argc UNUSED, const char **argv UNUSED)
     +{
    -+	TEST(setup(t_put, 0), "put works");
    -+	TEST(setup(t_put, 1), "put (case insensitive) works");
     +	TEST(setup(t_replace, 0), "replace works");
     +	TEST(setup(t_replace, 1), "replace (case insensitive) works");
     +	TEST(setup(t_get, 0), "get works");

 Makefile                 |   1 +
 t/helper/test-hashmap.c  | 100 +----------
 t/t0011-hashmap.sh       | 260 ----------------------------
 t/unit-tests/t-hashmap.c | 358 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 361 insertions(+), 358 deletions(-)
 delete mode 100755 t/t0011-hashmap.sh
 create mode 100644 t/unit-tests/t-hashmap.c

Comments

Ghanshyam Thakkar July 12, 2024, midnight UTC | #1
Ghanshyam Thakkar <shyamthakkar001@gmail.com> wrote:
> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h
> library. Migrate them to the unit testing framework for better
> debugging, runtime performance and concise code.
>
> Along with the migration, make 'add' tests from the shellscript order
> agnostic in unit tests, since they iterate over entries with the same
> keys and we do not guarantee the order.
>
> The helper/test-hashmap.c is still not removed because it contains a
> performance test meant to be run by the user directly (not used in
> t/perf). And it makes sense for such a utility to be a helper.
>
> Mentored-by: Christian Couder <chriscool@tuxfamily.org>
> Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
> Helped-by: Josh Steadmon <steadmon@google.com>
> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>
> ---
Changes in v3:

- I have removed the t_put() tests as they were identical to
  t_get() minus the size checks at the end. So, I've added those size
  checks to t_get().

- Besides that I've replaced check() with check_pointer_eq() where ever
  applicable.
  
- And replaced the third elements of key_val[] which recorded
  the presence of certain key-val pair with 'char seen[]'.

- Added some test_msg() statements for better debugging.

- Fixed typo in commit message.

Thanks.

> Range-diff against v2:
> 1: bbb4f2f23e ! 1: 03ba77665e t: port helper/test-hashmap.c to
> unit-tests/t-hashmap.c
> @@ Commit message
>      
> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h
> library. Migrate them to the unit testing framework for better
> - debugging, runtime performance and consice code.
> + debugging, runtime performance and concise code.
>      
> Along with the migration, make 'add' tests from the shellscript order
> agnostic in unit tests, since they iterate over entries with the same
> @@ Commit message
>      
> Mentored-by: Christian Couder <chriscool@tuxfamily.org>
> Mentored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
> + Helped-by: Josh Steadmon <steadmon@google.com>
> + Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> Signed-off-by: Ghanshyam Thakkar <shyamthakkar001@gmail.com>
>      
> ## Makefile ##
> @@ t/unit-tests/t-hashmap.c (new)
> + struct test_entry, ent);
> +}
> +
> -+static int key_val_contains(const char *key_val[][3], size_t n,
> ++static int key_val_contains(const char *key_val[][2], char seen[],
> size_t n,
> + struct test_entry *entry)
> +{
> + for (size_t i = 0; i < n; i++) {
> + if (!strcmp(entry->key, key_val[i][0]) &&
> + !strcmp(get_value(entry), key_val[i][1])) {
> -+ if (!strcmp(key_val[i][2], "USED"))
> ++ if (seen[i])
> + return 2;
> -+ key_val[i][2] = "USED";
> ++ seen[i] = 1;
> + return 0;
> + }
> + }
> @@ t/unit-tests/t-hashmap.c (new)
> + hashmap_clear_and_free(&map, struct test_entry, ent);
> +}
> +
> -+static void t_put(struct hashmap *map, int ignore_case)
> -+{
> -+ struct test_entry *entry;
> -+ const char *key_val[][2] = { { "key1", "value1" },
> -+ { "key2", "value2" },
> -+ { "fooBarFrotz", "value3" } };
> -+
> -+ for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> -+ entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> -+ }
> -+
> -+ entry = alloc_test_entry(ignore_case, "foobarfrotz", "value4");
> -+ entry = hashmap_put_entry(map, entry, ent);
> -+ check(ignore_case ? entry != NULL : entry == NULL);
> -+ free(entry);
> -+
> -+ check_int(map->tablesize, ==, 64);
> -+ check_int(hashmap_get_size(map), ==,
> -+ ignore_case ? ARRAY_SIZE(key_val) : ARRAY_SIZE(key_val) + 1);
> -+}
> -+
> +static void t_replace(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry;
> +
> + entry = alloc_test_entry(ignore_case, "key1", "value1");
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> +
> + entry = alloc_test_entry(ignore_case, ignore_case ? "Key1" : "key1",
> + "value2");
> @@ t/unit-tests/t-hashmap.c (new)
> + free(entry);
> +
> + entry = alloc_test_entry(ignore_case, "fooBarFrotz", "value3");
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> +
> + entry = alloc_test_entry(ignore_case,
> + ignore_case ? "foobarfrotz" : "fooBarFrotz",
> @@ t/unit-tests/t-hashmap.c (new)
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + }
> +
> + for (size_t i = 0; i < ARRAY_SIZE(query); i++) {
> + entry = get_test_entry(map, ignore_case, query[i][0]);
> + if (check(entry != NULL))
> + check_str(get_value(entry), query[i][1]);
> ++ else
> ++ test_msg("query key: %s", query[i][0]);
> + }
> +
> -+ check(get_test_entry(map, ignore_case, "notInMap") == NULL);
> ++ check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
> ++ check_int(map->tablesize, ==, 64);
> ++ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> +}
> +
> +static void t_add(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry;
> -+ const char *key_val[][3] = {
> -+ { "key1", "value1", "UNUSED" },
> -+ { ignore_case ? "Key1" : "key1", "value2", "UNUSED" },
> -+ { "fooBarFrotz", "value3", "UNUSED" },
> -+ { ignore_case ? "Foobarfrotz" : "fooBarFrotz", "value4", "UNUSED" }
> ++ const char *key_val[][2] = {
> ++ { "key1", "value1" },
> ++ { ignore_case ? "Key1" : "key1", "value2" },
> ++ { "fooBarFrotz", "value3" },
> ++ { ignore_case ? "Foobarfrotz" : "fooBarFrotz", "value4" }
> + };
> + const char *queries[] = { "key1",
> + ignore_case ? "Foobarfrotz" : "fooBarFrotz" };
> ++ char seen[ARRAY_SIZE(key_val)] = { 0 };
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> @@ t/unit-tests/t-hashmap.c (new)
> + {
> + int ret;
> + if (!check_int((ret = key_val_contains(
> -+ key_val, ARRAY_SIZE(key_val),
> -+ entry)), ==, 0)) {
> ++ key_val, seen,
> ++ ARRAY_SIZE(key_val), entry)),
> ++ ==, 0)) {
> + switch (ret) {
> + case 1:
> + test_msg("found entry was not given in the input\n"
> @@ t/unit-tests/t-hashmap.c (new)
> + }
> + check_int(count, ==, 2);
> + }
> ++
> ++ for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
> ++ if (!check_int(seen[i], ==, 1))
> ++ test_msg("following key-val pair was not iterated over:\n"
> ++ " key: %s\n value: %s",
> ++ key_val[i][0], key_val[i][1]);
> ++ }
> ++
> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> -+ check(get_test_entry(map, ignore_case, "notInMap") == NULL);
> ++ check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
> +}
> +
> +static void t_remove(struct hashmap *map, int ignore_case)
> @@ t/unit-tests/t-hashmap.c (new)
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + }
> +
> + for (size_t i = 0; i < ARRAY_SIZE(remove); i++) {
> @@ t/unit-tests/t-hashmap.c (new)
> + }
> +
> + entry = alloc_test_entry(ignore_case, "notInMap", "");
> -+ check(hashmap_remove_entry(map, entry, ent, "notInMap") == NULL);
> ++ check_pointer_eq(hashmap_remove_entry(map, entry, ent, "notInMap"),
> NULL);
> + free(entry);
> ++
> ++ check_int(map->tablesize, ==, 64);
> ++ check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) -
> ARRAY_SIZE(remove));
> +}
> +
> +static void t_iterate(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry;
> + struct hashmap_iter iter;
> -+ const char *key_val[][3] = { { "key1", "value1", "UNUSED" },
> -+ { "key2", "value2", "UNUSED" },
> -+ { "fooBarFrotz", "value3", "UNUSED" } };
> -+ int count = 0;
> ++ const char *key_val[][2] = { { "key1", "value1" },
> ++ { "key2", "value2" },
> ++ { "fooBarFrotz", "value3" } };
> ++ char seen[ARRAY_SIZE(key_val)] = { 0 };
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + }
> +
> + hashmap_for_each_entry(map, &iter, entry, ent /* member name */)
> + {
> + int ret;
> -+ if (!check_int((ret = key_val_contains(key_val, ARRAY_SIZE(key_val),
> ++ if (!check_int((ret = key_val_contains(key_val, seen,
> ++ ARRAY_SIZE(key_val),
> + entry)), ==, 0)) {
> + switch (ret) {
> + case 1:
> @@ t/unit-tests/t-hashmap.c (new)
> + entry->key, get_value(entry));
> + break;
> + }
> -+ } else {
> -+ count++;
> + }
> + }
> -+ check_int(count, ==, ARRAY_SIZE(key_val));
> ++
> ++ for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
> ++ if (!check_int(seen[i], ==, 1))
> ++ test_msg("following key-val pair was not iterated over:\n"
> ++ " key: %s\n value: %s",
> ++ key_val[i][0], key_val[i][1]);
> ++ }
> ++
> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> +}
> +
> @@ t/unit-tests/t-hashmap.c (new)
> + char *key = xstrfmt("key%d", i);
> + char *value = xstrfmt("value%d", i);
> + entry = alloc_test_entry(ignore_case, key, value);
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + free(key);
> + free(value);
> + }
> @@ t/unit-tests/t-hashmap.c (new)
> + check_int(hashmap_get_size(map), ==, 51);
> +
> + entry = alloc_test_entry(ignore_case, "key52", "value52");
> -+ check(hashmap_put_entry(map, entry, ent) == NULL);
> ++ check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + check_int(map->tablesize, ==, 256);
> + check_int(hashmap_get_size(map), ==, 52);
> +
> @@ t/unit-tests/t-hashmap.c (new)
> + else if (!check(i1 != values[i]))
> + test_msg("strintern(%s) returns input pointer\n",
> + values[i]);
> -+ else if (!check(i1 == i2))
> ++ else if (!check_pointer_eq(i1, i2))
> + test_msg("address('%s') != address('%s'), so strintern('%s') !=
> strintern('%s')",
> + i1, i2, values[i], values[i]);
> + else
> @@ t/unit-tests/t-hashmap.c (new)
> +
> +int cmd_main(int argc UNUSED, const char **argv UNUSED)
> +{
> -+ TEST(setup(t_put, 0), "put works");
> -+ TEST(setup(t_put, 1), "put (case insensitive) works");
> + TEST(setup(t_replace, 0), "replace works");
> + TEST(setup(t_replace, 1), "replace (case insensitive) works");
> + TEST(setup(t_get, 0), "get works");
>
> Makefile | 1 +
> t/helper/test-hashmap.c | 100 +----------
> t/t0011-hashmap.sh | 260 ----------------------------
> t/unit-tests/t-hashmap.c | 358 +++++++++++++++++++++++++++++++++++++++
> 4 files changed, 361 insertions(+), 358 deletions(-)
> delete mode 100755 t/t0011-hashmap.sh
> create mode 100644 t/unit-tests/t-hashmap.c
>
> diff --git a/Makefile b/Makefile
> index 3eab701b10..74bb026610 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1336,6 +1336,7 @@ THIRD_PARTY_SOURCES += sha1dc/%
> UNIT_TEST_PROGRAMS += t-ctype
> UNIT_TEST_PROGRAMS += t-example-decorate
> UNIT_TEST_PROGRAMS += t-hash
> +UNIT_TEST_PROGRAMS += t-hashmap
> UNIT_TEST_PROGRAMS += t-mem-pool
> UNIT_TEST_PROGRAMS += t-oidtree
> UNIT_TEST_PROGRAMS += t-prio-queue
> diff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c
> index 2912899558..7b854a7030 100644
> --- a/t/helper/test-hashmap.c
> +++ b/t/helper/test-hashmap.c
> @@ -12,11 +12,6 @@ struct test_entry
> char key[FLEX_ARRAY];
> };
>  
> -static const char *get_value(const struct test_entry *e)
> -{
> - return e->key + strlen(e->key) + 1;
> -}
> -
> static int test_entry_cmp(const void *cmp_data,
> const struct hashmap_entry *eptr,
> const struct hashmap_entry *entry_or_key,
> @@ -141,30 +136,16 @@ static void perf_hashmap(unsigned int method,
> unsigned int rounds)
> /*
> * Read stdin line by line and print result of commands to stdout:
> *
> - * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)
> - * put key value -> NULL / old value
> - * get key -> NULL / value
> - * remove key -> NULL / old value
> - * iterate -> key1 value1\nkey2 value2\n...
> - * size -> tablesize numentries
> - *
> * perfhashmap method rounds -> test hashmap.[ch] performance
> */
> int cmd__hashmap(int argc, const char **argv)
> {
> struct string_list parts = STRING_LIST_INIT_NODUP;
> struct strbuf line = STRBUF_INIT;
> - int icase;
> - struct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);
> -
> - /* init hash map */
> - icase = argc > 1 && !strcmp("ignorecase", argv[1]);
>  
> /* process commands from stdin */
> while (strbuf_getline(&line, stdin) != EOF) {
> char *cmd, *p1, *p2;
> - unsigned int hash = 0;
> - struct test_entry *entry;
>  
> /* break line into command and up to two parameters */
> string_list_setlen(&parts, 0);
> @@ -180,84 +161,8 @@ int cmd__hashmap(int argc, const char **argv)
> cmd = parts.items[0].string;
> p1 = parts.nr >= 1 ? parts.items[1].string : NULL;
> p2 = parts.nr >= 2 ? parts.items[2].string : NULL;
> - if (p1)
> - hash = icase ? strihash(p1) : strhash(p1);
> -
> - if (!strcmp("add", cmd) && p1 && p2) {
> -
> - /* create entry with key = p1, value = p2 */
> - entry = alloc_test_entry(hash, p1, p2);
> -
> - /* add to hashmap */
> - hashmap_add(&map, &entry->ent);
> -
> - } else if (!strcmp("put", cmd) && p1 && p2) {
> -
> - /* create entry with key = p1, value = p2 */
> - entry = alloc_test_entry(hash, p1, p2);
> -
> - /* add / replace entry */
> - entry = hashmap_put_entry(&map, entry, ent);
> -
> - /* print and free replaced entry, if any */
> - puts(entry ? get_value(entry) : "NULL");
> - free(entry);
> -
> - } else if (!strcmp("get", cmd) && p1) {
> - /* lookup entry in hashmap */
> - entry = hashmap_get_entry_from_hash(&map, hash, p1,
> - struct test_entry, ent);
> -
> - /* print result */
> - if (!entry)
> - puts("NULL");
> - hashmap_for_each_entry_from(&map, entry, ent)
> - puts(get_value(entry));
> -
> - } else if (!strcmp("remove", cmd) && p1) {
> -
> - /* setup static key */
> - struct hashmap_entry key;
> - struct hashmap_entry *rm;
> - hashmap_entry_init(&key, hash);
> -
> - /* remove entry from hashmap */
> - rm = hashmap_remove(&map, &key, p1);
> - entry = rm ? container_of(rm, struct test_entry, ent)
> - : NULL;
> -
> - /* print result and free entry*/
> - puts(entry ? get_value(entry) : "NULL");
> - free(entry);
> -
> - } else if (!strcmp("iterate", cmd)) {
> - struct hashmap_iter iter;
> -
> - hashmap_for_each_entry(&map, &iter, entry,
> - ent /* member name */)
> - printf("%s %s\n", entry->key, get_value(entry));
> -
> - } else if (!strcmp("size", cmd)) {
> -
> - /* print table sizes */
> - printf("%u %u\n", map.tablesize,
> - hashmap_get_size(&map));
> -
> - } else if (!strcmp("intern", cmd) && p1) {
> -
> - /* test that strintern works */
> - const char *i1 = strintern(p1);
> - const char *i2 = strintern(p1);
> - if (strcmp(i1, p1))
> - printf("strintern(%s) returns %s\n", p1, i1);
> - else if (i1 == p1)
> - printf("strintern(%s) returns input pointer\n", p1);
> - else if (i1 != i2)
> - printf("strintern(%s) != strintern(%s)", i1, i2);
> - else
> - printf("%s\n", i1);
> -
> - } else if (!strcmp("perfhashmap", cmd) && p1 && p2) {
> +
> + if (!strcmp("perfhashmap", cmd) && p1 && p2) {
>  
> perf_hashmap(atoi(p1), atoi(p2));
>  
> @@ -270,6 +175,5 @@ int cmd__hashmap(int argc, const char **argv)
>  
> string_list_clear(&parts, 0);
> strbuf_release(&line);
> - hashmap_clear_and_free(&map, struct test_entry, ent);
> return 0;
> }
> diff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh
> deleted file mode 100755
> index 46e74ad107..0000000000
> --- a/t/t0011-hashmap.sh
> +++ /dev/null
> @@ -1,260 +0,0 @@
> -#!/bin/sh
> -
> -test_description='test hashmap and string hash functions'
> -
> -TEST_PASSES_SANITIZE_LEAK=true
> -. ./test-lib.sh
> -
> -test_hashmap() {
> - echo "$1" | test-tool hashmap $3 > actual &&
> - echo "$2" > expect &&
> - test_cmp expect actual
> -}
> -
> -test_expect_success 'put' '
> -
> -test_hashmap "put key1 value1
> -put key2 value2
> -put fooBarFrotz value3
> -put foobarfrotz value4
> -size" "NULL
> -NULL
> -NULL
> -NULL
> -64 4"
> -
> -'
> -
> -test_expect_success 'put (case insensitive)' '
> -
> -test_hashmap "put key1 value1
> -put key2 value2
> -put fooBarFrotz value3
> -size" "NULL
> -NULL
> -NULL
> -64 3" ignorecase
> -
> -'
> -
> -test_expect_success 'replace' '
> -
> -test_hashmap "put key1 value1
> -put key1 value2
> -put fooBarFrotz value3
> -put fooBarFrotz value4
> -size" "NULL
> -value1
> -NULL
> -value3
> -64 2"
> -
> -'
> -
> -test_expect_success 'replace (case insensitive)' '
> -
> -test_hashmap "put key1 value1
> -put Key1 value2
> -put fooBarFrotz value3
> -put foobarfrotz value4
> -size" "NULL
> -value1
> -NULL
> -value3
> -64 2" ignorecase
> -
> -'
> -
> -test_expect_success 'get' '
> -
> -test_hashmap "put key1 value1
> -put key2 value2
> -put fooBarFrotz value3
> -put foobarfrotz value4
> -get key1
> -get key2
> -get fooBarFrotz
> -get notInMap" "NULL
> -NULL
> -NULL
> -NULL
> -value1
> -value2
> -value3
> -NULL"
> -
> -'
> -
> -test_expect_success 'get (case insensitive)' '
> -
> -test_hashmap "put key1 value1
> -put key2 value2
> -put fooBarFrotz value3
> -get Key1
> -get keY2
> -get foobarfrotz
> -get notInMap" "NULL
> -NULL
> -NULL
> -value1
> -value2
> -value3
> -NULL" ignorecase
> -
> -'
> -
> -test_expect_success 'add' '
> -
> -test_hashmap "add key1 value1
> -add key1 value2
> -add fooBarFrotz value3
> -add fooBarFrotz value4
> -get key1
> -get fooBarFrotz
> -get notInMap" "value2
> -value1
> -value4
> -value3
> -NULL"
> -
> -'
> -
> -test_expect_success 'add (case insensitive)' '
> -
> -test_hashmap "add key1 value1
> -add Key1 value2
> -add fooBarFrotz value3
> -add foobarfrotz value4
> -get key1
> -get Foobarfrotz
> -get notInMap" "value2
> -value1
> -value4
> -value3
> -NULL" ignorecase
> -
> -'
> -
> -test_expect_success 'remove' '
> -
> -test_hashmap "put key1 value1
> -put key2 value2
> -put fooBarFrotz value3
> -remove key1
> -remove key2
> -remove notInMap
> -size" "NULL
> -NULL
> -NULL
> -value1
> -value2
> -NULL
> -64 1"
> -
> -'
> -
> -test_expect_success 'remove (case insensitive)' '
> -
> -test_hashmap "put key1 value1
> -put key2 value2
> -put fooBarFrotz value3
> -remove Key1
> -remove keY2
> -remove notInMap
> -size" "NULL
> -NULL
> -NULL
> -value1
> -value2
> -NULL
> -64 1" ignorecase
> -
> -'
> -
> -test_expect_success 'iterate' '
> - test-tool hashmap >actual.raw <<-\EOF &&
> - put key1 value1
> - put key2 value2
> - put fooBarFrotz value3
> - iterate
> - EOF
> -
> - cat >expect <<-\EOF &&
> - NULL
> - NULL
> - NULL
> - fooBarFrotz value3
> - key1 value1
> - key2 value2
> - EOF
> -
> - sort <actual.raw >actual &&
> - test_cmp expect actual
> -'
> -
> -test_expect_success 'iterate (case insensitive)' '
> - test-tool hashmap ignorecase >actual.raw <<-\EOF &&
> - put key1 value1
> - put key2 value2
> - put fooBarFrotz value3
> - iterate
> - EOF
> -
> - cat >expect <<-\EOF &&
> - NULL
> - NULL
> - NULL
> - fooBarFrotz value3
> - key1 value1
> - key2 value2
> - EOF
> -
> - sort <actual.raw >actual &&
> - test_cmp expect actual
> -'
> -
> -test_expect_success 'grow / shrink' '
> -
> - rm -f in &&
> - rm -f expect &&
> - for n in $(test_seq 51)
> - do
> - echo put key$n value$n >> in &&
> - echo NULL >> expect || return 1
> - done &&
> - echo size >> in &&
> - echo 64 51 >> expect &&
> - echo put key52 value52 >> in &&
> - echo NULL >> expect &&
> - echo size >> in &&
> - echo 256 52 >> expect &&
> - for n in $(test_seq 12)
> - do
> - echo remove key$n >> in &&
> - echo value$n >> expect || return 1
> - done &&
> - echo size >> in &&
> - echo 256 40 >> expect &&
> - echo remove key40 >> in &&
> - echo value40 >> expect &&
> - echo size >> in &&
> - echo 64 39 >> expect &&
> - test-tool hashmap <in >out &&
> - test_cmp expect out
> -
> -'
> -
> -test_expect_success 'string interning' '
> -
> -test_hashmap "intern value1
> -intern Value1
> -intern value2
> -intern value2
> -" "value1
> -Value1
> -value2
> -value2"
> -
> -'
> -
> -test_done
> diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c
> new file mode 100644
> index 0000000000..3112b10b33
> --- /dev/null
> +++ b/t/unit-tests/t-hashmap.c
> @@ -0,0 +1,358 @@
> +#include "test-lib.h"
> +#include "hashmap.h"
> +#include "strbuf.h"
> +
> +struct test_entry {
> + int padding; /* hashmap entry no longer needs to be the first member
> */
> + struct hashmap_entry ent;
> + /* key and value as two \0-terminated strings */
> + char key[FLEX_ARRAY];
> +};
> +
> +static int test_entry_cmp(const void *cmp_data,
> + const struct hashmap_entry *eptr,
> + const struct hashmap_entry *entry_or_key,
> + const void *keydata)
> +{
> + const int ignore_case = cmp_data ? *((int *)cmp_data) : 0;
> + const struct test_entry *e1, *e2;
> + const char *key = keydata;
> +
> + e1 = container_of(eptr, const struct test_entry, ent);
> + e2 = container_of(entry_or_key, const struct test_entry, ent);
> +
> + if (ignore_case)
> + return strcasecmp(e1->key, key ? key : e2->key);
> + else
> + return strcmp(e1->key, key ? key : e2->key);
> +}
> +
> +static const char *get_value(const struct test_entry *e)
> +{
> + return e->key + strlen(e->key) + 1;
> +}
> +
> +static struct test_entry *alloc_test_entry(unsigned int ignore_case,
> + const char *key, const char *value)
> +{
> + size_t klen = strlen(key);
> + size_t vlen = strlen(value);
> + unsigned int hash = ignore_case ? strihash(key) : strhash(key);
> + struct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen,
> 2));
> +
> + hashmap_entry_init(&entry->ent, hash);
> + memcpy(entry->key, key, klen + 1);
> + memcpy(entry->key + klen + 1, value, vlen + 1);
> + return entry;
> +}
> +
> +static struct test_entry *get_test_entry(struct hashmap *map,
> + unsigned int ignore_case, const char *key)
> +{
> + return hashmap_get_entry_from_hash(
> + map, ignore_case ? strihash(key) : strhash(key), key,
> + struct test_entry, ent);
> +}
> +
> +static int key_val_contains(const char *key_val[][2], char seen[],
> size_t n,
> + struct test_entry *entry)
> +{
> + for (size_t i = 0; i < n; i++) {
> + if (!strcmp(entry->key, key_val[i][0]) &&
> + !strcmp(get_value(entry), key_val[i][1])) {
> + if (seen[i])
> + return 2;
> + seen[i] = 1;
> + return 0;
> + }
> + }
> + return 1;
> +}
> +
> +static void setup(void (*f)(struct hashmap *map, int ignore_case),
> + int ignore_case)
> +{
> + struct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);
> +
> + f(&map, ignore_case);
> + hashmap_clear_and_free(&map, struct test_entry, ent);
> +}
> +
> +static void t_replace(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry;
> +
> + entry = alloc_test_entry(ignore_case, "key1", "value1");
> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> +
> + entry = alloc_test_entry(ignore_case, ignore_case ? "Key1" : "key1",
> + "value2");
> + entry = hashmap_put_entry(map, entry, ent);
> + if (check(entry != NULL))
> + check_str(get_value(entry), "value1");
> + free(entry);
> +
> + entry = alloc_test_entry(ignore_case, "fooBarFrotz", "value3");
> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> +
> + entry = alloc_test_entry(ignore_case,
> + ignore_case ? "foobarfrotz" : "fooBarFrotz",
> + "value4");
> + entry = hashmap_put_entry(map, entry, ent);
> + if (check(entry != NULL))
> + check_str(get_value(entry), "value3");
> + free(entry);
> +}
> +
> +static void t_get(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry;
> + const char *key_val[][2] = { { "key1", "value1" },
> + { "key2", "value2" },
> + { "fooBarFrotz", "value3" },
> + { ignore_case ? "key4" : "foobarfrotz", "value4" } };
> + const char *query[][2] = {
> + { ignore_case ? "Key1" : "key1", "value1" },
> + { ignore_case ? "keY2" : "key2", "value2" },
> + { ignore_case ? "foobarfrotz" : "fooBarFrotz", "value3" }
> + };
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + }
> +
> + for (size_t i = 0; i < ARRAY_SIZE(query); i++) {
> + entry = get_test_entry(map, ignore_case, query[i][0]);
> + if (check(entry != NULL))
> + check_str(get_value(entry), query[i][1]);
> + else
> + test_msg("query key: %s", query[i][0]);
> + }
> +
> + check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
> + check_int(map->tablesize, ==, 64);
> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> +}
> +
> +static void t_add(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry;
> + const char *key_val[][2] = {
> + { "key1", "value1" },
> + { ignore_case ? "Key1" : "key1", "value2" },
> + { "fooBarFrotz", "value3" },
> + { ignore_case ? "Foobarfrotz" : "fooBarFrotz", "value4" }
> + };
> + const char *queries[] = { "key1",
> + ignore_case ? "Foobarfrotz" : "fooBarFrotz" };
> + char seen[ARRAY_SIZE(key_val)] = { 0 };
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> + hashmap_add(map, &entry->ent);
> + }
> +
> + for (size_t i = 0; i < ARRAY_SIZE(queries); i++) {
> + int count = 0;
> + entry = hashmap_get_entry_from_hash(map,
> + ignore_case ? strihash(queries[i]) :
> + strhash(queries[i]),
> + queries[i], struct test_entry, ent);
> +
> + hashmap_for_each_entry_from(map, entry, ent)
> + {
> + int ret;
> + if (!check_int((ret = key_val_contains(
> + key_val, seen,
> + ARRAY_SIZE(key_val), entry)),
> + ==, 0)) {
> + switch (ret) {
> + case 1:
> + test_msg("found entry was not given in the input\n"
> + " key: %s\n value: %s",
> + entry->key, get_value(entry));
> + break;
> + case 2:
> + test_msg("duplicate entry detected\n"
> + " key: %s\n value: %s",
> + entry->key, get_value(entry));
> + break;
> + }
> + } else {
> + count++;
> + }
> + }
> + check_int(count, ==, 2);
> + }
> +
> + for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
> + if (!check_int(seen[i], ==, 1))
> + test_msg("following key-val pair was not iterated over:\n"
> + " key: %s\n value: %s",
> + key_val[i][0], key_val[i][1]);
> + }
> +
> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> + check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
> +}
> +
> +static void t_remove(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry, *removed;
> + const char *key_val[][2] = { { "key1", "value1" },
> + { "key2", "value2" },
> + { "fooBarFrotz", "value3" } };
> + const char *remove[][2] = { { ignore_case ? "Key1" : "key1", "value1"
> },
> + { ignore_case ? "keY2" : "key2", "value2" } };
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + }
> +
> + for (size_t i = 0; i < ARRAY_SIZE(remove); i++) {
> + entry = alloc_test_entry(ignore_case, remove[i][0], "");
> + removed = hashmap_remove_entry(map, entry, ent, remove[i][0]);
> + if (check(removed != NULL))
> + check_str(get_value(removed), remove[i][1]);
> + free(entry);
> + free(removed);
> + }
> +
> + entry = alloc_test_entry(ignore_case, "notInMap", "");
> + check_pointer_eq(hashmap_remove_entry(map, entry, ent, "notInMap"),
> NULL);
> + free(entry);
> +
> + check_int(map->tablesize, ==, 64);
> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) -
> ARRAY_SIZE(remove));
> +}
> +
> +static void t_iterate(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry;
> + struct hashmap_iter iter;
> + const char *key_val[][2] = { { "key1", "value1" },
> + { "key2", "value2" },
> + { "fooBarFrotz", "value3" } };
> + char seen[ARRAY_SIZE(key_val)] = { 0 };
> +
> + for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> + entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + }
> +
> + hashmap_for_each_entry(map, &iter, entry, ent /* member name */)
> + {
> + int ret;
> + if (!check_int((ret = key_val_contains(key_val, seen,
> + ARRAY_SIZE(key_val),
> + entry)), ==, 0)) {
> + switch (ret) {
> + case 1:
> + test_msg("found entry was not given in the input\n"
> + " key: %s\n value: %s",
> + entry->key, get_value(entry));
> + break;
> + case 2:
> + test_msg("duplicate entry detected\n"
> + " key: %s\n value: %s",
> + entry->key, get_value(entry));
> + break;
> + }
> + }
> + }
> +
> + for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
> + if (!check_int(seen[i], ==, 1))
> + test_msg("following key-val pair was not iterated over:\n"
> + " key: %s\n value: %s",
> + key_val[i][0], key_val[i][1]);
> + }
> +
> + check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> +}
> +
> +static void t_alloc(struct hashmap *map, int ignore_case)
> +{
> + struct test_entry *entry, *removed;
> +
> + for (int i = 1; i <= 51; i++) {
> + char *key = xstrfmt("key%d", i);
> + char *value = xstrfmt("value%d", i);
> + entry = alloc_test_entry(ignore_case, key, value);
> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + free(key);
> + free(value);
> + }
> + check_int(map->tablesize, ==, 64);
> + check_int(hashmap_get_size(map), ==, 51);
> +
> + entry = alloc_test_entry(ignore_case, "key52", "value52");
> + check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> + check_int(map->tablesize, ==, 256);
> + check_int(hashmap_get_size(map), ==, 52);
> +
> + for (int i = 1; i <= 12; i++) {
> + char *key = xstrfmt("key%d", i);
> + char *value = xstrfmt("value%d", i);
> +
> + entry = alloc_test_entry(ignore_case, key, "");
> + removed = hashmap_remove_entry(map, entry, ent, key);
> + if (check(removed != NULL))
> + check_str(value, get_value(removed));
> + free(key);
> + free(value);
> + free(entry);
> + free(removed);
> + }
> + check_int(map->tablesize, ==, 256);
> + check_int(hashmap_get_size(map), ==, 40);
> +
> + entry = alloc_test_entry(ignore_case, "key40", "");
> + removed = hashmap_remove_entry(map, entry, ent, "key40");
> + if (check(removed != NULL))
> + check_str("value40", get_value(removed));
> + check_int(map->tablesize, ==, 64);
> + check_int(hashmap_get_size(map), ==, 39);
> + free(entry);
> + free(removed);
> +}
> +
> +static void t_intern(struct hashmap *map, int ignore_case)
> +{
> + const char *values[] = { "value1", "Value1", "value2", "value2" };
> +
> + for (size_t i = 0; i < ARRAY_SIZE(values); i++) {
> + const char *i1 = strintern(values[i]);
> + const char *i2 = strintern(values[i]);
> +
> + if (!check(!strcmp(i1, values[i])))
> + test_msg("strintern(%s) returns %s\n", values[i], i1);
> + else if (!check(i1 != values[i]))
> + test_msg("strintern(%s) returns input pointer\n",
> + values[i]);
> + else if (!check_pointer_eq(i1, i2))
> + test_msg("address('%s') != address('%s'), so strintern('%s') !=
> strintern('%s')",
> + i1, i2, values[i], values[i]);
> + else
> + check_str(i1, values[i]);
> + }
> +}
> +
> +int cmd_main(int argc UNUSED, const char **argv UNUSED)
> +{
> + TEST(setup(t_replace, 0), "replace works");
> + TEST(setup(t_replace, 1), "replace (case insensitive) works");
> + TEST(setup(t_get, 0), "get works");
> + TEST(setup(t_get, 1), "get (case insensitive) works");
> + TEST(setup(t_add, 0), "add works");
> + TEST(setup(t_add, 1), "add (case insensitive) works");
> + TEST(setup(t_remove, 0), "remove works");
> + TEST(setup(t_remove, 1), "remove (case insensitive) works");
> + TEST(setup(t_iterate, 0), "iterate works");
> + TEST(setup(t_iterate, 1), "iterate (case insensitive) works");
> + TEST(setup(t_alloc, 0), "grow / shrink works");
> + TEST(setup(t_intern, 0), "string interning works");
> + return test_done();
> +}
> --
> 2.45.2
Junio C Hamano July 12, 2024, 5:14 p.m. UTC | #2
"Ghanshyam Thakkar" <shyamthakkar001@gmail.com> writes:

> - And replaced the third elements of key_val[] which recorded
>   the presence of certain key-val pair with 'char seen[]'.

It makes it in-line with the oidmap test, which is good.
Christian Couder July 25, 2024, 12:48 p.m. UTC | #3
On Fri, Jul 12, 2024 at 1:52 AM Ghanshyam Thakkar
<shyamthakkar001@gmail.com> wrote:
>
> helper/test-hashmap.c along with t0011-hashmap.sh test the hashmap.h
> library. Migrate them to the unit testing framework for better
> debugging, runtime performance and concise code.
>
> Along with the migration, make 'add' tests from the shellscript order

Nit: s/shellscript/shell script/

> agnostic in unit tests, since they iterate over entries with the same
> keys and we do not guarantee the order.

The above is not very clear. Maybe rephrasing a bit and adding an
example could help.

> The helper/test-hashmap.c is still not removed because it contains a
> performance test meant to be run by the user directly (not used in
> t/perf). And it makes sense for such a utility to be a helper.


> diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c
> new file mode 100644
> index 0000000000..3112b10b33
> --- /dev/null
> +++ b/t/unit-tests/t-hashmap.c
> @@ -0,0 +1,358 @@
> +#include "test-lib.h"
> +#include "hashmap.h"
> +#include "strbuf.h"
> +
> +struct test_entry {
> +       int padding; /* hashmap entry no longer needs to be the first member */
> +       struct hashmap_entry ent;
> +       /* key and value as two \0-terminated strings */
> +       char key[FLEX_ARRAY];
> +};
> +
> +static int test_entry_cmp(const void *cmp_data,
> +                         const struct hashmap_entry *eptr,
> +                         const struct hashmap_entry *entry_or_key,
> +                         const void *keydata)
> +{
> +       const int ignore_case = cmp_data ? *((int *)cmp_data) : 0;
> +       const struct test_entry *e1, *e2;
> +       const char *key = keydata;
> +
> +       e1 = container_of(eptr, const struct test_entry, ent);
> +       e2 = container_of(entry_or_key, const struct test_entry, ent);
> +
> +       if (ignore_case)
> +               return strcasecmp(e1->key, key ? key : e2->key);
> +       else
> +               return strcmp(e1->key, key ? key : e2->key);
> +}
> +
> +static const char *get_value(const struct test_entry *e)
> +{
> +       return e->key + strlen(e->key) + 1;
> +}
> +
> +static struct test_entry *alloc_test_entry(unsigned int ignore_case,
> +                                          const char *key, const char *value)

Nit: `unsigned int ignore_case` is a bool flag argument which we often
put at the end of function arguments, so perhaps:

static struct test_entry *alloc_test_entry(const char *key, const char *value,
                                          unsigned int ignore_case)

> +{
> +       size_t klen = strlen(key);
> +       size_t vlen = strlen(value);
> +       unsigned int hash = ignore_case ? strihash(key) : strhash(key);
> +       struct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));
> +
> +       hashmap_entry_init(&entry->ent, hash);
> +       memcpy(entry->key, key, klen + 1);
> +       memcpy(entry->key + klen + 1, value, vlen + 1);
> +       return entry;
> +}
> +
> +static struct test_entry *get_test_entry(struct hashmap *map,
> +                                        unsigned int ignore_case, const char *key)

Here also `unsigned int ignore_case` might want to be the last argument.

> +{
> +       return hashmap_get_entry_from_hash(
> +               map, ignore_case ? strihash(key) : strhash(key), key,
> +               struct test_entry, ent);
> +}
> +
> +static int key_val_contains(const char *key_val[][2], char seen[], size_t n,
> +                           struct test_entry *entry)
> +{
> +       for (size_t i = 0; i < n; i++) {
> +               if (!strcmp(entry->key, key_val[i][0]) &&
> +                   !strcmp(get_value(entry), key_val[i][1])) {
> +                       if (seen[i])
> +                               return 2;
> +                       seen[i] = 1;
> +                       return 0;
> +               }
> +       }
> +       return 1;
> +}
> +
> +static void setup(void (*f)(struct hashmap *map, int ignore_case),
> +                 int ignore_case)

Why `int ignore_case` here, instead of `unsigned int ignore_case` above?

It's nice that the `ignore_case` argument is the last argument though.

> +{
> +       struct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);
> +
> +       f(&map, ignore_case);
> +       hashmap_clear_and_free(&map, struct test_entry, ent);
> +}
> +
> +static void t_replace(struct hashmap *map, int ignore_case)

Here also, why `int ignore_case` here, instead of `unsigned int
ignore_case` towards the top of this file?

I won't mention it anymore below, but I think it might be better to
have `unsigned int ignore_case` everywhere.

> +{
> +       struct test_entry *entry;
> +
> +       entry = alloc_test_entry(ignore_case, "key1", "value1");
> +       check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> +
> +       entry = alloc_test_entry(ignore_case, ignore_case ? "Key1" : "key1",
> +                                "value2");
> +       entry = hashmap_put_entry(map, entry, ent);
> +       if (check(entry != NULL))
> +               check_str(get_value(entry), "value1");
> +       free(entry);
> +
> +       entry = alloc_test_entry(ignore_case, "fooBarFrotz", "value3");
> +       check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> +
> +       entry = alloc_test_entry(ignore_case,
> +                                ignore_case ? "foobarfrotz" : "fooBarFrotz",
> +                                "value4");

Maybe using something like "FOObarFrotZ" instead of "foobarfrotz"
would make it clear that we don't need to downcase in case we ignore
case.

> +       entry = hashmap_put_entry(map, entry, ent);
> +       if (check(entry != NULL))
> +               check_str(get_value(entry), "value3");
> +       free(entry);
> +}
> +
> +static void t_get(struct hashmap *map, int ignore_case)
> +{
> +       struct test_entry *entry;
> +       const char *key_val[][2] = { { "key1", "value1" },
> +                                    { "key2", "value2" },
> +                                    { "fooBarFrotz", "value3" },
> +                                    { ignore_case ? "key4" : "foobarfrotz", "value4" } };
> +       const char *query[][2] = {
> +               { ignore_case ? "Key1" : "key1", "value1" },
> +               { ignore_case ? "keY2" : "key2", "value2" },
> +               { ignore_case ? "foobarfrotz" : "fooBarFrotz", "value3" }
> +       };

I think adding a test case like:

               { ignore_case ? "FOOBarFrotZ" : "foobarfrotz",
                 ignore_case ? : "value3" : "value4" }

which is a bit similar to what Junio suggested, could help a bit check
that things work well especially when not ignoring the case.

> +       for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> +               entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> +               check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
> +       }
> +
> +       for (size_t i = 0; i < ARRAY_SIZE(query); i++) {
> +               entry = get_test_entry(map, ignore_case, query[i][0]);
> +               if (check(entry != NULL))
> +                       check_str(get_value(entry), query[i][1]);
> +               else
> +                       test_msg("query key: %s", query[i][0]);
> +       }
> +
> +       check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
> +       check_int(map->tablesize, ==, 64);
> +       check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> +}
> +
> +static void t_add(struct hashmap *map, int ignore_case)
> +{
> +       struct test_entry *entry;
> +       const char *key_val[][2] = {
> +               { "key1", "value1" },
> +               { ignore_case ? "Key1" : "key1", "value2" },
> +               { "fooBarFrotz", "value3" },
> +               { ignore_case ? "Foobarfrotz" : "fooBarFrotz", "value4" }
> +       };
> +       const char *queries[] = { "key1",
> +                                 ignore_case ? "Foobarfrotz" : "fooBarFrotz" };

It is tricky that here `queries` is not defined and doesn't contain
the same things as in the other functions above:

const char *queries[][2] = ...

Maybe changing the name to something like `query_keys` would help make
people aware that here it contains only keys.

> +       char seen[ARRAY_SIZE(key_val)] = { 0 };
> +
> +       for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
> +               entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
> +               hashmap_add(map, &entry->ent);
> +       }
> +
> +       for (size_t i = 0; i < ARRAY_SIZE(queries); i++) {
> +               int count = 0;
> +               entry = hashmap_get_entry_from_hash(map,
> +                       ignore_case ? strihash(queries[i]) :
> +                                     strhash(queries[i]),
> +                       queries[i], struct test_entry, ent);
> +
> +               hashmap_for_each_entry_from(map, entry, ent)
> +               {
> +                       int ret;
> +                       if (!check_int((ret = key_val_contains(
> +                                               key_val, seen,
> +                                               ARRAY_SIZE(key_val), entry)),
> +                                      ==, 0)) {
> +                               switch (ret) {
> +                               case 1:
> +                                       test_msg("found entry was not given in the input\n"
> +                                                "    key: %s\n  value: %s",
> +                                                entry->key, get_value(entry));
> +                                       break;
> +                               case 2:
> +                                       test_msg("duplicate entry detected\n"
> +                                                "    key: %s\n  value: %s",
> +                                                entry->key, get_value(entry));
> +                                       break;
> +                               }
> +                       } else {
> +                               count++;
> +                       }
> +               }
> +               check_int(count, ==, 2);
> +       }
> +
> +       for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
> +               if (!check_int(seen[i], ==, 1))
> +                       test_msg("following key-val pair was not iterated over:\n"
> +                                "    key: %s\n  value: %s",
> +                                key_val[i][0], key_val[i][1]);
> +       }
> +
> +       check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
> +       check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
> +}

Thanks!
diff mbox series

Patch

diff --git a/Makefile b/Makefile
index 3eab701b10..74bb026610 100644
--- a/Makefile
+++ b/Makefile
@@ -1336,6 +1336,7 @@  THIRD_PARTY_SOURCES += sha1dc/%
 UNIT_TEST_PROGRAMS += t-ctype
 UNIT_TEST_PROGRAMS += t-example-decorate
 UNIT_TEST_PROGRAMS += t-hash
+UNIT_TEST_PROGRAMS += t-hashmap
 UNIT_TEST_PROGRAMS += t-mem-pool
 UNIT_TEST_PROGRAMS += t-oidtree
 UNIT_TEST_PROGRAMS += t-prio-queue
diff --git a/t/helper/test-hashmap.c b/t/helper/test-hashmap.c
index 2912899558..7b854a7030 100644
--- a/t/helper/test-hashmap.c
+++ b/t/helper/test-hashmap.c
@@ -12,11 +12,6 @@  struct test_entry
 	char key[FLEX_ARRAY];
 };
 
-static const char *get_value(const struct test_entry *e)
-{
-	return e->key + strlen(e->key) + 1;
-}
-
 static int test_entry_cmp(const void *cmp_data,
 			  const struct hashmap_entry *eptr,
 			  const struct hashmap_entry *entry_or_key,
@@ -141,30 +136,16 @@  static void perf_hashmap(unsigned int method, unsigned int rounds)
 /*
  * Read stdin line by line and print result of commands to stdout:
  *
- * hash key -> strhash(key) memhash(key) strihash(key) memihash(key)
- * put key value -> NULL / old value
- * get key -> NULL / value
- * remove key -> NULL / old value
- * iterate -> key1 value1\nkey2 value2\n...
- * size -> tablesize numentries
- *
  * perfhashmap method rounds -> test hashmap.[ch] performance
  */
 int cmd__hashmap(int argc, const char **argv)
 {
 	struct string_list parts = STRING_LIST_INIT_NODUP;
 	struct strbuf line = STRBUF_INIT;
-	int icase;
-	struct hashmap map = HASHMAP_INIT(test_entry_cmp, &icase);
-
-	/* init hash map */
-	icase = argc > 1 && !strcmp("ignorecase", argv[1]);
 
 	/* process commands from stdin */
 	while (strbuf_getline(&line, stdin) != EOF) {
 		char *cmd, *p1, *p2;
-		unsigned int hash = 0;
-		struct test_entry *entry;
 
 		/* break line into command and up to two parameters */
 		string_list_setlen(&parts, 0);
@@ -180,84 +161,8 @@  int cmd__hashmap(int argc, const char **argv)
 		cmd = parts.items[0].string;
 		p1 = parts.nr >= 1 ? parts.items[1].string : NULL;
 		p2 = parts.nr >= 2 ? parts.items[2].string : NULL;
-		if (p1)
-			hash = icase ? strihash(p1) : strhash(p1);
-
-		if (!strcmp("add", cmd) && p1 && p2) {
-
-			/* create entry with key = p1, value = p2 */
-			entry = alloc_test_entry(hash, p1, p2);
-
-			/* add to hashmap */
-			hashmap_add(&map, &entry->ent);
-
-		} else if (!strcmp("put", cmd) && p1 && p2) {
-
-			/* create entry with key = p1, value = p2 */
-			entry = alloc_test_entry(hash, p1, p2);
-
-			/* add / replace entry */
-			entry = hashmap_put_entry(&map, entry, ent);
-
-			/* print and free replaced entry, if any */
-			puts(entry ? get_value(entry) : "NULL");
-			free(entry);
-
-		} else if (!strcmp("get", cmd) && p1) {
-			/* lookup entry in hashmap */
-			entry = hashmap_get_entry_from_hash(&map, hash, p1,
-							struct test_entry, ent);
-
-			/* print result */
-			if (!entry)
-				puts("NULL");
-			hashmap_for_each_entry_from(&map, entry, ent)
-				puts(get_value(entry));
-
-		} else if (!strcmp("remove", cmd) && p1) {
-
-			/* setup static key */
-			struct hashmap_entry key;
-			struct hashmap_entry *rm;
-			hashmap_entry_init(&key, hash);
-
-			/* remove entry from hashmap */
-			rm = hashmap_remove(&map, &key, p1);
-			entry = rm ? container_of(rm, struct test_entry, ent)
-					: NULL;
-
-			/* print result and free entry*/
-			puts(entry ? get_value(entry) : "NULL");
-			free(entry);
-
-		} else if (!strcmp("iterate", cmd)) {
-			struct hashmap_iter iter;
-
-			hashmap_for_each_entry(&map, &iter, entry,
-						ent /* member name */)
-				printf("%s %s\n", entry->key, get_value(entry));
-
-		} else if (!strcmp("size", cmd)) {
-
-			/* print table sizes */
-			printf("%u %u\n", map.tablesize,
-			       hashmap_get_size(&map));
-
-		} else if (!strcmp("intern", cmd) && p1) {
-
-			/* test that strintern works */
-			const char *i1 = strintern(p1);
-			const char *i2 = strintern(p1);
-			if (strcmp(i1, p1))
-				printf("strintern(%s) returns %s\n", p1, i1);
-			else if (i1 == p1)
-				printf("strintern(%s) returns input pointer\n", p1);
-			else if (i1 != i2)
-				printf("strintern(%s) != strintern(%s)", i1, i2);
-			else
-				printf("%s\n", i1);
-
-		} else if (!strcmp("perfhashmap", cmd) && p1 && p2) {
+	
+		if (!strcmp("perfhashmap", cmd) && p1 && p2) {
 
 			perf_hashmap(atoi(p1), atoi(p2));
 
@@ -270,6 +175,5 @@  int cmd__hashmap(int argc, const char **argv)
 
 	string_list_clear(&parts, 0);
 	strbuf_release(&line);
-	hashmap_clear_and_free(&map, struct test_entry, ent);
 	return 0;
 }
diff --git a/t/t0011-hashmap.sh b/t/t0011-hashmap.sh
deleted file mode 100755
index 46e74ad107..0000000000
--- a/t/t0011-hashmap.sh
+++ /dev/null
@@ -1,260 +0,0 @@ 
-#!/bin/sh
-
-test_description='test hashmap and string hash functions'
-
-TEST_PASSES_SANITIZE_LEAK=true
-. ./test-lib.sh
-
-test_hashmap() {
-	echo "$1" | test-tool hashmap $3 > actual &&
-	echo "$2" > expect &&
-	test_cmp expect actual
-}
-
-test_expect_success 'put' '
-
-test_hashmap "put key1 value1
-put key2 value2
-put fooBarFrotz value3
-put foobarfrotz value4
-size" "NULL
-NULL
-NULL
-NULL
-64 4"
-
-'
-
-test_expect_success 'put (case insensitive)' '
-
-test_hashmap "put key1 value1
-put key2 value2
-put fooBarFrotz value3
-size" "NULL
-NULL
-NULL
-64 3" ignorecase
-
-'
-
-test_expect_success 'replace' '
-
-test_hashmap "put key1 value1
-put key1 value2
-put fooBarFrotz value3
-put fooBarFrotz value4
-size" "NULL
-value1
-NULL
-value3
-64 2"
-
-'
-
-test_expect_success 'replace (case insensitive)' '
-
-test_hashmap "put key1 value1
-put Key1 value2
-put fooBarFrotz value3
-put foobarfrotz value4
-size" "NULL
-value1
-NULL
-value3
-64 2" ignorecase
-
-'
-
-test_expect_success 'get' '
-
-test_hashmap "put key1 value1
-put key2 value2
-put fooBarFrotz value3
-put foobarfrotz value4
-get key1
-get key2
-get fooBarFrotz
-get notInMap" "NULL
-NULL
-NULL
-NULL
-value1
-value2
-value3
-NULL"
-
-'
-
-test_expect_success 'get (case insensitive)' '
-
-test_hashmap "put key1 value1
-put key2 value2
-put fooBarFrotz value3
-get Key1
-get keY2
-get foobarfrotz
-get notInMap" "NULL
-NULL
-NULL
-value1
-value2
-value3
-NULL" ignorecase
-
-'
-
-test_expect_success 'add' '
-
-test_hashmap "add key1 value1
-add key1 value2
-add fooBarFrotz value3
-add fooBarFrotz value4
-get key1
-get fooBarFrotz
-get notInMap" "value2
-value1
-value4
-value3
-NULL"
-
-'
-
-test_expect_success 'add (case insensitive)' '
-
-test_hashmap "add key1 value1
-add Key1 value2
-add fooBarFrotz value3
-add foobarfrotz value4
-get key1
-get Foobarfrotz
-get notInMap" "value2
-value1
-value4
-value3
-NULL" ignorecase
-
-'
-
-test_expect_success 'remove' '
-
-test_hashmap "put key1 value1
-put key2 value2
-put fooBarFrotz value3
-remove key1
-remove key2
-remove notInMap
-size" "NULL
-NULL
-NULL
-value1
-value2
-NULL
-64 1"
-
-'
-
-test_expect_success 'remove (case insensitive)' '
-
-test_hashmap "put key1 value1
-put key2 value2
-put fooBarFrotz value3
-remove Key1
-remove keY2
-remove notInMap
-size" "NULL
-NULL
-NULL
-value1
-value2
-NULL
-64 1" ignorecase
-
-'
-
-test_expect_success 'iterate' '
-	test-tool hashmap >actual.raw <<-\EOF &&
-	put key1 value1
-	put key2 value2
-	put fooBarFrotz value3
-	iterate
-	EOF
-
-	cat >expect <<-\EOF &&
-	NULL
-	NULL
-	NULL
-	fooBarFrotz value3
-	key1 value1
-	key2 value2
-	EOF
-
-	sort <actual.raw >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success 'iterate (case insensitive)' '
-	test-tool hashmap ignorecase >actual.raw <<-\EOF &&
-	put key1 value1
-	put key2 value2
-	put fooBarFrotz value3
-	iterate
-	EOF
-
-	cat >expect <<-\EOF &&
-	NULL
-	NULL
-	NULL
-	fooBarFrotz value3
-	key1 value1
-	key2 value2
-	EOF
-
-	sort <actual.raw >actual &&
-	test_cmp expect actual
-'
-
-test_expect_success 'grow / shrink' '
-
-	rm -f in &&
-	rm -f expect &&
-	for n in $(test_seq 51)
-	do
-		echo put key$n value$n >> in &&
-		echo NULL >> expect || return 1
-	done &&
-	echo size >> in &&
-	echo 64 51 >> expect &&
-	echo put key52 value52 >> in &&
-	echo NULL >> expect &&
-	echo size >> in &&
-	echo 256 52 >> expect &&
-	for n in $(test_seq 12)
-	do
-		echo remove key$n >> in &&
-		echo value$n >> expect || return 1
-	done &&
-	echo size >> in &&
-	echo 256 40 >> expect &&
-	echo remove key40 >> in &&
-	echo value40 >> expect &&
-	echo size >> in &&
-	echo 64 39 >> expect &&
-	test-tool hashmap <in >out &&
-	test_cmp expect out
-
-'
-
-test_expect_success 'string interning' '
-
-test_hashmap "intern value1
-intern Value1
-intern value2
-intern value2
-" "value1
-Value1
-value2
-value2"
-
-'
-
-test_done
diff --git a/t/unit-tests/t-hashmap.c b/t/unit-tests/t-hashmap.c
new file mode 100644
index 0000000000..3112b10b33
--- /dev/null
+++ b/t/unit-tests/t-hashmap.c
@@ -0,0 +1,358 @@ 
+#include "test-lib.h"
+#include "hashmap.h"
+#include "strbuf.h"
+
+struct test_entry {
+	int padding; /* hashmap entry no longer needs to be the first member */
+	struct hashmap_entry ent;
+	/* key and value as two \0-terminated strings */
+	char key[FLEX_ARRAY];
+};
+
+static int test_entry_cmp(const void *cmp_data,
+			  const struct hashmap_entry *eptr,
+			  const struct hashmap_entry *entry_or_key,
+			  const void *keydata)
+{
+	const int ignore_case = cmp_data ? *((int *)cmp_data) : 0;
+	const struct test_entry *e1, *e2;
+	const char *key = keydata;
+
+	e1 = container_of(eptr, const struct test_entry, ent);
+	e2 = container_of(entry_or_key, const struct test_entry, ent);
+
+	if (ignore_case)
+		return strcasecmp(e1->key, key ? key : e2->key);
+	else
+		return strcmp(e1->key, key ? key : e2->key);
+}
+
+static const char *get_value(const struct test_entry *e)
+{
+	return e->key + strlen(e->key) + 1;
+}
+
+static struct test_entry *alloc_test_entry(unsigned int ignore_case,
+					   const char *key, const char *value)
+{
+	size_t klen = strlen(key);
+	size_t vlen = strlen(value);
+	unsigned int hash = ignore_case ? strihash(key) : strhash(key);
+	struct test_entry *entry = xmalloc(st_add4(sizeof(*entry), klen, vlen, 2));
+
+	hashmap_entry_init(&entry->ent, hash);
+	memcpy(entry->key, key, klen + 1);
+	memcpy(entry->key + klen + 1, value, vlen + 1);
+	return entry;
+}
+
+static struct test_entry *get_test_entry(struct hashmap *map,
+					 unsigned int ignore_case, const char *key)
+{
+	return hashmap_get_entry_from_hash(
+		map, ignore_case ? strihash(key) : strhash(key), key,
+		struct test_entry, ent);
+}
+
+static int key_val_contains(const char *key_val[][2], char seen[], size_t n,
+			    struct test_entry *entry)
+{
+	for (size_t i = 0; i < n; i++) {
+		if (!strcmp(entry->key, key_val[i][0]) &&
+		    !strcmp(get_value(entry), key_val[i][1])) {
+			if (seen[i])
+				return 2;
+			seen[i] = 1;
+			return 0;
+		}
+	}
+	return 1;
+}
+
+static void setup(void (*f)(struct hashmap *map, int ignore_case),
+		  int ignore_case)
+{
+	struct hashmap map = HASHMAP_INIT(test_entry_cmp, &ignore_case);
+
+	f(&map, ignore_case);
+	hashmap_clear_and_free(&map, struct test_entry, ent);
+}
+
+static void t_replace(struct hashmap *map, int ignore_case)
+{
+	struct test_entry *entry;
+
+	entry = alloc_test_entry(ignore_case, "key1", "value1");
+	check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+
+	entry = alloc_test_entry(ignore_case, ignore_case ? "Key1" : "key1",
+				 "value2");
+	entry = hashmap_put_entry(map, entry, ent);
+	if (check(entry != NULL))
+		check_str(get_value(entry), "value1");
+	free(entry);
+
+	entry = alloc_test_entry(ignore_case, "fooBarFrotz", "value3");
+	check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+
+	entry = alloc_test_entry(ignore_case,
+				 ignore_case ? "foobarfrotz" : "fooBarFrotz",
+				 "value4");
+	entry = hashmap_put_entry(map, entry, ent);
+	if (check(entry != NULL))
+		check_str(get_value(entry), "value3");
+	free(entry);
+}
+
+static void t_get(struct hashmap *map, int ignore_case)
+{
+	struct test_entry *entry;
+	const char *key_val[][2] = { { "key1", "value1" },
+				     { "key2", "value2" },
+				     { "fooBarFrotz", "value3" },
+				     { ignore_case ? "key4" : "foobarfrotz", "value4" } };
+	const char *query[][2] = {
+		{ ignore_case ? "Key1" : "key1", "value1" },
+		{ ignore_case ? "keY2" : "key2", "value2" },
+		{ ignore_case ? "foobarfrotz" : "fooBarFrotz", "value3" }
+	};
+
+	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
+		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+	}
+
+	for (size_t i = 0; i < ARRAY_SIZE(query); i++) {
+		entry = get_test_entry(map, ignore_case, query[i][0]);
+		if (check(entry != NULL))
+			check_str(get_value(entry), query[i][1]);
+		else
+			test_msg("query key: %s", query[i][0]);
+	}
+
+	check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
+	check_int(map->tablesize, ==, 64);
+	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
+}
+
+static void t_add(struct hashmap *map, int ignore_case)
+{
+	struct test_entry *entry;
+	const char *key_val[][2] = {
+		{ "key1", "value1" },
+		{ ignore_case ? "Key1" : "key1", "value2" },
+		{ "fooBarFrotz", "value3" },
+		{ ignore_case ? "Foobarfrotz" : "fooBarFrotz", "value4" }
+	};
+	const char *queries[] = { "key1",
+				  ignore_case ? "Foobarfrotz" : "fooBarFrotz" };
+	char seen[ARRAY_SIZE(key_val)] = { 0 };
+
+	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
+		hashmap_add(map, &entry->ent);
+	}
+
+	for (size_t i = 0; i < ARRAY_SIZE(queries); i++) {
+		int count = 0;
+		entry = hashmap_get_entry_from_hash(map,
+			ignore_case ? strihash(queries[i]) :
+				      strhash(queries[i]),
+			queries[i], struct test_entry, ent);
+
+		hashmap_for_each_entry_from(map, entry, ent)
+		{
+			int ret;
+			if (!check_int((ret = key_val_contains(
+						key_val, seen,
+						ARRAY_SIZE(key_val), entry)),
+				       ==, 0)) {
+				switch (ret) {
+				case 1:
+					test_msg("found entry was not given in the input\n"
+						 "    key: %s\n  value: %s",
+						 entry->key, get_value(entry));
+					break;
+				case 2:
+					test_msg("duplicate entry detected\n"
+						 "    key: %s\n  value: %s",
+						 entry->key, get_value(entry));
+					break;
+				}
+			} else {
+				count++;
+			}
+		}
+		check_int(count, ==, 2);
+	}
+
+	for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
+		if (!check_int(seen[i], ==, 1))
+			test_msg("following key-val pair was not iterated over:\n"
+				 "    key: %s\n  value: %s",
+				 key_val[i][0], key_val[i][1]);
+	}
+
+	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
+	check_pointer_eq(get_test_entry(map, ignore_case, "notInMap"), NULL);
+}
+
+static void t_remove(struct hashmap *map, int ignore_case)
+{
+	struct test_entry *entry, *removed;
+	const char *key_val[][2] = { { "key1", "value1" },
+				     { "key2", "value2" },
+				     { "fooBarFrotz", "value3" } };
+	const char *remove[][2] = { { ignore_case ? "Key1" : "key1", "value1" },
+				    { ignore_case ? "keY2" : "key2", "value2" } };
+
+	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
+		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+	}
+
+	for (size_t i = 0; i < ARRAY_SIZE(remove); i++) {
+		entry = alloc_test_entry(ignore_case, remove[i][0], "");
+		removed = hashmap_remove_entry(map, entry, ent, remove[i][0]);
+		if (check(removed != NULL))
+			check_str(get_value(removed), remove[i][1]);
+		free(entry);
+		free(removed);
+	}
+
+	entry = alloc_test_entry(ignore_case, "notInMap", "");
+	check_pointer_eq(hashmap_remove_entry(map, entry, ent, "notInMap"), NULL);
+	free(entry);
+
+	check_int(map->tablesize, ==, 64);
+	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val) - ARRAY_SIZE(remove));
+}
+
+static void t_iterate(struct hashmap *map, int ignore_case)
+{
+	struct test_entry *entry;
+	struct hashmap_iter iter;
+	const char *key_val[][2] = { { "key1", "value1" },
+				     { "key2", "value2" },
+				     { "fooBarFrotz", "value3" } };
+	char seen[ARRAY_SIZE(key_val)] = { 0 };
+
+	for (size_t i = 0; i < ARRAY_SIZE(key_val); i++) {
+		entry = alloc_test_entry(ignore_case, key_val[i][0], key_val[i][1]);
+		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+	}
+
+	hashmap_for_each_entry(map, &iter, entry, ent /* member name */)
+	{
+		int ret;
+		if (!check_int((ret = key_val_contains(key_val, seen,
+						       ARRAY_SIZE(key_val),
+						       entry)), ==, 0)) {
+			switch (ret) {
+			case 1:
+				test_msg("found entry was not given in the input\n"
+					 "    key: %s\n  value: %s",
+					 entry->key, get_value(entry));
+				break;
+			case 2:
+				test_msg("duplicate entry detected\n"
+					 "    key: %s\n  value: %s",
+					 entry->key, get_value(entry));
+				break;
+			}
+		}
+	}
+
+	for (size_t i = 0; i < ARRAY_SIZE(seen); i++) {
+		if (!check_int(seen[i], ==, 1))
+			test_msg("following key-val pair was not iterated over:\n"
+				 "    key: %s\n  value: %s",
+				 key_val[i][0], key_val[i][1]);
+	}
+
+	check_int(hashmap_get_size(map), ==, ARRAY_SIZE(key_val));
+}
+
+static void t_alloc(struct hashmap *map, int ignore_case)
+{
+	struct test_entry *entry, *removed;
+
+	for (int i = 1; i <= 51; i++) {
+		char *key = xstrfmt("key%d", i);
+		char *value = xstrfmt("value%d", i);
+		entry = alloc_test_entry(ignore_case, key, value);
+		check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+		free(key);
+		free(value);
+	}
+	check_int(map->tablesize, ==, 64);
+	check_int(hashmap_get_size(map), ==, 51);
+
+	entry = alloc_test_entry(ignore_case, "key52", "value52");
+	check_pointer_eq(hashmap_put_entry(map, entry, ent), NULL);
+	check_int(map->tablesize, ==, 256);
+	check_int(hashmap_get_size(map), ==, 52);
+
+	for (int i = 1; i <= 12; i++) {
+		char *key = xstrfmt("key%d", i);
+		char *value = xstrfmt("value%d", i);
+
+		entry = alloc_test_entry(ignore_case, key, "");
+		removed = hashmap_remove_entry(map, entry, ent, key);
+		if (check(removed != NULL))
+			check_str(value, get_value(removed));
+		free(key);
+		free(value);
+		free(entry);
+		free(removed);
+	}
+	check_int(map->tablesize, ==, 256);
+	check_int(hashmap_get_size(map), ==, 40);
+
+	entry = alloc_test_entry(ignore_case, "key40", "");
+	removed = hashmap_remove_entry(map, entry, ent, "key40");
+	if (check(removed != NULL))
+		check_str("value40", get_value(removed));
+	check_int(map->tablesize, ==, 64);
+	check_int(hashmap_get_size(map), ==, 39);
+	free(entry);
+	free(removed);
+}
+
+static void t_intern(struct hashmap *map, int ignore_case)
+{
+	const char *values[] = { "value1", "Value1", "value2", "value2" };
+
+	for (size_t i = 0; i < ARRAY_SIZE(values); i++) {
+		const char *i1 = strintern(values[i]);
+		const char *i2 = strintern(values[i]);
+
+		if (!check(!strcmp(i1, values[i])))
+			test_msg("strintern(%s) returns %s\n", values[i], i1);
+		else if (!check(i1 != values[i]))
+			test_msg("strintern(%s) returns input pointer\n",
+				 values[i]);
+		else if (!check_pointer_eq(i1, i2))
+			test_msg("address('%s') != address('%s'), so strintern('%s') != strintern('%s')",
+				 i1, i2, values[i], values[i]);
+		else
+			check_str(i1, values[i]);
+	}
+}
+
+int cmd_main(int argc UNUSED, const char **argv UNUSED)
+{
+	TEST(setup(t_replace, 0), "replace works");
+	TEST(setup(t_replace, 1), "replace (case insensitive) works");
+	TEST(setup(t_get, 0), "get works");
+	TEST(setup(t_get, 1), "get (case insensitive) works");
+	TEST(setup(t_add, 0), "add works");
+	TEST(setup(t_add, 1), "add (case insensitive) works");
+	TEST(setup(t_remove, 0), "remove works");
+	TEST(setup(t_remove, 1), "remove (case insensitive) works");
+	TEST(setup(t_iterate, 0), "iterate works");
+	TEST(setup(t_iterate, 1), "iterate (case insensitive) works");
+	TEST(setup(t_alloc, 0), "grow / shrink works");
+	TEST(setup(t_intern, 0), "string interning works");
+	return test_done();
+}