From 1d601eb83b1b849edba047feae5137f0adb93ee2 Mon Sep 17 00:00:00 2001 From: Rob Norris Date: Tue, 5 May 2026 12:44:11 +1000 Subject: [PATCH] unit/test_zap: a trivial ZAP unit test suite This commit adds the bones of a unit test suite for the ZAP subsystem. The actual tests themselves don't do much, just ZAP creation and destruction and basic KV ops. At this point its intended to be enough to demonstrate what tests under this framework would look like. Sponsored-by: TrueNAS Reviewed-by: Brian Behlendorf Signed-off-by: Rob Norris Closes #18564 --- tests/unit/.gitignore | 2 + tests/unit/Makefile.am | 24 +++- tests/unit/test_zap.c | 273 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_zap.c diff --git a/tests/unit/.gitignore b/tests/unit/.gitignore index 52315f0b5fa..12a60a65666 100644 --- a/tests/unit/.gitignore +++ b/tests/unit/.gitignore @@ -1,2 +1,4 @@ /test_*.info /test_*_coverage + +/test_zap diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am index 25c1c0788cc..43b7ddb84ca 100644 --- a/tests/unit/Makefile.am +++ b/tests/unit/Makefile.am @@ -14,10 +14,32 @@ libunit_la_SOURCES = \ # all test binaries -UNIT_TESTS = +UNIT_TESTS = \ + %D%/test_zap noinst_PROGRAMS = $(UNIT_TESTS) +%C%_test_zap_CFLAGS = $(AM_CFLAGS) + +nodist_%C%_test_zap_SOURCES = \ + module/zfs/zap.c \ + module/zfs/zap_fat.c \ + module/zfs/zap_impl.c \ + module/zfs/zap_micro.c \ + module/zfs/zap_leaf.c \ + module/zfs/u8_textprep.c + +%C%_test_zap_SOURCES = \ + %D%/test_zap.c + +%C%_test_zap_LDADD = \ + libspl.la \ + libbtree.la \ + libunit.la + +%C%_test_zap_LDFLAGS = -pthread + + # test run and coverage targets below PHONY += unit unit-coverage diff --git a/tests/unit/test_zap.c b/tests/unit/test_zap.c new file mode 100644 index 00000000000..d8ec4288ec1 --- /dev/null +++ b/tests/unit/test_zap.c @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: CDDL-1.0 +/* + * This file and its contents are supplied under the terms of the + * Common Development and Distribution License ("CDDL"), version 1.0. + * You may only use this file in accordance with the terms of version + * 1.0 of the CDDL. + * + * A full copy of the text of the CDDL should have accompanied this + * source. A copy of the CDDL is also available via the Internet at + * http://www.illumos.org/license/CDDL. + */ + +/* + * Copyright (c) 2026, TrueNAS. + */ + +#include + +#include +#include +typedef struct spa spa_t; /* forward decl for zap_impl.h */ +#include + +#include "mock_dmu.h" +#include "unit.h" + +/* ========== */ + +/* + * Normally defined and initialised in arc.c. We define and initialise it + * ourselves here so this mock can be linked without arc.c. + */ +uint64_t zfs_crc64_table[256]; + +static void +mock_crc64_init(void) +{ + for (int i = 0; i < 256; i++) { + uint64_t ct = i; + for (int j = 8; j > 0; j--) + ct = (ct >> 1) ^ (-(ct & 1) & ZFS_CRC64_POLY); + zfs_crc64_table[i] = ct; + } +} + +/* Misc utility functions. */ + +#define rd64(ptr, off) (*(uint64_t *)((const char *)(ptr) + (off))) + +/* ========== */ + +/* ZAP-specific mocks and other test helpers. */ + +/* Create a microzap backed by a mock dnode. */ +static dnode_t * +mock_zap_create_microzap(void) { + /* + * We use DMU_OTN_ZAP_DATA so that DMU_OT_BYTESWAP() returns + * DMU_BSWAP_ZAP without consulting dmu_ot[], which is not currently + * provided in the mock. + */ + mock_dnode_t *mdn = mock_dnode_create(512, DMU_OTN_ZAP_DATA); + dnode_t *dn = (dnode_t *)mdn; + dmu_tx_t *tx = (dmu_tx_t *)mock_tx_create(); + mzap_create_impl(dn, 0, 0, tx); + mock_tx_destroy((mock_dmu_tx_t *)tx); + return (dn); +} + +/* Create a fatzap backed by a mock dnode. */ +static dnode_t * +mock_zap_create_fatzap(void) +{ + /* + * We can only create microzaps directly. They only take u64s as a + * value, so we add a u16 to trigger an upgrade to fatzap. + */ + dnode_t *dn = mock_zap_create_microzap(); + dmu_tx_t *tx = (dmu_tx_t *)mock_tx_create(); + uint16_t upgrade = 0; + zap_add_by_dnode(dn, "_upgrade", sizeof (uint16_t), 1, &upgrade, tx); + zap_remove_by_dnode(dn, "_upgrade", tx); + mock_tx_destroy((mock_dmu_tx_t *)tx); + return (dn); +} + +static bool +mock_zap_is_microzap(dnode_t *dn) +{ + /* check block 0 has a microzap header */ + const void *blk = mock_dnode_block_data((mock_dnode_t *)dn, 0); + return (rd64(blk, 0) == ZBT_MICRO); +} + +static bool +mock_zap_is_fatzap(dnode_t *dn) +{ + /* check block 0 has a fatzap header */ + const void *blk = mock_dnode_block_data((mock_dnode_t *)dn, 0); + return (rd64(blk, 0) == ZBT_HEADER && rd64(blk, 8) == ZAP_MAGIC); +} + +static void +mock_zap_destroy(dnode_t *dn) +{ + mock_dnode_destroy((mock_dnode_t *)dn); +} + +/* Create a ZAP of the type named in the given test params. */ +static dnode_t * +mock_zap_create_params(const MunitParameter params[], const char *key) { + const char *type = munit_parameters_get(params, key); + if (type == NULL) + munit_error("mock_zap_create_params: missing type param"); + else if (strcmp(type, "micro") == 0) + return (mock_zap_create_microzap()); + else if (strcmp(type, "fat") == 0) + return (mock_zap_create_fatzap()); + else + munit_errorf("mock_zap_create_params: invalid type '%s'", type); + __builtin_unreachable(); +} + +/* + * Confirm the stored ZAP is of the type named in the given test params. This + * is useful for sanity checks within tests that a ZAP wasn't unexpectedly + * upgraded during the test. + */ +static bool +mock_zap_is_params(dnode_t *dn, const MunitParameter params[], + const char *key) +{ + const char *type = munit_parameters_get(params, key); + if (type == NULL) + munit_error("mock_zap_is_params: missing type param"); + else if (strcmp(type, "micro") == 0) + return (mock_zap_is_microzap(dn)); + else if (strcmp(type, "fat") == 0) + return (mock_zap_is_fatzap(dn)); + else + munit_errorf("mock_zap_is_params: invalid type '%s'", type); + __builtin_unreachable(); +} + +/* ========== */ + +/* + * Sanity checks for mock ZAPs. Ensures that the mock_zap_create_* functions + * really do create the right kind of ZAPs, since many of the tests need to + * run against both kinds to confirm that they all work the same way. + */ +static MunitResult +test_mock_microzap_sanity(const MunitParameter params[], void *data) +{ + (void) params, (void) data; + + dnode_t *dn = mock_zap_create_microzap(); + unit_true(mock_zap_is_microzap(dn)); + mock_zap_destroy(dn); + + return (MUNIT_OK); +} + +static MunitResult +test_mock_fatzap_sanity(const MunitParameter params[], void *data) +{ + (void) params, (void) data; + + dnode_t *dn = mock_zap_create_fatzap(); + unit_true(mock_zap_is_fatzap(dn)); + mock_zap_destroy(dn); + + return (MUNIT_OK); +} + +/* ========== */ + +/* + * A simple add, lookup and remove test. Confirms basic operation. These are + * tested together simply because all other tests rely on these primitives. + */ +static MunitResult +test_zap_basic(const MunitParameter params[], void *data) +{ + (void) data; + + dnode_t *dn = mock_zap_create_params(params, "type"); + dmu_tx_t *tx = (dmu_tx_t *)mock_tx_create(); + + /* Insert a few entries. */ + uint64_t val42 = 42; + uint64_t val99 = 99; + uint64_t val0 = 0; + + unit_ok(zap_add_by_dnode(dn, "hello", + sizeof (uint64_t), 1, &val42, tx)); + unit_ok(zap_add_by_dnode(dn, "world", + sizeof (uint64_t), 1, &val99, tx)); + unit_ok(zap_add_by_dnode(dn, "zero", + sizeof (uint64_t), 1, &val0, tx)); + + /* Lookup each entry. */ + uint64_t result = 0; + unit_ok(zap_lookup_by_dnode(dn, "hello", + sizeof (uint64_t), 1, &result)); + unit_eq(result, 42); + + unit_ok(zap_lookup_by_dnode(dn, "world", + sizeof (uint64_t), 1, &result)); + unit_eq(result, 99); + + unit_ok(zap_lookup_by_dnode(dn, "zero", + sizeof (uint64_t), 1, &result)); + unit_eq(result, 0); + + /* Non-existent key should return ENOENT. */ + unit_err(zap_lookup_by_dnode(dn, "nope", + sizeof (uint64_t), 1, &result), ENOENT); + + /* Removing an entry should make it impossible to look up. */ + unit_ok(zap_remove_by_dnode(dn, "world", tx)); + unit_err(zap_lookup_by_dnode(dn, "world", + sizeof (uint64_t), 1, &result), ENOENT); + + mock_tx_destroy((mock_dmu_tx_t *)tx); + unit_true(mock_zap_is_params(dn, params, "type")); + mock_zap_destroy(dn); + + return (MUNIT_OK); +} + +/* ========== */ + +/* Test suite definition and boilerplate. */ + +#define UNIT_PARAM_ZAP_TYPES(p) \ + UNIT_PARAM((p), "micro", "fat") + +static const MunitParameterEnum zap_type_params[] = { + UNIT_PARAM_ZAP_TYPES("type"), + { 0 }, +}; + +static const MunitTest zap_tests[] = { + UNIT_TEST("mock_microzap_sanity", test_mock_microzap_sanity), + UNIT_TEST("mock_fatzap_sanity", test_mock_fatzap_sanity), + + UNIT_TEST("zap_basic", test_zap_basic, zap_type_params), + + { 0 }, +}; + +static const MunitSuite zap_test_suite = { + "zap.", + zap_tests, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; + +int +main(int argc, char **argv) +{ + mock_crc64_init(); + + zap_init(); + + int rc = munit_suite_main(&zap_test_suite, NULL, argc, argv); + + zap_fini(); + + return (rc); +}