# BST-TIMEOUT: 300 . "$BST_LIB" # hunspell-dictionaries (meta-oe recipes-support/hunspell) -- 63 .dic/.aff pairs # from wooorm/dictionaries at SRCREV 820a65e5. DATA, not code: the recipe # compiles nothing, so there is no wasm32 codegen question to ask about it. The # only question worth asking is whether the ported hunspell in this image reads # the data correctly, and that is what every case below does -- the data is # checked by the code that consumes it, never by a census of files. # # --------------------------------------------------------------------------- # SEVEN LANGUAGES ARE ON THIS IMAGE, NOT 63, AND THAT IS THE PORT'S ONE # PACKAGING DECISION. # # The full set is 237 MB of .dic/.aff (measured over the recipe's own SRCREV; # rw alone is 36 MB). This image boots in a browser tab over a cloud virtio-fs, # so the bbappend splits the recipe into one hunspell-dictionary- package # per language and the image names seven: en-us, en-gb, de, fr, es, ru, fy -- # 19.6 MB. The other 56 are built and go to the feed. `hsd/unshipped-*` below # asserts that absence rather than leaving it to be discovered, because a # package list that quietly grew back to 237 MB would otherwise read as green. # # WHY THESE SEVEN. Each one buys an assertion the others cannot: # en-us the ASCII baseline and hunspell's own default dictionary # en-gb the same language, different dictionary -- `colour` is a hit here and # a miss in en-us, which is the one case an oracle captured from the # WRONG dictionary cannot survive # de compounding, and sharp-s / umlauts in both the input and the output # fr accents plus the apostrophe and ligature (cœur) handling # es tilde-n, and a large affix table # ru a non-Latin script -- every headword two bytes per character, so a # UTF-8 decode that is wrong anywhere is wrong everywhere here # fy the one dictionary in the set whose .aff declared NO encoding; see # the encoding section below # # WHAT IS COMPARED BYTE-FOR-BYTE, AND WHY IT IS THE PIPE OUTPUT. # # `hunspell -a` is the ispell pipe protocol: `*` for a dictionary hit, # `& word N off: s1, s2, ...` for a miss with suggestions, `# word off` for a # miss with none. The suggestion list is an ORDERED set produced by # edit-distance scoring, the REP table and n-grams -- decided by # floating-point scores and a sort, which is exactly the thing that goes # quietly wrong on a new arm while every yes/no answer stays right. # # So for each language the whole `-a` stream over a fixed word list is # compared against data/hunspell-dict--native.txt, captured from the # same hunspell 1.7.2 sources (SRCREV 2969be99, the version this image's own # hunspell reports) built and run on x86-64 glibc 2.39 over the same dictionary # bytes this image installs. No tolerance band: one differing line is a defect. # There is no pointer, size, clock or path anywhere in that stream. # # 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 # DICTIONARIES fix -- which words each language accepts, and which correction it # offers for a named misspelling -- are also asserted against literals. Those # hold whatever the control says. # # --------------------------------------------------------------------------- # THE ENCODING QUESTION, AND THE ANSWER THIS SET ACTUALLY GIVES. # # The ticket asks for a non-UTF-8 dictionary to be checked. Measured over all # 63: there is no such dictionary here. wooorm/dictionaries normalises every # one to UTF-8 and 62 of the 63 .aff files say `SET UTF-8`. What there IS, is # one that declares NOTHING -- fy (Frisian) -- and hunspell's default when # there is no SET line is ISO8859-1. Stock, fy therefore rejects its own # headwords: `hûs` came back `& hûs 5 0: hts, hos, hbs, hys, h's`. No error, # no warning, no failure to load -- wrong verdicts, silently, which is the # failure mode the ticket names. # # The bbappend adds the missing `SET UTF-8` at install (only where absent, and # only when the data really is UTF-8). Two kinds of case cover it: # # hsd/aff-set-* every shipped .aff declares UTF-8 -- fy included, which # is the fix's own regression guard # hsd/fy-* fy now accepts the accented headwords it rejected # hsd/noset-* the MECHANISM, on this arm, from a 3-word fixture: # with no SET line hunspell reads UTF-8 bytes as Latin-1 # and offers `jûn` for `jûn`; with the line it does not. # # That last group is what stops the fy cases being vacuous. Without it, "fy # accepts hûs" is equally consistent with hunspell ignoring encodings # altogether -- the fixture shows the arm really does change behaviour on the # SET line, and shows it in 16 bytes rather than fy's 10 MB. # # LICENCE TEXTS: UPSTREAM SHIPS NONE, FOR ALL 63. # # The stock do_install copies `$LANG/LICENSE`; the repository names it # `license`, lowercase. So the install fails 63 times out of 63 and a # `|| echo` swallows it -- the package ships the data with none of the licence # texts its own LIC_FILES_CHKSUM checksums, and reports success. The bbappend # fixes the case; hsd/licence-* is the guard. et, eu and tr genuinely have no # licence file under any name and none of the three is shipped here. # --------------------------------------------------------------------------- DICT=/usr/share/hunspell require hunspell hsd/hunspell-present # --------------------------------------------------------------------------- # EVERY hunspell RUN BELOW PASSES `-i UTF-8`, AND THAT IS LOAD-BEARING. # # hunspell does not read or write UTF-8 because its dictionary says so. It # transcodes its I/O to the CHARSET OF THE LOCALE, via nl_langinfo(CODESET), and # this image's locale is C whatever LC_ALL says -- the glibc arm ships no locale # data at all (linux-in-the-browser#160), so setlocale() cannot leave C and # nl_langinfo(CODESET) answers ANSI_X3.4-1968. Under that charset the damage is # silent and total for anything outside ASCII. Measured on x86-64 with the same # 1.7.2 sources, forcing LC_ALL=C: # # LC_ALL=C printf 'Häuser\n' | hunspell -d de -a # & user 3 3: User, unser, -user <- the word was TRUNCATED to its # ASCII tail and then declared a # misspelling, at offset 3 # LC_ALL=C printf 'привет\n' | hunspell -d ru -a # <- NO LINE AT ALL. The word # vanishes from the pipe stream, # so the ispell protocol loses a # response its caller is waiting # for. # # SETTING A UTF-8 LOCALE IS NOT THE ANSWER HERE, AND THE FIRST VERSION OF THIS # SUITE ASSUMED IT WAS. `LC_ALL=C.UTF-8` was set at the top of the file on the # belief that glibc has carried C.UTF-8 as a BUILT-IN locale since 2.35 and so # needs no data files. It does not: C.UTF-8 is localedata like any other locale, # installed under /usr/lib/locale, and this image has no /usr/lib/locale at all. # The export therefore did nothing, and the first on-target run answered the # point exactly -- eighteen reds, every one of them the ANSI_X3.4-1968 signature # above (`error - iconv: ANSI_X3.4-1968 -> UTF-8`), on a port with nothing wrong # with it. # # `-i` is hunspell's own option for exactly this: it sets the I/O charset # directly and never consults the locale, so it needs no locale data. Under # LC_ALL=C with `-i UTF-8` every case below is correct on x86-64, and it is what # a caller of hunspell on this image has to do too. That is the ONE THING this # port asks of its callers and it is recorded in the test-plan entry as such. # # The locale gap itself is still pinned rather than papered over -- hsd/locale-* # below keeps asserting what a caller who sets neither gets, and # hsd/locale-utf8-one-line-* stays RED against linux-in-the-browser#160 so that # it flips green by itself the day the image grows locale data. # --------------------------------------------------------------------------- HS="hunspell -i UTF-8" if [ ! -d "$DICT" ]; then bst_skip hsd/present "no $DICT on this image -- hunspell-dictionary-* not installed" exit 0 fi bst_ok hsd/present SHIPPED="en_US en_GB de fr es ru fy" t=${TMPDIR:-/tmp}/bst-hunspell-dict.$$ rm -rf "$t"; mkdir -p "$t" || { bst_notok hsd/scratch "cannot create $t"; exit 0; } cd "$t" || { bst_notok hsd/scratch "cannot cd $t"; exit 0; } # present : ok iff exists and is non-empty. present() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "$2 missing or empty"; fi } # absent absent() { if [ -e "$2" ]; then bst_notok "$1" "$2 is installed -- the image took more than the seven it names" else bst_ok "$1"; fi } # === 1. the seven pairs arrived, and nothing else did ====================== for l in $SHIPPED; do present "hsd/dic-$l" "$DICT/$l.dic" present "hsd/aff-$l" "$DICT/$l.aff" done # A sample of the 56 that belong in the feed, picked as the three largest (rw is # 36 MB on its own) plus one small one, so a regression that reinstated the whole # set is caught by size and not only by name. for l in rw el el_polyton ko bg; do absent "hsd/unshipped-$l" "$DICT/$l.dic" done # The exact count follows from both halves above: 7 dictionaries, 7 affix files, # 7 licence texts. Nothing else installs into this directory. check_eq hsd/file-count 21 "$(find "$DICT" -type f 2>/dev/null | wc -l | tr -d ' ')" # === 2. the licence texts, which upstream drops ============================ for l in $SHIPPED; do present "hsd/licence-$l" "$DICT/LICENSE-$l" done # === 3. the install path is the one hunspell searches ====================== # # /usr/share/hunspell is the FIRST compiled-in entry of hunspell's search path # (src/tools/hunspell.cxx), so `-d en_US` must resolve with no DICPATH set. This # is the whole reason do_install puts the files there, and it is asserted rather # than assumed -- a datadir that moved would leave every dictionary present and # every one of them unreachable by name. check_out hsd/search-path-listed "$DICT/en_US" sh -c "echo hello | $HS -D -d $DICT/en_US 2>&1" out=$(echo hello | DICPATH= $HS -d en_US -a 2>&1 | tail -n +2 | head -1) check_eq hsd/bare-name-resolves '*' "$out" # === 4. the whole -a stream against a captured oracle, per language ======== # # `cat | hunspell` rather than `hunspell < file`: a stdin redirect the top-level # shell performs on the virtiofs root makes dup2(2) return 513 and ash exits, # ending the suite silently. A pipeline forks, which is safe. # BRINTOS-WORKAROUND: brintos/linux#73 remove -- when dup2(2) stops returning 513 # on the virtiofs root this can read from $BST_DATA directly again. # # TWO PROPERTIES OF HUNSPELL, NOT OF THE DICTIONARIES, DECIDE WHAT THIS SECTION # CAN ASSERT. Both were measured on this image and both are answered here rather # than left to be rediscovered as a red. # # (a) THE WORD HASH IS 32-BIT ON A 32-BIT ARM, SO THE SUGGESTION ORDER DIFFERS. # HashMgr::hash() accumulates into an `unsigned long` while the ROTATE macro # it calls (src/hunspell/htypes.hxx) is written for a 32-bit value. On LP64 # the "rotate" is not a rotate and the hash runs on into the upper 32 bits; # on an ILP32 target it is the 32-bit rotate that was meant. A different hash # is a different bucket layout, and ngsuggest picks its MAX_ROOTS roots by # walking that table, resolving equal scores by the order it meets them -- so # a borderline candidate lands on the other side. Three of the seven streams # move: en_GB drops `cor` from `color`, es drops `endorse` from `senor` and # answers `manana` with `malagana` for `mantuana`, ru gains `перевеет` for # `превет`. Narrowing that one variable (hv as a 32-bit unsigned, everything # else identical, x86-64, hunspell 1.7.2 at the SRCREV this image ships) # reproduces all three streams BYTE FOR BYTE, and leaves en_US, de, fr and fy # -- which carry n-gram suggestions too -- unchanged. # # So those three languages get a second oracle, `-ilp32.txt`, captured from # that build: it is this arm's correct answer and is what they are held to. # The x86-64 `-native.txt` stays beside it for the other four and as the # record of what the difference is. Both suggestion sets are valid, so there # is nothing to fix in the dictionaries and nothing to file -- it is a # property of hunspell's hash on a 32-bit arm. # # (b) THE SUGGESTION SEARCH IS WALL-CLOCK-BOUNDED, SO THE TAIL OF A LIST IS NOT # ASSERTABLE ON THIS GUEST. Hunspell::suggest() abandons each remaining # stage once `clock() > timelimit + TIMELIMIT_GLOBAL` -- a quarter second of # process time (src/hunspell/atypes.hxx). This guest is a browser runtime # whose Workers stall for seconds at a time (`hwjs-worker-stall ... loop # unavailable 5702ms` in this image's own boot log), and that stall is # charged to the process, so the later stages are cut off and the list is # truncated -- never reordered, never wrong, just short. It is not stable # from run to run either: two identical `fr` runs in the SAME boot answered # `garcon` with two suggestions and then with three. # # A byte-for-byte comparison of the whole stream is therefore a flaky # assertion, and this section does not make one. What is asserted instead is # everything the truncation cannot touch: the verdict for every word (`*`, # `&`, `#`) exactly, the miss offsets exactly, the suggestions the guest DID # offer exactly and in the oracle's order, and the count field against the # list it heads. Only a shortfall at the tail of a `&` line is tolerated, and # it is reported so a stream that quietly stopped suggesting anything is # visible in the capture rather than green. The corrections that matter are # asserted against literals in section 5, which the time limit does not reach # -- those come out of the edit-distance pass, long before the budget. pipe_cmp() { printf '%s\n@@SEP@@\n%s\n' "$2" "$3" | awk ' $0 == "@@SEP@@" { g = 1; next } { if (g) got[++ng] = $0; else want[++nw] = $0 } function head(s) { return substr(s, 1, index(s, ": ") - 1) } function list(s) { return substr(s, index(s, ": ") + 2) } END { if (nw != ng) { print "BAD stream is " ng " lines, oracle is " nw; exit } trunc = 0 for (i = 1; i <= nw; i++) { if (want[i] == got[i]) continue if (want[i] !~ /^& / || got[i] !~ /^& /) { print "BAD line " i ": want <" want[i] "> got <" got[i] ">"; exit } split(head(want[i]), wh, " "); split(head(got[i]), gh, " ") if (wh[2] != gh[2] || wh[4] != gh[4]) { print "BAD line " i ": word/offset differ -- want <" want[i] "> got <" got[i] ">"; exit } wl = list(want[i]); gl = list(got[i]) if (index(wl ", ", gl ", ") != 1) { print "BAD line " i ": suggestions are not the oracle prefix -- want <" wl "> got <" gl ">"; exit } if (gh[3] + 0 != split(gl, _s, ", ")) { print "BAD line " i ": count " gh[3] " does not head a list of " split(gl, _s, ", "); exit } trunc++ } print "OK " trunc }' } for l in $SHIPPED; do words=$BST_DATA/hunspell-dict-$l.txt # This arm's oracle where the hash moves the stream, x86-64's where it does not. oracle=$BST_DATA/hunspell-dict-$l-ilp32.txt [ -s "$oracle" ] || oracle=$BST_DATA/hunspell-dict-$l-native.txt if [ ! -s "$words" ] || [ ! -s "$oracle" ]; then bst_notok "hsd/pipe-$l" "suite fixture missing for $l" continue fi got=$(cat "$words" | $HS -d "$DICT/$l" -a 2>&1 | tail -n +2) want=$(cat "$oracle") verdict=$(pipe_cmp "$l" "$want" "$got") case "$verdict" in 'OK 0') bst_ok "hsd/pipe-$l" ;; 'OK '*) bst_ok "hsd/pipe-$l" echo "[note] hsd/pipe-$l: ${verdict#OK } suggestion list(s) truncated by hunspell's TIMELIMIT_GLOBAL on this guest" ;; *) bst_notok "hsd/pipe-$l" "${verdict#BAD }" ;; esac done # === 5. anchored answers the dictionaries fix ============================== # # `verdict ` -- `*` for a hit, `&` for a # miss with suggestions, `#` for a miss with none. verdict() { _v=$(echo "$3" | $HS -d "$DICT/$2" -a 2>&1 | tail -n +2 | head -1 | cut -c1) check_eq "$1" "$4" "$_v" } # `suggests ` -- the correction appears in the # offered list. suggests() { _s=$(echo "$3" | $HS -d "$DICT/$2" -a 2>&1 | tail -n +2 | head -1) case "$_s" in '&'*': '*) case ", ${_s#*: }," in *", $4,"*) bst_ok "$1" ;; *) bst_notok "$1" "'$4' not offered for '$3': $_s" ;; esac ;; *) bst_notok "$1" "'$3' was not reported as a miss with suggestions: $_s" ;; esac } # en-US / en-GB are the SAME language and disagree. An oracle captured from the # wrong dictionary passes section 4 and fails here. verdict hsd/en-us-hit en_US hello '*' verdict hsd/en-us-colour en_US colour '&' suggests hsd/en-us-colour-sug en_US colour color verdict hsd/en-gb-colour en_GB colour '*' verdict hsd/en-gb-color en_GB color '&' suggests hsd/en-gb-color-sug en_GB color colour suggests hsd/en-us-typo en_US rediculous ridiculous # Affix generation: `running`/`runner` are not headwords, they are produced from # `run` by the affix table at lookup time. verdict hsd/en-us-affix-ing en_US running '*' verdict hsd/en-us-affix-er en_US runner '*' # Multi-byte input AND multi-byte output. `Häuser` is a hit; the suggestion # offered for the ASCII-ised `Baeckerei` is the accented form, so the correction # path carries UTF-8 out as well as in. verdict hsd/de-hit de Häuser '*' verdict hsd/de-sharp-s de Straße '*' suggests hsd/de-umlaut-sug de Baeckerei Bäckerei suggests hsd/fr-accent-sug fr deja déjà verdict hsd/fr-ligature fr cœur '*' suggests hsd/es-tilde-sug es corazon corazón # Cyrillic: two bytes per character throughout, so a decode that is wrong # anywhere is wrong here. Case matters too -- the lowercase `москва` is a miss # whose correction is the capitalised headword. verdict hsd/ru-hit ru привет '*' suggests hsd/ru-sug ru превет привет verdict hsd/ru-case ru москва '&' suggests hsd/ru-case-sug ru москва Москва # === 6. the encoding declaration is honoured =============================== # # Every shipped .aff must declare UTF-8. For six of the seven that is upstream's # own line; for fy it is the one the bbappend adds, and this is where a lost fix # shows up. for l in $SHIPPED; do if grep -q '^SET[[:space:]]*UTF-8' "$DICT/$l.aff" 2>/dev/null; then bst_ok "hsd/aff-set-$l" else bst_notok "hsd/aff-set-$l" "$l.aff does not declare SET UTF-8 -- hunspell will read it as ISO8859-1"; fi done # fy's own headwords, which stock fy rejects. These are the behavioural half of # the case above: the declaration is worth nothing if the verdicts do not follow. verdict hsd/fy-accented-jun fy jûn '*' verdict hsd/fy-accented-hus fy hûs '*' verdict hsd/fy-accented-weze fy wêze '*' verdict hsd/fy-ascii fy sjen '*' verdict hsd/fy-nonword fy xqzzy '#' # THE MECHANISM, from a 3-word fixture, so the fy cases above cannot be vacuous. # Same data, same words, one line of difference between the two .aff files. # THE THREE NOSET CASES THAT READ THE UNDECLARED DICTIONARY ARE EXPECTED RED ON # THIS IMAGE, AND FOR THE SAME REASON AS THE LOCALE PAIR BELOW. With no SET line # hunspell takes the dictionary to be ISO8859-1 and asks iconv for it BY THAT # NAME; glibc converts ISO-8859-1 with a built-in and resolves the undashed # spelling through the gconv alias table, which lives in the locale data this # image does not ship -- so iconv_open fails outright and an # `error - iconv_open: ... -> ISO8859-1` line arrives where the verdict should # be. linux-in-the-browser#160 again. hsd/noset-declared-utf8 is unaffected: its # fixture declares UTF-8, so no conversion is asked for at all. NOSET=$BST_DATA/hunspell-dict-noset WITHSET=$BST_DATA/hunspell-dict-withset if [ -s "$NOSET.aff" ] && [ -s "$WITHSET.aff" ]; then v=$(echo 'hûs' | $HS -d "$WITHSET" -a 2>&1 | tail -n +2 | head -1 | cut -c1) check_eq hsd/noset-declared-utf8 '*' "$v" v=$(echo 'hûs' | $HS -d "$NOSET" -a 2>&1 | tail -n +2 | head -1 | cut -c1) check_eq hsd/noset-undeclared-miss '&' "$v" # The Latin-1 signature itself: the UTF-8 bytes of û read back as two # characters, so the correction hunspell offers for its own headword is the # mojibake. This is the exact shape of what stock fy did. s=$(echo 'hûs' | $HS -d "$NOSET" -a 2>&1 | tail -n +2 | head -1) case "$s" in *'hûs'*) bst_ok hsd/noset-mojibake ;; *) bst_notok hsd/noset-mojibake "expected the Latin-1 misread in the suggestion, got: $s" ;; esac # The ASCII headword is unaffected either way, which is why the defect is # silent: a smoke test over ASCII words passes on a dictionary that is broken # for every accented one. v=$(echo 'hat' | $HS -d "$NOSET" -a 2>&1 | tail -n +2 | head -1 | cut -c1) check_eq hsd/noset-ascii-unaffected '*' "$v" else bst_notok hsd/noset-declared-utf8 "suite fixture hunspell-dict-noset.{aff,dic} missing" fi # === 6b. the I/O charset, and the locale gap under it ======================= # # The first case is the one the whole file rests on: `-i UTF-8` gives the right # answer with the locale left exactly as the image leaves it. If this goes red # every multi-byte case above is meaningless and this line says why. n=$(echo 'Häuser' | $HS -d "$DICT/de" -a 2>&1 | tail -n +2 | grep -c '^[*&#]') check_eq hsd/io-utf8-one-line 1 "$n" v=$(echo 'Häuser' | $HS -d "$DICT/de" -a 2>&1 | tail -n +2 | head -1 | cut -c1) check_eq hsd/io-utf8-works '*' "$v" # And the next two say what a caller who passes neither `-i` nor a working # locale gets, which is the reason `-i` is not optional advice. The ispell pipe # protocol promises ONE result line per input word, and that is the promise the # C locale breaks -- in both directions, which is why two cases and not one: # # -i UTF-8 de/Häuser -> 1 line ru/привет -> 1 line # LC_ALL=C de/Häuser -> 2 lines ru/привет -> 0 lines # # Under C the ä is not an alphabetic byte, so `Häuser` is two words (`H`, then # `user`) and hunspell answers twice -- the caller reads the `*` for `H` as the # verdict on `Häuser` and is now one line out of step for the rest of the # stream. It is not a mangled answer, it is a DESYNCHRONISED one. n=$(echo 'Häuser' | LC_ALL=C hunspell -d "$DICT/de" -a 2>&1 | tail -n +2 | grep -c '^[*&#]') check_eq hsd/locale-c-splits-word 2 "$n" # And Cyrillic has no alphabetic byte at all under C, so the word disappears and # hunspell answers ZERO times -- the failure a pipe-protocol caller blocks on. # Invisible to anything that only inspects the lines it did receive. n=$(echo 'привет' | LC_ALL=C hunspell -d "$DICT/ru" -a 2>&1 | tail -n +2 | grep -c '^[*&#]') check_eq hsd/locale-c-drops-line 0 "$n" # THESE TWO ARE RED ON THIS IMAGE AND ARE MEANT TO BE. They ask for the thing a # caller should be able to do instead of passing `-i` -- set a UTF-8 locale -- # and the image cannot honour it: there is no /usr/lib/locale, so C.UTF-8 does # not resolve, setlocale() stays in C and hunspell answers as it does two cases # above (2 lines for de, 0 for ru). linux-in-the-browser#160, and nothing to do # with the dictionaries or with wasm32. They are left asserting the CORRECT # behaviour rather than today's, so they turn green by themselves when the image # grows locale data, and go red again if it loses it. n=$(echo 'Häuser' | LC_ALL=C.UTF-8 hunspell -d "$DICT/de" -a 2>&1 | tail -n +2 | grep -c '^[*&#]') check_eq hsd/locale-utf8-one-line-de 1 "$n" n=$(echo 'привет' | LC_ALL=C.UTF-8 hunspell -d "$DICT/ru" -a 2>&1 | tail -n +2 | grep -c '^[*&#]') check_eq hsd/locale-utf8-one-line-ru 1 "$n" # === 7. -l agrees with -a ================================================== # # `-l` prints just the misspellings. It is a different code path to the pipe # protocol and must select the same words; a disagreement means one of the two # is reading the dictionary differently. for l in en_US ru; do words=$BST_DATA/hunspell-dict-$l.txt from_a=$(cat "$words" | $HS -d "$DICT/$l" -a 2>&1 | tail -n +2 \ | sed -n 's/^[&#] \([^ ]*\) .*/\1/p' | sort) from_l=$(cat "$words" | $HS -d "$DICT/$l" -l 2>&1 | sort) check_eq "hsd/list-agrees-$l" "$from_a" "$from_l" done cd /; rm -rf "$t"