# BST-TIMEOUT: 300 . "$BST_LIB" # hunspell 1.7.2 (meta-oe recipes-support/hunspell) -- the spell checker behind # LibreOffice, Firefox and Chrome. Three separable things are exercised here and # they fail differently: the AFFIX ENGINE (a dictionary is stored compressed, as # stems plus affix flags, and every surface form is generated at lookup time), # the SUGGESTION generator (edit distance, the REP table and n-gram scoring), # and the ISPELL PIPE PROTOCOL the editors actually speak. # # --------------------------------------------------------------------------- # THE DICTIONARY IS BUNDLED, AND THAT IS A SCOPING DECISION, NOT A SHORTCUT. # # meta-oe ships hunspell-dictionaries as a separate recipe pulling the # LibreOffice dictionary repository -- tens of megabytes of natural-language # data, none of it about wasm32. It is NOT installed on this image and this # port does not take it. data/hunspell-bst.{aff,dic} is a ten-entry dictionary # written for this suite: one prefix class, one suffix class with a two-branch # condition, a REP table and a TRY string. Small enough to reason about line by # line, and it still drives every code path a real dictionary does. # # If hunspell-dictionaries is ported later, nothing here changes: this suite # is what says the ENGINE works, and it would say so just the same. # # WHAT IS COMPARED BYTE-FOR-BYTE, AND WHY IT IS THE PIPE OUTPUT. # # `hunspell -a` is the ispell pipe protocol: one line per word -- `*` for a # dictionary hit, `& word N off: s1, s2, ...` for a miss WITH suggestions, # `# word off` for a miss with none -- and a blank line after each input line. # The suggestion list is an ORDERED set produced by edit-distance scoring, the # REP table and n-grams, and its order is decided by floating-point scores and # a sort. That is exactly the thing that goes quietly wrong on a new arm while # every yes/no answer stays right, which is why the ticket asks for it rather # than for the misspelling list alone. # # So the whole `-a` stream is compared against data/hunspell-native.txt, the # same hunspell 1.7.2 built from the same sources and run on x86-64 over the # same dictionary and the same corpus, with no tolerance band: one differing # line is a defect. There is no pointer, size, clock or path anywhere in that # stream, so nothing in it is ALLOWED to differ between the arms. The same is # done for `-l`, for analyze(1) and for unmunch(1). # # THE ANCHORED CASES EXIST BECAUSE AN ORACLE CAN BE WRONG. A control captured # from an already-broken build agrees with itself perfectly. So the answers the # affix file FIXES -- which words are in the dictionary, which are reachable # only through PFX A / SFX B, and that `f` -> `ph` is offered because the REP # table says so -- are ALSO asserted against literals below. Those hold whatever # the control says. # # THE ONE THING THAT DOES NOT WORK IS UPSTREAM'S, AND IT IS NOT ABOUT wasm32. # hunzip cannot read hzip's output -- on x86-64 either, measured with the same # sources. hzip itself was broken outright until this port's one patch # (fdopen(fd, "rw") is not a stdio mode, so every write to its scratch file # failed); with the patch it emits a well-formed hz0 stream, byte-identical to # x86-64's, which is what the hz cases below assert. hunzip is covered only on # the path that does work -- its header check -- rather than being given a # vacuous green for returning nothing. See the port's design doc. # --------------------------------------------------------------------------- # A CANARY, AND IT ASSERTS NOTHING ABOUT hunspell. brintos_local_verify.py loses # a suite's first BST result line before the host parser sees it (BRINTOS-1149, # our own host tooling) and counts the dropped line as a pass. Rather than let # it eat a real case, a case that claims nothing is put in front of it. It comes # out when 1149 closes. bst_ok hs/canary require hunspell hs/present AFF=$BST_DATA/hunspell-bst.aff DIC=$BST_DATA/hunspell-bst.dic BASE=$BST_DATA/hunspell-bst CORPUS=$BST_DATA/hunspell-bst.txt WORDS=$BST_DATA/hunspell-bst-words.txt O_PIPE=$BST_DATA/hunspell-native.txt O_LIST=$BST_DATA/hunspell-native-list.txt O_ANA=$BST_DATA/hunspell-native-analyze.txt O_UNM=$BST_DATA/hunspell-native-unmunch.txt O_HZ=$BST_DATA/hunspell-native-hz.bin t=${TMPDIR:-/tmp}/bst-hunspell.$$ rm -rf "$t"; mkdir -p "$t" || { bst_notok hs/scratch "cannot create $t"; exit 0; } cd "$t" || { bst_notok hs/scratch "cannot cd $t"; exit 0; } # has : ok iff contains that line exactly. has_line() { if [ -f "$2" ] && grep -qxF -- "$3" "$2" 2>/dev/null; then bst_ok "$1" else bst_notok "$1" "missing line '$3' in $2"; fi } # present present() { if [ -e "$2" ]; then bst_ok "$1"; else bst_notok "$1" "$2 missing from this image"; fi } # === 1. the tools and the payload ========================================== # # Seven binaries come out of src/tools and all seven are named, because a build # that dropped one would still install a working `hunspell` and read as green. # ispellaff2myspell is deliberately NOT among them: the recipe splits it into # hunspell-ispell (it is a perl converter for legacy ispell affix tables, a # dictionary-authoring tool with no part in checking spelling) and this image # does not install that package. Its absence is recorded as a skip rather than # left to be discovered. for tool in analyze chmorph munch unmunch hzip hunzip; do if command -v "$tool" >/dev/null 2>&1; then bst_ok "hs/have-$tool" else bst_notok "hs/have-$tool" "$tool missing from the image"; fi done if command -v ispellaff2myspell >/dev/null 2>&1; then bst_ok hs/have-ispellaff2myspell else bst_skip hs/have-ispellaff2myspell "hunspell-ispell is not installed on this image (by design)"; fi ARCHIVE=/usr/lib/libhunspell-1.7.a present hs/archive "$ARCHIVE" present hs/header-hunspell-hxx /usr/include/hunspell/hunspell.hxx present hs/header-hunspell-h /usr/include/hunspell/hunspell.h present hs/header-w-char /usr/include/hunspell/w_char.hxx present hs/header-atypes /usr/include/hunspell/atypes.hxx present hs/header-hunvisapi /usr/include/hunspell/hunvisapi.h present hs/pkgconfig /usr/lib/pkgconfig/hunspell.pc # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape hs/no-shared "/usr/lib/libhunspell-1.7.so*" # THE C++ ARM, WITNESSED IN THE SHIPPED ARCHIVE. Itanium-mangled Hunspell # members, and libc++'s inline namespace (__1) in the same symbols: a C compile # cannot produce either, so these are the on-target evidence that the recipe's # CXX opt-in reached the compiler rather than merely being written down. if grep -q '_ZN8Hunspell' "$ARCHIVE" 2>/dev/null; then bst_ok hs/cxx-mangled else bst_notok hs/cxx-mangled "no Itanium-mangled Hunspell members in $ARCHIVE"; fi if grep -q 'NSt3__1' "$ARCHIVE" 2>/dev/null; then bst_ok hs/cxx-libcxx else bst_notok hs/cxx-libcxx "no libc++ (__1) inline-namespace symbols in $ARCHIVE"; fi # === 2. identity =========================================================== check_out hs/version "Hunspell 1.7.2" hunspell -vv # === 3. the bundled dictionary and the four controls are staged ============ present hs/data-aff "$AFF" present hs/data-dic "$DIC" present hs/data-corpus "$CORPUS" present hs/data-words "$WORDS" present hs/data-oracle-pipe "$O_PIPE" present hs/data-oracle-list "$O_LIST" present hs/data-oracle-analyze "$O_ANA" present hs/data-oracle-unmunch "$O_UNM" present hs/data-oracle-hz "$O_HZ" # === 4. the misspelling list (`-l`) ======================================== # # THE CORPUS IS COPIED ONTO THE SCRATCH TMPFS FIRST, AND THAT IS NOT TIDINESS. # A command whose stdin the SHELL opens on the virtiofs root and which reads it # to EOF kills the invoking shell -- silently, with no status, no stderr and no # kernel line -- so the three `< "$CORPUS"` redirects below would end the suite # here and take every case after them with it, while the runner still reported # the cases it had already seen as a clean pass. That is the whole of # BRINTOS-WORKAROUND below; reading the same bytes from the tmpfs copy is the # issue's own measured row 5. Stdin stays the input channel, so these three # streams are byte-for-byte what the x86-64 control recorded -- which a filename # argument would not guarantee. # # BRINTOS-WORKAROUND: brintos/linux-in-the-browser#172 remove -- shell dies on a # virtiofs fd inherited as a child's stdin; drop the copy when the fd is fixed. cp "$CORPUS" corpus.txt || bst_notok hs/corpus-copy "cannot copy $CORPUS to scratch" # # The corpus is eight lines: dictionary words, three affix-generated forms, and # seven deliberate misses. -l prints one misspelled word per line and nothing # else, so the whole of it is both compared against the control and pinned as a # literal here. hunspell -d "$BASE" -l < corpus.txt > list.out 2>list.err check hs/list-matches-x86-64 cmp -s list.out "$O_LIST" printf 'speling\ncheking\nwrold\nhelo\nfone\ngraf\nqwzxjv\n' > list.exp check hs/list-literal cmp -s list.out list.exp check_eq hs/list-count 7 "$(wc -l < list.out | tr -d ' ')" check_eq hs/list-quiet "" "$(cat list.err)" # `-G` is the complement -- the words it ACCEPTS. Asserting both directions is # what separates a working checker from one that calls everything correct (which # would give an empty -l and pass every case above) or everything wrong. hunspell -d "$BASE" -G < corpus.txt > good.out 2>/dev/null printf 'hello\nworld\nbrowser\nkernel\nrespelled\nrechecked\nbrintos\nwasm\n' > good.exp check hs/good-literal cmp -s good.out good.exp check_eq hs/good-count 8 "$(wc -l < good.out | tr -d ' ')" # === 5. the ispell pipe protocol (`-a`) -- the port's central claim ======== hunspell -d "$BASE" -a < corpus.txt > pipe.out 2>/dev/null check hs/pipe-matches-x86-64 cmp -s pipe.out "$O_PIPE" # The shape of the stream, counted rather than eyeballed: eight accepted words, # six misses that drew suggestions, one that drew none. has_line hs/pipe-banner pipe.out "@(#) International Ispell Version 3.2.06 (but really Hunspell 1.7.2)" check_eq hs/pipe-star-count 8 "$(grep -c '^\*$' pipe.out)" check_eq hs/pipe-amp-count 6 "$(grep -c '^& ' pipe.out)" check_eq hs/pipe-hash-count 1 "$(grep -c '^# ' pipe.out)" # The suggestion lines themselves, byte for byte. The trailing number on a `&` # line is the BYTE OFFSET of the word in its input line, so these pin the # offsets as well as the suggestions -- an off-by-one in the tokenizer shows up # here and nowhere else. has_line hs/sug-speling pipe.out "& speling 1 0: spell" has_line hs/sug-cheking pipe.out "& cheking 1 8: check" has_line hs/sug-wrold pipe.out "& wrold 1 0: world" has_line hs/sug-helo pipe.out "& helo 1 6: hello" # THE REP TABLE IS THE POINT OF THESE TWO. Neither `fone`->`phone` nor # `graf`->`graph` is within one edit of its target by character operations; both # are reachable only through the affix file's `REP f ph`. A port that dropped # the REP table would still suggest for the four above and fail exactly here. has_line hs/sug-rep-fone pipe.out "& fone 1 0: phone" has_line hs/sug-rep-graf pipe.out "& graf 1 5: graph" # The `#` form: a miss with NO suggestion. A generator that always produced # something -- the other way this can go wrong -- fails here while every `&` # case above stays green. has_line hs/pipe-nosug pipe.out "# qwzxjv 0" # === 6. the affix engine =================================================== # # `spell/AB` and `check/AB` are the only two flagged stems. Everything asserted # here is a form the dictionary does NOT contain and the engine must GENERATE: # PFX A prepends `re`, SFX B appends `ed`, and both are declared Y (cross # product), so re+stem+ed is reachable too. `respelled` in the corpus is that # three-way form and it is accepted above. accepts() { _w=$2 if [ -z "$(printf '%s\n' "$_w" | hunspell -d "$BASE" -l 2>/dev/null)" ]; then bst_ok "$1" else bst_notok "$1" "'$_w' rejected; the affix engine did not generate it"; fi } rejects() { _w=$2 if [ "$(printf '%s\n' "$_w" | hunspell -d "$BASE" -l 2>/dev/null)" = "$_w" ]; then bst_ok "$1" else bst_notok "$1" "'$_w' accepted; it is not a form this affix file generates"; fi } accepts hs/affix-stem spell accepts hs/affix-sfx spelled accepts hs/affix-pfx respell accepts hs/affix-cross respelled accepts hs/affix-cross2 rechecked accepts hs/affix-pfx-check recheck # The negative half, and it is the half that catches an engine which has stopped # checking the affix CONDITION and simply accepts anything: `spelling` is not a # form SFX B generates (it appends `ed`/`ied`, never `ing`), and `unspell` is not # a form PFX A generates. rejects hs/affix-rejects-ing spelling rejects hs/affix-rejects-unpfx unspell # === 7. morphological analysis ============================================= # # analyze(1) decomposes a surface form into the flags and stem that produced it # -- the inverse of what section 6 checks, and the second thing the ticket names. analyze "$AFF" "$DIC" "$WORDS" > ana.out 2>/dev/null check hs/analyze-matches-x86-64 cmp -s ana.out "$O_ANA" has_line hs/analyze-cross ana.out "analyze(respelled) = fl:A st:spell fl:B" has_line hs/analyze-stem ana.out "stem(respelled) = spell" has_line hs/analyze-bare ana.out "analyze(hello) = st:hello" has_line hs/analyze-unknown ana.out "Unknown word." # === 8. unmunch -- the affix-compressed dictionary, expanded =============== # # unmunch(1) turns the ten stems plus their flags back into every surface form # the dictionary denotes. It is the only case here that exercises the whole # expansion at once, and its COUNT is asserted as well as its content: ten stems # where two carry AB expand to sixteen forms, and a cross-product that had # silently stopped producing one branch would still match a subset. unmunch "$DIC" "$AFF" > unm.out 2>/dev/null check hs/unmunch-matches-x86-64 cmp -s unm.out "$O_UNM" check_eq hs/unmunch-count 16 "$(wc -l < unm.out | tr -d ' ')" has_line hs/unmunch-cross unm.out respelled has_line hs/unmunch-sfx unm.out checked # === 9. hzip / hunzip ====================================================== # # hzip compresses a sorted dictionary (prefix-suffix encoding plus 16-bit # Huffman). It writes .hz BESIDE its input and uses /tmp for scratch, so # the input is copied into this scratch directory first -- the deployed rootfs # is read-only at browser runtime and hzip would otherwise fail for a reason # that has nothing to do with the codec. # # THE BYTES ARE COMPARED, not merely the exit status. Huffman code assignment # depends on a frequency sort, which is precisely the kind of thing that can # come out differently on another arm while still producing a "valid" stream. cp "$DIC" w.dic check_rc hs/hzip-rc 0 hzip w.dic check_eq hs/hzip-magic "hz0" "$(head -c 3 w.dic.hz 2>/dev/null)" check_eq hs/hzip-size 160 "$(wc -c < w.dic.hz 2>/dev/null | tr -d ' ')" check hs/hzip-matches-x86-64 cmp -s w.dic.hz "$O_HZ" # hunzip is covered ONLY on the path that works. Its header check rejects a file # that is not hz-format, with this exact message -- that exercises the Hunzip # decoder's magic/format handling and is a real assertion. It is NOT asked to # read hzip's output back: it returns nothing there, on x86-64 as well as here, # which is upstream's defect and not this port's. Asserting the empty result as # a pass would be a green case that means nothing. check_out hs/hunzip-rejects-nonhz "not in hzip format" hunzip w.dic # Its usage text is a rc=1 path, so it is checked by hand rather than through # check_out, which requires a zero exit. _o=$(hunzip 2>&1); _rc=$? case "$_o" in *"decompress a hzip file to the standard output"*) if [ "$_rc" -eq 1 ]; then bst_ok hs/hunzip-usage else bst_notok hs/hunzip-usage "usage printed but rc=$_rc, want 1"; fi ;; *) bst_notok hs/hunzip-usage "no usage text: $(_bst_1line "$_o")" ;; esac cd /; rm -rf "$t"