diff --git a/dev/make-window-cmd-tests.sh b/dev/make-window-cmd-tests.sh new file mode 100755 index 0000000000..b2f1d57a74 --- /dev/null +++ b/dev/make-window-cmd-tests.sh @@ -0,0 +1,435 @@ +#!/usr/bin/env bash + +set -e + +# Regenerate the window-cmd-*.g tests in tst/testspecial. Run this, then +# tst/testspecial/regenerate_tests.sh to update the expected output. +# +# It lives in dev/ because it is a maintenance tool, needed only to change the +# tests and not to run them, and dev/ is not shipped in a release; the tests it +# writes are committed. +# +# A test here is fed to GAP on stdin. When 'WindowCmd' is evaluated the +# kernel writes '@w+' to stdout and then reads the window handler's +# answer back from stdin, so the answer bytes simply follow the newline of the +# line that called 'WindowCmd'. With stdin redirected GAP reads input one byte +# at a time and never reads ahead past a newline -- 'syBuf[0].bufno' is -1, so +# 'syGetchNonTerm' asks for a single character at a time -- and the answer is +# therefore still in the stream when the kernel asks for it. No window handler +# is involved, and the kernel never interprets the three character command +# name, so "TST" needs no support anywhere. +# +# The answers are written out by hand, which is fiddly enough (lengths, digit +# order, escape counts) to be worth generating rather than editing in place. + +# SRCDIR is the top of the source tree, found from where this script lives. +SRCDIR=$(cd "$(dirname "$0")/.." && pwd) + +# GAPDIR points to the directory containing the gap executable +# (so for out-of-tree builds, builddir and not srcdir). Resolve it while we +# are still in the directory the user invoked us from, as we change directory +# below and a relative GAPDIR would then mean something else entirely. +GAPDIR=$(cd "${GAPDIR:-$SRCDIR}" && pwd) + +# The refill buffer size the tests are built around is the kernel's, so take +# it from the kernel rather than keeping a second copy in step by hand. +WCBUFSIZE=$(sed -n 's/^#define SYS_WIN_BUF_SIZE *\([0-9][0-9]*\).*/\1/p' \ + "$SRCDIR/src/sysfiles.c") +if [ -z "${WCBUFSIZE}" ]; then + echo "$0: cannot find SYS_WIN_BUF_SIZE in $SRCDIR/src/sysfiles.c" >&2 + exit 1 +fi +export WCBUFSIZE + +# The tests are written to the working directory, so run GAP from where they +# belong; this also keeps the GAP program below free of any path handling. +cd "$SRCDIR/tst/testspecial" + +"$GAPDIR/gap" -A -b -q -r <<'GAPEOF' + +# every count in this protocol is written least significant digit first +WCRev := n -> Reversed(String(n));; + +# '@' doubles, a control character becomes '@' and the corresponding letter +WCEsc := function(str) + local out, c, i; + out := ""; + for c in str do + i := INT_CHAR(c); + if c = '@' then + Append(out, "@@"); + elif 1 <= i and i <= 26 then + Add(out, '@'); + Add(out, CHAR_INT(i - 1 + INT_CHAR('A'))); + else + Add(out, c); + fi; + od; + return out; +end;; + +WCInt := function(n) + local sign; + if n < 0 then sign := "-"; else sign := "+"; fi; + return Concatenation("I", WCRev(AbsInt(n)), sign); +end;; + +# the digits give the un-escaped length, so they need not match the bytes +WCStr := s -> Concatenation("S", WCRev(Length(s)), "+", WCEsc(s));; + +# an answer is '@a+' +WCAnswer := function(entries) + local payload; + payload := Concatenation(entries); + return Concatenation("@a", WCRev(Length(payload)), "+", payload); +end;; + +# payload offset at which the data of a string entry of length starts, +# given the entries in front of it; used to aim an escape at a refill boundary +WCDataOffset := {before, len} -> + Length(Concatenation(before)) + 1 + Length(WCRev(len)) + 1;; + +WCRep := {block, n} -> Concatenation(ListWithIdenticalEntries(n, block));; + +# The kernel serves the payload through a refill buffer of this size, read +# out of src/sysfiles.c by the wrapper so that the two cannot drift apart. +WCBufSize := Int(GAPInfo.SystemEnvironment.WCBUFSIZE);; + +# Check that really does put an escape pair across the first refill +# boundary: the '@' as the last byte the kernel can serve from its first +# bufferful, its partner as the first byte of the next. Recomputing this from +# the finished bytes is the point -- deriving it from the same arithmetic that +# placed it would assert nothing. +WCCheckStraddle := function(answer) + local payload, at, run; + + payload := answer{[Position(answer, '+') + 1 .. Length(answer)]}; + if Length(payload) <= WCBufSize then + Error("payload of ", Length(payload), " bytes never reaches the ", + WCBufSize, " byte refill boundary"); + fi; + # 0 based payload offset WCBufSize-1 is 1 based position WCBufSize + at := WCBufSize; + if payload[at] <> '@' or payload[at + 1] <> '@' then + Error("no '@@' pair at the refill boundary: found ", + payload{[at .. at + 1]}); + fi; + # and it must open a pair, not close one, so the run before it must be even + run := 0; + while at - run - 1 >= 1 and payload[at - run - 1] = '@' do + run := run + 1; + od; + if run mod 2 <> 0 then + Error("the '@' at the refill boundary closes an earlier pair"); + fi; +end;; + +# The header every generated test opens with, given its one line description. +# The '#GAPOPTS' line has to come first: run_gap.sh reads the options for the +# run out of it, and these tests are useless without '-p'. +# +# The answers are raw protocol and need the format to hand, but spelling it +# out in each test is expensive -- a comment in a test is echoed into its +# expected output as well, so every such line costs twice on disk. Hence a +# pointer to the README instead. +WCHeader := description -> Concatenation( + "#GAPOPTS -p\n", + description, + "# Generated by dev/make-window-cmd-tests.sh -- do not edit.\n", + "# README-window-cmd.md explains the '@a' answers below, in particular\n", + "# that every digit run is least significant digit first.\n");; + +MakeCases := function() + local cases, status, dense, first, second, len, pad, aimed, answer; + + status := WCInt(0); # leading status entry, dropped by WindowCmd + cases := []; + + # 1. An escape every few bytes, over several refills, so that escaped and + # plain bytes keep arriving in different pieces of the answer. The escaped + # block is 11 bytes, which does not divide the refill size, so the + # boundaries fall at different offsets within the block; splitting a '@X' + # pair itself is left to case 3, which aims one there exactly. + # + # "Long" here means long against the kernel's refill buffer, which is the + # only length in this protocol that means anything: answers have no size + # limit, and the 8000 byte buffer that used to cap them is long gone. + dense := WCRep("@ab\ncdefg", 100); + Add(cases, rec( + name := "dense escapes", + expected := "WCRep(\"@ab\\ncdefg\", 100)", + answer := WCAnswer([status, WCStr(dense)]))); + + # 2. Two strings in one answer, the first spanning a refill, so that entry + # parsing resumes correctly in the middle of a buffer after a payload that + # required a refill of its own. + first := WCRep("@wx\nyz", 100); + second := WCRep("mnopqr", 40); + Add(cases, rec( + name := "two long strings", + expected := "[ WCRep(\"@wx\\nyz\", 100), WCRep(\"mnopqr\", 40) ]", + answer := WCAnswer([status, WCStr(first), WCStr(second)]))); + + # 3. A '@@' pair aimed at the first refill boundary, the '@' at payload + # offset WCBufSize-1 and its double at WCBufSize, so that the kernel has to + # carry a half read escape across a refill. Offsets are 0 based and count + # payload bytes only, as the '@a+' header is read separately and does + # not go through the buffer. + # + # This aim holds as long as the first refill returns a whole buffer, which + # it does here because the entire file is already in the pipe. A short read + # would not break the test, but would quietly reduce it to another escape + # case. + # + # There is nothing here for the reader this replaced to fail: it had no + # refill buffer at all, and read a whole answer in one go. Escapes as such + # are covered against it by the escape table in window-cmd-entries.g; this + # case exists for the streaming reader that took its place. + len := 2 * WCBufSize; + pad := WCBufSize - 1 - WCDataOffset([status], len); + aimed := Concatenation(WCRep("x", pad), "@", WCRep("y", len - pad - 1)); + answer := WCAnswer([status, WCStr(aimed)]); + WCCheckStraddle(answer); + Add(cases, rec( + name := "escape across refill", + expected := Concatenation("Concatenation(WCRep(\"x\", ", String(pad), + "), \"@\", WCRep(\"y\", ", + String(len - pad - 1), "))"), + answer := answer)); + + return cases; +end;; + +# 'WriteAll' writes the string as it stands. 'PrintTo' would fold the long +# answer lines to the screen width, which would corrupt them. +WriteLongStringTest := function(name) + local out, cases, c; + + cases := MakeCases(); + out := OutputTextFile(name, false); + + WriteAll(out, Concatenation( + WCHeader(Concatenation( + "# Strings longer than the kernel's answer refill buffer, in package\n", + "# mode, so that the answer has to be read in several pieces.\n")), + "WCRep := {block, n} -> Concatenation(ListWithIdenticalEntries(n, block));;\n", + "Check := function(name, got, want)\n", + " local i, s, e;\n", + " Print(name, \": \");\n", + " if Length(got) <> Length(want) then\n", + " Print(\"FAIL - expected \", Length(want), \" results, got \",", + " Length(got), \"\\n\");\n", + " return;\n", + " fi;\n", + " for i in [1..Length(want)] do\n", + " s := got[i]; e := want[i];\n", + " if s <> e then\n", + " Print(\"FAIL - entry \", i, \" has length \", Length(s),\n", + " \", expected \", Length(e), \", first difference at \",\n", + " First([1..Minimum(Length(s), Length(e))],", + " j -> s[j] <> e[j]), \"\\n\");\n", + " return;\n", + " fi;\n", + " od;\n", + " Print(\"ok, lengths \", List(got, Length), \"\\n\");\n", + "end;;\n")); + + for c in cases do + WriteAll(out, Concatenation("want := ", c.expected, ";;\n")); + # 'Check' compares lists, so a case expecting a single string has to have + # it wrapped; a case whose expectation is already a list starts with '[' + if c.expected[1] <> '[' then + WriteAll(out, "want := [ want ];;\n"); + fi; + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + WriteAll(out, c.answer); + WriteAll(out, "\n"); + WriteAll(out, Concatenation("Check(\"", c.name, "\", got, want);\n")); + od; + + CloseStream(out); +end;; + +## The entry test: the small shapes an answer entry can take, which +## 'FuncWindowCmd' has to turn into GAP objects. Every case stays inside its +## declared payload, so the input stream is still in sync afterwards and they +## can simply follow one another. Results are printed as they stand, which is +## both the check and the documentation. +## +## Several cases raise an error on purpose. The test turns 'BreakOnError' +## off, so that those print their message and carry on rather than opening a +## break loop. That keeps the expected output to the error messages this +## code is responsible for, instead of also pinning GAP's break loop banner +## and stack trace, which are nothing to do with reading an answer and whose +## wording has changed between releases. It also means an error case need +## not be last, and needs no 'quit;' after it. + +WriteEntryTest := function(name) + local out, cases, ctrl, i, c, payload; + + ctrl := ""; + for i in [1..26] do + Add(ctrl, CHAR_INT(i)); + od; + + cases := [ + # 'I+' with no digits at all is the documented way of writing zero + rec(name := "implicit zero", + payload := Concatenation(WCInt(0), "I+"), + show := "got"), + + # signs, and an integer following another integer + rec(name := "signed integers", + payload := Concatenation(WCInt(0), WCInt(-13), WCInt(7), WCInt(0)), + show := "got"), + + # 'S+' and 'S0+' are both an empty string + rec(name := "empty strings", + payload := Concatenation(WCInt(0), "S+", "S0+"), + show := "got"), + + # every control character, i.e. the whole '@A' .. '@Z' escape table + rec(name := "escape table", + payload := Concatenation(WCInt(0), WCStr(ctrl)), + show := "List(got[1], INT_CHAR)"), + + # a literal '@' round trips as '@@', mixed in with other entry kinds + rec(name := "mixed entries", + payload := Concatenation(WCInt(0), WCStr("a@b"), WCInt(-1), + WCStr(""), WCStr("x\ny")), + show := "got"), + + # '@' followed by neither '@' nor a capital is malformed; the kernel + # drops both bytes and carries on, so this decodes to just "abcd" + rec(name := "malformed escape", + payload := Concatenation(WCInt(0), "S", WCRev(4), "+", "ab@1cd"), + show := "got"), + + # an entry after a string long enough to force a refill, to check that + # entry parsing resumes correctly part way through the buffer + rec(name := "entry after refill", + payload := Concatenation(WCInt(0), WCStr(WCRep("pq", 300)), + WCInt(42)), + show := "[ Length(got[1]), got[2] ]")]; + + out := OutputTextFile(name, false); + + WriteAll(out, WCHeader("# The shapes an answer entry can take, in package mode.\n")); + WriteAll(out, "BreakOnError := false;;\n"); + + for c in cases do + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + WriteAll(out, Concatenation("@a", WCRev(Length(c.payload)), "+", + c.payload)); + WriteAll(out, "\n"); + WriteAll(out, Concatenation("Print(\"", c.name, " = \", ", c.show, + ", \"\\n\");\n")); + od; + + # An entry kind that is neither 'I' nor 'S' is an error. The kernel drains + # the rest of the answer before reporting it, so the junk after the 'X' is + # swallowed and the next case still reads its answer correctly -- which is + # the only coverage the drain path gets. + # + # The junk deliberately runs past one bufferful, so that draining it has to + # read from the input rather than just step over bytes already in hand. It + # costs nothing in the expected output, as the kernel consumes these bytes + # and GAP never sees them. + payload := Concatenation(WCInt(0), "X", WCRep("junk", WCBufSize / 2)); + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + WriteAll(out, Concatenation("@a", WCRev(Length(payload)), "+", payload)); + WriteAll(out, "\n"); + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + payload := Concatenation(WCInt(0), WCStr("ok")); + WriteAll(out, Concatenation("@a", WCRev(Length(payload)), "+", payload)); + WriteAll(out, "\n"); + WriteAll(out, "Print(\"in sync after drain = \", got, \"\\n\");\n"); + + # A status entry of 1 is how a window handler reports a failure back to GAP, + # and is the form every real front end uses; the entries after it become the + # arguments of 'Error'. + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + payload := Concatenation(WCInt(1), WCStr("bad news")); + WriteAll(out, Concatenation("@a", WCRev(Length(payload)), "+", payload)); + WriteAll(out, "\n"); + + # A header that is not '@a+' at all. Writing it as a bare '@a' + # means the newline ending the line is what fails the check, so nothing is + # left over to be mistaken for input -- which the answer read afterwards + # confirms. + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + WriteAll(out, "@a\n"); + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + payload := Concatenation(WCInt(0), WCStr("still here")); + WriteAll(out, Concatenation("@a", WCRev(Length(payload)), "+", payload)); + WriteAll(out, "\n"); + WriteAll(out, "Print(\"in sync after bad header = \", got, \"\\n\");\n"); + + CloseStream(out); +end;; + +## The truncated answer test: two ways an answer can come up short. The +## first stays inside its declared payload and is merely an entry claiming +## more than the payload holds, which is clamped. The second is a payload +## the input never delivers, where the kernel meets EOF part way through -- +## that is the one that used to spin forever, subtracting a read() of 0 from +## the count of bytes outstanding and so never reducing it. +## +## The second case runs the stream out entirely, so it has to come last and +## its checks have to sit on the same input line as the 'WindowCmd' call. +## GAP reads a whole line before evaluating any of it, so the 'Print' calls +## are already in hand by the time the answer swallows the rest of the file. + +WriteTruncatedTest := function(name) + local out, data, payload, tail; + + out := OutputTextFile(name, false); + + WriteAll(out, WCHeader("# Answers that come up short, in package mode.\n")); + + # 1. The answer is well formed and complete, but the string entry claims + # 9000 bytes where the payload has only 20 left, so the length is clamped to + # what is really there. Nothing is read past the payload and the input + # stream is still in sync afterwards. + data := "0123456789abcdefghij"; + payload := Concatenation(WCInt(0), "S", WCRev(9000), "+", data); + WriteAll(out, "got := WindowCmd([\"TST\"]);;\n"); + WriteAll(out, Concatenation("@a", WCRev(Length(payload)), "+", payload)); + WriteAll(out, "\n"); + WriteAll(out, Concatenation( + "Print(\"clamped length = \", Length(got[1]), \"\\n\");\n", + "Print(\"clamped content = \", got[1] = \"", data, "\", \"\\n\");\n")); + + # 2. The header promises 5000 payload bytes and the input runs out long + # before that, so the kernel meets EOF in the middle of a string entry and + # has to zero fill the rest instead of spinning on a read() of 0. + # + # The length follows from the header alone: 9 payload bytes go on 'I0+' and + # the 'S' length prefix, so the string is clamped to the remaining 4991. + # The leading bytes are the ten written here plus the newline that ends the + # file. What comes immediately after that is whatever run_gap.sh appends to + # the test before GAP reaches EOF, so this says nothing about it -- but well + # past that everything must be the zero fill, and checking so is what keeps + # uninitialised memory from reaching a GAP string unnoticed. + tail := Concatenation(WCInt(0), "S", WCRev(9000), "+", "abcdefghij"); + WriteAll(out, Concatenation( + "got := WindowCmd([\"TST\"]);;", + " Print(\"eof entries = \", Length(got), \"\\n\");", + " Print(\"eof length = \", Length(got[1]), \"\\n\");", + " Print(\"eof prefix = \", got[1]{[1..11]} = \"abcdefghij\\n\", \"\\n\");", + " Print(\"eof zero filled = \",", + " ForAll(got[1]{[100..Length(got[1])]}, c -> c = CHAR_INT(0)),", + " \"\\n\");\n")); + WriteAll(out, Concatenation("@a", WCRev(5000), "+", tail)); + WriteAll(out, "\n"); + + CloseStream(out); +end;; + +WriteLongStringTest("window-cmd-long-string.g"); +WriteEntryTest("window-cmd-entries.g"); +WriteTruncatedTest("window-cmd-truncated.g"); +QUIT; +GAPEOF + +echo "wrote window-cmd-long-string.g window-cmd-entries.g window-cmd-truncated.g" diff --git a/src/gap.c b/src/gap.c index e88bcc8711..f09ac0efd6 100644 --- a/src/gap.c +++ b/src/gap.c @@ -557,6 +557,21 @@ static Obj FuncSizeScreen(Obj self, Obj args) } +/**************************************************************************** +** +*F WindowCmdError( ) . . . . . . . . . . report a window system error +** +** Report the way an answer from the window handler starting with the +** integer 1 is reported, i.e. as 'Error( "window system: ", )'. +*/ +static Obj WindowCmdError(const Char * msg) +{ + return CALL_XARGS( Error, + NewPlistFromArgs( MakeImmString( "window system: " ), + MakeImmString( msg ) ) ); +} + + /**************************************************************************** ** *F FuncWindowCmd( , ) . . . . . . . . execute a window command @@ -568,10 +583,11 @@ static Obj FuncWindowCmd(Obj self, Obj args) Obj tmp; Obj list; Int len; - Int n, m; + Int m; Int i; + Int kind; + UInt slen; Char * ptr; - const Char * inptr; const Char * qtr; RequireSmallList(SELF_NAME, args); @@ -638,36 +654,36 @@ static Obj FuncWindowCmd(Obj self, Obj args) } *ptr = 0; - // now call the window front end with the argument string + // send the command to the window front end qtr = CONST_CSTR_STRING(WindowCmdString); - inptr = SyWinCmd( qtr, strlen(qtr) ); - len = strlen(inptr); + if ( ! SyWindow ) + return WindowCmdError( "No Window Handler Present" ); + SyWinSendCmd( qtr ); + + // read the '@a+' answer header + if ( ! SyWinBeginAnswer() ) + return WindowCmdError( "Illegal Answer" ); - // now convert result back into a list + // read the answer entries into a new list, allocating each string at its + // known length and reading it straight into the bag list = NEW_PLIST( T_PLIST, 11 ); i = 1; - while ( 0 < len ) { - if ( *inptr == 'I' ) { - inptr++; - for ( n=0,m=1; '0' <= *inptr && *inptr <= '9'; inptr++,m *= 10,len-- ) - n += (*inptr-'0') * m; - if ( *inptr++ == '-' ) - n *= -1; - len -= 2; - AssPlist( list, i, INTOBJ_INT(n) ); + while ( 0 <= (kind = SyWinReadEntryKind()) ) { + if ( kind == 'I' ) { + AssPlist( list, i, INTOBJ_INT( SyWinReadInt() ) ); } - else if ( *inptr == 'S' ) { - inptr++; - for ( n=0,m=1; '0' <= *inptr && *inptr <= '9'; inptr++,m *= 10,len-- ) - n += (*inptr-'0') * m; - inptr++; // ignore the '+' - tmp = MakeImmStringWithLen(inptr, n); - inptr += n; - len -= n+2; + else if ( kind == 'S' ) { + slen = SyWinReadStrLen(); + tmp = NEW_STRING( slen ); + // 'SyWinReadStr' only reads from the input, so it cannot trigger a + // garbage collection that would move the bag under 'CHARS_STRING' + SyWinReadStr( CHARS_STRING(tmp), slen ); + MakeImmutableNoRecurse( tmp ); AssPlist( list, i, tmp ); } else { - ErrorQuit( "unknown return value '%s'", (Int)inptr, 0 ); + SyWinEndAnswer(); // drain the rest, keeping the stream in sync + ErrorQuit( "WindowCmd: unknown entry kind '%c' in answer", kind, 0 ); } i++; } diff --git a/src/sysfiles.c b/src/sysfiles.c index 98aeb14694..db014539de 100644 --- a/src/sysfiles.c +++ b/src/sysfiles.c @@ -447,34 +447,23 @@ void syWinPut ( /**************************************************************************** ** -*F SyWinCmd( , ) . . . . . . . . . . . . . execute a window cmd +*F SyWinSendCmd( ) . . . . . . . send a window command, no answer read ** -** 'SyWinCmd' send the command to the window handler ( is -** ignored). In the string '@' characters are duplicated, and control -** characters are converted to '@', e.g., is converted to -** '@J'. Then 'SyWinCmd' waits for the window handlers answer and returns -** that string. +** 'SyWinSendCmd' sends the command to the window handler as +** '@w+', duplicating '@' characters and converting control +** characters to '@' as 'syWinPut' does. The answer is read separately +** by 'SyWinBeginAnswer' and the entry readers below, so that the caller can +** allocate each result at its known size. */ -static Char WinCmdBuffer[8000]; - -const Char * SyWinCmd ( - const Char * str, - UInt len ) +void SyWinSendCmd ( + const Char * str ) { Char buf [130]; // temporary buffer const Char * s; // pointer into the string - const Char * bb; // pointer into the temporary Char * b; // pointer into the temporary - UInt i; // loop variable -#ifdef SYS_IS_CYGWIN32 - UInt len1; // temporary storage for len -#endif - - // if not running under a window handler, don't do nothing - if ( ! SyWindow ) - return "I1+S52+No Window Handler Present"; + UInt len; // length of the expanded string - // compute the length of the (expanded) string (and ignore argument) + // compute the length of the (expanded) string len = 0; for ( s = str; *s != '\0'; s++ ) len += 1 + (*s == '@' || (CTR('A') <= *s && *s <= CTR('Z'))); @@ -490,61 +479,241 @@ const Char * SyWinCmd ( // send the string to the window handler syWinPut( 1, "", str ); +} - // read the length of the answer - b = WinCmdBuffer; - i = 3; - while ( 0 < i ) { - len = read( 0, b, i ); - i -= len; - b += len; - } - if ( WinCmdBuffer[0] != '@' || WinCmdBuffer[1] != 'a' ) - return "I1+S41+Illegal Answer"; - b = WinCmdBuffer+2; - for ( i=1,len=0; '0' <= *b && *b <= '9'; i *= 10 ) { - len += (*b-'0')*i; - while ( read( 0, b, 1 ) != 1 ) ; - } - - // read the arguments of the answer - b = WinCmdBuffer; - i = len; -#ifdef SYS_IS_CYGWIN32 - len1 = len; - while ( 0 < i ) { - len = read( 0, b, i ); - b += len; - i -= len; - s += len; - } - len = len1; -#else - while ( 0 < i ) { - len = read( 0, b, i ); - i -= len; - s += len; - } -#endif - // shrink '@@' into '@' - for ( bb = b = WinCmdBuffer; 0 < len; len-- ) { - if ( *bb == '@' ) { - bb++; - if ( *bb == '@' ) - *b++ = '@'; - else if ( 'A' <= *bb && *bb <= 'Z' ) - *b++ = CTR(*bb); - bb++; - } - else { - *b++ = *bb++; +/**************************************************************************** +** +** A window handler answers a command with '@a+', where is +** the number of (still '@'-escaped) payload bytes and is a sequence +** of entries. Each entry is either 'I', an integer with +** '+' or '-', or 'S+', a string of the given +** un-escaped length. Every run in this protocol, here and in the +** header, is written least significant digit first, so "52" means 25. +*/ + +// The payload is read in bulk into a small fixed buffer and served from there +// one still-escaped byte at a time, as 'syGetchNonTerm' serves terminal +// input. 'syWinAnswerRemaining' counts payload bytes not yet read from the +// input, the buffer holds bytes read but not yet served. Every refill is +// bounded by 'syWinAnswerRemaining', so a read never runs past the answer +// into whatever follows it on the stream. The size only decides how many +// reads a long answer costs; short reads are handled anyway. +#define SYS_WIN_BUF_SIZE 512 +static Int syWinAnswerRemaining; +static UChar syWinBuf [SYS_WIN_BUF_SIZE]; +static Int syWinBufStart; // next byte to serve +static Int syWinBufLen; // number of valid bytes in the buffer + +// payload bytes still to serve: buffered but unserved, plus not yet read +static Int syWinAnswerLeft ( void ) +{ + return syWinAnswerRemaining + (syWinBufLen - syWinBufStart); +} + +// read one byte from the input, retrying on EINTR and EAGAIN; -1 on EOF/error +static Int syWinGetch ( void ) +{ + UChar c; // the byte read + Int ret; // return value of 'SyRead' + + do { + ret = SyRead( 0, &c, 1 ); + } while ( ret == -1 && (errno == EINTR || errno == EAGAIN) ); + return ( ret == 1 ) ? (Int)c : -1; +} + +// serve one still-escaped payload byte, refilling the buffer; -1 at the end +static Int syWinAnswerRaw ( void ) +{ + Int want; // bytes to ask for + Int got; // bytes actually read + + if ( syWinBufStart >= syWinBufLen ) { + if ( syWinAnswerRemaining <= 0 ) + return -1; + want = ( syWinAnswerRemaining < SYS_WIN_BUF_SIZE ) + ? syWinAnswerRemaining : SYS_WIN_BUF_SIZE; + do { + got = SyRead( 0, syWinBuf, want ); + } while ( got == -1 && (errno == EINTR || errno == EAGAIN) ); + if ( got <= 0 ) { // EOF/error: no more answer to read + syWinAnswerRemaining = 0; + return -1; } + syWinBufStart = 0; + syWinBufLen = got; + syWinAnswerRemaining -= got; + } + return syWinBuf[syWinBufStart++]; +} + +// read one un-escaped ('@@' -> '@', '@X' -> ctrl) payload byte; -1 at the end +static Int syWinAnswerByte ( void ) +{ + Int c; // byte of the payload + Int d; // byte following an '@' + + // Decoding is sequential by nature: an escape turns two input bytes into + // one, so the input and output counts advance independently. The old + // reader conflated them and read past the payload as soon as an answer + // contained an escape; serving a byte at a time makes that impossible. + for (;;) { + c = syWinAnswerRaw(); + if ( c < 0 ) + return -1; + if ( c != '@' ) + return c; + d = syWinAnswerRaw(); + if ( d < 0 ) + return -1; + if ( d == '@' ) + return '@'; + if ( 'A' <= d && d <= 'Z' ) + return CTR(d); + // a malformed '@': drop both bytes and keep scanning + } +} + + +/**************************************************************************** +** +*F SyWinBeginAnswer() . . . . . . . . . . read the '@a+' answer header +** +** Read and validate the answer header and arm the entry readers. Returns +** 'TRUE' on success, 'FALSE' if the header is not a well-formed '@a+'. +*/ +BOOL SyWinBeginAnswer ( void ) +{ + Int c; // byte of the header + UInt len; // declared payload length + UInt place; // power of ten for the digits + + // start from a clean slate: bytes left over from an answer that was + // abandoned part way through are not part of this one + syWinAnswerRemaining = 0; + syWinBufStart = syWinBufLen = 0; + + // the header is not payload, so it is read past the buffer + if ( syWinGetch() != '@' ) return FALSE; + if ( syWinGetch() != 'a' ) return FALSE; + + len = 0; place = 1; + while ( '0' <= (c = syWinGetch()) && c <= '9' ) { + len += (UInt)(c - '0') * place; + place *= 10; + } + if ( c != '+' ) return FALSE; + if ( (Int)len < 0 ) return FALSE; // length too big for Int: illegal + + syWinAnswerRemaining = (Int)len; + return TRUE; +} + + +/**************************************************************************** +** +*F SyWinReadEntryKind() . . . . . . . . . . read the kind of the next entry +** +** Return the kind of the next entry, 'I' or 'S' in a well-formed answer, or +** -1 once the answer is exhausted. Anything else the caller must reject. +*/ +Int SyWinReadEntryKind ( void ) +{ + return syWinAnswerByte(); +} + + +/**************************************************************************** +** +*F SyWinReadInt() . . . . . . . . . . . . . . . . . . . . read an 'I' entry +** +** Read ''; the 'I' has already been read as the entry kind. +** As everywhere in this protocol, the digits come least significant first. +*/ +Int SyWinReadInt ( void ) +{ + Int c; // byte of the entry + Int n; // the integer read so far + UInt place; // power of ten for the digits + + n = 0; place = 1; + while ( '0' <= (c = syWinAnswerByte()) && c <= '9' ) { + n += (c - '0') * (Int)place; + place *= 10; + } + if ( c == '-' ) + n = -n; + return n; +} + + +/**************************************************************************** +** +*F SyWinReadStrLen() . . . . . . . . . . . . read the length of an 'S' entry +** +** Read the '+' length prefix; the 'S' has already been read as the +** entry kind. The caller allocates a result of that length and then calls +** 'SyWinReadStr' to fill it. +*/ +UInt SyWinReadStrLen ( void ) +{ + Int c; // byte of the entry + UInt n; // the length read so far + UInt place; // power of ten for the digits + + n = 0; place = 1; + while ( '0' <= (c = syWinAnswerByte()) && c <= '9' ) { + n += (UInt)(c - '0') * place; + place *= 10; } - *b = 0; + // the loop has consumed the '+' terminator (or the end of the answer). + // An un-escaped string cannot be longer than the escaped payload bytes + // still to come, so clamp to those: that also keeps the length in range + // for NEW_STRING, whose parameter is a signed Int. + if ( n > (UInt) syWinAnswerLeft() ) + n = (UInt) syWinAnswerLeft(); + return n; +} - // return the string - return WinCmdBuffer; + +/**************************************************************************** +** +*F SyWinReadStr( , ) . . . . read un-escaped bytes of a string +** +** Fill with un-escaped bytes of the current 'S' entry. On a short +** or truncated answer the remainder is zero-filled rather than read past. +*/ +void SyWinReadStr ( + UChar * dst, + UInt n ) +{ + UInt k; // loop variable + Int c; // byte of the string + + for ( k = 0; k < n; k++ ) { + c = syWinAnswerByte(); + if ( c < 0 ) + break; + dst[k] = (UChar)c; + } + while ( k < n ) + dst[k++] = '\0'; +} + + +/**************************************************************************** +** +*F SyWinEndAnswer() . . . . . . . . . discard any unread bytes of the answer +** +** Consume and drop any payload bytes not yet read, so that the input stream +** stays in sync when the caller stops parsing an answer early, e.g. a +** malformed one. A fully-parsed answer leaves nothing, so this is a no-op. +*/ +void SyWinEndAnswer ( void ) +{ + while ( 0 <= syWinAnswerRaw() ) + ; } diff --git a/src/sysfiles.h b/src/sysfiles.h index 96799788ee..2adb4516be 100644 --- a/src/sysfiles.h +++ b/src/sysfiles.h @@ -59,15 +59,38 @@ void syWinPut(Int fid, const Char * cmd, const Char * str); /**************************************************************************** ** -*F SyWinCmd( , ) . . . . . . . . . . . . . execute a window cmd +*F SyWinSendCmd( ) . . . . . . . send a window command, no answer read ** -** 'SyWinCmd' send the command to the window handler ( is -** ignored). In the string '@' characters are duplicated, and control -** characters are converted to '@', e.g., is converted to -** '@J'. Then 'SyWinCmd' waits for the window handlers answer and returns -** that string. +** 'SyWinSendCmd' sends the command to the window handler as +** '@w+', duplicating '@' characters and converting control +** characters to '@' as 'syWinPut' does. The answer is read separately +** by 'SyWinBeginAnswer' and the entry readers below, so that the caller can +** allocate each result at its known size. Like 'syWinPut', this is only +** meaningful when 'SyWindow' is set. */ -const Char * SyWinCmd(const Char * str, UInt len); +void SyWinSendCmd(const Char * str); + + +/**************************************************************************** +** +*F SyWinBeginAnswer() . . . . . . . . . . read the '@a+' answer header +*F SyWinReadEntryKind() . . . . . . . . . . read the kind of the next entry +*F SyWinReadInt() . . . . . . . . . . . . . . . . . . . . read an 'I' entry +*F SyWinReadStrLen() . . . . . . . . . . . . read the length of an 'S' entry +*F SyWinReadStr( , ) . . . . read un-escaped bytes of a string +*F SyWinEndAnswer() . . . . . . . . . discard any unread bytes of the answer +** +** Read the answer to the last 'SyWinSendCmd' one entry at a time, so that a +** string can be allocated at its known length and read straight into the +** bag. 'SyWinBeginAnswer' returns 'FALSE' on a malformed header, and +** 'SyWinReadEntryKind' returns -1 at the end of the answer. +*/ +BOOL SyWinBeginAnswer(void); +Int SyWinReadEntryKind(void); +Int SyWinReadInt(void); +UInt SyWinReadStrLen(void); +void SyWinReadStr(UChar * dst, UInt n); +void SyWinEndAnswer(void); /**************************************************************************** diff --git a/tst/testspecial/README-window-cmd.md b/tst/testspecial/README-window-cmd.md new file mode 100644 index 0000000000..db8a1a9ad2 --- /dev/null +++ b/tst/testspecial/README-window-cmd.md @@ -0,0 +1,117 @@ +# The `window-cmd-*.g` tests + +These tests cover the kernel's reading of a window handler's answer in package +mode: `SyWinBeginAnswer` and the entry readers in `src/sysfiles.c`, and +`FuncWindowCmd` in `src/gap.c`. + +`tst/testinstall/kernel/gap.tst` covers `WindowCmd`'s argument checks and the +`No Window Handler Present` path. These tests cover parsing an answer +from a (faked) window handler. + +## How they work + +`WindowCmd` writes `@w+` to stdout, then waits for the +handler's answer back from **stdin**. A test can therefore put the answer +bytes on the line after the call: + + got := WindowCmd(["TST"]);; + @a8+I0+S2+ok + Print(got, "\n"); + +GAP reads stdin only to the newline, so the answer is still in the stream +when the kernel asks for it. No handler and no second process are +involved, so there is no timing in these tests at all. The kernel does not +interpret the three character command name, so `"TST"` is used as a +placeholder. + +The tests need `gap -p`, and package mode cannot be entered from within GAP +(`SyWindow` is set at startup only), so `run_gap.sh` takes options from a +first line of the form + + #GAPOPTS -p + +As an ordinary GAP comment, this says right in the test the GAP flags +that are needed. + +## Reading an answer + +An answer is `@a+`, where `` counts the payload bytes as +written. The payload is a run of entries: + + I an integer, being + or - + S+ a string, its un-escaped length + +**Every digit run in this protocol is least significant digit first**, so +`52` is 25 and `0009` is 9000; this is the thing most often misread as a bug. +A string's `` count its length after un-escaping, so they may be +fewer than the bytes written. Within a payload `@` is written `@@`, and a +control character as `@A` .. `@Z`. + +The first entry is a status code, which `FuncWindowCmd` removes before +returning the rest; hence the leading `I0+` in every answer here. A status +of `1` means failure, and turns the remaining entries into the arguments of +`Error`. Note that `I+` and `S+`, with no digits at all, are exactly what +`FuncWindowCmd` itself emits for `0` and `""`. + +The newline ending an answer stays in the input, where GAP reads it as an +empty line: the blank `gap>` that follows each answer in the expected output. + +## The tests + +`window-cmd-long-string.g` -- strings longer than the kernel's refill buffer, +so an answer is read in several pieces: dense escapes over several refills, +two strings in one answer, and a `@@` pair aimed at a refill boundary so that +half an escape is carried across. + +`window-cmd-entries.g` -- the shapes an entry can take: `I+`, signed +integers, `S+` and `S0+`, the whole `@A` .. `@Z` escape table, mixed kinds, a +malformed `@`, an entry after a refill, an unknown entry kind and the +drain that keeps the stream in sync after it, a status of `1`, and a header +that is not `@a+` at all. It turns `BreakOnError` off, so that the +cases which raise an error print their message and carry on instead of +opening a break loop. That keeps the expected output to the messages this +code is responsible for, rather than also pinning GAP's break loop banner and +stack trace, whose wording has changed between releases. + +`window-cmd-truncated.g` -- answers that come up short: an entry claiming +more bytes than the payload holds, which is clamped; and a payload the input +never delivers, where the kernel meets EOF part way through and zero fills +the rest. + +Not covered: the outgoing direction, since `run_gap.sh` captures GAP's log +rather than its stdout, so the `@w` framing and the escaping of the command +are invisible here; `EAGAIN` and the `@y`/`@s` sync handshake, which need a +pseudo terminal, input side `@` decoding living only in `syGetchTerm`; and +answers that are not merely short but invalid, such as one carrying no status +entry at all, which is a broken handler rather than something the kernel is +expected to survive gracefully. + +## Regenerating + + dev/make-window-cmd-tests.sh + tst/testspecial/regenerate_tests.sh + +The generator runs from any directory. It lives in `dev/` because it is +needed only to change the tests, not to run them, and `dev/` is not shipped +in a release. The answers are generated rather than edited because each has +three interlocking counts -- the payload length, each string's un-escaped +length, and the escaping -- and a wrong one is not necessarily a visible +failure, as the kernel clamps a string to the bytes that remain. It takes +the refill buffer size from `SYS_WIN_BUF_SIZE` in `src/sysfiles.c` rather +than keeping a copy, and checks in the bytes it has just produced that the +boundary case really does straddle a refill. **Regenerate if +`SYS_WIN_BUF_SIZE` changes**, or that case will go on passing while no longer +testing a boundary. + +Two things not to tidy up. `window-cmd-truncated.g.out` ends without a +trailing newline, because GAP is killed by EOF mid-prompt. And that same +test covers a bug whose old behaviour was an infinite loop, so a regression +in it hangs rather than fails; where the system has a `timeout` command +`run_gap.sh` uses it, reports the test as killed, and lets the output +comparison fail it so the other tests still run. + +## A note for maintainers + +This file should eventually be folded into `README.md`, along with +descriptions of the other tests here, many of which appear to be +undocumented as of this writing. diff --git a/tst/testspecial/README.md b/tst/testspecial/README.md index 7d2c7403df..aa9935a2ea 100644 --- a/tst/testspecial/README.md +++ b/tst/testspecial/README.md @@ -7,3 +7,4 @@ filenames which occur in output. `./run_gap.sh` : This runs GAP, capturing its input/output `./run_all.sh` : This runs all the tests `./regenerate_tests.sh` : Regenerate all outputs +`README-window-cmd.md` : Notes on the `window-cmd-*.g` tests diff --git a/tst/testspecial/run_gap.sh b/tst/testspecial/run_gap.sh index aa7f088494..02ff51a032 100755 --- a/tst/testspecial/run_gap.sh +++ b/tst/testspecial/run_gap.sh @@ -14,11 +14,46 @@ outfile="${3:-$gfile.out}" # 3) Rewrite the root of gap with the string GAPROOT, # so the output is usable on other machines # 4) Set lower and upper memory limits, for consistency +# 5) Pass any extra GAP command line options the test asks for. A test whose +# first line reads '#GAPOPTS ' is run with those added; it is a +# GAP comment, so it stays part of the test and says on the face of it +# what the test needs. The window-cmd-*.g tests use it to ask for -p, +# which puts GAP into package mode. +gapopts=() +gapoptline=$(sed -n '1s/^#GAPOPTS[[:space:]]*//p' "$gfile") +if [ -n "${gapoptline}" ]; then + read -ra gapopts <<< "${gapoptline}" +fi + +# 6) Stop a wedged GAP from hanging the whole suite; window-cmd-truncated.g +# covers a bug whose old behaviour was an infinite loop. Skipped where +# there is no timeout command, which is the usual case on macOS. +limit=300 +guard=() +if command -v timeout >/dev/null 2>&1 ; then + guard=(timeout "${limit}") +elif command -v gtimeout >/dev/null 2>&1 ; then + guard=(gtimeout "${limit}") +fi + GAPROOT=$("$gap" --print-gaproot) +# Start from no log at all, so that output left by an earlier run can never be +# mistaken for output of this one +rm -f "${outfile}.tmp" +status=0 ( echo "LogTo(\"${outfile}.tmp\");" ; cat "$gfile" ; echo "QUIT;" ) | - "$gap" -r -A -b -m 256m -o 512m -x 800 \ + "${guard[@]}" "$gap" -r -A -b -m 256m -o 512m -x 800 "${gapopts[@]}" \ -c 'SetUserPreference("UseColorsInTerminal",false);' \ -c 'SetUserPreference("WhereDepth", 5);' \ - 2>/dev/null >/dev/null + 2>/dev/null >/dev/null || status=$? +# A timeout is reported and then left to the comparison of the output, so that +# the remaining tests still run. Any other failure aborts, as it always has. +case ${status} in + 0) ;; + 124|137) echo "${gfile}: killed after ${limit}s" >&2 ;; + *) exit ${status} ;; +esac +# GAP killed before it opened the log leaves nothing to compare against +[ -f "${outfile}.tmp" ] || : > "${outfile}.tmp" sed -E -e "s:${GAPROOT//:/\\:}:GAPROOT/:g" -e "s;(GAPROOT(/[^/]+)+):[0-9]+;\1:LINE;g" < "${outfile}.tmp" > "${outfile}" rm "${outfile}.tmp" diff --git a/tst/testspecial/window-cmd-entries.g b/tst/testspecial/window-cmd-entries.g new file mode 100644 index 0000000000..6bb5bcc749 --- /dev/null +++ b/tst/testspecial/window-cmd-entries.g @@ -0,0 +1,39 @@ +#GAPOPTS -p +# The shapes an answer entry can take, in package mode. +# Generated by dev/make-window-cmd-tests.sh -- do not edit. +# README-window-cmd.md explains the '@a' answers below, in particular +# that every digit run is least significant digit first. +BreakOnError := false;; +got := WindowCmd(["TST"]);; +@a5+I0+I+ +Print("implicit zero = ", got, "\n"); +got := WindowCmd(["TST"]);; +@a31+I0+I31-I7+I0+ +Print("signed integers = ", got, "\n"); +got := WindowCmd(["TST"]);; +@a8+I0+S+S0+ +Print("empty strings = ", got, "\n"); +got := WindowCmd(["TST"]);; +@a95+I0+S62+@A@B@C@D@E@F@G@H@I@J@K@L@M@N@O@P@Q@R@S@T@U@V@W@X@Y@Z +Print("escape table = ", List(got[1], INT_CHAR), "\n"); +got := WindowCmd(["TST"]);; +@a32+I0+S3+a@@bI1-S0+S3+x@Jy +Print("mixed entries = ", got, "\n"); +got := WindowCmd(["TST"]);; +@a21+I0+S4+ab@1cd +Print("malformed escape = ", got, "\n"); +got := WindowCmd(["TST"]);; +@a216+I0+S006+pqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqpqI24+ +Print("entry after refill = ", [ Length(got[1]), got[2] ], "\n"); +got := WindowCmd(["TST"]);; +@a8201+I0+Xjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunk +got := WindowCmd(["TST"]);; +@a8+I0+S2+ok +Print("in sync after drain = ", got, "\n"); +got := WindowCmd(["TST"]);; +@a41+I1+S8+bad news +got := WindowCmd(["TST"]);; +@a +got := WindowCmd(["TST"]);; +@a71+I0+S01+still here +Print("in sync after bad header = ", got, "\n"); diff --git a/tst/testspecial/window-cmd-entries.g.out b/tst/testspecial/window-cmd-entries.g.out new file mode 100644 index 0000000000..9c0b09138c --- /dev/null +++ b/tst/testspecial/window-cmd-entries.g.out @@ -0,0 +1,51 @@ +gap> #GAPOPTS -p +gap> # The shapes an answer entry can take, in package mode. +gap> # Generated by dev/make-window-cmd-tests.sh -- do not edit. +gap> # README-window-cmd.md explains the '@a' answers below, in particular +gap> # that every digit run is least significant digit first. +gap> BreakOnError := false;; +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("implicit zero = ", got, "\n"); +implicit zero = [ 0 ] +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("signed integers = ", got, "\n"); +signed integers = [ -13, 7, 0 ] +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("empty strings = ", got, "\n"); +empty strings = [ "", "" ] +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("escape table = ", List(got[1], INT_CHAR), "\n"); +escape table = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 ] +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("mixed entries = ", got, "\n"); +mixed entries = [ "a@b", -1, "", "x\ny" ] +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("malformed escape = ", got, "\n"); +malformed escape = [ "abcd" ] +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("entry after refill = ", [ Length(got[1]), got[2] ], "\n"); +entry after refill = [ 600, 42 ] +gap> got := WindowCmd(["TST"]);; +Error, WindowCmd: unknown entry kind 'X' in answer +gap> +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("in sync after drain = ", got, "\n"); +in sync after drain = [ "ok" ] +gap> got := WindowCmd(["TST"]);; +Error, window system: bad news +gap> +gap> got := WindowCmd(["TST"]);; +Error, window system: Illegal Answer +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("in sync after bad header = ", got, "\n"); +in sync after bad header = [ "still here" ] +gap> QUIT; diff --git a/tst/testspecial/window-cmd-long-string.g b/tst/testspecial/window-cmd-long-string.g new file mode 100644 index 0000000000..d83c1300d3 --- /dev/null +++ b/tst/testspecial/window-cmd-long-string.g @@ -0,0 +1,39 @@ +#GAPOPTS -p +# Strings longer than the kernel's answer refill buffer, in package +# mode, so that the answer has to be read in several pieces. +# Generated by dev/make-window-cmd-tests.sh -- do not edit. +# README-window-cmd.md explains the '@a' answers below, in particular +# that every digit run is least significant digit first. +WCRep := {block, n} -> Concatenation(ListWithIdenticalEntries(n, block));; +Check := function(name, got, want) + local i, s, e; + Print(name, ": "); + if Length(got) <> Length(want) then + Print("FAIL - expected ", Length(want), " results, got ", Length(got), "\n"); + return; + fi; + for i in [1..Length(want)] do + s := got[i]; e := want[i]; + if s <> e then + Print("FAIL - entry ", i, " has length ", Length(s), + ", expected ", Length(e), ", first difference at ", + First([1..Minimum(Length(s), Length(e))], j -> s[j] <> e[j]), "\n"); + return; + fi; + od; + Print("ok, lengths ", List(got, Length), "\n"); +end;; +want := WCRep("@ab\ncdefg", 100);; +want := [ want ];; +got := WindowCmd(["TST"]);; +@a8011+I0+S009+@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg@@ab@Jcdefg +Check("dense escapes", got, want); +want := [ WCRep("@wx\nyz", 100), WCRep("mnopqr", 40) ];; +got := WindowCmd(["TST"]);; +@a3501+I0+S006+@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@Jyz@@wx@JyzS042+mnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqrmnopqr +Check("two long strings", got, want); +want := Concatenation(WCRep("x", 502), "@", WCRep("y", 521));; +want := [ want ];; +got := WindowCmd(["TST"]);; +@a4301+I0+S4201+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@@yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy +Check("escape across refill", got, want); diff --git a/tst/testspecial/window-cmd-long-string.g.out b/tst/testspecial/window-cmd-long-string.g.out new file mode 100644 index 0000000000..489c6b0143 --- /dev/null +++ b/tst/testspecial/window-cmd-long-string.g.out @@ -0,0 +1,43 @@ +gap> #GAPOPTS -p +gap> # Strings longer than the kernel's answer refill buffer, in package +gap> # mode, so that the answer has to be read in several pieces. +gap> # Generated by dev/make-window-cmd-tests.sh -- do not edit. +gap> # README-window-cmd.md explains the '@a' answers below, in particular +gap> # that every digit run is least significant digit first. +gap> WCRep := {block, n} -> Concatenation(ListWithIdenticalEntries(n, block));; +gap> Check := function(name, got, want) +> local i, s, e; +> Print(name, ": "); +> if Length(got) <> Length(want) then +> Print("FAIL - expected ", Length(want), " results, got ", Length(got), "\n"); +> return; +> fi; +> for i in [1..Length(want)] do +> s := got[i]; e := want[i]; +> if s <> e then +> Print("FAIL - entry ", i, " has length ", Length(s), +> ", expected ", Length(e), ", first difference at ", +> First([1..Minimum(Length(s), Length(e))], j -> s[j] <> e[j]), "\n"); +> return; +> fi; +> od; +> Print("ok, lengths ", List(got, Length), "\n"); +> end;; +gap> want := WCRep("@ab\ncdefg", 100);; +gap> want := [ want ];; +gap> got := WindowCmd(["TST"]);; +gap> +gap> Check("dense escapes", got, want); +dense escapes: ok, lengths [ 900 ] +gap> want := [ WCRep("@wx\nyz", 100), WCRep("mnopqr", 40) ];; +gap> got := WindowCmd(["TST"]);; +gap> +gap> Check("two long strings", got, want); +two long strings: ok, lengths [ 600, 240 ] +gap> want := Concatenation(WCRep("x", 502), "@", WCRep("y", 521));; +gap> want := [ want ];; +gap> got := WindowCmd(["TST"]);; +gap> +gap> Check("escape across refill", got, want); +escape across refill: ok, lengths [ 1024 ] +gap> QUIT; diff --git a/tst/testspecial/window-cmd-truncated.g b/tst/testspecial/window-cmd-truncated.g new file mode 100644 index 0000000000..5b0fb67bd2 --- /dev/null +++ b/tst/testspecial/window-cmd-truncated.g @@ -0,0 +1,11 @@ +#GAPOPTS -p +# Answers that come up short, in package mode. +# Generated by dev/make-window-cmd-tests.sh -- do not edit. +# README-window-cmd.md explains the '@a' answers below, in particular +# that every digit run is least significant digit first. +got := WindowCmd(["TST"]);; +@a92+I0+S0009+0123456789abcdefghij +Print("clamped length = ", Length(got[1]), "\n"); +Print("clamped content = ", got[1] = "0123456789abcdefghij", "\n"); +got := WindowCmd(["TST"]);; Print("eof entries = ", Length(got), "\n"); Print("eof length = ", Length(got[1]), "\n"); Print("eof prefix = ", got[1]{[1..11]} = "abcdefghij\n", "\n"); Print("eof zero filled = ", ForAll(got[1]{[100..Length(got[1])]}, c -> c = CHAR_INT(0)), "\n"); +@a0005+I0+S0009+abcdefghij diff --git a/tst/testspecial/window-cmd-truncated.g.out b/tst/testspecial/window-cmd-truncated.g.out new file mode 100644 index 0000000000..f392ddaac8 --- /dev/null +++ b/tst/testspecial/window-cmd-truncated.g.out @@ -0,0 +1,17 @@ +gap> #GAPOPTS -p +gap> # Answers that come up short, in package mode. +gap> # Generated by dev/make-window-cmd-tests.sh -- do not edit. +gap> # README-window-cmd.md explains the '@a' answers below, in particular +gap> # that every digit run is least significant digit first. +gap> got := WindowCmd(["TST"]);; +gap> +gap> Print("clamped length = ", Length(got[1]), "\n"); +clamped length = 20 +gap> Print("clamped content = ", got[1] = "0123456789abcdefghij", "\n"); +clamped content = true +gap> got := WindowCmd(["TST"]);; Print("eof entries = ", Length(got), "\n"); Print("eof length = ", Length(got[1]), "\n"); Print("eof prefix = ", got[1]{[1..11]} = "abcdefghij\n", "\n"); Print("eof zero filled = ", ForAll(got[1]{[100..Length(got[1])]}, c -> c = CHAR_INT(0)), "\n"); +eof entries = 1 +eof length = 4991 +eof prefix = true +eof zero filled = true +gap> \ No newline at end of file