SDL3 has text input off by default and per-window, so without SDL_StartTextInput() it emits no SDL_EVENT_TEXT_INPUT at all and every keystroke reaches libakgl's ring with an empty text field. The editor had just been changed to prefer that composed text over the keycode, so it read every key as not-a-character: no echo in the window, and nothing on stdout either, because RUN could never be typed. One cause, both symptoms. The editor now falls back to the keycode when there is no composed text, so a host that forgets to start text input gets a worse keyboard rather than none. The suite missed this because every keyboard test pushes the text-input event into SDL's queue by hand -- which is what a real keyboard produces, but only once text input has been started. Synthesising the end of a chain cannot test the beginning of it. The new test asserts SDL_TextInputActive() directly, and both halves were checked by reverting each and watching it fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
556 lines
20 KiB
C
556 lines
20 KiB
C
/**
|
|
* @file akgl_frontend.c
|
|
* @brief Tests the standalone SDL frontend: the host, not the adaptors.
|
|
*
|
|
* tests/akgl_backends.c stands in for a host and asserts that each adaptor
|
|
* reaches the right libakgl call. This file asserts the thing that was missing
|
|
* until the frontend existed: that there *is* a host, that it composes the two
|
|
* output paths so a program's bytes reach the window and stdout alike, that its
|
|
* event pump feeds the keystroke ring a script reads, that its line editor turns
|
|
* keystrokes into typed lines, and that closing the window ends a program that
|
|
* would otherwise never stop.
|
|
*
|
|
* Everything runs under the dummy video and audio drivers with a software
|
|
* renderer, following deps/libakgl/tests/draw.c, so it needs no display and no
|
|
* sound card. The font is the reference's own Commodore one -- the acceptance
|
|
* criterion in TODO.md section 3 asks for the drawn text to be in that font, and
|
|
* the frontend is the only place that opens it.
|
|
*
|
|
* Only one frontend may exist at a time: libakgl's renderer and window are
|
|
* process globals, and a host that never calls akgl_game_init() populates them
|
|
* itself. Each test therefore brings one up and takes it down again.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/controller.h>
|
|
#include <akgl/error.h>
|
|
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/frontend.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "testutil.h"
|
|
|
|
/** @brief Window size. Small enough to read the whole target back cheaply. */
|
|
#define WINDOW_W 320
|
|
/** @brief Window height. */
|
|
#define WINDOW_H 200
|
|
|
|
/* Both of these carry pools far too large for a stack. */
|
|
static akbasic_AkglFrontend FRONTEND;
|
|
static akbasic_Runtime RUNTIME;
|
|
static char OUTPUT[8192];
|
|
static FILE *MIRROR = NULL;
|
|
static FILE *PROGRAM = NULL;
|
|
|
|
/** @brief Report whether any pixel in a rectangle is not the background. */
|
|
static bool anything_drawn(SDL_Surface *shot, int x0, int y0, int w, int h)
|
|
{
|
|
uint8_t r = 0;
|
|
uint8_t g = 0;
|
|
uint8_t b = 0;
|
|
uint8_t a = 0;
|
|
int x = 0;
|
|
int y = 0;
|
|
|
|
if ( shot == NULL ) {
|
|
return false;
|
|
}
|
|
for ( y = y0; y < y0 + h; y++ ) {
|
|
for ( x = x0; x < x0 + w; x++ ) {
|
|
if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
|
|
continue;
|
|
}
|
|
if ( r != 0 || g != 0 || b != 0 ) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @brief Queue one key-down, exactly as a keyboard would.
|
|
*
|
|
* Straight into SDL's own queue rather than into libakgl's ring, so the
|
|
* frontend's event pump is the thing under test: nothing reaches the
|
|
* interpreter unless pump_events() drained this and handed it over.
|
|
*
|
|
* Key-down only, with no composed text -- which is what a cursor key, a function
|
|
* key or a bare modifier produces.
|
|
*/
|
|
static void push_key(int keycode)
|
|
{
|
|
SDL_Event event;
|
|
|
|
memset(&event, 0, sizeof(event));
|
|
event.type = SDL_EVENT_KEY_DOWN;
|
|
event.key.key = keycode;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
/**
|
|
* @brief Queue a printable keystroke: the key-down *and* the text it composed to.
|
|
*
|
|
* Two events, because that is what a real keyboard produces and what libakgl
|
|
* assembles a keystroke from -- SDL_EVENT_KEY_DOWN records the press and
|
|
* SDL_EVENT_TEXT_INPUT delivers the character the layout, the shift state, a
|
|
* compose key and a dead key between them worked out. A keycode alone cannot
|
|
* express any of that, which is why the ring carries both.
|
|
*
|
|
* `text` is deliberately allowed to differ from `keycode`: that is how a shifted
|
|
* character and a non-US layout are tested at all.
|
|
*/
|
|
static void push_text_key(int keycode, const char *text)
|
|
{
|
|
SDL_Event event;
|
|
|
|
push_key(keycode);
|
|
memset(&event, 0, sizeof(event));
|
|
event.type = SDL_EVENT_TEXT_INPUT;
|
|
event.text.text = text;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
/**
|
|
* @brief A one-character string with static storage, for one printable byte.
|
|
*
|
|
* SDL_PushEvent copies the SDL_Event but *not* the string an
|
|
* SDL_EVENT_TEXT_INPUT points at, so the text has to outlive the call that
|
|
* queued it and must not be reused before the queue is drained. A local buffer
|
|
* is neither: the first attempt at push_line() used one and every event in the
|
|
* line ended up pointing at the same overwritten two bytes, which showed up as
|
|
* a REPL that never received a line and a test that hung rather than failed.
|
|
*/
|
|
static const char *one_char(unsigned char c)
|
|
{
|
|
static char table[128][2];
|
|
static bool built = false;
|
|
int i = 0;
|
|
|
|
if ( !built ) {
|
|
for ( i = 0; i < 128; i++ ) {
|
|
table[i][0] = (char)i;
|
|
table[i][1] = '\0';
|
|
}
|
|
built = true;
|
|
}
|
|
return table[c & 0x7f];
|
|
}
|
|
|
|
/** @brief Queue a whole typed line, terminator included. */
|
|
static void push_line(const char *text)
|
|
{
|
|
size_t i = 0;
|
|
|
|
for ( i = 0; text[i] != '\0'; i++ ) {
|
|
push_text_key((int)(unsigned char)text[i], one_char((unsigned char)text[i]));
|
|
}
|
|
push_key('\r');
|
|
}
|
|
|
|
/** @brief Queue the event a window manager sends when the close button is hit. */
|
|
static void push_quit(void)
|
|
{
|
|
SDL_Event event;
|
|
|
|
memset(&event, 0, sizeof(event));
|
|
event.type = SDL_EVENT_QUIT;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
/**
|
|
* @brief Bring up a frontend whose mirror is a buffer this file can read.
|
|
*
|
|
* @param source A program to feed through the stdio half, or NULL to type it.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *start_frontend(const char *source)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
memset(OUTPUT, 0, sizeof(OUTPUT));
|
|
MIRROR = fmemopen(OUTPUT, sizeof(OUTPUT), "w");
|
|
FAIL_ZERO_RETURN(errctx, (MIRROR != NULL), AKERR_IO, "could not open the mirror buffer");
|
|
setvbuf(MIRROR, NULL, _IONBF, 0);
|
|
|
|
PROGRAM = NULL;
|
|
if ( source != NULL ) {
|
|
PROGRAM = fmemopen((void *)(uintptr_t)source, strlen(source), "r");
|
|
FAIL_ZERO_RETURN(errctx, (PROGRAM != NULL), AKERR_IO,
|
|
"could not open the program buffer");
|
|
}
|
|
|
|
PASS(errctx, akbasic_frontend_akgl_init(&FRONTEND, "akbasic frontend test",
|
|
WINDOW_W, WINDOW_H,
|
|
AKBASIC_TEST_C64_FONT,
|
|
AKBASIC_FRONTEND_FONT_SIZE,
|
|
MIRROR, PROGRAM));
|
|
PASS(errctx, akbasic_frontend_akgl_attach(&FRONTEND, &RUNTIME));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
static void stop_frontend(void)
|
|
{
|
|
akbasic_frontend_akgl_shutdown(&FRONTEND);
|
|
if ( MIRROR != NULL ) {
|
|
fclose(MIRROR);
|
|
MIRROR = NULL;
|
|
}
|
|
if ( PROGRAM != NULL ) {
|
|
fclose(PROGRAM);
|
|
PROGRAM = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief A program run through the frontend reaches stdout *and* the window.
|
|
*
|
|
* This is the assertion the whole frontend exists for, and it is two assertions
|
|
* on purpose: the mirrored bytes are what the golden corpus compares, and the
|
|
* lit pixels are what a user actually sees. Before the frontend existed an AKGL
|
|
* build produced the first and not the second.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_program_reaches_both(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
SDL_Surface *shot = NULL;
|
|
|
|
PASS(errctx, start_frontend("10 PRINT \"HELLO\"\n20 QUIT\n"));
|
|
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUNSTREAM));
|
|
PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME));
|
|
|
|
/* Byte for byte, exactly what the stdio-only driver would have written. */
|
|
TEST_REQUIRE_STR(OUTPUT, "HELLO\n");
|
|
/* And the same characters are in the grid the window draws from. */
|
|
TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "HELLO");
|
|
|
|
/*
|
|
* Asserted as "the first five cells have something in them and the sixth
|
|
* does not", rather than against particular pixels: which ones a glyph
|
|
* lights is FreeType's business. What is being proved is that five
|
|
* characters of Commodore font reached the renderer where the sink's cursor
|
|
* said they would.
|
|
*/
|
|
shot = SDL_RenderReadPixels(FRONTEND.renderer->sdl_renderer, NULL);
|
|
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
|
TEST_REQUIRE(anything_drawn(shot, 0, 0,
|
|
5 * FRONTEND.akglstate.cellw,
|
|
FRONTEND.akglstate.cellh),
|
|
"HELLO should have been drawn into the first five character cells");
|
|
TEST_REQUIRE(!anything_drawn(shot, 0,
|
|
3 * FRONTEND.akglstate.cellh,
|
|
WINDOW_W, FRONTEND.akglstate.cellh),
|
|
"nothing was printed on the fourth row, so nothing should be drawn there");
|
|
SDL_DestroySurface(shot);
|
|
|
|
stop_frontend();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The event pump is what carries a keystroke to the interpreter.
|
|
*
|
|
* Pushed into SDL's queue and read back through the frontend's own input
|
|
* backend, so every link in the chain the host owns is in the path: SDL queue,
|
|
* pump_events, akgl_controller_handle_event, the ring, the backend.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_pump_feeds_input(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool running = false;
|
|
bool available = false;
|
|
int keycode = 0;
|
|
|
|
PASS(errctx, start_frontend("10 QUIT\n"));
|
|
PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));
|
|
|
|
/* Nothing pumped yet: the ring is empty and that is success, not an error. */
|
|
PASS(errctx, FRONTEND.input.poll_key(&FRONTEND.input, &keycode, &available));
|
|
TEST_REQUIRE(!available, "an unpumped frontend should have no keys waiting");
|
|
|
|
push_key(SDLK_A);
|
|
PASS(errctx, akbasic_frontend_akgl_pump(&FRONTEND, &running));
|
|
TEST_REQUIRE(running, "a key event should not stop the frontend");
|
|
|
|
PASS(errctx, FRONTEND.input.poll_key(&FRONTEND.input, &keycode, &available));
|
|
TEST_REQUIRE(available, "the pump should have handed the key to the interpreter");
|
|
TEST_REQUIRE_INT(keycode, SDLK_A);
|
|
|
|
stop_frontend();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief Closing the window stops a program that would otherwise never stop.
|
|
*
|
|
* `10 GOTO 10` is the case that matters: an unbounded run() would own the
|
|
* process forever and the close button would do nothing. The bounded frame loop
|
|
* is what makes this test finish at all.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_window_close_stops(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool running = true;
|
|
|
|
PASS(errctx, start_frontend("10 GOTO 10\n"));
|
|
PASS(errctx, akbasic_runtime_load(&RUNTIME, "10 GOTO 10\n"));
|
|
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
|
|
|
|
push_quit();
|
|
PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME));
|
|
TEST_REQUIRE(!FRONTEND.running, "the close event should have stopped the frontend");
|
|
TEST_REQUIRE(RUNTIME.mode != AKBASIC_MODE_QUIT,
|
|
"closing the window stops the host, it does not QUIT the script");
|
|
|
|
/* And a pump after the close keeps reporting the same thing. */
|
|
PASS(errctx, akbasic_frontend_akgl_pump(&FRONTEND, &running));
|
|
TEST_REQUIRE(!running, "a closed frontend stays closed");
|
|
|
|
stop_frontend();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The line editor turns keystrokes into a line, echoed as it is typed.
|
|
*
|
|
* Read through the sink directly rather than through the REPL, because what is
|
|
* being asserted here is the editing itself: that the *composed text* is what
|
|
* gets typed rather than the keycode, the backspace, the escape, and the echo
|
|
* landing in the grid where the cursor says it is.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_line_editor(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
char line[64];
|
|
bool eof = true;
|
|
|
|
PASS(errctx, start_frontend(NULL));
|
|
PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));
|
|
|
|
/* "prXnt" with the X backspaced away. Lower case survives, which it did not
|
|
* before libakgl 0.3.0 -- the editor folded everything to upper case because
|
|
* a keycode was all it had. */
|
|
push_text_key('p', "p");
|
|
push_text_key('r', "r");
|
|
push_text_key('x', "x");
|
|
push_key(SDLK_BACKSPACE);
|
|
push_text_key('i', "i");
|
|
push_text_key('n', "n");
|
|
push_text_key('t', "t");
|
|
push_key('\r');
|
|
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE(!eof, "a submitted line is not end of input");
|
|
TEST_REQUIRE_STR(line, "print");
|
|
/* The echo is in the grid, and the backspace really removed a character. */
|
|
TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "print");
|
|
TEST_REQUIRE_INT(FRONTEND.akglstate.cursorrow, 1);
|
|
TEST_REQUIRE_INT(FRONTEND.akglstate.cursorcol, 0);
|
|
TEST_REQUIRE(!FRONTEND.akglstate.editing, "the editor should be idle once the line is in");
|
|
|
|
/*
|
|
* A shifted character, which is the whole point of the 0.3.0 keystroke API:
|
|
* the key is still SDLK_2 and the character is '"'. Without the composed
|
|
* text there is no way to tell those apart, and a BASIC string literal could
|
|
* not be typed at the window at all.
|
|
*/
|
|
push_text_key(SDLK_2, "\"");
|
|
push_text_key('h', "H");
|
|
push_text_key(SDLK_2, "\"");
|
|
push_key('\r');
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE_STR(line, "\"H\"");
|
|
|
|
/*
|
|
* And a key whose composed character has nothing to do with its keycode,
|
|
* which is what a non-US layout looks like: AZERTY's SDLK_Q types "a".
|
|
*/
|
|
push_text_key(SDLK_Q, "a");
|
|
push_key('\r');
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE_STR(line, "a");
|
|
|
|
/* A key that composes to nothing is not typed: an arrow is not a character. */
|
|
push_key(SDLK_UP);
|
|
push_key(SDLK_LEFT);
|
|
push_text_key('z', "z");
|
|
push_key('\r');
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE_STR(line, "z");
|
|
|
|
/* Escape abandons a line rather than submitting it. */
|
|
push_text_key('z', "z");
|
|
push_key(SDLK_ESCAPE);
|
|
push_text_key('o', "o");
|
|
push_text_key('k', "k");
|
|
push_key('\r');
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE_STR(line, "ok");
|
|
|
|
/* A closed window ends the wait, reported as end of input rather than raised. */
|
|
push_quit();
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE(eof, "closing the window during an INPUT is end of input");
|
|
|
|
stop_frontend();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief The frontend starts SDL text input, and the editor survives it not being on.
|
|
*
|
|
* **This is the regression test for a bug the rest of this file could not catch.**
|
|
* Every other test here pushes SDL_EVENT_TEXT_INPUT into the queue by hand,
|
|
* which is what a real keyboard produces -- but only once text input has been
|
|
* *started*. SDL3 has it off by default and per-window, so a host that never
|
|
* calls SDL_StartTextInput() gets no such events at all, every keystroke reaches
|
|
* the ring with an empty `text`, and an editor that treats that as "not a
|
|
* character" is silently dead. That is exactly what shipped: no echo in the
|
|
* window, and nothing on stdout either, because RUN could never be typed.
|
|
*
|
|
* Two assertions, because there are two independent things wrong with that:
|
|
* the frontend must turn text input on, and the editor must not be helpless if
|
|
* it is ever off.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_text_input_is_started(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
char line[64];
|
|
bool eof = true;
|
|
|
|
PASS(errctx, start_frontend(NULL));
|
|
|
|
/* The frontend's job: SDL will not emit a text-input event without this. */
|
|
TEST_REQUIRE(SDL_TextInputActive(FRONTEND.window),
|
|
"the frontend must start SDL text input, or nothing can be typed");
|
|
|
|
/*
|
|
* The editor's job: keystrokes carrying *no* composed text still type,
|
|
* folded to upper case, which is all a bare keycode can offer. push_key()
|
|
* sends the key-down alone, so this is precisely the shape the broken build
|
|
* saw for every single key.
|
|
*/
|
|
PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));
|
|
push_key('r');
|
|
push_key('u');
|
|
push_key('n');
|
|
push_key('\r');
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE(!eof, "a text-less line is still a line");
|
|
TEST_REQUIRE_STR(line, "RUN");
|
|
TEST_REQUIRE_STR(FRONTEND.akglstate.text[0], "RUN");
|
|
|
|
/* And a text-less key that is not printable is still not a character. */
|
|
push_key(SDLK_F1);
|
|
push_key(SDLK_RIGHT);
|
|
push_key('a');
|
|
push_key('\r');
|
|
PASS(errctx, FRONTEND.akglsink.readline(&FRONTEND.akglsink, line, sizeof(line), &eof));
|
|
TEST_REQUIRE_STR(line, "A");
|
|
|
|
stop_frontend();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/**
|
|
* @brief A whole REPL session typed at the window, ending with QUIT.
|
|
*
|
|
* The end-to-end case: keystrokes into SDL's queue, out through the editor, into
|
|
* the interpreter, and the result on both output paths. Nothing here is mocked
|
|
* except the keyboard and the mirror.
|
|
*/
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_repl_session(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, start_frontend(NULL));
|
|
PASS(errctx, FRONTEND.input.flush_keys(&FRONTEND.input));
|
|
|
|
/*
|
|
* Every keystroke queued up front, and the session ended with a typed QUIT
|
|
* rather than a close event: the pump drains the whole SDL queue in one
|
|
* call, so a close event queued here would be seen before the first
|
|
* keystroke was ever read.
|
|
*/
|
|
push_line("10 print 1");
|
|
push_line("run");
|
|
push_line("quit");
|
|
|
|
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_REPL));
|
|
PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME));
|
|
|
|
TEST_REQUIRE_INT(RUNTIME.mode, AKBASIC_MODE_QUIT);
|
|
/* The reference prints READY at the prompt and after a RUN. */
|
|
TEST_REQUIRE_STR(OUTPUT, "READY\n1\nREADY\n");
|
|
|
|
stop_frontend();
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Every entry point validates its pointers before touching SDL. */
|
|
static akerr_ErrorContext AKERR_NOIGNORE *test_arguments_refused(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
bool running = false;
|
|
|
|
TEST_REQUIRE_STATUS(akbasic_frontend_akgl_init(NULL, "t", WINDOW_W, WINDOW_H,
|
|
AKBASIC_TEST_C64_FONT, 16, NULL, NULL),
|
|
AKERR_NULLPOINTER);
|
|
TEST_REQUIRE_STATUS(akbasic_frontend_akgl_init(&FRONTEND, "t", WINDOW_W, WINDOW_H,
|
|
NULL, 16, NULL, NULL),
|
|
AKERR_NULLPOINTER);
|
|
/* A zero-point font would divide by zero measuring the character grid. */
|
|
TEST_REQUIRE_STATUS(akbasic_frontend_akgl_init(&FRONTEND, "t", WINDOW_W, WINDOW_H,
|
|
AKBASIC_TEST_C64_FONT, 0, NULL, NULL),
|
|
AKBASIC_ERR_VALUE);
|
|
TEST_REQUIRE_STATUS(akbasic_frontend_akgl_attach(NULL, &RUNTIME), AKERR_NULLPOINTER);
|
|
TEST_REQUIRE_STATUS(akbasic_frontend_akgl_pump(NULL, &running), AKERR_NULLPOINTER);
|
|
TEST_REQUIRE_STATUS(akbasic_frontend_akgl_drive(&FRONTEND, NULL), AKERR_NULLPOINTER);
|
|
/* Shutting down something that was never brought up is not an error. */
|
|
akbasic_frontend_akgl_shutdown(NULL);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_error_init());
|
|
CATCH(errctx, akbasic_error_register());
|
|
|
|
CATCH(errctx, test_arguments_refused());
|
|
CATCH(errctx, test_program_reaches_both());
|
|
CATCH(errctx, test_pump_feeds_input());
|
|
CATCH(errctx, test_window_close_stops());
|
|
CATCH(errctx, test_line_editor());
|
|
CATCH(errctx, test_text_input_is_started());
|
|
CATCH(errctx, test_repl_session());
|
|
} CLEANUP {
|
|
stop_frontend();
|
|
} PROCESS(errctx) {
|
|
} HANDLE_DEFAULT(errctx) {
|
|
LOG_ERROR_WITH_MESSAGE(errctx, "akgl frontend test failed");
|
|
akbasic_test_failures += 1;
|
|
/*
|
|
* FINISH_NORETURN rather than FINISH, matching tests/akgl_backends.c:
|
|
* FINISH expands a `return __err_context` that this int-returning
|
|
* function cannot compile even where the branch is unreachable.
|
|
* HANDLE_DEFAULT has already marked the context handled, so nothing
|
|
* aborts.
|
|
*/
|
|
} FINISH_NORETURN(errctx);
|
|
|
|
return akbasic_test_failures;
|
|
}
|