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("档位变化时应算改动") } } // 实际用哪个档位:0 = 自动(第二高),填了具体码率 = 不超过它的最高档, // 都比它高时退回最低档 func TestResolveQualityBitrate(t *testing.T) { cases := []struct { name string bitrate int want int }{ {"自动取第二高", 0, 4000}, {"指定最高档", 8000, 8000}, {"指定第二高", 4000, 4000}, {"指定最低档", 1000, 1000}, {"填了中间值取不超过它的最高档", 3000, 2000}, {"比所有档位都高就取最高档", 99999, 8000}, {"比所有档位都低就退回最低档", 200, 1000}, } for _, c := range cases { if got := resolveQualityBitrate(tiersForTest(), c.bitrate); got != c.want { t.Errorf("%s:应为 %d,实际 %d", c.name, c.want, got) } } // 没有档位表时保持原样(取流逻辑会退回最高档) if got := resolveQualityBitrate(nil, 0); got != 0 { t.Errorf("没有档位表时应返回 0,实际 %d", got) } if got := resolveQualityBitrate(nil, 2000); got != 2000 { t.Errorf("没有档位表时应原样返回,实际 %d", got) } // 只有一档时自动档就是它 one := []qualityTier{{"高清", 2000}} if got := resolveQualityBitrate(one, 0); got != 2000 { t.Errorf("只有一档时自动应为 2000,实际 %d", got) } } // 手动合并的通知靠文件名猜主播:只有一个名字对得上才认 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) } } }