. "$BST_LIB" require joe joe/present # BST-TIMEOUT: 600 # fourteen pty sessions, each paced by settle delays past a slow repaint # joe is the image's SECOND full-screen editor (58-nano.test has the first), so the # question this suite exists to answer is not "does a curses app run here" -- nano # settled that -- but the two things joe does that nano does not: # # 1. joe READS THE TERMINFO DATABASE THE TERMCAP WAY, and is the first thing on # this image that does. 07-ncurses-terminfo-base.test can only check that the # entry files exist and carry the right magic, because the runner is piped; joe # resolves an entry on a real tty and paints from it, and paints DIFFERENTLY for # a different $TERM — which is what separates reading the database from making # an ANSI guess, since joe/termcap.c carries a complete fallback parser of its # own. It is also the consumer that found brintos/linux-in-the-browser#152: the # termcap-name half of that lookup returns an incomplete set on this arm. Three # cases below are that defect's expected reds. # 2. ONE BINARY, FIVE PERSONALITIES. joe/jmacs/jstar/jpico/rjoe are the same wasm # module reached through four symlinks; joe picks its rc file from argv[0], and # the rc file IS the key map. So the same keystroke means different things # depending on the name it was invoked under -- checked on the screen (each # personality's first paint advertises different keys) and on the disk (^O writes # a file under jpico where under joe it opens a menu and writes nothing). # --- 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. check_out joe/version "Joe's Own Editor v4.6" joe --help check_out joe/help-global "Global options:" joe --help check_out joe/help-local "Local options:" joe --help check_out joe/help-opt "guess_crlf" joe --help # joe --help walks the compiled-in option tables, so its length is a pure function of # the release: 131 lines, measured identical on an x86_64 build of the same tarball. check_eq joe/help-lines 131 "$(joe --help 2>&1 | wc -l | tr -d ' ')" # --- one binary, five names --------------------------------------------------- # Each symlink must resolve to joe and must report ITS OWN name in the usage line, # which is joe echoing argv[0] -- the same value it selects the rc file from. for p in jmacs jstar jpico rjoe; do if [ -L "/usr/bin/$p" ] && [ "$(readlink /usr/bin/$p)" = joe ]; then bst_ok "joe/$p-symlink" else bst_notok "joe/$p-symlink" "/usr/bin/$p is not a symlink to joe ($(ls -l /usr/bin/$p 2>&1 | tr -s ' '))" fi check_out "joe/$p-argv0" "Usage: $p " sh -c "$p --help 2>&1 | sed -n 3p" done # --- the rc files, which are what make the personalities differ --------------- for rc in joerc jmacsrc jstarrc jpicorc rjoerc ftyperc; do if [ -s "/etc/joe/$rc" ]; then bst_ok "joe/rc-$rc" else bst_notok "joe/rc-$rc" "/etc/joe/$rc missing or empty"; fi done # rjoe is "restricted joe" and is the one personality with no distinctive SCREEN -- # it advertises the same keys as joe, because what rjoerc changes is which commands # exist (no shell, no second file), not which keys reach them. So its rc is compared # by content instead: identical rc files would mean the restriction is not shipped. # Both must EXIST and differ: `cmp` on two absent files also reports "different", so # a bare cmp would go green on an image that shipped no rc at all. if [ ! -s /etc/joe/rjoerc ] || [ ! -s /etc/joe/joerc ]; then bst_notok joe/rjoerc-differs "one of /etc/joe/{rjoerc,joerc} is missing or empty" elif cmp -s /etc/joe/rjoerc /etc/joe/joerc; then bst_notok joe/rjoerc-differs "rjoerc is byte-identical to joerc — the restricted profile is not shipped" else bst_ok joe/rjoerc-differs fi # --- the data joe loads at runtime -------------------------------------------- # Counts, not existence: the syntax and colour sets are fixed by the 4.6 tarball, so a # number that moves means the packaging dropped files, which is invisible to `test -d`. check_eq joe/syntax-count 81 "$(ls /usr/share/joe/syntax/*.jsf 2>/dev/null | wc -l | tr -d ' ')" check_eq joe/color-count 9 "$(ls /usr/share/joe/colors/*.jcf 2>/dev/null | wc -l | tr -d ' ')" check joe/charmap-klingon sh -c '[ -s /usr/share/joe/charmaps/klingon ]' # --- the real test: a full-screen editor, driven over a real pty -------------- # joe has no batch mode, so everything above is preamble. expect supplies the pty; # 45-expect.test covers whether that machinery works at all. require expect joe/expect-present work=${TMPDIR:-/tmp}/bst-joe.$$ mkdir -p "$work" 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" # 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_joe.exp" "$work" 2>&1); rc=$? case "$out" in *ALL-JOE-OK*) all=1 ;; *) all=0 ;; esac if [ "$rc" -eq 0 ] && [ "$all" -eq 1 ]; then bst_ok joe/all-ok else bst_notok joe/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 exited 0 of its own accord under the key map its argv[0] # selected. for tag in J1start J2exit J3cutexit J4arrowexit J5picoexit J6macsexit J7utf8exit \ J8findexit J9lockheld; do case "$out" in *"J-$tag OK"*) bst_ok "joe/$tag" ;; *) bst_notok "joe/$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 } # --- THE TERMINFO VERDICT ------------------------------------------------------ # Split in two, because half of it holds on this arm today and half is held by an # open platform defect. # # WHAT HOLDS: joe consults the database per $TERM. Its paint under `dumb` differs # from its paint under `xterm` — no status banner, because `dumb` defines none of # the capabilities the banner needs. A joe that had fallen back to its own built-in # termcap parser (joe/termcap.c carries a complete one) would paint both alike, so # this is the case that says the lookup by $TERM happened at all. esc=$(printf '\033') pcase joe/ti-dumb-nobanner 0 joe-dumb "Joe's Own Editor 4.6" \ "TERM=dumb painted the status banner, which needs capabilities the dumb entry lacks" pcase joe/ti-xterm-banner 1 joe-xterm "Joe's Own Editor 4.6" \ "TERM=xterm painted no status banner — joe resolved no usable terminal" pcase joe/ti-dumb-plain 0 joe-dumb "$esc[7m" \ "TERM=dumb paint carries ESC[7m, an attribute the dumb entry does not define" # WHAT DOES NOT, AND WHY THESE THREE ARE EXPECTED REDS. # joe reads capabilities the TERMCAP way — tgetent() plus tgetstr() with the # two-letter names — and on wasm32-hwjs that path returns an INCOMPLETE set: # tgetstr("cl") and the standout pair come back NULL while other lookups from the # same entry succeed, so joe clears by emitting newlines and draws its status line # without reverse video. It is not the entry (the target's own /etc/terminfo/x/xterm # produces the full paint when x86_64 ncurses reads it), not the database, and not # joe (`less`, on the same target, pty and $TERM, gets smcup/cup/smso — it uses the # TERMINFO api, which is unaffected). # # BRINTOS-WORKAROUND: brintos/linux-in-the-browser#152 remove # — expected reds, not tolerated behaviour. Each asserts the value a WORKING # platform gives, so all three flip green by themselves when #152 lands, and # nothing here needs editing when it does. # # The x86_64 control for all three (same source, same entry file, same 50x160 pty): # xterm clear=\E[H\E[2J -> ESC[2J present, ESC[7m present # linux clear=\E[H\E[J -> ESC[J present ti152="brintos/linux-in-the-browser#152" pcase joe/ti-xterm-clear 1 joe-xterm "$esc[2J" \ "$ti152: TERM=xterm paint carries no ESC[2J — tgetstr(\"cl\") returned NULL" pcase joe/ti-linux-clear 1 joe-linux "$esc[J" \ "$ti152: TERM=linux paint carries no ESC[J — tgetstr(\"cl\") returned NULL" pcase joe/ti-xterm-rev 1 joe-xterm "$esc[7m" \ "$ti152: TERM=xterm paint carries no ESC[7m — the standout pair returned NULL" # NOT asserted: "TERM=linux must NOT carry xterm's ESC[2J". It passes today, and it # passes for the wrong reason — no entry yields a clear string at all while #152 # holds — so it would read as evidence of a differential that is not there. # --- THE PERSONALITY VERDICT ON SCREEN ---------------------------------------- # Each name loads its own rc, and the banner advertises that rc's keys. The pairs are # asserted in both directions: the key the personality DOES offer, and the one it does # NOT — a paint that carried both would mean the rc never took and joe fell back. pcase joe/screen-joe-exitkey 1 joe-xterm "Ctrl-K Q" \ "joe's banner does not offer Ctrl-K Q to exit" pcase joe/screen-macs-exitkey 1 jmacs "^X ^C" \ "jmacs' banner does not offer ^X ^C to exit — jmacsrc did not load" pcase joe/screen-macs-not-joe 0 jmacs "Ctrl-K Q" \ "jmacs' banner still offers joe's Ctrl-K Q — argv[0] did not select jmacsrc" pcase joe/screen-star-helpkey 1 jstar "Ctrl-J for help" \ "jstar's status line does not offer Ctrl-J for help — jstarrc did not load" pcase joe/screen-pico-menu 1 jpico "cut line" \ "jpico did not paint pico's key menu — jpicorc did not load" pcase joe/screen-pico-not-joe 0 jpico "Ctrl-K Q" \ "jpico's paint still offers joe's Ctrl-K Q — argv[0] did not select jpicorc" pcase joe/screen-rjoe-starts 1 rjoe "Joe's Own Editor 4.6" \ "rjoe painted no banner — the fifth name does not start" # --- what actually reached the disk (the verdicts that count) ---------------- # S1: text typed at the editor, saved through the ^K D PROMPT (not the promptless # ^K X), in the right order and nothing else. The prompt line is its own code path. check_out joe/typed-first "hello from joe" sh -c "cat '$work/typed.txt'" check_out joe/typed-second "second line" sh -c "cat '$work/typed.txt'" # 26 bytes and ONE newline: "hello from joe\n" + "second line". joe writes the buffer # as it stands and does NOT append a terminating newline to a last line that has none # — which is where it differs from nano, whose equivalent case counts two. Pinned as # an exact byte count so a silent "helpfully add \n" would be a red rather than a # rounding of the line count. check_eq joe/typed-bytes 26 "$(wc -c < "$work/typed.txt" 2>/dev/null | tr -d ' ')" check_eq joe/typed-lines 1 "$(wc -l < "$work/typed.txt" 2>/dev/null | tr -d ' ')" # S2: ^Y on line 1 removed exactly that line and left the rest intact. check_eq joe/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 joe/cut-removed "alpha still present after ^Y + ^K X" else bst_ok joe/cut-removed fi check_out joe/cut-kept "charlie" sh -c "cat '$work/cut.txt'" # joe 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, not the shell, # and its CONTENT must be the file as it was before the edit. check joe/cut-backup sh -c "[ -f '$work/cut.txt~' ]" check_out joe/cut-backup-orig "alpha" sh -c "cat '$work/cut.txt~'" # S3: THE ESCAPE-SEQUENCE CASE, AND WHAT IT DELIBERATELY DOES NOT ASSERT. # # The session sends the Down arrow (ESC [ B) and then ^Y. What it asserts is that the # three bytes were consumed AS A SEQUENCE: nothing resembling "[B" reaches the file. # If the ESC had failed to join its bracket, joe would have inserted the tail as # ordinary text, and this is the case that would see it. # # WHAT IS NOT ASSERTED IS WHICH LINE THE FOLLOWING ^Y TOOK, and that omission is # measured, not assumed. Across five runs of this exact session on this arm the # outcome was NOT reproducible: once bravo was cut (the correct decode), twice # nothing was cut at all, once charlie was cut, and once the ^Y was lost entirely. # Widening the gap between the arrow and the ^Y from 2 s to 8 s did not settle it. # # joe decides whether an ESC is standalone or the head of a sequence with a TIMED # READ (select(2) with a timeout, joe/tty.c), and this arm has an open defect # covering exactly that -- brintos/linux-in-the-browser#120 names select/poll # timeouts in its impact list. A cursor-position assertion sitting on top of that is # a coin flip, and a case that is green one run in five is worse than no case: it # reads as an intermittently-self-fixing defect. So the stable half ships and the # unstable half is recorded in the test plan (section 79) instead of being asserted # here. 58-nano.test's arrow case is not affected -- nano decodes keys through # ncurses, not through its own timed reader. if grep -q '\[B' "$work/arrow.txt" 2>/dev/null; then bst_notok joe/arrow-not-literal "the escape sequence was inserted as literal text" else bst_ok joe/arrow-not-literal fi # S5/S6: THE PERSONALITY VERDICT ON DISK. ^O is jpico's write-out and ^X^S is jmacs'. # Under joerc, ^O opens the options menu and ^X is "next word" — neither writes a # file, so these two files existing with this content is argv[0] selecting the rc. check_out joe/pico-written "pico line" sh -c "cat '$work/pico.txt'" check_out joe/macs-written "emacs style" sh -c "cat '$work/emacs.txt'" # S7: UTF-8 ROUND TRIP. joe carries its own Unicode tables (unicat-9.0.0.c) and # decodes the buffer to characters rather than bytes, so a load+save is a real # decode/encode cycle. 2-, 3- and 4-byte sequences must come back byte-identical. check joe/utf8-roundtrip sh -c "cmp -s '$work/utf8.txt' '$work/utf8.want'" check_eq joe/utf8-bytes 25 "$(wc -c < "$work/utf8.txt" 2>/dev/null | tr -d ' ')" # S8: ^K F search positioned the cursor on the matching line, so the ^Y that followed # took bravo and not line 1. A search that silently failed leaves alpha gone instead. if grep -q '^bravo$' "$work/find.txt" 2>/dev/null; then bst_notok joe/find-positioned "bravo survived: ^K F did not move the cursor to the match" else bst_ok joe/find-positioned fi check_out joe/find-kept-first "alpha" sh -c "cat '$work/find.txt'" check_out joe/find-kept-last "delta" sh -c "cat '$work/find.txt'" # S9: joe holds an edit lock while a modified file is open -- a SYMLINK named # .# whose target is user@host.pid -- and removes it when the session ends # cleanly. The payload asserted it exists mid-session (J9lockheld); this asserts it is # gone, which together make a symlink create+unlink on the virtiofs root, by an # application rather than by the shell. if [ -e "$work/.#typed.txt" ] || [ -L "$work/.#typed.txt" ]; then bst_notok joe/lock-released "joe left its .#typed.txt edit lock behind after a clean exit" else bst_ok joe/lock-released fi rm -rf "$work"