/** * @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 #include #include #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 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(); } 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(); return akbasic_test_failures; }