/** * @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 #include #include #include #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"); } /* * A half that has a character grid. The two stdio sinks above have neither * `moveto` nor `window`, which is exactly why they cannot show that either one * is forwarded -- so this stands in for the akgl sink, without the SDL this file * deliberately does not link. */ static int MOVETOS = 0; static int WINDOWS = 0; static int WINDOW_ARGS[4] = { 0, 0, 0, 0 }; static akerr_ErrorContext AKERR_NOIGNORE *counting_moveto(akbasic_TextSink *self, int col, int row) { PREPARE_ERROR(errctx); (void)self; (void)col; (void)row; MOVETOS += 1; SUCCEED_RETURN(errctx); } static akerr_ErrorContext AKERR_NOIGNORE *counting_window(akbasic_TextSink *self, int left, int top, int right, int bottom) { PREPARE_ERROR(errctx); (void)self; WINDOWS += 1; WINDOW_ARGS[0] = left; WINDOW_ARGS[1] = top; WINDOW_ARGS[2] = right; WINDOW_ARGS[3] = bottom; SUCCEED_RETURN(errctx); } int main(void) { akbasic_TextSink primary; akbasic_StdioSink primarystate; akbasic_TextSink mirror; akbasic_StdioSink mirrorstate; akbasic_TextSink broken; akbasic_TextSink gridded; 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); /* * `moveto` and `window` are offered only when a half can do them, so that a * `CHAR` or a `WINDOW` against a stdio-only pair still refuses by name * rather than appearing to work. Two stdio halves have neither. */ TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &mirror, NULL)); TEST_REQUIRE(tee.moveto == NULL, "a tee over two grid-less sinks must not offer moveto"); TEST_REQUIRE(tee.window == NULL, "a tee over two grid-less sinks must not offer window"); /* * And when a half *does* have them, they are forwarded. `window` used to be * the one entry point this function never wired, which made WINDOW * unreachable in the only build that has a character grid: the standalone * frontend runs the interpreter against a tee, so the akgl sink's fully * implemented window() answered nothing and the verb refused with "needs a * text device with a character grid". TODO.md section 6 item 31. */ gridded.self = &gridded; gridded.write = broken_write; gridded.writeln = broken_write; gridded.readline = NULL; gridded.clear = broken_clear; gridded.moveto = counting_moveto; gridded.window = counting_window; MOVETOS = 0; WINDOWS = 0; TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &primary, &gridded, NULL)); TEST_REQUIRE(tee.moveto != NULL, "a tee with a gridded half must offer moveto"); TEST_REQUIRE(tee.window != NULL, "a tee with a gridded half must offer window"); TEST_REQUIRE_OK(tee.moveto(&tee, 3, 4)); TEST_REQUIRE_OK(tee.window(&tee, 0, 20, 39, 24)); TEST_REQUIRE_INT(MOVETOS, 1); TEST_REQUIRE_INT(WINDOWS, 1); TEST_REQUIRE_INT(WINDOW_ARGS[0], 0); TEST_REQUIRE_INT(WINDOW_ARGS[1], 20); TEST_REQUIRE_INT(WINDOW_ARGS[2], 39); TEST_REQUIRE_INT(WINDOW_ARGS[3], 24); /* The gridded half being first is wired the same way round. */ WINDOWS = 0; TEST_REQUIRE_OK(akbasic_sink_init_tee(&tee, &teestate, &gridded, &primary, NULL)); TEST_REQUIRE(tee.window != NULL, "the gridded half may be either one"); TEST_REQUIRE_OK(tee.window(&tee, 1, 2, 3, 4)); TEST_REQUIRE_INT(WINDOWS, 1); /* * 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; }