package domain import "testing" // 本文件实现 devdocs/09-测试策略.md 第 2 节 17 条用例矩阵中的 13 条(01–07、10–15); // 第 8 / 9 / 17 / 18 条没有同名测试函数,第 16 条(联队)已整条移除。 // 用例名带编号,go test -v 一眼能看出哪条矩阵用例通过。 // case 1:8 人参赛正常排名 → 9/7/6/5/4/3/2/1。 func TestMatrix01_NormalRanking8(t *testing.T) { entries, results := fixture(lineup(8)) es := mustScore(t, EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, }) assertEntryPoints(t, pointsByEntry(es.Ledger), []float64{9, 7, 6, 5, 4, 3, 2, 1}) } // case 2:两人并列第 2(skip)→ 7/7/-/5/4/3/2/1。 func TestMatrix02_TieAtSecondSkip(t *testing.T) { entries, results := fixture(lineupValues(1, 2, 2, 4, 5, 6, 7, 8)) es := mustScore(t, EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, }) assertEntryPoints(t, pointsByEntry(es.Ledger), []float64{9, 7, 7, 5, 4, 3, 2, 1}) ranks := rankByEntry(es.Ranked) if ranks[2] != 2 || ranks[3] != 2 || ranks[4] != 4 { t.Fatalf("并列名次错误:%v", ranks) } } // case 3:两人并列第 2(average)→ 6.5/6.5/-/5/4/3/2/1。 func TestMatrix03_TieAtSecondAverage(t *testing.T) { entries, results := fixture(lineupValues(1, 2, 2, 4, 5, 6, 7, 8)) es := mustScore(t, EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieAverage)}, Entries: entries, Results: results, }) assertEntryPoints(t, pointsByEntry(es.Ledger), []float64{9, 6.5, 6.5, 5, 4, 3, 2, 1}) } // case 4:三人并列第 3(skip)→ 6/6/6/-/-/3/2/1。 func TestMatrix04_ThreeWayTieAtThird(t *testing.T) { entries, results := fixture(lineupValues(1, 2, 3, 3, 3, 6, 7, 8)) es := mustScore(t, EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, }) assertEntryPoints(t, pointsByEntry(es.Ledger), []float64{9, 7, 6, 6, 6, 3, 2, 1}) } // case 5:团体项目双倍 → 18/14/12/… func TestMatrix05_TeamEventDoubled(t *testing.T) { ps := lineup(8) for i := range ps { // 纯团体项目的报名项是团体报名项(无运动员):团体名次由 results.rank 兜底。 ps[i].athleteID = 0 ps[i].rank = i + 1 } entries, results := fixture(ps) es := mustScore(t, EventScoreInput{ Event: teamEvent(2), Category: testCategory(), Channels: []ScoreChannel{teamManualChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, }) assertEntryPoints(t, pointsByEntry(es.Ledger), []float64{18, 14, 12, 10, 8, 6, 4, 2}) } // case 6:破纪录 + 双倍 → 成绩分双倍,破纪录加分不双倍。 func TestMatrix06_RecordBonusNotDoubled(t *testing.T) { entries, results := fixture(lineup(3)) es := mustScore(t, EventScoreInput{ Event: individualEvent(2), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, RecordBonus: &RecordBonusConfig{ Enabled: true, Mode: RecordBonusMax, Levels: []RecordLevel{{Code: "meet", Name: "赛会纪录", Points: 2}}, }, RecordBreaks: []RecordBreak{{ ID: 1, EventID: 1, EntryID: 1, AthleteID: 101, TeamID: 1001, Level: "meet", State: RecordBreakApproved, }}, }) var bonus *LedgerEntry for i := range es.Ledger { if es.Ledger[i].SourceType == SourceRecordBonus { bonus = &es.Ledger[i] } } if bonus == nil { t.Fatal("未产生破纪录加分流水") } if !nearlyEqual(bonus.Points, 2) { t.Errorf("破纪录加分 = %v,期望 2(不随项目权重翻倍)", bonus.Points) } if got := pointsByEntry(es.Ledger)[1]; !nearlyEqual(got, 20) { t.Errorf("entry1 合计 = %v,期望 18(成绩分)+ 2(加分)= 20", got) } } // case 7:并列第一的奖牌计数 → 2 金 0 银 1 铜。 func TestMatrix07_TiedFirstMedalCount(t *testing.T) { entries, results := fixture(lineupValues(1, 1, 3)) es := mustScore(t, EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, }) rows := BuildTeamStandings(StandingsInput{ Teams: testTeams(1001, 1002, 1003), Ledger: es.Ledger, Ranked: es.Ranked, }) var gold, silver, bronze int for _, r := range rows { gold += r.Gold silver += r.Silver bronze += r.Bronze } if gold != 2 || silver != 0 || bronze != 1 { t.Fatalf("奖牌统计 = %d金 %d银 %d铜,期望 2/0/1", gold, silver, bronze) } } // case 10:弃权者不占名次 → 名次顺延。 func TestMatrix10_WithdrawnDoesNotOccupyRank(t *testing.T) { ps := lineup(5) ps[2].status = ResultStatusWithdrawn entries, results := fixture(ps) es := mustScore(t, EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, }) assertEntryPoints(t, pointsByEntry(es.Ledger), []float64{9, 7, 0, 6, 5}) if _, ok := rankByEntry(es.Ranked)[3]; ok { t.Fatalf("弃权者不应出现在名次明细中: %v", rankByEntry(es.Ranked)) } } // case 11:报名不足 8 人 → 按实际名次取分。 func TestMatrix11_FewerThanAwardRanks(t *testing.T) { entries, results := fixture(lineup(5)) es := mustScore(t, EventScoreInput{ Event: individualEvent(1), Category: testCategory(), Channels: []ScoreChannel{athleteResultChannel(3)}, Profiles: map[int64]ScoringProfile{3: nineProfile(TieSkip)}, Entries: entries, Results: results, }) assertEntryPoints(t, pointsByEntry(es.Ledger), []float64{9, 7, 6, 5, 4}) } // case 12:人工扣分后排名变化 → tie-break 正确重排。 func TestMatrix12_ManualPenaltyReorders(t *testing.T) { ledger := []LedgerEntry{ledgerFor(1, 10), ledgerFor(2, 10)} in := StandingsInput{Teams: testTeams(1, 2), Ledger: ledger} before := BuildTeamStandings(in) if before[0].TeamID != 1 { t.Fatalf("扣分前榜首 = 单位 %d,期望 1(同分时编码小者在前)", before[0].TeamID) } // 人工扣分:单位1 -3 分(本期入口关闭,这里直接构造调整流水验证榜单口径)。 in.Ledger = append(in.Ledger, LedgerEntry{ TeamID: 1, SourceType: SourceAdjustment, SourceID: 1, Points: -3, CountToTeam: true, }) after := BuildTeamStandings(in) if after[0].TeamID != 2 { t.Fatalf("扣分后榜首 = 单位 %d,期望 2", after[0].TeamID) } if !nearlyEqual(after[0].Points, 10) || !nearlyEqual(after[1].Points, 7) { t.Fatalf("扣分后总分 = %v/%v,期望 10/7", after[0].Points, after[1].Points) } } // case 13:同分 tie-break → 金牌 → 银牌 → 铜牌 → 编码。 func TestMatrix13_TieBreakOrder(t *testing.T) { cases := []struct { name string ranked []RankedResult want int64 }{ { name: "金牌优先", ranked: []RankedResult{mkRanked(2, ScopeAthlete, 1)}, want: 2, }, { name: "银牌优先", ranked: []RankedResult{ mkRanked(1, ScopeAthlete, 1), mkRanked(2, ScopeAthlete, 1), mkRanked(2, ScopeAthlete, 2), }, want: 2, }, { name: "铜牌优先", ranked: []RankedResult{ mkRanked(1, ScopeAthlete, 3), mkRanked(2, ScopeAthlete, 3), mkRanked(1, ScopeAthlete, 3), }, want: 1, }, { name: "编码兜底", ranked: []RankedResult{}, want: 1, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rows := BuildTeamStandings(StandingsInput{ Teams: testTeams(1, 2), Ledger: []LedgerEntry{ledgerFor(1, 10), ledgerFor(2, 10)}, Ranked: tc.ranked, }) if rows[0].TeamID != tc.want { t.Fatalf("榜首 = 单位 %d,期望 %d(%+v)", rows[0].TeamID, tc.want, rows) } }) } } // case 14:破纪录多级别 → 取最高档(max 模式)。 func TestMatrix14_RecordBonusMax(t *testing.T) { cfg := RecordBonusConfig{ Enabled: true, Mode: RecordBonusMax, Levels: []RecordLevel{ {Code: "meet", Points: 2}, {Code: "city", Points: 5}, {Code: "province", Points: 8}, }, } breaks := []RecordBreak{ {ID: 1, EntryID: 7, EventID: 1, Level: "meet", State: RecordBreakApproved}, {ID: 2, EntryID: 7, EventID: 1, Level: "city", State: RecordBreakApproved}, {ID: 3, EntryID: 7, EventID: 1, Level: "province", State: RecordBreakApproved}, } got := RecordBonusPoints(cfg, breaks) if len(got) != 1 || !nearlyEqual(got[0].Points, 8) { t.Fatalf("max 模式 = %+v,期望 8 分一条", got) } if len(got[0].Levels) != 3 { t.Errorf("档位记录 = %v,期望记录全部 3 个档位", got[0].Levels) } } // case 15:破纪录多级别(sum 模式)→ 累加。 func TestMatrix15_RecordBonusSum(t *testing.T) { cfg := RecordBonusConfig{ Enabled: true, Mode: RecordBonusSum, Levels: []RecordLevel{ {Code: "meet", Points: 2}, {Code: "city", Points: 5}, {Code: "province", Points: 8}, }, } breaks := []RecordBreak{ {ID: 1, EntryID: 7, EventID: 1, Level: "meet", State: RecordBreakApproved}, {ID: 2, EntryID: 7, EventID: 1, Level: "city", State: RecordBreakApproved}, {ID: 3, EntryID: 7, EventID: 1, Level: "province", State: RecordBreakApproved}, } got := RecordBonusPoints(cfg, breaks) if len(got) != 1 || !nearlyEqual(got[0].Points, 15) { t.Fatalf("sum 模式 = %+v,期望 15 分一条", got) } }