. "$BST_LIB" # BST-TIMEOUT: 2400 # TWO python3 starts, and the probe's corpus is the only # # thing here with real work in it. # # # # The two: a bare `import snowballstemmer', whose STDERR is # # its own assertion, and the probe -- ONE start for 72 # # measurements, among them 41417 stemmed words checked # # against Snowball's published output. Everything else # # reads the filesystem or dpkg with shell builtins, or # # greps one probe transcript. This package ships NO # # executable, so there is no program to run as a real # # process and no exit status to assert: nothing needs a # # third start. # # # # THE PROBE'S WORK IS ~55000 STEM CALLS, measured at 1.35s # # on the x86-64 control. The bound is not sized from that # # figure but from the interpreter start, which is tens of # # seconds on this arm before the first line of the program # # runs, and from not knowing this arm's slowdown factor on # # a pure-Python inner loop -- which is what a stemmer is, # # and the one thing here that is NOT dominated by the # # start. 2400 leaves the corpus a factor of ~1500 over the # # control before it can time out. Nothing in this file is # # quadratic in anything: the corpus is one pass per # # language plus two more over the first 5000 English words. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed goes in the # # test plan rather than in this comment, which would cost an # # image build to keep the shipped suite byte-identical. # # python3-snowballstemmer 2.2.0 (poky/meta # recipes-devtools/python/python3-snowballstemmer_2.2.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS. # ============================================================================ # # The Snowball stemming algorithms compiled to Python: 29 stemmers for 28 # languages, each one reducing an inflected word to its stem so that a search # for `connection' also finds `connected'. `english' is Porter2, the algorithm # Sphinx builds its search index with; `porter' is Dr Porter's ORIGINAL 1980 # algorithm, kept for reproducing old results and NOT a synonym for it. The # generated stemmer modules are the whole package -- there is no compiler here, # only its output. # # ============================================================================ # THE PURE-PYTHON PATH IS THE ONE UNDER TEST, AND IT IS CHOSEN AT IMPORT TIME. # ============================================================================ # # `__init__.py' opens with `try: import Stemmer' -- the PyStemmer C extension -- # and sets `cext_available' from whether that worked. When it works, # `algorithms()' and `stemmer()' BOTH DELEGATE to the C library, and every # corpus case below would be measuring PyStemmer rather than the Python this # package ships. # # On this image python3 is one static binary with an empty lib-dynload and no # dynamic loader (BRINTOS-1476), so PyStemmer could not load even if it were # installed. But "could not" is a premise, and a premise is not a result: the # suite asserts `cext-available' is `no' and that the import raises ImportError # specifically. Without that pair, a green corpus would be evidence about an # unknown implementation. # # The `ImportError' half is the sharper of the two. `__init__.py' catches ONLY # ImportError, so an import that failed any other way -- which is exactly the # shape BRINTOS-2046 describes, a CPython extension refused at `dlopen' on this # image -- would propagate straight out of `import snowballstemmer' and take the # whole package down. That the failure is the ordinary "no such module" one is # a fact about this image worth holding. # # ============================================================================ # THE CORPUS IS SNOWBALL'S OWN PUBLISHED TEST DATA, PINNED TO THE RELEASE. # ============================================================================ # # `python3-snowballstemmer-corpus.txt' carries `wordexpected-stem' pairs cut # from snowball-data, the project's own published vocabulary/output pairs, at # commit 0703f1d (2021-09-07) -- THE LAST COMMIT BEFORE snowball v2.2.0 WAS # TAGGED (2021-11-10), which is the release this package is generated from. # # That pin is the load-bearing part, and it was established by measurement # rather than assumed. On the x86-64 control, snowball-data at HEAD disagrees # with snowballstemmer 2.2.0 on 57 English words, 838 French and 1265 German: # the data tracks the algorithms, and the algorithms moved after 2.2.0. A # fixture cut from HEAD would have produced a suite that was red on this image # for reasons having nothing to do with this image -- the worst kind of red, # because it looks exactly like a port defect. # # At 0703f1d, every one of the 28 corpora this package has an algorithm for # agrees with it EXACTLY -- 0 mismatches over 1.6 million words, checked on the # control before the fixture was cut. So a red here is a statement about this # image and nothing else. That is what justifies carrying 759 KB of fixture. # # English is carried WHOLE, 29417 pairs, because Porter2 is the algorithm the # port is really about. The other ten are STRIDED samples of 1200 -- strided and # not head, so a sample spans the whole alphabet instead of the words beginning # with `a', which is the difference between testing a stemmer and testing its # first few hundred prefixes. The ten are chosen for the scripts and rules they # exercise rather than for coverage of the roster: `porter' (the original, to # hold it apart from english), french and german and spanish (Latin-1 accents # and umlauts), russian and greek and armenian (three non-Latin scripts), # turkish (the dotted/dotless `i', the classic case-folding trap), hindi # (Devanagari), irish (its own initial mutations). # # WHAT A DIGEST WOULD NOT HAVE TOLD US. The probe reports a SHA-256 over every # stem it produced, which is the assertion that catches a single wrong word # anywhere in the corpus -- but it also reports `corpus_first_bad', the first # disagreeing word with what was wanted and what was got, and a per-language # count. A digest alone says the corpus moved without saying which word moved # it, and the difference between those two is whether the next reader has to # reproduce a 41417-word run to learn anything. # # ============================================================================ # THE ROSTER IS ASSERTED AS A COUNT, AN ORDER AND A DIGEST. # ============================================================================ # # `algorithms()' is `list(_languages.keys())' over a dict literal in # `__init__.py'. The failure this port is exposed to is a packaging step that # dropped stemmer modules: `stemmer("english")' would go on working while a # dozen languages had quietly gone, and any hand-written sample of names would # pass straight through that. So the roster is reduced to a digest over its # sorted names, with the count beside it to say which way a mismatch moved, and # each of the 32 shipped `.py' files asserted present by name as well. # # Three further cases say the roster is REAL rather than 29 names resolving to # one class: every name constructs, the 29 objects have 29 distinct classes, and # each class comes from its OWN module (`snowballstemmer._stemmer'). # # ============================================================================ # TWO UPSTREAM QUIRKS ARE PINNED HERE RATHER THAN REPORTED ANYWHERE. # ============================================================================ # # `__all__' IS WRONG. It is `('language', 'stemmer')', and the module defines # `algorithms' and `stemmer'. There is no `language' -- that is PyStemmer's name # for the same thing -- so `from snowballstemmer import *' raises AttributeError. # Asserted as AttributeError, so a future upstream fix shows up as a red that # says what changed rather than as a silent behaviour change. # # STEMMING IS NOT IDEMPOTENT, and the suite pins the NUMBER rather than claiming # the property. Re-stemming the stems of the first 5000 English words changes # 165 of them: Porter2 is not defined to be a fixed point, and a caller who # assumed it was would be wrong. A pinned count is a regression signal; a # pinned property would have been a false claim. # # Neither is reported upstream. We do not file against third-party public # repositories -- see docs/conventions/no-public-upstream-reports.md -- and for # a behaviour we merely depend on, a pinned assertion is the durable record. # # ============================================================================ # THE ISO CODES ARE NOT ACCEPTED HERE, AND THAT IS A PORTABILITY TRAP. # ============================================================================ # # `modules.txt' lists `english,en,eng' and the like, and PyStemmer accepts all # three. `stemmer()' on the pure-Python path lowercases its argument and looks # it up in `_languages', WHICH IS KEYED BY FULL LANGUAGE NAMES ONLY -- so # `stemmer("en")' raises KeyError. A caller who wrote that because the C # extension took it gets a KeyError and nothing else. Both halves are measured: # `English' and `EnGlIsH' resolve (the lowercasing works), `en', `eng' and `' # raise, and the KeyError's message is asserted too, because it is the only # thing that names WHICH algorithm was refused. # # ============================================================================ # THE STEMMER OBJECT IS REUSED ACROSS WORDS, WHICH IS A STATE HAZARD. # ============================================================================ # # A stemmer keeps its cursor state on the instance (`current', `cursor', `bra', # `ket'), and `stemWords()' drives ONE instance over the whole list. The failure # shape is a word leaving state behind that changes the next word's answer -- # the same defect python3-html2text's `handle()' has. It is checked three ways: # the same word stemmed twice in a row, a long word stemmed between two short # ones, and `stemWords(list)' asserted equal to a list comprehension over # `stemWord()' across 5000 words, which is the same instance driven in a # different order. # # ============================================================================ # THE PACKAGE'S SHAPE, AND ITS ONE DEPENDENCY. # ============================================================================ # # `Depends: python3-core' and NOTHING ELSE -- asserted as the whole line, not as # a substring, because the point is the absence. The recipe declares no # RDEPENDS at all and the scanner found nothing to add: the library imports # nothing outside itself. Nothing in this round or outside it gated this port. # # The wheel says `Root-Is-Purelib: true' and the .deb ships 32 `.py', their 32 # `.pyc' and a five-file dist-info -- 69 files, no shared object, no script in # /usr/bin. All of that is asserted, because "pure Python" is the premise the # whole port rests on and BRINTOS-2046 does not reach a package that has no # compiled extension to be refused. # # ============================================================================ P=python3-snowballstemmer # The canary: this line proves the suite was entered and its first case # reached, which is what tells a truncated run apart from an absent one. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/snowballstemmer DI=$SITE/snowballstemmer-2.2.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-snowballstemmer.list PROBE=$BST_DATA/python3-snowballstemmer-probe.py CORPUS=$BST_DATA/python3-snowballstemmer-corpus.txt tmp=${TMPDIR:-/tmp} W=$tmp/bst-snowballstemmer.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # --------------------------------------------------------------------------- # Where it landed. # --------------------------------------------------------------------------- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/package-init test -s "$PKGDIR/__init__.py" # setup.py maps package_dir snowballstemmer -> src/snowballstemmer, so a # flat module beside the package would mean the mapping was ignored. check $P/no-flat-module test ! -f "$SITE/snowballstemmer.py" # The 32 shipped modules by name: the 29 stemmers, plus the two the generated # code is built on. Named individually rather than counted, so a red says WHICH # language went missing -- which is the whole question when a roster shrinks. for _m in arabic armenian basque catalan danish dutch english finnish french \ german greek hindi hungarian indonesian irish italian lithuanian \ nepali norwegian porter portuguese romanian russian serbian spanish \ swedish tamil turkish yiddish; do check $P/stemmer-$_m test -s "$PKGDIR/${_m}_stemmer.py" done check $P/module-among test -s "$PKGDIR/among.py" check $P/module-basestemmer test -s "$PKGDIR/basestemmer.py" # The .pyc are built at packaging time, not at first import: this rootfs is # read-only to the guest, so a missing .pyc would be recompiled on every start # of every program that imports the package -- a cost, not a failure, and one # that would otherwise be invisible. check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-english test -s "$PKGDIR/__pycache__/english_stemmer.cpython-312.pyc" check $P/pycache-base test -s "$PKGDIR/__pycache__/basestemmer.cpython-312.pyc" # --------------------------------------------------------------------------- # What the .deb actually contains. # # The file list is copied under $TMPDIR first: the `while ... done < file' below # must run in THIS shell to keep the variable it sets, and a stdin redirect the # top-level shell performs on the virtiofs root makes dup2(2) return 513, at # which point ash prints `Unknown error 513' and EXITS -- ending the suite with # every case so far counted as a pass (brintos/linux#73). # --------------------------------------------------------------------------- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # Pure Python is the premise of this port: no shared object anywhere in the # package, so there is nothing for BRINTOS-2046 (an extension refused at # dlopen on this image) to reach. check_eq $P/pkg-no-so "" "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # And no executable: the package is a library only, which is why this suite # runs no shipped program and asserts no exit status. check_eq $P/pkg-no-bin "" "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # Nothing but .py, .pyc and dist-info shipped. Catches a stray data file or a # packaging step that swept in something from the source tree. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # 69 files: 32 .py, their 32 .pyc, and the five-file dist-info. check_eq $P/pkg-file-count "69" \ "$(grep -c '\.py$\|\.pyc$\|dist-info/' "$W/pkgf" 2>/dev/null)" # --------------------------------------------------------------------------- # dist-info. # --------------------------------------------------------------------------- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 2.2.0" cat "$DI/METADATA" check_out $P/dist-info-top-level "snowballstemmer" cat "$DI/top_level.txt" # `Root-Is-Purelib: true' is the wheel saying in its own words what every case # above infers from the file list. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # py2 AND py3 tags: the package is version-agnostic, which is why nothing here # depends on the interpreter being 3.12 beyond the .pyc paths. check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" # The licence ships INSIDE the dist-info here (as COPYING), which is not what # every pypi package does -- python3-webencodings ships none at all. Asserted # because a BSD-3-Clause package that lost its copyright file is a licensing # fact, not a cosmetic one. check $P/dist-info-copying test -s "$DI/COPYING" check_out $P/dist-info-copyright "Dr Martin Porter" cat "$DI/COPYING" # METADATA's own summary carries the two counts, generated by setup.py from # modules.txt: 29 stemmers, 28 languages. They are the package's own statement # of its roster size, and the probe's `algorithms_count' is asserted against # the same 29 independently. check_out $P/dist-info-summary "29 stemmers for 28 languages" cat "$DI/METADATA" # --------------------------------------------------------------------------- # dpkg's view. # --------------------------------------------------------------------------- sed -n '/^Package: python3-snowballstemmer$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 2.2.0-r0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE WHOLE Depends LINE, not a substring. The point of this case is the # ABSENCE: the recipe declares no RDEPENDS, the scanner found nothing to add, # and `python3-core' is what the build class supplies. A substring match would # pass just as happily with three more names on the line, and those three names # are exactly what would mean this port had acquired a gate it did not have. check_eq $P/pkg-depends-exact "Depends: python3-core" \ "$(grep '^Depends:' "$W/st" 2>/dev/null)" # --------------------------------------------------------------------------- # It imports, and it imports SILENTLY. # # The second case is not redundant: a DeprecationWarning or a failed lazy # import that upstream swallowed would leave the first case green while every # program using the package printed noise to stderr on every run. # --------------------------------------------------------------------------- check $P/import python3 -c 'import snowballstemmer' check_eq $P/import-quiet "" "$(python3 -c 'import snowballstemmer' 2>&1)" # --------------------------------------------------------------------------- # The probe. # --------------------------------------------------------------------------- check $P/corpus-present test -s "$CORPUS" if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # 2100s, inside the file's own 2400s bound: the probe must be killed and # reported by bst_probe -- which names the last line it printed, and so the # language it stopped in -- rather than by the runner killing the whole suite, # which would report nothing. bst_probe $P/probe-run "$W/probe" 2100 python3 "$PROBE" "$BST_DATA" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # `probe_done' is the LAST line the probe writes, so it is the only thing that # distinguishes a complete transcript from one cut off mid-corpus -- which would # otherwise read as a run whose later cases simply came back empty. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/emitted-count "72" "$(pv emitted_count)" # --- identity ------------------------------------------------------------- check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/import-ok "ok" "$(pv import)" check_eq $P/dist-version "2.2.0" "$(pv dist_version)" # The package carries no `__version__'; the dist-info is its only version # statement. Pinned so a release that adds one is noticed. check_eq $P/no-dunder-version "no" "$(pv has_dunder_version)" # --- the pure-Python path ------------------------------------------------- # Together these two say the corpus below measured THIS package's Python and # not a C extension that had shadowed it. check_eq $P/cext-unavailable "no" "$(pv cext_available)" check_eq $P/cext-import-error "ImportError" "$(pv stemmer_ext_import)" # --- __all__ names a function that does not exist ------------------------- check_eq $P/all-tuple "language,stemmer" "$(pv all_tuple)" check_eq $P/no-language-attr "no" "$(pv has_language_attr)" check_eq $P/has-algorithms "yes" "$(pv has_algorithms_attr)" check_eq $P/star-import-raises "AttributeError" "$(pv star_import)" # --- the roster ----------------------------------------------------------- check_eq $P/algorithms-count "29" "$(pv algorithms_count)" check_eq $P/algorithms-sorted "yes" "$(pv algorithms_sorted)" check_eq $P/algorithms-digest \ "da9c8974aefe4bb54c2163e815dea315a6bfc16ad5a3274f039b6cfa30b03356" \ "$(pv algorithms_digest)" # A fresh list per call: a caller that mutates the result must not corrupt the # roster for everyone else in the process. check_eq $P/algorithms-fresh-list "yes" "$(pv algorithms_fresh_list)" check_eq $P/constructed-count "29" "$(pv constructed_count)" check_eq $P/construct-failures "none" "$(pv construct_failures)" # 29 names, 29 distinct classes, 29 distinct modules, each class from its own # `_stemmer' module. Without these, 29 names all resolving to # EnglishStemmer would pass every count above. check_eq $P/distinct-classes "29" "$(pv distinct_classes)" check_eq $P/distinct-modules "29" "$(pv distinct_modules)" check_eq $P/class-module-match "none" "$(pv class_module_mismatch)" check_eq $P/api-complete "none" "$(pv api_missing)" # --- name resolution ------------------------------------------------------ check_eq $P/uppercase-name "EnglishStemmer" "$(pv uppercase_ok)" check_eq $P/mixedcase-name "EnglishStemmer" "$(pv mixedcase_ok)" check_eq $P/reject-unknown "KeyError" "$(pv reject_unknown)" check_eq $P/unknown-message "Stemming algorithm 'klingon' not found" \ "$(pv unknown_message)" # The ISO codes are a PyStemmer feature and raise here. Asserted so the trap is # a recorded fact rather than something a caller discovers in production. check_eq $P/reject-iso2 "KeyError" "$(pv reject_iso2)" check_eq $P/reject-iso3 "KeyError" "$(pv reject_iso3)" check_eq $P/reject-empty "KeyError" "$(pv reject_empty)" # --- the two calls, and instance reuse ------------------------------------ check_eq $P/stemword-type "str" "$(pv stemword_type)" check_eq $P/stemwords-type "list" "$(pv stemwords_type)" check_eq $P/stemword-running "run" "$(pv stemword_running)" check_eq $P/repeat-stable "yes" "$(pv repeat_stable)" # The same short word after a long one: cursor state left behind by # `generalization' would show up here and nowhere else. check_eq $P/after-long-word "run" "$(pv after_long_word)" check_eq $P/empty-word "[]" "$(pv empty_word)" check_eq $P/empty-list "[]" "$(pv empty_list)" check_eq $P/stemwords-equals-map "yes" "$(pv stemwords_equals_map)" check_eq $P/stemwords-map-n "5000" "$(pv stemwords_map_n)" # NOT a claim that stemming is idempotent -- it is not. The pinned count is a # regression signal; the property would have been a false statement. check_eq $P/idempotent-changed "165" "$(pv idempotent_changed)" # --- the interpreter's encodings ------------------------------------------ # The corpus is Devanagari, Greek, Cyrillic and Armenian as well as Latin, and # the probe reads it with an explicit encoding="utf-8" rather than the locale's. # # ONLY `default_encoding' IS ASSERTED, and deliberately. sys.getdefaultencoding() # is a Python 3 invariant -- it is `utf-8' whatever the locale -- so a red there # is a statement about the interpreter and cannot be a false alarm. # # `filesystem_encoding' and `preferred_encoding' are EMITTED AND NOT SCORED. # They are the first two values to read when the corpus goes red, which is why # the probe reports them; but they are properties of this image's locale rather # than of this package, the probe does not depend on either (it passes # encoding="utf-8" explicitly), and asserting them would risk a red that says # nothing about the port. `corpus-read' below is the case that actually covers # the reading. check_eq $P/default-encoding "utf-8" "$(pv default_encoding)" # --- the corpus ----------------------------------------------------------- check_eq $P/corpus-read "ok" "$(pv corpus_read)" check_eq $P/corpus-langs "11" "$(pv corpus_langs)" check_eq $P/corpus-order \ "english,porter,french,german,spanish,russian,greek,turkish,hindi,armenian,irish" \ "$(pv corpus_order)" # Per language: the count fixes that the fixture arrived whole, the bad count # fixes that it agreed. Both, because a corpus that failed to parse would # report zero mismatches over zero words and read as green. for _l in english porter french german spanish russian greek turkish hindi \ armenian irish; do check_eq $P/corpus-$_l-bad "0" "$(pv corpus_${_l}_bad)" done check_eq $P/corpus-english-n "29417" "$(pv corpus_english_n)" check_eq $P/corpus-porter-n "1200" "$(pv corpus_porter_n)" check_eq $P/corpus-armenian-n "1200" "$(pv corpus_armenian_n)" check_eq $P/corpus-total-n "41417" "$(pv corpus_total_n)" check_eq $P/corpus-total-bad "0" "$(pv corpus_total_bad)" # Names the first disagreeing word rather than leaving a digest to say only # that something moved. check_eq $P/corpus-first-bad "none" "$(pv corpus_first_bad)" # The digest over every stem produced: catches a single wrong word anywhere in # the 41417, including one the per-language counts would have to be read # together to find. check_eq $P/corpus-digest \ "8d71cb354958134a9c63770b787ed3b2a8864430f89d7ad8d13e645e2705cc44" \ "$(pv corpus_digest)" rm -rf "$W"