diff --git a/tests/fm-checkout-return-boundary.test.sh b/tests/fm-checkout-return-boundary.test.sh index c83eeb2f74f..b12611fc9b7 100755 --- a/tests/fm-checkout-return-boundary.test.sh +++ b/tests/fm-checkout-return-boundary.test.sh @@ -2,10 +2,13 @@ # Tests for the destructive Treehouse-return boundary walk in # bin/fm-checkout-lock-lib.sh (fm_checkout_treehouse_return_locked). # -# The walk exists to prove that `treehouse return --force` cannot ESCAPE the -# worktree it is pointed at. It must refuse a redirected target - a symlinked -# path component, a non-directory, a mount point, a cross-device child - and it -# must NOT refuse a tree merely because ordinary symlinks live inside it. +# The walk validates that `treehouse return --force` starts from an acceptable +# worktree boundary. It must refuse a redirected target - a symlinked path +# component, a non-directory, a mount point, a cross-device child - and it must +# NOT refuse a tree merely because ordinary symlinks live inside it. The root +# descriptor remains open across exec so the return stays bound to that root. +# Descendant validation is point-in-time: descendant descriptors are released +# after their subtrees are checked instead of being inherited by Treehouse. # # That distinction is the regression this file pins. The walk originally # refused on ANY symlink entry anywhere in the tree, which made every repo @@ -67,11 +70,14 @@ make_case() { # -> prints " " printf '%s %s %s' "$case_dir" "$project" "$worktree" } -run_boundary() { # [cd-dir] -> exit status - local project=$1 target=$2 cd_dir=${3:-$1} fakebin status +run_boundary() { # [cd-dir] [fd-limit] -> exit status + local project=$1 target=$2 cd_dir=${3:-$1} fd_limit=${4:-} fakebin status fakebin=$(fm_fakebin "$(dirname "$project")/fake") fm_fake_exit0 "$fakebin" treehouse ( + if [ -n "$fd_limit" ]; then + ulimit -n "$fd_limit" || exit 71 + fi cd "$cd_dir" || exit 70 PATH="$fakebin:$PATH" python3 "$BOUNDARY_PY" "$target" "$project" ) >/dev/null 2>&1 @@ -171,20 +177,51 @@ EOF pass "a project/cwd mismatch is refused" } -test_wide_tree_does_not_exhaust_descriptors() { - local rec case_dir project worktree status directory - rec=$(make_case wide-tree-low-fd) +test_realistically_large_and_deep_tree_respects_fd_limit() { + local rec case_dir project worktree fakebin status + rec=$(make_case realistically-large-tree) read -r case_dir project worktree < "$fakebin/treehouse" <<'SH' +#!/bin/sh +root_descriptor=${FM_TREEHOUSE_RETURN_ROOT_FD:-} +[ -n "$root_descriptor" ] || exit 91 +[ "$FM_TREEHOUSE_RETURN_BOUNDARY_FDS" = "$root_descriptor" ] || exit 92 +[ -d "/dev/fd/$root_descriptor" ] || exit 93 +for candidate in /dev/fd/*; do + [ -d "$candidate" ] || continue + [ "${candidate##*/}" = "$root_descriptor" ] || exit 94 +done +exit 0 +SH + chmod +x "$fakebin/treehouse" + ( + ulimit -n 128 || exit 71 + cd "$project" || exit 70 + PATH="$fakebin:$PATH" python3 "$BOUNDARY_PY" "$worktree" "$project" + ) >/dev/null 2>&1 + status=$? + expect_code 0 "$status" "a worktree with more than 42,000 directories and depth beyond the descriptor limit must return" + pass "a real greater-than-42,000-directory fixture keeps only its root descriptor across exec" } extract_boundary_program @@ -193,4 +230,4 @@ test_symlink_escaping_the_tree_is_still_not_followed test_symlinked_ancestor_is_refused test_non_directory_target_is_refused test_project_cwd_mismatch_is_refused -test_wide_tree_does_not_exhaust_descriptors +test_realistically_large_and_deep_tree_respects_fd_limit