joeylib2/tests/host/saveHost.c

137 lines
4.2 KiB
C

// saveHost.c - Host-cc harness for the save-file core (src/core/save.c).
//
// Built by scripts/check-save.sh on the serialHost.c pattern: compiled with
// -DJOEYLIB_PLATFORM_BLANK plus the JL_HAS_SAVE_* flags so port.h routes
// jlpSave*/jlpDiskFree to the POSIX HAL below. Unlike the serial mock, this
// exercises the REAL core end to end -- jlSaveWrite/Read/Open/Exists/Delete do
// actual stdio into a SAVES/ directory in the current working directory, which
// check-save.sh points at a throwaway temp dir. Prints one line per check and
// a final tally; exits nonzero on any failure, so it doubles as a CI gate.
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include "joey/file.h"
#include "port.h"
// ----- POSIX save HAL (satisfies the jlpSave* dispatch under -DJL_HAS_SAVE_*) -----
uint32_t jlpDiskFree(void) {
struct statvfs vfs;
unsigned long long bytes;
if (statvfs(".", &vfs) != 0) {
return 0u;
}
bytes = (unsigned long long)vfs.f_bavail * (unsigned long long)vfs.f_frsize;
if (bytes > 0xFFFFFFFFull) {
return 0xFFFFFFFFu;
}
return (uint32_t)bytes;
}
bool jlpSaveDelete(const char *path) {
return remove(path) == 0;
}
bool jlpSaveDirEnsure(const char *dir) {
struct stat st;
if (mkdir(dir, 0777) == 0) {
return true;
}
return stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
}
// ----- Test harness -----
static int gPass = 0;
static int gFail = 0;
static void check(const char *name, bool ok) {
if (ok) {
gPass++;
printf(" ok %s\n", name);
} else {
gFail++;
printf(" FAIL %s\n", name);
}
}
int main(void) {
uint8_t out[64];
uint8_t in[64];
uint32_t n;
FILE *fp;
int i;
printf("save-host: core save/read/exists/delete/diskfree (real files)\n");
for (i = 0; i < 64; i++) {
out[i] = (uint8_t)(i * 7 + 1);
}
// 1. Clean slate + a plausible free-space number.
check("fresh: hero.sav absent", jlSaveExists("hero.sav") == false);
check("diskFree > 0", jlDiskFree() > 0u);
// 2. Blob write creates SAVES/ on demand and round-trips.
check("write blob (64)", jlSaveWrite("hero.sav", out, 64u) == true);
check("exists after write", jlSaveExists("hero.sav") == true);
memset(in, 0, sizeof(in));
n = jlSaveRead("hero.sav", in, sizeof(in));
check("read full length", n == 64u);
check("payload round-trips", memcmp(in, out, 64) == 0);
// 3. Read honors the caller's max.
memset(in, 0, sizeof(in));
n = jlSaveRead("hero.sav", in, 16u);
check("read capped at max", n == 16u);
check("capped payload matches", memcmp(in, out, 16) == 0);
// 4. Streaming open for incremental writes.
fp = jlSaveOpen("stream.sav", "wb");
check("stream open (wb)", fp != NULL);
if (fp != NULL) {
fputc('J', fp);
fputc('L', fp);
fclose(fp);
}
memset(in, 0, sizeof(in));
n = jlSaveRead("stream.sav", in, sizeof(in));
check("stream read length 2", n == 2u);
check("stream payload 'JL'", in[0] == (uint8_t)'J' && in[1] == (uint8_t)'L');
// 5. Zero-length write makes an empty file.
check("write empty", jlSaveWrite("empty.sav", NULL, 0u) == true);
check("empty exists", jlSaveExists("empty.sav") == true);
check("empty reads 0", jlSaveRead("empty.sav", in, sizeof(in)) == 0u);
// 6. Delete removes it; deleting a missing file reports false.
check("delete hero.sav", jlSaveDelete("hero.sav") == true);
check("hero.sav gone", jlSaveExists("hero.sav") == false);
check("delete missing -> false", jlSaveDelete("nope.sav") == false);
// 7. NULL / bad-argument guards.
check("write NULL name", jlSaveWrite(NULL, out, 4u) == false);
check("read NULL name", jlSaveRead(NULL, in, 4u) == 0u);
check("read NULL buf", jlSaveRead("empty.sav", NULL, 4u) == 0u);
check("open NULL name", jlSaveOpen(NULL, "rb") == NULL);
check("exists NULL name", jlSaveExists(NULL) == false);
check("delete NULL name", jlSaveDelete(NULL) == false);
// Best-effort cleanup of the files this run created.
jlSaveDelete("stream.sav");
jlSaveDelete("empty.sav");
printf("save-host: %d passed, %d failed\n", gPass, gFail);
return (gFail == 0) ? 0 : 1;
}