Files
akbasic/tests/interrupts.c
Logikoma d715bc0625
Some checks failed
akbasic CI Build / coverage (push) Failing after 27s
akbasic CI Build / cmake_build (push) Failing after 33s
akbasic CI Build / akgl_build (push) Failing after 18s
akbasic CI Build / sanitizers (push) Failing after 1m7s
akbasic CI Build / mutation_test (push) Failing after 39s
Report prescan errors on their source lines
2026-08-05 06:40:14 -04:00

392 lines
15 KiB
C

/**
* @file interrupts.c
* @brief Tests the interrupt machinery: arm, raise, enter a handler, RETURN.
*
* This is the part of COLLISION that has nothing to do with sprites, and it is
* tested on its own for two reasons. It is what group C's TRAP will reuse, so it
* has to be correct before anything is built on it; and driving it through the
* API rather than through a verb is the only way to fire an interrupt at an
* exactly known point in a program, which is what most of these assertions turn
* on.
*
* **Every program here is numbered 1, 2, 3, ...** One step of the runtime
* advances by one source line *index*, empty ones included -- so consecutive
* numbering is what makes a step count mean a statement count. The first step of
* any run is line 0, which is always empty, and is accounted for in each count
* below.
*/
#include <stdio.h>
#include "harness.h"
/**
* @brief A program with a handler at 3 and a main loop that never ends.
*
* The endless loop is on purpose: every test here steps a fixed number of times
* and asserts what came out, so a regression that fails to enter or fails to
* return shows up as the wrong output rather than as a run that stops early and
* looks plausible.
*/
static const char *PROGRAM_BY_LINE =
"1 PRINT \"START\"\n"
"2 GOTO 5\n"
"3 PRINT \"HANDLER\"\n"
"4 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n";
/** @brief Load a program and put the runtime in RUN mode without stepping it. */
static akerr_ErrorContext AKERR_NOIGNORE *load_program(const char *source)
{
PREPARE_ERROR(errctx);
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
SUCCEED_RETURN(errctx);
}
/** @brief A handler named by line number runs, and RETURN resumes where it left. */
static akerr_ErrorContext AKERR_NOIGNORE *test_enter_and_return(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program(PROGRAM_BY_LINE));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));
/* Line 0, then 1 and 2, so the program is sitting on 5 with nothing pending. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\n");
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "raising must not enter anything by itself");
/* One step: the handler is entered and its first line runs. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nHANDLER\n");
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL, "the runtime should know it is in a handler");
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"entering the handler should have consumed the event");
/* The RETURN, then the line the interrupt took the program away from. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nHANDLER\nMAIN\n");
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "RETURN should have left the handler");
/* Still armed: one collision handled is not a subscription cancelled. */
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
"the handler should still be armed after it returns");
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief A handler named by label, on a line the program never falls into.
*
* This is the shape a handler is actually written in -- jumped over by the main
* flow, reachable only through the interrupt -- and it works only because
* akbasic_runtime_scan_labels() files LABEL before anything runs. Without the
* prescan the label does not exist when the interrupt fires and the run stops
* with AKBASIC_ERR_UNDEFINED.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_label_handler(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 GOTO 5\n"
"2 LABEL BUMPED\n"
"3 PRINT \"HANDLER\"\n"
"4 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
0, "BUMPED"));
/* Line 0, the GOTO, then the main loop's PRINT. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\n");
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
/* The LABEL line the handler is entered on, then the handler's PRINT. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nHANDLER\n");
/* RETURN, then back into the main loop where it left off. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nHANDLER\nMAIN\n");
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief An interrupt does not interrupt an interrupt.
*
* Without the guard a collision that is still true while its own handler runs
* re-enters on the next line, and keeps re-entering until the environment pool
* is gone -- the failure would be AKBASIC_ERR_ENVIRONMENT from somewhere with
* nothing to do with sprites. The event raised inside the handler is not lost,
* though: it is taken as soon as the RETURN lands.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_no_reentry(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 GOTO 5\n"
"2 PRINT \"IN\"\n"
"3 PRINT \"OUT\"\n"
"4 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 2, NULL));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\n");
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\n");
/* Fires again while the handler is still running. */
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\nOUT\n");
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"an event raised inside a handler should be held, not dropped");
/* The RETURN lands, and the held event is taken on the very next step. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nIN\nOUT\nIN\n");
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief A GOSUB the handler itself makes returns without re-arming interrupts. */
static akerr_ErrorContext AKERR_NOIGNORE *test_nested_gosub_keeps_the_flag(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 GOTO 5\n"
"2 GOSUB 8\n"
"3 RETURN\n"
"5 PRINT \"MAIN\"\n"
"6 GOTO 5\n"
"8 PRINT \"INNER\"\n"
"9 RETURN\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 2, NULL));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 3));
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
/* Enter the handler, which immediately GOSUBs deeper. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL, "the handler should be running");
/* The inner PRINT and the inner RETURN: still inside the handler. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "MAIN\nINNER\n");
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv != NULL,
"the inner RETURN belongs to the inner GOSUB, not to the handler");
/* The handler's own RETURN is the one that clears it. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE(HARNESS_RUNTIME.handlerenv == NULL, "the handler's RETURN should have cleared it");
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief Raising an unarmed source records nothing; disarming drops what is held. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arm_and_disarm(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program(PROGRAM_BY_LINE));
/* Unarmed. A backend may raise every frame without asking who is listening. */
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"an unarmed source should record nothing");
/* Armed, raised, then taken away again before it could be taken. */
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
PASS(errctx, akbasic_runtime_disarm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].pending,
"disarming should drop a pending event rather than deferring it");
/* And the program runs straight through without ever seeing line 3. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 6));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "START\nMAIN\nMAIN\n");
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief NEW disarms everything: the handler lines belong to a program that is gone. */
static akerr_ErrorContext AKERR_NOIGNORE *test_new_disarms(void)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *out = NULL;
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program(PROGRAM_BY_LINE));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE, 3, NULL));
PASS(errctx, akbasic_environment_zero(HARNESS_RUNTIME.environment));
PASS(errctx, harness_parse("NEW", &leaf));
PASS(errctx, akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
"NEW should have disarmed the interrupt");
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief A handler label that names nothing is the program's error, not the host's.
*
* It has to be *reported* and stop the run. Letting it out of
* akbasic_runtime_step() would tear down an embedding game over a typo in a
* script, which is what goal 3 exists to prevent.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_undefined_label_is_reported(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 PRINT \"MAIN\"\n"
"2 GOTO 1\n"));
PASS(errctx, akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
0, "NOSUCHLABEL"));
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 2));
PASS(errctx, akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE));
/* Returns cleanly -- the error is on the sink, not in the return value. */
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 1));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "NOSUCHLABEL") != NULL,
"the report should name the label that could not be found, got: %s",
HARNESS_OUTPUT);
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief Arming refuses a source out of range, and a target that is both or neither. */
static akerr_ErrorContext AKERR_NOIGNORE *test_arm_refusals(void)
{
PREPARE_ERROR(errctx);
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME,
(akbasic_InterruptSource)AKBASIC_MAX_INTERRUPTS,
3, NULL),
AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
0, NULL),
AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(&HARNESS_RUNTIME, AKBASIC_INTERRUPT_SPRITE,
3, "BUMPED"),
AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_runtime_raise_interrupt(&HARNESS_RUNTIME,
(akbasic_InterruptSource)-1),
AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_runtime_disarm_interrupt(&HARNESS_RUNTIME,
(akbasic_InterruptSource)AKBASIC_MAX_INTERRUPTS),
AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_runtime_arm_interrupt(NULL, AKBASIC_INTERRUPT_SPRITE, 3, NULL),
AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_runtime_service_interrupts(NULL, NULL), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_runtime_scan_labels(NULL), AKERR_NULLPOINTER);
harness_stop();
SUCCEED_RETURN(errctx);
}
/**
* @brief The label prescan reads LABEL where it is a statement, and nowhere else.
*
* The forward jump itself is covered by the golden case at
* tests/language/statements/label_forward.bas. What is here is the part a
* running program cannot show: that a string literal holding the word LABEL
* files nothing, and that a second statement on a line is still a statement.
*/
static akerr_ErrorContext AKERR_NOIGNORE *test_prescan_boundaries(void)
{
PREPARE_ERROR(errctx);
int64_t line = 0;
TEST_REQUIRE_OK(harness_start(NULL));
PASS(errctx, load_program("1 PRINT \"LABEL DECOY\"\n"
"2 A# = 1 : LABEL SECOND\n"
"3 LABELLED# = 2\n"));
PASS(errctx, akbasic_environment_get_label(HARNESS_RUNTIME.environment, "SECOND", &line));
TEST_REQUIRE_INT(line, 2);
TEST_REQUIRE_STATUS(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "DECOY", &line),
AKBASIC_ERR_UNDEFINED);
/* LABELLED# is an identifier that starts with the word, not the verb. */
TEST_REQUIRE_STATUS(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "ED", &line),
AKBASIC_ERR_UNDEFINED);
harness_stop();
SUCCEED_RETURN(errctx);
}
/** @brief A full label table reports the line whose label could not be filed. */
static void test_label_prescan_error_line(void)
{
char source[4096] = "";
size_t used = 0;
int i = 0;
for ( i = 1; i <= AKBASIC_MAX_LABELS + 1; i++ ) {
used += (size_t)snprintf(source + used, sizeof(source) - used,
"%d LABEL L%d\n", i, i);
}
(void)snprintf(source + used, sizeof(source) - used, "100 PRINT 1\n");
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, source));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 65 : PARSE ERROR") != NULL,
"a full label table should report its source line, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
}
int main(void)
{
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, test_enter_and_return());
CATCH(errctx, test_label_handler());
CATCH(errctx, test_no_reentry());
CATCH(errctx, test_nested_gosub_keeps_the_flag());
CATCH(errctx, test_arm_and_disarm());
CATCH(errctx, test_new_disarms());
CATCH(errctx, test_undefined_label_is_reported());
CATCH(errctx, test_arm_refusals());
CATCH(errctx, test_prescan_boundaries());
test_label_prescan_error_line();
} CLEANUP {
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "interrupt test failed");
akbasic_test_failures += 1;
} FINISH_NORETURN(errctx);
return akbasic_test_failures;
}