Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 52 additions & 30 deletions linux_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ TOTAL_START_TIME=0

SELECTED_MODULES=()

declare -A MODULE_PREPARATION_ERROR
declare -A MODULE_STATUS
declare -A MODULE_EXEC_TIME

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -976,6 +968,7 @@ discover_and_prepare_modules() {
fi

if (( register_result != 2 )); then
register_unavailable_module "$module" "语法或元数据验证失败"
rm -f "$module_file"
fi
done
Expand Down Expand Up @@ -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]:-}"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions tests/test-linux-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,119 @@ 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'

# 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