. "$BST_LIB" require mg mg/present # BST-TIMEOUT: 600 # eleven pty sessions, each paced by settle delays past a repaint # mg is the image's THIRD full-screen editor (58-nano.test has the first, # 58-joe.test the second), so what this suite exists to answer is not "does a # curses app run here" but the three things mg does that neither of those does: # # 1. mg READS THE DATABASE THROUGH THE TERMINFO API — setupterm() plus the # cursor_address / cursor_up / enter_ca_mode variables — where joe reads the # SAME entries through the termcap compatibility entry points. That makes mg # the control for brintos/linux-in-the-browser#152, which is specific to the # termcap half of the lookup: 58-joe.test carries three expected reds for it, # and the equivalent assertions here are expected GREEN. A red here and a red # there is a much larger defect than #152. # 2. IT REFUSES A TERMINAL IT CANNOT ADDRESS, BY NAME. Under TERM=dumb mg exits # 1 saying so, rather than painting at it. joe under dumb paints a degraded # screen and nano starts. This is the only case on the image that asserts a # terminfo lookup was consulted by observing a REFUSAL, which a fallback # guess cannot produce. # 3. IT PROMPTS ON THE ECHO LINE FOR THINGS THE OTHER TWO DECIDE SILENTLY — # a missing trailing newline, a missing directory. Those prompts are a second # keyboard consumer with its own read path, and they are what a blind payload # must answer or hang on. # # THE ONE THING THIS SUITE DOES NOT DO IS DRIVE mg NON-INTERACTIVELY, and that is # upstream's doing rather than this target's. See mg/batch-refused below. # --- the surface that needs no terminal -------------------------------------- # These run before the pty leg on purpose: if expect is ever absent, the `require` # below ends the file, and these have already reported. Every one of them is a # path that returns BEFORE main() reaches vtinit(), which is why they work at all # for a program that otherwise panics without a tty. # mg has no --version and no --help; the usage line is what an unknown option # prints, and __progname is what it names itself. check_out mg/usage "usage: mg [-nR] [-b file] [-f mode] [-u file] [+number] [file ...]" \ sh -c 'mg -Z 2>&1; exit 0' check_rc mg/usage-rc 1 mg -Z # -b and -u both name a startup file and are rejected together, before either is # opened. This one is asserted because it is the ONLY diagnostic on the -b path # that a caller can read: everything after it goes to the pty mg makes for itself # (see mg/batch-refused) and is therefore written to a terminal nobody holds. check_out mg/opt-exclusive "-b and -u are mutually exclusive." \ sh -c 'mg -b /dev/null -u /dev/null 2>&1; exit 0' # -u with an unreadable startup file, and -f with a function that is not in the # funmap: two different argument validations, both fatal, both diagnosed by name. check_out mg/startup-missing "Problem with file: /nonexistent/mg-startup" \ sh -c 'mg -u /nonexistent/mg-startup 2>&1; exit 0' check_out mg/unknown-fn "Unknown function \`no-such-mg-function'" \ sh -c 'mg -f no-such-mg-function /dev/null 2>&1; exit 0' # No terminal at all: mg checks isatty() on stdin AND stdout and refuses. This is # the assertion that says every case below genuinely needed the pty it was given. check_out mg/needs-tty "standard input and output must be a terminal" \ sh -c 'mg /dev/null < /dev/null 2>&1; exit 0' # THE BATCH MODE, AND WHY IT IS NOT THIS TARGET'S FAULT. # # `mg -b ` is upstream's own regress harness: it calls openpty()+login_tty() # to give itself a terminal and then runs as an mg script. It cannot work # on a Linux image, because tty.c:70 hardcodes the terminal NAME in batch mode -- # # if (batch == 1) tty = "pty"; else tty = NULL; # if (setupterm(tty, STDOUT_FILENO, &errret)) panic("Terminal setup failed"); # # -- and `pty` is an OpenBSD terminfo entry. It is not in ncurses' base set, not # in ncurses-terminfo-base which this image ships, and not in ncurses-term on a # Debian host either. So setupterm() fails and mg exits 1 before reading a line of # the script. VERIFIED ON BOTH ARMS: an x86_64 build of this same source, on a # host with a working openpty(), exits 1 on the identical script. That is the null # control, and it is what makes this NOT a wasm32 finding. # # The exit status is the whole of the observable behaviour: the panic message is # written to fd 2, which login_tty() has already pointed at the pty mg allocated # for itself and nobody reads. Asserted as 1 rather than as a message for that # reason. # # The port does not ship a `pty` entry to make this work. Inventing a terminal # name no other Linux distribution carries would make our image the odd one out # and would be a divergence in the image rather than a fix in mg. printf 'find-file "/dev/null"\n' > /tmp/bst-mg-batch.$$ check_rc mg/batch-refused 1 mg -b /tmp/bst-mg-batch.$$ rm -f /tmp/bst-mg-batch.$$ # --- the real test: a full-screen editor, driven over a real pty -------------- # Everything above is preamble; mg has no working batch mode, so expect supplies # the pty. 45-expect.test covers whether that machinery works at all. require expect mg/expect-present work=${TMPDIR:-/tmp}/bst-mg.$$ mkdir -p "$work" printf '%s\n' "hello world" > "$work/typed.txt" printf '%s\n' alpha bravo charlie > "$work/cut.txt" printf '%s\n' alpha bravo charlie > "$work/arrow.txt" printf '%s\n' alpha bravo charlie delta > "$work/find.txt" printf '%s\n' alpha bravo charlie > "$work/repl.txt" printf '%s\n' alpha bravo charlie > "$work/ro.txt" chmod 444 "$work/ro.txt" printf '%s\n' alpha bravo charlie > "$work/probe.txt" # naive cafe, two CJK ideographs and an astral-plane earth: 2-, 3- and 4-byte UTF-8 printf 'na\303\257ve caf\303\251 \344\270\255\346\226\207 \360\237\214\215\n' > "$work/utf8.txt" cp "$work/utf8.txt" "$work/utf8.want" out=$(expect "$BST_DATA/selftest_mg.exp" "$work" 2>&1); rc=$? case "$out" in *ALL-MG-OK*) all=1 ;; *) all=0 ;; esac if [ "$rc" -eq 0 ] && [ "$all" -eq 1 ]; then bst_ok mg/all-ok else bst_notok mg/all-ok "rc=$rc $(_bst_1line "$out")"; fi # Each session reached a real terminal (terminfo resolved out of /etc/terminfo, # curses init, first repaint) and left of its own accord with the status it should. for tag in M1start M2exit M3cutexit M4arrowexit M5findexit M6replexit \ M7newlineask M8nonlexit M9readonly M10roexit M11nodirask M12nodirexit \ M13utf8exit M14linuxexit M15dumbrefused M16dumbexit; do case "$out" in *"M-$tag OK"*) bst_ok "mg/$tag" ;; *) bst_notok "mg/$tag" "not OK in payload output (rc=$rc)" ;; esac done # paint : is the literal in that logged first paint? paint() { grep -qaF -- "$2" "$work/raw-$1.bin" 2>/dev/null; } # pcase pcase() { if paint "$3" "$4"; then _got=1; else _got=0; fi if [ "$_got" = "$2" ]; then bst_ok "$1"; else bst_notok "$1" "$5"; fi } esc=$(printf '\033') # --- THE TERMINFO VERDICT, AND WHY IT IS EXPECTED GREEN HERE ------------------ # mg calls setupterm() and then reads the terminfo VARIABLES (enter_ca_mode, # clr_eos, enter_standout_mode). 58-joe.test's three reds are the termcap-name # lookup returning an incomplete set for the same entries -- tgetstr("cl") and the # standout pair come back NULL -- and that half is not on mg's path at all. So # these assert the full paint, and a red here means the defect has spread from the # termcap entry points into terminfo itself, which is a strictly larger finding # than brintos/linux-in-the-browser#152 and must not be filed as that issue. # # The x86_64 control for all of them (same source, same 24x80 pty): xterm -> # alternate-screen enter, ESC[J and ESC[7m all present; linux -> ESC[J and ESC[7m # present, no alternate-screen enter. The entry FILES are not the same on the two # arms and must not be assumed to be -- see mg/ti-xterm-smcup. # smcup, with the SPELLING deliberately not pinned. This image's xterm entry is # ncurses 6.4's own, whose enter_ca_mode is the X11R6-vintage `\E7\E[?47h` (save # cursor, then the 47 alternate screen); a Debian host's `xterm` resolves instead # to the modern `\E[?1049h\E[22;0;0t`. Both are correct alternate-screen entries # and xterm.js honours both, so what is asserted is that mg emitted THE ENTRY'S # string — pinning the control host's bytes measured RED here against a paint that # was doing exactly the right thing, which is the mistake this comment exists to # stop the next reader repeating. if paint mg-xterm "$esc[?1049h" || paint mg-xterm "$esc[?47h"; then bst_ok mg/ti-xterm-smcup else bst_notok mg/ti-xterm-smcup "TERM=xterm paint carries no alternate-screen enter (neither ESC[?1049h nor ESC[?47h) — enter_ca_mode was not read from the entry" fi pcase mg/ti-xterm-clear 1 mg-xterm "$esc[J" \ "TERM=xterm paint carries no ESC[J — clr_eos was not read from the entry" pcase mg/ti-xterm-standout 1 mg-xterm "$esc[7m" \ "TERM=xterm paint carries no ESC[7m — the standout pair was not read from the entry" # smkx, and it is a PRECONDITION of mg/arrow-decoded below rather than a decoration. # mg puts the terminal into application-cursor mode at startup, and only then does # Down arrive as ESC O B, which is what the payload sends. If this goes red the # arrow case is measuring a sequence mg never asked for, so read the two together. pcase mg/ti-xterm-smkx 1 mg-xterm "$esc[?1h" \ "TERM=xterm paint carries no ESC[?1h — keypad_xmit was not read, so Down is not ESC O B" # THE DIFFERENTIAL, which is what separates reading the database from making an # ANSI guess: the linux entry defines no alternate screen, so mg must NOT emit # smcup under it while still clearing and still painting the mode line in reverse. # A build that had fallen back to a built-in guess would paint the two alike. # ...and the linux entry defines NO enter_ca_mode at all, so its paint must carry # neither spelling. Both are rejected rather than just the modern one: a paint # carrying the OTHER spelling would be mg reusing xterm's capability under a # terminal that does not define it, which is exactly the failure this case exists # to catch and is invisible to a check for one string. if paint mg-linux "$esc[?1049h" || paint mg-linux "$esc[?47h"; then bst_notok mg/ti-linux-no-smcup "TERM=linux paint carries an alternate-screen enter — the linux entry defines none, so the lookup was not per \$TERM" else bst_ok mg/ti-linux-no-smcup fi pcase mg/ti-linux-clear 1 mg-linux "$esc[J" \ "TERM=linux paint carries no ESC[J — clr_eos was not read from the linux entry" pcase mg/ti-linux-standout 1 mg-linux "$esc[7m" \ "TERM=linux paint carries no ESC[7m — standout was not read from the linux entry" # The mode line is mg's own composition over those capabilities, and it carries # the buffer state in its first three columns: `--` clean, `**` modified, `%%` # read-only. Asserted on the read-only paint because that is the one a WRITE # cannot produce by accident. pcase mg/modeline-name 1 mg-xterm "Mg: " \ "the first paint has no mode line — the screen was never composed" # --- what actually reached the disk (the verdicts that count) ---------------- # S1: two lines typed at the end of an existing buffer and saved through ^X^S, # in order, with the original line kept. check_out mg/typed-orig "hello world" sh -c "cat '$work/typed.txt'" check_out mg/typed-first "hello from mg" sh -c "cat '$work/typed.txt'" check_out mg/typed-second "second line" sh -c "cat '$work/typed.txt'" check_eq mg/typed-lines 3 "$(wc -l < "$work/typed.txt" 2>/dev/null | tr -d ' ')" # mg backs an existing file up to ~ the first time it saves it. That is a # create+write pair on the virtiofs root performed by the application rather than # by the shell, and its CONTENT must be the file as it was before the edit. check mg/typed-backup sh -c "[ -f '$work/typed.txt~' ]" check_eq mg/typed-backup-body "hello world" "$(cat "$work/typed.txt~" 2>/dev/null)" # S2: ^K^K on line 1 removed exactly that line and left the rest intact. check_eq mg/cut-lines 2 "$(wc -l < "$work/cut.txt" 2>/dev/null | tr -d ' ')" if grep -q '^alpha$' "$work/cut.txt" 2>/dev/null; then bst_notok mg/cut-removed "alpha still present after ^K^K + ^X^S" else bst_ok mg/cut-removed fi check_out mg/cut-kept "charlie" sh -c "cat '$work/cut.txt'" # S3: THE ARROW VERDICT. Down (ESC O B — see mg/ti-xterm-smkx) then ^K^K must take # line 2. alpha kept + charlie kept + bravo gone == the three bytes decoded as one # key. bravo present with alpha gone would mean the sequence never assembled and # the cursor stayed on line 1; a literal "OB" in the file would mean the tail was # inserted as ordinary text instead. check_eq mg/arrow-lines 2 "$(wc -l < "$work/arrow.txt" 2>/dev/null | tr -d ' ')" if grep -q '^bravo$' "$work/arrow.txt" 2>/dev/null; then bst_notok mg/arrow-decoded "bravo survived: Down (ESC [ B) did not move the cursor to line 2" else bst_ok mg/arrow-decoded fi check_out mg/arrow-kept-first "alpha" sh -c "cat '$work/arrow.txt'" check_out mg/arrow-kept-last "charlie" sh -c "cat '$work/arrow.txt'" if grep -q 'OB' "$work/arrow.txt" 2>/dev/null; then bst_notok mg/arrow-not-literal "the escape sequence was inserted as literal text" else bst_ok mg/arrow-not-literal fi # S4: ^S incremental search positioned the cursor on the third line, so the ^K^K # that followed took charlie and not line 1. A search that silently failed leaves # alpha gone instead, which is why both ends are asserted. if grep -q '^charlie$' "$work/find.txt" 2>/dev/null; then bst_notok mg/find-positioned "charlie survived: ^S did not move the cursor to the match" else bst_ok mg/find-positioned fi check_out mg/find-kept-first "alpha" sh -c "cat '$work/find.txt'" check_out mg/find-kept-last "delta" sh -c "cat '$work/find.txt'" check_eq mg/find-lines 3 "$(wc -l < "$work/find.txt" 2>/dev/null | tr -d ' ')" # S5: M-x replace-string replaced the one occurrence and nothing else. The # ESC-prefix decode is the point; the replacement is how it is observed. check_out mg/repl-done "BRAVO" sh -c "cat '$work/repl.txt'" if grep -q '^bravo$' "$work/repl.txt" 2>/dev/null; then bst_notok mg/repl-replaced "bravo survived M-x replace-string" else bst_ok mg/repl-replaced fi check_out mg/repl-kept-first "alpha" sh -c "cat '$work/repl.txt'" check_out mg/repl-kept-last "charlie" sh -c "cat '$work/repl.txt'" # S6: the answered newline prompt. 20 bytes = "no trailing newline" + the \n mg # added on `y`. Pinned as an exact byte count so that mg quietly adding one # WITHOUT asking -- which is what nano does -- would be a red rather than a match. check_eq mg/nonl-bytes 20 "$(wc -c < "$work/nonl.txt" 2>/dev/null | tr -d ' ')" check_eq mg/nonl-lines 1 "$(wc -l < "$work/nonl.txt" 2>/dev/null | tr -d ' ')" # S7: -R refused the keystrokes at the buffer as well as on the echo line. The # file must be byte-identical and must have acquired no backup, since mg only # writes one when it writes the file. check_eq mg/ro-unchanged "alpha bravo charlie" "$(tr '\n' ' ' < "$work/ro.txt" 2>/dev/null | sed 's/ $//')" if [ -e "$work/ro.txt~" ]; then bst_notok mg/ro-nobackup "mg wrote a backup for a file it was told to open read-only" else bst_ok mg/ro-nobackup fi # S8: answering n to "Missing directory, create?" left the tree untouched. mg # offering to mkdir the parent is a filesystem write the editor performs itself, # so declining it has to be observable as an absence. if [ -e "$work/nodir" ]; then bst_notok mg/nodir-declined "mg created $work/nodir after the prompt was answered n" else bst_ok mg/nodir-declined fi # S9: UTF-8 ROUND TRIP. mg is byte-oriented -- it has no Unicode tables of its own, # unlike joe -- so a load+save must return the buffer unchanged rather than # re-encoded. 2-, 3- and 4-byte sequences together. check mg/utf8-roundtrip sh -c "cmp -s '$work/utf8.txt' '$work/utf8.want'" check_eq mg/utf8-bytes 25 "$(wc -c < "$work/utf8.txt" 2>/dev/null | tr -d ' ')" rm -rf "$work"