471 lines
13 KiB
C
471 lines
13 KiB
C
#include "ktest.h"
|
|
#include "sfs.h"
|
|
#include "ata.h"
|
|
#include "string.h"
|
|
#include "heap.h"
|
|
#include "console.h"
|
|
#include "rtc.h"
|
|
#include "fs.h"
|
|
|
|
/*
|
|
* Every test starts with an explicit sfsFormat() so results do not depend on
|
|
* whatever build/disk.img happened to contain from a previous `make test`
|
|
* run (the disk image is not recreated between runs unless deleted). That
|
|
* keeps the gate deterministic: a stale leftover file on disk must never be
|
|
* the reason a check passes or fails.
|
|
*/
|
|
|
|
KTEST(sfsFormatAndMountAreReal)
|
|
{
|
|
if (!KT_TRUE(t, ataPresent()))
|
|
return;
|
|
KT_TRUE(t, sfsFormat());
|
|
KT_TRUE(t, sfsMounted());
|
|
KT_GT(t, sfsCapacityBytes(), 1024 * 1024);
|
|
|
|
int32 root = sfsLookup("");
|
|
KT_EQ(t, root, sfsRootInode);
|
|
KT_EQ(t, sfsTypeOf(root), sfsTypeDir);
|
|
KT_EQ(t, sfsSizeOf(root), 0);
|
|
}
|
|
|
|
KTEST(sfsCreateFileRoundTrip)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("hello.txt");
|
|
KT_TRUE(t, f > 0);
|
|
KT_EQ(t, sfsTypeOf(f), sfsTypeFile);
|
|
KT_EQ(t, sfsSizeOf(f), 0);
|
|
|
|
const char* msg = "hello, disk!";
|
|
uint64 len = strlen(msg);
|
|
KT_EQ(t, sfsWriteAt(f, 0, msg, len), (int64)len);
|
|
KT_EQ(t, sfsSizeOf(f), len);
|
|
|
|
char back[32];
|
|
memset(back, 0, sizeof(back));
|
|
KT_EQ(t, sfsReadAt(f, 0, back, len), (int64)len);
|
|
KT_EQ(t, memcmp(back, msg, len), 0);
|
|
}
|
|
|
|
KTEST(sfsOverwriteReflectsLatestData)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("a.bin");
|
|
KT_TRUE(t, f > 0);
|
|
KT_EQ(t, sfsWriteAt(f, 0, "AAAA", 4), 4);
|
|
KT_EQ(t, sfsWriteAt(f, 0, "BB", 2), 2);
|
|
|
|
char back[4];
|
|
KT_EQ(t, sfsReadAt(f, 0, back, 4), 4);
|
|
KT_EQ(t, memcmp(back, "BBAA", 4), 0);
|
|
KT_EQ(t, sfsSizeOf(f), 4);
|
|
}
|
|
|
|
KTEST(sfsWritePastEndZeroFillsGap)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("gap.bin");
|
|
KT_TRUE(t, f > 0);
|
|
KT_EQ(t, sfsWriteAt(f, 10, "X", 1), 1);
|
|
KT_EQ(t, sfsSizeOf(f), 11);
|
|
|
|
uint8 back[11];
|
|
KT_EQ(t, sfsReadAt(f, 0, back, 11), 11);
|
|
for (int i = 0; i < 10; i++)
|
|
KT_EQ(t, back[i], 0);
|
|
KT_EQ(t, back[10], 'X');
|
|
}
|
|
|
|
KTEST(sfsMultiBlockFileSpansDirectPointers)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("multi.bin");
|
|
KT_TRUE(t, f > 0);
|
|
|
|
/* Big enough to span several blocks: kmalloc'd rather than kept on the
|
|
stack, matching the rbt/slab stress-test convention (this ktest
|
|
battery runs on the fixed 16 KiB boot stack, not a per-thread one). */
|
|
#define sfsTestSpan (3 * sfsBlockSize + 200)
|
|
uint8* pattern = (uint8*)kmalloc(sfsTestSpan);
|
|
uint8* back = (uint8*)kmalloc(sfsTestSpan);
|
|
KT_NOTNULL(t, pattern);
|
|
KT_NOTNULL(t, back);
|
|
if (!pattern || !back) {
|
|
kfree(pattern);
|
|
kfree(back);
|
|
return;
|
|
}
|
|
for (int i = 0; i < sfsTestSpan; i++)
|
|
pattern[i] = (uint8)(i * 31 + 7);
|
|
|
|
KT_EQ(t, sfsWriteAt(f, 0, pattern, sfsTestSpan), sfsTestSpan);
|
|
KT_EQ(t, sfsSizeOf(f), sfsTestSpan);
|
|
|
|
memset(back, 0, sfsTestSpan);
|
|
KT_EQ(t, sfsReadAt(f, 0, back, sfsTestSpan), sfsTestSpan);
|
|
KT_EQ(t, memcmp(back, pattern, sfsTestSpan), 0);
|
|
|
|
kfree(pattern);
|
|
kfree(back);
|
|
#undef sfsTestSpan
|
|
}
|
|
|
|
KTEST(sfsFileGrowsThroughIndirectBlock)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("big.bin");
|
|
KT_TRUE(t, f > 0);
|
|
|
|
/* 10 direct blocks (5120 bytes) plus a chunk that only the single
|
|
indirect block can address: this must reach past sfsDirectBlocks.
|
|
kmalloc'd (three of these at once would be ~18 KiB, more than the
|
|
whole 16 KiB boot stack ktest runs on). */
|
|
#define sfsBigSpan (12 * sfsBlockSize + 33)
|
|
uint8* pattern = (uint8*)kmalloc(sfsBigSpan);
|
|
uint8* back = (uint8*)kmalloc(sfsBigSpan);
|
|
uint8* verify = (uint8*)kmalloc(sfsBigSpan);
|
|
KT_NOTNULL(t, pattern);
|
|
KT_NOTNULL(t, back);
|
|
KT_NOTNULL(t, verify);
|
|
if (!pattern || !back || !verify) {
|
|
kfree(pattern);
|
|
kfree(back);
|
|
kfree(verify);
|
|
return;
|
|
}
|
|
for (int i = 0; i < sfsBigSpan; i++)
|
|
pattern[i] = (uint8)(i ^ 0x5A);
|
|
|
|
KT_EQ(t, sfsWriteAt(f, 0, pattern, sfsBigSpan), sfsBigSpan);
|
|
KT_EQ(t, sfsSizeOf(f), sfsBigSpan);
|
|
|
|
memset(back, 0, sfsBigSpan);
|
|
KT_EQ(t, sfsReadAt(f, 0, back, sfsBigSpan), sfsBigSpan);
|
|
KT_EQ(t, memcmp(back, pattern, sfsBigSpan), 0);
|
|
|
|
/* Overwrite a slice that straddles the direct/indirect boundary and
|
|
confirm only that slice changes. */
|
|
uint8 patch[64];
|
|
memset(patch, 0xEE, sizeof(patch));
|
|
uint64 patchOffset = 10 * sfsBlockSize - 20;
|
|
KT_EQ(t, sfsWriteAt(f, patchOffset, patch, sizeof(patch)), (int64)sizeof(patch));
|
|
|
|
KT_EQ(t, sfsReadAt(f, 0, verify, sfsBigSpan), sfsBigSpan);
|
|
KT_EQ(t, memcmp(verify + patchOffset, patch, sizeof(patch)), 0);
|
|
KT_EQ(t, memcmp(verify, pattern, patchOffset), 0);
|
|
|
|
kfree(pattern);
|
|
kfree(back);
|
|
kfree(verify);
|
|
#undef sfsBigSpan
|
|
}
|
|
|
|
KTEST(sfsNestedDirectoriesAndLookup)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
KT_TRUE(t, sfsCreateDir("etc") > 0);
|
|
KT_TRUE(t, sfsCreateDir("etc/sub") > 0);
|
|
int32 f = sfsCreateFile("etc/sub/leaf.txt");
|
|
KT_TRUE(t, f > 0);
|
|
KT_EQ(t, sfsWriteAt(f, 0, "leaf", 4), 4);
|
|
|
|
int32 viaLookup = sfsLookup("etc/sub/leaf.txt");
|
|
KT_EQ(t, viaLookup, f);
|
|
int32 subDir = sfsLookup("etc/sub");
|
|
KT_TRUE(t, subDir > 0);
|
|
KT_EQ(t, sfsTypeOf(subDir), sfsTypeDir);
|
|
KT_EQ(t, sfsLookup("etc/nope"), -1);
|
|
KT_EQ(t, sfsLookup("nope/at/all"), -1);
|
|
}
|
|
|
|
KTEST(sfsDuplicateCreateRejected)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
KT_TRUE(t, sfsCreateFile("dup.txt") > 0);
|
|
KT_EQ(t, sfsCreateFile("dup.txt"), -1);
|
|
KT_EQ(t, sfsCreateDir("dup.txt"), -1);
|
|
}
|
|
|
|
KTEST(sfsRemoveFileFreesBlocksForReuse)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("tofree.bin");
|
|
KT_TRUE(t, f > 0);
|
|
#define sfsFreeChunk (4 * sfsBlockSize)
|
|
uint8* chunk = (uint8*)kmalloc(sfsFreeChunk);
|
|
KT_NOTNULL(t, chunk);
|
|
if (!chunk)
|
|
return;
|
|
memset(chunk, 0x42, sfsFreeChunk);
|
|
KT_EQ(t, sfsWriteAt(f, 0, chunk, sfsFreeChunk), sfsFreeChunk);
|
|
kfree(chunk);
|
|
#undef sfsFreeChunk
|
|
|
|
KT_TRUE(t, sfsRemove("tofree.bin"));
|
|
KT_EQ(t, sfsLookup("tofree.bin"), -1);
|
|
|
|
/* A same-size file created afterward must succeed, proving the blocks
|
|
(and the inode slot) actually came back to the free pool rather than
|
|
leaking on every create/remove cycle. */
|
|
int32 again = sfsCreateFile("tofree.bin");
|
|
KT_TRUE(t, again > 0);
|
|
KT_EQ(t, sfsSizeOf(again), 0);
|
|
}
|
|
|
|
KTEST(sfsRmdirRequiresEmpty)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
KT_TRUE(t, sfsCreateDir("full") > 0);
|
|
KT_TRUE(t, sfsCreateFile("full/child.txt") > 0);
|
|
KT_FALSE(t, sfsRemove("full"));
|
|
|
|
KT_TRUE(t, sfsRemove("full/child.txt"));
|
|
KT_TRUE(t, sfsRemove("full"));
|
|
KT_EQ(t, sfsLookup("full"), -1);
|
|
}
|
|
|
|
KTEST(sfsReaddirListsAllEntriesAndSwapRemoveKeepsRest)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 dir = sfsCreateDir("listing");
|
|
KT_TRUE(t, dir > 0);
|
|
KT_TRUE(t, sfsCreateFile("listing/a") > 0);
|
|
KT_TRUE(t, sfsCreateFile("listing/b") > 0);
|
|
KT_TRUE(t, sfsCreateFile("listing/c") > 0);
|
|
|
|
uint32 seen = 0;
|
|
char name[sfsNameMax + 1];
|
|
uint32 type;
|
|
for (uint32 i = 0; sfsReaddirAt(dir, i, name, sizeof(name), &type); i++) {
|
|
KT_EQ(t, type, sfsTypeFile);
|
|
seen++;
|
|
}
|
|
KT_EQ(t, seen, 3);
|
|
|
|
KT_TRUE(t, sfsRemove("listing/b"));
|
|
KT_NE(t, sfsLookup("listing/a"), -1);
|
|
KT_NE(t, sfsLookup("listing/c"), -1);
|
|
KT_EQ(t, sfsLookup("listing/b"), -1);
|
|
|
|
seen = 0;
|
|
for (uint32 i = 0; sfsReaddirAt(dir, i, name, sizeof(name), &type); i++)
|
|
seen++;
|
|
KT_EQ(t, seen, 2);
|
|
}
|
|
|
|
KTEST(sfsTruncateShrinksAndFreesBlocks)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("shrink.bin");
|
|
KT_TRUE(t, f > 0);
|
|
#define sfsShrinkChunk (5 * sfsBlockSize)
|
|
uint8* chunk = (uint8*)kmalloc(sfsShrinkChunk);
|
|
KT_NOTNULL(t, chunk);
|
|
if (!chunk)
|
|
return;
|
|
memset(chunk, 0x7A, sfsShrinkChunk);
|
|
KT_EQ(t, sfsWriteAt(f, 0, chunk, sfsShrinkChunk), sfsShrinkChunk);
|
|
|
|
KT_TRUE(t, sfsTruncate(f, sfsBlockSize));
|
|
KT_EQ(t, sfsSizeOf(f), sfsBlockSize);
|
|
|
|
uint8 back[sfsBlockSize];
|
|
KT_EQ(t, sfsReadAt(f, 0, back, sfsBlockSize), sfsBlockSize);
|
|
KT_EQ(t, memcmp(back, chunk, sfsBlockSize), 0);
|
|
kfree(chunk);
|
|
#undef sfsShrinkChunk
|
|
|
|
KT_TRUE(t, sfsTruncate(f, 0));
|
|
KT_EQ(t, sfsSizeOf(f), 0);
|
|
}
|
|
|
|
KTEST(sfsPersistsAcrossUnmountRemount)
|
|
{
|
|
/* The real point of this test: sfsUnmount() drops every in-memory
|
|
structure (bitmaps freed, mounted flag cleared). If sfsMount() can
|
|
still find the file and its exact bytes afterward, the filesystem is
|
|
genuinely backed by disk, not by a cache that happens to survive. */
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
KT_TRUE(t, sfsCreateDir("keep") > 0);
|
|
int32 f = sfsCreateFile("keep/data.bin");
|
|
KT_TRUE(t, f > 0);
|
|
const char* payload = "this must survive a real remount";
|
|
uint64 len = strlen(payload);
|
|
KT_EQ(t, sfsWriteAt(f, 0, payload, len), (int64)len);
|
|
|
|
sfsUnmount();
|
|
KT_FALSE(t, sfsMounted());
|
|
|
|
KT_TRUE(t, sfsMount());
|
|
KT_TRUE(t, sfsMounted());
|
|
|
|
int32 reloaded = sfsLookup("keep/data.bin");
|
|
KT_EQ(t, reloaded, f);
|
|
KT_EQ(t, sfsSizeOf(reloaded), len);
|
|
|
|
char back[64];
|
|
memset(back, 0, sizeof(back));
|
|
KT_EQ(t, sfsReadAt(reloaded, 0, back, len), (int64)len);
|
|
KT_EQ(t, memcmp(back, payload, len), 0);
|
|
}
|
|
|
|
KTEST(sfsMountWithoutFormatRecoversPriorState)
|
|
{
|
|
/* sfsMount() on an already-formatted disk must NOT reformat: a second
|
|
mount call has to see the first mount's data, exactly like a real
|
|
reboot re-mounting an existing filesystem. */
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("survives.txt");
|
|
KT_TRUE(t, f > 0);
|
|
KT_EQ(t, sfsWriteAt(f, 0, "still here", 10), 10);
|
|
sfsUnmount();
|
|
|
|
KT_TRUE(t, sfsMount());
|
|
int32 found = sfsLookup("survives.txt");
|
|
KT_EQ(t, found, f);
|
|
char back[10];
|
|
KT_EQ(t, sfsReadAt(found, 0, back, 10), 10);
|
|
KT_EQ(t, memcmp(back, "still here", 10), 0);
|
|
}
|
|
|
|
/*
|
|
* mtime/ctime coverage below. Every bound uses a real rtcNow() reading taken
|
|
* strictly before or after the operation under test, in this same thread, so
|
|
* "before <= stamped-value <= after" is a real chronological fact (the clock
|
|
* only ever moves forward), never a flaky timing assumption -- unlike a
|
|
* cross-run "did it increase" check, which would need a real elapsed second
|
|
* to be meaningful. sfsLinkBumpsCtimeButNeverTouchesMtime instead checks
|
|
* exact, timing-independent equality: the link code path must never write
|
|
* inode.mtime at all, so it has to come back byte-for-byte unchanged
|
|
* regardless of how much wall-clock time passed.
|
|
*/
|
|
|
|
KTEST(sfsCreateFileStampsMtimeAndCtimeNearNow)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int64 before = rtcNow();
|
|
int32 f = sfsCreateFile("stamped.txt");
|
|
if (!KT_TRUE(t, f > 0))
|
|
return;
|
|
int64 after = rtcNow();
|
|
|
|
uint64 mtime = sfsMtimeOf(f);
|
|
uint64 ctime = sfsCtimeOf(f);
|
|
KT_EQ(t, mtime, ctime);
|
|
KT_GE(t, mtime, before);
|
|
KT_LE(t, mtime, after);
|
|
}
|
|
|
|
KTEST(sfsWriteAtBumpsMtimeAndCtimeStayingInSync)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("written.txt");
|
|
if (!KT_TRUE(t, f > 0))
|
|
return;
|
|
uint64 createdMtime = sfsMtimeOf(f);
|
|
|
|
int64 before = rtcNow();
|
|
const char* payload = "sfs mtime test payload";
|
|
uint64 len = strlen(payload);
|
|
KT_EQ(t, sfsWriteAt(f, 0, payload, len), (int64)len);
|
|
int64 after = rtcNow();
|
|
|
|
uint64 mtime = sfsMtimeOf(f);
|
|
uint64 ctime = sfsCtimeOf(f);
|
|
KT_EQ(t, mtime, ctime);
|
|
KT_GE(t, mtime, createdMtime);
|
|
KT_GE(t, mtime, before);
|
|
KT_LE(t, mtime, after);
|
|
}
|
|
|
|
KTEST(sfsLinkBumpsCtimeButNeverTouchesMtime)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("original.txt");
|
|
if (!KT_TRUE(t, f > 0))
|
|
return;
|
|
KT_EQ(t, sfsWriteAt(f, 0, "hi", 2), 2);
|
|
|
|
uint64 mtimeBeforeLink = sfsMtimeOf(f);
|
|
uint64 ctimeBeforeLink = sfsCtimeOf(f);
|
|
|
|
int64 before = rtcNow();
|
|
KT_EQ(t, sfsLink("original.txt", "alias.txt"), f);
|
|
int64 after = rtcNow();
|
|
|
|
KT_EQ(t, sfsMtimeOf(f), mtimeBeforeLink);
|
|
KT_GE(t, sfsCtimeOf(f), ctimeBeforeLink);
|
|
KT_GE(t, sfsCtimeOf(f), before);
|
|
KT_LE(t, sfsCtimeOf(f), after);
|
|
}
|
|
|
|
KTEST(sfsRemoveOneOfTwoHardlinksBumpsRemainingLinkCtime)
|
|
{
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("keepme.txt");
|
|
if (!KT_TRUE(t, f > 0))
|
|
return;
|
|
KT_EQ(t, sfsLink("keepme.txt", "alias2.txt"), f);
|
|
KT_EQ(t, sfsLinkCountOf(f), 2u);
|
|
|
|
uint64 ctimeBeforeRemove = sfsCtimeOf(f);
|
|
int64 before = rtcNow();
|
|
KT_TRUE(t, sfsRemove("alias2.txt"));
|
|
int64 after = rtcNow();
|
|
|
|
KT_EQ(t, sfsLinkCountOf(f), 1u);
|
|
KT_GE(t, sfsCtimeOf(f), ctimeBeforeRemove);
|
|
KT_GE(t, sfsCtimeOf(f), before);
|
|
KT_LE(t, sfsCtimeOf(f), after);
|
|
}
|
|
|
|
KTEST(sfsPersistsMtimeAndCtimeAcrossUnmountRemount)
|
|
{
|
|
/* Same real point as sfsPersistsAcrossUnmountRemount above, applied to
|
|
the new fields: sfsUnmount() drops every in-memory structure, so an
|
|
exact match after sfsMount() proves these timestamps genuinely live on
|
|
disk inside the inode, not in some cache that happens to survive. */
|
|
if (!KT_TRUE(t, sfsFormat()))
|
|
return;
|
|
|
|
int32 f = sfsCreateFile("timepersist.txt");
|
|
if (!KT_TRUE(t, f > 0))
|
|
return;
|
|
KT_EQ(t, sfsWriteAt(f, 0, "persisted", 9), 9);
|
|
|