135 lines
5.0 KiB
C
135 lines
5.0 KiB
C
|
|
/**
|
||
|
|
* @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;
|
||
|
|
}
|