/** * @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 "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); } 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()); } 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; }