. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps a line out of the transcript the # # third start left behind. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for a hundred and forty measurements. # # # # The work inside the probe is building a fourteen-entry # # fixture in tmpfs and walking it a couple of dozen times, # # plus some string matching. Microseconds beside an # # interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # 1200 is 400s per start, the same per-start budget # # 99-python3-expandvars and 99-python3-imagesize set for # # their three, and roughly six times the ~60s a python3 # # start has measured at here. Nothing in this suite is # # quadratic in anything, so the start is the only thing the # # number has to cover. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-pathtools3 0.2.1 (meta-openembedded/meta-python # recipes-devtools/python/python3-pathtools3_0.2.1.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # Two modules under one package directory. `pathtools.patterns' matches paths # against include/exclude glob lists with a case-sensitivity switch -- # `match_path', `match_path_against', `match_any_paths', `filter_paths'. # `pathtools.path' walks and lists directories -- `walk', `listdir', # `list_directories', `list_files', `get_dir_walker', `absolute_path', # `real_absolute_path', `parent_dir_path'. # # It is `pathtools3', the maintained fork of the abandoned `pathtools', and it # keeps the old IMPORT name on purpose so it can be dropped in. Twelve public # functions, four source files, no configure step, no compiled extension and no # C anywhere in the package. # # NO PATCH AND NO SITE ANSWER. The recipe is meta-python's exactly as written -- # the port is one IMAGE_INSTALL line, this suite and its probe, with no bbappend # at all. MEASURED rather than read off the recipe: all four installed .py files # are BYTE-IDENTICAL to the sdist the SRC_URI sha256 pins. # # ============================================================================ # THE HEADLINE FINDING: `recursive=False' TURNS A SILENT EMPTY INTO A CRASH. # ============================================================================ # # `walk(d)' on a directory that does not exist yields NOTHING and says nothing -- # `os.walk' swallows the error, because its `onerror' is None. The same call with # `recursive=False' raises # # RuntimeError: generator raised StopIteration # # `get_dir_walker(False)' returns a GENERATOR whose body is `yield # next(os.walk(path))'. `os.walk' yields nothing, so `next()' raises # StopIteration inside a generator, and PEP 479 (Python 3.7+) converts that to # RuntimeError. The message names neither the path nor the real cause. # # `missing-recursive' and `missing-nonrecursive' assert the two answers side by # side, and `missing-nonrecursive-msg' pins the message that makes the mechanism # legible. This is the finding most likely to reach a caller: flipping # `recursive=False' to make a listing cheaper is an optimisation, and here it # converts a silently-empty result into an exception -- in the one situation # (the directory is gone) a caller is least likely to have covered. # # `file-root-recursive' / `file-root-nonrecursive' are the SECOND instance of the # same divergence, over an input a caller reaches by accident far more often: a # path that exists but is a FILE. Same split, same RuntimeError. # # ============================================================================ # THE SURFACE THIS VERSION SHIPS IS NOT THE ONE THE PORT WAS ASKED FOR. # ============================================================================ # # BRINTOS-2201 names `list_symlinks' among the functions to exercise. THERE IS NO # `list_symlinks' in pathtools3 0.2.1 -- `path.__all__' is eight names and that # is not among them. `has-list-symlinks' asserts its absence, so the gap is a # measurement in the record rather than a note somebody has to remember, and # `path-all' pins the whole list in order so that a release which added one says # so here. # # What this version does instead is classify symlinks through the two listings it # has, and the answers are not the ones a caller would guess. `os.walk' sorts by # `is_dir()', which FOLLOWS symlinks, so: # # broken-link-is-file a DANGLING symlink is a FILE (it cannot be stat'ed) # dir-link-is-dir a symlink TO A DIRECTORY is a DIRECTORY # dir-link-not-descended ...but is NOT walked into, `followlinks' being False # # ============================================================================ # THE CYCLE, AND WHY THIS SUITE CANNOT HANG ON IT. # ============================================================================ # # The fixture contains `cyc/self -> ../cyc', a genuine symlink cycle. It is # bounded ONLY by `followlinks' defaulting to False, and `walk-entered-cycle' # plus `walk-roots-count' are what prove the default holds: five roots, each # exactly once, none of them under the link. # # NOTHING IN THE PROBE EVER PASSES `followlinks=True'. That is deliberate and it # is the one rule the probe's own docstring states as a rule: a walker that # followed the link would recurse forever, and a hang costs the whole BATCH its # verify, where a wrong answer would cost one case. # # ============================================================================ # `filter_paths' DEFERS THE ValueError ITS SIBLINGS RAISE AT ONCE. # ============================================================================ # # A pattern appearing in both the include and the exclude list is a ValueError -- # `_match_path' intersects the two SETS and refuses before it matches anything, # so it fires even for a pathname that matches neither (`mp-conflict-irrelevant- # path'). # # But `filter_paths' is a GENERATOR and `match_path' / `match_any_paths' are # not. So the identical mistake raises AT THE CALL for two of them and only at # the first `next()' for the third: `fp-conflict-deferred' reaching its line at # all is the proof, and `fp-conflict-on-iter' is where the exception finally # appears. A caller who builds the generator inside a `try' and iterates it # outside catches nothing. # # And the conflict is decided AFTER lowercasing when `case_sensitive=False', so # `["*.py"]' against `["*.PY"]' is fine case-sensitively and raises when the # switch is turned off (`mp-conflict-case-sensitive-ok' beside # `mp-conflict-case-insensitive'). Turning off case sensitivity can therefore # break a call that worked. # # ============================================================================ # A BARE STRING WHERE A LIST BELONGS IS ACCEPTED, AND LIES. # ============================================================================ # # `match_path_against' iterates `set(patterns)'. Handed a STRING it iterates the # string's CHARACTERS, so `"*.txt"' becomes the four patterns `*', `.', `t', `x' # -- and `*' matches everything. `str-pattern-false-hit' asserts the True that # comes back for a path `*.txt' does not match. # # `str-pattern-spurious-conflict' is the same bug wearing the other face: two # DIFFERENT string patterns share the characters `*' and `.', so the intersection # is non-empty and the call raises "conflicting patterns" naming patterns the # caller never wrote. There is no type check anywhere on this path. # # ============================================================================ # THE SMALLER SURPRISES, EACH ASSERTED WHERE IT SITS. # ============================================================================ # # parent-trailing-slash-is-self `parent_dir_path("a/b/")' is "a/b" -- itself. # `dirname' of a trailing slash strips nothing. # parent-of-bare-name-is-cwd `parent_dir_path("top.py")' is the CWD, because # `dirname("top.py")' is "" and "" normpaths to ".". # parent-has-version `import pathtools' gives you a package with NO # public name at all: `__init__.py' is 1209 bytes # of licence header. `__version__' is not there. # version-type ...and where it does live, `pathtools.version', # it is a TUPLE `(0, 2, 1)', not a string. # dist-version-pathtools the DISTRIBUTION is `pathtools3' and the IMPORT # is `pathtools', so asking importlib.metadata for # the name you imported is a PackageNotFoundError. # mp-include-empty-list `[]' is not `None': an empty include list matches # NOTHING, where the default matches everything. A # caller passing a filtered-empty list gets the # opposite of what they intended. # mp-conflict2-names the ValueError message is built from `list(set)', # so with TWO conflicting patterns the ORDER is not # stable across interpreter starts. The suite asserts # the SET of names rather than the message, because # asserting the message would be flaky by construction. # listings-take-no-pattern `pathtools.path' has no pattern argument anywhere. # Combining the two halves is the CALLER's job, and # `combined-filter' is that composition done once. # # ============================================================================ # ON TARGET: 140 probe measurements over three python3 starts. # ============================================================================ # # EVERY STRING RESULT IS WRAPPED IN ANGLE BRACKETS and the empty string is `<>'. # Load-bearing here: half of this library's answers are empty -- an empty # listing, a filter that matched nothing, the parent of "/" -- and `grep | cut' # cannot otherwise tell an empty ANSWER from a key the probe never reached. A # refusal is `!'. # # ============================================================================ P=python3-pathtools3 # The canary is first so that a suite which dies before its own first assertion # is distinguishable from one that never started. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/pathtools DI=$SITE/pathtools3-0.2.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-pathtools3.list PROBE=$BST_DATA/python3-pathtools3-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pathtools3.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # IT IS A PACKAGE, NOT A FLAT MODULE -- unlike most of the small pure-Python # ports beside it on this image. site-packages holds a `pathtools/' DIRECTORY # with four modules in it, and no `pathtools.py'. A release that flattened it # would still import and still pass every behavioural case below, so the shape # is asserted on its own. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/no-flat-module test ! -e "$SITE/pathtools.py" check $P/init-file test -s "$PKG/__init__.py" check $P/path-module test -s "$PKG/path.py" check $P/patterns-module test -s "$PKG/patterns.py" check $P/version-module test -s "$PKG/version.py" # THE IMPORT NAME IS `pathtools' AND THE DISTRIBUTION IS `pathtools3', which is # the fork's whole packaging story and the reason the two cannot be co-installed: # both would own this directory. A `pathtools3/' directory appearing here would # mean upstream had renamed the import and every caller's `import pathtools' had # broken. check $P/no-pathtools3-dir test ! -d "$SITE/pathtools3" # Upstream's `docs/' and its test suite are not packaged -- `packages=["pathtools"]' # in setup.py builds the wheel from the package directory alone. check $P/no-docs-dir test ! -d "$PKG/docs" check $P/no-tests-dir test ! -d "$PKG/tests" # The four .pyc ship in the .deb, which is what lets the package import on a # READ-ONLY root: nothing has to be compiled at first import. check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-path test -s "$PKG/__pycache__/path.cpython-312.pyc" check $P/pycache-patterns test -s "$PKG/__pycache__/patterns.cpython-312.pyc" check $P/pycache-version test -s "$PKG/__pycache__/version.cpython-312.pyc" # ------------------------------------------------------------- the .deb ------ cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN". This image's python3 is ONE # STATIC BINARY with an empty lib-dynload and no dynamic loader (BRINTOS-1476), # so a module carrying a compiled extension could not be imported here at all. # pathtools3 ships no C -- no .c, no .pyx, no ext_modules in setup.py -- so this # is a REGRESSION TRIPWIRE on a version bump rather than a statement about today. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...and nothing but Python in the payload, which is the same claim from the # other side: a data file or a script appearing here would be a packaging change # nobody asked for. _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" # THE PACKAGE INSTALLS NO EXECUTABLE. setup.py declares no `entry_points' and no # `scripts', so there is nothing in ${bindir} and nothing to reach through a # shebang -- the library is importable only. (path.py and patterns.py both carry # a `#!/usr/bin/env python' line, which is why this is worth asserting: the # shebang is there and the install bit is not.) check_eq $P/pkg-no-bindir "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # EXACTLY FOUR .py FILES. A repackaging that started shipping a fifth module # would show up here first, before any behavioural case noticed. check_eq $P/pkg-py-count "4" \ "$(grep -c '/site-packages/pathtools/[a-z_]*\.py$' "$W/pkgf" 2>/dev/null)" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.2.1" cat "$DI/METADATA" check_out $P/dist-info-name "Name: pathtools3" cat "$DI/METADATA" check_out $P/dist-info-author "Author: Nuxeo" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # `setuptools3' BUILT THIS, and it leaves three traces the `python_hatchling' # ports beside it on this image do not: the generator names `bdist_wheel', there # IS a `top_level.txt', and the licence lands FLAT in the dist-info rather than # under `licenses/'. All three are asserted, so a recipe moved onto another # backend says so here rather than silently. check_out $P/dist-info-generator "Generator: bdist_wheel (0.42.0)" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" # THE WHEEL IS UNIVERSAL -- it carries a `py2' tag as well, because setup.py # still declares the Python 2 classifiers. Nothing on this image can use it and # it costs nothing; it is asserted because it is the one visible remnant of the # 2018 codebase and a re-release would drop it. check_out $P/dist-info-tag-py2 "Tag: py2-none-any" cat "$DI/WHEEL" check $P/dist-info-top-level test -s "$DI/top_level.txt" check_eq $P/dist-info-top-level-name "pathtools" "$(cat "$DI/top_level.txt" 2>/dev/null)" check $P/dist-info-flat-license test -s "$DI/LICENSE" check $P/dist-info-no-licenses-dir test ! -d "$DI/licenses" # The RECORD names nine entries: four payload modules and five dist-info files. check_out $P/record-path "pathtools/path.py,sha256=" cat "$DI/RECORD" check_out $P/record-patterns "pathtools/patterns.py,sha256=" cat "$DI/RECORD" check_eq $P/record-lines "9" "$(grep -c . "$DI/RECORD" 2>/dev/null)" sed -n '/^Package: python3-pathtools3$/,/^$/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: 0.2.1-r0" cat "$W/st" # THE ONE RDEPENDS IS A SUB-PACKAGE OF THE PORTED python3 (BRINTOS-1476) and is # already on this image. CHECKED against poky's python3 split rather than # assumed: the modules' entire import surface is `os.path', `functools' and # `fnmatch' -- and `fnmatch' pulls `re', which pulls `enum' -- plus # `importlib.metadata', which the `dist-version' cases reach through. Every one # of those is in `python3-core'. The line is asserted exactly, so a leaner image # that split any of them out would say so here once rather than present as a # broken library. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact "Depends: python3-core" "$(cat "$W/dep")" # ---------------------------------------------------------------- import ----- # THREE imports, because the parent package imports NOTHING for you: `import # pathtools' alone gives a namespace with no public name in it, so a caller who # stops there has imported nothing usable. Each submodule is therefore its own # assertion. check $P/import python3 -c 'import pathtools' check $P/import-path python3 -c 'from pathtools import path' check $P/import-patterns python3 -c 'from pathtools import patterns' # A DeprecationWarning or a stray print on import would be invisible to the cases # above, which score only the exit status. The 2018 codebase is a plausible # source of one: `imp' is referenced in setup.py's fallback branch, and importing # `imp' warns on 3.12. check_eq $P/import-quiet "" \ "$(python3 -c 'import pathtools.path, pathtools.patterns' 2>&1)" # ----------------------------------------------------------------- probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" 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 # The probe's own two bookends. `probe-complete' says it reached its last line # and `probe-count' says it emitted every key it meant to: a probe that died # two-thirds of the way through would otherwise leave the missing cases scoring # against an empty string, which `check_eq' would read as a wrong answer rather # than as an absent one. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "140" "$(pv emitted_count)" # The probe builds its fixture and tears it down. Both are asserted: a fixture # that failed to build makes every listing case below meaningless, and one left # behind would leak a directory per boot into the guest's tmpfs. check_eq $P/fixture-built "yes" "$(pv fixture_built)" check_eq $P/fixture-torn-down "yes" "$(pv torn_down)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the two # modules named here. check_eq $P/path-answered "<$PKG/path.py>" "$(pv path_module_file)" check_eq $P/patterns-answered "<$PKG/patterns.py>" "$(pv patterns_module_file)" check_eq $P/is-package "yes" "$(pv is_package)" # `import pathtools' GIVES YOU NOTHING. `__init__.py' is 1209 bytes of licence # header: it imports no submodule and defines no name, so the package namespace # is bare. The three cases fail differently -- a release adding `from .path # import *' flips the attr cases, one merely setting `__version__' flips only the # first -- so each is asserted rather than inferred from the others. check_eq $P/parent-has-version "no" "$(pv parent_has_version)" check_eq $P/parent-has-path-attr "no" "$(pv parent_has_path_attr)" check_eq $P/parent-has-patterns-attr "no" "$(pv parent_has_patterns_attr)" check_eq $P/parent-public-names "<>" "$(pv parent_public_names)" # ...and the attribute appears the moment the submodule is imported, which is # what makes the three above a statement about `__init__.py' rather than about # the import system. check_eq $P/parent-path-attr-after "yes" "$(pv parent_path_attr_after)" # `__version__' IS A TUPLE AND IT IS NOT ON THE PACKAGE. `"0.2.1" == # pathtools.version.__version__' is False and always was; the string lives beside # it under a different name. check_eq $P/version-attr "<(0, 2, 1)>" "$(pv version_attr)" check_eq $P/version-type "" "$(pv version_type)" check_eq $P/version-string "<0.2.1>" "$(pv version_string)" check_eq $P/version-info "<(0, 2, 1)>" "$(pv version_info)" # THE IMPORT NAME IS NOT THE DISTRIBUTION NAME, asserted from both sides. check_eq $P/dist-version-pathtools3 "<0.2.1>" "$(pv dist_version_pathtools3)" check_eq $P/dist-version-pathtools "!" "$(pv dist_version_pathtools)" # ------------------------------------------------------ the exported surface - check_eq $P/path-all \ "" \ "$(pv path_all)" check_eq $P/path-all-count "8" "$(pv path_all_count)" check_eq $P/patterns-all \ "" \ "$(pv patterns_all)" check_eq $P/patterns-all-count "4" "$(pv patterns_all_count)" check_eq $P/path-has-get-dir-walker "yes" "$(pv path_has_get_dir_walker)" check_eq $P/path-has-walk "yes" "$(pv path_has_walk)" check_eq $P/path-has-listdir "yes" "$(pv path_has_listdir)" check_eq $P/path-has-list-directories "yes" "$(pv path_has_list_directories)" check_eq $P/path-has-list-files "yes" "$(pv path_has_list_files)" check_eq $P/path-has-absolute-path "yes" "$(pv path_has_absolute_path)" check_eq $P/path-has-real-absolute-path "yes" "$(pv path_has_real_absolute_path)" check_eq $P/path-has-parent-dir-path "yes" "$(pv path_has_parent_dir_path)" check_eq $P/patterns-has-match-path "yes" "$(pv patterns_has_match_path)" check_eq $P/patterns-has-match-path-against "yes" "$(pv patterns_has_match_path_against)" check_eq $P/patterns-has-match-any-paths "yes" "$(pv patterns_has_match_any_paths)" check_eq $P/patterns-has-filter-paths "yes" "$(pv patterns_has_filter_paths)" # THE FUNCTION BRINTOS-2201 ASKED FOR DOES NOT EXIST. See the header. check_eq $P/has-list-symlinks "no" "$(pv has_list_symlinks)" # ============================================================================ # pathtools.patterns # ============================================================================ # # ---------------------------------------------------------- the defaults ----- # `included_patterns=None' becomes `["*"]', `excluded_patterns=None' becomes # `[]'. These functions NEVER TOUCH THE FILESYSTEM, which is why a path naming # nothing on disk matches just as well as a real one. check_eq $P/mp-default "" "$(pv mp_default)" check_eq $P/mp-default-insensitive "" "$(pv mp_default_insensitive)" check_eq $P/mp-default-nonexistent "" "$(pv mp_default_nonexistent)" check_eq $P/mp-default-empty-string "" "$(pv mp_default_empty_string)" # ------------------------------------------------------ include / exclude ---- check_eq $P/mp-include-hit "" "$(pv mp_include_hit)" check_eq $P/mp-include-miss "" "$(pv mp_include_miss)" check_eq $P/mp-include-one-of-many "" "$(pv mp_include_one_of_many)" check_eq $P/mp-exclude-overrides "" "$(pv mp_exclude_overrides)" check_eq $P/mp-exclude-miss "" "$(pv mp_exclude_miss)" check_eq $P/mp-exclude-only "" "$(pv mp_exclude_only)" # `[]' IS NOT `None'. An empty include list matches nothing where the default # matches everything, so a caller who passes a list that filtering happened to # empty gets the exact opposite of "no restriction". check_eq $P/mp-include-empty-list "" "$(pv mp_include_empty_list)" # ------------------------------------------------------- case sensitivity ---- check_eq $P/mp-case-sensitive-hit "" "$(pv mp_case_sensitive_hit)" check_eq $P/mp-case-sensitive-miss "" "$(pv mp_case_sensitive_miss)" check_eq $P/mp-case-insensitive-hit "" "$(pv mp_case_insensitive_hit)" check_eq $P/mp-case-insensitive-pattern "" "$(pv mp_case_insensitive_pattern)" check_eq $P/mpa-case-sensitive-miss "" "$(pv mpa_case_sensitive_miss)" check_eq $P/mpa-case-insensitive-hit "" "$(pv mpa_case_insensitive_hit)" # --------------------------------------------------- the conflict ValueError - # The refusal is about the PATTERNS ALONE and fires before any matching, which # is why it reaches a pathname that matches neither list. check_eq $P/mp-conflict "!" "$(pv mp_conflict)" check_eq $P/mp-conflict-irrelevant-path "!" "$(pv mp_conflict_irrelevant_path)" check_eq $P/mp-conflict-msg \ "" "$(pv mp_conflict_msg)" # TURNING CASE SENSITIVITY OFF CAN BREAK A WORKING CALL: both lists are # lowercased first, so `*.py' and `*.PY' become the same pattern and collide. check_eq $P/mp-conflict-case-sensitive-ok "" "$(pv mp_conflict_case_sensitive_ok)" check_eq $P/mp-conflict-case-insensitive "!" "$(pv mp_conflict_case_insensitive)" # ...and the message names the LOWERCASED pattern, which is the proof that the # collision happened after the transform rather than before it. check_eq $P/mp-conflict-case-msg \ "" "$(pv mp_conflict_case_msg)" # WITH TWO CONFLICTING PATTERNS THE MESSAGE ORDER IS NOT STABLE -- it is built # from `list(common_patterns)', a list of a SET, and set iteration order for # strings depends on PYTHONHASHSEED. So the SET of names is asserted and the # message is not: a case asserting that message verbatim would be flaky by # construction, and would read as a library regression when it fired. check_eq $P/mp-conflict2-names "<*.md,*.py>" "$(pv mp_conflict2_names)" check_eq $P/mp-conflict2-is-valueerror "!" "$(pv mp_conflict2_is_valueerror)" # -------------------------------------- a bare STRING where a list belongs ---- # `set("*.txt")' is the four one-character patterns `*', `.', `t', `x', and `*' # matches everything -- so a forgotten pair of brackets returns True for a path # the pattern does not match. There is no type check anywhere on this path. check_eq $P/str-pattern-false-hit "" "$(pv str_pattern_false_hit)" # The same bug is a False when the string happens to contain no wildcard, which # is what makes it so hard to see: the failure is INCONSISTENT, not uniform. check_eq $P/str-pattern-no-star "" "$(pv str_pattern_no_star)" # ...and two DIFFERENT string patterns share `*' and `.', so the intersection is # non-empty and the call raises "conflicting patterns" about patterns the caller # never wrote. check_eq $P/str-pattern-spurious-conflict "!" "$(pv str_pattern_spurious_conflict)" # ------------------------------------------------- filter_paths / any_paths -- # ONE IS A GENERATOR AND THE OTHER IS NOT, and that decides WHEN the conflict # ValueError above is raised. `fp-conflict-deferred' is `yes' because the probe # REACHED the line after constructing a conflicting generator -- construction # alone raises nothing. check_eq $P/fp-is-generator "yes" "$(pv fp_is_generator)" check_eq $P/map-is-generator "no" "$(pv map_is_generator)" check_eq $P/fp-conflict-deferred "yes" "$(pv fp_conflict_deferred)" check_eq $P/fp-conflict-on-iter "!" "$(pv fp_conflict_on_iter)" check_eq $P/map-conflict-eager "!" "$(pv map_conflict_eager)" # ORDER IS THE INPUT'S -- `filter_paths' iterates `pathnames' in sequence, so a # list comes back in list order. (The PATTERNS go through a set and lose theirs, # which is why no case here asserts which pattern matched.) check_eq $P/fp-order "" "$(pv fp_order)" check_eq $P/fp-all-default "" \ "$(pv fp_all_default)" check_eq $P/fp-exclude "" "$(pv fp_exclude)" check_eq $P/fp-none-match "<>" "$(pv fp_none_match)" check_eq $P/fp-empty-input "<>" "$(pv fp_empty_input)" check_eq $P/map-hit "" "$(pv map_hit)" check_eq $P/map-miss "" "$(pv map_miss)" # An empty input is False even under the match-everything default, because the # loop simply never runs. check_eq $P/map-empty-input "" "$(pv map_empty_input)" check_eq $P/map-default "" "$(pv map_default)" check_eq $P/mp-duplicate-patterns "" "$(pv mp_duplicate_patterns)" # ============================================================================ # pathtools.path -- over the fourteen-entry fixture the probe builds in tmpfs # ============================================================================ # # The fixture's own shape first, measured with `os.lstat' rather than with the # library. A guest whose filesystem refused a symlink would turn every symlink # case below red, and these four are what would name that cause instead of # leaving it to be guessed from the wreckage. check_eq $P/fx-link-is-symlink "yes" "$(pv fx_link_is_symlink)" check_eq $P/fx-broken-is-symlink "yes" "$(pv fx_broken_is_symlink)" check_eq $P/fx-broken-is-dangling "yes" "$(pv fx_broken_is_dangling)" check_eq $P/fx-cycle-is-symlink "yes" "$(pv fx_cycle_is_symlink)" # --------------------------------------------------------------- walk -------- # EVERY DIRECTORY EXACTLY ONCE, AND THE CYCLE NOT ENTERED. Five roots: the top, # `a', `a/b', `c' and `cyc'. `link_to_a' and `cyc/self' are not among them -- # they are symlinks and `followlinks' is False. The last two cases are the ones # that make this suite safe to run at all. check_eq $P/walk-roots-recursive "<.,a,a/b,c,cyc>" "$(pv walk_roots_recursive)" check_eq $P/walk-roots-count "5" "$(pv walk_roots_count)" check_eq $P/walk-roots-unique "yes" "$(pv walk_roots_unique)" check_eq $P/walk-entered-cycle "no" "$(pv walk_entered_cycle)" check_eq $P/walk-entered-link "no" "$(pv walk_entered_link)" # TOP-DOWN AND BOTTOM-UP DIFFER ONLY IN ORDER, and the top is the marker: first # when topdown, last when not. Asserted as the first and last root rather than # the whole sequence, because the order among SIBLINGS is `scandir''s and is not # a promise on any filesystem this image can present. check_eq $P/walk-topdown-first "<.>" "$(pv walk_topdown_first)" check_eq $P/walk-bottomup-last "<.>" "$(pv walk_bottomup_last)" check_eq $P/walk-bottomup-count "5" "$(pv walk_bottomup_count)" # NON-RECURSIVE IS EXACTLY ONE ROOT -- the walker yields `next(os.walk())' and # stops. (This is the same code path that raises RuntimeError when there is no # first element to take; see the missing-directory block below.) check_eq $P/walk-nonrecursive-count "1" "$(pv walk_nonrecursive_count)" check_eq $P/walk-nonrecursive-root "<.>" "$(pv walk_nonrecursive_root)" # An EMPTY directory is still walked: one root, no children, either way. check_eq $P/walk-empty-dir-count "1" "$(pv walk_empty_dir_count)" check_eq $P/walk-empty-dir-entries "<>" "$(pv walk_empty_dir_entries)" # ------------------------------------------------------------- listings ------ # Each listing asserted against the EXACT set it should return, recursive and # non-recursive. Sorted before comparison, for the scandir-order reason above. check_eq $P/list-files-recursive \ "" "$(pv list_files_recursive)" check_eq $P/list-files-nonrecursive \ "" "$(pv list_files_nonrecursive)" check_eq $P/list-dirs-recursive \ "" "$(pv list_dirs_recursive)" check_eq $P/list-dirs-nonrecursive \ "" "$(pv list_dirs_nonrecursive)" # `listdir' is the union, and per root it yields DIRECTORIES BEFORE FILES. check_eq $P/listdir-recursive \ "" \ "$(pv listdir_recursive)" check_eq $P/listdir-nonrecursive \ "" "$(pv listdir_nonrecursive)" # THE SYMLINK CLASSIFICATION, stated as the two cases it actually is. Both # answers come from `is_dir()', which follows symlinks: a DANGLING link cannot be # stat'ed and so is a FILE, and a link to a directory IS a directory -- but is # not descended into. Neither is what a caller looking for "the symlinks" would # guess, and there is no `list_symlinks' to ask instead. check_eq $P/broken-link-is-file "yes" "$(pv broken_link_is_file)" check_eq $P/broken-link-not-dir "yes" "$(pv broken_link_not_dir)" check_eq $P/dir-link-is-dir "yes" "$(pv dir_link_is_dir)" check_eq $P/dir-link-not-file "yes" "$(pv dir_link_not_file)" check_eq $P/dir-link-not-descended "yes" "$(pv dir_link_not_descended)" check_eq $P/cycle-link-is-dir "yes" "$(pv cycle_link_is_dir)" # EVERY LISTING IS A GENERATOR, which is why a caller who wants a count must # realise it -- and why one who iterates the same object twice gets six entries # and then none. check_eq $P/list-files-is-generator "yes" "$(pv list_files_is_generator)" check_eq $P/walk-is-generator "yes" "$(pv walk_is_generator)" check_eq $P/generator-exhausts "6,0" "$(pv generator_exhausts)" check_eq $P/listing-absolute "yes" "$(pv listing_absolute)" # THE LISTINGS DO NOT FILTER. There is no pattern argument anywhere in # `pathtools.path'; combining the two halves of this package is the CALLER's job, # and `combined-filter' is that composition done once -- `filter_paths' over # `list_files', which is the way the package is meant to be used. check_eq $P/listings-take-no-pattern "yes" "$(pv listings_take_no_pattern)" check_eq $P/combined-filter "" "$(pv combined_filter)" # ============================================================================ # THE HEADLINE: A MISSING DIRECTORY IS SILENT WHEN RECURSIVE AND RAISES WHEN NOT # ============================================================================ # # Asserted side by side, because either answer alone looks reasonable and it is # the DIVERGENCE that is the finding. See the header for the mechanism. check_eq $P/missing-recursive "<0>" "$(pv missing_recursive)" check_eq $P/missing-nonrecursive "!" "$(pv missing_nonrecursive)" # The message names neither the path nor the cause, which is why a caller who # hits this in the field has nothing to search for. check_eq $P/missing-nonrecursive-msg \ "" "$(pv missing_nonrecursive_msg)" # The same split reaches the listings built on `walk', which is how a caller # actually meets it. check_eq $P/missing-files-recursive "<0>" "$(pv missing_files_recursive)" check_eq $P/missing-files-nonrecursive "!" "$(pv missing_files_nonrecursive)" # THE SECOND INSTANCE, over an input reached by accident far more often than a # missing directory: a path that EXISTS but is a FILE. check_eq $P/file-root-recursive "<0>" "$(pv file_root_recursive)" check_eq $P/file-root-nonrecursive "!" "$(pv file_root_nonrecursive)" # `get_dir_walker' is PUBLIC, so the divergence is reachable directly -- and the # two walkers are not even the same kind of object, which is the mechanism in one # line: a `partial' over os.walk cannot raise this, and a generator function can. check_eq $P/walker-recursive-type "" "$(pv walker_recursive_type)" check_eq $P/walker-nonrecursive-type "" "$(pv walker_nonrecursive_type)" # ============================================================================ # CASE SENSITIVITY OVER A FIXTURE WHOSE NAMES DIFFER ONLY IN CASE # ============================================================================ # # `case-fixture-count' is the load-bearing one: it says the guest's tmpfs kept # `Case.TXT' and `case.txt' as TWO files. A case-insensitive filesystem would # collapse them into one, and every case below would go red for a reason that has # nothing to do with this library -- so the count is asserted first and names # that cause. check_eq $P/case-fixture-built "yes" "$(pv case_fixture_built)" check_eq $P/case-fixture-count "2" "$(pv case_fixture_count)" check_eq $P/case-listed "" "$(pv case_listed)" check_eq $P/case-sensitive-filter "" "$(pv case_sensitive_filter)" check_eq $P/case-insensitive-filter "" "$(pv case_insensitive_filter)" check_eq $P/case-sensitive-upper "" "$(pv case_sensitive_upper)" # ============================================================================ # absolute_path / real_absolute_path / parent_dir_path # ============================================================================ # # `absolute_path' is CWD-RELATIVE and normalises. (It is `abspath(normpath(p))', # and `abspath' already normpaths -- the inner call is redundant, which is # harmless and worth knowing when reading a profile.) check_eq $P/abs-of-relative "" "$(pv abs_of_relative)" check_eq $P/abs-of-dotted "" "$(pv abs_of_dotted)" check_eq $P/abs-of-empty "yes" "$(pv abs_of_empty)" check_eq $P/abs-of-dot "yes" "$(pv abs_of_dot)" check_eq $P/abs-is-idempotent "yes" "$(pv abs_is_idempotent)" # THE ONLY DIFFERENCE BETWEEN THE TWO is whether symlinks are resolved, which is # what a caller comparing two paths for equality has to know: `link_to_a' and `a' # are different strings from one and the same string from the other. check_eq $P/abs-keeps-symlink "yes" "$(pv abs_keeps_symlink)" check_eq $P/real-resolves-symlink "yes" "$(pv real_resolves_symlink)" # `parent_dir_path' IS `dirname' WITH TWO TRAPS. A trailing slash makes the # "parent" of a directory that directory ITSELF, and the "parent" of a bare # relative name is the CURRENT DIRECTORY rather than the name's parent -- because # `dirname("top.py")' is "" and "" normpaths to ".". check_eq $P/parent-of-file "" "$(pv parent_of_file)" check_eq $P/parent-of-dir "" "$(pv parent_of_dir)" check_eq $P/parent-of-trailing-slash "" "$(pv parent_of_trailing_slash)" check_eq $P/parent-trailing-slash-is-self "yes" "$(pv parent_trailing_slash_is_self)" check_eq $P/parent-of-bare-name-is-cwd "yes" "$(pv parent_of_bare_name_is_cwd)" # The root is its own parent, which is `dirname''s behaviour and terminates any # caller that walks upward looking for a fixed point. check_eq $P/parent-of-root "" "$(pv parent_of_root)" check_eq $P/parent-of-empty "yes" "$(pv parent_of_empty)" rm -rf "$W"