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()); }