nexfs/tests/test_nexfs.zig

681 lines
24 KiB
Zig

const std = @import("std");
const nexfs = @import("nexfs");
// ============================================================================
// Phase 1 Tests
// ============================================================================
test "version check" {
try std.testing.expectEqualStrings("0.1.0", nexfs.version);
}
test "config validation - valid config" {
var read_buf: [4096]u8 = undefined;
var write_buf: [4096]u8 = undefined;
var workspace: [256]u8 = undefined;
const config = nexfs.Config{
.flash = undefined,
.device_size = 1024 * 1024,
.block_size = 4096,
.block_count = 256,
.page_size = 256,
.checksum_algo = .CRC32C,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
.workspace = &workspace,
.time_source = null,
.verbose = false,
};
try nexfs.validateConfigRuntime(&config);
}
test "config validation - block size not power of 2" {
var read_buf: [512]u8 = undefined;
var write_buf: [512]u8 = undefined;
var workspace: [64]u8 = undefined;
const config = nexfs.Config{
.flash = undefined,
.device_size = 2048,
.block_size = 768,
.block_count = 3,
.page_size = 64,
.checksum_algo = .CRC32C,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
.workspace = &workspace,
.time_source = null,
.verbose = false,
};
const err = nexfs.validateConfigRuntime(&config);
try std.testing.expectError(nexfs.NexFSError.InvalidConfig, err);
}
test "config validation - 2048 byte blocks valid" {
var read_buf: [2048]u8 = undefined;
var write_buf: [2048]u8 = undefined;
var workspace: [256]u8 = undefined;
const config = nexfs.Config{
.flash = undefined,
.device_size = 2048,
.block_size = 2048,
.block_count = 1,
.page_size = 256,
.checksum_algo = .CRC32C,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
.workspace = &workspace,
.time_source = null,
.verbose = false,
};
try nexfs.validateConfigRuntime(&config);
}
test "error types exist" {
const err1 = nexfs.NexFSError.InvalidConfig;
const err2 = nexfs.NexFSError.NotMounted;
const err3 = nexfs.NexFSError.AlreadyMounted;
try std.testing.expect(err1 != err2);
try std.testing.expect(err2 != err3);
}
test "types - block addr is u32" {
const info = @typeInfo(nexfs.BlockAddr);
try std.testing.expectEqual(32, info.int.bits);
}
test "types - inode id is u32" {
const info = @typeInfo(nexfs.InodeId);
try std.testing.expectEqual(32, info.int.bits);
}
test "types - file handle fields" {
const handle = nexfs.FileHandle{
.inode_id = 42,
.position = 12345,
.open_flags = 0,
};
try std.testing.expectEqual(42, handle.inode_id);
try std.testing.expectEqual(12345, handle.position);
}
test "checksum - crc16 non-zero" {
const data = "123456789";
const crc = nexfs.crc16(data);
try std.testing.expect(crc != 0);
}
test "checksum - crc32c non-zero" {
const data = "123456789";
const crc = nexfs.crc32c(data);
try std.testing.expect(crc != 0);
}
// ============================================================================
// Phase 2 Tests - On-Disk Format
// ============================================================================
test "superblock size check" {
// Superblock should be 128 bytes
try std.testing.expectEqual(128, @sizeOf(nexfs.Superblock));
}
test "superblock magic number" {
try std.testing.expectEqual(0x4E455846, nexfs.NEXFS_MAGIC);
}
test "superblock validation - valid" {
var sb: nexfs.Superblock = .{
.magic = nexfs.NEXFS_MAGIC,
.version = nexfs.NEXFS_VERSION,
.generation = 0,
.block_size = 4096,
.page_size = 256,
.block_count = 256,
.data_block_count = 250,
.root_inode = 1,
.inode_table_start = 10,
.inode_table_blocks = 4,
.bam_start = 2,
.bam_blocks = 8,
.create_time = 0,
.last_mount_time = 0,
.mount_count = 0,
.flags = 0,
.reserved = 0,
._padding = .{0} ** 44,
.checksum = 0,
};
sb.computeChecksum();
try sb.validate();
}
test "superblock validation - bad magic" {
var sb: nexfs.Superblock = .{
.magic = 0x12345678, // Wrong magic
.version = nexfs.NEXFS_VERSION,
.generation = 0,
.block_size = 4096,
.page_size = 256,
.block_count = 256,
.data_block_count = 250,
.root_inode = 1,
.inode_table_start = 10,
.inode_table_blocks = 4,
.bam_start = 2,
.bam_blocks = 8,
.create_time = 0,
.last_mount_time = 0,
.mount_count = 0,
.flags = 0,
.reserved = 0,
._padding = .{0} ** 44,
.checksum = 0,
};
const err = sb.validate();
try std.testing.expectError(nexfs.NexFSError.Corruption, err);
}
test "inode size check" {
// Inode is 120 bytes (with padding for alignment)
try std.testing.expectEqual(120, @sizeOf(nexfs.Inode));
}
test "inode validation" {
var inode: nexfs.Inode = .{
.id = 1,
.file_type = .Regular,
.mode = 0o644,
.uid = 0,
.gid = 0,
.size = 0,
.block_count = 0,
.link_count = 1,
.flags = 0,
.atime = 0,
.mtime = 0,
.ctime = 0,
.inline_extent = .{ .logical_block = 0, .physical_block = 0, .length = 0, .reserved = 0 },
.extent_table = 0,
.extent_count = 0,
.reserved1 = 0,
.reserved2 = 0,
._padding = .{0} ** 24,
.checksum = 0,
};
inode.computeChecksum();
try inode.validate();
}
test "inode isValid check" {
var inode: nexfs.Inode = .{
.id = 1,
.file_type = .Regular,
.mode = 0o644,
.uid = 0,
.gid = 0,
.size = 0,
.block_count = 0,
.link_count = 1,
.flags = 0,
.atime = 0,
.mtime = 0,
.ctime = 0,
.inline_extent = .{ .logical_block = 0, .physical_block = 0, .length = 0, .reserved = 0 },
.extent_table = 0,
.extent_count = 0,
.reserved1 = 0,
.reserved2 = 0,
._padding = .{0} ** 24,
.checksum = 0,
};
try std.testing.expect(inode.isValid());
// Invalid inode
var invalid_inode: nexfs.Inode = .{
.id = 0,
.file_type = .None,
.mode = 0,
.uid = 0,
.gid = 0,
.size = 0,
.block_count = 0,
.link_count = 0,
.flags = 0,
.atime = 0,
.mtime = 0,
.ctime = 0,
.inline_extent = .{ .logical_block = 0, .physical_block = 0, .length = 0, .reserved = 0 },
.extent_table = 0,
.extent_count = 0,
.reserved1 = 0,
.reserved2 = 0,
._padding = .{0} ** 24,
.checksum = 0,
};
try std.testing.expect(!invalid_inode.isValid());
}
test "extent size check" {
// Extent should be 16 bytes
try std.testing.expectEqual(16, @sizeOf(nexfs.Extent));
}
test "dir entry size check" {
// DirEntry header is 20 bytes (without name)
try std.testing.expectEqual(20, @sizeOf(nexfs.DirEntry));
}
test "dir entry size calculation" {
// Entry with name "test" (4 chars) should be rounded up to 8-byte boundary
// DirEntry header is 20 bytes, plus 4 chars + 1 null = 25, rounded up to 32
const size1 = nexfs.dirEntrySize(4);
try std.testing.expectEqual(32, size1);
}
test "bam entry size check" {
// BAM Entry should be small
try std.testing.expect(@sizeOf(nexfs.BamEntry) <= 16);
}
test "file type enum values" {
try std.testing.expectEqual(0, @intFromEnum(nexfs.FileType.None));
try std.testing.expectEqual(1, @intFromEnum(nexfs.FileType.Regular));
try std.testing.expectEqual(2, @intFromEnum(nexfs.FileType.Directory));
}
test "max name length constant" {
try std.testing.expectEqual(255, nexfs.NEXFS_MAX_NAME_LEN);
}
test "superblock version constant" {
try std.testing.expectEqual(1, nexfs.NEXFS_VERSION);
}
// ============================================================================
// Phase 2 Tests - format() and round-trip verification
// ============================================================================
/// Mock flash for format and mount tests.
/// Aligned to 8 bytes so we can safely cast superblock/inode pointers.
const MockFlash = struct {
data: [512 * 32]u8 align(8) = [_]u8{0xFF} ** (512 * 32),
fn readFn(ctx: *anyopaque, addr: u64, buffer: []u8) nexfs.NexFSError!usize {
const flash: *MockFlash = @ptrCast(@alignCast(ctx));
const start: usize = @intCast(addr);
if (start + buffer.len > flash.data.len) return nexfs.NexFSError.ReadFailed;
@memcpy(buffer[0..buffer.len], flash.data[start..][0..buffer.len]);
return buffer.len;
}
fn writeFn(ctx: *anyopaque, addr: u64, buffer: []const u8) nexfs.NexFSError!void {
const flash: *MockFlash = @ptrCast(@alignCast(ctx));
const start: usize = @intCast(addr);
if (start + buffer.len > flash.data.len) return nexfs.NexFSError.WriteFailed;
for (buffer, 0..) |byte, i| {
flash.data[start + i] = byte;
}
}
fn eraseFn(ctx: *anyopaque, block_addr: nexfs.BlockAddr) nexfs.NexFSError!void {
_ = block_addr;
_ = ctx;
}
fn syncFn(ctx: *anyopaque) nexfs.NexFSError!void {
_ = ctx;
}
fn interface(self: *MockFlash) nexfs.FlashInterface {
return .{
.ctx = @ptrCast(self),
.read = &readFn,
.write = &writeFn,
.erase = &eraseFn,
.sync = &syncFn,
};
}
};
test "format - writes valid superblock to block 0" {
var mock = MockFlash{};
var read_buf: [512]u8 = undefined;
var write_buf: [512]u8 = undefined;
var workspace: [64]u8 = undefined;
const config = nexfs.Config{
.flash = mock.interface(),
.device_size = 512 * 32,
.block_size = 512,
.block_count = 32,
.page_size = 64,
.checksum_algo = .CRC32C,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
.workspace = &workspace,
.time_source = null,
.verbose = false,
};
var fmt_buf: [512]u8 = undefined;
try nexfs.format(&config.flash, &config, &fmt_buf);
// Read back superblock from block 0
const sb: *const nexfs.Superblock = @ptrCast(@alignCast(&mock.data[0]));
try std.testing.expectEqual(nexfs.NEXFS_MAGIC, sb.magic);
try std.testing.expectEqual(nexfs.NEXFS_VERSION, sb.version);
try std.testing.expectEqual(@as(nexfs.BlockSize, 512), sb.block_size);
try std.testing.expectEqual(@as(u32, 32), sb.block_count);
try std.testing.expectEqual(@as(nexfs.InodeId, 1), sb.root_inode);
// Validate CRC
try sb.validate();
}
test "format - writes backup superblock to block 1" {
var mock = MockFlash{};
var read_buf: [512]u8 = undefined;
var write_buf: [512]u8 = undefined;
var workspace: [64]u8 = undefined;
const config = nexfs.Config{
.flash = mock.interface(),
.device_size = 512 * 32,
.block_size = 512,
.block_count = 32,
.page_size = 64,
.checksum_algo = .CRC32C,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
.workspace = &workspace,
.time_source = null,
.verbose = false,
};
var fmt_buf: [512]u8 = undefined;
try nexfs.format(&config.flash, &config, &fmt_buf);
// Backup superblock at block 1 (offset 512)
const backup: *const nexfs.Superblock = @ptrCast(@alignCast(&mock.data[512]));
try backup.validate();
// Primary and backup should be identical
const primary = mock.data[0..512];
const secondary = mock.data[512..1024];
try std.testing.expectEqualSlices(u8, primary, secondary);
}
test "format - root inode written to inode table" {
var mock = MockFlash{};
var read_buf: [512]u8 = undefined;
var write_buf: [512]u8 = undefined;
var workspace: [64]u8 = undefined;
const config = nexfs.Config{
.flash = mock.interface(),
.device_size = 512 * 32,
.block_size = 512,
.block_count = 32,
.page_size = 64,
.checksum_algo = .CRC32C,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
.workspace = &workspace,
.time_source = null,
.verbose = false,
};
var fmt_buf: [512]u8 = undefined;
try nexfs.format(&config.flash, &config, &fmt_buf);
// Find inode table start from superblock
const sb: *const nexfs.Superblock = @ptrCast(@alignCast(&mock.data[0]));
const inode_offset = @as(usize, sb.inode_table_start) * 512;
const root_inode: *const nexfs.Inode = @ptrCast(@alignCast(&mock.data[inode_offset]));
try std.testing.expectEqual(@as(nexfs.InodeId, 1), root_inode.id);
try std.testing.expectEqual(nexfs.FileType.Directory, root_inode.file_type);
try std.testing.expectEqual(@as(u16, 0o755), root_inode.mode);
try std.testing.expectEqual(@as(u16, 2), root_inode.link_count);
try root_inode.validate();
}
test "format - buffer too small" {
var mock = MockFlash{};
var read_buf: [512]u8 = undefined;
var write_buf: [512]u8 = undefined;
var workspace: [64]u8 = undefined;
const config = nexfs.Config{
.flash = mock.interface(),
.device_size = 512 * 32,
.block_size = 512,
.block_count = 32,
.page_size = 64,
.checksum_algo = .CRC32C,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
.workspace = &workspace,
.time_source = null,
.verbose = false,
};
var tiny_buf: [64]u8 = undefined;
const err = nexfs.format(&config.flash, &config, &tiny_buf);
try std.testing.expectError(nexfs.NexFSError.BufferTooSmall, err);
}
// ============================================================================
// Phase 3 Tests - Block Allocator and Inode Operations
// ============================================================================
test "block allocator - init" {
const allocator = nexfs.BlockAllocator.init(10, 100, 1);
try std.testing.expectEqual(@as(nexfs.BlockAddr, 10), allocator.data_start);
try std.testing.expectEqual(@as(u32, 100), allocator.data_count);
try std.testing.expectEqual(@as(nexfs.BlockAddr, 10), allocator.next_block);
try std.testing.expectEqual(@as(u32, 1), allocator.generation);
}
test "block allocator - alloc finds free block" {
var bam_entries: [4]nexfs.BamEntry = .{
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
};
var allocator = nexfs.BlockAllocator.init(100, 4, 1);
const block = try allocator.alloc(&bam_entries);
try std.testing.expectEqual(@as(nexfs.BlockAddr, 101), block);
}
test "block allocator - alloc returns NoSpace when full" {
var bam_entries: [4]nexfs.BamEntry = .{
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
};
var allocator = nexfs.BlockAllocator.init(100, 4, 1);
const err = allocator.alloc(&bam_entries);
try std.testing.expectError(nexfs.NexFSError.NoSpace, err);
}
test "block allocator - markAllocated" {
var bam_entries: [4]nexfs.BamEntry = .{
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
};
var allocator = nexfs.BlockAllocator.init(100, 4, 5);
try allocator.markAllocated(&bam_entries, 102);
try std.testing.expectEqual(@as(u1, 1), bam_entries[2].flags.allocated);
try std.testing.expectEqual(@as(u32, 5), bam_entries[2].generation);
}
test "block allocator - free marks for erasure" {
var bam_entries: [4]nexfs.BamEntry = .{
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
};
var allocator = nexfs.BlockAllocator.init(100, 4, 1);
try allocator.free(&bam_entries, 101);
try std.testing.expectEqual(@as(u1, 0), bam_entries[1].flags.allocated);
try std.testing.expectEqual(@as(u1, 1), bam_entries[1].flags.needs_erase);
try std.testing.expectEqual(@as(u32, 1), bam_entries[1].erase_count);
}
test "block allocator - isFree" {
var bam_entries: [4]nexfs.BamEntry = .{
.{ .flags = .{ .allocated = 1, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 1, .reserved = 0, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
.{ .flags = .{ .allocated = 0, .bad = 0, .reserved = 1, .needs_erase = 0, ._reserved = 0 }, .erase_count = 0, .generation = 0, .reserved = 0, ._reserved1 = 0, ._reserved2 = 0 },
};
var allocator = nexfs.BlockAllocator.init(100, 4, 1);
try std.testing.expect(!allocator.isFree(&bam_entries, 100)); // allocated
try std.testing.expect(allocator.isFree(&bam_entries, 101)); // free
try std.testing.expect(!allocator.isFree(&bam_entries, 102)); // bad
try std.testing.expect(!allocator.isFree(&bam_entries, 103)); // reserved
}
test "inode table - alloc creates valid inode" {
var mock = MockFlash{};
// Pre-erase the flash (set to zeros for empty inodes)
@memset(&mock.data, 0);
// Use aligned buffer for inode operations
var read_buf: [512]u8 align(8) = undefined;
const flash = mock.interface();
var inode: nexfs.Inode = undefined;
const id = try nexfs.InodeTable.alloc(
&flash,
10, // inode_table_start
4, // inode_table_blocks
512, // block_size
.Regular,
0o644,
&inode,
&read_buf,
);
try std.testing.expectEqual(@as(nexfs.InodeId, 1), id);
try std.testing.expectEqual(nexfs.FileType.Regular, inode.file_type);
try std.testing.expectEqual(@as(u16, 0o644), inode.mode);
try std.testing.expect(inode.isValid());
}
test "inode table - write and load roundtrip" {
var mock = MockFlash{};
@memset(&mock.data, 0);
var read_buf: [512]u8 align(8) = undefined;
var write_buf: [512]u8 align(8) = undefined;
const flash = mock.interface();
// Create and write an inode
var inode: nexfs.Inode = .{
.id = 5,
.file_type = .Directory,
.mode = 0o755,
.uid = 1000,
.gid = 1000,
.size = 4096,
.block_count = 8,
.link_count = 2,
.flags = 0,
.atime = 1234567890,
.mtime = 1234567890,
.ctime = 1234567890,
.inline_extent = .{ .logical_block = 0, .physical_block = 100, .length = 8, .reserved = 0 },
.extent_table = 0,
.extent_count = 0,
.reserved1 = 0,
.reserved2 = 0,
._padding = .{0} ** 24,
.checksum = 0,
};
inode.computeChecksum();
try nexfs.InodeTable.write(&flash, 10, 512, &inode, &write_buf);
// Load it back
var loaded: nexfs.Inode = undefined;
try nexfs.InodeTable.load(&flash, 10, 512, 5, &loaded, &read_buf);
try std.testing.expectEqual(inode.id, loaded.id);
try std.testing.expectEqual(inode.file_type, loaded.file_type);
try std.testing.expectEqual(inode.mode, loaded.mode);
try std.testing.expectEqual(inode.uid, loaded.uid);
try std.testing.expectEqual(inode.size, loaded.size);
}
test "inode table - delete clears inode" {
var mock = MockFlash{};
@memset(&mock.data, 0);
var read_buf: [512]u8 align(8) = undefined;
var write_buf: [512]u8 align(8) = undefined;
const flash = mock.interface();
// Create and write an inode
var inode: nexfs.Inode = .{
.id = 3,
.file_type = .Regular,
.mode = 0o644,
.uid = 0,
.gid = 0,
.size = 0,
.block_count = 0,
.link_count = 1,
.flags = 0,
.atime = 0,
.mtime = 0,
.ctime = 0,
.inline_extent = .{ .logical_block = 0, .physical_block = 0, .length = 0, .reserved = 0 },
.extent_table = 0,
.extent_count = 0,
.reserved1 = 0,
.reserved2 = 0,
._padding = .{0} ** 24,
.checksum = 0,
};
inode.computeChecksum();
try nexfs.InodeTable.write(&flash, 10, 512, &inode, &write_buf);
// Verify it exists first
var loaded: nexfs.Inode = undefined;
try nexfs.InodeTable.load(&flash, 10, 512, 3, &loaded, &read_buf);
try std.testing.expect(loaded.isValid());
// Delete it
try nexfs.InodeTable.delete(&flash, 10, 512, 3, &write_buf);
// Verify it's cleared by reading raw bytes (id should be 0)
const addr = @as(u64, 10) * 512 + (3 - 1) * @sizeOf(nexfs.Inode);
_ = try flash.read(flash.ctx, addr, read_buf[0..@sizeOf(nexfs.Inode)]);
const cleared: *const nexfs.Inode = @ptrCast(@alignCast(&read_buf));
try std.testing.expectEqual(@as(nexfs.InodeId, 0), cleared.id);
try std.testing.expect(!cleared.isValid());
}