refactor: 迁移至 bashly CLI 与功能域模块结构

- CLI 定义迁移至 src/bashly.yml(bashly 1.4 生成参数解析),入口收敛为 src/root_command.sh
- 源码按功能域重组:config/storage/integrate/media/pipeline,消除扁平散落
- 新增零依赖测试框架 tests/(123 用例)与 CI 流水线(lint + test + build + 产物一致性)
- build.sh 支持版本单一来源注入与 --check 产物校验;新增 lint.sh(bash -n + shellcheck 零容忍)
- 删除旧单文件 media_organizer.sh 与旧扁平模块
This commit is contained in:
Shuery committed 2026-08-14 22:41:28 +08:00
1 parent d978a13a83
commit 0df07ad0ae
48 files changed
+5813 -6812

No files matched your search

Executable
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# ============================================================================
# 单元测试运行器(零依赖,纯 bash)
#
# 用法:
# ./tests/run.sh 运行全部用例
# ./tests/run.sh cache 仅运行匹配名字的用例文件(如 cache、filename)
# ./tests/run.sh -v 显示每个用例名称(默认只显示失败与汇总)
#
# 设计:
# - 按 src/modules.list 顺序加载全部模块(跳过 lib/entry.sh,避免触发 main)
# - 每个用例文件在独立子 shell 中运行:全局状态自动隔离、互不污染
# - 用例文件可定义可选 setup()(每个测试前调用)与 teardown()(测试后调用)
# - 断言使用 tests/lib/assert.sh,失败返回非 0,不影响其余用例继续执行
# ============================================================================
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERBOSE=false
FILTER=""
for a in "$@"; do
case "$a" in
-v) VERBOSE=true ;;
*) FILTER="$a" ;;
esac
done
# 加载全部模块(bashly 源码树):
# - src/lib/main.sh 常量与全局变量最先
# - src/lib/*.sh 纯函数定义库
# - 跳过 src/root_command.sh(bashly 命令文件为顶层代码,仅由 bashly 包装执行)
# 注意:必须在顶层加载(不能包在函数里)——模块中的 declare -A 在函数内
# 会变成函数局部变量,函数返回后关联数组全部丢失。
# shellcheck source=/dev/null
source "$ROOT/src/lib/main.sh"
while IFS= read -r -d '' m; do
# main.sh 已加载(readonly 常量不能重复 source)
[[ "$(basename "$m")" == "main.sh" ]] && continue
# shellcheck source=/dev/null
source "$m"
done < <(find "$ROOT/src/lib" -name '*.sh' -print0 | sort -z)
# 断言库
# shellcheck source=/dev/null
source "$ROOT/tests/lib/assert.sh"
# 测试环境约定:_log 仅作日志输出(不参与业务逻辑),覆盖为 no-op 保持测试输出干净;
# 如需断言日志行为,可在具体用例中临时重新定义。
_log() { return 0; }
pass=0
fail=0
failed_names=()
run_case_file() {
local case_file="$1"
local case_name
case_name="$(basename "$case_file" .sh)"
# 子 shell:全局状态隔离(每个用例文件一组干净状态)
(
set +e
# shellcheck source=/dev/null
source "$case_file"
local fn
for fn in $(compgen -A function | grep '^test_' | sort); do
if declare -F setup >/dev/null; then setup; fi
# 注意:不能在 if 条件中运行 ( set -e; "$fn" )——bash 的条件上下文会
# 抑制子 shell 内的 errexit(多断言测试会漏报)。先执行再取退出码。
# stdout 吞掉(_log 等噪声),stderr 透传(断言失败诊断可见)
(
set -e
"$fn"
) >/dev/null
rc=$?
if ((rc == 0)); then
printf 'PASS\t%s\t%s\n' "$case_name" "$fn"
else
printf 'FAIL\t%s\t%s\n' "$case_name" "$fn"
fi
if declare -F teardown >/dev/null; then teardown; fi
done
)
}
while IFS= read -r -d '' case_file; do
if [[ -n "$FILTER" ]] && [[ "$(basename "$case_file" .sh)" != *"$FILTER"* ]]; then
continue
fi
[[ $VERBOSE == true ]] && echo "── $case_file"
while IFS=$'\t' read -r kind case_name fn; do
if [[ "$kind" == "PASS" ]]; then
pass=$((pass + 1))
[[ $VERBOSE == true ]] && echo " ✅ $case_name/$fn"
elif [[ "$kind" == "FAIL" ]]; then
fail=$((fail + 1))
failed_names+=("$case_name/$fn")
echo " ❌ $case_name/$fn"
fi
done < <(run_case_file "$case_file")
done < <(find "$ROOT/tests/cases" -name '*.sh' -print0 | sort -z)
echo ""
echo "════════════════════════════════════"
echo "结果:$pass 通过,$fail 失败"
if ((fail > 0)); then
printf '失败用例:\n'
for n in "${failed_names[@]}"; do
echo " - $n"
done
exit 1
fi