Port the standalone SDL frontend, and give the sink a line editor
An AKGL build of `basic` was a terminal program with unused SDL linked into it. It now opens the reference's 800x600 window, draws BASIC output in the Commodore font, pumps events, lets you type at it, and still mirrors every byte to stdout. The stdout mirror is a composing sink rather than a second write inside the interpreter, and lives in the core library where it needs no SDL. The line editor waits for a typed line by borrowing one frame at a time from the host, so nothing blocks and nothing owns an event loop it should not. The whole golden corpus now runs through the SDL binary as well as the stdio one, byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
134
tests/sink_tee.c
Normal file
134
tests/sink_tee.c
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file sink_tee.c
|
||||
* @brief Tests the composing sink that mirrors output to a second sink.
|
||||
*
|
||||
* This is the piece that lets an AKGL build draw BASIC output in a window and
|
||||
* still put it on stdout, without the interpreter carrying a second write. Two
|
||||
* stdio sinks over fmemopen buffers stand in for the two halves here -- the
|
||||
* composition is what is being tested, not what either half does with the bytes,
|
||||
* and using two of the same kind keeps this file free of SDL.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/sink.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
static char PRIMARY[1024];
|
||||
static char MIRROR[1024];
|
||||
|
||||
/** @brief A sink whose every entry point fails, for asserting the pair stops. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *broken_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 *broken_clear(akbasic_TextSink *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
FAIL_RETURN(errctx, AKERR_IO, "this sink always fails");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akbasic_TextSink primary;
|
||||
akbasic_StdioSink primarystate;
|
||||
akbasic_TextSink mirror;
|
||||
akbasic_StdioSink mirrorstate;
|
||||
akbasic_TextSink broken;
|
||||
akbasic_TextSink tee;
|
||||
akbasic_TeeSink teestate;
|
||||
FILE *primaryout = NULL;
|
||||
FILE *mirrorout = NULL;
|
||||
FILE *in = NULL;
|
||||
char line[64];
|
||||
bool eof = false;
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_error_register());
|
||||
|
||||
memset(PRIMARY, 0, sizeof(PRIMARY));
|
||||
memset(MIRROR, 0, sizeof(MIRROR));
|
||||
primaryout = fmemopen(PRIMARY, sizeof(PRIMARY), "w");
|
||||
mirrorout = fmemopen(MIRROR, sizeof(MIRROR), "w");
|
||||
in = fmemopen((void *)(uintptr_t)"typed\n", 6, "r");
|
||||
TEST_REQUIRE(primaryout != NULL && mirrorout != NULL && in != NULL,
|
||||
"could not open the test buffers");
|
||||
setvbuf(primaryout, NULL, _IONBF, 0);
|
||||
setvbuf(mirrorout, NULL, _IONBF, 0);
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_sink_init_stdio(&primary, &primarystate, primaryout, in));
|
||||
TEST_REQUIRE_OK(akbasic_sink_init_stdio(&mirror, &mirrorstate, mirrorout, NULL));
|
||||
|
||||
/* A reader that is neither half is a wiring mistake, and is refused. */
|
||||
TEST_REQUIRE_STATUS(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, &broken),
|
||||
AKBASIC_ERR_VALUE);
|
||||
TEST_REQUIRE_STATUS(akbasic_sink_init_tee(&tee, &teestate, &primary, NULL, NULL),
|
||||
AKERR_NULLPOINTER);
|
||||
TEST_REQUIRE_STATUS(akbasic_sink_init_tee(NULL, &teestate, &primary, &mirror, NULL),
|
||||
AKERR_NULLPOINTER);
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, &primary));
|
||||
|
||||
/* Every write reaches both halves, byte for byte, including the newline. */
|
||||
TEST_REQUIRE_OK(tee.write(&tee, "AB"));
|
||||
TEST_REQUIRE_OK(tee.writeln(&tee, "CD"));
|
||||
TEST_REQUIRE_STR(PRIMARY, "ABCD\n");
|
||||
TEST_REQUIRE_STR(MIRROR, "ABCD\n");
|
||||
|
||||
/* The error-line double newline survives the composition unchanged. */
|
||||
TEST_REQUIRE_OK(tee.writeln(&tee, "? 20 : RUNTIME ERROR something\n"));
|
||||
TEST_REQUIRE_STR(MIRROR, "ABCD\n? 20 : RUNTIME ERROR something\n\n");
|
||||
|
||||
/* clear reaches both; the stdio half has no screen, so it just succeeds. */
|
||||
TEST_REQUIRE_OK(tee.clear(&tee));
|
||||
|
||||
/* readline comes from the designated reader only. */
|
||||
TEST_REQUIRE_OK(tee.readline(&tee, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE(!eof, "the reader had a line to give");
|
||||
TEST_REQUIRE_STR(line, "typed");
|
||||
TEST_REQUIRE_OK(tee.readline(&tee, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE(eof, "running off the reader's end must set eof, not raise");
|
||||
|
||||
/* With no reader at all, readline reports end of input rather than raising. */
|
||||
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, NULL));
|
||||
eof = false;
|
||||
TEST_REQUIRE_OK(tee.readline(&tee, line, sizeof(line), &eof));
|
||||
TEST_REQUIRE(eof, "a tee with no reader reports EOF");
|
||||
TEST_REQUIRE_STR(line, "");
|
||||
TEST_REQUIRE_STATUS(tee.readline(&tee, NULL, sizeof(line), &eof), AKERR_NULLPOINTER);
|
||||
|
||||
/*
|
||||
* A failing half stops the pair rather than being swallowed. A half-written
|
||||
* line is worse than a refused one: the golden corpus compares whole files.
|
||||
*/
|
||||
broken.self = &broken;
|
||||
broken.write = broken_write;
|
||||
broken.writeln = broken_write;
|
||||
broken.readline = NULL;
|
||||
broken.clear = broken_clear;
|
||||
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &broken, NULL));
|
||||
TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_IO);
|
||||
TEST_REQUIRE_STATUS(tee.writeln(&tee, "X"), AKERR_IO);
|
||||
TEST_REQUIRE_STATUS(tee.clear(&tee), AKERR_IO);
|
||||
|
||||
TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &broken, &primary, NULL));
|
||||
TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_IO);
|
||||
|
||||
/* A sink whose state pointer was never set is caught rather than dereferenced. */
|
||||
tee.self = NULL;
|
||||
TEST_REQUIRE_STATUS(tee.write(&tee, "X"), AKERR_NULLPOINTER);
|
||||
|
||||
fclose(primaryout);
|
||||
fclose(mirrorout);
|
||||
fclose(in);
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
Reference in New Issue
Block a user