. "$BST_LIB" require dhex dhex/present # BST-TIMEOUT: 600 # Eight pty sessions with settle delays between keystrokes are the bulk of it; # measured ~95 s on an x86-64 control and the guest is a good deal slower, so the # declared bound carries the usual ~3x headroom over the worst rate the suites # have been observed at. The batch leg below is all sub-second. # dhex is a hex editor with a diff mode, and -- unusually for a curses editor -- # it has a REAL BATCH MODE. main() guards initscr() on # !(gosearch1 && search1.writesearchlog) # so `dhex -sa -w ` runs the whole search engine, the 64-bit # offset arithmetic and the config parser and then exits WITHOUT EVER OPENING A # SCREEN. Most of this file is built on that, and it is a better place to decide # the port than any screen scrape: the searchlog is a plain text file with a # four-line header and one %016llx per hit, so it is compared BYTE FOR BYTE. # # The pty leg (data/selftest_dhex.exp) covers what batch mode structurally cannot # -- that initscr() resolves $TERM out of /etc/terminfo, that the panes paint, # that keystrokes reach the buffer and that savechanges() writes the edited bytes # back. A build whose ncurses was a stub would pass every batch case here. # # EVERY INVOCATION IS WRAPPED IN `timeout`, for one measured reason: a DIRECTORY # argument hangs dhex outright. fopen(dir,"rb") succeeds on Linux, getfilesize() # then fseeks to a nonsense end, and searchfor() runs that many iterations. It is # upstream behaviour, reproducible off-target, and the wrapper is what keeps a # mistake here a red case rather than a hung sweep. dh() { timeout 20 dhex "$@" < /dev/null; } work=${TMPDIR:-/tmp}/bst-dhex.$$ mkdir -p "$work" home="$work/home"; mkdir -p "$home" # 94 bytes. "BR" sits at 0x00, 0x0e and 0x15 and nowhere else, which is what the # searchlog cases are written against; byte 7 is 0x35 and byte 8 is 0x33, which is # what the pty leg's -od case is written against. seed() { printf 'BRINTOS531dhexBR-two-BR-three-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!!' > "$1"; } for f in view edit keep ascii goto undo pristine; do seed "$work/$f.bin"; done # The same content behind seven bytes of padding: every "BR" moves by 7, which is # what makes the diff-mode pair's two searchlogs differ in a predictable way. printf 'GARBAGEBRINTOS531dhexBR-two-BR-three-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!!' > "$work/view2.bin" byte() { od -An -tx1 -j "$2" -N 1 "$1" 2>/dev/null | tr -d ' \n'; } # the hit lines of a searchlog, on one line, so an ORDER can be asserted as a # single string rather than case by case. hits() { sed -n '5,$p' "$1" 2>/dev/null | tr '\n' ' ' | sed 's/ $//'; } # --- the surface that exits inside the argument loop -------------------------- # -h, -v and -g are handled where the options are parsed, before the $HOME lookup # and before any file is opened, so none of them needs a config or a file. # -v is the ONLY thing dhex ever writes to stdout. Everything else -- the usage, # the licence, the occurrence count, every diagnostic -- goes to stderr, so the # empty-stdout case below is a real assertion about the tool's stream discipline # and not a restatement of the one above it. check_rc dhex/version-rc 0 dh -v check_eq dhex/version-out "0.69" "$(dh -v 2>/dev/null)" check_eq dhex/version-stderr-empty "" "$(dh -v 2>&1 >/dev/null)" check_rc dhex/help-rc 0 dh -h out=$(dh -h 2>&1) case "$out" in *"-sa, -SA, -sab, -SAB"*) bst_ok dhex/help-search ;; *) bst_notok dhex/help-search "$(_bst_1line "$out")" ;; esac case "$out" in *"Diff mode"*) bst_ok dhex/help-diffmode ;; *) bst_notok dhex/help-diffmode "$(_bst_1line "$out")" ;; esac check_eq dhex/help-stdout-empty "" "$(dh -h 2>/dev/null)" # No argument at all reaches the same helpscreen(argv[0],0) as -h, EXIT STATUS 0. # That is upstream's choice and it reads as a port defect to anyone expecting the # usual "usage -> 2"; pinning it is how a future upstream bump that changes it # becomes visible instead of silent. check_rc dhex/noargs-rc 0 dh check_rc dhex/qmark-rc 0 dh -? # -g prints the GPL out of gpl.c's string table. The anchors are one at each end, # because a truncated or misassembled table is the failure worth catching here. check_rc dhex/license-rc 0 dh -g out=$(dh -g 2>&1) case "$out" in *"GNU GENERAL PUBLIC LICENSE"*) bst_ok dhex/license-head ;; *) bst_notok dhex/license-head "$(_bst_1line "$out")" ;; esac case "$out" in *"END OF TERMS AND CONDITIONS"*) bst_ok dhex/license-terms ;; *) bst_notok dhex/license-terms "$(_bst_1line "$out")" ;; esac case "$out" in *"GNU Library General"*) bst_ok dhex/license-tail ;; *) bst_notok dhex/license-tail "$(_bst_1line "$out")" ;; esac # --- the config file ---------------------------------------------------------- # readconfigfile() returns 1 when the file is absent; main() then WRITES a default # one and turns that into keyboardsetupreq. In batch mode the wizard is never # reached, but the file is still written -- so this is the one case that exercises # the config WRITER, and it is also why dhex needs a writable home. HOME="$home" dh -sa BR -w "$work/cfg.log" "$work/view.bin" >/dev/null 2>&1 if [ -f "$home/.dhexrc" ]; then bst_ok dhex/config-created else bst_notok dhex/config-created "no .dhexrc under HOME=$home after a batch run"; fi case "$(head -1 "$home/.dhexrc" 2>/dev/null)" in "#DHEXCOLORSCHEME") bst_ok dhex/config-scheme ;; *) bst_notok dhex/config-scheme "first line: $(head -1 "$home/.dhexrc" 2>/dev/null)" ;; esac # -f names the config outright, and main() then never reads $HOME at all. That is # the escape hatch on an image whose root may be mounted read-only, and it is what # the pty leg uses, so it is asserted here rather than assumed there. check_rc dhex/config-f-nohome 0 env -u HOME timeout 20 dhex -f "$BST_DATA/selftest_dhex.dhexrc" -sa BR -w "$work/f.log" "$work/view.bin" # $HOME unset and no -f: dhex exits 1 BEFORE opening anything. out=$(env -u HOME timeout 20 dhex -sa BR -w "$work/nh.log" "$work/view.bin" &1); rc=$? case "$out" in *"HOME-variable not set"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 1 ] && [ "$hit" -eq 1 ]; then bst_ok dhex/nohome else bst_notok dhex/nohome "rc=$rc $(_bst_1line "$out")"; fi # A home it cannot create the config in. The directory is ABSENT rather than # read-only on purpose: this suite runs as root, and root ignores the mode bits, # so a chmod 500 would not refuse the write and the case would pass for the wrong # reason. ENOENT refuses everyone. out=$(HOME="$work/absent-home" timeout 20 dhex -sa BR -w "$work/uh.log" "$work/view.bin" &1); rc=$? case "$out" in *"i couldn't create it"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 1 ] && [ "$hit" -eq 1 ]; then bst_ok dhex/home-uncreatable else bst_notok dhex/home-uncreatable "rc=$rc $(_bst_1line "$out")"; fi # From here on every batch invocation names the config, so none of them depends on # the home directory the two cases above have just finished abusing. RC="$BST_DATA/selftest_dhex.dhexrc" db() { timeout 20 dhex -f "$RC" "$@" < /dev/null; } # --- the searchlog, byte for byte --------------------------------------------- out=$(db -sa BR -w "$work/a.log" "$work/view.bin" 2>&1); rc=$? check_eq dhex/search-rc 0 "$rc" check_eq dhex/search-count "3 occurances found in $work/view.bin" "$(_bst_1line "$out")" # The whole file, header included. The header records the search bytes in hex, so # comparing it is also how "-sa BR and -sh 4252 are the same search" is decided # below without re-deriving anything. printf '#DHEX SEARCHLOG\n#VERSION 0\n#Search was for 42 52\n#lines are all in hex\n000000000000000e\n0000000000000015\n0000000000000000\n' > "$work/a.expect" if cmp -s "$work/a.log" "$work/a.expect"; then bst_ok dhex/search-log-exact else bst_notok dhex/search-log-exact "$(_bst_1line "$(cmp "$work/a.log" "$work/a.expect" 2>&1)")"; fi check_eq dhex/search-log-size 122 "$(wc -c < "$work/a.log" | tr -d ' ')" # THE ORDER IS A WRAP, NOT A SORT, and that is the assertion. searchfor() starts # ONE BYTE PAST the cursor and wraps at the end of the buffer, so from the default # cursor 0 the match AT 0 is reported LAST. A port that quietly sorted, or that # lost the wrap, would still report three hits. check_eq dhex/search-order "000000000000000e 0000000000000015 0000000000000000" "$(hits "$work/a.log")" # The hex form of the same needle must produce the same file, header included -- # the header is written from search->searchstring, so an equal header means the # two option paths filled the same buffer. db -sh 4252 -w "$work/h.log" "$work/view.bin" >/dev/null 2>&1 if cmp -s "$work/h.log" "$work/a.log"; then bst_ok dhex/search-hex-same else bst_notok dhex/search-hex-same "-sh 4252 and -sa BR disagree"; fi # -ah adds a base address to every logged position and to nothing else, which is # the only 64-bit arithmetic in the port that reaches a file. db -ah 1000 -sa BR -w "$work/b.log" "$work/view.bin" >/dev/null 2>&1 check_eq dhex/search-baseaddr "000000000000100e 0000000000001015 0000000000001000" "$(hits "$work/b.log")" # -od moves the start, so the wrap rotates with it: from 20 the first hit past the # cursor is 0x15, then it wraps through 0 to 0x0e. Same three offsets, different # order -- which is what separates "the option reached searchfor()" from "the # option was parsed and dropped". db -od 20 -sa BR -w "$work/o.log" "$work/view.bin" >/dev/null 2>&1 check_eq dhex/search-cursor-order "0000000000000015 0000000000000000 000000000000000e" "$(hits "$work/o.log")" # -sab walks backwards, which is a third order over the same set. db -sab BR -w "$work/r.log" "$work/view.bin" >/dev/null 2>&1 check_eq dhex/search-backward-order "0000000000000015 000000000000000e 0000000000000000" "$(hits "$work/r.log")" # A needle that is not there still writes a well-formed log with no hit lines -- # an empty file here would be indistinguishable from a search that never ran. out=$(db -sa ZZZZ -w "$work/z.log" "$work/view.bin" 2>&1) check_eq dhex/search-absent-count "0 occurances found in $work/view.bin" "$(_bst_1line "$out")" check_eq dhex/search-absent-hits "" "$(hits "$work/z.log")" check_eq dhex/search-absent-header "#lines are all in hex" "$(sed -n '4p' "$work/z.log" 2>/dev/null)" # Degenerate inputs, both of which reach searchfor() and must come back rather # than loop: an empty buffer (the `while (!done && buf->bufsize)` guard) and a # needle longer than the file (the `buf->bufsize < search->searchlen` guard). : > "$work/empty.bin" out=$(db -sa BR -w "$work/e.log" "$work/empty.bin" 2>&1); rc=$? check_eq dhex/empty-rc 0 "$rc" check_eq dhex/empty-count "0 occurances found in $work/empty.bin" "$(_bst_1line "$out")" printf 'ab' > "$work/tiny.bin" out=$(db -sa BRINTOS -w "$work/t.log" "$work/tiny.bin" 2>&1); rc=$? check_eq dhex/needle-longer-rc 0 "$rc" check_eq dhex/needle-longer-count "0 occurances found in $work/tiny.bin" "$(_bst_1line "$out")" # --- diff mode, still without a screen ---------------------------------------- # Two files, two searches, two logs: the initscr() guard covers gosearch2 as well, # so this exercises the second tBuffer -- and the second buffer is where a port # that got the allocation wrong would show it, each one carrying a 1 MB data[] and # a 1 MB changes[] inline. out=$(db -s1a BR -w1 "$work/d1.log" -s2a BR -w2 "$work/d2.log" "$work/view.bin" "$work/view2.bin" 2>&1); rc=$? check_eq dhex/diff-rc 0 "$rc" check_eq dhex/diff-order1 "000000000000000e 0000000000000015 0000000000000000" "$(hits "$work/d1.log")" # Every offset moved by the seven padding bytes, and 0 is no longer a match, so # this log does NOT wrap -- the two files together say the wrap is a property of # where the hits are and not of the writer. check_eq dhex/diff-order2 "0000000000000007 0000000000000015 000000000000001c" "$(hits "$work/d2.log")" # -r reads the positions back out of a log instead of scanning. It is the only # consumer of the format dhex itself writes, so a header the reader could not # parse would show up as a missing offset rather than as a parse error. # # The DISTINCT set is asserted rather than the sequence, and that is deliberate: # re-searching from a log re-reports the last position, so the control emits four # lines for three offsets. Pinning the duplicate would be pinning an artefact of # searchfor()'s log cursor; pinning the set says the reader recovered every # position the writer put there, which is the property that belongs to the port. db -sa BR -r "$work/a.log" -w "$work/rl.log" "$work/view.bin" >/dev/null 2>&1 check_eq dhex/readlog-distinct "0000000000000000 000000000000000e 0000000000000015" \ "$(sed -n '5,$p' "$work/rl.log" 2>/dev/null | sort -u | tr '\n' ' ' | sed 's/ $//')" # --- the diagnostics that refuse ---------------------------------------------- out=$(db -sa BR -w "$work/m.log" "$work/no-such-file.bin" 2>&1); rc=$? case "$out" in *"error opening inputfile"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 1 ] && [ "$hit" -eq 1 ]; then bst_ok dhex/openfail else bst_notok dhex/openfail "rc=$rc $(_bst_1line "$out")"; fi # An unknown correlation algorithm is the one option error dhex exits NON-ZERO on. out=$(db -cz "$work/view.bin" "$work/view2.bin" 2>&1); rc=$? case "$out" in *"unknown correlation algorithm"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 1 ] && [ "$hit" -eq 1 ]; then bst_ok dhex/badcorr else bst_notok dhex/badcorr "rc=$rc $(_bst_1line "$out")"; fi # A bad hex needle complains and then falls through to the helpscreen, EXIT 0 -- # the same upstream shape as the no-argument case above. out=$(db -sh zz -w "$work/q.log" "$work/view.bin" 2>&1); rc=$? case "$out" in *"illegal character in hex searchstring"*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 0 ] && [ "$hit" -eq 1 ]; then bst_ok dhex/badhex else bst_notok dhex/badhex "rc=$rc $(_bst_1line "$out")"; fi # A DIRECTORY does not refuse, it HANGS -- see the `dh`/`db` wrappers at the top. # Asserted as "not 0" rather than as the control's 124, because a target whose VFS # refuses fopen() on a directory would exit 1 instead and that is strictly better # behaviour, not a regression. What must never happen is a silent success. # # On wasm32-hwjs it IS a silent success: measured rc=0, promptly, where the # x86-64 control loops until the 20 s bound. Same upstream defect either way -- # dhex has no S_ISDIR check and fopen("rb") on a directory succeeds on both -- # the arms differ only in what getfilesize() then reports, so this target exits # at once instead of grinding. The case is left RED deliberately: it is the # upstream bug this suite exists to keep visible, not a brintOS defect and not # something to assert away. timeout 20 dhex -f "$RC" -sa BR -w "$work/dd.log" "$work" < /dev/null > /dev/null 2>&1; rc=$? if [ "$rc" -ne 0 ]; then bst_ok dhex/dir-refused else bst_notok dhex/dir-refused "a directory argument exited 0"; fi # --- the editor itself, over a real pty --------------------------------------- require expect dhex/expect-present out=$(expect "$BST_DATA/selftest_dhex.exp" "$work" "$RC" 2>&1); rc=$? case "$out" in *ALL-DHEX-OK*) all=1 ;; *) all=0 ;; esac if [ "$rc" -eq 0 ] && [ "$all" -eq 1 ]; then bst_ok dhex/all-ok else bst_notok dhex/all-ok "rc=$rc $(_bst_1line "$out")"; fi for tag in D1render D2quit D3render D4savequery D5saveexit D6render D7discardquery \ D8discardexit D9render D10asciiquery D11asciiexit D12render D13gotoquery \ D14gotoexit D15render D16undoexit D17diffrender D18diffexit \ D19calcrender D20calcexit; do case "$out" in *"D-$tag OK"*) bst_ok "dhex/$tag" ;; *) bst_notok "dhex/$tag" "not OK in payload output (rc=$rc)" ;; esac done # --- what actually reached the disk ------------------------------------------- # savechanges() reopens the file "r+b" and fwrites ONE BYTE per recorded change at # its own offset, so `cmp -l` over the whole file catches the two ways a save can # be wrong without changing the length: a byte written at the wrong offset, and a # whole-buffer rewrite that drags the untouched bytes along. check_eq dhex/edit-byte0 41 "$(byte "$work/edit.bin" 0)" check_eq dhex/edit-size 94 "$(wc -c < "$work/edit.bin" 2>/dev/null | tr -d ' ')" check_eq dhex/edit-one-byte 1 "$(cmp -l "$work/edit.bin" "$work/pristine.bin" 2>/dev/null | wc -l | tr -d ' ')" check_eq dhex/edit-at-zero 1 "$(cmp -l "$work/edit.bin" "$work/pristine.bin" 2>/dev/null | awk 'NR==1{print $1}')" # "No" at the save dialog: the buffer was edited with the SAME keystrokes as above # and deliberately not written. if cmp -s "$work/keep.bin" "$work/pristine.bin"; then bst_ok dhex/discard-unchanged else bst_notok dhex/discard-unchanged "keep.bin differs from the seed after answering 'n'"; fi # TAB put the editor in the ASCII pane, so one "Z" is the byte 0x5A. In the hex # pane "Z" is not a digit at all and nothing would have been written. check_eq dhex/ascii-byte0 5a "$(byte "$work/ascii.bin" 0)" check_eq dhex/ascii-size 94 "$(wc -c < "$work/ascii.bin" 2>/dev/null | tr -d ' ')" # -od 8 moved the interactive cursor and the edit landed THERE. Byte 0 staying at # the seed's 0x42 is half the assertion: 0x43 would have landed at byte 0 had the # option been dropped, where it is distinguishable from the seed. check_eq dhex/goto-byte8 43 "$(byte "$work/goto.bin" 8)" check_eq dhex/goto-byte0 42 "$(byte "$work/goto.bin" 0)" check_eq dhex/goto-byte7 35 "$(byte "$work/goto.bin" 7)" check_eq dhex/goto-one-byte 1 "$(cmp -l "$work/goto.bin" "$work/pristine.bin" 2>/dev/null | wc -l | tr -d ' ')" # Undo removed the change before F10 could ask about it, so this file was never # written and the session exited with no dialog at all. if cmp -s "$work/undo.bin" "$work/pristine.bin"; then bst_ok dhex/undo-unchanged else bst_notok dhex/undo-unchanged "undo.bin differs from the seed after 'u'"; fi rm -rf "$work"