fluffos/testsuite/README.md
Yucong Sun 12d99317d8 Testsuite coverage round 2: ctest test named "testsuite"; save_object overflow fixed
The ctest registration is now simply `testsuite` (label too; CI steps
run `ctest -LE testsuite` / `-L testsuite`), with README/AGENTS updated.

New coverage, and what it caught:

- save_object.lpc's ".lpc" case caught a REAL heap-buffer-overflow on
  CI's clang+ASan RelWithDebInfo job: save_object() sized its buffer
  for the stripped name + ".o" but strcpy'd the UNSTRIPPED path -- the
  4-byte ".lpc" strip overflows the 2-byte ".o" headroom (the legacy
  ".c" strip fit by coincidence; DEBUGMALLOC padding hid it from local
  ASan Debug runs). Fixed with a bounded copy, plus the pre-existing
  file[len-sel] underflow read guarded. Verified in the exact failing
  configuration (clang RelWithDebInfo sanitizer: file case + full suite).

- replace_program.lpc (new; the efun had NO testsuite coverage): pins
  name matching -- extension-less resolves the inherited program's real
  .lpc file, explicit ".lpc" matches exactly, explicit ".c" of an
  .lpc-compiled parent errors, non-inherited program errors. This
  surfaced that pending replace_ob_t records tripped check_memory()
  between test files: they legitimately live until the backend's
  replace_programs() sweep, so they get a dedicated whitelisted
  TAG_REPLACE_OB (same pattern as TAG_SCRATCHPAD) instead of
  TAG_TEMPORARY.

- dual_extension.lpc: the inherit-retry loop keeps the caller's exact
  spelling (a ".c" child inheriting an unloaded parent must reload as
  ".c", not fall back to its .lpc twin -- pins load_object's
  raw-spelling retry), and function_exists() strips a real ".c" suffix.

- restore_object.lpc: ".lpc"/".c" argument spellings resolve the same
  ".o" save file.

- std/harness.lpc: self-test of the failure-recording machinery
  (record/query/pop round-trip via the new master::pop_failure(), and
  the per-assertion check counter).

Verified: testsuite x3 + ctest 297/297 (ASan Debug), full ctest
(RelWithDebInfo), and the clang RelWithDebInfo sanitizer build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:48:48 -04:00

6.4 KiB
Raw Permalink Blame History

FluffOS Testsuite

This directory is a minimal mudlib (a descendant of the classic Lil bootstrap mudlib) plus the driver's LPC regression suite. The driver boots it directly; every efun/compiler/VM behavior change is expected to come with a test here.

Source file extension

All LPC sources in this tree use the .lpc extension. The driver resolves source files by these rules (implemented in load_object(), src/vm/internal/simulate.cc, and pinned by single/tests/efuns/dual_extension.lpc):

  • An explicit extension is exact: load_object("/foo.c") probes only foo.c, load_object("/foo.lpc") probes only foo.lpc — the other spelling is never looked up.
  • Extension-less names prefer .lpc and fall back to .c (load_object("/foo") loads foo.lpc if present, else foo.c).
  • Object identity is extension-blind: object names never carry an extension, and find_object() strips either spelling, so "/foo", "/foo.c", and "/foo.lpc" all find the same loaded object. The program name (prog->filename, what inherit_list() and diagnostics report) carries the real extension of the file that was compiled.
  • The registry wins over the filesystem: load_object() returns an already-loaded object for any spelling of its name — the exactness rule only applies when the load actually hits the disk.
  • When an exact probe misses, the load falls through to the master's compile_object() virtual-object hook, which receives the stripped name; if it declines, load_object() returns 0 (no error is thrown).
  • children(), save_object()/restore_object(), replace_program() and function_exists() treat both spellings equivalently (name stripping / suffix handling covers .lpc and .c alike).

A few .c files exist on purpose: the clone/dual_*.c fixtures for dual_extension.lpc, and /tmp_eval_file.c written at runtime by the eval/codefor commands (live proof that genuinely .c-named sources still compile).

