// 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 #include #include #include #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; }