package main import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) func tiersForTest() []qualityTier { // 故意乱序,验证排序 return []qualityTier{ {Name: "高清", Bitrate: 2000}, {Name: "蓝光 8M", Bitrate: 8000}, {Name: "标清", Bitrate: 1000}, {Name: "蓝光 4M", Bitrate: 4000}, } } func TestSortQualitiesDesc(t *testing.T) { got := sortQualitiesDesc(tiersForTest()) want := []string{"蓝光 8M", "蓝光 4M", "高清", "标清"} for i, name := range want { if got[i].Name != name { t.Fatalf("第 %d 个应为 %s,实际 %s(%v)", i, name, got[i].Name, got) } } // 不能改动传进来的切片 in := tiersForTest() _ = sortQualitiesDesc(in) if in[0].Name != "高清" { t.Errorf("排序不该改动原切片:%v", in) } } // 默认档位 = 自上往下第二档(不是最高档) func TestDefaultQualityBitrate(t *testing.T) { cases := []struct { name string tiers []qualityTier want int }{ {"四档取第二高", tiersForTest(), 4000}, {"三档取第二高(你给的例子)", []qualityTier{{"高清", 2000}, {"蓝光 8M", 8000}, {"蓝光 4M", 4000}}, 4000}, {"两档取低的那档", []qualityTier{{"蓝光 4M", 4000}, {"高清", 2000}}, 2000}, {"只有一档就用它", []qualityTier{{"高清", 2000}}, 2000}, {"没有档位返回 0", nil, 0}, } for _, c := range cases { if got := defaultQualityBitrate(c.tiers); got != c.want { t.Errorf("%s:应为 %d,实际 %d", c.name, c.want, got) } } } func TestQualityNameFor(t *testing.T) { if got := qualityNameFor(tiersForTest(), 4000); got != "蓝光 4M" { t.Errorf("4000 应匹配「蓝光 4M」,实际 %q", got) } if got := qualityNameFor(tiersForTest(), 9999); got != "" { t.Errorf("匹配不上应返回空,实际 %q", got) } } func TestSameQualities(t *testing.T) { a := tiersForTest() b := []qualityTier{{"标清", 1000}, {"蓝光 4M", 4000}, {"高清", 2000}, {"蓝光 8M", 8000}} if !sameQualities(a, b) { t.Error("顺序不同但内容相同的档位列表应视为一样") } c := append([]qualityTier(nil), a[:3]...) if sameQualities(a, c) { t.Error("档位数量不同应视为不一样") } } // 记录档位时只缓存档位表,不动用户选的码率: // 0 表示「自动、按实际档位取第二高」,主播换了档位表要能跟着走 func TestSetStreamerQualities(t *testing.T) { savedConfig := config savedStreamers := streamers.crt defer func() { config = savedConfig streamers.crt = savedStreamers }() config.UIDs = uidList{ {UID: 1, Danmu: true, Record: true, Bitrate: 0}, // 自动 {UID: 2, Danmu: true, Record: true, Bitrate: 1000}, // 手动指定过 } streamers.crt = map[int]streamer{ 1: {UID: 1, Bitrate: 0}, 2: {UID: 2, Bitrate: 1000}, } if !setStreamerQualities(1, tiersForTest()) { t.Error("第一次记录档位应该算改动了配置") } if config.UIDs[0].Bitrate != 0 { t.Errorf("自动的主播不该被写死成具体码率,实际 %d", config.UIDs[0].Bitrate) } if len(config.UIDs[0].Qualities) != 4 || len(streamers.crt[1].Qualities) != 4 { t.Errorf("档位没缓存到配置或运行时数据:%+v / %+v", config.UIDs[0], streamers.crt[1]) } if streamers.crt[1].Bitrate != 0 { t.Errorf("运行时数据的码率不该被改动,实际 %d", streamers.crt[1].Bitrate) } // 手动设过的同样不动 setStreamerQualities(2, tiersForTest()) if config.UIDs[1].Bitrate != 1000 { t.Errorf("uid 2 手动设过 1000,不该被改成 %d", config.UIDs[1].Bitrate) } // 同样的档位再来一次不算改动 if setStreamerQualities(1, tiersForTest()) { t.Error("档位没变化时不该算改动") } // 档位变了才算改动 if !setStreamerQualities(1, []qualityTier{{"高清", 2000}, {"流畅", 500}}) { t.Error("档位变化时应算改动") } } // 按偏好决定实际用哪个档位:最高/第二高/最低按实际档位排序取, // 自定义取不超过给定码率的最高档、都比它高时退回最低档 func TestResolveQualityBitrate(t *testing.T) { cases := []struct { name string pref string custom int want int }{ {"最高档", qualityPrefTop, 0, 8000}, {"第二高(默认)", qualityPrefSecond, 0, 4000}, {"最低档", qualityPrefLowest, 0, 1000}, {"空偏好按默认的第二高", "", 0, 4000}, {"没见过的偏好也按第二高", "bogus", 0, 4000}, {"自定义取不超过上限的最高档", qualityPrefCustom, 3000, 2000}, {"自定义上限正好等于某一档", qualityPrefCustom, 4000, 4000}, {"自定义上限比所有档位都高就取最高档", qualityPrefCustom, 99999, 8000}, {"自定义上限比所有档位都低就退回最低档", qualityPrefCustom, 200, 1000}, {"自定义填 0 等于最低档", qualityPrefCustom, 0, 1000}, } for _, c := range cases { if got := resolveQualityBitrate(tiersForTest(), c.pref, c.custom); got != c.want { t.Errorf("%s:应为 %d,实际 %d", c.name, c.want, got) } } // 没有档位表时:相对偏好返回 0(取流时退回最高档),自定义返回填的上限 if got := resolveQualityBitrate(nil, qualityPrefSecond, 0); got != 0 { t.Errorf("没有档位表时相对偏好应返回 0,实际 %d", got) } if got := resolveQualityBitrate(nil, qualityPrefCustom, 2500); got != 2500 { t.Errorf("没有档位表时自定义应返回填的 2500,实际 %d", got) } // 只有一档时:最高/第二高都是它,最低也是它 one := []qualityTier{{"高清", 2000}} for _, pref := range []string{qualityPrefTop, qualityPrefSecond, qualityPrefLowest} { if got := resolveQualityBitrate(one, pref, 0); got != 2000 { t.Errorf("只有一档时 %s 应为 2000,实际 %d", pref, got) } } } // 老配置只有 bitrate,读进来时要能推断出偏好:>0 是「自定义上限」,0 是默认的第二高 func TestMigrateQualityPref(t *testing.T) { cases := []struct { pref string bitrate int want string }{ {"", 0, qualityPrefSecond}, {"", 5000, qualityPrefCustom}, {"", 1000, qualityPrefCustom}, {qualityPrefTop, 0, qualityPrefTop}, {qualityPrefLowest, 5000, qualityPrefLowest}, {qualityPrefCustom, 3000, qualityPrefCustom}, {" ", 0, qualityPrefSecond}, // 认不出来的值按默认 {"TOP", 0, qualityPrefSecond}, // 大小写不敏感不做,保持严格 } for _, c := range cases { if got := migrateQualityPref(c.pref, c.bitrate); got != c.want { t.Errorf("pref=%q bitrate=%d 应为 %s,实际 %s", c.pref, c.bitrate, c.want, got) } } // normalizeQualityPref 对四种合法值原样返回 for _, p := range []string{qualityPrefTop, qualityPrefSecond, qualityPrefLowest, qualityPrefCustom} { if got := normalizeQualityPref(p); got != p { t.Errorf("%s 应原样返回,实际 %s", p, got) } } } // 升级老配置:config.UIDs 和默认档位都要补上 qualityPref func TestUpgradeQualityPrefs(t *testing.T) { savedConfig := config defer func() { config = savedConfig }() config.UIDs = uidList{ {UID: 1}, // 老配置:bitrate 0 → 第二高 {UID: 2, Bitrate: 5000}, // 老配置:有码率 → 自定义 {UID: 3, QualityPref: qualityPrefTop}, // 已经是新配置 → 不动 } config.DefaultQualityPref = "" config.DefaultBitrate = 0 upgradeQualityPrefs() if config.UIDs[0].QualityPref != qualityPrefSecond { t.Errorf("uid 1 应补成 second,实际 %s", config.UIDs[0].QualityPref) } if config.UIDs[1].QualityPref != qualityPrefCustom { t.Errorf("uid 2 应补成 custom,实际 %s", config.UIDs[1].QualityPref) } if config.UIDs[2].QualityPref != qualityPrefTop { t.Errorf("uid 3 不该被改动,实际 %s", config.UIDs[2].QualityPref) } if config.DefaultQualityPref != qualityPrefSecond { t.Errorf("默认档位应补成 second,实际 %s", config.DefaultQualityPref) } // 幂等 upgradeQualityPrefs() if config.UIDs[1].QualityPref != qualityPrefCustom { t.Error("重复升级不该改变结果") } } // 改档位偏好:写配置 + 同步运行时数据 func TestSetQuality(t *testing.T) { savedConfig := config savedStreamers := streamers.crt savedLocation := configFileLocation oldDir := configDir if configDir == nil { configDir = new(string) } defer func() { config = savedConfig streamers.crt = savedStreamers configFileLocation = savedLocation configDir = oldDir }() *configDir = t.TempDir() configFileLocation = filepath.Join(*configDir, configFile) config.UIDs = uidList{{UID: 7, QualityPref: qualityPrefSecond}} streamers.crt = map[int]streamer{7: {UID: 7, QualityPref: qualityPrefSecond}} if !setQuality(7, qualityPrefTop, 0) { t.Fatal("改档位应该成功") } if config.UIDs[0].QualityPref != qualityPrefTop || streamers.crt[7].QualityPref != qualityPrefTop { t.Errorf("档位偏好没同步:%+v / %+v", config.UIDs[0], streamers.crt[7]) } // 自定义时码率上限也要存下来 if !setQuality(7, qualityPrefCustom, 2500) { t.Fatal("改成自定义应该成功") } if streamers.crt[7].Bitrate != 2500 || config.UIDs[0].Bitrate != 2500 { t.Errorf("自定义码率没存:%+v / %+v", config.UIDs[0], streamers.crt[7]) } // 认不出来的偏好按默认处理,负数码率直接拒绝 setQuality(7, "bogus", 2500) if streamers.crt[7].QualityPref != qualityPrefSecond { t.Errorf("非法偏好应按默认第二高,实际 %s", streamers.crt[7].QualityPref) } if setQuality(7, qualityPrefCustom, -1) { t.Error("负数码率应该被拒绝") } if setQuality(999, qualityPrefTop, 0) { t.Error("不在列表里的 uid 应该失败") } } // 手动合并的通知靠文件名猜主播:只有一个名字对得上才认 func TestGuessUIDByFileName(t *testing.T) { savedConfig := config savedStreamers := streamers.crt defer func() { config = savedConfig streamers.crt = savedStreamers }() config.UIDs = uidList{{UID: 1}, {UID: 2}, {UID: 3}} streamers.crt = map[int]streamer{ 1: {UID: 1, Name: "AC娘本体"}, 2: {UID: 2, Name: "随便唠会"}, 3: {UID: 3, Name: "阿"}, } cases := []struct { file string want int }{ {"2024-10-14 00-33-32 AC娘本体 嘿嘿 醒了_弹幕合并.mp4", 1}, {"AC娘本体.mp4", 1}, {"2024-10-14 00-33-32 随便唠会 摸鱼.mp4", 2}, // 两个名字都在文件名里,猜不准就不猜 {"AC娘本体 和 随便唠会 联动.mp4", 0}, {"完全不认识的主播.mp4", 0}, // 名字只有一个字的不参与匹配,太容易误伤 {"阿 的录播.mp4", 0}, {"", 0}, } for _, c := range cases { if got := guessUIDByFileName(c.file); got != c.want { t.Errorf("%q 应猜出 %d,实际 %d", c.file, c.want, got) } } } // 档位要能存进 config.json 再读回来 func TestQualitiesConfigRoundTrip(t *testing.T) { savedConfig := config savedLocation := configFileLocation oldDir := configDir if configDir == nil { configDir = new(string) } defer func() { config = savedConfig configFileLocation = savedLocation configDir = oldDir }() dir := t.TempDir() *configDir = dir configFileLocation = filepath.Join(dir, configFile) body := `{"uids":[{"uid":23682490,"danmu":true,"record":true,"bitrate":4000, "qualities":[{"name":"蓝光 8M","bitrate":8000},{"name":"蓝光 4M","bitrate":4000}]}]}` if err := os.WriteFile(configFileLocation, []byte(body), 0644); err != nil { t.Fatal(err) } config = configData{} loadConfig() if len(config.UIDs) != 1 { t.Fatalf("应解析出 1 个主播,实际 %d", len(config.UIDs)) } u := config.UIDs[0] if u.Bitrate != 4000 || len(u.Qualities) != 2 || u.Qualities[0].Name != "蓝光 8M" { t.Fatalf("档位没正确读出来:%+v", u) } if got := qualityNameFor(u.Qualities, u.Bitrate); got != "蓝光 4M" { t.Errorf("当前档位名应为「蓝光 4M」,实际 %q", got) } // 再序列化一次,字段名要对得上(qualities / name / bitrate) config.UIDs[0].Qualities = sortQualitiesDesc(config.UIDs[0].Qualities) data, err := json.Marshal(config.UIDs[0]) if err != nil { t.Fatal(err) } for _, key := range []string{`"qualities"`, `"name"`, `"bitrate"`} { if !json.Valid(data) || !strings.Contains(string(data), key) { t.Errorf("序列化结果里应包含 %s:%s", key, data) } } }