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,61 @@
|
||||
# ============================================================================
|
||||
# 特典类别判定表外置测试(src/lib/config/maps.sh + ai_resolve.sh)
|
||||
# ============================================================================
|
||||
|
||||
setup() {
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
AUTOMATED=true
|
||||
load_special_categories "$TEST_DIR/categories.json"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
# load_special_categories:数组加载(顺序 = 优先级)
|
||||
test_load_special_categories_basic() {
|
||||
cat >"$TEST_DIR/categories.json" <<'EOF'
|
||||
[
|
||||
{"match": "wow", "tag": "新类别"},
|
||||
{"match": "menu", "tag": "自定义Menu"}
|
||||
]
|
||||
EOF
|
||||
load_special_categories "$TEST_DIR/categories.json"
|
||||
assert_eq "2" "${#SPECIAL_CATEGORIES[@]}"
|
||||
# 用户表优先于内置默认表
|
||||
assert_eq "自定义Menu" "$(special_category_tag "Menu01")"
|
||||
assert_eq "新类别" "$(special_category_tag "wow01")"
|
||||
}
|
||||
|
||||
# special_category_tag:用户表未命中 → 回退内置默认表
|
||||
test_special_category_tag_fallback_builtin() {
|
||||
assert_eq "NCED" "$(special_category_tag "NCED02")"
|
||||
assert_eq "CM" "$(special_category_tag "TVSP")"
|
||||
assert_eq "Special" "$(special_category_tag "完全未知词")"
|
||||
}
|
||||
|
||||
# special_category_tag:空表(文件缺失/自动化)→ 内置默认表全量工作
|
||||
test_special_category_tag_empty_table() {
|
||||
assert_eq "Menu" "$(special_category_tag "Menu01")"
|
||||
assert_eq "PV" "$(special_category_tag "Announcement")"
|
||||
assert_eq "ED" "$(special_category_tag "Lyric")"
|
||||
assert_eq "Music Video" "$(special_category_tag "MV")"
|
||||
assert_eq "Picture Drama" "$(special_category_tag "Drama")"
|
||||
assert_eq "IV" "$(special_category_tag "IV01")"
|
||||
}
|
||||
|
||||
# load_special_categories:非法格式 → 空表不阻断
|
||||
test_load_special_categories_invalid() {
|
||||
echo '{"a": 1}' >"$TEST_DIR/categories.json"
|
||||
load_special_categories "$TEST_DIR/categories.json"
|
||||
assert_eq "0" "${#SPECIAL_CATEGORIES[@]}"
|
||||
}
|
||||
|
||||
# init_special_categories:自动化 + 文件缺失 → 不创建、空表(内置兜底可用)
|
||||
test_init_special_categories_automated_missing() {
|
||||
MATCH_FILE="$TEST_DIR/missing/special_categories.json"
|
||||
init_special_categories "$MATCH_FILE"
|
||||
assert_file_absent "$MATCH_FILE"
|
||||
assert_eq "0" "${#SPECIAL_CATEGORIES[@]}"
|
||||
assert_eq "Special" "$(special_category_tag "未知")"
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
# ============================================================================
|
||||
# 匹配规则测试(src/lib/rules.sh):match_rules.json 加载与查询
|
||||
# ============================================================================
|
||||
|
||||
setup() {
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
AUTOMATED=true # 跳过交互创建
|
||||
load_match_rules "$TEST_DIR/match_rules.json"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
# load_match_rules:搜索别名(字符串与对象形式)+ ID 映射
|
||||
test_load_match_rules_basic() {
|
||||
cat >"$TEST_DIR/match_rules.json" <<'EOF'
|
||||
{
|
||||
"search_aliases": {
|
||||
"俺妹": "Ore no Imouto ga Konna ni Kawaii Wake ga Nai",
|
||||
"路人女主剧场版": {"term": "Saekano the Movie", "year": "2019"}
|
||||
},
|
||||
"id_maps": {
|
||||
"movie": {"刀剑神域": 20982},
|
||||
"tv": {"魔法禁书目录": 4654}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
load_match_rules "$TEST_DIR/match_rules.json"
|
||||
# 别名:字符串形式(无 year)与对象形式(带 year)
|
||||
assert_eq "Ore no Imouto ga Konna ni Kawaii Wake ga Nai " \
|
||||
"$(match_rule_lookup_alias "俺妹")"
|
||||
assert_eq "Saekano the Movie 2019" \
|
||||
"$(match_rule_lookup_alias "路人女主剧场版")"
|
||||
# ID 映射:movie/tv 分表,键归一化后命中
|
||||
assert_eq "20982" "${MATCH_RULES_ID_MOVIE[刀剑神域]:-}"
|
||||
assert_eq "4654" "${MATCH_RULES_ID_TV[魔法禁书目录]:-}"
|
||||
# 未命中
|
||||
assert_empty "$(match_rule_lookup_alias "不存在的标题")"
|
||||
assert_empty "${MATCH_RULES_ID_MOVIE[不存在的标题]:-}"
|
||||
assert_empty "${MATCH_RULES_ID_TV[不存在的标题]:-}"
|
||||
}
|
||||
|
||||
# load_match_rules:键归一化(大小写/空白)后命中
|
||||
test_load_match_rules_key_normalized() {
|
||||
cat >"$TEST_DIR/match_rules.json" <<'EOF'
|
||||
{
|
||||
"search_aliases": {
|
||||
"Sword Art Online": "ソードアート・オンライン"
|
||||
},
|
||||
"id_maps": {
|
||||
"movie": {"A B": 1}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
load_match_rules "$TEST_DIR/match_rules.json"
|
||||
assert_eq "ソードアート・オンライン " "$(match_rule_lookup_alias "sword art online")"
|
||||
assert_eq "1" "${MATCH_RULES_ID_MOVIE[a b]:-}"
|
||||
}
|
||||
|
||||
# load_match_rules:文件缺失 → 空规则(不报错)
|
||||
test_load_match_rules_missing_file() {
|
||||
assert_eq "0" "${#MATCH_RULES_ALIAS_TERM[@]}"
|
||||
assert_empty "$(match_rule_lookup_alias "任意标题")"
|
||||
}
|
||||
|
||||
# load_match_rules:非 JSON 对象 → 空规则 + 不阻断
|
||||
test_load_match_rules_invalid_json() {
|
||||
echo 'not json' >"$TEST_DIR/match_rules.json"
|
||||
load_match_rules "$TEST_DIR/match_rules.json"
|
||||
assert_eq "0" "${#MATCH_RULES_ALIAS_TERM[@]}"
|
||||
assert_eq "0" "${#MATCH_RULES_ID_MOVIE[@]}"
|
||||
}
|
||||
|
||||
# load_match_rules:空别名 term 的条目跳过(不产生空键)
|
||||
test_load_match_rules_empty_term_skipped() {
|
||||
cat >"$TEST_DIR/match_rules.json" <<'EOF'
|
||||
{
|
||||
"search_aliases": {
|
||||
"空词": ""
|
||||
},
|
||||
"id_maps": {
|
||||
"tv": {"空ID": ""}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
load_match_rules "$TEST_DIR/match_rules.json"
|
||||
assert_eq "0" "${#MATCH_RULES_ALIAS_TERM[@]}"
|
||||
assert_eq "0" "${#MATCH_RULES_ID_TV[@]}"
|
||||
}
|
||||
|
||||
# init_match_rules:自动化模式 + 文件缺失 → 不创建、空规则
|
||||
test_init_match_rules_automated_missing() {
|
||||
MATCH_RULES_FILE="$TEST_DIR/nonexistent/match_rules.json"
|
||||
init_match_rules "$MATCH_RULES_FILE"
|
||||
assert_file_absent "$MATCH_RULES_FILE"
|
||||
assert_eq "0" "${#MATCH_RULES_ALIAS_TERM[@]}"
|
||||
}
|
||||
|
||||
# init_match_rules:文件存在 → 正常加载
|
||||
test_init_match_rules_existing() {
|
||||
cat >"$TEST_DIR/rules.json" <<'EOF'
|
||||
{"search_aliases": {"别名": "Term"}, "id_maps": {"movie": {"影": 9}}}
|
||||
EOF
|
||||
init_match_rules "$TEST_DIR/rules.json"
|
||||
assert_eq "Term " "$(match_rule_lookup_alias "别名")"
|
||||
assert_eq "9" "${MATCH_RULES_ID_MOVIE[影]:-}"
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
# ============================================================================
|
||||
# 配置文件类目分区测试(src/lib/config/maps.sh)
|
||||
# ============================================================================
|
||||
|
||||
setup() {
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
AUTOMATED=true
|
||||
# 干净状态
|
||||
SPECIAL_WORDS_CAT=()
|
||||
SKIP_DIRS_CAT=()
|
||||
SPECIAL_MAP_CAT=()
|
||||
RAW_SPECIAL_MAP_CAT=()
|
||||
SEASON_OFFSET_CAT=()
|
||||
SKIP_DIRS=()
|
||||
SKIP_DIRS_LOWER=()
|
||||
SPECIAL_WORDS=()
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
# is_category_partitioned:分区形态判定
|
||||
test_is_category_partitioned() {
|
||||
echo '{"tv": ["menu"], "musicvideo": ["mv"], "default": ["x"]}' >"$TEST_DIR/p.json"
|
||||
assert_success is_category_partitioned "$TEST_DIR/p.json"
|
||||
echo '{"menu": ["Menu"]}' >"$TEST_DIR/g.json"
|
||||
assert_failure is_category_partitioned "$TEST_DIR/g.json"
|
||||
echo '{"tv": ["menu"], "menu": ["x"]}' >"$TEST_DIR/mixed.json"
|
||||
assert_failure is_category_partitioned "$TEST_DIR/mixed.json"
|
||||
}
|
||||
|
||||
# init_special_words:分区形态 → 词按类目查询(musicvideo 不回退 tv 词表)
|
||||
test_partitioned_special_words() {
|
||||
cat >"$TEST_DIR/kw.json" <<'EOF'
|
||||
{"tv": ["menu", "cm"], "musicvideo": ["mv", "live"], "default": ["通用"]}
|
||||
EOF
|
||||
init_special_words "$TEST_DIR/kw.json"
|
||||
# tv 分区词
|
||||
assert_eq "menu" "$(special_word_hit tv "menu01")"
|
||||
# musicvideo 分区词
|
||||
assert_eq "mv" "$(special_word_hit musicvideo "mv")"
|
||||
# tv 分区词不落入 musicvideo(子串 "mv" 在 musicvideo 分区,tv 词不回退)
|
||||
assert_empty "$(special_word_hit musicvideo "cm")"
|
||||
# default 分区词两则命中
|
||||
assert_eq "通用" "$(special_word_hit tv "通用词")"
|
||||
assert_eq "通用" "$(special_word_hit musicvideo "通用词")"
|
||||
# 精确匹配:目录信号语义("tmp.xxx" 不命中任何词)
|
||||
assert_failure special_word_exact musicvideo "tmp.xxx"
|
||||
assert_success special_word_exact musicvideo "mv"
|
||||
assert_success special_word_exact musicvideo "live"
|
||||
}
|
||||
|
||||
# init_skip_dirs:分区形态 = 完整语义(不叠加内置默认)
|
||||
test_partitioned_skip_dirs() {
|
||||
cat >"$TEST_DIR/skips.json" <<'EOF'
|
||||
{"video": ["sps", "specials"], "music": ["scans", "fonts"], "default": ["特典"]}
|
||||
EOF
|
||||
init_skip_dirs "$TEST_DIR/skips.json"
|
||||
assert_success skip_dir_in video "sps"
|
||||
# music 分区未含 sps → 不回退内置默认(分区完整语义)
|
||||
assert_failure skip_dir_in music "sps"
|
||||
assert_success skip_dir_in music "scans"
|
||||
# default 分区回退
|
||||
assert_success skip_dir_in video "特典"
|
||||
assert_success skip_dir_in music "特典"
|
||||
# 分区外词不命中(内置默认不叠加)
|
||||
assert_failure skip_dir_in video "cds"
|
||||
}
|
||||
|
||||
# 全局形态(旧版数组文件)保持行为:文件内容优先,空数组回退内置默认
|
||||
test_legacy_skip_dirs_global() {
|
||||
echo '["myspecial"]' >"$TEST_DIR/skips_legacy.json"
|
||||
init_skip_dirs "$TEST_DIR/skips_legacy.json"
|
||||
assert_success skip_dir_in video "myspecial"
|
||||
# 非空数组文件 = 文件内容优先(不叠加内置默认)
|
||||
assert_failure skip_dir_in video "sps"
|
||||
}
|
||||
|
||||
# 全局形态空数组 → 内置默认兜底
|
||||
test_legacy_skip_dirs_empty_falls_back() {
|
||||
echo '[]' >"$TEST_DIR/skips_empty.json"
|
||||
init_skip_dirs "$TEST_DIR/skips_empty.json"
|
||||
assert_success skip_dir_in video "sps"
|
||||
}
|
||||
|
||||
# load_special_map:分区形态(tv 分区 + default 分区回退 + 引用展开)
|
||||
test_partitioned_special_map() {
|
||||
cat >"$TEST_DIR/maps.json" <<'EOF'
|
||||
{"tv": {"menu": ["Menu", "菜单"], "menu01": "menu"}, "default": {"cm": ["CM"]}}
|
||||
EOF
|
||||
load_special_map "$TEST_DIR/maps.json"
|
||||
assert_eq '["Menu","菜单"]' "$(special_map_value tv "menu")"
|
||||
# 分区内引用展开
|
||||
assert_eq '["Menu","菜单"]' "$(special_map_value tv "menu01")"
|
||||
# default 分区回退
|
||||
assert_eq '["CM"]' "$(special_map_value tv "cm")"
|
||||
# 无映射键 → 空
|
||||
assert_empty "$(special_map_value tv "不存在")"
|
||||
}
|
||||
|
||||
# init_season_offset:分区形态(tv 分区 + default 分区回退)
|
||||
test_partitioned_season_offset() {
|
||||
cat >"$TEST_DIR/offsets.json" <<'EOF'
|
||||
{"tv": {"demo show": 5}, "default": {"id:123": 3}}
|
||||
EOF
|
||||
SEASON_OFFSET_FILE="$TEST_DIR/offsets.json" init_season_offset "$TEST_DIR/offsets.json"
|
||||
assert_eq "5" "$(season_offset_get tv "demo show")"
|
||||
assert_eq "3" "$(season_offset_get tv "id:123")"
|
||||
# 内置默认仍可用(全局表兜底)
|
||||
assert_eq "23" "$(season_offset_get tv "jujutsu kaisen")"
|
||||
# 未命中 → 空
|
||||
assert_empty "$(season_offset_get tv "no such show")"
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# ============================================================================
|
||||
# 用户自定义规则测试(src/lib/rules.sh):命名模板渲染 + 匹配规则键
|
||||
# ============================================================================
|
||||
|
||||
# render_naming_template:普通占位符 + 补零
|
||||
test_render_plain_and_padding() {
|
||||
assert_eq "S01E02" "$(render_naming_template 'S{season:02}E{episode:02}' season=1 episode=2)"
|
||||
assert_eq "Season 12" "$(render_naming_template 'Season {season}' season=12)"
|
||||
}
|
||||
|
||||
# render_naming_template:条件段(year 非空渲染,空则不渲染)
|
||||
test_render_conditional_year() {
|
||||
assert_eq "Movie (2020)" "$(render_naming_template '{title}{?year: ({year})}' title=Movie year=2020)"
|
||||
assert_eq "Movie" "$(render_naming_template '{title}{?year: ({year})}' title=Movie year=)"
|
||||
}
|
||||
|
||||
# render_naming_template:条件段正文含嵌套占位符
|
||||
test_render_conditional_nested_placeholder() {
|
||||
assert_eq "Show [2020]" "$(render_naming_template '{title}{?year: [{year}]}' title=Show year=2020)"
|
||||
}
|
||||
|
||||
# render_naming_template:默认剧集模板全链路(含空集名条件段)
|
||||
test_render_episode_template_full() {
|
||||
assert_eq "S01E02 - Name" \
|
||||
"$(render_naming_template 'S{season:02}E{episode:02}{range}{?episode_name: - {episode_name}}' season=01 episode=02 range= 'episode_name=Name')"
|
||||
assert_eq "S01E02" \
|
||||
"$(render_naming_template 'S{season:02}E{episode:02}{range}{?episode_name: - {episode_name}}' season=01 episode=02 range= 'episode_name=')"
|
||||
assert_eq "S01E02-E03 - A - B" \
|
||||
"$(render_naming_template 'S{season:02}E{episode:02}{range}{?episode_name: - {episode_name}}' season=01 episode=02 range=-E03 'episode_name=A - B')"
|
||||
}
|
||||
|
||||
# render_naming_template:值含 & 不被替换语义破坏(${var//pat/repl} 会把 & 当匹配整体)
|
||||
test_render_ampersand_safe() {
|
||||
assert_eq "A & B" "$(render_naming_template '{episode_name}' 'episode_name=A & B')"
|
||||
assert_eq "Artist & Band - Album" \
|
||||
"$(render_naming_template '{artist} - {album}' 'artist=Artist & Band' album=Album)"
|
||||
}
|
||||
|
||||
# render_naming_template:值内花括号被剔除(防破坏模板解析)
|
||||
test_render_braces_in_value_stripped() {
|
||||
assert_eq "AB" "$(render_naming_template '{title}' 'title=A{B')"
|
||||
}
|
||||
|
||||
# render_naming_template:未知占位符渲染为空
|
||||
test_render_unknown_placeholder_empty() {
|
||||
assert_eq "X " "$(render_naming_template '{title} {foo}' title=X)"
|
||||
}
|
||||
|
||||
# render_naming_template:补零对非数字值原样保留
|
||||
test_render_padding_non_numeric() {
|
||||
assert_eq "abc" "$(render_naming_template '{n:03}' n=abc)"
|
||||
}
|
||||
|
||||
# render_naming_template:无配对右花括号按字面保留
|
||||
test_render_unclosed_brace_literal() {
|
||||
assert_eq "X {tail" "$(render_naming_template 'X {tail' title=X)"
|
||||
}
|
||||
|
||||
# render_naming:配置键未设置时用内置默认(测试环境不经过 load_config)
|
||||
test_render_naming_default_fallback() {
|
||||
assert_eq "Season 03" "$(render_naming NAMING_SEASON season=3)"
|
||||
assert_eq "Movie (2020)" "$(render_naming NAMING_MOVIE title=Movie year=2020)"
|
||||
assert_eq "S00Menu - Menu01" "$(render_naming NAMING_SPECIAL tag=Menu fragment=Menu01)"
|
||||
assert_eq "Artist/Album" "$(render_naming NAMING_MUSIC artist=Artist album=Album)"
|
||||
}
|
||||
|
||||
# template_token_names:提取占位符名(含条件段正文内的;重复名由调用方去重)
|
||||
test_template_token_names() {
|
||||
assert_eq "$(printf 'title\nyear' | sort)" "$(template_token_names '{title}{?year: ({year})}' | sort -u)"
|
||||
assert_eq "$(printf 'season\nepisode\nrange\nepisode_name' | sort)" \
|
||||
"$(template_token_names 'S{season:02}E{episode:02}{range}{?episode_name: - {episode_name}}' | sort -u)"
|
||||
}
|
||||
|
||||
# rule_key:小写 + 空白折叠 + 去首尾空白
|
||||
test_rule_key_normalization() {
|
||||
assert_eq "sword art online" "$(rule_key ' Sword Art ONLINE ')"
|
||||
assert_eq "sword art online" "$(rule_key 'Sword Art Online')"
|
||||
assert_eq "俺妹" "$(rule_key '俺妹')"
|
||||
}
|
||||
Reference in new issue
Block a user