From b25981335722e606a28c0ddd049dd08b68cbdaf1 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Fri, 19 Dec 2025 20:20:20 +0300 Subject: [PATCH 1/3] tests: refactor: rename and consolidate create_executable() functions --- test/exec.bats | 44 +++++++++++------------------------- test/init.bats | 12 ++-------- test/latest.bats | 28 +++++++++-------------- test/pip-rehash.bats | 23 +++++++------------ test/rehash.bats | 31 ++++++++++---------------- test/test_helper.bash | 34 ++++++++++++++++++++++++++++ test/versions.bats | 12 ++-------- test/whence.bats | 15 ++++--------- test/which.bats | 52 +++++++++++++++++-------------------------- 9 files changed, 105 insertions(+), 146 deletions(-) diff --git a/test/exec.bats b/test/exec.bats index cf818dbe..c1db5fbc 100644 --- a/test/exec.bats +++ b/test/exec.bats @@ -2,18 +2,6 @@ load test_helper -create_executable() { - name="${1?}" - shift 1 - bin="${PYENV_ROOT}/versions/${PYENV_VERSION}/bin" - mkdir -p "$bin" - { if [ $# -eq 0 ]; then cat - - else printf '%s\n' "$@" - fi - } | sed -Ee '1s/^ +//' > "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "fails with invalid version" { bats_require_minimum_version 1.5.0 export PYENV_VERSION="3.4" @@ -38,8 +26,8 @@ EOF @test "completes with names of executables" { export PYENV_VERSION="3.4" - create_executable "fab" "#!/bin/sh" - create_executable "python" "#!/bin/sh" + create_alt_executable "fab" + create_alt_executable "python" pyenv-rehash run pyenv-completions exec @@ -65,7 +53,7 @@ SH @test "forwards all arguments" { export PYENV_VERSION="3.4" - create_executable "python" < "${PYENV_TEST_DIR}/python3" < "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "creates shims and versions directories" { assert [ ! -d "${PYENV_ROOT}/shims" ] assert [ ! -d "${PYENV_ROOT}/versions" ] @@ -179,7 +171,7 @@ echo "\$PATH" } @test "outputs sh-compatible case syntax" { - create_executable pyenv-commands < "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "read from installed" { - create_executable pyenv-versions < "${bin}/$1" - chmod +x "${bin}/$1" -} - copy_src_pyenvd() { mkdir -p "${PYENV_ROOT}" cp -r "${BATS_TEST_DIRNAME}/../pyenv.d" "${PYENV_ROOT}" @@ -16,8 +9,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'pip'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "pip" + create_alt_executable "example" + create_alt_executable "pip" copy_src_pyenvd run command -v example 2> /dev/null assert_failure @@ -29,8 +22,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'pip3'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "pip3" + create_alt_executable "example" + create_alt_executable "pip3" copy_src_pyenvd run command -v example 2> /dev/null assert_failure @@ -42,8 +35,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'pip3.x'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "pip3.7" + create_alt_executable "example" + create_alt_executable "pip3.7" copy_src_pyenvd run command -v example 2> /dev/null assert_failure @@ -55,8 +48,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'python -m pip install'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "python" + create_alt_executable "example" + create_alt_executable "python" copy_src_pyenvd run command -v example 2> /dev/null assert_failure diff --git a/test/rehash.bats b/test/rehash.bats index 860d9d79..25cdd808 100755 --- a/test/rehash.bats +++ b/test/rehash.bats @@ -2,13 +2,6 @@ load test_helper -create_executable() { - local bin="${PYENV_ROOT}/versions/${1}/bin" - mkdir -p "$bin" - touch "${bin}/$2" - chmod +x "${bin}/$2" -} - @test "empty rehash" { assert [ ! -d "${PYENV_ROOT}/shims" ] run pyenv-rehash @@ -42,10 +35,10 @@ create_executable() { } @test "creates shims" { - create_executable "2.7" "python" - create_executable "2.7" "fab" - create_executable "3.4" "python" - create_executable "3.4" "py.test" + create_alt_executable_in_version "2.7" "python" + create_alt_executable_in_version "2.7" "fab" + create_alt_executable_in_version "3.4" "python" + create_alt_executable_in_version "3.4" "py.test" assert [ ! -e "${PYENV_ROOT}/shims/fab" ] assert [ ! -e "${PYENV_ROOT}/shims/python" ] @@ -68,8 +61,8 @@ OUT touch "${PYENV_ROOT}/shims/oldshim1" chmod +x "${PYENV_ROOT}/shims/oldshim1" - create_executable "3.4" "fab" - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "fab" + create_alt_executable_in_version "3.4" "python" run pyenv-rehash assert_success "" @@ -78,8 +71,8 @@ OUT } @test "binary install locations containing spaces" { - create_executable "dirname1 p247" "python" - create_executable "dirname2 preview1" "py.test" + create_alt_executable_in_version "dirname1 p247" "python" + create_alt_executable_in_version "dirname2 preview1" "py.test" assert [ ! -e "${PYENV_ROOT}/shims/python" ] assert [ ! -e "${PYENV_ROOT}/shims/py.test" ] @@ -108,28 +101,28 @@ SH } @test "sh-rehash in bash" { - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" PYENV_SHELL=bash run pyenv-sh-rehash assert_success "command pyenv rehash hash -r 2>/dev/null || true" } @test "sh-rehash in bash (integration)" { - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" run eval "$(pyenv-sh-rehash)" assert_success assert [ -x "${PYENV_ROOT}/shims/python" ] } @test "sh-rehash in fish" { - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" PYENV_SHELL=fish run pyenv-sh-rehash assert_success "command pyenv rehash" } @test "sh-rehash in fish (integration)" { command -v fish >/dev/null || skip "-- fish not installed" - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" run fish -Nc "eval (pyenv-sh-rehash)" assert_success assert [ -x "${PYENV_ROOT}/shims/python" ] diff --git a/test/test_helper.bash b/test/test_helper.bash index 0a760b2f..b28f3b17 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -2,6 +2,7 @@ unset PYENV_VERSION unset PYENV_DIR setup() { + export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' if ! enable -f "${BATS_TEST_DIRNAME}"/../libexec/pyenv-realpath.dylib realpath 2>/dev/null; then if [ -n "$PYENV_NATIVE_EXT" ]; then echo "pyenv: failed to load \`realpath' builtin" >&2 @@ -141,6 +142,39 @@ path_without() { echo "$path" } +create_path_executable() { + create_executable "${PYENV_TEST_DIR}/bin" "$@" +} + +create_alt_executable() { + create_alt_executable_in_version "${PYENV_VERSION}" "$@" +} + +create_alt_executable_in_version() { + local version="${1:?}" + shift 1 + create_executable "${PYENV_ROOT}/versions/$version/bin" "$@" +} + +create_executable() { + bin="${1:?}" + name="${2:?}" + shift 2 + mkdir -p "$bin" + local payload + # Bats doesn't redirect stdin + if [[ $# -eq 0 && ! -t 0 ]]; then + payload="$(cat -)" + else + payload="$(printf '%s\n' "$@")" + fi + if [[ $payload != "#!/"* ]]; then + payload="#!$BASH"$'\n'"$payload" + fi + echo "$payload" > "${bin}/$name" + chmod +x "${bin}/$name" +} + create_hook() { mkdir -p "${PYENV_HOOK_PATH}/$1" touch "${PYENV_HOOK_PATH}/$1/$2" diff --git a/test/versions.bats b/test/versions.bats index e14285fc..58ab032c 100644 --- a/test/versions.bats +++ b/test/versions.bats @@ -22,14 +22,6 @@ stub_system_python() { touch "$stub" && chmod +x "$stub" } -create_executable() { - local name="$1" - local bin="${PYENV_TEST_DIR}/bin" - mkdir -p "$bin" - sed -Ee '1s/^ +//' > "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "no versions installed" { stub_system_python assert [ ! -d "${PYENV_ROOT}/versions" ] @@ -199,7 +191,7 @@ OUT create_version "1.9.0" create_version "1.53.0" create_version "1.218.0" - create_executable sort </dev/null if [ "\$1" == "--version-sort" ]; then @@ -223,7 +215,7 @@ OUT create_version "1.9.0" create_version "1.53.0" create_version "1.218.0" - create_executable sort < "${PYENV_ROOT}/version" <<<"3.4" - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" mkdir -p "$PYENV_TEST_DIR" cd "$PYENV_TEST_DIR" @@ -165,7 +153,7 @@ SH 2.7 3.4 EOF - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" mkdir -p "$PYENV_TEST_DIR" cd "$PYENV_TEST_DIR" @@ -175,7 +163,7 @@ EOF } @test "resolves pyenv-latest prefixes" { - create_executable "3.4.2" "python" + create_alt_executable_in_version "3.4.2" "python" PYENV_VERSION=3.4 run pyenv-which python assert_success "${PYENV_ROOT}/versions/3.4.2/bin/python" @@ -187,7 +175,7 @@ echo version=\$version exit ! - create_executable "3.4.2" "python" + create_alt_executable_in_version "3.4.2" "python" PYENV_VERSION=3.4 run pyenv-which python assert_success "version=3.4.2" @@ -195,9 +183,9 @@ exit @test "skip advice supresses error messages" { bats_require_minimum_version 1.5.0 - create_executable "2.7" "python" - create_executable "3.3" "py.test" - create_executable "3.4" "py.test" + create_alt_executable_in_version "2.7" "python" + create_alt_executable_in_version "3.3" "py.test" + create_alt_executable_in_version "3.4" "py.test" PYENV_VERSION=2.7 run -127 pyenv-which py.test --skip-advice assert_failure From 74188359086b17801d21900a166ac39d008da286 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Fri, 19 Dec 2025 00:28:32 +0300 Subject: [PATCH 2/3] Prevent infinite loop if a shim is linked and called from a nonstandard location with "system" active --- libexec/pyenv-exec | 8 ++++++ libexec/pyenv-rehash | 4 +++ libexec/pyenv-which | 24 ++++++++++------- test/exec.bats | 23 ++++++++++++++++ test/rehash.bats | 25 +++++++++++++++++ test/shims-linked-from-elsewhere.bats | 39 +++++++++++++++++++++++++++ test/test_helper.bash | 12 ++++++--- test/which.bats | 26 ++++++++++++++++++ 8 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 test/shims-linked-from-elsewhere.bats diff --git a/libexec/pyenv-exec b/libexec/pyenv-exec index 6ed5ac20..2368bdd3 100755 --- a/libexec/pyenv-exec +++ b/libexec/pyenv-exec @@ -29,6 +29,14 @@ if [ -z "$PYENV_COMMAND" ]; then exit 1 fi +if [[ -n $_PYENV_SHIM_PATH ]]; then + PROGRAM="$(echo "$PYENV_COMMAND" | tr a-z- A-Z_ | sed 's/[^A-Z0-9_]/_/g')" + NR_PYENV_SHIM_PATHS_PROGRAM="_PYENV_SHIM_PATHS_${PROGRAM}" + export -- "$NR_PYENV_SHIM_PATHS_PROGRAM=$_PYENV_SHIM_PATH${!NR_PYENV_SHIM_PATHS_PROGRAM:+:${!NR_PYENV_SHIM_PATHS_PROGRAM}}" + unset PROGRAM NR_PYENV_SHIM_PATHS_PROGRAM + unset _PYENV_SHIM_PATH +fi + PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")" PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}" export PYENV_VERSION diff --git a/libexec/pyenv-rehash b/libexec/pyenv-rehash index 745cfb8e..61000ff6 100755 --- a/libexec/pyenv-rehash +++ b/libexec/pyenv-rehash @@ -82,6 +82,10 @@ set -e program="\${0##*/}" export PYENV_ROOT="$PYENV_ROOT" +SHIM_PATH=\${0%/*} +if [[ \$SHIM_PATH != "$PYENV_ROOT/shims" ]]; then + export _PYENV_SHIM_PATH="\$SHIM_PATH" +fi exec "$(command -v pyenv)" exec "\$program" "\$@" SH chmod +x "$PROTOTYPE_SHIM_PATH" diff --git a/libexec/pyenv-which b/libexec/pyenv-which index 9ff3ddae..0ebf8219 100755 --- a/libexec/pyenv-which +++ b/libexec/pyenv-which @@ -42,15 +42,19 @@ done remove_from_path() { - local path_to_remove="$1" - local path_before + local -a paths_to_remove + IFS=: paths_to_remove=($1) + local path_to_remove path_before local result=":${PATH//\~/$HOME}:" - while [ "$path_before" != "$result" ]; do - path_before="$result" - result="${result//:$path_to_remove:/:}" + for path_to_remove in "${paths_to_remove[@]}"; do + while true; do + path_before="$result" + result="${result//:$path_to_remove:/:}" + if [[ ${#path_before} == "${#result}" ]]; then break; fi + done done - result="${result%:}" - echo "${result#:}" + result="${result:1:${#result}-2}" + echo "$result" } if [ -z "$PYENV_COMMAND" ]; then @@ -66,8 +70,10 @@ declare -a nonexistent_versions for version in "${versions[@]}" "$system"; do if [ "$version" = "system" ]; then - PATH="$(remove_from_path "${PYENV_ROOT}/shims")" - PYENV_COMMAND_PATH="$(command -v "$PYENV_COMMAND" || true)" + PROGRAM="$(echo "$PYENV_COMMAND" | tr a-z- A-Z_ | sed 's/[^A-Z0-9_]/_/g')" + NR_CUSTOM_SHIM_PATHS="_PYENV_SHIM_PATHS_$PROGRAM" + SEARCH_PATH="$(remove_from_path "${PYENV_ROOT}/shims${!NR_CUSTOM_SHIM_PATHS:+:${!NR_CUSTOM_SHIM_PATHS}}")" + PYENV_COMMAND_PATH="$(PATH="$SEARCH_PATH" command -v "$PYENV_COMMAND" || true)" else # $version may be a prefix to be resolved by pyenv-latest version_path="$(pyenv-prefix "${version}" 2>/dev/null)" || \ diff --git a/test/exec.bats b/test/exec.bats index c1db5fbc..ff49912b 100644 --- a/test/exec.bats +++ b/test/exec.bats @@ -116,3 +116,26 @@ OUT PYENV_VERSION=system:custom run pyenv-exec python3 assert_success "$PATH" } + +@test "sets/adds to _PYENV_SHIM_PATHS_{PROGRAM} when _PYENV_SHIM_PATH is set, unsets _PYENV_SHIM_PATH" { + progname='123;wacky-prog.name ^%$#' + envvarname="_PYENV_SHIM_PATHS_123_WACKY_PROG_NAME_____" + create_path_executable "$progname" </dev/null || true" assert_success assert [ -x "${PYENV_ROOT}/shims/python" ] } + +@test "shim sets _PYENV_SHIM_PATH when linked from elsewhere" { + export PYENV_VERSION="custom" + create_alt_executable python3 + #must stub pyenv before rehash 'cuz the path is hardcoded into shims + create_stub pyenv </dev/null | grep ^XDG_ | cut -d= -f1`; do unset "$xdg_var"; done unset xdg_var @@ -128,7 +128,7 @@ path_without() { if [ "$found" != "${PYENV_ROOT}/shims" ]; then alt="${PYENV_TEST_DIR}/$(echo "${found#/}" | tr '/' '-')" mkdir -p "$alt" - for util in bash head cut readlink greadlink; do + for util in bash head cut readlink greadlink tr sed; do if [ -x "${found}/$util" ]; then ln -s "${found}/$util" "${alt}/$util" fi @@ -156,9 +156,13 @@ create_alt_executable_in_version() { create_executable "${PYENV_ROOT}/versions/$version/bin" "$@" } +create_stub() { + create_executable "${BATS_TEST_TMPDIR}/stubs" "$@" +} + create_executable() { - bin="${1:?}" - name="${2:?}" + local bin="${1:?}" + local name="${2:?}" shift 2 mkdir -p "$bin" local payload diff --git a/test/which.bats b/test/which.bats index 75601b0c..e54c1704 100644 --- a/test/which.bats +++ b/test/which.bats @@ -193,3 +193,29 @@ exit pyenv: py.test: command not found OUT } + +@test "excludes paths in _PYENV_SHIM_PATHS_{PROGRAM} from search only for PROGRAM" { + progname='123;wacky-prog.name ^%$#' + envvarname="_PYENV_SHIM_PATHS_123_WACKY_PROG_NAME_____" + create_path_executable "$progname" + + for dir_ in "$BATS_TEST_TMPDIR/alt-path"{1,2}; do + mkdir -p "$dir_" + ln -s "${PYENV_TEST_DIR}/bin/$progname" "$dir_/$progname" + eval 'export '"$envvarname"'="$dir_${'"$envvarname"':+:$'"$envvarname"'}"' + PATH="$dir_:$PATH" + done + create_executable "$dir_" "normal_program" + + run pyenv-which "$progname" + assert_success + assert_output < Date: Sat, 20 Dec 2025 09:31:05 +0300 Subject: [PATCH 3/3] tests: refactor: migrate to newly-added create_stub() --- test/init.bats | 10 ++-------- test/latest.bats | 36 ++++++++++-------------------------- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/test/init.bats b/test/init.bats index a6845fe4..2b963249 100755 --- a/test/init.bats +++ b/test/init.bats @@ -2,10 +2,6 @@ load test_helper -_setup() { - export PATH="${PYENV_TEST_DIR}/bin:$PATH" -} - @test "creates shims and versions directories" { assert [ ! -d "${PYENV_ROOT}/shims" ] assert [ ! -d "${PYENV_ROOT}/versions" ] @@ -171,16 +167,14 @@ echo "\$PATH" } @test "outputs sh-compatible case syntax" { - create_path_executable pyenv-commands <