| // 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; |
| } |