| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Copyright (C) 2026 Meta. All rights reserved. | |
| */ | |
| #include <linux/sizes.h> | |
| #include "btrfs-tests.h" | |
| #include "../volumes.h" | |
| #include "../disk-io.h" | |
| #include "../extent-io-tree.h" | |
| /* | |
| * Tests for chunk allocator pending extent internals. | |
| * These two functions form the core of searching the chunk allocation pending | |
| * extent bitmap and have relatively easily definable semantics, so unit | |
| * testing them can help ensure the correctness of chunk allocation. | |
| */ | |
| /* | |
| * Describes the inputs to the system and expected results | |
| * when testing btrfs_find_hole_in_pending_extents(). | |
| */ | |
| struct pending_extent_test_case { | |
| const char *name; | |
| /* Input range to search. */ | |
| u64 hole_start; | |
| u64 hole_len; | |
| /* The size of hole we are searching for. */ | |
| u64 min_hole_size; | |
| /* | |
| * Pending extents to set up (up to 2 for up to 3 holes) | |
| * If len == 0, then it is skipped. | |
| */ | |
| struct { | |
| u64 start; | |
| u64 len; | |
| } pending_extents[2]; | |
| /* Expected outputs. */ | |
| bool expected_found; | |
| u64 expected_start; | |
| u64 expected_len; | |
| }; | |
| static const struct pending_extent_test_case find_hole_tests[] = { | |
| { | |
| .name = "no pending extents", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { }, | |
| .expected_found = true, | |
| .expected_start = 0, | |
| .expected_len = 10ULL * SZ_1G, | |
| }, | |
| { | |
| .name = "pending extent at start of range", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { | |
| { .start = 0, .len = SZ_1G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = SZ_1G, | |
| .expected_len = 9ULL * SZ_1G, | |
| }, | |
| { | |
| .name = "pending extent overlapping start of range", | |
| .hole_start = SZ_1G, | |
| .hole_len = 9ULL * SZ_1G, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { | |
| { .start = 0, .len = SZ_2G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = SZ_2G, | |
| .expected_len = 8ULL * SZ_1G, | |
| }, | |
| { | |
| .name = "two holes; first hole is exactly big enough", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { | |
| { .start = SZ_1G, .len = SZ_1G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = 0, | |
| .expected_len = SZ_1G, | |
| }, | |
| { | |
| .name = "two holes; first hole is big enough", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { | |
| { .start = SZ_2G, .len = SZ_1G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = 0, | |
| .expected_len = SZ_2G, | |
| }, | |
| { | |
| .name = "two holes; second hole is big enough", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_2G, | |
| .pending_extents = { | |
| { .start = SZ_1G, .len = SZ_1G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = SZ_2G, | |
| .expected_len = 8ULL * SZ_1G, | |
| }, | |
| { | |
| .name = "three holes; first hole big enough", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_2G, | |
| .pending_extents = { | |
| { .start = SZ_2G, .len = SZ_1G }, | |
| { .start = 4ULL * SZ_1G, .len = SZ_1G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = 0, | |
| .expected_len = SZ_2G, | |
| }, | |
| { | |
| .name = "three holes; second hole big enough", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_2G, | |
| .pending_extents = { | |
| { .start = SZ_1G, .len = SZ_1G }, | |
| { .start = 5ULL * SZ_1G, .len = SZ_1G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = SZ_2G, | |
| .expected_len = 3ULL * SZ_1G, | |
| }, | |
| { | |
| .name = "three holes; third hole big enough", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_2G, | |
| .pending_extents = { | |
| { .start = SZ_1G, .len = SZ_1G }, | |
| { .start = 3ULL * SZ_1G, .len = 5ULL * SZ_1G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = 8ULL * SZ_1G, | |
| .expected_len = SZ_2G, | |
| }, | |
| { | |
| .name = "three holes; all holes too small", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_2G, | |
| .pending_extents = { | |
| { .start = SZ_1G, .len = SZ_1G }, | |
| { .start = 3ULL * SZ_1G, .len = 6ULL * SZ_1G }, | |
| }, | |
| .expected_found = false, | |
| .expected_start = 0, | |
| .expected_len = SZ_1G, | |
| }, | |
| { | |
| .name = "three holes; all holes too small; first biggest", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = 3ULL * SZ_1G, | |
| .pending_extents = { | |
| { .start = SZ_2G, .len = SZ_1G }, | |
| { .start = 4ULL * SZ_1G, .len = 5ULL * SZ_1G }, | |
| }, | |
| .expected_found = false, | |
| .expected_start = 0, | |
| .expected_len = SZ_2G, | |
| }, | |
| { | |
| .name = "three holes; all holes too small; second biggest", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = 3ULL * SZ_1G, | |
| .pending_extents = { | |
| { .start = SZ_1G, .len = SZ_1G }, | |
| { .start = 4ULL * SZ_1G, .len = 5ULL * SZ_1G }, | |
| }, | |
| .expected_found = false, | |
| .expected_start = SZ_2G, | |
| .expected_len = SZ_2G, | |
| }, | |
| { | |
| .name = "three holes; all holes too small; third biggest", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = 3ULL * SZ_1G, | |
| .pending_extents = { | |
| { .start = SZ_1G, .len = SZ_1G }, | |
| { .start = 3ULL * SZ_1G, .len = 5ULL * SZ_1G }, | |
| }, | |
| .expected_found = false, | |
| .expected_start = 8ULL * SZ_1G, | |
| .expected_len = SZ_2G, | |
| }, | |
| { | |
| .name = "hole entirely allocated by pending", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { | |
| { .start = 0, .len = 10ULL * SZ_1G }, | |
| }, | |
| .expected_found = false, | |
| .expected_start = 10ULL * SZ_1G, | |
| .expected_len = 0, | |
| }, | |
| { | |
| .name = "pending extent at end of range", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { | |
| { .start = 9ULL * SZ_1G, .len = SZ_2G }, | |
| }, | |
| .expected_found = true, | |
| .expected_start = 0, | |
| .expected_len = 9ULL * SZ_1G, | |
| }, | |
| { | |
| .name = "zero length input", | |
| .hole_start = SZ_1G, | |
| .hole_len = 0, | |
| .min_hole_size = SZ_1G, | |
| .pending_extents = { }, | |
| .expected_found = false, | |
| .expected_start = SZ_1G, | |
| .expected_len = 0, | |
| }, | |
| }; | |
| static int test_find_hole_in_pending(u32 sectorsize, u32 nodesize) | |
| { | |
| struct btrfs_fs_info *fs_info; | |
| struct btrfs_device *device; | |
| int ret = 0; | |
| test_msg("running find_hole_in_pending_extents tests"); | |
| fs_info = btrfs_alloc_dummy_fs_info(nodesize, sectorsize); | |
| if (!fs_info) { | |
| test_std_err(TEST_ALLOC_FS_INFO); | |
| return -ENOMEM; | |
| } | |
| device = btrfs_alloc_dummy_device(fs_info); | |
| if (IS_ERR(device)) { | |
| test_err("failed to allocate dummy device"); | |
| ret = PTR_ERR(device); | |
| goto out_free_fs_info; | |
| } | |
| device->fs_info = fs_info; | |
| for (int i = 0; i < ARRAY_SIZE(find_hole_tests); i++) { | |
| const struct pending_extent_test_case *test_case = &find_hole_tests[i]; | |
| u64 hole_start = test_case->hole_start; | |
| u64 hole_len = test_case->hole_len; | |
| bool found; | |
| for (int j = 0; j < ARRAY_SIZE(test_case->pending_extents); j++) { | |
| u64 start = test_case->pending_extents[j].start; | |
| u64 len = test_case->pending_extents[j].len; | |
| if (!len) | |
| continue; | |
| btrfs_set_extent_bit(&device->alloc_state, | |
| start, start + len - 1, | |
| CHUNK_ALLOCATED, NULL); | |
| } | |
| mutex_lock(&fs_info->chunk_mutex); | |
| found = btrfs_find_hole_in_pending_extents(device, &hole_start, &hole_len, | |
| test_case->min_hole_size); | |
| mutex_unlock(&fs_info->chunk_mutex); | |
| if (found != test_case->expected_found) { | |
| test_err("%s: expected found=%d, got found=%d", | |
| test_case->name, test_case->expected_found, found); | |
| ret = -EINVAL; | |
| goto out_clear_pending_extents; | |
| } | |
| if (hole_start != test_case->expected_start || | |
| hole_len != test_case->expected_len) { | |
| test_err("%s: expected [%llu, %llu), got [%llu, %llu)", | |
| test_case->name, test_case->expected_start, | |
| test_case->expected_start + | |
| test_case->expected_len, | |
| hole_start, hole_start + hole_len); | |
| ret = -EINVAL; | |
| goto out_clear_pending_extents; | |
| } | |
| out_clear_pending_extents: | |
| btrfs_clear_extent_bit(&device->alloc_state, 0, (u64)-1, | |
| CHUNK_ALLOCATED, NULL); | |
| if (ret) | |
| break; | |
| } | |
| out_free_fs_info: | |
| btrfs_free_dummy_fs_info(fs_info); | |
| return ret; | |
| } | |
| /* | |
| * Describes the inputs to the system and expected results | |
| * when testing btrfs_first_pending_extent(). | |
| */ | |
| struct first_pending_test_case { | |
| const char *name; | |
| /* The range to look for a pending extent in. */ | |
| u64 hole_start; | |
| u64 hole_len; | |
| /* The pending extent to look for. */ | |
| struct { | |
| u64 start; | |
| u64 len; | |
| } pending_extent; | |
| /* Expected outputs. */ | |
| bool expected_found; | |
| u64 expected_pending_start; | |
| u64 expected_pending_end; | |
| }; | |
| static const struct first_pending_test_case first_pending_tests[] = { | |
| { | |
| .name = "no pending extent", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .pending_extent = { 0, 0 }, | |
| .expected_found = false, | |
| }, | |
| { | |
| .name = "pending extent at search start", | |
| .hole_start = SZ_1G, | |
| .hole_len = 9ULL * SZ_1G, | |
| .pending_extent = { SZ_1G, SZ_1G }, | |
| .expected_found = true, | |
| .expected_pending_start = SZ_1G, | |
| .expected_pending_end = SZ_2G - 1, | |
| }, | |
| { | |
| .name = "pending extent overlapping search start", | |
| .hole_start = SZ_1G, | |
| .hole_len = 9ULL * SZ_1G, | |
| .pending_extent = { 0, SZ_2G }, | |
| .expected_found = true, | |
| .expected_pending_start = 0, | |
| .expected_pending_end = SZ_2G - 1, | |
| }, | |
| { | |
| .name = "pending extent inside search range", | |
| .hole_start = 0, | |
| .hole_len = 10ULL * SZ_1G, | |
| .pending_extent = { SZ_2G, SZ_1G }, | |
| .expected_found = true, | |
| .expected_pending_start = SZ_2G, | |
| .expected_pending_end = 3ULL * SZ_1G - 1, | |
| }, | |
| { | |
| .name = "pending extent outside search range", | |
| .hole_start = 0, | |
| .hole_len = SZ_1G, | |
| .pending_extent = { SZ_2G, SZ_1G }, | |
| .expected_found = false, | |
| }, | |
| { | |
| .name = "pending extent overlapping end of search range", | |
| .hole_start = 0, | |
| .hole_len = SZ_2G, | |
| .pending_extent = { SZ_1G, SZ_2G }, | |
| .expected_found = true, | |
| .expected_pending_start = SZ_1G, | |
| .expected_pending_end = 3ULL * SZ_1G - 1, | |
| }, | |
| }; | |
| static int test_first_pending_extent(u32 sectorsize, u32 nodesize) | |
| { | |
| struct btrfs_fs_info *fs_info; | |
| struct btrfs_device *device; | |
| int ret = 0; | |
| test_msg("running first_pending_extent tests"); | |
| fs_info = btrfs_alloc_dummy_fs_info(nodesize, sectorsize); | |
| if (!fs_info) { | |
| test_std_err(TEST_ALLOC_FS_INFO); | |
| return -ENOMEM; | |
| } | |
| device = btrfs_alloc_dummy_device(fs_info); | |
| if (IS_ERR(device)) { | |
| test_err("failed to allocate dummy device"); | |
| ret = PTR_ERR(device); | |
| goto out_free_fs_info; | |
| } | |
| device->fs_info = fs_info; | |
| for (int i = 0; i < ARRAY_SIZE(first_pending_tests); i++) { | |
| const struct first_pending_test_case *test_case = &first_pending_tests[i]; | |
| u64 start = test_case->pending_extent.start; | |
| u64 len = test_case->pending_extent.len; | |
| u64 pending_start, pending_end; | |
| bool found; | |
| if (len) { | |
| btrfs_set_extent_bit(&device->alloc_state, | |
| start, start + len - 1, | |
| CHUNK_ALLOCATED, NULL); | |
| } | |
| mutex_lock(&fs_info->chunk_mutex); | |
| found = btrfs_first_pending_extent(device, test_case->hole_start, | |
| test_case->hole_len, | |
| &pending_start, &pending_end); | |
| mutex_unlock(&fs_info->chunk_mutex); | |
| if (found != test_case->expected_found) { | |
| test_err("%s: expected found=%d, got found=%d", | |
| test_case->name, test_case->expected_found, found); | |
| ret = -EINVAL; | |
| goto out_clear_pending_extents; | |
| } | |
| if (!found) | |
| goto out_clear_pending_extents; | |
| if (pending_start != test_case->expected_pending_start || | |
| pending_end != test_case->expected_pending_end) { | |
| test_err("%s: expected pending [%llu, %llu], got [%llu, %llu]", | |
| test_case->name, | |
| test_case->expected_pending_start, | |
| test_case->expected_pending_end, | |
| pending_start, pending_end); | |
| ret = -EINVAL; | |
| goto out_clear_pending_extents; | |
| } | |
| out_clear_pending_extents: | |
| btrfs_clear_extent_bit(&device->alloc_state, 0, (u64)-1, | |
| CHUNK_ALLOCATED, NULL); | |
| if (ret) | |
| break; | |
| } | |
| out_free_fs_info: | |
| btrfs_free_dummy_fs_info(fs_info); | |
| return ret; | |
| } | |
| int btrfs_test_chunk_allocation(u32 sectorsize, u32 nodesize) | |
| { | |
| int ret; | |
| test_msg("running chunk allocation tests"); | |
| ret = test_first_pending_extent(sectorsize, nodesize); | |
| if (ret) | |
| return ret; | |
| ret = test_find_hole_in_pending(sectorsize, nodesize); | |
| if (ret) | |
| return ret; | |
| return 0; | |
| } |