mirror of
https://github.com/fluffos/fluffos
synced 2026-08-12 18:26:06 -04:00
Systematic review of all 326 spec-declared efuns against testsuite/single/tests/efuns/; every efun now has a test file named after it. Pure functions get exact behavioral pins (trig/log/vector math with domain-error cases, trim family, pcre group semantics, min/max index form, compress/encoding round trips, bit strings, Levenshtein string_difference, class assembly/introspection, deep copy() independence, dump_trace shape, save-string round trips); environment-dependent efuns get honest contracts (sockets: real create/bind/listen/connect lifecycle on ephemeral ports; interactive/ protocol/ed efuns: graceful no-interactive behavior; package-gated efuns guarded by __PACKAGE_*__ / efun_defined()). testsuite/include gains the driver-shipped socket.h/socket_err.h. Fixtures: catch_tell_probe (catch_tell recorder + self-mover + make_living), event_probe, shadow_probe, syntax_parent. The new coverage flushed out SEVEN real driver bugs, all fixed: - memory_summary: four division-by-zero sites in memory_share() when a value's refcount is 0 (UBSan) - send_zmp/start_request_term_type: command_giver dereferenced before the null check -- crash with no interactive (UBSan) - socket_create: LPC int loaded into enum socket_mode before validation -- UB for out-of-range modes; validated as int first - async_db_exec: manual callback ref taken before handle validation -- error() unwind leaked the function pointer (AGENTS.md section 4) - link(): epilogue abandoned both string arguments -- two shared-string refs leaked per call - assemble_class(): built through copy_array then retagged T_CLASS, skewing num_arrays/num_classes and the DEBUGMALLOC tag; now built with allocate_class_by_size - parser_mark_verbs(): marked only the HEAD of each verb's rule list (later rules unaccounted), double-marked base verbs through synonym entries (verb_syn_t::real overlays the node slot), and computed header offsets off NULL for rule-less verbs (UBSan) - pending resolve() queries had no DEBUGMALLOC accounting at all: new pending-query registry + mark_dns_requests(), mirroring mark_call_outs Runner: uncaught errors are now PRINTED as well as recorded (a failing file was otherwise silent about why). Verified: testsuite x3 randomized (ASan Debug) + ctest 297, clang RelWithDebInfo sanitizer full suite, RelWithDebInfo full ctest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
208 lines
6.4 KiB
Text
208 lines
6.4 KiB
Text
#include <globals.h>
|
|
|
|
// gtest-style LPC test runner (driver flag -ftest[:file-or-glob]).
|
|
//
|
|
// Output protocol, one block per test file:
|
|
// [ RUN ] /single/tests/efuns/foo.lpc
|
|
// [ OK ] /single/tests/efuns/foo.lpc (3 ms) -- or --
|
|
// [ FAILED ] /single/tests/efuns/foo.lpc (3 ms)
|
|
// then a recap:
|
|
// [==========] 4128 checks from 289 file(s) ran. (9214 ms total)
|
|
// [ PASSED ] 289 file(s).
|
|
// Checks succeeded.
|
|
// The "Checks succeeded." line and the process exit code are the
|
|
// machine-readable pass signals (ctest test "testsuite" and CI key
|
|
// on them).
|
|
//
|
|
// Failed checks are RECORDED (include/tests.h OUTPUT ->
|
|
// master::record_failure) and the run CONTINUES -- like gtest, one run
|
|
// reports every failure. Any failure makes the recap list the failed
|
|
// files and the driver exit nonzero. Files are run in randomized order
|
|
// to catch order dependencies.
|
|
//
|
|
// -ftest:single/tests/efuns/foo.lpc runs one file
|
|
// -ftest:efuns/dual* runs files whose path matches the
|
|
// (suffix-anchored) glob
|
|
|
|
private int total_files = 0;
|
|
private string *failed_files = ({ });
|
|
private string filter_re = 0;
|
|
|
|
private void check_leaks(string what) {
|
|
#if defined(__DEBUGMALLOC__) && defined(__DEBUGMALLOC_EXTENSIONS__) && defined(__PACKAGE_DEVELOP__)
|
|
string leaks = check_memory();
|
|
if (sizeof(filter(explode(leaks, "\n"), (: $1 && $1[0] :))) != 1) {
|
|
write("After " + what + ":\n");
|
|
write(leaks);
|
|
error("LEAK\n");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// kind: 0 = regular test (load + do_tests), 1 = must FAIL to load,
|
|
// 2 = crasher (errors ignored; only crashing the driver fails).
|
|
private void run_one(string path, int kind) {
|
|
string err;
|
|
object tp = this_player();
|
|
int before, t0, ms;
|
|
|
|
if (filter_re && !sizeof(regexp(({ path }), filter_re))) {
|
|
return;
|
|
}
|
|
|
|
before = sizeof("/single/master"->query_failures());
|
|
total_files++;
|
|
write("[ RUN ] " + path + "\n");
|
|
t0 = perf_counter_ns();
|
|
|
|
switch (kind) {
|
|
case 0:
|
|
err = catch(path->do_tests());
|
|
if (err) {
|
|
// Print AND record: assert failures print through OUTPUT, but an
|
|
// uncaught error would otherwise fail the file silently.
|
|
write(path + ": uncaught error: " + err + "\n");
|
|
"/single/master"->record_failure(path + ": uncaught error: " + err);
|
|
}
|
|
if (tp != this_player()) {
|
|
write(path + ": bad this_player() after test\n");
|
|
"/single/master"->record_failure(path + ": bad this_player() after test");
|
|
}
|
|
break;
|
|
case 1:
|
|
ASSERT2(catch(load_object(path)), path + " loaded");
|
|
break;
|
|
case 2:
|
|
catch(path->do_tests());
|
|
break;
|
|
}
|
|
|
|
ms = (perf_counter_ns() - t0) / 1000000;
|
|
if (sizeof("/single/master"->query_failures()) > before) {
|
|
failed_files += ({ path });
|
|
write("[ FAILED ] " + path + " (" + ms + " ms)\n");
|
|
} else {
|
|
write("[ OK ] " + path + " (" + ms + " ms)\n");
|
|
}
|
|
check_leaks(path);
|
|
}
|
|
|
|
private void recurse(string dir) {
|
|
foreach (string file in sort_array(get_dir(dir + "*.lpc"), (: random(2) - random(2) :))) {
|
|
run_one(dir + file, 0);
|
|
}
|
|
foreach (string subdir in map(filter(get_dir(dir + "*", -1),
|
|
(: $1[1] == -2 :)),
|
|
(: $1[0] :)) - ({ ".", ".." }))
|
|
{
|
|
if (subdir == "fail") {
|
|
foreach (string fn in get_dir(dir + "fail/*.lpc")) {
|
|
run_one(dir + "fail/" + fn, 1);
|
|
}
|
|
// Only when a fail-test actually compiled something (a filtered
|
|
// run may have skipped them all; cp() throws on a missing file).
|
|
if (file_size("/log/compile") != -1) {
|
|
cp("/log/compile", "/log/compile_fail");
|
|
rm("/log/compile");
|
|
}
|
|
} else if (subdir == "crasher") {
|
|
foreach (string fn in get_dir(dir + subdir + "/*.lpc")) {
|
|
run_one(dir + subdir + "/" + fn, 2);
|
|
}
|
|
} else {
|
|
recurse(dir + subdir + "/");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shell-style glob -> suffix-anchored regexp over the full test path,
|
|
// so "efuns/dual*" matches "/single/tests/efuns/dual_extension.lpc".
|
|
private string glob_to_regexp(string pat) {
|
|
string re = ".*";
|
|
foreach (int c in pat) {
|
|
switch (c) {
|
|
case '*':
|
|
re += ".*";
|
|
break;
|
|
case '?':
|
|
re += ".";
|
|
break;
|
|
case '.': case '[': case ']': case '(': case ')': case '+':
|
|
case '^': case '$': case '\\': case '|':
|
|
re += "\\" + sprintf("%c", c);
|
|
break;
|
|
default:
|
|
re += sprintf("%c", c);
|
|
}
|
|
}
|
|
return re + "$";
|
|
}
|
|
|
|
private int report(int total_ms) {
|
|
string *failures = "/single/master"->query_failures();
|
|
|
|
write("[==========] " + "/single/master"->query_num_checks() + " checks from " +
|
|
total_files + " file(s) ran. (" + total_ms + " ms total)\n");
|
|
if (!sizeof(failed_files) && !sizeof(failures)) {
|
|
write("[ PASSED ] " + total_files + " file(s).\n");
|
|
write("Checks succeeded.\n");
|
|
return 1;
|
|
}
|
|
write("[ FAILED ] " + sizeof(failed_files) + " file(s), " +
|
|
sizeof(failures) + " failed check(s), listed below:\n");
|
|
foreach (string f in failed_files) {
|
|
write("[ FAILED ] " + f + "\n");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int execute(string fun, int single_test: (: 0 :))
|
|
{
|
|
int t0;
|
|
int plain_single = 0;
|
|
|
|
set_eval_limit(0x7fffffff);
|
|
"/single/master"->reset_test_stats();
|
|
total_files = 0;
|
|
failed_files = ({ });
|
|
filter_re = 0;
|
|
|
|
t0 = perf_counter_ns();
|
|
if (!fun || fun == "") {
|
|
recurse("/single/tests/");
|
|
} else if (strsrch(fun, "*") != -1 || strsrch(fun, "?") != -1) {
|
|
filter_re = glob_to_regexp(fun);
|
|
recurse("/single/tests/");
|
|
filter_re = 0;
|
|
} else {
|
|
plain_single = 1;
|
|
run_one(fun, 0);
|
|
}
|
|
|
|
if (!report((perf_counter_ns() - t0) / 1000000)) {
|
|
// Autotest mode: exit nonzero. Interactively, just report.
|
|
if (!this_player()) {
|
|
shutdown(-1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (plain_single && "/single/master"->get_inherit_called() == 0) {
|
|
error("MASTER valid inherit functions are not being called!");
|
|
}
|
|
|
|
if (single_test) {
|
|
// A full suite run eventually hits the shutdown test, which schedules
|
|
// the clean exit; a single/filtered run must schedule it itself.
|
|
"single/tests/efuns/shutdown"->do_the_nasty_deed(); // 🤨
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int main(string file) {
|
|
#if !(defined(__DEBUGMALLOC__) && defined(__DEBUGMALLOC_EXTENSIONS__) && defined(__PACKAGE_DEVELOP__))
|
|
write("WARNING: Possible RELEASE build, check_memory() is not being executed.\n");
|
|
#endif
|
|
return execute(file || "", stringp(file) && strlen(file) > 0);
|
|
}
|