Running the suite

The suite is a first-class ctest test and a set of CMake targets:

Invocation What it does
ctest -R testsuite (or ctest -L testsuite) Runs the whole LPC suite through ctest, alongside ctest -LE testsuite for the GTest binaries. This is what CI runs.
driver-autotest (CMake target) Same run, invoked directly; exits nonzero on any failure.
driver-testsuite (CMake target) Boots the driver against this mudlib for interactive poking (log in and type tests).

The runner (command/tests.lpc) prints a gtest-style protocol:

[ RUN      ] /single/tests/efuns/dual_extension.lpc
[       OK ] /single/tests/efuns/dual_extension.lpc (24 ms)
[==========] 3401 checks from 297 file(s) ran. (7470 ms total)
[  PASSED  ] 297 file(s).
Checks succeeded.

Failures do not stop the run: a failed check is printed with its expected/actual diff and trace, recorded, and the run continues — one run reports every failure (gtest semantics), then the recap lists each [ FAILED ] file and the driver exits nonzero. Checks succeeded. plus exit 0 is the machine-readable pass signal.

Run one file, or a glob over test paths:

./build/bin/driver testsuite/etc/config.test '-ftest:single/tests/efuns/dual_extension.lpc'
./build/bin/driver testsuite/etc/config.test '-ftest:efuns/dual*'

Without an argument the runner walks /single/tests/ recursively in randomized order (run it 23× when touching the lexer/parser), so tests must not depend on each other having run.

How the runner treats each directory

command/tests.lpc walks /single/tests/:

  • single/tests/**/*.lpc — regular tests: each file is loaded and its do_tests() is called; any uncaught error or failed assertion fails the suite.
  • .../fail/*.lpc — files that must fail to compile/load; the runner asserts catch(load_object(...)) throws.
  • .../crasher/*.lpc — regression cases that only need to not crash the driver; errors are ignored.
  • In DEBUGMALLOC builds the runner calls check_memory() after every file and fails on any leaked allocation, so tests must clean up (destruct clones, remove temp files).

Writing a test

Create single/tests/<area>/<name>.lpc with a do_tests() entry point. etc/config.test auto-includes <globals.h> into every object, which provides the assertion macros from include/tests.h:

void do_tests() {
    ASSERT(intp(1));                    // truthiness
    ASSERT2(sizeof(x) == 3, "reason");  // with message
    ASSERT_EQ("expected", actual);      // equality with diff output
    ASSERT_NE(a, b);
}

Failed assertions print file:line, Check failed (with expected/actual and a trace for ASSERT_EQ/ASSERT_NE), are recorded, and the run continues; any recorded failure makes the final recap fail the run with a nonzero exit (that is how CI detects failure). Helper fixtures that should not be executed as tests live outside /single/tests/ — conventionally in /clone (e.g. inh0inh2, dual_*) or as #includes under /include. A fixture whose test depends on it being unloaded (e.g. a fail/ test pinning that a name does not resolve) must be private to that one test: the object registry is extension-blind and survives across files, so any other test loading the fixture first would change the outcome under the randomized order.

Efun tests are named after the efun (single/tests/efuns/<efun>.lpc) and should aim to cover every branch of the C++ implementation, including error paths (catch(...)).

Layout

Path What
etc/config.test Driver config the suite boots with (mudlib dir, include dirs, global include, limits).
single/master.lpc Master object: flag() test entry, compile_object() virtual-object hook (/test/virtual), get_include_path() cases, error handling.
single/simul_efun.lpc Simul-efuns available everywhere in the suite.
single/tests/ The test tree (efuns/, compiler/, operators/, applies/, std/, plus fail/ and crasher/ subdirs).
command/ Interactive commands; tests.lpc is the suite runner, speed.lpc the benchmark entry (-fspeed).
clone/, inherit/, std/, u/ Fixture objects, inheritance helpers, minimal std lib, user dirs.
include/ Headers; tests.h (assertions), globals.h (auto-included).
data/, log/ Runtime state and logs written by the suite.