. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is very nearly the whole cost. # # Every assertion below either reads the filesystem/dpkg with # # shell builtins or greps the single probe run's output; the # # probe composes strings, writes one small text file and does # # no compression, no compilation and no network. The bound # # matches 99-python3-editables' rather than being tightened, # # because the dominant term in both is the interpreter start # # and that is what has not been measured on this arm yet. # # 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-platformdirs 4.2.0 (meta-openembedded meta-python/recipes-devtools/python, # python3-platformdirs_4.2.0.bb, `inherit pypi python_hatchling`) -- THE MODULE # THAT ANSWERS WHERE THIS PLATFORM KEEPS A USER'S DATA, CONFIG, CACHE, STATE AND # LOG DIRECTORIES, on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THIS PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # platformdirs computes PATHS. It does not create them (with one exception, # ensure_exists, which is asserted on its own below), it does not read them, and # it opens exactly one file in its whole surface -- user-dirs.dirs. Everything it # returns is a string composed by os.path.join out of an environment variable, a # home directory and a uid. # # So the subject is a COMPOSITION RULE, and the rule is the freedesktop.org # basedir spec: each accessor reads its OWN variable, falls back to a documented # path under $HOME when that variable is unset or blank, and appends the appname # and version it was constructed with. Every case below asserts one clause of # that rule. It is the successor to appdirs and what modern packaging tools call # to place their caches, so getting the rule wrong here is not cosmetic -- it is # where pip, hatch and their friends would put files on this image. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASES ARE REALLY ASSERTING. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly). A module that # carried a compiled extension therefore could not be imported here -- not "would # be slow", could not be imported. platformdirs is pure Python (eight source # files and a py.typed marker), which is the entire reason it ports. # # So `module-no-so` is not boilerplate. It is a REGRESSION TRIPWIRE on a version # bump: platformdirs grows no extension today, and the day a release adds a # compiled speedup this case goes red at INSTALL time rather than at `import # platformdirs` on somebody else's ticket. It is anchored on the package's own # dpkg file list as well as on a find, because the two fail differently -- a find # catches a file the package does not declare, the list catches a declared file # that is not a .py. # # ============================================================================ # THE MODULE IS DRIVEN END TO END, AND THE EXPECTATIONS ARE MEASURED. # ============================================================================ # # data/python3-platformdirs-probe.py takes every measurement in ONE interpreter # start and emits one key=value per line; the suite asserts on that output. It # clears every XDG_* variable before measuring the defaults and sets them # explicitly, to distinct scratch directories, before measuring the overrides -- # so the answers are a statement about platformdirs and not about whatever this # guest's init happened to export, and a crossed wire (cache read out of # XDG_DATA_HOME) is visible where setting them all to one path would hide it. # # EVERY VALUE ASSERTED BELOW WAS MEASURED, on x86-64, against THIS EXACT PROBE # and platformdirs 4.2.0 unpacked from the same platformdirs-4.2.0.tar.gz this # image's recipe builds (sha256 # ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768, recorded # 2026-08-31; the full control is in the ticket's design doc). They are not # predictions and they are not read off the source. # # WHAT MAKES THAT CONTROL TRANSFERABLE. Nothing platformdirs returns depends on # pointer width, byte order or the platform tag. Two inputs DO differ between the # control machine and this guest -- the home directory and the uid -- and they # are not asserted as constants: the probe emits `home` and `uid`, and every case # below composes its expectation from them. What is pinned is therefore the RULE # ($HOME/.local/share, /run/user/$uid) rather than one machine's answer. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - The Windows, macOS and Android backends. Their modules ship (the package is # one wheel for every platform) and are asserted PRESENT, but they are # unreachable here: _set_platform_dir_class() branches on sys.platform, which # is "linux", and `platform-class` asserts the branch it took. Driving them # would be testing a code path no consumer on this image can reach. # - upstream's tests/ corpus. It is in the sdist and not in the wheel, so it is # not in the package; this port closes on this suite instead. The corpus is # also pytest-based, and pytest is not on this image. # - python3-platformdirs-ptest. The recipe inherits ptest and ships a # run-ptest, but the -ptest package RDEPENDS on six further Python packages # (appdirs, covdefaults, pytest, pytest-cov, pytest-mock, # unittest-automake-output), none of which is ported. It is not taken and the # distro's ptest is off; see 99-pyxdg for the one recipe where that is # deliberately reversed. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-platformdirs # THE CANARY, which asserts nothing about python3-platformdirs. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE IS # COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require` guard deliberately, # because a `require` that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PD=$SITE/platformdirs DIST=$SITE/platformdirs-4.2.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-platformdirs.list D=$BST_DATA PROBE=$D/python3-platformdirs-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-platformdirs.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import platformdirs'` further down is # the functional statement; these are the ones that say WHICH copy answered, so a # module that imported out of some other path is distinguishable from one that # imported out of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$PD" check $P/module-init test -f "$PD/__init__.py" # THE BACKEND THAT ACTUALLY RUNS HERE, named on its own. Every path answer below # comes out of unix.py, and api.py holds the ABC they all go through; a package # that shipped __init__.py without either would import and then fail at the first # accessor, forty cases later. check $P/module-api test -f "$PD/api.py" check $P/module-unix test -f "$PD/unix.py" # The version is a GENERATED file (setuptools_scm writes version.py at build # time), so its presence is a statement about the build rather than about the # sdist -- and `version` below reads its value. check $P/module-version-py test -f "$PD/version.py" # The three unreachable backends. They ship because the wheel is one artifact for # every platform, and they are asserted present rather than ignored: a package # built with them stripped would be a different package from the one upstream # publishes, and the `platform-class` case below is only meaningful if the # alternatives it did NOT select were actually there to select. check $P/module-macos test -f "$PD/macos.py" check $P/module-windows test -f "$PD/windows.py" check $P/module-android test -f "$PD/android.py" # The executable module -- this package's ONLY command-line surface, since it # declares no console script. check $P/module-main test -f "$PD/__main__.py" # PEP 561's marker. Not decoration: it is the one non-Python regular file the # package ships under the module directory, and `pkg-only-python` below allows it # BY NAME, so a second such file is still a red. check $P/module-py-typed test -f "$PD/py.typed" # The installed distribution metadata, which is what makes the package visible to # importlib.metadata and therefore to any consumer resolving a requirement on it. # The directory name carries the version, so this is a second and independent # statement of the pin that `version` reads out of the module. check $P/dist-info-dir test -d "$DIST" check $P/dist-info-metadata test -f "$DIST/METADATA" check $P/dist-info-record test -f "$DIST/RECORD" check $P/dist-info-wheel test -f "$DIST/WHEEL" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. See the header: on this image # a .so could not be loaded at all, so this is the case that fails a version bump # which grew one -- loudly, here, instead of at `import` on a later ticket. check_eq $P/module-no-so "" \ "$(find "$PD" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # A find sees what is on disk; the dpkg list sees what the package CLAIMS. A # compiled extension that arrived without being declared shows up in the first, # one that is declared but landed elsewhere shows up in the second. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a shared archive -- # without having to enumerate what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a name-only # filter would report every directory in the package as an unexpected shape; each # entry is therefore tested for being a regular file first. `py.typed` is allowed # by name rather than by a wildcard, for the reason `module-py-typed` gives. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */platformdirs/py.typed) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # THE EIGHT SOURCE FILES, AS A SET. Asserted whole rather than counted: a missing # backend and a stray extra module are different defects that a count reports # identically, and the count is asserted too because the two fail differently # when a name CHANGES rather than disappears. Measured against the built .deb. check_eq $P/pkg-py-names \ "__init__.py,__main__.py,android.py,api.py,macos.py,unix.py,version.py,windows.py" \ "$(ls "$PD" 2>/dev/null | grep '\.py$' | sort | tr '\n' ',' | sed 's/,$//')" # --------------------------------------------------------- the package ------ # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-platformdirs is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. The stanza is read from a tmpfs # copy for the same reason as the file list, and the anchor is exact. sed -n '/^Package: python3-platformdirs$/,/^$/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: 4.2.0-r0" cat "$W/st" # THE DEPENDENCY SET, PINNED. Measured against the built .deb: python3-core and # nothing else. It is the honest answer -- platformdirs reaches os, sys, pathlib, # typing and configparser, all of which python3-core carries on this image -- and # pinning it is what catches a release that grew a dependency on a package this # image does not have, at install time rather than at import. check_out $P/pkg-depends "Depends: python3-core" cat "$W/st" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If platformdirs cannot be imported # then every probe case below reds at once and each names a different symptom; # this case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable. check $P/import python3 -c 'import platformdirs' # ------------------------------------------------------------- the 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 # ONE interpreter start for everything that follows. bst_probe bounds it and keeps # BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" "$W" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors make `version` and `home` # distinguishable from the keys they are prefixes of. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading an # absent file: each would red with got:'' and the set would read exactly like a # broken module, when what actually happened is that no measurement was taken. # The word `unscorable` is PARSED -- brintos_local_verify re-measures a suite that # prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not in a comment. # # The cases below still print their reds: withdraw, do not hide. This case is what # says what those reds measure. 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 two machine-dependent inputs every composed expectation below is built # from. They are checked for SHAPE, not for a value: a probe that reported an # empty home would make every default case below compare two strings that agree # with each other and mean nothing. HOME_PD="$(pv home)" UID_PD="$(pv uid)" # Compose the way posixpath.expanduser does, which is to strip a trailing slash # off the home directory before appending. It matters for exactly one home # directory, `/`, where the naive join gives `//.local/share` and platformdirs # gives `/.local/share` -- and the difference would read as a defect in the # module rather than in this file. This image DOES take that branch: the guest's # environment carries HOME=/ (measured, batch-1826 verify), regardless of the # /home/root in the rootfs's /etc/passwd, which expanduser never consults while # HOME is set. Every accessor that composes through posixpath uses HOME_J; the # one that does not is user-dirs.dirs, below. HOME_J="${HOME_PD%/}" case "$HOME_PD" in /*) bst_ok $P/probe-home ;; *) bst_notok $P/probe-home "home is not an absolute path: '$HOME_PD'" ;; esac case "$UID_PD" in ''|*[!0-9]*) bst_notok $P/probe-uid "uid is not numeric: '$UID_PD'" ;; *) bst_ok $P/probe-uid ;; esac # ...and the interpreter's idea of $HOME agrees with the shell's. Only asserted # when the shell has one: expanduser("~") falls back to the passwd database when # HOME is unset, and the two answers being different THEN is correct behaviour # rather than a defect. if [ -n "${HOME:-}" ]; then check_eq $P/home-matches-shell "$HOME" "$HOME_PD" else bst_skip $P/home-matches-shell "HOME is unset in this shell; expanduser falls back to the passwd database" fi # The probe's own import, reported from inside the interpreter. Distinguishable # from $P/import above: that one says the shell could start python3 and import, # this one says the SAME interpreter run that took every measurement below did. check_eq $P/probe-import "yes" "$(pv import_ok)" # THE VERSION THE RECIPE PINS, read from the module rather than from dpkg. A # mismatch here means the recipe and what actually landed have gone out of step, # and every expectation in this file is then describing a different platformdirs. # __version_info__ is asserted beside it because it is a SEPARATE binding # (a tuple, not a parse of the string) that a hand-edited version.py could leave # stale. check_eq $P/version "4.2.0" "$(pv version)" check_eq $P/version-info "4.2.0" "$(pv version_info)" # The module answered out of the package this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME platformdirs -- for the two modules that # do the work as well as for the package root. check_eq $P/module-file "$PD/__init__.py" "$(pv module_file)" check_eq $P/api-file "$PD/api.py" "$(pv api_file)" check_eq $P/unix-file "$PD/unix.py" "$(pv unix_file)" check_eq $P/pkg-dir "$PD" "$(pv pkg_dir)" # The same no-extension statement from inside the interpreter, over the directory # the imported module actually came from. check_eq $P/import-no-so "0" "$(pv pkg_so_count)" # ...and the eight source names as the IMPORTED package sees them. The `pkg-py-names` # case above reads the same set off the filesystem, and the two fail differently: # `ls` sees a file that arrived without the package declaring it, this one sees what # the copy that answered every measurement below actually holds. check_eq $P/import-py-names \ "__init__.py,__main__.py,android.py,api.py,macos.py,unix.py,version.py,windows.py" \ "$(pv pkg_py_names)" # ---------------------------------------------------------- the public API --- # __all__ is the module's own statement of its surface, asserted as an ordered # join so an addition and a removal read differently. It is LONG on purpose: the # 32 module-level functions are what most callers use, and a release that dropped # one would break them while leaving the class intact. check_eq $P/api-all \ "__version__,__version_info__,PlatformDirs,AppDirs,PlatformDirsABC,user_data_dir,user_config_dir,user_cache_dir,user_state_dir,user_log_dir,user_documents_dir,user_downloads_dir,user_pictures_dir,user_videos_dir,user_music_dir,user_desktop_dir,user_runtime_dir,site_data_dir,site_config_dir,site_cache_dir,site_runtime_dir,user_data_path,user_config_path,user_cache_path,user_state_path,user_log_path,user_documents_path,user_downloads_path,user_pictures_path,user_videos_path,user_music_path,user_desktop_path,user_runtime_path,site_data_path,site_config_path,site_cache_path,site_runtime_path" \ "$(pv all_names)" # WHICH BACKEND WAS SELECTED, asserted directly rather than inferred from a path. # _set_platform_dir_class() branches on sys.platform and on the two ANDROID_* # variables; a build that had resolved to Android or macOS would still answer # every accessor below -- with paths that do not exist on this image -- so the # selection is pinned on its own. check_eq $P/sys-platform "linux" "$(pv sys_platform)" check_eq $P/platform-class "Unix" "$(pv platform_class)" check_eq $P/platform-class-module "platformdirs.unix" "$(pv platform_class_module)" check_eq $P/platform-is-abc "yes" "$(pv platform_is_abc_subclass)" # appdirs' name for the same class -- the whole of this package's backwards # compatibility with its predecessor. Asserted as identity, not equality: a # consumer that still says AppDirs must get the same class object. check_eq $P/appdirs-alias "yes" "$(pv appdirs_is_platformdirs)" # ------------------------------------------- the defaults, with XDG_* unset --- # THE ORDINARY CASE ON THIS IMAGE. Nothing here exports an XDG_* variable, so # these are the paths an application actually gets. Each is composed from the # probe's own `home` rather than pinned to the control machine's, so what is # asserted is the basedir spec's rule. check_eq $P/defaults-ok "yes" "$(pv defaults_ok)" check_eq $P/default-user-data "$HOME_J/.local/share" "$(pv unset_user_data)" check_eq $P/default-user-config "$HOME_J/.config" "$(pv unset_user_config)" check_eq $P/default-user-cache "$HOME_J/.cache" "$(pv unset_user_cache)" check_eq $P/default-user-state "$HOME_J/.local/state" "$(pv unset_user_state)" # user_log_dir is user_state_dir with "log" appended, and ONLY because `opinion` # defaults True. Both directions are asserted: an implementation that had stopped # being opinionated would return the two equal, and one that appended # unconditionally would ignore opinion=False. check_eq $P/default-user-log "$HOME_J/.local/state/log" "$(pv unset_user_log)" check_eq $P/opinion-ok "yes" "$(pv opinion_ok)" check_eq $P/unopinionated-log "$HOME_J/.local/state" "$(pv unopinionated_log)" # THE ONE DEFAULT THAT IS NOT UNDER $HOME. /run/user/$uid is composed from the # uid the probe reported, so this asserts the rule on a guest running as root # (where it is /run/user/0) exactly as on the control. check_eq $P/default-user-runtime "/run/user/$UID_PD" "$(pv unset_user_runtime)" # The site accessors are FIXED absolute paths with no environment input at all. # site_data_dir returns the FIRST of XDG_DATA_DIRS' two defaults because multipath # is off; the multipath half is asserted below. check_eq $P/default-site-data "/usr/local/share" "$(pv unset_site_data)" check_eq $P/default-site-config "/etc/xdg" "$(pv unset_site_config)" check_eq $P/default-site-cache "/var/cache" "$(pv unset_site_cache)" check_eq $P/default-site-runtime "/run" "$(pv unset_site_runtime)" # The six media directories, which take their capitalised English names from the # spec's own examples and are NOT XDG_*_HOME-derived. check_eq $P/default-documents "$HOME_J/Documents" "$(pv unset_user_documents)" check_eq $P/default-downloads "$HOME_J/Downloads" "$(pv unset_user_downloads)" check_eq $P/default-pictures "$HOME_J/Pictures" "$(pv unset_user_pictures)" check_eq $P/default-videos "$HOME_J/Videos" "$(pv unset_user_videos)" check_eq $P/default-music "$HOME_J/Music" "$(pv unset_user_music)" check_eq $P/default-desktop "$HOME_J/Desktop" "$(pv unset_user_desktop)" # ------------------------------------------- the overrides, with XDG_* set ---- # THE SPEC'S WHOLE POINT: each accessor follows its OWN variable and no other. The # probe set five of them to five DISTINCT scratch directories, so a crossed wire # -- the cache read out of XDG_DATA_HOME -- is a red here where setting them all # to one path would have hidden it. check_eq $P/overrides-ok "yes" "$(pv overrides_ok)" check_eq $P/xdg-user-data "$W/xdg-data" "$(pv set_user_data)" check_eq $P/xdg-user-config "$W/xdg-config" "$(pv set_user_config)" check_eq $P/xdg-user-cache "$W/xdg-cache" "$(pv set_user_cache)" check_eq $P/xdg-user-state "$W/xdg-state" "$(pv set_user_state)" check_eq $P/xdg-user-log "$W/xdg-state/log" "$(pv set_user_log)" check_eq $P/xdg-user-runtime "$W/xdg-runtime" "$(pv set_user_runtime)" # site_runtime_dir follows XDG_RUNTIME_DIR when it is set and falls back to /run # rather than /run/user/$uid when it is not -- the documented asymmetry with # user_runtime_dir, and both halves are now pinned. check_eq $P/xdg-site-runtime "$W/xdg-runtime" "$(pv set_site_runtime)" # A VARIABLE SET TO WHITESPACE IS TREATED AS UNSET. It is upstream's documented # behaviour (`if not path.strip()`), and it is what actually runs on a target # whose shell exports an empty variable -- which is common enough that the # fallback, not the override, is the live path. check_eq $P/xdg-blank-falls-back "$HOME_J/.local/share" "$(pv blank_user_data)" # ----------------------------------------- appname, version and appauthor ----- # The path elements platformdirs appends, which is what separates one # application's files from another's on a shared image. check_eq $P/appname-ok "yes" "$(pv appname_ok)" check_eq $P/app-data "$W/xdg-data/bstapp" "$(pv app_data)" check_eq $P/app-ver-data "$W/xdg-data/bstapp/1.2" "$(pv app_ver_data)" check_eq $P/app-ver-config "$W/xdg-config/bstapp/1.2" "$(pv app_ver_config)" check_eq $P/app-ver-cache "$W/xdg-cache/bstapp/1.2" "$(pv app_ver_cache)" check_eq $P/app-ver-state "$W/xdg-state/bstapp/1.2" "$(pv app_ver_state)" # The two rules compose in one order and not the other: the version goes UNDER # the appname, and the opinionated "log" goes under BOTH. check_eq $P/app-ver-log "$W/xdg-state/bstapp/1.2/log" "$(pv app_ver_log)" # appauthor is a WINDOWS concept. On Unix it must change nothing at all; a backend # that had started honouring it would put every application's files one directory # deeper than the spec says, silently. check_eq $P/appauthor-ignored "yes" "$(pv appauthor_ignored)" # ----------------------------------------------------------- multipath ------- # site_data_dir and site_config_dir return the FIRST entry by default and the # whole os.pathsep-joined list when multipath is on. Both are asserted, because a # consumer that iterated the default answer would silently read one directory -- # and the appname is appended to EVERY entry, not just the first. check_eq $P/multipath-ok "yes" "$(pv multipath_ok)" check_eq $P/site-data-first "/bst/a/bstapp" "$(pv site_data_first)" check_eq $P/site-data-multi "/bst/a/bstapp:/bst/b/bstapp" "$(pv site_data_multi)" check_eq $P/site-config-first "/bst/c/bstapp" "$(pv site_config_first)" check_eq $P/site-config-multi "/bst/c/bstapp:/bst/d/bstapp" "$(pv site_config_multi)" # site_data_path collapses a multipath answer back to ONE Path. That asymmetry # between the _dir and _path halves is documented and easy to lose in a refactor, # and a caller doing `site_data_path / "x"` on a colon-joined string would build a # path that cannot exist. check_eq $P/site-data-path-multi "/bst/a/bstapp" "$(pv site_data_path_multi)" # The two iterators, which are what a consumer searching for a config file walks. # Asserted whole: the user directory comes FIRST and the site directories follow # in XDG order, and a reversal would make a system file shadow the user's. check_eq $P/iter-data-dirs \ "$W/xdg-data/bstapp,/bst/a/bstapp,/bst/b/bstapp" "$(pv iter_data_dirs)" check_eq $P/iter-config-dirs \ "$W/xdg-config/bstapp,/bst/c/bstapp,/bst/d/bstapp" "$(pv iter_config_dirs)" # ------------------------------------------------------- the module functions -- # The 32 module-level functions are the surface most callers use, and each must # agree with the class property it wraps -- a divergence would be invisible to # anyone testing only the class. check_eq $P/functions-ok "yes" "$(pv functions_ok)" check_eq $P/func-checked "10" "$(pv func_checked)" check_eq $P/func-matches-property "yes" "$(pv func_matches_property)" # THE SURFACE IS NOT UNIFORM, and that is upstream's shape rather than a defect: # the six MEDIA functions take NO arguments, because ~/Documents belongs to the # user and not to an application. The refusal is asserted by count rather than # assumed, so a release that quietly gave them a version parameter -- changing # what every caller's paths look like -- is a red here. check_eq $P/media-checked "6" "$(pv media_checked)" check_eq $P/media-matches-property "yes" "$(pv media_matches_property)" check_eq $P/media-rejects-appname "6" "$(pv media_reject_appname)" # The *_path half returns pathlib.Path over the same string. Asserted as both a # type and a value: a Path built from a different string would still be a Path, # and a str returned where a Path is documented breaks `/` on the caller's side. check_eq $P/path-is-pathlib "yes" "$(pv path_is_pathlib)" check_eq $P/path-matches-dir "yes" "$(pv path_matches_dir)" # ------------------------------------------------------------ ensure_exists --- # THE ONE ACCESSOR WITH A SIDE EFFECT, and the only place platformdirs touches the # filesystem at all. It gets cases of its own because it is the one behaviour a # read-only or otherwise unusual rootfs could break while every string answer # stayed right -- and this image's writable tree is tmpfs over a virtiofs root, # which is not the shape the control machine has. check_eq $P/ensure-ok "yes" "$(pv ensure_ok)" check_eq $P/ensure-absent-before "yes" "$(pv ensure_absent_before)" check_eq $P/ensure-returns "$W/xdg-data/bstmk/9.9" "$(pv ensure_returns)" check_eq $P/ensure-created "yes" "$(pv ensure_created)" # ...and the DEFAULT must not create anything, or every path query on this image # would litter the rootfs. Asserted from the shell as well as from the probe, so # a probe that mis-reported it is still caught. check_eq $P/ensure-default-no-create "yes" "$(pv nocreate_default)" check $P/ensure-created-on-disk test -d "$W/xdg-data/bstmk/9.9" check_eq $P/ensure-nocreate-on-disk "" \ "$(test -d "$W/xdg-data/bstnomk/9.9" && echo present)" # --------------------------------------------------------- user-dirs.dirs ----- # THE ONE PLACE platformdirs READS A FILE, and the only accessor here that can # fail on an image where every string-composing one works: it parses # $XDG_CONFIG_HOME/user-dirs.dirs with ConfigParser, so it is a statement about # this image's python3 (configparser is a python3-core split, and the package's # Depends resolves to exactly that) as well as about platformdirs. check_eq $P/userdirs-ok "yes" "$(pv userdirs_ok)" # A literal $HOME inside the file is expanded -- the form xdg-user-dirs itself # writes, so a version that stopped expanding it would return a path with a dollar # sign in it to every desktop consumer. It is expanded by `str.replace("$HOME", # expanduser("~"))` and nothing else (platformdirs/unix.py), so unlike every # accessor above, no posixpath normalisation runs on the result: this expectation # is composed from the UNSTRIPPED home. With HOME=/ upstream returns `//BstDocs`, # which is correct for a raw substitution and is what this image measures. check_eq $P/userdirs-documents "${HOME_PD}/BstDocs" "$(pv userdirs_documents)" # ...and an absolute value is taken as it stands, quotes stripped. check_eq $P/userdirs-music "/bst/abs/music" "$(pv userdirs_music)" # THE FALLTHROUGH, BOTH STEPS. A key absent from the file falls to the # environment, and only then to the tilde default. Getting the order wrong would # make a present-but-incomplete user-dirs.dirs silently override nothing, or # override everything. check_eq $P/userdirs-env-fallthrough "$W/envvid" "$(pv userdirs_videos_env)" check_eq $P/userdirs-tilde-fallthrough "$HOME_J/Pictures" "$(pv userdirs_pictures_fallback)" # --------------------------------------------------------------- __main__ ----- # The package declares NO console script, so `python3 -m platformdirs` is its only # command-line surface. It is driven inside the probe rather than from the shell # because a second interpreter start would cost this suite more than every other # case in it put together. check_eq $P/main-ran "yes" "$(pv main_ran)" check_eq $P/main-first-line "-- platformdirs 4.2.0 --" "$(pv main_first_line)" # main() prints four blocks of fifteen properties plus a header line each. The # count is asserted whole: a property that raised would shorten it, and on this # image the one that could is user_runtime_dir, which needs getuid(). check_eq $P/main-blocks "4" "$(pv main_blocks)" check_eq $P/main-line-count "65" "$(pv main_line_count)" check_eq $P/main-prints-user-data "yes" "$(pv main_has_user_data)" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv done)" rm -rf "$W"