Files
git-velocity/internal/domain/scoring/calculator_test.go
T

681 lines
18 KiB
Go

package scoring
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/git-velocity/internal/config"
"github.com/lukaszraczylo/git-velocity/internal/domain/models"
)
func TestNewCalculator(t *testing.T) {
t.Parallel()
cfg := &config.Config{}
calc := NewCalculator(cfg)
assert.NotNil(t, calc)
assert.Equal(t, cfg, calc.config)
}
func TestCalculator_ScoringDisabled(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = false
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 100},
},
},
},
}
result := calc.Calculate(metrics)
// Should return unchanged metrics when scoring is disabled
assert.Equal(t, 0, result.Repositories[0].Contributors[0].Score.Total)
assert.Empty(t, result.Leaderboard)
}
func TestCalculator_BasicScoring(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{
Commit: 10,
PROpened: 25,
PRMerged: 50,
PRReviewed: 30,
ReviewComment: 5,
LinesAdded: 0.1,
LinesDeleted: 0.05,
}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "user1",
Name: "User One",
CommitCount: 10,
LinesAdded: 1000,
LinesDeleted: 500,
PRsOpened: 5,
PRsMerged: 3,
ReviewsGiven: 8,
ReviewComments: 20,
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
require.Len(t, result.Leaderboard, 1)
entry := result.Leaderboard[0]
assert.Equal(t, "user1", entry.Login)
assert.Equal(t, 1, entry.Rank)
// Verify score breakdown:
// Commits: 10 * 10 = 100
// Lines: 1000 * 0.1 + 500 * 0.05 = 100 + 25 = 125
// PRs: 5 * 25 + 3 * 50 = 125 + 150 = 275
// Reviews: 8 * 30 + 20 * 5 = 240 + 100 = 340
// Total: 100 + 125 + 275 + 340 = 840
assert.Equal(t, 840, entry.Score)
}
func TestCalculator_FastReviewBonus(t *testing.T) {
t.Parallel()
tests := []struct {
name string
avgReviewTime float64
expectedBonus int
expectedPoints config.PointsConfig
}{
{
name: "1 hour review gets 1h bonus",
avgReviewTime: 0.5,
expectedBonus: 50,
expectedPoints: config.PointsConfig{
FastReview1h: 50,
FastReview4h: 30,
FastReview24h: 10,
},
},
{
name: "3 hour review gets 4h bonus",
avgReviewTime: 3.0,
expectedBonus: 30,
expectedPoints: config.PointsConfig{
FastReview1h: 50,
FastReview4h: 30,
FastReview24h: 10,
},
},
{
name: "12 hour review gets 24h bonus",
avgReviewTime: 12.0,
expectedBonus: 10,
expectedPoints: config.PointsConfig{
FastReview1h: 50,
FastReview4h: 30,
FastReview24h: 10,
},
},
{
name: "48 hour review gets no bonus",
avgReviewTime: 48.0,
expectedBonus: 0,
expectedPoints: config.PointsConfig{
FastReview1h: 50,
FastReview4h: 30,
FastReview24h: 10,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = tt.expectedPoints
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "user1",
ReviewsGiven: 5,
AvgReviewTime: tt.avgReviewTime,
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
require.Len(t, result.Leaderboard, 1)
// Get the contributor from the repository to check breakdown
contributor := result.Repositories[0].Contributors[0]
assert.Equal(t, tt.expectedBonus, contributor.Score.Breakdown.ResponseBonus)
})
}
}
func TestCalculator_MultipleContributorsRanking(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{
Commit: 10,
}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "user1",
CommitCount: 100,
RepositoriesContributed: []string{"owner/repo"},
},
{
Login: "user2",
CommitCount: 50,
RepositoriesContributed: []string{"owner/repo"},
},
{
Login: "user3",
CommitCount: 200,
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
require.Len(t, result.Leaderboard, 3)
// Should be sorted by score (highest first)
assert.Equal(t, "user3", result.Leaderboard[0].Login)
assert.Equal(t, 1, result.Leaderboard[0].Rank)
assert.Equal(t, 2000, result.Leaderboard[0].Score)
assert.Equal(t, "user1", result.Leaderboard[1].Login)
assert.Equal(t, 2, result.Leaderboard[1].Rank)
assert.Equal(t, 1000, result.Leaderboard[1].Score)
assert.Equal(t, "user2", result.Leaderboard[2].Login)
assert.Equal(t, 3, result.Leaderboard[2].Rank)
assert.Equal(t, 500, result.Leaderboard[2].Score)
}
func TestCalculator_PercentileRank(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{Commit: 10}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 100, RepositoriesContributed: []string{"owner/repo"}},
{Login: "user2", CommitCount: 80, RepositoriesContributed: []string{"owner/repo"}},
{Login: "user3", CommitCount: 60, RepositoriesContributed: []string{"owner/repo"}},
{Login: "user4", CommitCount: 40, RepositoriesContributed: []string{"owner/repo"}},
},
},
},
}
result := calc.Calculate(metrics)
require.Len(t, result.Leaderboard, 4)
// Leaderboard should be sorted by score (highest first)
// user1: 100 commits * 10 = 1000, rank 1
// user2: 80 commits * 10 = 800, rank 2
// user3: 60 commits * 10 = 600, rank 3
// user4: 40 commits * 10 = 400, rank 4
assert.Equal(t, "user1", result.Leaderboard[0].Login)
assert.Equal(t, 1, result.Leaderboard[0].Rank)
assert.Equal(t, 1000, result.Leaderboard[0].Score)
assert.Equal(t, "user2", result.Leaderboard[1].Login)
assert.Equal(t, 2, result.Leaderboard[1].Rank)
assert.Equal(t, 800, result.Leaderboard[1].Score)
assert.Equal(t, "user3", result.Leaderboard[2].Login)
assert.Equal(t, 3, result.Leaderboard[2].Rank)
assert.Equal(t, 600, result.Leaderboard[2].Score)
assert.Equal(t, "user4", result.Leaderboard[3].Login)
assert.Equal(t, 4, result.Leaderboard[3].Rank)
assert.Equal(t, 400, result.Leaderboard[3].Score)
}
func TestCalculator_Achievements(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
// Achievements are now hardcoded, no need to set them
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "user1",
CommitCount: 15, // Should earn commit-1, commit-10
PRsOpened: 6, // Should earn pr-1
ReviewsGiven: 5, // Should earn review-1
AvgReviewTime: 0.5, // Should earn review-time-1h, review-time-4h, review-time-24h
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
contributor := result.Repositories[0].Contributors[0]
// Should have hardcoded achievements based on thresholds
assert.Contains(t, contributor.Achievements, "commit-1")
assert.Contains(t, contributor.Achievements, "commit-10")
assert.Contains(t, contributor.Achievements, "pr-1")
assert.Contains(t, contributor.Achievements, "review-1")
assert.Contains(t, contributor.Achievements, "review-time-1h") // 0.5h < 1h threshold
// Should NOT have commit-50 (only 15 commits)
assert.NotContains(t, contributor.Achievements, "commit-50")
// Should NOT have review-10 (only 5 reviews)
assert.NotContains(t, contributor.Achievements, "review-10")
}
func TestCalculator_AllAchievementTypes(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
// Achievements are now hardcoded
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo1",
Contributors: []models.ContributorMetrics{
{
Login: "user1",
CommitCount: 15,
PRsOpened: 6,
PRsMerged: 4,
ReviewsGiven: 10,
ReviewComments: 25,
LinesAdded: 1500,
LinesDeleted: 600,
AvgReviewTime: 1.5,
UniqueReviewees: 7,
RepositoriesContributed: []string{"owner/repo1", "owner/repo2"},
},
},
},
},
}
result := calc.Calculate(metrics)
contributor := result.Repositories[0].Contributors[0]
// Should have various hardcoded achievements based on thresholds
// Check some key achievements are earned
assert.Contains(t, contributor.Achievements, "commit-1")
assert.Contains(t, contributor.Achievements, "commit-10")
assert.Contains(t, contributor.Achievements, "pr-1")
assert.Contains(t, contributor.Achievements, "review-1")
assert.Contains(t, contributor.Achievements, "review-10")
assert.Contains(t, contributor.Achievements, "comment-10")
assert.Contains(t, contributor.Achievements, "lines-added-100")
assert.Contains(t, contributor.Achievements, "lines-added-1000")
assert.Contains(t, contributor.Achievements, "lines-deleted-100")
assert.Contains(t, contributor.Achievements, "lines-deleted-500")
assert.Contains(t, contributor.Achievements, "review-time-4h") // 1.5h < 4h
assert.Contains(t, contributor.Achievements, "repo-2") // 2 repos
assert.Contains(t, contributor.Achievements, "reviewees-3") // 7 reviewees >= 3
// Should have earned multiple achievements (more than 10)
assert.Greater(t, len(contributor.Achievements), 10)
}
func TestCalculator_TopAchievers(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{
Commit: 10,
PROpened: 25,
PRReviewed: 30,
}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "committer",
CommitCount: 100,
PRsOpened: 5,
ReviewsGiven: 2,
RepositoriesContributed: []string{"owner/repo"},
},
{
Login: "pr-author",
CommitCount: 10,
PRsOpened: 50,
ReviewsGiven: 3,
RepositoriesContributed: []string{"owner/repo"},
},
{
Login: "reviewer",
CommitCount: 5,
PRsOpened: 2,
ReviewsGiven: 100,
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
assert.Equal(t, "committer", result.TopAchievers["commits"])
assert.Equal(t, "pr-author", result.TopAchievers["pull_requests"])
assert.Equal(t, "reviewer", result.TopAchievers["reviews"])
// Overall top achiever has highest score
assert.NotEmpty(t, result.TopAchievers["overall"])
}
func TestCalculator_TeamScoring(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{Commit: 10}
cfg.Teams = []config.TeamConfig{
{
Name: "Backend Team",
Members: []string{"user1", "user2"},
Color: "#ff0000",
},
}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 50, RepositoriesContributed: []string{"owner/repo"}},
{Login: "user2", CommitCount: 30, RepositoriesContributed: []string{"owner/repo"}},
},
},
},
Teams: []models.TeamMetrics{
{
Name: "Backend Team",
Members: []string{"user1", "user2"},
MemberMetrics: []models.ContributorMetrics{
{Login: "user1"},
{Login: "user2"},
},
},
},
}
result := calc.Calculate(metrics)
require.Len(t, result.Teams, 1)
team := result.Teams[0]
// Total: 500 + 300 = 800
assert.Equal(t, 800, team.TotalScore)
// Avg: 800 / 2 = 400
assert.Equal(t, 400.0, team.AvgScore)
// Check individual member scores
assert.Equal(t, 500, team.MemberMetrics[0].Score.Total)
assert.Equal(t, 300, team.MemberMetrics[1].Score.Total)
}
func TestCalculator_TeamInLeaderboard(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{Commit: 10}
cfg.Teams = []config.TeamConfig{
{
Name: "Backend Team",
Members: []string{"user1"},
},
}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 50, RepositoriesContributed: []string{"owner/repo"}},
{Login: "user2", CommitCount: 30, RepositoriesContributed: []string{"owner/repo"}},
},
},
},
}
result := calc.Calculate(metrics)
// user1 should have team name in leaderboard
assert.Equal(t, "Backend Team", result.Leaderboard[0].Team)
// user2 should not have a team
assert.Empty(t, result.Leaderboard[1].Team)
}
func TestCalculator_DetermineTopCategory(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
calc := NewCalculator(cfg)
tests := []struct {
name string
contributor models.ContributorMetrics
expectedCategory string
}{
{
name: "Top committer",
contributor: models.ContributorMetrics{
CommitCount: 100,
PRsOpened: 10,
ReviewsGiven: 5,
ReviewComments: 20,
},
expectedCategory: "Commits",
},
{
name: "Top PR author",
contributor: models.ContributorMetrics{
CommitCount: 10,
PRsOpened: 100,
ReviewsGiven: 5,
ReviewComments: 20,
},
expectedCategory: "PRs",
},
{
name: "Top reviewer",
contributor: models.ContributorMetrics{
CommitCount: 10,
PRsOpened: 5,
ReviewsGiven: 100,
ReviewComments: 20,
},
expectedCategory: "Reviews",
},
{
name: "Top commenter",
contributor: models.ContributorMetrics{
CommitCount: 10,
PRsOpened: 5,
ReviewsGiven: 20,
ReviewComments: 100,
},
expectedCategory: "Comments",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := calc.determineTopCategory(&tt.contributor)
assert.Equal(t, tt.expectedCategory, result)
})
}
}
func TestCalculator_MultipleRepositories(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{Commit: 10}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo1",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 50, RepositoriesContributed: []string{"owner/repo1"}},
},
},
{
FullName: "owner/repo2",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 30, RepositoriesContributed: []string{"owner/repo2"}},
},
},
},
}
result := calc.Calculate(metrics)
// Should aggregate commits from both repos
require.Len(t, result.Leaderboard, 1)
// 50 + 30 = 80 commits * 10 = 800
assert.Equal(t, 800, result.Leaderboard[0].Score)
// Per-repo scores should reflect repo-specific metrics, not global
// Repo1 has 50 commits * 10 = 500
contributor := result.Repositories[0].Contributors[0]
assert.Equal(t, 500, contributor.Score.Total)
}
func TestCalculator_EmptyMetrics(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{},
}
result := calc.Calculate(metrics)
assert.Empty(t, result.Leaderboard)
assert.Empty(t, result.TopAchievers)
}
func TestCalculator_NoReviewsNoBonus(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{
FastReview1h: 50,
}
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "user1",
ReviewsGiven: 0,
AvgReviewTime: 0.5, // Fast but no reviews
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
contributor := result.Repositories[0].Contributors[0]
// Should not get bonus if no reviews given
assert.Equal(t, 0, contributor.Score.Breakdown.ResponseBonus)
}
func TestContains(t *testing.T) {
t.Parallel()
slice := []string{"a", "b", "c"}
assert.True(t, contains(slice, "a"))
assert.True(t, contains(slice, "b"))
assert.True(t, contains(slice, "c"))
assert.False(t, contains(slice, "d"))
assert.False(t, contains([]string{}, "a"))
}