From 7a32f26f4e05069e50d1ec72702d8012a169e4c2 Mon Sep 17 00:00:00 2001 From: LucaLin <78164141+LucaLin233@users.noreply.github.com> Date: Wed, 9 Sep 2026 02:25:23 +0800 Subject: [PATCH 1/2] fix(setup): account for selected preparation and dependency failures --- linux_setup.sh | 82 +++++++++++++++++++++++++-------------- tests/test-linux-setup.sh | 79 +++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 30 deletions(-) diff --git a/linux_setup.sh b/linux_setup.sh index 26d7e50..9acac76 100755 --- a/linux_setup.sh +++ b/linux_setup.sh @@ -42,6 +42,7 @@ TOTAL_START_TIME=0 SELECTED_MODULES=() +declare -A MODULE_PREPARATION_ERROR declare -A MODULE_STATUS declare -A MODULE_EXEC_TIME @@ -684,6 +685,7 @@ resolve_dependencies() { visit_state["$current_module"]="visiting" for dependency in ${MODULE_DEPS[$current_module]:-}; do + [[ -n "${MODULES[$dependency]:-}" ]] || continue collect_dependencies "$dependency" || return 1 done @@ -862,37 +864,26 @@ register_module() { MODULE_FILES["$module"]="$module_file" } -remove_module_registration() { - local module="$1" - - unset 'MODULES[$module]' - unset 'MODULE_DEPS[$module]' - unset 'MODULE_ORDER_VALUE[$module]' - unset 'MODULE_FILES[$module]' -} - validate_module_dependencies() { - local changed=true - local module - local dependency - - # 重复检查,确保依赖于已被剔除模块的模块也会被剔除。 - while [[ "$changed" == "true" ]]; do - changed=false - - for module in "${!MODULES[@]}"; do - for dependency in ${MODULE_DEPS[$module]:-}; do - if [[ -z "${MODULES[$dependency]:-}" ]]; then - log "模块依赖不存在,已跳过:$module -> $dependency" "error" - remove_module_registration "$module" - changed=true - break - fi - done + local module dependency + for module in "${!MODULES[@]}"; do + for dependency in ${MODULE_DEPS[$module]:-}; do + if [[ -z "${MODULES[$dependency]:-}" ]]; then + MODULE_PREPARATION_ERROR["$module"]="依赖不存在或已禁用:$dependency" + log "$module:${MODULE_PREPARATION_ERROR[$module]}" "warn" + fi done done } +register_unavailable_module() { + local module="$1" reason="$2" + MODULES["$module"]="$module(准备失败)" + MODULE_ORDER_VALUE["$module"]=900 + MODULE_DEPS["$module"]="" + MODULE_PREPARATION_ERROR["$module"]="$reason" +} + build_module_order() { local module @@ -965,7 +956,8 @@ discover_and_prepare_modules() { module_file="$TEMP_DIR/$file_name" if ! download_module "$module"; then - log "模块下载失败,已跳过:$module" "error" + register_unavailable_module "$module" "下载或完整性验证失败" + log "模块准备失败,选择后将计入失败:$module" "error" continue fi @@ -976,6 +968,7 @@ discover_and_prepare_modules() { fi if (( register_result != 2 )); then + register_unavailable_module "$module" "语法或元数据验证失败" rm -f "$module_file" fi done @@ -1240,6 +1233,35 @@ self_update() { # 模块执行 # ============================================================================= +execute_selected_module() { + local module="$1" dependency + case "${MODULE_STATUS[$module]:-}" in + success|degraded) return 0 ;; + failed) return 1 ;; + running) + MODULE_STATUS["$module"]=failed + log "模块循环依赖:$module" "error" + return 1 ;; + esac + if [[ -n "${MODULE_PREPARATION_ERROR[$module]:-}" ]]; then + MODULE_STATUS["$module"]=failed + log "模块 $module 准备失败:${MODULE_PREPARATION_ERROR[$module]}" "error" + return 1 + fi + MODULE_STATUS["$module"]=running + for dependency in ${MODULE_DEPS[$module]:-}; do + # An explicitly omitted dependency retains the existing user-confirmed + # external-dependency contract; only selected modules are scheduled here. + module_is_selected "$dependency" || continue + if ! execute_selected_module "$dependency"; then + MODULE_STATUS["$module"]=failed + log "模块 $module 被依赖失败阻塞:$dependency" "error" + return 1 + fi + done + execute_module "$module" +} + execute_module() { local module="$1" local module_file="${MODULE_FILES[$module]:-}" @@ -1248,8 +1270,8 @@ execute_module() { local duration local result - if [[ ! -f "$module_file" ]]; then - log "模块文件不存在:$module" "error" + if ! validate_bash_script "$module_file"; then + log "模块文件缺失或执行前语法验证失败:$module" "error" MODULE_STATUS["$module"]="failed" return 1 fi @@ -1745,7 +1767,7 @@ main() { echo echo "[$current/$total] ${MODULES[$module]}" - if ! execute_module "$module"; then + if ! execute_selected_module "$module"; then log "模块失败,但继续执行后续模块:$module" "warn" fi done diff --git a/tests/test-linux-setup.sh b/tests/test-linux-setup.sh index 1c21489..ab626ea 100755 --- a/tests/test-linux-setup.sh +++ b/tests/test-linux-setup.sh @@ -255,3 +255,82 @@ grep -Fq 'Linux/${repository_ref}/p10k-config.zsh' "$ROOT_DIR/modules/zsh-setup. pass "pin repository module resource" printf 'All linux_setup integrity tests passed.\n' + +# Project 4: preparation failures count only when selected; dependencies run first. +( + MODULES=(); MODULE_DEPS=(); MODULE_FILES=(); MODULE_ORDER_VALUE=() + MODULE_STATUS=(); MODULE_PREPARATION_ERROR=() + register_unavailable_module unavailable 'download or validation failed' + [[ -n ${MODULES[unavailable]} && -z ${MODULE_STATUS[unavailable]:-} ]] + SELECTED_MODULES=(unavailable) + assert_fail 'selected preparation failure returns nonzero' execute_selected_module unavailable + assert_fail 'selected preparation failure reaches final status' finish_deployment + MODULE_STATUS=(); SELECTED_MODULES=(ok) + export MODULE_CAPTURE="$TEST_DIR/module-order" + : > "$MODULE_CAPTURE" + for module in ok dependency dependent transitive degraded; do + MODULES[$module]=$module + MODULE_FILES[$module]="$TEST_DIR/status-$module.sh" + printf '#!/usr/bin/env bash\nprintf "%%s\\n" "%s" >> "$MODULE_CAPTURE"\nexit 0\n' "$module" > "${MODULE_FILES[$module]}" + done + assert_ok 'unselected preparation failure does not taint success' execute_selected_module ok + assert_ok 'unselected preparation failure stays nonfatal' finish_deployment + MODULE_STATUS=(); : > "$MODULE_CAPTURE" + printf '#!/usr/bin/env bash\nexit 9\n' > "${MODULE_FILES[dependency]}" + MODULE_DEPS[dependent]=dependency; MODULE_DEPS[transitive]=dependent + SELECTED_MODULES=(transitive dependent dependency ok) + for module in "${SELECTED_MODULES[@]}"; do execute_selected_module "$module" || true; done + [[ ${MODULE_STATUS[dependency]} == failed && ${MODULE_STATUS[dependent]} == failed && ${MODULE_STATUS[transitive]} == failed ]] + assert_file_order=$(cat "$MODULE_CAPTURE") + assert_eq ok "$assert_file_order" 'dependency failure blocks transitive consumers but not unrelated module' + assert_fail 'dependency failures reach final status' finish_deployment + MODULE_STATUS=(); : > "$MODULE_CAPTURE" + printf '#!/usr/bin/env bash\nprintf "dependency\\n" >> "$MODULE_CAPTURE"\nexit 0\n' > "${MODULE_FILES[dependency]}" + for module in "${SELECTED_MODULES[@]}"; do execute_selected_module "$module" || true; done + assert_eq $'dependency\ndependent\ntransitive\nok' "$(cat "$MODULE_CAPTURE")" 'dependency order overrides numeric selection and avoids duplicate execution' + MODULE_STATUS=(); SELECTED_MODULES=(degraded) + printf '#!/usr/bin/env bash\nexit 2\n' > "${MODULE_FILES[degraded]}" + assert_ok 'valid degraded status remains nonfatal' execute_selected_module degraded + assert_ok 'degraded final status remains zero' finish_deployment + MODULE_STATUS=(); SELECTED_MODULES=(ok) + printf '#!/usr/bin/env bash\nif then\n' > "${MODULE_FILES[ok]}" + assert_fail 'syntax failure cannot masquerade as degraded exit 2' execute_selected_module ok + assert_eq failed "${MODULE_STATUS[ok]}" 'invalid syntax recorded as failed' + MODULE_STATUS=(); MODULES[disabled]=''; MODULE_DEPS[dependent]=disabled + validate_module_dependencies + SELECTED_MODULES=(dependent) + assert_fail 'unavailable dependency fails selected consumer' execute_selected_module dependent +) + +# Real discovery with inert curl/download boundaries: no production network. +( + TEMP_DIR="$TEST_DIR/discovery-status"; mkdir "$TEMP_DIR" + MODULES=(); MODULE_DEPS=(); MODULE_FILES=(); MODULE_ORDER_VALUE=() + MODULE_STATUS=(); MODULE_PREPARATION_ERROR=() + curl() { + local output='' + while (( $# )); do + if [[ $1 == -o ]]; then output=$2; shift; fi + shift + done + printf '[{"type":"file","name":"broken.sh"},{"type":"file","name":"disabled.sh"},{"type":"file","name":"valid.sh"},{"type":"file","name":"badsyntax.sh"}]' > "$output" + } + download_module() { + [[ $1 != broken ]] || return 1 + case $1 in + disabled) printf '#!/usr/bin/env bash\n# linux-setup:enabled=false\nexit 0\n' > "$TEMP_DIR/$1.sh" ;; + badsyntax) printf '#!/usr/bin/env bash\nif then\n' > "$TEMP_DIR/$1.sh" ;; + *) printf '#!/usr/bin/env bash\nexit 0\n' > "$TEMP_DIR/$1.sh" ;; + esac + } + assert_ok 'discovery retains failed candidates and valid modules' discover_and_prepare_modules + [[ -n ${MODULES[broken]:-} && -n ${MODULES[badsyntax]:-} && -z ${MODULES[disabled]:-} ]] + SELECTED_MODULES=(valid) + assert_ok 'valid-only selection ignores failed and disabled discoveries' execute_selected_module valid + assert_ok 'valid-only deployment succeeds' finish_deployment + SELECTED_MODULES=(broken badsyntax valid) + for module in "${SELECTED_MODULES[@]}"; do execute_selected_module "$module" || true; done + [[ ${MODULE_STATUS[broken]} == failed && ${MODULE_STATUS[badsyntax]} == failed && ${MODULE_STATUS[valid]} == success ]] + assert_fail 'selected discovery failures produce nonzero deployment' finish_deployment +) +printf 'All module failure-status tests passed.\n' From f26942096533528c00d9be418588be0ee60470e3 Mon Sep 17 00:00:00 2001 From: LucaLin <78164141+LucaLin233@users.noreply.github.com> Date: Wed, 9 Sep 2026 02:30:13 +0800 Subject: [PATCH 2/2] test(setup): verify real main failure aggregation and continuation --- tests/test-linux-setup.sh | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test-linux-setup.sh b/tests/test-linux-setup.sh index ab626ea..f23ceec 100755 --- a/tests/test-linux-setup.sh +++ b/tests/test-linux-setup.sh @@ -334,3 +334,40 @@ printf 'All linux_setup integrity tests passed.\n' assert_fail 'selected discovery failures produce nonzero deployment' finish_deployment ) printf 'All module failure-status tests passed.\n' + +# Exercise the real main scheduler and final exit, stubbing only system/network/UI. +for scenario in mixed valid; do + main_root="$TEST_DIR/main-status-$scenario"; mkdir "$main_root" + rc=0 + env ROOT_DIR="$ROOT_DIR" MAIN_ROOT="$main_root" SCENARIO="$scenario" \ + timeout --signal=TERM --kill-after=1s 10s bash -c ' + source "$ROOT_DIR/linux_setup.sh" + init_logging() { :; }; create_temp_dir() { TEMP_DIR=""; } + self_update() { :; }; pre_check() { :; }; clear() { :; } + migrate_legacy_apt_source_backups() { :; }; install_dependencies() { :; } + system_update() { :; }; fix_hosts_file() { :; } + discover_and_prepare_modules() { + register_unavailable_module broken "download failed" + MODULES[consumer]=consumer; MODULE_DEPS[consumer]=broken; MODULE_ORDER_VALUE[consumer]=1 + MODULES[good]=good; MODULE_ORDER_VALUE[good]=2 + MODULE_FILES[good]="$MAIN_ROOT/good.sh" + printf "#!/usr/bin/env bash\nprintf good > \"%s/executed\"\n" "$MAIN_ROOT" > "${MODULE_FILES[good]}" + build_module_order + } + select_deployment_mode() { + if [[ "$SCENARIO" == mixed ]]; then SELECTED_MODULES=(consumer good); else SELECTED_MODULES=(good); fi + } + generate_summary() { declare -p MODULE_STATUS > "$MAIN_ROOT/status"; } + show_recommendations() { :; } + main <<< $'\''y\ny\n'\'' + ' > "$main_root/output" 2>&1 || rc=$? + expected=0; [[ $scenario != mixed ]] || expected=1 + assert_eq "$expected" "$rc" "real main $scenario returns exact deployment status" + [[ -f "$main_root/executed" ]] + if [[ $scenario == mixed ]]; then + grep -Fq '[consumer]="failed"' "$main_root/status" + grep -Fq '[broken]="failed"' "$main_root/status" + else + ! grep -Fq '[broken]=' "$main_root/status" + fi +done