. "$BST_LIB" # BST-TIMEOUT: 1800 # THREE python3 starts, plus ~30 execs of a two-line # # /bin/sh stub inside the third. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for 86 measurements -- the CLI cases # # included, because they go through runpy rather than # # through a fourth interpreter. # # # # This suite is NOT like its pure-Python neighbours, and # # the number has to say so: `editor' RUNS A PROGRAM. Every # # edit() case is a fork, an execve of a `#!' script, and a # # wait -- so unlike 99-python3-appdirs next door, the # # interpreter start is not the whole cost here. 1800 is # # 400s per start (the per-start budget the neighbouring # # python3 suites use) plus 600s for the exec traffic. # # # # A suite-timeout red is a batch-wide cost, so if the # # execs 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-editor 1.0.4 (meta-openembedded meta-python/recipes-devtools/python, # python3-editor_1.0.4.bb, `inherit pypi setuptools3', PyPI sdist) on the # wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # `editor' RESOLVES A PROGRAM AND RUNS IT ON A FILE. `get_editor()' picks the # name, `get_editor_args()' decides the flags that go with it, and `edit()' # writes the seed bytes, spawns the program, waits for it, and reads the file # back. One 133-line flat module, `editor.py'. # # So this is NOT a pure-composition library like appdirs or platformdirs next # door: it is fork, execve and wait. That is what shapes the suite -- every # interesting case needs a program that actually runs, and the probe writes its # own: a two-line `/bin/sh' stub that records its argv, rewrites the file it was # handed, and exits. NOTHING HERE NEEDS A HUMAN OR A TERMINAL, which is what # makes this port close on its suite. # # ============================================================================ # THE PORT TURNS ON ONE LINE, AND IT IS AN IMPORT. # ============================================================================ # # editor.py line 11 is # # from distutils.spawn import find_executable # # and DISTUTILS WAS REMOVED FROM THE STANDARD LIBRARY IN PYTHON 3.12, which is # this image's interpreter. There is no `python3-distutils' package: poky's # python3 3.12.13 splits into 66 packages and none of them carries it. # # `import editor' works here ONLY because setuptools ships # `distutils-precedence.pth', which `site' executes at interpreter start and # which installs an import hook mapping the name `distutils' onto # `setuptools._distutils'. So the recipe's # # RDEPENDS:${PN} += " python3-io python3-setuptools " # # is LOAD-BEARING, not decorative: without python3-setuptools this package does # not import at all on this arm. `distutils-shim', `distutils-spawn-file', # `find-executable-module' and `setuptools-installed' are the four cases that # say so, and `pth-present' asserts the mechanism itself is on the rootfs. # # The other half of that RDEPENDS is equally load-bearing and for an ordinary # reason: `tempfile' is in python3-io, while `subprocess', `locale' and # `signal' are in python3-core, which setuptools3-base already declares. The # built .deb's line is exactly "Depends: python3-core, python3-io, # python3-setuptools" and `pkg-depends-exact' asserts it verbatim. # # ============================================================================ # SIX BEHAVIOURS A CALLER WILL GET WRONG. THESE ARE THE POINT OF THE SUITE. # ============================================================================ # # THE CURRENT DIRECTORY IS SEARCHED BEFORE $PATH, AND IT IS NOT ON $PATH. # With neither $VISUAL nor $EDITOR set, get_editor() walks # `editor, vim, emacs, nano' through find_executable, and # find_executable tests `os.path.isfile(executable)' -- the # BARE NAME, resolved against the process's current directory # -- BEFORE it looks at $PATH at all. So a file called `nano' # in whatever directory the process happens to be in is picked # up ahead of every real editor, and comes back as a RELATIVE # name. `ge-cwd-injection' measures it: a program a caller # never installed, chosen because of where they were standing. # # A FILE THAT IS NOT EXECUTABLE IS STILL "FOUND". # find_executable checks isfile and never os.access(X_OK), so # a non-executable `vim' on $PATH is what get_editor() returns # and the failure is deferred to the exec inside edit(), as a # PermissionError. Two cases, side by side, because the useful # part is that the two halves disagree. # # A MISSING EDITOR RAISES FileNotFoundError, NOT EditorError. # The ticket expected EditorError. get_editor() returns # whatever $EDITOR said without checking that it exists, so # the failure surfaces out of subprocess.Popen with CPython's # name on it. EditorError is reachable through EXACTLY ONE # path -- nothing in the environment and nothing found on the # fallback walk -- and `ge-none-found' is the only case in # this file that produces one. # # THE EDITOR'S EXIT STATUS IS DISCARDED. # The ticket expected a non-zero exit to be surfaced. edit() # calls `proc.communicate()', throws the result away and never # reads `returncode', so an editor that failed is # indistinguishable from one that saved: # `edit-nonzero-exit-result' asserts the ordinary return # value from a stub that exited 3. A caller who needs to know # a user aborted cannot learn it from this API. # # AN $EDITOR CARRYING ARGUMENTS IS NOT SPLIT. # The whole environment string becomes argv[0], so the # spelling every other tool documents -- EDITOR="vim -f" -- # fails here with FileNotFoundError. get_editor() returns it # untouched, which is why `ge-editor-with-args' passes and # `edit-editor-with-args' does not. # # THE FLAGS FOLLOW THE SYMLINK'S TARGET, NOT THE NAME THAT WAS TYPED. # edit() computes its flags from # `basename(realpath(editor))'. So a link called `myeditor' # pointing at `vim' IS invoked with vim's `-f -o', and a link # called `vim' pointing at anything else gets none. # `args-symlink-takes-target-name' and # `args-link-named-vim-gets-none' are that pair. # # ============================================================================ # WHAT THE TICKET ASKED FOR THAT 1.0.4 DOES NOT HAVE. # ============================================================================ # # `get_tmp_dir()' does not exist -- the temporary file is made inline in edit() # by tempfile.NamedTemporaryFile. `has-get-tmp-dir' asserts its absence rather # than leaving a reader to wonder whether the suite forgot it. # # ============================================================================ # `__all__' EXISTS HERE. # ============================================================================ # # It names `edit', `get_editor' and `EditorError', so `from editor import *' # leaks nothing -- the opposite of appdirs next door, whose star-import rebinds # `os'. Asserted because the two neighbours differ. # # ============================================================================ # THERE IS NO EXECUTABLE, AND THERE IS A CLI. # ============================================================================ # # editor.py has an argparse `__main__' block, and setup.py declares neither # `scripts' nor `entry_points' -- so nothing lands in /usr/bin and the only way # to reach it is `python3 -m editor'. `pkg-no-bin' asserts the first; # `cli-get-editor' and `cli-no-subcommand' exercise the second THROUGH runpy # inside the probe, so they cost no interpreter start. A bare `python3 -m # editor' raises AttributeError rather than printing usage: argparse subparsers # are optional on Python 3 and the module never caught up from 2.7. # # ============================================================================ # IT IS A FLAT MODULE, NOT A PACKAGE. # ============================================================================ # # `py_modules=["editor"]' in setup.py, so site-packages holds `editor.py' and no # `editor/' directory. Upstream's `test.py' is not packaged either. Unlike # appdirs there is no `universal = 1' in setup.cfg, so the wheel carries # `Tag: py3-none-any' and NO py2 tag -- asserted on both sides, because the # neighbouring suite asserts the opposite and a reader should see that the # difference was measured. # # ============================================================================ # WHY NO FIXTURE IS SHIPPED AND ALL OF THEM ARE BUILT. # ============================================================================ # # Every editor this suite resolves to is a stub the probe writes under $TMPDIR, # along with the $PATH directories, the symlinks and the seed files. That is # what keeps it deterministic -- no case depends on which editors this image # happens to install -- and it is why every expectation below is a LITERAL. The # probe reports paths with its own random workdir replaced by `$W', removes the # tree at the end, and `workdir-gone' says so. # # ============================================================================ P=python3-editor # 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/editor.py DI=$SITE/python_editor-1.0.4.dist-info PKGLIST=/var/lib/dpkg/info/python3-editor.list PROBE=$BST_DATA/python3-editor-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-editor-suite.$$ 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/editor" # Upstream's test.py is not packaged (setup.py declares py_modules, not # packages), and neither is README.md. check $P/no-test-py test ! -e "$SITE/test.py" check $P/no-readme test ! -e "$SITE/README.md" check $P/pycache-mod test -s "$SITE/__pycache__/editor.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. # python-editor ships no C -- no .c, 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)" # NOTHING LANDS IN /usr/bin. setup.py declares no `entry_points' and no # `scripts', so the argparse main in editor.py is reachable only through # `python3 -m editor' -- which is what the cli-* cases below drive. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/(s?bin)/' "$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 DIST-INFO IS NAMED FOR THE DISTRIBUTION, NOT THE MODULE. The distribution # is `python-editor' and the module is `editor', so the directory is # `python_editor-1.0.4.dist-info' while the import name is neither. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: python-editor" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 1.0.4" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-license test -s "$DI/LICENSE" # `setuptools3' BUILT THIS, WHICH IS VISIBLE IN THE WHEEL METADATA. Unlike # appdirs next door there is no `universal = 1' in setup.cfg, so there is ONE # tag and it is py3 -- asserted on both sides so that the difference between the # two neighbouring ports is a measurement rather than an omission. 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_eq $P/dist-info-no-py2-tag "" \ "$(grep '^Tag: py2' "$DI/WHEEL" 2>/dev/null)" check_out $P/dist-info-top-level "editor" cat "$DI/top_level.txt" sed -n '/^Package: python3-editor$/,/^$/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.0.4-r0" cat "$W/st" # THE RDEPENDS IS ASSERTED EXACTLY, and both of its halves are load-bearing -- # see the header. python3-io carries `tempfile'; python3-setuptools carries the # distutils shim without which this module does not import on 3.12. A lean # image that dropped either 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, python3-io, python3-setuptools" \ "$(cat "$W/dep")" # THE SHIM ITSELF, ON THE ROOTFS. `site' runs this .pth at every interpreter # start; it is the whole reason `import distutils' resolves on 3.12. check $P/setuptools-installed test -d "$SITE/setuptools" check $P/pth-present test -s "$SITE/distutils-precedence.pth" check $P/hack-present test -s "$SITE/_distutils_hack/__init__.py" check $P/shim-spawn test -s "$SITE/setuptools/_distutils/spawn.py" # ...and no stdlib distutils, which is the other half of the claim: 3.12 removed # it and no python3 split puts it back. check $P/no-stdlib-distutils test ! -d /usr/lib/python3.12/distutils # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import editor' # A DeprecationWarning from the distutils shim, or a stray print on import, # would be invisible to the case above, which scores only the exit status. # setuptools 69.1.1 warns only when SETUPTOOLS_USE_DISTUTILS is set to the # non-default value, so the expected output here is nothing at all. check_eq $P/import-quiet "" \ "$(python3 -c 'import editor' 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" 1200 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 "86" "$(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.0.4" "$(pv version_attr)" # `__all__' EXISTS, and a star-import therefore leaks exactly three names -- # see the header. check_eq $P/has-dunder-all "yes" "$(pv has_dunder_all)" check_eq $P/dunder-all "EditorError,edit,get_editor" "$(pv dunder_all)" check_eq $P/star-names "EditorError,edit,get_editor" "$(pv star_names)" # THE TICKET ASKED FOR get_tmp_dir(). 1.0.4 has no such function. check_eq $P/has-get-tmp-dir "no" "$(pv has_get_tmp_dir)" check_eq $P/has-get-editor "yes" "$(pv has_get_editor)" check_eq $P/has-edit "yes" "$(pv has_edit)" check_eq $P/has-get-editor-args "yes" "$(pv has_get_editor_args)" check_eq $P/has-get-default-editors "yes" "$(pv has_get_default_editors)" check_eq $P/has-get-tty-filename "yes" "$(pv has_get_tty_filename)" # EditorError IS A RuntimeError, which is what a caller has to catch if they # want to catch it without naming the module's own class. check_eq $P/editor-error-runtimeerror "yes" "$(pv editor_error_is_runtimeerror)" check_eq $P/editor-error-exception "yes" "$(pv editor_error_is_exception)" # THE FALLBACK LIST AND ITS ORDER. This is what a user with nothing set gets. check_eq $P/default-editors "editor,vim,emacs,nano" "$(pv default_editors)" check_eq $P/tty-filename "/dev/tty" "$(pv tty_filename)" check_eq $P/sys-platform "linux" "$(pv sys_platform)" # --------------------------------------------------------- the distutils shim - # The four cases the port turns on -- see the header. check_eq $P/distutils-spawn-file \ "$SITE/setuptools/_distutils/spawn.py" "$(pv distutils_spawn_file)" check_eq $P/distutils-shim "yes" "$(pv distutils_is_setuptools_shim)" check_eq $P/find-executable-module "distutils.spawn" "$(pv find_executable_module)" check_eq $P/distutils-hack-loaded "yes" "$(pv distutils_hack_loaded)" # ----------------------------------------------------------- the stub, first -- # MEASURED BEFORE ANYTHING DEPENDS ON IT: this image can fork, exec a `#!' # script and reap it. Every edit() case below does exactly that, so a red here # explains all of them at once and a red anywhere else is then not about # `editor'. check_eq $P/stub-execs "yes" "$(pv stub_execs)" # NEITHER STREAM IS A TERMINAL under the runner, so edit()'s default # `sys.stdin.isatty() and not sys.stdout.isatty()' is False and nothing opens # /dev/tty. That is what makes this package usable non-interactively here. check_eq $P/stdin-isatty "no" "$(pv stdin_isatty)" check_eq $P/stdout-isatty "no" "$(pv stdout_isatty)" check_eq $P/use-tty-default "no" "$(pv use_tty_default)" # ------------------------------------------- get_editor(): VISUAL, then EDITOR - check_eq $P/ge-visual-only "/x/visual" "$(pv ge_visual_only)" check_eq $P/ge-editor-only "/x/editor" "$(pv ge_editor_only)" check_eq $P/ge-both "/x/visual" "$(pv ge_both)" # SET-AND-EMPTY IS TREATED AS ABSENT, on both variables, because the test is # `or' over the VALUES rather than `in os.environ'. `ge-both-empty' is the # second of the two places EditorError comes from, and it is the same place: # with both variables falsy the fallback walk runs and finds nothing. check_eq $P/ge-visual-empty "/x/editor" "$(pv ge_visual_empty)" check_eq $P/ge-both-empty "!EditorError" "$(pv ge_both_empty)" # ...but a variable set to a SPACE is truthy and comes back untouched: nothing # strips, splits or validates what the environment said. check_eq $P/ge-editor-space "' '" "$(pv ge_editor_space)" check_eq $P/ge-editor-with-args "vim -f -o" "$(pv ge_editor_with_args)" # --------------------------- get_editor(): the PATH fallback, and its order --- check_eq $P/ge-path-nano '$W/pathdir/nano' "$(pv ge_path_nano)" check_eq $P/ge-path-emacs-beats-nano '$W/pathdir/emacs' "$(pv ge_path_emacs_beats_nano)" check_eq $P/ge-path-vim-beats-emacs '$W/pathdir/vim' "$(pv ge_path_vim_beats_emacs)" check_eq $P/ge-path-editor-wins '$W/pathdir/editor' "$(pv ge_path_editor_wins)" # NOTHING SET AND NOTHING FOUND IS THE ONLY WAY EditorError IS RAISED. Its # message is upstream's, and upstream concatenated two string literals without a # separator, so it reads `...system.Please consider...' with no space. Asserted # verbatim, because it is the one message this package produces and a caller # grepping for it will grep for this. check_eq $P/ge-none-found "!EditorError" "$(pv ge_none_found)" check_eq $P/ge-none-found-message \ 'Unable to find a viable editor on this system.Please consider setting your $EDITOR variable' \ "$(pv ge_none_found_message)" # AN EMPTY $PATH IS NOT A SEARCH OF THE CURRENT DIRECTORY: find_executable # returns None outright rather than splitting `' into one empty component. check_eq $P/ge-path-empty "!EditorError" "$(pv ge_path_empty)" # THE CURRENT DIRECTORY IS SEARCHED FIRST, AND IT IS NOT ON $PATH -- the # headline finding. See the header. check_eq $P/ge-cwd-injection "nano" "$(pv ge_cwd_injection)" check_eq $P/ge-cwd-injection-is-relative "yes" "$(pv ge_cwd_injection_is_relative)" # A FILE THAT IS NOT EXECUTABLE IS STILL "FOUND", and the two halves disagree: # get_editor() hands it back, edit() cannot run it. check_eq $P/ge-nonexecutable-found '$W/noexec/vim' "$(pv ge_nonexecutable_found)" check_eq $P/ge-nonexecutable-edit "!PermissionError" "$(pv ge_nonexecutable_edit)" # ------------------------------------------------------------ get_editor_args - # A NAME TABLE, NOT A PATH TABLE. Every arm matches a BARE program name, so a # caller who passes what get_editor() returned from $EDITOR -- routinely an # absolute path -- silently gets no flags. edit() itself does not have this # problem because it passes basename(realpath(...)); the args-* cases below are # the same table reached the other way. check_eq $P/gea-vim "-f -o" "$(pv gea_vim)" check_eq $P/gea-gvim "-f -o" "$(pv gea_gvim)" check_eq $P/gea-vim-basic "-f -o" "$(pv gea_vim_basic)" check_eq $P/gea-vim-tiny "-f -o" "$(pv gea_vim_tiny)" check_eq $P/gea-emacs "-nw" "$(pv gea_emacs)" check_eq $P/gea-gedit "-w --new-window" "$(pv gea_gedit)" check_eq $P/gea-nano "-R" "$(pv gea_nano)" check_eq $P/gea-vi "" "$(pv gea_vi)" check_eq $P/gea-code "" "$(pv gea_code)" check_eq $P/gea-empty "" "$(pv gea_empty)" check_eq $P/gea-abspath "" "$(pv gea__usr_bin_vim)" check_eq $P/gea-uppercase "" "$(pv gea_VIM)" # ==================================================================== edit ==== # `EDITED\n' is 45 44 49 54 45 44 0a -- what every rewriting stub writes. check_eq $P/edit-filename-contents "4544495445440a" "$(pv edit_filename_contents)" check_eq $P/edit-filename-argv '$W/seed.txt' "$(pv edit_filename_argv)" check_eq $P/edit-return-type "bytes" "$(pv edit_return_type)" # THE NAMED FILE IS REWRITTEN IN PLACE, which is the contract a caller who # passes `filename' is relying on. check_eq $P/edit-filename-inplace "4544495445440a" "$(pv edit_filename_inplace)" # contents=None DOES NOT WRITE: with a stub that touches nothing, what comes # back is exactly what was already in the file (`keepme'). check_eq $P/edit-no-contents-preserves "6b6565706d65" "$(pv edit_no_contents_preserves)" # A str IS ENCODED WITH THE DEFAULT ENCODING, not the locale's -- the module # calls `contents.encode()' with no argument. `héllo' is 68 c3 a9 6c 6c 6f. check_eq $P/edit-str-contents "68c3a96c6c6f" "$(pv edit_str_contents)" check_eq $P/default-encoding "utf-8" "$(pv default_encoding)" # AN EMPTY contents IS NOT None, so it writes -- and truncates the file. check_eq $P/edit-empty-contents "" "$(pv edit_empty_contents)" # NO FILENAME: a NamedTemporaryFile is made, edited, read back, and DELETED when # the local goes out of scope at return. The stub recorded the path, so the # temporary file's disappearance is measured rather than assumed. check_eq $P/edit-tmp-result "4544495445440a" "$(pv edit_tmp_result)" check_eq $P/edit-tmp-path-absolute "yes" "$(pv edit_tmp_path_absolute)" check_eq $P/edit-tmp-removed "yes" "$(pv edit_tmp_removed)" # `suffix' reaches NamedTemporaryFile, so it is the temporary file's extension # -- which is how a caller gets their editor's syntax highlighting. check_eq $P/edit-suffix-applied "yes" "$(pv edit_suffix_applied)" check_eq $P/edit-suffix-removed "yes" "$(pv edit_suffix_removed)" # THE EDITOR'S EXIT STATUS IS DISCARDED -- see the header. A stub that exited 3 # is indistinguishable from one that saved. check_eq $P/edit-nonzero-exit-result "4544495445440a" "$(pv edit_nonzero_exit_result)" # A MISSING EDITOR RAISES FileNotFoundError, NOT EditorError -- see the header. check_eq $P/edit-missing-editor "!FileNotFoundError" "$(pv edit_missing_editor)" # AN $EDITOR CARRYING ARGUMENTS IS NOT SPLIT: the whole string is argv[0]. check_eq $P/edit-editor-with-args "!FileNotFoundError" "$(pv edit_editor_with_args)" # A DIRECTORY named as the editor gets as far as the exec and fails there. check_eq $P/edit-editor-is-dir "!PermissionError" "$(pv edit_editor_is_dir)" # A filename that does not exist, with contents=None: nothing creates it, so the # READ-BACK at the end of edit() is what fails -- after the editor has already # been spawned on a path that was never there. check_eq $P/edit-missing-file "!FileNotFoundError" "$(pv edit_missing_file)" # ------------------------------------------------- the flags edit() actually -- # basename(realpath(editor)), NOT what the user typed. An absolute path still # gets its flags -- which is what makes `gea-abspath' above harmless in # practice -- and a SYMLINK is resolved first, so the flags follow the TARGET. check_eq $P/args-vim-by-abspath '-f -o $W/seed.txt' "$(pv args_vim_by_abspath)" check_eq $P/args-nano-by-abspath '-R $W/seed.txt' "$(pv args_nano_by_abspath)" check_eq $P/args-symlink-takes-target-name \ '-f -o $W/seed.txt' "$(pv args_symlink_takes_target_name)" check_eq $P/args-link-named-vim-gets-none \ '$W/seed.txt' "$(pv args_link_named_vim_gets_none)" # THE DEFAULT use_tty PATH, taken rather than forced: nothing opened /dev/tty # and edit() returned normally. check_eq $P/edit-default-use-tty "4544495445440a" "$(pv edit_default_use_tty)" # edit() RESOLVES THROUGH get_editor(), so the $PATH fallback drives it too -- # no $EDITOR set at all in this case, and the flags are vim's because vim is # what the walk found. check_eq $P/edit-via-path-fallback "4544495445440a" "$(pv edit_via_path_fallback)" check_eq $P/edit-via-path-fallback-argv '-f -o $W/seed.txt' "$(pv edit_via_path_fallback_argv)" # ===================================================================== CLI ==== # Reached through runpy inside the probe, so these cost no interpreter start. check_eq $P/cli-get-editor "/x/from-env" "$(pv cli_get_editor)" check_eq $P/cli-no-subcommand "!AttributeError" "$(pv cli_no_subcommand)" # `edit' PRINTS THE repr OF THE BYTES, not the bytes, so the CLI's output # carries the b'...' wrapper and a shell caller cannot use it directly. check_eq $P/cli-edit "b'EDITED\\n'" "$(pv cli_edit)" # The CLI encodes with locale.getpreferredencoding() rather than the default # encoding edit() itself uses -- the one place the module-level `locale' import # is live. The VALUE is the image's locale rather than this package's business, # so what is asserted is that there is one. check_eq $P/cli-preferred-encoding-known "yes" \ "$([ -n "$(pv preferred_encoding)" ] && echo yes || echo no)" # --------------------------------------------------------------- filesystem -- # The probe built every fixture it used and took it away again. check_eq $P/workdir-gone "yes" "$(pv workdir_gone)" check_eq $P/cwd-restored "yes" "$(pv cwd_restored)" rm -rf "$W"