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 <behlendorf1@llnl.gov>
Signed-off-by: Rob Norris <rob.norris@truenas.com>
Closes #18564
This commit is contained in:
Rob Norris
2026-05-05 12:44:11 +10:00
committed by Brian Behlendorf
parent a20ef9c4e7
commit 1d601eb83b
3 changed files with 298 additions and 1 deletions
+2
View File
@@ -1,2 +1,4 @@
/test_*.info
/test_*_coverage
/test_zap
+23 -1
View File
@@ -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
+273
View File
@@ -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 <stdbool.h>
#include <sys/zap.h>
#include <sys/btree.h>
typedef struct spa spa_t; /* forward decl for zap_impl.h */
#include <sys/zap_impl.h>
#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);
}