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:
1 parent
d978a13a83
commit
0df07ad0ae
48 files changed
+5813
-6812
No files matched your search
@@ -0,0 +1,207 @@
|
||||
# ============================================================================
|
||||
# 识别搜索链测试(src/lib/media/identify.sh)
|
||||
#
|
||||
# 覆盖 R1 搜索链抽象后的行为保持:统一尝试器(tv_search_once/movie_search_once)
|
||||
# 的命中/无结果/请求失败返回码、两行协议回传、识别函数的层级短路语义
|
||||
# (主搜索失败即 return 2,后续层失败忽略)、目录/MAL/别名/后缀剥离兜底。
|
||||
#
|
||||
# mock 策略:按 "endpoint|语言" 查 MOCK_RESPONSES 表;无表项 = 空哨兵(无结果);
|
||||
# MOCK_FAIL_ENDPOINTS 匹配 → 请求失败(rc=2)。tmdb_api_lang 经动态作用域
|
||||
# 透传 TMDB_LANG,mock 用它区分 zh/en 响应。
|
||||
# ============================================================================
|
||||
|
||||
# mock 响应表(文件级全局:declare 在函数内是函数局部,mock 函数访问不到)
|
||||
declare -A MOCK_RESPONSES=()
|
||||
declare -A MOCK_FAIL_ENDPOINTS=()
|
||||
|
||||
setup() {
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
DRY_RUN=false
|
||||
TMDB_DELAY=0
|
||||
TMDB_LANG="zh-CN"
|
||||
SOURCE_DIR="$TEST_DIR/src"
|
||||
FOLDER_SHOWS="节目"
|
||||
FOLDER_MOVIES="电影"
|
||||
mkdir -p "$SOURCE_DIR"
|
||||
MOCK_RESPONSES=()
|
||||
MOCK_FAIL_ENDPOINTS=()
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
# mock:按 "endpoint|lang" 查表;无表项 → 无结果(rc=1);失败表命中 → rc=2
|
||||
tmdb_api() {
|
||||
local endpoint="$1"
|
||||
shift
|
||||
local key="${endpoint}|${TMDB_LANG:-}"
|
||||
if [[ -n "${MOCK_FAIL_ENDPOINTS[$endpoint]:-}" ]]; then
|
||||
return 2
|
||||
fi
|
||||
local resp="${MOCK_RESPONSES[$key]:-}"
|
||||
[[ -z "$resp" ]] && return 1
|
||||
echo "$resp"
|
||||
}
|
||||
|
||||
# mock:用户规则别名查找(真实函数读规则文件,测试桩直接返回)
|
||||
match_rule_lookup_alias() {
|
||||
echo ""
|
||||
}
|
||||
|
||||
# --- 统一尝试器 ---
|
||||
|
||||
# tv_search_once:zh 命中 → 输出 id,返回 0
|
||||
test_tv_search_once_hit() {
|
||||
MOCK_RESPONSES["/search/tv|zh-CN"]='{"results":[{"id":100,"name":"Show A"}]}'
|
||||
local id="" rc=0
|
||||
id=$(tv_search_once "Show A" "1" "" "") || rc=$?
|
||||
assert_eq "0" "$rc" "tv_search_once 命中返回码"
|
||||
assert_eq "100" "$id" "tv_search_once 命中 id"
|
||||
}
|
||||
|
||||
# tv_search_once:无结果 → 返回 1,无输出
|
||||
test_tv_search_once_miss() {
|
||||
local id="" rc=0
|
||||
id=$(tv_search_once "No Such Show" "1" "" "") || rc=$?
|
||||
assert_eq "1" "$rc" "tv_search_once 无结果返回码"
|
||||
assert_empty "$id" "tv_search_once 无结果输出"
|
||||
}
|
||||
|
||||
# tv_search_once:请求失败 → 返回 2(不重试语义由调用方处理)
|
||||
test_tv_search_once_request_failed() {
|
||||
MOCK_FAIL_ENDPOINTS["/search/tv"]=1
|
||||
local rc=0
|
||||
tv_search_once "Show A" "1" "" "" >/dev/null || rc=$?
|
||||
assert_eq "2" "$rc" "tv_search_once 请求失败返回码"
|
||||
}
|
||||
|
||||
# tv_search_once:zh 空 → en-US 命中(语言分支走 tmdb_api_lang)
|
||||
test_tv_search_once_en_fallback() {
|
||||
MOCK_RESPONSES["/search/tv|en-US"]='{"results":[{"id":200,"name":"Show A"}]}'
|
||||
local id="" rc=0
|
||||
id=$(tv_search_once "Show A" "1" "" "en-US") || rc=$?
|
||||
assert_eq "0" "$rc" "en 命中返回码"
|
||||
assert_eq "200" "$id" "en 命中 id"
|
||||
}
|
||||
|
||||
# movie_search_once:两行协议(id + 单行响应 JSON)
|
||||
test_movie_search_once_two_line_protocol() {
|
||||
MOCK_RESPONSES["/search/movie|zh-CN"]='{"results":[{"id":50,"title":"Movie X","release_date":"2020-01-01"}]}'
|
||||
local out="" rc=0
|
||||
out=$(movie_search_once "Movie X" "2020" "") || rc=$?
|
||||
assert_eq "0" "$rc" "movie_search_once 命中返回码"
|
||||
local id resp
|
||||
id=$(printf '%s\n' "$out" | sed -n '1p')
|
||||
resp=$(printf '%s\n' "$out" | sed -n '2p')
|
||||
assert_eq "50" "$id" "两行协议 id 行"
|
||||
assert_contains "$resp" '"title":"Movie X"' "两行协议响应行"
|
||||
}
|
||||
|
||||
# --- 目录兜底 ---
|
||||
|
||||
# tv_search_by_dir:源根直属散放不兜底(无输出)
|
||||
test_tv_search_by_dir_source_root_skipped() {
|
||||
local id
|
||||
id=$(tv_search_by_dir "$SOURCE_DIR/Show.mkv" "Show" "1" "")
|
||||
assert_empty "$id" "源根散放不兜底"
|
||||
}
|
||||
|
||||
# tv_search_by_dir:目录名命中(zh → en)
|
||||
test_tv_search_by_dir_hit() {
|
||||
mkdir -p "$TEST_DIR/src/[Group] Show X"
|
||||
local f="$TEST_DIR/src/[Group] Show X/01.mkv"
|
||||
MOCK_RESPONSES["/search/tv|zh-CN"]='{"results":[{"id":300,"name":"Show X"}]}'
|
||||
local id
|
||||
id=$(tv_search_by_dir "$f" "01" "1" "")
|
||||
assert_eq "300" "$id" "目录名兜底命中"
|
||||
}
|
||||
|
||||
# --- 完整识别链 ---
|
||||
|
||||
# identify_tv_show:zh 命中 → build_tv_dest 输出目标
|
||||
test_identify_tv_show_zh_hit() {
|
||||
MOCK_RESPONSES["/search/tv|zh-CN"]='{"results":[{"id":100,"name":"Show A"}]}'
|
||||
MOCK_RESPONSES["/tv/100|zh-CN"]='{"name":"Show A","first_air_date":"2020-01-01","number_of_seasons":2,"seasons":[{"season_number":1},{"season_number":2}]}'
|
||||
MOCK_RESPONSES["/tv/100/season/1|zh-CN"]='{"episodes":[{"episode_number":1,"name":"Pilot"},{"episode_number":2,"name":"Two"}]}'
|
||||
local dest="" rc=0
|
||||
dest=$(identify_tv_show "Show A" "1" "1" "" "Show A S01E01" "mkv" "" "$TEST_DIR/src/Show A/Show A S01E01.mkv") || rc=$?
|
||||
assert_eq "0" "$rc" "zh 命中识别返回码"
|
||||
assert_contains "$dest" "节目/Show A (2020)/Season 01|S01E01 - Pilot.mkv" "zh 命中目标"
|
||||
}
|
||||
|
||||
# identify_tv_show:zh 空 → en 命中
|
||||
test_identify_tv_show_zh_miss_en_hit() {
|
||||
MOCK_RESPONSES["/search/tv|en-US"]='{"results":[{"id":200,"name":"Show B"}]}'
|
||||
MOCK_RESPONSES["/tv/200|zh-CN"]='{"name":"Show B","first_air_date":"2021-01-01","number_of_seasons":1,"seasons":[{"season_number":1}]}'
|
||||
MOCK_RESPONSES["/tv/200/season/1|zh-CN"]='{"episodes":[{"episode_number":1,"name":"First"}]}'
|
||||
local dest="" rc=0
|
||||
dest=$(identify_tv_show "Show B" "1" "1" "" "Show B S01E01" "mkv" "" "$TEST_DIR/src/Show B/Show B S01E01.mkv") || rc=$?
|
||||
assert_eq "0" "$rc" "en 命中识别返回码"
|
||||
assert_contains "$dest" "Show B (2021)" "en 命中目标"
|
||||
}
|
||||
|
||||
# identify_tv_show:主搜索(zh)请求失败 → 短路 return 2(不试后续层)
|
||||
test_identify_tv_show_zh_fail_shortcircuit() {
|
||||
MOCK_FAIL_ENDPOINTS["/search/tv"]=1
|
||||
MOCK_RESPONSES["/search/tv|en-US"]='{"results":[{"id":200,"name":"Show B"}]}'
|
||||
local rc=0
|
||||
identify_tv_show "Show B" "1" "1" "" "Show B S01E01" "mkv" "" "$TEST_DIR/src/Show B/Show B S01E01.mkv" >/dev/null 2>&1 || rc=$?
|
||||
assert_eq "2" "$rc" "主搜索失败短路返回码"
|
||||
}
|
||||
|
||||
# identify_tv_show:zh/en 空 → 后缀二次剥离重搜命中("Railgun S" → "Railgun")
|
||||
test_identify_tv_show_suffix_strip() {
|
||||
MOCK_RESPONSES["/search/tv|zh-CN"]='{"results":[{"id":400,"name":"Railgun"}]}'
|
||||
MOCK_RESPONSES["/tv/400|zh-CN"]='{"name":"Railgun","first_air_date":"2020-01-01","number_of_seasons":3,"seasons":[{"season_number":1},{"season_number":2},{"season_number":3}]}'
|
||||
MOCK_RESPONSES["/tv/400/season/1|zh-CN"]='{"episodes":[{"episode_number":1,"name":"First"}]}'
|
||||
# 注意:首次搜索 "Railgun S" 无结果(mock 表无 "Railgun S" 条目),二次剥离后 "Railgun" 命中
|
||||
local dest="" rc=0
|
||||
dest=$(identify_tv_show "Railgun S" "1" "1" "" "Railgun S S01E01" "mkv" "" "$TEST_DIR/src/Railgun S/Railgun S S01E01.mkv") || rc=$?
|
||||
assert_eq "0" "$rc" "后缀剥离识别返回码"
|
||||
assert_contains "$dest" "Railgun (2020)" "后缀剥离命中目标"
|
||||
}
|
||||
|
||||
# identify_tv_show:全链失败 → return 1(无结果)
|
||||
test_identify_tv_show_all_miss() {
|
||||
local rc=0
|
||||
identify_tv_show "No Such Show" "1" "1" "" "No Such Show S01E01" "mkv" "" "$TEST_DIR/src/No Such Show/No Such Show S01E01.mkv" >/dev/null 2>&1 || rc=$?
|
||||
assert_eq "1" "$rc" "全链无结果返回码"
|
||||
}
|
||||
|
||||
# identify_movie:zh 命中 → build_movie_dest 输出目标
|
||||
test_identify_movie_zh_hit() {
|
||||
MOCK_RESPONSES["/search/movie|zh-CN"]='{"results":[{"id":50,"title":"Movie X","release_date":"2020-01-01"}]}'
|
||||
MOCK_RESPONSES["/movie/50|zh-CN"]='{"title":"Movie X","release_date":"2020-01-01"}'
|
||||
local dest="" rc=0
|
||||
dest=$(identify_movie "Movie X" "mkv" "$TEST_DIR/src/Movie X (2020).mkv" "Movie X" "2020") || rc=$?
|
||||
assert_eq "0" "$rc" "movie zh 命中返回码"
|
||||
assert_contains "$dest" "电影/Movie X (2020)|Movie X (2020).mkv" "movie zh 命中目标"
|
||||
}
|
||||
|
||||
# identify_movie:年份不符且无一致候选 → 需甄别(emit MATCH,返回 3)
|
||||
test_identify_movie_year_mismatch_match() {
|
||||
MOCK_RESPONSES["/search/movie|zh-CN"]='{"results":[{"id":50,"title":"Movie X","release_date":"2019-05-01"}]}'
|
||||
local rc=0
|
||||
identify_movie "Movie X" "mkv" "$TEST_DIR/src/Movie X (2020).mkv" "Movie X" "2020" >/dev/null 2>&1 || rc=$?
|
||||
assert_eq "3" "$rc" "年份不符需甄别返回码"
|
||||
assert_success test -n "${PENDING_AI_MATCH["$TEST_DIR/src/Movie X (2020).mkv"]:-}"
|
||||
}
|
||||
|
||||
# identify_movie:主搜索失败 → 短路 return 2
|
||||
test_identify_movie_zh_fail_shortcircuit() {
|
||||
MOCK_FAIL_ENDPOINTS["/search/movie"]=1
|
||||
local rc=0
|
||||
identify_movie "Movie X" "mkv" "$TEST_DIR/src/Movie X (2020).mkv" "Movie X" "2020" >/dev/null 2>&1 || rc=$?
|
||||
assert_eq "2" "$rc" "movie 主搜索失败短路返回码"
|
||||
}
|
||||
|
||||
# identify_movie:zh 空 → en 命中(两行协议回传年份校验通过)
|
||||
test_identify_movie_en_fallback() {
|
||||
MOCK_RESPONSES["/search/movie|en-US"]='{"results":[{"id":60,"title":"Movie Y","release_date":"2021-01-01"}]}'
|
||||
MOCK_RESPONSES["/movie/60|zh-CN"]='{"title":"Movie Y","release_date":"2021-01-01"}'
|
||||
local dest="" rc=0
|
||||
dest=$(identify_movie "Movie Y" "mkv" "$TEST_DIR/src/Movie Y (2021).mkv" "Movie Y" "2021") || rc=$?
|
||||
assert_eq "0" "$rc" "movie en 命中返回码"
|
||||
assert_contains "$dest" "Movie Y (2021)" "movie en 命中目标"
|
||||
}
|
||||
Reference in new issue
Block a user