test: extend coverage of sfsTest

This commit is contained in:
dualfroz committed 2026-09-11 19:27:37 +02:00
1 parent fe18aab0a9
commit ae47c8258f
1 file changed
+162
+162
View File
@@ -943,3 +943,165 @@ KTEST(sfsRemoveSymlinkDoesNotAffectTarget)
KT_EQ(t, memcmp(back, "untouched", 9), 0);
}
KTEST(sfsSymlinkPersistsAcrossUnmountRemount)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("real.txt");
KT_TRUE(t, f > 0);
KT_EQ(t, sfsWriteAt(f, 0, "on disk for real", 17), 17);
int32 link = sfsCreateSymlink("link.txt", "real.txt");
KT_TRUE(t, link > 0);
sfsUnmount();
KT_TRUE(t, sfsMount());
KT_EQ(t, sfsLookup("link.txt"), f);
KT_EQ(t, sfsLookupNoFollow("link.txt"), link);
KT_EQ(t, sfsTypeOf(link), sfsTypeSymlink);
char target[32];
memset(target, 0, sizeof(target));
KT_TRUE(t, sfsReadlink(link, target, sizeof(target)));
KT_STREQ(t, target, "real.txt");
}
KTEST(sfsFileGrowsThroughDoubleIndirectBlock)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("huge.bin");
KT_TRUE(t, f > 0);
/* sfsDirectBlocks (10) + sfsPtrsPerBlock (128) = 138 logical blocks are
reachable through direct + single indirect addressing; block 138
onward can only be reached through the double indirect block added
in this wave. Starting a few blocks before that boundary and writing
past it exercises the crossing and the double-indirect-only region
in one write (sfsWriteAt zero-fills 0..134 first, all still within
single-indirect range, before this pattern lands on 135..144). */
#define sfsDoubleSpanBlocks 10
#define sfsDoubleStartBlock 135
#define sfsDoubleSpan (sfsDoubleSpanBlocks * sfsBlockSize)
uint8* pattern = (uint8*)kmalloc(sfsDoubleSpan);
uint8* verify = (uint8*)kmalloc(sfsDoubleSpan);
KT_NOTNULL(t, pattern);
KT_NOTNULL(t, verify);
if (!pattern || !verify) {
kfree(pattern);
kfree(verify);
return;
}
for (int i = 0; i < sfsDoubleSpan; i++)
pattern[i] = (uint8)(i * 17 + 3);
uint64 offset = (uint64)sfsDoubleStartBlock * sfsBlockSize;
KT_EQ(t, sfsWriteAt(f, offset, pattern, sfsDoubleSpan), sfsDoubleSpan);
KT_EQ(t, sfsSizeOf(f), offset + sfsDoubleSpan);
memset(verify, 0, sfsDoubleSpan);
KT_EQ(t, sfsReadAt(f, offset, verify, sfsDoubleSpan), sfsDoubleSpan);
KT_EQ(t, memcmp(verify, pattern, sfsDoubleSpan), 0);
/* Patch a window straddling the single-indirect/double-indirect
boundary itself (logical block 137/138) and confirm only that slice
changed -- the same "boundary patch" shape as
sfsFileGrowsThroughIndirectBlock, one level deeper. */
uint8 patch[48];
memset(patch, 0xCC, sizeof(patch));
uint64 patchOffset = 138 * sfsBlockSize - 24;
KT_EQ(t, sfsWriteAt(f, patchOffset, patch, sizeof(patch)), (int64)sizeof(patch));
KT_EQ(t, sfsReadAt(f, offset, verify, sfsDoubleSpan), sfsDoubleSpan);
uint64 patchInSpan = patchOffset - offset;
KT_EQ(t, memcmp(verify + patchInSpan, patch, sizeof(patch)), 0);
KT_EQ(t, memcmp(verify, pattern, patchInSpan), 0);
kfree(pattern);
kfree(verify);
#undef sfsDoubleSpan
#undef sfsDoubleStartBlock
#undef sfsDoubleSpanBlocks
}
KTEST(sfsTruncateFreesDoubleIndirectBlocks)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("hugeshrink.bin");
KT_TRUE(t, f > 0);
/* Baseline is taken AFTER creating the file, not before: creating the
very first entry in a previously-empty root directory grows the
root's own data block permanently (it is a directory entry, not file
content, and stays even after truncating the FILE back to empty) --
that is correct, unrelated bookkeeping this test must not conflate
with whether truncating hugeshrink.bin's own content gives its
blocks back. */
uint64 baseline = sfsFreeDataBlocks();
KT_GT(t, baseline, 0);
#define sfsShrinkSpanBlocks 8
#define sfsShrinkStartBlock 136
#define sfsShrinkSpan (sfsShrinkSpanBlocks * sfsBlockSize)
uint8* chunk = (uint8*)kmalloc(sfsShrinkSpan);
KT_NOTNULL(t, chunk);
if (!chunk)
return;
memset(chunk, 0x91, sfsShrinkSpan);
uint64 offset = (uint64)sfsShrinkStartBlock * sfsBlockSize;
KT_EQ(t, sfsWriteAt(f, offset, chunk, sfsShrinkSpan), sfsShrinkSpan);
kfree(chunk);
#undef sfsShrinkSpan
#undef sfsShrinkStartBlock
#undef sfsShrinkSpanBlocks
/* This file now owns: 10 direct + 1 single-indirect holder + 128
single-indirect data blocks + 1 double-indirect holder + 1 inner
indirect holder + a handful of double-indirect data blocks. Every
one of those must come back. */
KT_LT(t, sfsFreeDataBlocks(), baseline);
KT_TRUE(t, sfsTruncate(f, 0));
KT_EQ(t, sfsSizeOf(f), 0);
KT_EQU(t, sfsFreeDataBlocks(), baseline);
/* And the inode itself must still be usable afterward, not left in some
half-truncated state. */
KT_EQ(t, sfsWriteAt(f, 0, "reused", 6), 6);
KT_EQ(t, sfsSizeOf(f), 6);
}
KTEST(sfsFileGrowsThroughTripleIndirectBlock)
{
if (!KT_TRUE(t, sfsFormat()))
return;
int32 f = sfsCreateFile("huger.bin");
KT_TRUE(t, f > 0);
/* sfsDirectBlocks (10) + sfsPtrsPerBlock (128) + sfsPtrsPerBlock^2
(16384) = 16522 logical blocks are reachable through direct + single
+ double indirect addressing; block 16522 onward can only be reached
through the triple indirect block added in this wave. Physically
reaching that boundary with real writes from offset 0 would mean
sfsWriteAt zero-filling ~8 MiB of real leading blocks first (its own
"no sparse holes" contract, see its doc comment) -- correct, but far
too slow for a headless boot-time gate. sfsTruncate's own grow path
is a genuine sparse hole instead (see sfsTruncateInode: growing only
ever rewrites the size field, no block is touched), the same
lseek-past-EOF-then-write shape a real filesystem's holes have -- so
pre-growing with it reaches the boundary for free, in O(1), then a
real write of a handful of blocks exercises the actual double/triple
crossing without paying for the untouched blocks in between. */
#define sfsTripleBoundaryBlock 16522
#define sfsTripleSpanBlocks 12
#define sfsTripleStartBlock (sfsTripleBoundaryBlock - 6)
#define sfsTripleSpan (sfsTripleSpanBlocks * sfsBlockSize)
uint64 startOffset = (uint64)sfsTripleStartBlock * sfsBlockSize;
KT_TRUE(t, sfsTruncate(f, startOffset));
KT_EQ(t, sfsSizeOf(f), startOffset);