diff --git a/.gitignore b/.gitignore index a1359c7..239c5c7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ **/pforth **/pforth.dic **/pforth_standalone +**/pfqaterm + platform/win32/**/.vs platform/win32/**/Debug platform/win32/**/Release diff --git a/csrc/pf_io.c b/csrc/pf_io.c index 8491771..1245cc7 100644 --- a/csrc/pf_io.c +++ b/csrc/pf_io.c @@ -90,7 +90,7 @@ cell_t ioKey( void ) /************************************************************** ** Receive line from keyboard. -** Return number of characters enterred. +** Return number of characters entered. */ #define SPACE (0x20) #define BACKSPACE (0x08) diff --git a/csrc/pf_io.h b/csrc/pf_io.h index 242e9e6..2126523 100644 --- a/csrc/pf_io.h +++ b/csrc/pf_io.h @@ -30,13 +30,54 @@ #ifdef __cplusplus extern "C" { #endif + +/** Send a character to an output device so it can be + * seen by the user. + * + * On posix systems, this could be implemented using putchar(c). + */ int sdTerminalOut( char c ); + +/** Echo a character to an output device if sdTerminalIn does NOT + * already automatically echo characters. + * If sdTerminalIn() echos characters then this could be a NOOP. + */ int sdTerminalEcho( char c ); + +/** + * Flush any characters that may be sitting in an output buffer. + * If sdTerminalFlush() is not called then output characters may be held + * indefinitely. + */ int sdTerminalFlush( void ); + +/** Return immediately with a character from the terminal if is is available. + * If not available then wait for the user to press a key. + * + * On posix systems, this could be implemented using getchar(). + */ int sdTerminalIn( void ); + +/** + * Return 1 if a character is available to be read using sdTerminalIn(). + */ int sdQueryTerminal( void ); + +/** + * Initialize the terminal IO system. + */ void sdTerminalInit( void ); + +/** + * Cleanup the terminal IO system. + * If you call sdTerminalInit() twice and sdTerminalTerm() once then + * the input system will be terminated. + */ void sdTerminalTerm( void ); + +/** + * Sleep for msec milliseconds. + */ cell_t sdSleepMillis( cell_t msec ); #ifdef __cplusplus } diff --git a/csrc/pfqaterm.c b/csrc/pfqaterm.c new file mode 100644 index 0000000..726d248 --- /dev/null +++ b/csrc/pfqaterm.c @@ -0,0 +1,310 @@ +/* @(#) pfqaterm.c 26/07/28 1.0 */ +/*************************************************************** +** Interactive test for the pForth terminal I/O subsystem. +** +** The terminal API is defined in "pf_io.h". A developer who +** implements that API for a new platform can run this program +** to check that the character I/O behaves as pForth expects. +** +** This test is interactive. It must be run from a terminal +** because it asks the user to press a key. +** +** Author: Phil Burk +** Copyright 1994 3DO, Phil Burk, Larry Polansky, David Rosenboom +** +** Permission to use, copy, modify, and/or distribute this +** software for any purpose with or without fee is hereby granted. +** +** THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +** WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +** WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +** THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +** CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING +** FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +** CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +** OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +** +***************************************************************/ + +#include +#include +#include +#include + +#include "pf_all.h" +#include "paging/unittest.h" + +PFQA_INSTANTIATE_GLOBALS; + +/* How long to nap between calls to sdQueryTerminal(). */ +#define POLL_PERIOD_MSEC (50) +/* How long to wait for the user before giving up. */ +#define TIMEOUT_MSEC (10000) +#define MAX_POLLS (TIMEOUT_MSEC / POLL_PERIOD_MSEC) + +/* sdTerminalIn() should not block because a character is already +** available. time() only has one second of resolution so allow for +** the second to tick over while we are calling it. */ +#define MAX_READ_SECONDS (1) + +/* How many characters must be buffered while the program is asleep. */ +#define NUM_BUFFERED_CHARS (5) +/* How long the user has to type them. */ +#define TYPING_TIME_MSEC (5000) + +#define MYEOL "\r\n" + +/*************************************************************** +** Print a message using the terminal API under test. +*/ +static void TermPrint( const char *msg ) +{ + const char *s = msg; + while( *s ) + { + sdTerminalOut( *s++ ); + } + sdTerminalFlush(); +} + +/** Print a message and a line terminator. + */ +static void TermPrintLine( const char *msg ) +{ + TermPrint(msg); + TermPrint(MYEOL); + sdTerminalFlush(); +} + +/*************************************************************** +** Throw away any characters that are already waiting so that +** stale input cannot satisfy the test. +** The loop is bounded because sdQueryTerminal() stays true forever +** if stdin is at the end of a file or a closed pipe. +*/ +#define MAX_DRAIN (100) +static void DrainTerminal( void ) +{ + int i; + for( i = 0; i < MAX_DRAIN; i++ ) + { + if( sdQueryTerminal() == FFALSE ) break; + if( sdTerminalIn() == EOF ) break; + } +} + +/*************************************************************** +** Test sdQueryTerminal(), sdTerminalIn() and sdSleepMillis(). +*/ +static void pfQaQueryTerminal( void ) +{ + int numPolls; + int query; + int c; + time_t startTime; + time_t stopTime; + + DrainTerminal(); + +/* Nothing has been typed yet so the terminal should be quiet. */ + ASSERT_EQ( FFALSE, sdQueryTerminal() ); + + ASSERT_EQ( 0, sdSleepMillis( POLL_PERIOD_MSEC ) ); + + TermPrintLine(MYEOL "----- pfQaQueryTerminal"); + TermPrintLine( "Please press a letter key." ); + +/* Poll until a character arrives or we run out of patience. */ + for( numPolls = 0; numPolls < MAX_POLLS; numPolls++ ) + { + query = sdQueryTerminal(); + if( query != FFALSE ) break; + sdSleepMillis( POLL_PERIOD_MSEC ); + } + + if( numPolls >= MAX_POLLS ) + { + TermPrintLine( "TIMED OUT! No key was pressed." ); + ASSERT_LT( numPolls, MAX_POLLS ); + } + +/* sdQueryTerminal() must not consume the character. */ + ASSERT_NE( FFALSE, sdQueryTerminal() ); + ASSERT_NE( FFALSE, sdQueryTerminal() ); + +/* A character is waiting so this should return immediately. */ + startTime = time( NULL ); + c = sdTerminalIn(); + stopTime = time( NULL ); + ASSERT_LE( stopTime - startTime, MAX_READ_SECONDS ); + +/* Did we get the key that the user pressed? */ + ASSERT_NE( EOF, c ); + EXPECT_TRUE( isalpha( c ) ); + TermPrint( "You pressed '" ); + sdTerminalOut( (char) c ); + TermPrintLine( "'" ); + +/* The character was consumed so the terminal should be quiet again. */ + ASSERT_EQ( FFALSE, sdQueryTerminal() ); + +error: + return; +} + +/*************************************************************** +** Test that characters typed while the program is not looking +** are buffered by the terminal instead of being dropped. +*/ +static void pfQaTerminalBuffering( void ) +{ + int i; + int c; + char typed[NUM_BUFFERED_CHARS + 1]; + time_t startTime; + time_t stopTime; + + DrainTerminal(); + +/* Nothing has been typed yet so the terminal should be quiet. */ + ASSERT_EQ( FFALSE, sdQueryTerminal() ); + + TermPrintLine(MYEOL "----- pfQaTerminalBuffering"); + TermPrintLine( "Please type any 5 letters within the next 5 seconds." ); + TermPrintLine( "They may not be echoed." ); + +/* Ignore the terminal completely while the user types. */ + ASSERT_EQ( 0, sdSleepMillis( TYPING_TIME_MSEC ) ); + +/* All 5 characters should have been buffered while we were asleep. */ + startTime = time( NULL ); + for( i = 0; i < NUM_BUFFERED_CHARS; i++ ) + { + ASSERT_NE( FFALSE, sdQueryTerminal() ); + c = sdTerminalIn(); + ASSERT_NE( EOF, c ); + EXPECT_TRUE( isalpha( c ) ); + typed[i] = (char) c; + } + typed[NUM_BUFFERED_CHARS] = '\0'; + +/* The characters were already waiting so none of the reads should block. */ + stopTime = time( NULL ); + ASSERT_LE( stopTime - startTime, MAX_READ_SECONDS ); + + TermPrint( "You typed '" ); + TermPrint( typed ); + TermPrintLine( "'" ); + +/* We read every character so the terminal should be quiet again. */ + ASSERT_EQ( FFALSE, sdQueryTerminal() ); + +error: + return; +} + +/*************************************************************** +** Test that characters echoed while the user is typing. +*/ +static void pfQaTerminalEcho( void ) +{ + int i; + int c; + + DrainTerminal(); + +/* Nothing has been typed yet so the terminal should be quiet. */ + ASSERT_EQ( FFALSE, sdQueryTerminal() ); + + TermPrintLine(MYEOL "----- pfQaTerminalEcho"); + TermPrintLine( "Please type 5 letters or numbers at any speed:" ); + +/* All 5 characters should have been buffered while we were asleep. */ + for( i = 0; i < NUM_BUFFERED_CHARS; i++ ) + { + c = sdTerminalIn(); + sdTerminalEcho(c); + } + TermPrintLine(""); + TermPrintLine("You should have seen each key echo once and only once.\n"); + +error: + return; +} + +/*************************************************************** +** Test for overflow when blasting characters. +*/ +static void pfQaTerminalOverflow( void ) +{ + int i; + int c; + const char text[] = "abcdefghijklmnopqrstuvwxyz1234567890"; + char typed[sizeof(text)]; + int enabled = 1; + + DrainTerminal(); + +/* Nothing has been typed yet so the terminal should be quiet. */ + ASSERT_EQ( FFALSE, sdQueryTerminal() ); + + TermPrintLine(MYEOL "----- pfQaTerminalOverflow"); + TermPrintLine("Testing for terminal input buffer overflow."); + TermPrintLine("Copy the string below, with no spaces, to your Paste buffer."); + TermPrint(MYEOL " "); + TermPrintLine(text); + + TermPrintLine(MYEOL "Now Paste the String into the terminal and hit ENTER or RETURN"); + TermPrintLine("as many times as you like."); + TermPrintLine("When you are done, hit '.' and ENTER or RETURN to stop."); + + while (enabled) { + for( i = 0; i < sizeof(typed); i++ ) typed[i] = 0; /* Clear typed buffer. */ + for( i = 0; i < (int)(sizeof(text) - 1); i++ ) + { + c = sdTerminalIn(); + sdTerminalEcho(c); + if (c == '.') { + TermPrintLine(MYEOL "Stop requested."); + enabled = 0; + break; + } + typed[i] = c; + /* TermPrint(" - got "); TermPrintLine(typed); */ + if (c != text[i]) { + TermPrintLine(MYEOL "Dropped character"); + TermPrintLine(typed); + DrainTerminal(); + enabled = 0; + ASSERT_EQ(text[i], c); + break; + } + } + TermPrintLine(MYEOL "Waiting for ENTER or RETURN."); + c = sdTerminalIn(); /* Discard EOL */ + ASSERT_TRUE(!isalpha(c)); + if (enabled) { + TermPrintLine(MYEOL "SUCCESS - Paste again and hit ENTER or RETURN."); + } + } + + TermPrintLine("pfQaTerminalOverflow test complete."); + +error: + return; +} + +/***************************************************************/ +int main( void ) +{ + sdTerminalInit(); + TermPrintLine( "Terminal QA for pForth" ); + pfQaQueryTerminal(); + pfQaTerminalBuffering(); + pfQaTerminalEcho(); + pfQaTerminalOverflow(); + sdTerminalTerm(); + + PFQA_PRINT_RESULT; + return PFQA_EXIT_RESULT; +} diff --git a/csrc/posix/pf_io_posix.c b/csrc/posix/pf_io_posix.c index b4c518d..1fb96e2 100644 --- a/csrc/posix/pf_io_posix.c +++ b/csrc/posix/pf_io_posix.c @@ -135,6 +135,15 @@ void sdTerminalInit(void) { perror("sdTerminalInit: setvbuf"); } +/* Do not let stdio buffer input. sdTerminalIn() uses getchar(), which would + * read a whole block of characters into the stdio buffer, but sdQueryTerminal() + * asks the file descriptor. Characters sitting in the stdio buffer would then + * be invisible to sdQueryTerminal(). + */ + if (setvbuf(stdin, NULL, _IONBF, (size_t) 0) != 0) + { + perror("sdTerminalInit: setvbuf"); + } } } diff --git a/platforms/unix/Makefile b/platforms/unix/Makefile index 67f9131..fb43709 100644 --- a/platforms/unix/Makefile +++ b/platforms/unix/Makefile @@ -42,6 +42,7 @@ FTHDIR = $(PFORTHDIR)/fth BUILDDIR = $(shell pwd) PFDICAPP = pforth +PFQATERMAPP = pfqaterm PFORTHDIC = pforth.dic PFDICDAT = pfdicdat.h PFORTHAPP = pforth_standalone @@ -98,6 +99,9 @@ PFBASESOURCE = pf_cglue.c pf_clib.c pf_core.c pf_inner.c \ paging/pagedmem.c paging/lockpage.c paging/qadmpage.c PFSOURCE = pf_main.c $(PFBASESOURCE) $(IO_SOURCE) +# Interactive QA test for the terminal I/O API defined in "pf_io.h". +PFQATERMSOURCE = pfqaterm.c $(IO_SOURCE) + VPATH = .:$(CSRCDIR):$(CSRCDIR)/posix:$(CSRCDIR)/stdio:$(CSRCDIR)/win32_console:$(CSRCDIR)/win32 XCFLAGS = $(CCOPTS) @@ -114,6 +118,7 @@ LINK = $(CC) $(LDFLAGS) $(ASANOPTS) .SUFFIXES: .c .o .eo PFOBJS = $(PFSOURCE:.c=.o) +PFQATERMOBJS = $(PFQATERMSOURCE:.c=.o) PFEMBOBJS = $(PFSOURCE:.c=.eo) %.o: %.c $(PFINCLUDES) @@ -141,6 +146,12 @@ pffiles: $(PFDICAPP): $(PFINCLUDES) $(PFOBJS) $(LINK) -o $@ $(PFOBJS) $(LDADD) -lm +# Build the interactive terminal I/O test. +$(PFQATERMAPP): $(PFINCLUDES) $(PFQATERMOBJS) + $(LINK) -o $@ $(PFQATERMOBJS) $(LDADD) + @echo "" + @echo "Run './$(PFQATERMAPP)' from a terminal to test the pForth I/O subsystem." + # Build basic dictionary image by running newly built pforth and including "system.fth". $(PFORTHDIC): $(PFDICAPP) wd=$$(pwd); (cd $(FTHDIR); "$${wd}/$(PFDICAPP)" -i system.fth) @@ -173,6 +184,7 @@ help: @echo " pforthdic = executable pforth plus pforth.dic file" @echo " pfdicdat = header image of full dictionary build by compiling Forth code." @echo " pforthapp = executable with embedded dictionary image. DEFAULT 'all' target." + @echo " pfqaterm = interactive test for the terminal I/O API in 'pf_io.h'." @echo "" @echo " The file 'fth/pfdicdat.h' is generated by pForth. It contains a binary image of the Forth dictionary." @echo " It allows pForth to work as a standalone image that does not need to load a dictionary file." @@ -197,3 +209,4 @@ clean: rm -f $(PFDICDAT) $(FTHDIR)/$(PFDICDAT) $(CSRCDIR)/$(PFDICDAT) rm -f $(PFORTHDIC) $(FTHDIR)/$(PFORTHDIC) rm -f $(PFDICAPP) + rm -f $(PFQATERMOBJS) $(PFQATERMAPP)