Create a PV/VG generator that can generate images with various non-standard transformations, then use it to test various types of both valid and invalid images.
Signed-off-by: Tobias Waldekranz <[email protected]> --- test/self/Kconfig | 14 + test/self/Makefile | 2 + test/self/lvm.c | 818 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 834 insertions(+) create mode 100644 test/self/lvm.c diff --git a/test/self/Kconfig b/test/self/Kconfig index 352deadf05..6db4349e23 100644 --- a/test/self/Kconfig +++ b/test/self/Kconfig @@ -49,6 +49,7 @@ config SELFTEST_ENABLE_ALL select SELFTEST_IDR select SELFTEST_TLV select SELFTEST_DM + select SELFTEST_LVM if BLOCK select SELFTEST_TALLOC select SELFTEST_BLSPEC if BLSPEC && DEFAULT_ENVIRONMENT help @@ -153,6 +154,19 @@ config SELFTEST_TLV select BASE64 select BOARD_LXA +config SELFTEST_LVM + bool "LVM selftest" + depends on BLOCK + select DM_LVM + select RAMDISK_BLK + help + Tests the parsing of LVM2 metadata, using physical volume + images that are assembled in memory and backed by ramdisks. + This covers the tokenizer, the recovery from damaged labels + and metadata areas, the selection of the most recent metadata + when the physical volumes of a group disagree, and the mapping + of logical volumes onto the extents they are made of. + config SELFTEST_DM bool "Device mapper selftest" select DISK diff --git a/test/self/Makefile b/test/self/Makefile index 2bfdbb9949..821b9fa5f3 100644 --- a/test/self/Makefile +++ b/test/self/Makefile @@ -27,6 +27,8 @@ obj-$(CONFIG_SELFTEST_TLV) += tlv.o tlv.dtb.o obj-$(CONFIG_SELFTEST_DM) += dm.o obj-$(CONFIG_SELFTEST_BLSPEC) += blspec.o bbenv-$(CONFIG_SELFTEST_BLSPEC) += defaultenv-blspec-test +obj-$(CONFIG_SELFTEST_LVM) += lvm.o +CFLAGS_lvm.o += -I$(srctree)/drivers/block/dm/lvm ifdef REGENERATE_KEYTOC diff --git a/test/self/lvm.c b/test/self/lvm.c new file mode 100644 index 0000000000..71f63914e2 --- /dev/null +++ b/test/self/lvm.c @@ -0,0 +1,818 @@ +// SPDX-License-Identifier: GPL-2.0-only +// SPDX-FileCopyrightText: 2026 Tobias Waldekranz <[email protected]> + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <block.h> +#include <bselftest.h> +#include <driver.h> +#include <linux/err.h> +#include <linux/kernel.h> +#include <linux/sizes.h> +#include <lvm.h> +#include <ramdisk.h> +#include <stdio.h> +#include <string.h> +#include <xfuncs.h> + +#include <asm/unaligned.h> + +/* Makefile pulls in drivers/block/dm/lvm for access to on-disk + * structures. + */ +#include <lvm2.h> +#include <lvm-md.h> + +BSELFTEST_GLOBALS(); + +#define PV_SIZE SZ_1M +#define MDA_OFFSET (8 * SECTOR_SIZE) +#define MDA_SIZE (SZ_64K - MDA_OFFSET) +#define PE_START SZ_64K +#define EXTENT_SIZE SZ_64K + +#define PE_START_SECTORS (PE_START / SECTOR_SIZE) +#define EXTENT_SECTORS (EXTENT_SIZE / SECTOR_SIZE) +#define PE_COUNT ((PV_SIZE - PE_START) / EXTENT_SIZE) + +#define VG_UUID "vgvgvg11112222333344445555vgvgvg" +#define LV_UUID "lvlvlv11112222333344445555lvlvlv" + +struct test_pv { + struct ramdisk *rd; + struct block_device *blk; + const char *devname; + char uuid[LVM_UUID_LEN + 1]; + u8 *img; +}; + +struct test_seg { + int pv; + u64 start_extent; + u64 extent_count; + u64 pv_extent; +}; + +/* How the image should deviate from a well formed one. */ +struct test_vg_xfrm { + struct { + struct { + int image; /* The sector in which to store the label */ + int header; /* The recorded sector in the header */ + } sector; + + bool bad_crc; + } label; + + struct { + int copies; + + bool bad_text_crc; + bool bad_hdr_crc; + bool precommitted; /* Leave a second document behind */ + bool wrap_text; + } md; +}; + +#define TEST_VG_XFRM_DEFAULT \ + .label = { \ + .sector = { \ + .image = 1, \ + .header = -1, \ + }, \ + }, \ + .md = { \ + .copies = 1, \ + } + +#define TEST_MAX_PVS 2 +#define TEST_MAX_SEGS 3 + +struct test_vg { + struct test_pv pvs[TEST_MAX_PVS]; + int num_pvs; + + struct test_seg segs[TEST_MAX_SEGS]; + int num_segs; + + u64 seqno; + + struct test_vg_xfrm xfrm; +}; + +static char *generate_md(struct test_vg *tvg) +{ + char *text, *pvs = NULL, *segs = NULL; + struct test_seg *seg; + int i; + + for (i = 0; i < tvg->num_pvs; i++) + pvs = xrasprintf(pvs, + " pv%u {\n" + " id = \"%s\"\n" + " device = \"/dev/%s\"\n" + " status = [\"ALLOCATABLE\"]\n" + " flags = []\n" + " dev_size = %u\n" + " pe_start = %u\n" + " pe_count = %u\n" + " }\n", + i, tvg->pvs[i].uuid, tvg->pvs[i].devname, + PV_SIZE / SECTOR_SIZE, PE_START_SECTORS, + PE_COUNT); + + for (i = 0, seg = tvg->segs; i < tvg->num_segs; i++, seg++) + segs = xrasprintf(segs, + " segment%u {\n" + " start_extent = %llu\n" + " extent_count = %llu\n" + " type = \"striped\"\n" + " stripe_count = 1\n" + " stripes = [\n" + " \"pv%u\", %llu\n" + " ]\n" + " }\n", + i + 1, seg->start_extent, seg->extent_count, + seg->pv, seg->pv_extent); + + text = xasprintf("vgtest {\n" + " id = \"%s\"\n" + " seqno = %llu\n" + " format = \"lvm2\"\n" + " status = [\"RESIZEABLE\", \"READ\", \"WRITE\"]\n" + " flags = []\n" + " extent_size = %u\n" + " max_lv = 0\n" + " max_pv = 0\n" + " metadata_copies = 0\n" + " physical_volumes {\n" + "%s" + " }\n" + " logical_volumes {\n" + " lv0 {\n" + " id = \"%s\"\n" + " status = [\"READ\", \"WRITE\", \"VISIBLE\"]\n" + " flags = []\n" + " segment_count = %u\n" + "%s" + " }\n" + " }\n" + "}\n" + "# Generated by the barebox self test\n", + VG_UUID, tvg->seqno, EXTENT_SECTORS, pvs, LV_UUID, + tvg->num_segs, segs); + + free(pvs); + free(segs); + return text; +} + +static void inject_md(struct test_pv *tpv, const char *text, int copy, + const struct test_vg_xfrm *xfrm) +{ + u64 start, head, offset = copy ? PV_SIZE - MDA_SIZE : MDA_OFFSET; + size_t len = strlen(text) + 1; + struct lvm2_md_header *hdr; + u32 crc; + + hdr = (void *)(tpv->img + offset); + + /* The document may start anywhere in the circular buffer that + * follows the header, and wraps back to its beginning. + */ + start = xfrm->md.wrap_text ? MDA_SIZE - len / 2 : SECTOR_SIZE; + head = min_t(u64, len, MDA_SIZE - start); + + memcpy(tpv->img + offset + start, text, head); + if (head < len) + memcpy(tpv->img + offset + SECTOR_SIZE, text + head, + len - head); + + crc = lvm2_crc(text, len); + if (xfrm->md.bad_text_crc && !copy) + crc = ~crc; + + *hdr = (struct lvm2_md_header) { + .version = cpu_to_le32(LVM2_MDA_VERSION), + .start = cpu_to_le64(offset), + .size = cpu_to_le64(MDA_SIZE), + }; + memcpy(hdr->magic, LVM2_MDA_MAGIC, sizeof(hdr->magic)); + + hdr->area[0] = (struct lvm2_md_area) { + .offset = cpu_to_le64(start), + .size = cpu_to_le64(len), + .checksum = cpu_to_le32(crc), + }; + + if (xfrm->md.precommitted && !copy) { + /* LVM only leaves a second document behind if it was + * interrupted while committing new metadata. + */ + start = SECTOR_SIZE + len; + memcpy(tpv->img + offset + start, text, len); + + hdr->area[1] = (struct lvm2_md_area) { + .offset = cpu_to_le64(start), + .size = cpu_to_le64(len), + .checksum = cpu_to_le32(lvm2_crc(text, len)), + }; + } + + crc = lvm2_crc(&hdr->magic, SECTOR_SIZE - sizeof(hdr->checksum)); + if (xfrm->md.bad_hdr_crc && !copy) + crc = ~crc; + + hdr->checksum = cpu_to_le32(crc); +} + +static void inject_label(struct test_pv *tpv, const struct test_vg_xfrm *xfrm) +{ + u8 *sector = tpv->img + xfrm->label.sector.image * SECTOR_SIZE; + struct lvm2_label *label = (void *)sector; + struct lvm2_pv_header *pvh; + struct lvm2_area *mda; + u32 crc; + int i; + + *label = (struct lvm2_label) { + .sector = cpu_to_le64(xfrm->label.sector.header < 0 ? + xfrm->label.sector.image : xfrm->label.sector.header), + .pv_offset = cpu_to_le32(sizeof(*label)), + }; + memcpy(label->id, LVM2_LABEL_ID, sizeof(label->id)); + memcpy(label->type, LVM2_LABEL_TYPE, sizeof(label->type)); + + pvh = (void *)(sector + sizeof(*label)); + *pvh = (struct lvm2_pv_header) { + .size = cpu_to_le64(PV_SIZE >> SECTOR_SHIFT), + }; + memcpy(pvh->uuid, tpv->uuid, sizeof(pvh->uuid)); + + /* One data area, then the metadata areas, each list terminated + * by a zeroed entry. + */ + mda = pvh->area; + mda->offset = cpu_to_le64(PE_START); + mda += 2; + + for (i = 0; i < xfrm->md.copies; i++, mda++) { + *mda = (struct lvm2_area) { + .offset = cpu_to_le64(i ? PV_SIZE - MDA_SIZE : MDA_OFFSET), + .size = cpu_to_le64(MDA_SIZE), + }; + } + + crc = lvm2_crc(&label->pv_offset, + SECTOR_SIZE - offsetof(struct lvm2_label, pv_offset)); + if (xfrm->label.bad_crc) + crc = ~crc; + + label->crc = cpu_to_le32(crc); +} + +static int tvg_init(struct test_vg *tvg) +{ + struct test_pv *tpv; + char *text; + int i, j; + + for (i = 0, tpv = tvg->pvs; i < tvg->num_pvs; i++, tpv++) { + memset(tpv->uuid, 'a' + i, LVM_UUID_LEN); + tpv->uuid[LVM_UUID_LEN] = '\0'; + + tpv->rd = ramdisk_init(SECTOR_SIZE); + if (!tpv->rd) { + pr_err("Failed to create ramdisk\n"); + return -ENODEV; + } + + tpv->img = xzalloc(PV_SIZE); + ramdisk_setup_rw(tpv->rd, tpv->img, PV_SIZE); + + tpv->blk = ramdisk_get_block_device(tpv->rd); + tpv->devname = cdev_name(&tpv->blk->cdev); + } + + text = generate_md(tvg); + + for (i = 0, tpv = tvg->pvs; i < tvg->num_pvs; i++, tpv++) { + for (j = 0; j < tvg->xfrm.md.copies; j++) + inject_md(tpv, text, j, &tvg->xfrm); + + inject_label(tpv, &tvg->xfrm); + } + + free(text); + return 0; +} + +static int tvg_init_simple(struct test_vg *tvg, const struct test_vg_xfrm *xfrm) +{ + *tvg = (struct test_vg) { + .num_pvs = 1, + .num_segs = 1, + .segs = { + { + .pv = 0, + .pv_extent = 0, + .start_extent = 0, + .extent_count = 2, + }, + }, + .seqno = 7, + + .xfrm = *xfrm, + }; + + return tvg_init(tvg); +} + +static void tpv_free(struct test_pv *tpv) +{ + if (tpv->rd) { + ramdisk_setup_rw(tpv->rd, NULL, 0); + ramdisk_free(tpv->rd); + tpv->rd = NULL; + } + + free(tpv->img); + tpv->img = NULL; +} + +static void tvg_free(struct test_vg *tvg) +{ + struct test_pv *tpv; + int i; + + for (i = 0, tpv = tvg->pvs; i < tvg->num_pvs; i++, tpv++) + tpv_free(tpv); +} + +static void test_lvm_simple(void) +{ + struct test_vg_xfrm xfrm = { TEST_VG_XFRM_DEFAULT }; + struct test_vg tvg; + struct lvm_vg *vg; + struct lvm_lv *lv; + char *table, *expect; + + if (tvg_init_simple(&tvg, &xfrm)) + return; + + if (!assert_cond(!lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg))) + goto out; + + assert_streq(vg->name, "vgtest"); + assert_streq(vg->uuid, VG_UUID); + assert_inteq(vg->seqno, 7); + assert_inteq(vg->pe_size, EXTENT_SECTORS); + assert_inteq(vg->num_pvs, 1); + assert_inteq(vg->num_lvs, 1); + + lv = lvm_vg_lv_by_name(vg, "lv0"); + if (!assert_cond(lv)) + goto out_free; + + assert_streq(lv->uuid, LV_UUID); + assert_inteq(lv->type, LVM_LV_LINEAR); + assert_inteq(lv->size, 2 * EXTENT_SECTORS); + + table = lvm_lv_dm_ctable(lv); + if (assert_cond(!IS_ERR(table))) { + expect = xasprintf("0 %u linear /dev/%s %u\n", + 2 * EXTENT_SECTORS, tvg.pvs[0].devname, + PE_START_SECTORS); + assert_streq(table, expect); + free(expect); + free(table); + } + +out_free: + lvm_vg_free(vg); +out: + tvg_free(&tvg); +} +bselftest(core, test_lvm_simple); + + +/* Two PVs, holding an LV whose extents are neither contiguous nor in + * the order they appear on disk. + */ +static void test_lvm_scattered(void) +{ + struct test_vg tvg = { + .num_pvs = 2, + .num_segs = 3, + .segs = { + { + .pv = 0, + .pv_extent = 0, + .start_extent = 4, + .extent_count = 2, + }, + { + .pv = 1, + .pv_extent = 1, + .start_extent = 2, + .extent_count = 2, + }, + { + .pv = 0, + .start_extent = 0, + .extent_count = 2, + .pv_extent = 4, + } + }, + + .seqno = 3, + + .xfrm = { TEST_VG_XFRM_DEFAULT }, + }; + struct lvm_vg *vg; + struct lvm_lv *lv; + char *table, *expect; + + if (tvg_init(&tvg)) + goto out; + + if (!assert_cond(!lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg))) + goto out; + + assert_inteq(vg->num_pvs, 2); + + lv = lvm_vg_lv_by_name(vg, "lv0"); + if (!assert_cond(lv)) + goto out_free; + + assert_inteq(lv->size, 6 * EXTENT_SECTORS); + + table = lvm_lv_dm_ctable(lv); + if (assert_cond(!IS_ERR(table))) { + /* Sorted by logical offset, with every segment mapped to + * the right offset of the right device. + */ + expect = xasprintf("0 %u linear /dev/%s %u\n" + "%u %u linear /dev/%s %u\n" + "%u %u linear /dev/%s %u\n", + 2 * EXTENT_SECTORS, tvg.pvs[0].devname, + PE_START_SECTORS + 4 * EXTENT_SECTORS, + + 2 * EXTENT_SECTORS, 2 * EXTENT_SECTORS, + tvg.pvs[1].devname, + PE_START_SECTORS + 1 * EXTENT_SECTORS, + + 4 * EXTENT_SECTORS, 2 * EXTENT_SECTORS, + tvg.pvs[0].devname, PE_START_SECTORS); + assert_streq(table, expect); + free(expect); + free(table); + } + +out_free: + lvm_vg_free(vg); +out: + tvg_free(&tvg); +} +bselftest(core, test_lvm_scattered); + +struct header_test_case { + const char *name; + bool valid; + struct test_vg_xfrm xfrm; +}; + +static const struct header_test_case header_cases[] = { + { + .name = "pristine", + .valid = true, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + } + }, + { + .name = "label in sector 0", + .valid = true, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .label.sector.image = 0, + } + }, + { + .name = "label out of range", + .valid = false, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .label.sector.image = 4, + } + }, + { + .name = "bad label crc", + .valid = false, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .label.bad_crc = true, + } + }, + { + .name = "wrapped metadata", + .valid = true, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .md.wrap_text = true, + } + }, + { + .name = "bad metadata crc", + .valid = false, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .md.bad_text_crc = true, + } + }, + { + .name = "backup metadata area", + .valid = true, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .md = { + .bad_text_crc = true, + .copies = 2, + }, + } + }, + { + .name = "bad metadata header crc", + .valid = false, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .md.bad_hdr_crc = true, + } + }, + { + .name = "backup after bad header", + .valid = true, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .md = { + .bad_hdr_crc = true, + .copies = 2, + }, + } + }, + { + .name = "relocated label", + .valid = false, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .label.sector.header = 2, + } + }, + { + .name = "precommitted metadata", + .valid = false, + .xfrm = { + TEST_VG_XFRM_DEFAULT, + .md = { + .bad_text_crc = true, + .precommitted = true, + }, + } + }, +}; + +/* Images that must be parsed, and images that must be rejected. */ +static void test_lvm_headers(void) +{ + struct test_vg tvg; + struct lvm_vg *vg; + int err, i; + + for (i = 0; i < ARRAY_SIZE(header_cases); i++) { + if (tvg_init_simple(&tvg, &header_cases[i].xfrm)) + return; + + err = lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg); + if (!assert_inteq(!err, header_cases[i].valid)) + pr_warn("%s: unexpectedly %s\n", header_cases[i].name, + err ? "rejected" : "accepted"); + + if (!err) + lvm_vg_free(vg); + + tvg_free(&tvg); + } +} +bselftest(core, test_lvm_headers); + +/* When the PVs of a VG disagree, the most recently updated metadata + * wins. + */ +static void test_lvm_seqno(void) +{ + struct test_vg tvg = { + .num_pvs = 2, + .num_segs = 1, + .segs = { + { + .pv = 0, + .start_extent = 0, + .extent_count = 2 + }, + }, + .seqno = 4, + .xfrm = { TEST_VG_XFRM_DEFAULT }, + }; + struct lvm_vg_iter *iter; + struct lvm_vg *vg; + int found = 0; + char *text; + + if (tvg_init(&tvg)) + goto out; + + /* Rewrite the second PV with a newer document. */ + tvg.seqno = 9; + text = generate_md(&tvg); + inject_md(&tvg.pvs[1], text, 0, &tvg.xfrm); + free(text); + + iter = lvm_vg_iter_new(); + + /* The iterator hands over ownership of every VG it returns. */ + while ((vg = lvm_vg_iter_next(iter))) { + if (!strcmp(vg->name, "vgtest")) { + found++; + assert_inteq(vg->seqno, 9); + } + + lvm_vg_free(vg); + } + + /* Both PVs describe the same VG, which must be reported once. */ + assert_inteq(found, 1); + lvm_vg_iter_free(iter); +out: + tvg_free(&tvg); +} +bselftest(core, test_lvm_seqno); + +/* An LV that references a PV which is not present must not be mapped. */ +static void test_lvm_missing_pv(void) +{ + struct test_vg tvg = { + .num_pvs = 2, + .num_segs = 2, + .segs = { + { .pv = 0, .start_extent = 0, .extent_count = 2 }, + { .pv = 1, .start_extent = 2, .extent_count = 2 }, + }, + .seqno = 1, + .xfrm = { TEST_VG_XFRM_DEFAULT }, + }; + struct lvm_vg *vg; + struct lvm_lv *lv; + char *table; + + if (tvg_init(&tvg)) + goto out; + + /* Take the second PV away again, leaving the metadata on the + * first one referring to a device that is not there. + */ + tpv_free(&tvg.pvs[1]); + + if (!assert_cond(!lvm_vg_alloc_by_cdev(&tvg.pvs[0].blk->cdev, &vg))) + goto out; + + lv = lvm_vg_lv_by_name(vg, "lv0"); + if (!assert_cond(lv)) + goto out_free; + + table = lvm_lv_dm_ctable(lv); + if (!assert_cond(IS_ERR(table))) { + pr_warn("mapped an LV with a missing PV: %s\n", table); + free(table); + } + +out_free: + lvm_vg_free(vg); +out: + tvg_free(&tvg); +} +bselftest(core, test_lvm_missing_pv); + +/* Documents that the tokenizer must reject, and one that it must + * not. + */ +static void test_lvm_md_parse(void) +{ + static const struct { + const char *text; + bool valid; + } md_cases[] = { + { "", true }, + { "vg {\n}\n", true }, + { "vg {\nid = \"x\"\n}\n", true }, + { "vg {\nflags = []\n}\n", true }, + { "vg {\na = [1, 2, 3]\n}\n", true }, + { "# just a comment\n", true }, + { "vg {\nid = \"x\" # trailing\n}\n", true }, + { "a = 1\0garbage", true }, + { "vg {\n", false }, + { "vg }\n", false }, + { "vg {\nid = \n}\n", false }, + { "vg {\nid = \"unterminated\n}\n", false }, + { "vg {\na = [1, 2\n}\n", false }, + { "= 1\n", false }, + { "vg {\n}\n}\n", false }, + { "\x01\x02\x03", false }, + }; + struct lvm_md *md; + int err, i; + + for (i = 0; i < ARRAY_SIZE(md_cases); i++) { + err = lvm_md_parse_alloc(md_cases[i].text, + strlen(md_cases[i].text), &md); + + if (!assert_inteq(!err, md_cases[i].valid)) + pr_warn("\"%s\" unexpectedly %s\n", md_cases[i].text, + err ? "rejected" : "accepted"); + + if (!err) + lvm_md_free(md); + } +} +bselftest(parser, test_lvm_md_parse); + +/* The accessors on top of the tokenizer. */ +static void test_lvm_md_access(void) +{ + static const char text[] = + "vg {\n" + " id = \"abc\"\n" + " seqno = 42\n" + " nested {\n" + " deep = 7\n" + " }\n" + " arr = [\"a\", 2]\n" + " dangling\n" + "}\n"; + const lvm_tok_t *vg, *key, *tok; + struct lvm_md *md; + char *str; + u64 v; + + if (!assert_cond(!lvm_md_parse_alloc(text, strlen(text), &md))) + return; + + vg = lvm_md_vgsect(md, &key); + if (!assert_cond(vg)) + goto out; + + str = lvm_md_tok_xstrdup(md, key); + assert_streq(str, "vg"); + free(str); + + str = lvm_md_strdup(md, vg, "id"); + if (assert_cond(str)) { + assert_streq(str, "abc"); + free(str); + } + + assert_cond((!lvm_md_u64(md, vg, "seqno", &v) && v == 42)); + + /* A missing key, and a key without a value, both come back + * empty rather than as something one past the end. + */ + assert_cond(!lvm_md_find(md, vg, "nosuchkey")); + assert_cond(!lvm_md_find(md, vg, "dangling")); + assert_cond(lvm_md_u64(md, vg, "nosuchkey", &v)); + + tok = lvm_md_find(md, vg, "nested"); + if (assert_cond(tok)) { + assert_inteq(tok->type, LVM_TOK_SECTION); + assert_cond((!lvm_md_u64(md, tok, "deep", &v) && v == 7)); + } + + tok = lvm_md_find(md, vg, "arr"); + if (assert_cond(tok)) { + assert_inteq(tok->type, LVM_TOK_ARRAY); + assert_inteq(tok->size, 2); + + key = lvm_md_first(md, tok); + if (assert_cond(key)) { + str = lvm_md_tok_xstrdup(md, key); + assert_streq(str, "a"); + free(str); + + key = lvm_md_next(md, tok, key); + assert_cond((key && + !lvm_md_tok_u64(md, key, &v) && v == 2)); + } + } + +out: + lvm_md_free(md); +} +bselftest(parser, test_lvm_md_access); -- 2.43.0
