. "$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 139 measurements. # # # # The work inside each start is `os.getenv' and # # `os.path.join' over strings under 64 characters. There is # # NO filesystem access anywhere in the module -- see # # `home-absent' below, which measures that rather than # # asserting it -- so nothing here is even I/O-bound, let # # alone quadratic in anything. The interpreter start is the # # only term the number has to cover. # # # # 1200 is 400s per start, the same per-start budget # # 99-python3-imagesize sets for its three, and roughly six # # times the ~60s a python3 start has measured at here. # # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # 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-appdirs 1.4.4 (meta-openembedded meta-python/recipes-devtools/python, # python3-appdirs_1.4.4.bb, `inherit pypi setuptools3 ptest', PyPI sdist) on the # wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # appdirs COMPOSES PATHS. Seven functions -- user_data_dir, user_config_dir, # user_cache_dir, user_state_dir, user_log_dir, site_data_dir, site_config_dir # -- plus an `AppDirs' class that binds an appname, an appauthor and a version # to all seven. Each one is an `os.getenv(XDG_..., )' followed by an # `os.path.join', and that is the whole of it. # # It NEVER TOUCHES THE FILESYSTEM. Not one stat, open, mkdir or listdir appears # in the module -- unlike its successor platformdirs, which opens # user-dirs.dirs. That is not a claim read off the source here: the probe runs # every case below against a HOME of `/bst-appdirs-home', which is not a # directory on this image, and `home-absent' asserts it is still not one after # all 139 measurements. Nothing raised for a missing directory and nothing was # created. # # So the subject is a COMPOSITION RULE over an environment, and the environment # is the input. Every case below states the XDG block it ran under, and the # probe restores that block between cases so one cannot leak into the next. # # ============================================================================ # THE PLATFORM IS DECIDED AT IMPORT TIME AND NEVER RE-READ. # ============================================================================ # # `system = sys.platform' runs at module scope, and every branch in the file # tests that module-level global. So which platform's rules a caller gets is # fixed at the first `import appdirs', and monkeypatching `sys.platform' # afterwards changes nothing. `system' asserts it is `linux' on this image, # which is what makes every XDG expectation below a statement about THIS arm # rather than about Unix in general. # # ============================================================================ # FOUR BEHAVIOURS A CALLER WILL GET WRONG. THESE ARE THE POINT OF THE SUITE. # ============================================================================ # # AN EMPTY OR RELATIVE XDG VALUE IS USED AS-IS. # The XDG base-directory specification says a value that is # not an absolute path "must be ignored"; appdirs does not # check. `os.getenv(name, default)' returns the default only # for an ABSENT variable, so `XDG_DATA_HOME=' -- set, empty -- # yields the empty string and `user_data_dir("MyApp")' answers # the bare relative name `MyApp'. A caller who trusts it # writes into the process's current directory. # # The SITE functions have the same hole and answer # DIFFERENTLY, because they join with `os.sep' rather than # `os.path.join': an empty XDG_DATA_DIRS gives `/MyApp', at # the root. `udata-empty' and `sdata-empty' sit next to each # other so the two answers are legible as one defect with two # shapes. # # user_log_dir PUTS THE VERSION BEFORE `log', NOT AFTER IT. # Every other function appends the version last. user_log_dir # delegates to user_cache_dir WITH the version, sets its own # `version' to False so the trailing block cannot fire, and # only then appends `log' -- so version 1.0's log directory is # `~/.cache/MyApp/1.0/log', a CHILD of the versioned cache # dir, which is the opposite nesting from everywhere else. # # appauthor AND roaming ARE ACCEPTED HERE AND READ ON NO PLATFORM BUT WINDOWS. # Both are in the signatures on every platform. An application # that keys its data directory by author is, on this image, # keying it by nothing: `udata-author-ignored' asserts the two # paths are the same string. `appauthor=False', the documented # way to SUPPRESS the author component on Windows, is likewise # a no-op and does not raise. # # THERE IS NO `__all__'. # `from appdirs import *' therefore exports every public name # the module binds -- including `os', `sys', `unicode' and # `system'. A caller who star-imports this module has silently # rebound `os'. Asserted as four separate cases, because which # names leak is the part that matters. # # ============================================================================ # THE WINDOWS HELPERS ARE PRESENT AND THE ALIAS THAT PICKS ONE IS NOT. # ============================================================================ # # `_get_win_folder_with_pywin32', `_get_win_folder_with_ctypes', # `_get_win_folder_with_jna' and `_get_win_folder_from_registry' are all # module-level `def's, so all four exist on this image. The `_get_win_folder' # alias that chooses among them is assigned inside `if system == "win32"', so it # does NOT. Dead weight rather than dead code, and asserted because four # helpers being present makes it look as though the alias must be too. # # ============================================================================ # IT IS A FLAT MODULE, NOT A PACKAGE. # ============================================================================ # # `py_modules=["appdirs"]' in setup.py, so site-packages holds `appdirs.py' and # no `appdirs/' directory at all -- the opposite shape from python3-imagesize # next door. A release that turned it into a package would still import and # still pass every behavioural case here, so the shape is asserted on its own. # # Upstream's `test/' is NOT packaged either (setup.py declares py_modules, not # packages), which `no-test-dir' asserts. # # ============================================================================ # WHY NO FIXTURE IS SHIPPED AND NONE IS BUILT. # ============================================================================ # # There is nothing to build. A path-composition library needs no input but its # environment, and the environment is what the probe varies -- which is why the # HOME it uses can be a literal that does not exist, and why every expectation # below is a LITERAL STRING rather than something derived from whatever HOME the # runner happened to have. That is the whole reason this suite is deterministic. # # ============================================================================ P=python3-appdirs # 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 MOD=$SITE/appdirs.py DI=$SITE/appdirs-1.4.4.dist-info PKGLIST=/var/lib/dpkg/info/python3-appdirs.list PROBE=$BST_DATA/python3-appdirs-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-appdirs.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # A FLAT MODULE, NOT A PACKAGE -- see the header. check $P/site-packages-dir test -d "$SITE" check $P/module-file test -s "$MOD" check $P/not-a-package test ! -d "$SITE/appdirs" # Upstream's test/ is not packaged. check $P/no-test-dir test ! -d "$SITE/test" check $P/no-py-typed test ! -e "$SITE/appdirs.pyi" check $P/pycache-mod test -s "$SITE/__pycache__/appdirs.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. # appdirs 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. setup.py declares no `entry_points' and no `scripts', so the # package installs NO executable and there is nothing to reach through a shebang # -- which is why this suite has no CLI cases at all. _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" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.4.4" cat "$DI/METADATA" check_out $P/dist-info-name "Name: appdirs" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-license test -s "$DI/LICENSE.txt" # `setuptools3' BUILT THIS, WHICH IS VISIBLE IN THE WHEEL METADATA and is the # half of the recipe's `inherit' line that leaves a trace on the rootfs. The # `py2-none-any' tag is upstream's `universal = 1' -- and it is in a `[wheel]' # section of setup.cfg, which is the DEPRECATED spelling of `[bdist_wheel]'. # wheel 0.42.0 still honours it, MEASURED on the built package rather than # assumed; a wheel release that finally dropped the old section name would turn # this one case red and change nothing else, which is exactly what it is here to # say. check_out $P/dist-info-generator "Generator: bdist_wheel" 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" check_out $P/dist-info-tag-py2 "Tag: py2-none-any" cat "$DI/WHEEL" check_out $P/dist-info-top-level "appdirs" cat "$DI/top_level.txt" sed -n '/^Package: python3-appdirs$/,/^$/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: 1.4.4-r0" cat "$W/st" # THE RDEPENDS IS ASSERTED EXACTLY. The module's only imports are `sys' and # `os', both in python3-core, which setuptools3-base declares -- so unlike # python3-aniso8601 (calendar, datetime) there is no under-declared split here # and no bbappend is needed. A lean image that dropped a split 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 ----- check $P/import python3 -c 'import appdirs' # A DeprecationWarning or a stray print on import would be invisible to the case # above, which scores only the exit status. check_eq $P/import-quiet "" \ "$(python3 -c 'import appdirs' 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 "139" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # module named here. check_eq $P/module-answered "$MOD" "$(pv module_file)" check_eq $P/is-package "no" "$(pv is_package)" check_eq $P/version-attr "1.4.4" "$(pv version_attr)" check_eq $P/version-info "1.4.4" "$(pv version_info)" check_eq $P/version-info-tuple "yes" "$(pv version_info_is_tuple)" # THE PLATFORM IS FIXED AT IMPORT -- see the header. This is the case that makes # every XDG expectation below a statement about this image. check_eq $P/system "linux" "$(pv system)" check_eq $P/sys-platform "linux" "$(pv sys_platform)" check_eq $P/system-is-platform "yes" "$(pv system_is_platform)" check_eq $P/py3-flag "yes" "$(pv py3_flag)" # NO `__all__', SO A STAR-IMPORT REBINDS `os'. Which names leak is the part that # matters, so they are four cases and not one. check_eq $P/no-dunder-all "no" "$(pv has___all__)" check_eq $P/star-exports-os "yes" "$(pv star_exports_os)" check_eq $P/star-exports-sys "yes" "$(pv star_exports_sys)" check_eq $P/star-exports-unicode "yes" "$(pv star_exports_unicode)" check_eq $P/star-exports-system "yes" "$(pv star_exports_system)" # The four Windows helpers are all defined; the alias that would pick one is not. check_eq $P/no-get-win-folder "no" "$(pv has_get_win_folder)" check_eq $P/win-registry "yes" "$(pv has_win_registry)" check_eq $P/win-pywin32 "yes" "$(pv has_win_pywin32)" check_eq $P/win-ctypes "yes" "$(pv has_win_ctypes)" check_eq $P/win-jna "yes" "$(pv has_win_jna)" # ------------------------------------------------- user_*: the XDG defaults --- # With no XDG variable set -- which is this image -- these are the documented # defaults, and they are the paths a caller actually gets here. check_eq $P/udata-default "/bst-appdirs-home/.local/share/MyApp" "$(pv udata_default)" check_eq $P/uconfig-default "/bst-appdirs-home/.config/MyApp" "$(pv uconfig_default)" check_eq $P/ucache-default "/bst-appdirs-home/.cache/MyApp" "$(pv ucache_default)" check_eq $P/ustate-default "/bst-appdirs-home/.local/state/MyApp" "$(pv ustate_default)" check_eq $P/ulog-default "/bst-appdirs-home/.cache/MyApp/log" "$(pv ulog_default)" # APPNAME None RETURNS THE BASE DIRECTORY, and the version is dropped with it -- # the guard is `if appname and version', so a caller who passes a version and no # appname silently loses it rather than being told. check_eq $P/udata-noname "/bst-appdirs-home/.local/share" "$(pv udata_noname)" check_eq $P/uconfig-noname "/bst-appdirs-home/.config" "$(pv uconfig_noname)" check_eq $P/ucache-noname "/bst-appdirs-home/.cache" "$(pv ucache_noname)" check_eq $P/ustate-noname "/bst-appdirs-home/.local/state" "$(pv ustate_noname)" check_eq $P/udata-noname-versioned "/bst-appdirs-home/.local/share" "$(pv udata_noname_versioned)" # ...but user_log_dir STILL APPENDS `log' with no appname, because the opinion # block is not guarded by appname the way the version block is. check_eq $P/ulog-noname "/bst-appdirs-home/.cache/log" "$(pv ulog_noname)" # THE VERSION GOES ON THE END -- in six of the seven. check_eq $P/udata-versioned "/bst-appdirs-home/.local/share/MyApp/1.0" "$(pv udata_versioned)" check_eq $P/uconfig-versioned "/bst-appdirs-home/.config/MyApp/1.0" "$(pv uconfig_versioned)" check_eq $P/ucache-versioned "/bst-appdirs-home/.cache/MyApp/1.0" "$(pv ucache_versioned)" check_eq $P/ustate-versioned "/bst-appdirs-home/.local/state/MyApp/1.0" "$(pv ustate_versioned)" # ...AND IN user_log_dir IT GOES BEFORE `log'. The headline ordering finding -- # see the header. Both the literal path and the relationship are asserted, so a # future release that moved it says which of the two changed. check_eq $P/ulog-versioned "/bst-appdirs-home/.cache/MyApp/1.0/log" "$(pv ulog_versioned)" check_eq $P/ulog-versioned-is-cache-child "yes" "$(pv ulog_versioned_is_cache_child)" # `opinion=False' DROPS `log' AND GIVES BACK THE CACHE DIR, which means # user_log_dir(opinion=False) IS user_cache_dir -- asserted as an equality, not # only as a string, because that is the surprising half. check_eq $P/ulog-no-opinion "/bst-appdirs-home/.cache/MyApp" "$(pv ulog_no_opinion)" check_eq $P/ulog-no-opinion-is-cache "yes" "$(pv ulog_no_opinion_is_cache)" check_eq $P/ulog-no-opinion-versioned "/bst-appdirs-home/.cache/MyApp/1.0" "$(pv ulog_no_opinion_versioned)" # `opinion' ON user_cache_dir IS A NO-OP ON THIS PLATFORM -- it exists for the # Windows "Cache" suffix alone and the Unix branch never reads it. Asserted # because the signature offers it here. check_eq $P/ucache-no-opinion "/bst-appdirs-home/.cache/MyApp" "$(pv ucache_no_opinion)" check_eq $P/ucache-opinion-ignored "yes" "$(pv ucache_opinion_ignored)" # ------------------------------- appauthor and roaming are Windows-only ------ # The ticket asks for this explicitly and it is the most likely thing for a # caller to get wrong -- see the header. check_eq $P/udata-author "/bst-appdirs-home/.local/share/MyApp" "$(pv udata_author)" check_eq $P/udata-author-ignored "yes" "$(pv udata_author_ignored)" check_eq $P/udata-author-false "/bst-appdirs-home/.local/share/MyApp" "$(pv udata_author_false)" check_eq $P/uconfig-author "/bst-appdirs-home/.config/MyApp" "$(pv uconfig_author)" check_eq $P/ucache-author "/bst-appdirs-home/.cache/MyApp" "$(pv ucache_author)" check_eq $P/ustate-author "/bst-appdirs-home/.local/state/MyApp" "$(pv ustate_author)" check_eq $P/ulog-author "/bst-appdirs-home/.cache/MyApp/log" "$(pv ulog_author)" check_eq $P/sdata-author "/usr/local/share/MyApp" "$(pv sdata_author)" check_eq $P/sconfig-author "/etc/xdg/MyApp" "$(pv sconfig_author)" check_eq $P/udata-roaming "/bst-appdirs-home/.local/share/MyApp" "$(pv udata_roaming)" check_eq $P/udata-roaming-ignored "yes" "$(pv udata_roaming_ignored)" check_eq $P/uconfig-roaming "/bst-appdirs-home/.config/MyApp" "$(pv uconfig_roaming)" check_eq $P/ustate-roaming "/bst-appdirs-home/.local/state/MyApp" "$(pv ustate_roaming)" check_eq $P/udata-roaming-versioned "/bst-appdirs-home/.local/share/MyApp/1.0" "$(pv udata_roaming_versioned)" # ------------------------------------------- user_*: one variable each ------- # The mapping is asserted ONE VARIABLE AT A TIME, so a red names which one moved # -- and each group also asserts that a NEIGHBOURING function did not read it. check_eq $P/udata-xdg "/xdg/data/MyApp" "$(pv udata_xdg)" check_eq $P/udata-xdg-versioned "/xdg/data/MyApp/1.0" "$(pv udata_xdg_versioned)" check_eq $P/udata-xdg-noname "/xdg/data" "$(pv udata_xdg_noname)" check_eq $P/uconfig-not-data-home "/bst-appdirs-home/.config/MyApp" "$(pv uconfig_under_data_home)" check_eq $P/uconfig-xdg "/xdg/config/MyApp" "$(pv uconfig_xdg)" check_eq $P/uconfig-xdg-versioned "/xdg/config/MyApp/1.0" "$(pv uconfig_xdg_versioned)" check_eq $P/udata-not-config-home "/bst-appdirs-home/.local/share/MyApp" "$(pv udata_under_config_home)" check_eq $P/ucache-xdg "/xdg/cache/MyApp" "$(pv ucache_xdg)" check_eq $P/ucache-xdg-versioned "/xdg/cache/MyApp/1.0" "$(pv ucache_xdg_versioned)" # THERE IS NO XDG_LOG_HOME. user_log_dir IS user_cache_dir plus `log', so # XDG_CACHE_HOME is the only variable that moves it. check_eq $P/ulog-xdg "/xdg/cache/MyApp/log" "$(pv ulog_xdg)" check_eq $P/ustate-xdg "/xdg/state/MyApp" "$(pv ustate_xdg)" check_eq $P/ustate-xdg-versioned "/xdg/state/MyApp/1.0" "$(pv ustate_xdg_versioned)" # All four at once, which is the shape a desktop session actually sets. check_eq $P/all-udata "/xdg/data/MyApp" "$(pv all_udata)" check_eq $P/all-uconfig "/xdg/config/MyApp" "$(pv all_uconfig)" check_eq $P/all-ucache "/xdg/cache/MyApp" "$(pv all_ucache)" check_eq $P/all-ustate "/xdg/state/MyApp" "$(pv all_ustate)" check_eq $P/all-ulog "/xdg/cache/MyApp/log" "$(pv all_ulog)" # ------------------------------ the empty/relative XDG defect ---------------- # The headline finding -- see the header. A relative value is used as-is, and an # EMPTY one is worse: the answer is a bare name with no directory part at all. check_eq $P/udata-relative "relative/data/MyApp" "$(pv udata_relative)" check_eq $P/udata-relative-isabs "no" "$(pv udata_relative_isabs)" check_eq $P/udata-empty "MyApp" "$(pv udata_empty)" check_eq $P/udata-empty-isabs "no" "$(pv udata_empty_isabs)" check_eq $P/uconfig-empty "MyApp" "$(pv uconfig_empty)" check_eq $P/ucache-empty "MyApp" "$(pv ucache_empty)" check_eq $P/ulog-empty "MyApp/log" "$(pv ulog_empty)" check_eq $P/ustate-empty "MyApp" "$(pv ustate_empty)" # A `~' IS NOT EXPANDED FOR THE USER DIRECTORIES -- the value goes straight into # os.path.join. The SITE directories DO expanduser each element, and # `sdata-tilde' below is the other half of that asymmetry. check_eq $P/udata-tilde "~/mydata/MyApp" "$(pv udata_tilde)" # ------------------------------------------------------------- site_* -------- # The site directories take a LIST, and `multipath' chooses between the first # element and all of them joined by os.pathsep. check_eq $P/pathsep ":" "$(pv pathsep)" check_eq $P/sdata-default "/usr/local/share/MyApp" "$(pv sdata_default)" check_eq $P/sdata-default-multi "/usr/local/share/MyApp:/usr/share/MyApp" "$(pv sdata_default_multi)" check_eq $P/sdata-default-noname "/usr/local/share" "$(pv sdata_default_noname)" check_eq $P/sdata-default-noname-multi "/usr/local/share:/usr/share" "$(pv sdata_default_noname_multi)" # THE DEFAULT XDG_CONFIG_DIRS IS ONE ELEMENT, so site_config_dir reads the same # both ways while site_data_dir (two elements) does not. That is why # `sconfig-default-multi' is not a copy-paste error. check_eq $P/sconfig-default "/etc/xdg/MyApp" "$(pv sconfig_default)" check_eq $P/sconfig-default-multi "/etc/xdg/MyApp" "$(pv sconfig_default_multi)" check_eq $P/sconfig-default-noname "/etc/xdg" "$(pv sconfig_default_noname)" # THE VERSION IS FOLDED INTO THE APPNAME here, not appended at the end, so it # lands under EVERY element of a multipath answer rather than only the last. check_eq $P/sdata-versioned "/usr/local/share/MyApp/1.0" "$(pv sdata_versioned)" check_eq $P/sdata-versioned-multi "/usr/local/share/MyApp/1.0:/usr/share/MyApp/1.0" "$(pv sdata_versioned_multi)" check_eq $P/sconfig-versioned "/etc/xdg/MyApp/1.0" "$(pv sconfig_versioned)" check_eq $P/sconfig-versioned-multi "/etc/xdg/MyApp/1.0" "$(pv sconfig_versioned_multi)" check_eq $P/sdata-noname-versioned "/usr/local/share" "$(pv sdata_noname_versioned)" check_eq $P/sdata-xdg "/opt/share/MyApp" "$(pv sdata_xdg)" check_eq $P/sdata-xdg-multi "/opt/share/MyApp:/srv/share/MyApp" "$(pv sdata_xdg_multi)" check_eq $P/sdata-xdg-versioned-multi "/opt/share/MyApp/1.0:/srv/share/MyApp/1.0" "$(pv sdata_xdg_versioned_multi)" check_eq $P/sconfig-xdg "/opt/etc/MyApp" "$(pv sconfig_xdg)" check_eq $P/sconfig-xdg-multi "/opt/etc/MyApp:/srv/etc/MyApp" "$(pv sconfig_xdg_multi)" # A TRAILING SEPARATOR IS STRIPPED, and it is `rstrip(os.sep)' rather than a # single-character chop, so ALL of them go. A lone `/' therefore strips to the # EMPTY STRING and the join puts the result back at the root. check_eq $P/sdata-trailing-slash "/opt/share/MyApp" "$(pv sdata_trailing_slash)" check_eq $P/sdata-trailing-slashes "/opt/share/MyApp" "$(pv sdata_trailing_slashes)" check_eq $P/sdata-root-only "/MyApp" "$(pv sdata_root_only)" check_eq $P/sconfig-root-only "/MyApp" "$(pv sconfig_root_only)" # AN EMPTY XDG_DATA_DIRS DOES NOT FALL BACK EITHER -- and here the empty string # becomes an ABSOLUTE path, because the elements are joined with `os.sep' rather # than os.path.join. The same defect as `udata-empty' above with the opposite # answer, which is why the two are asserted in the same suite. check_eq $P/sdata-empty "/MyApp" "$(pv sdata_empty)" check_eq $P/sdata-empty-isabs "yes" "$(pv sdata_empty_isabs)" check_eq $P/sconfig-empty "/MyApp" "$(pv sconfig_empty)" # EACH ELEMENT IS expanduser'd, unlike the user directories -- see # `udata-tilde'. The asymmetry is upstream's and is asserted from both sides. check_eq $P/sdata-tilde "/bst-appdirs-home/share/MyApp" "$(pv sdata_tilde)" check_eq $P/sdata-tilde-multi "/bst-appdirs-home/share/MyApp:/srv/share/MyApp" "$(pv sdata_tilde_multi)" check_eq $P/sconfig-tilde "/bst-appdirs-home/etc/MyApp" "$(pv sconfig_tilde)" # A SINGLE-ELEMENT LIST MAKES multipath INDISTINGUISHABLE from the default. check_eq $P/sdata-one-multi "/only/one/MyApp" "$(pv sdata_one_multi)" check_eq $P/sdata-one-single "/only/one/MyApp" "$(pv sdata_one_single)" check_eq $P/sdata-one-same "yes" "$(pv sdata_one_same)" # AN EMPTY ELEMENT IN THE MIDDLE IS KEPT, not skipped: the list is split on # os.pathsep with no filtering, so `/a::/b' is three elements and the middle one # resolves to the root. check_eq $P/sdata-empty-element-multi "/a/MyApp:/MyApp:/b/MyApp" "$(pv sdata_empty_element_multi)" # ------------------------------------------------------- the AppDirs class --- # A thin property wrapper. The equivalence with the free functions is the only # thing it promises, and `cls-matches-free' asserts all seven at once. check_eq $P/cls-udata "/bst-appdirs-home/.local/share/MyApp/1.0" "$(pv cls_udata)" check_eq $P/cls-uconfig "/bst-appdirs-home/.config/MyApp/1.0" "$(pv cls_uconfig)" check_eq $P/cls-ucache "/bst-appdirs-home/.cache/MyApp/1.0" "$(pv cls_ucache)" check_eq $P/cls-ustate "/bst-appdirs-home/.local/state/MyApp/1.0" "$(pv cls_ustate)" check_eq $P/cls-ulog "/bst-appdirs-home/.cache/MyApp/1.0/log" "$(pv cls_ulog)" check_eq $P/cls-sdata "/usr/local/share/MyApp/1.0" "$(pv cls_sdata)" check_eq $P/cls-sconfig "/etc/xdg/MyApp/1.0" "$(pv cls_sconfig)" check_eq $P/cls-matches-free "yes" "$(pv cls_matches_free)" # THE PROPERTIES RECOMPUTE ON EVERY READ, so an AppDirs is a live view of its # own fields rather than a snapshot taken in __init__ -- mutating `.version' # after construction moves every path. check_eq $P/cls-before-mutate "/bst-appdirs-home/.local/share/MyApp" "$(pv cls_before_mutate)" check_eq $P/cls-after-mutate "/bst-appdirs-home/.local/share/MyApp/1.0" "$(pv cls_after_mutate)" # `multipath' IS A CONSTRUCTOR ARGUMENT and reaches only the two site # properties. check_eq $P/cls-sdata-multi "/usr/local/share/MyApp:/usr/share/MyApp" "$(pv cls_sdata_multi)" check_eq $P/cls-sconfig-multi "/etc/xdg/MyApp" "$(pv cls_sconfig_multi)" # `roaming' REACHES ONLY THREE OF THE SEVEN PROPERTIES: AppDirs.user_state_dir # does NOT forward it, even though the free `user_state_dir' accepts one. # Invisible on this platform, where roaming is ignored anyway, and asserted as # the shape it is so the port's record says the asymmetry was looked at rather # than missed. check_eq $P/cls-roaming-udata "/bst-appdirs-home/.local/share/MyApp" "$(pv cls_roaming_udata)" check_eq $P/cls-roaming-ustate "/bst-appdirs-home/.local/state/MyApp" "$(pv cls_roaming_ustate)" check_eq $P/cls-roaming-same "yes" "$(pv cls_roaming_same_as_not)" check_eq $P/cls-noname-udata "/bst-appdirs-home/.local/share" "$(pv cls_noname_udata)" check_eq $P/cls-noname-ulog "/bst-appdirs-home/.cache/log" "$(pv cls_noname_ulog)" # ------------------------------------------------------- the argument types -- # A NON-STRING APPNAME IS JOINED, NOT VALIDATED, so the failure surfaces as a # TypeError out of os.path.join rather than as anything appdirs says. Asserted # by exception NAME, because that is the contract a caller has to code against # and CPython's wording for it has moved between releases. check_eq $P/udata-int-appname "!TypeError" "$(pv udata_int_appname)" check_eq $P/sdata-int-appname "!TypeError" "$(pv sdata_int_appname)" # AN EMPTY APPNAME IS FALSY, so it behaves exactly like None rather than adding # an empty component. check_eq $P/udata-empty-appname "/bst-appdirs-home/.local/share" "$(pv udata_empty_appname)" check_eq $P/udata-empty-appname-is-noname "yes" "$(pv udata_empty_appname_is_noname)" # A NON-STRING VERSION REACHES os.path.join TOO -- but only when an appname is # present, which is what makes this a raise and `udata-noname-versioned' above a # quiet drop. check_eq $P/udata-int-version "!TypeError" "$(pv udata_int_version)" # AN APPNAME WITH A SEPARATOR IN IT IS NOT SANITISED: it becomes two components. check_eq $P/udata-sep-appname "/bst-appdirs-home/.local/share/My/App" "$(pv udata_sep_appname)" # --------------------------------------------------------------- filesystem -- # THE MODULE NEVER TOUCHES THE FILESYSTEM. Every path above was composed against # a HOME that does not exist, and after all 139 measurements it still does not: # nothing was created, and nothing raised for a directory that was absent. This # is the header's central claim, measured. check_eq $P/home-env "/bst-appdirs-home" "$(pv home_env)" check_eq $P/home-absent "yes" "$(pv home_absent)" check $P/home-absent-onrootfs test ! -e /bst-appdirs-home check_eq $P/cwd-absolute "yes" "$(pv cwd_unchanged)" rm -rf "$W"