Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m21s
akbasic CI Build / sanitizers (push) Failing after 4m32s
akbasic CI Build / coverage (push) Failing after 3m39s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m29s
A mutation run over src/runtime.c found that deleting the `LOG_ERROR_WITH_MESSAGE` in `report_and_reraise()` left the whole suite green. That line *is* the visible half of the "a failed report must not swallow the program's own error" fix -- without it the secondary failure goes nowhere and the only evidence is the original error still being the one raised. Nothing looked at it, so nothing noticed. `tests/trap_verbs.c` now redirects `akerr_log_method` into a buffer and drives the path with a sink that refuses every write, which is the only way left to make reporting fail now that the `TRAP` dispatch no longer allocates. It asserts the log line and that `errclass` still records the program's own error, and it kills the mutant. TODO.md gains a section for what the run found and what it did not: it was cut off at 551 of 997 mutants after ninety minutes, so 446 are unexamined and a full run belongs in the release workflow rather than here. The three regions this work touched were all covered, and the survivors in the skipped-block guard are named and assessed -- three of the four are equivalent for any program that can actually be written, and the one that is not would need a test with a loop on line 1. Also recorded: src/variable.c at 68.2% with every mutant on the inline-storage lines killed, and src/runtime_trap.c at 82.5% with no survivors in `set_error_variables()`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
404 lines
14 KiB
C
404 lines
14 KiB
C
/**
|
|
* @file trap_verbs.c
|
|
* @brief Tests the group C verbs: TRAP, RESUME and ERR().
|
|
*
|
|
* The interesting half of error trapping is what *stops* happening: an armed
|
|
* TRAP suppresses the "? line : CLASS message" report and stops the run from
|
|
* ending, and neither is visible unless a test says the output is empty of one
|
|
* and the program kept going.
|
|
*
|
|
* `ER` and `EL` are `ER#` and `EL#` here -- ordinary globals, because this
|
|
* dialect has no bare variable names. See TODO.md section 5.
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "harness.h"
|
|
#include "testutil.h"
|
|
|
|
/** @brief Run a program to completion in RUN mode, from a string. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, harness_start(NULL));
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
/* Bounded: a bare RESUME that never fixes anything is an infinite retry. */
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 4000));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief An armed TRAP swallows the report and enters the handler instead. */
|
|
static void test_trap_catches(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
|
|
"20 DIM Q#(2)\n"
|
|
"30 PRINT Q#(9)\n"
|
|
"40 PRINT \"AFTER\"\n"
|
|
"50 END\n"
|
|
"100 PRINT \"TRAPPED \" + ER# + \" ON LINE \" + EL#\n"
|
|
"110 RESUME NEXT\n"));
|
|
/*
|
|
* AKBASIC_ERR_BOUNDS is 515: base 512 plus SYNTAX, TYPE, UNDEFINED. Spelled
|
|
* out rather than referenced so that a renumbering of the error band shows up
|
|
* here as a failure rather than as a silently different program.
|
|
*/
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "TRAPPED 515 ON LINE 30\nAFTER\n");
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") == NULL,
|
|
"a trapped error must not also be reported, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A *parse* error is trappable too, and carries its code into ER#.
|
|
*
|
|
* Found while writing the error-code appendix, and it was the worst kind of
|
|
* defect: arming a handler made errors disappear. `akbasic_runtime_error()`
|
|
* intercepts for the trap and leaves `errclass` clear, but the parse branch of
|
|
* `process_line_run()` set `run_finished_mode` regardless -- QUIT for a file --
|
|
* so the run ended before the next line boundary the handler would have been
|
|
* entered at. No error line, because the trap suppressed it; no handler, because
|
|
* the program had stopped. Nothing on stdout at all.
|
|
*
|
|
* The second half is that the same branch never recorded the status, so the
|
|
* handler that now runs used to be handed `ER# 0`.
|
|
*/
|
|
static void test_trap_catches_parse_error(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
|
|
"20 SCALE\n"
|
|
"30 PRINT \"AFTER\"\n"
|
|
"40 END\n"
|
|
"100 PRINT \"TRAPPED \" + ER# + \" ON LINE \" + EL#\n"
|
|
"110 RESUME NEXT\n"));
|
|
/* 512 is AKBASIC_ERR_SYNTAX, the base of the band. Spelled out for the same
|
|
reason test_trap_catches() spells out 515. */
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "TRAPPED 512 ON LINE 20\nAFTER\n");
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "PARSE ERROR") == NULL,
|
|
"a trapped parse error must not also be reported, got \"%s\"",
|
|
HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/* And with no trap it still reports and still stops -- the half of the
|
|
behaviour that was never broken and must not become so. */
|
|
TEST_REQUIRE_OK(run_program("10 SCALE\n"
|
|
"20 PRINT \"AFTER\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 10 : PARSE ERROR") != NULL,
|
|
"an untrapped parse error should be reported, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "AFTER") == NULL,
|
|
"an untrapped parse error should stop the run, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief With no TRAP the error is reported and the program stops, as before. */
|
|
static void test_untrapped_still_reports(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 DIM Q#(2)\n"
|
|
"20 PRINT Q#(9)\n"
|
|
"30 PRINT \"UNREACHED\"\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
|
|
"an untrapped error should be reported, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHED") == NULL,
|
|
"an untrapped error should stop the program, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief Bare TRAP disarms, and the next error is reported normally again. */
|
|
static void test_trap_disarms(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
|
|
"20 TRAP\n"
|
|
"30 DIM Q#(2)\n"
|
|
"40 PRINT Q#(9)\n"
|
|
"50 PRINT \"UNREACHED\"\n"
|
|
"100 PRINT \"NOT SEEN\"\n"
|
|
"110 RESUME NEXT\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
|
|
"a disarmed TRAP should let the error through, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "NOT SEEN") == NULL,
|
|
"the handler should not have run, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief A handler may be named by label, which is where a handler usually sits. */
|
|
static void test_trap_by_label(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 TRAP HANDLER\n"
|
|
"20 DIM Q#(2)\n"
|
|
"30 PRINT Q#(9)\n"
|
|
"40 PRINT \"AFTER\"\n"
|
|
"50 END\n"
|
|
"100 LABEL HANDLER\n"
|
|
"110 PRINT \"CAUGHT\"\n"
|
|
"120 RESUME NEXT\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "CAUGHT\nAFTER\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief RESUME's three forms land on three different lines. */
|
|
static void test_resume_forms(void)
|
|
{
|
|
/* RESUME <line> goes where it is told. */
|
|
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
|
|
"20 DIM Q#(2)\n"
|
|
"30 PRINT Q#(9)\n"
|
|
"40 PRINT \"NOT HERE\"\n"
|
|
"50 PRINT \"HERE\"\n"
|
|
"60 END\n"
|
|
"100 RESUME 50\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "HERE\n");
|
|
harness_stop();
|
|
|
|
/*
|
|
* Bare RESUME retries the failing line. The handler fixes the array first,
|
|
* so the retry succeeds -- which is the only way a bare RESUME terminates,
|
|
* and the reason the verb exists.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
|
|
"20 DIM Q#(2)\n"
|
|
"30 PRINT Q#(N#)\n"
|
|
"40 PRINT \"AFTER\"\n"
|
|
"50 END\n"
|
|
"100 N# = 1\n"
|
|
"110 RESUME\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\nAFTER\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief RESUME outside a handler is refused rather than popping something else. */
|
|
static void test_resume_outside_handler(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 RESUME NEXT\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside the context of a TRAP handler") != NULL,
|
|
"a bare RESUME should be refused, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief An error inside a handler is reported rather than re-entering it.
|
|
*
|
|
* Otherwise a broken handler traps its own failure forever and the program can
|
|
* never be stopped. A C128 behaves the same way.
|
|
*/
|
|
static void test_error_inside_handler(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
|
|
"20 DIM Q#(2)\n"
|
|
"30 PRINT Q#(9)\n"
|
|
"40 END\n"
|
|
"100 PRINT \"IN HANDLER\"\n"
|
|
"110 PRINT Q#(9)\n"
|
|
"120 RESUME NEXT\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "IN HANDLER") != NULL,
|
|
"the handler should have run once, got \"%s\"", HARNESS_OUTPUT);
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
|
|
"an error inside the handler should be reported, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
}
|
|
|
|
/** @brief ERR() names a status code, using the registry every code is named in. */
|
|
static void test_err_function(void)
|
|
{
|
|
TEST_REQUIRE_OK(run_program("10 TRAP 100\n"
|
|
"20 DIM Q#(2)\n"
|
|
"30 PRINT Q#(9)\n"
|
|
"40 END\n"
|
|
"100 PRINT ERR(ER#)\n"
|
|
"110 RESUME NEXT\n"));
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "Out Of Bounds") != NULL,
|
|
"ERR should name the status, got \"%s\"", HARNESS_OUTPUT);
|
|
harness_stop();
|
|
|
|
/*
|
|
* A code nobody reserved reads back as libakerror's own "Unknown Error",
|
|
* which is the same string a stack trace would print for it.
|
|
*/
|
|
TEST_REQUIRE_OK(run_program("10 PRINT ERR(30000)\n"));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "Unknown Error\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief `ER#` and `EL#` exist before a program runs, so the dispatch cannot fail.
|
|
*
|
|
* The dispatch used to reach them through akbasic_runtime_global(), which
|
|
* *creates* a name the program never used -- and creating one takes a variable
|
|
* slot. So the handler could not be entered by a program that had filled the
|
|
* table, which is the one kind of program most likely to raise. Asserted here as
|
|
* the invariant rather than only through the symptom below, because the symptom
|
|
* needs 124 lines of setup and this needs none.
|
|
*/
|
|
static void test_error_globals_are_reserved(void)
|
|
{
|
|
bool haveer = false;
|
|
bool haveel = false;
|
|
int i = 0;
|
|
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
for ( i = 0; i < AKBASIC_MAX_VARIABLES; i++ ) {
|
|
if ( !HARNESS_RUNTIME.variables[i].used ) {
|
|
continue;
|
|
}
|
|
if ( strcmp(HARNESS_RUNTIME.variables[i].name, "ER#") == 0 ) {
|
|
haveer = true;
|
|
}
|
|
if ( strcmp(HARNESS_RUNTIME.variables[i].name, "EL#") == 0 ) {
|
|
haveel = true;
|
|
}
|
|
}
|
|
TEST_REQUIRE(haveer, "ER# must exist as soon as the runtime is initialised");
|
|
TEST_REQUIRE(haveel, "EL# must exist as soon as the runtime is initialised");
|
|
harness_stop();
|
|
}
|
|
|
|
/**
|
|
* @brief A handler is entered even by a program that has filled the variable table.
|
|
*
|
|
* TODO.md section 6 item 33, and much worse than an abort: the dispatch failed
|
|
* *inside* the error path rather than raising, so nothing was reported, the
|
|
* handler never ran, and the program carried on with the failing statement's
|
|
* effect quietly missing -- a wrong answer delivered as a right one.
|
|
*
|
|
* 124 names, which leaves the table with no room to invent `ER#` and `EL#` at
|
|
* the moment they are wanted. It is 124 rather than 128 because the runtime
|
|
* reserves those two and the loop counter is a third.
|
|
*/
|
|
static void test_handler_runs_with_a_full_variable_table(void)
|
|
{
|
|
char source[AKBASIC_MAX_VARIABLES * 32];
|
|
size_t used = 0;
|
|
int i = 0;
|
|
|
|
used = (size_t)snprintf(source, sizeof(source), "10 TRAP CAUGHT\n");
|
|
for ( i = 0; i < 124; i++ ) {
|
|
used += (size_t)snprintf(source + used, sizeof(source) - used,
|
|
"%d V%03d# = %d\n", 20 + i, i, i);
|
|
}
|
|
snprintf(source + used, sizeof(source) - used,
|
|
"800 DIM TOOBIG#(5000)\n"
|
|
"810 PRINT \"NOT TRAPPED\"\n"
|
|
"820 END\n"
|
|
"900 LABEL CAUGHT\n"
|
|
"910 PRINT \"TRAPPED\"\n"
|
|
"920 END\n");
|
|
|
|
TEST_REQUIRE_OK(run_program(source));
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "TRAPPED\n");
|
|
harness_stop();
|
|
}
|
|
|
|
/*
|
|
* A sink whose writes fail, and a place to catch what libakerror logs.
|
|
*
|
|
* Between them these drive the one path that has no BASIC-visible output at all:
|
|
* reporting an error failing while an error is already being reported.
|
|
*/
|
|
static char LOGGED[2048];
|
|
|
|
static void capture_log(const char *format, ...)
|
|
{
|
|
va_list args;
|
|
size_t used = strlen(LOGGED);
|
|
|
|
va_start(args, format);
|
|
vsnprintf(LOGGED + used, sizeof(LOGGED) - used, format, args);
|
|
va_end(args);
|
|
}
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *failing_write(akbasic_TextSink *self, const char *text)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)self; (void)text;
|
|
FAIL_RETURN(errctx, AKERR_IO, "this sink always fails");
|
|
}
|
|
|
|
static akerr_ErrorContext AKERR_NOIGNORE *failing_clear(akbasic_TextSink *self)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)self;
|
|
FAIL_RETURN(errctx, AKERR_IO, "this sink always fails");
|
|
}
|
|
|
|
/**
|
|
* @brief A failure while *reporting* does not replace the program's own error.
|
|
*
|
|
* `report_and_reraise()` used a plain `PASS` around the report, so anything that
|
|
* went wrong while reporting became the error that propagated -- and the one the
|
|
* program actually made was gone. A user was told "could not write to the sink"
|
|
* about a subscript that was out of range.
|
|
*
|
|
* Provoked here by a sink that refuses every write, which is the only way left
|
|
* to make reporting fail now that the `TRAP` dispatch no longer allocates. The
|
|
* two assertions are the two halves of the fix: the secondary failure is
|
|
* *logged*, where a developer will see it, and the *original* status is what
|
|
* comes back out.
|
|
*
|
|
* The log assertion needs `akerr_log_method` redirected, and that is the point:
|
|
* a mutation run showed the log line could be deleted with the whole suite still
|
|
* passing, because nothing looked at it. TODO.md section 6 item 33.
|
|
*/
|
|
static void test_a_failed_report_keeps_the_original_error(void)
|
|
{
|
|
akerr_ErrorLogFunction saved = akerr_log_method;
|
|
akerr_ErrorContext *raised = NULL;
|
|
|
|
LOGGED[0] = '\0';
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
|
/* Out of bounds, which is what the program did wrong. */
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
|
"10 DIM Q#(2)\n"
|
|
"20 PRINT Q#(9)\n"));
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
|
|
|
HARNESS_SINK.write = failing_write;
|
|
HARNESS_SINK.writeln = failing_write;
|
|
HARNESS_SINK.clear = failing_clear;
|
|
akerr_log_method = capture_log;
|
|
raised = akbasic_runtime_run(&HARNESS_RUNTIME, 0);
|
|
akerr_log_method = saved;
|
|
test_discard_error(raised);
|
|
|
|
/*
|
|
* The secondary failure is logged rather than swallowed, and it says which
|
|
* one it was: "could not report" names the reporting, and the status on the
|
|
* end of the line is the *sink's* AKERR_IO. A mutation run showed this line
|
|
* could be deleted with the whole suite still green, because nothing looked
|
|
* at it.
|
|
*/
|
|
TEST_REQUIRE(strstr(LOGGED, "could not report a BASIC error") != NULL,
|
|
"the failure to report must be logged, got \"%s\"", LOGGED);
|
|
|
|
/*
|
|
* And the program's own error is still what happened: the class was recorded
|
|
* before the write that failed, so the run ended as a runtime error rather
|
|
* than carrying on past a statement that did not work.
|
|
*/
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.errclass, AKBASIC_ERRCLASS_RUNTIME);
|
|
harness_stop();
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
test_trap_catches();
|
|
test_trap_catches_parse_error();
|
|
test_untrapped_still_reports();
|
|
test_trap_disarms();
|
|
test_trap_by_label();
|
|
test_resume_forms();
|
|
test_resume_outside_handler();
|
|
test_error_inside_handler();
|
|
test_err_function();
|
|
test_error_globals_are_reserved();
|
|
test_handler_runs_with_a_full_variable_table();
|
|
test_a_failed_report_keeps_the_original_error();
|
|
return akbasic_test_failures;
|
|
}
|