mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-06-05 22:43:56 +00:00
3bd9807e50
Git Level (per commit):
- Track unique file paths in FilesModified slice
- FilesChanged = count of unique files in THIS commit
Aggregator Level (per contributor):
- Collect all file paths from all commits into a SET
- FilesChanged = size of the unique file set
Result:
- Contributor.FilesChanged = count of UNIQUE files they touched
- Repository contributor = unique files in THAT repo only
1384 lines
40 KiB
Go
1384 lines
40 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,
|
|
MeaningfulLinesAdded: 1000, // Same as raw for this test
|
|
MeaningfulLinesDeleted: 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 (meaningful): 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_GlobalContributorsPopulatedWithScores(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)
|
|
|
|
// Contributor appears in multiple repos with different stats
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo1",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "alice",
|
|
Name: "Alice",
|
|
CommitCount: 50,
|
|
PRsOpened: 5,
|
|
PRsMerged: 3,
|
|
},
|
|
{
|
|
Login: "bob",
|
|
Name: "Bob",
|
|
CommitCount: 20,
|
|
PRsOpened: 2,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
FullName: "owner/repo2",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "alice",
|
|
Name: "Alice",
|
|
CommitCount: 30, // Additional commits in second repo
|
|
PRsOpened: 3,
|
|
PRsMerged: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
// Verify metrics.Contributors is populated
|
|
require.NotEmpty(t, result.Contributors, "metrics.Contributors should be populated")
|
|
require.Len(t, result.Contributors, 2, "Should have 2 unique contributors")
|
|
|
|
// Find alice in Contributors
|
|
var alice *models.ContributorMetrics
|
|
for i := range result.Contributors {
|
|
if result.Contributors[i].Login == "alice" {
|
|
alice = &result.Contributors[i]
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, alice, "Alice should be in Contributors")
|
|
|
|
// Verify alice has AGGREGATED stats
|
|
assert.Equal(t, 80, alice.CommitCount, "Alice should have aggregated commits (50+30)")
|
|
assert.Equal(t, 8, alice.PRsOpened, "Alice should have aggregated PRs opened (5+3)")
|
|
assert.Equal(t, 5, alice.PRsMerged, "Alice should have aggregated PRs merged (3+2)")
|
|
|
|
// Verify alice has a calculated score with breakdown
|
|
assert.Greater(t, alice.Score.Total, 0, "Alice should have a calculated score")
|
|
assert.Greater(t, alice.Score.Breakdown.Commits, 0, "Score breakdown should have commits")
|
|
assert.Greater(t, alice.Score.Breakdown.PRs, 0, "Score breakdown should have PRs")
|
|
|
|
// Verify score calculation:
|
|
// Commits: 80 * 10 = 800
|
|
// PRs: 8 * 25 + 5 * 50 = 200 + 250 = 450
|
|
// Total: 800 + 450 = 1250
|
|
assert.Equal(t, 800, alice.Score.Breakdown.Commits, "Commit points should be 80 * 10 = 800")
|
|
assert.Equal(t, 450, alice.Score.Breakdown.PRs, "PR points should be 8*25 + 5*50 = 450")
|
|
assert.Equal(t, 1250, alice.Score.Total, "Total score should be 1250")
|
|
|
|
// Verify rank is assigned
|
|
assert.Equal(t, 1, alice.Score.Rank, "Alice should be rank 1 (highest scorer)")
|
|
|
|
// Verify bob also has scores
|
|
var bob *models.ContributorMetrics
|
|
for i := range result.Contributors {
|
|
if result.Contributors[i].Login == "bob" {
|
|
bob = &result.Contributors[i]
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, bob, "Bob should be in Contributors")
|
|
assert.Greater(t, bob.Score.Total, 0, "Bob should have a calculated score")
|
|
assert.Equal(t, 2, bob.Score.Rank, "Bob should be rank 2")
|
|
}
|
|
|
|
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,
|
|
MeaningfulLinesAdded: 1500,
|
|
MeaningfulLinesDeleted: 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 TestCalculator_OutOfHoursScoring(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
cfg.Scoring.Points = config.PointsConfig{
|
|
Commit: 10,
|
|
OutOfHours: 5, // 5 points per out-of-hours commit
|
|
}
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "night-owl",
|
|
CommitCount: 10,
|
|
OutOfHoursCount: 8, // 8 commits outside 9am-5pm
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Commits: 10 * 10 = 100
|
|
// OutOfHours: 8 * 5 = 40
|
|
// Total: 140
|
|
assert.Equal(t, 100, contributor.Score.Breakdown.Commits)
|
|
assert.Equal(t, 40, contributor.Score.Breakdown.OutOfHours)
|
|
assert.Equal(t, 140, contributor.Score.Total)
|
|
}
|
|
|
|
func TestCalculator_WorkWeekStreakAchievement(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/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "consistent-worker",
|
|
CommitCount: 20,
|
|
WorkWeekStreak: 5, // 5-day work week streak
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Should have earned work week streak achievements for 3 and 5 days
|
|
assert.Contains(t, contributor.Achievements, "workweek-3")
|
|
assert.Contains(t, contributor.Achievements, "workweek-5")
|
|
}
|
|
|
|
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"))
|
|
}
|
|
|
|
func TestCalculator_MeaningfulLinesScoring(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("uses meaningful lines when enabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
cfg.Scoring.Points = config.PointsConfig{
|
|
Commit: 10,
|
|
LinesAdded: 0.1,
|
|
LinesDeleted: 0.05,
|
|
}
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "user1",
|
|
CommitCount: 10,
|
|
LinesAdded: 1000, // Raw lines
|
|
LinesDeleted: 500,
|
|
MeaningfulLinesAdded: 800, // Meaningful lines (excluding comments/whitespace)
|
|
MeaningfulLinesDeleted: 400,
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Line change points should use meaningful lines:
|
|
// Meaningful: 800 * 0.1 + 400 * 0.05 = 80 + 20 = 100
|
|
// (Not raw: 1000 * 0.1 + 500 * 0.05 = 100 + 25 = 125)
|
|
assert.Equal(t, 100, contributor.Score.Breakdown.LineChanges)
|
|
// Total: Commits (10 * 10 = 100) + Lines (100) = 200
|
|
assert.Equal(t, 200, contributor.Score.Total)
|
|
})
|
|
|
|
t.Run("comment-only changes score zero meaningful lines", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
cfg.Scoring.Points = config.PointsConfig{
|
|
Commit: 10,
|
|
LinesAdded: 0.1,
|
|
LinesDeleted: 0.05,
|
|
}
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "commenter",
|
|
CommitCount: 5,
|
|
LinesAdded: 100, // All comment lines
|
|
LinesDeleted: 50,
|
|
MeaningfulLinesAdded: 0, // No meaningful code
|
|
MeaningfulLinesDeleted: 0,
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Line change points should be 0 since all lines were comments
|
|
assert.Equal(t, 0, contributor.Score.Breakdown.LineChanges)
|
|
// Total: Commits (5 * 10 = 50) + Lines (0) = 50
|
|
assert.Equal(t, 50, contributor.Score.Total)
|
|
})
|
|
}
|
|
|
|
func TestCalculator_CommentLinesAchievements(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("earns documentation achievements for adding comments", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "documenter",
|
|
CommitCount: 10,
|
|
CommentLinesAdded: 1500, // Should earn docs-100, docs-500, docs-1000
|
|
CommentLinesDeleted: 100, // Should earn docs-del-50
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Should have documentation achievements
|
|
assert.Contains(t, contributor.Achievements, "docs-100", "Should earn docs-100 for 100+ comment lines")
|
|
assert.Contains(t, contributor.Achievements, "docs-500", "Should earn docs-500 for 500+ comment lines")
|
|
assert.Contains(t, contributor.Achievements, "docs-1000", "Should earn docs-1000 for 1000+ comment lines")
|
|
assert.NotContains(t, contributor.Achievements, "docs-2500", "Should not earn docs-2500 for <2500 comment lines")
|
|
// Should have comment cleanup achievement
|
|
assert.Contains(t, contributor.Achievements, "docs-del-50", "Should earn docs-del-50 for 50+ comment deletions")
|
|
assert.NotContains(t, contributor.Achievements, "docs-del-200", "Should not earn docs-del-200 for <200 deletions")
|
|
})
|
|
|
|
t.Run("earns all documentation deletion achievements", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "cleanup-expert",
|
|
CommitCount: 50,
|
|
CommentLinesAdded: 100,
|
|
CommentLinesDeleted: 3000, // Should earn all deletion tiers
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Should have all comment cleanup achievements
|
|
assert.Contains(t, contributor.Achievements, "docs-del-50")
|
|
assert.Contains(t, contributor.Achievements, "docs-del-200")
|
|
assert.Contains(t, contributor.Achievements, "docs-del-500")
|
|
assert.Contains(t, contributor.Achievements, "docs-del-1000")
|
|
assert.Contains(t, contributor.Achievements, "docs-del-2500")
|
|
})
|
|
|
|
t.Run("aggregates comment lines across multiple repositories", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo1",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "multi-repo-doc",
|
|
CommitCount: 5,
|
|
CommentLinesAdded: 300,
|
|
CommentLinesDeleted: 30,
|
|
RepositoriesContributed: []string{"owner/repo1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
FullName: "owner/repo2",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "multi-repo-doc",
|
|
CommitCount: 5,
|
|
CommentLinesAdded: 300,
|
|
CommentLinesDeleted: 30,
|
|
RepositoriesContributed: []string{"owner/repo2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
// Check leaderboard entry (aggregated)
|
|
require.Len(t, result.Leaderboard, 1)
|
|
entry := result.Leaderboard[0]
|
|
// Aggregated: 300 + 300 = 600 comment lines added, 30 + 30 = 60 deleted
|
|
assert.Contains(t, entry.Achievements, "docs-100")
|
|
assert.Contains(t, entry.Achievements, "docs-500")
|
|
assert.NotContains(t, entry.Achievements, "docs-1000", "600 < 1000")
|
|
assert.Contains(t, entry.Achievements, "docs-del-50")
|
|
assert.NotContains(t, entry.Achievements, "docs-del-200", "60 < 200")
|
|
})
|
|
}
|
|
|
|
func TestCalculator_IssueScoring(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("calculates issue points correctly", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
cfg.Scoring.Points = config.PointsConfig{
|
|
Commit: 10,
|
|
IssueOpened: 10, // 10 points per issue opened
|
|
IssueClosed: 20, // 20 points per issue closed
|
|
IssueComment: 5, // 5 points per issue comment
|
|
IssueReference: 5, // 5 points per issue reference in commit
|
|
}
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "issue-worker",
|
|
CommitCount: 10,
|
|
IssuesOpened: 5, // 5 * 10 = 50
|
|
IssuesClosed: 3, // 3 * 20 = 60
|
|
IssueComments: 10, // 10 * 5 = 50
|
|
IssueReferencesInCommits: 8, // 8 * 5 = 40
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Issue points: 50 + 60 + 50 + 40 = 200
|
|
assert.Equal(t, 200, contributor.Score.Breakdown.Issues)
|
|
// Commits: 10 * 10 = 100
|
|
// Total: 100 + 200 = 300
|
|
assert.Equal(t, 300, contributor.Score.Total)
|
|
})
|
|
|
|
t.Run("aggregates issue metrics across repositories", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
cfg.Scoring.Points = config.PointsConfig{
|
|
IssueOpened: 10,
|
|
IssueClosed: 20,
|
|
IssueComment: 5,
|
|
IssueReference: 5,
|
|
}
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo1",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "issue-worker",
|
|
IssuesOpened: 3,
|
|
IssuesClosed: 2,
|
|
IssueComments: 5,
|
|
IssueReferencesInCommits: 4,
|
|
RepositoriesContributed: []string{"owner/repo1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
FullName: "owner/repo2",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "issue-worker",
|
|
IssuesOpened: 2,
|
|
IssuesClosed: 1,
|
|
IssueComments: 3,
|
|
IssueReferencesInCommits: 2,
|
|
RepositoriesContributed: []string{"owner/repo2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
require.Len(t, result.Leaderboard, 1)
|
|
// Aggregated: 5 opened, 3 closed, 8 comments, 6 references
|
|
// Points: 5*10 + 3*20 + 8*5 + 6*5 = 50 + 60 + 40 + 30 = 180
|
|
assert.Equal(t, 180, result.Leaderboard[0].Score)
|
|
})
|
|
|
|
t.Run("zero issue metrics results in zero issue points", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
cfg.Scoring.Points = config.PointsConfig{
|
|
Commit: 10,
|
|
IssueOpened: 10,
|
|
IssueClosed: 20,
|
|
IssueComment: 5,
|
|
IssueReference: 5,
|
|
}
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "code-only",
|
|
CommitCount: 20,
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
assert.Equal(t, 0, contributor.Score.Breakdown.Issues)
|
|
// Only commits: 20 * 10 = 200
|
|
assert.Equal(t, 200, contributor.Score.Total)
|
|
})
|
|
}
|
|
|
|
func TestCalculator_IssueAchievements(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("earns issue opened achievements", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "bug-hunter",
|
|
IssuesOpened: 12, // Should earn issue-1, issue-5, issue-10
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
assert.Contains(t, contributor.Achievements, "issue-1", "Should earn issue-1 for 1+ issues opened")
|
|
assert.Contains(t, contributor.Achievements, "issue-5", "Should earn issue-5 for 5+ issues opened")
|
|
assert.Contains(t, contributor.Achievements, "issue-10", "Should earn issue-10 for 10+ issues opened")
|
|
assert.NotContains(t, contributor.Achievements, "issue-25", "Should not earn issue-25 for <25 issues")
|
|
})
|
|
|
|
t.Run("earns issue closed achievements", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "problem-solver",
|
|
IssuesClosed: 8, // Should earn issue-close-1, issue-close-5
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
assert.Contains(t, contributor.Achievements, "issue-close-1", "Should earn issue-close-1 for 1+ issues closed")
|
|
assert.Contains(t, contributor.Achievements, "issue-close-5", "Should earn issue-close-5 for 5+ issues closed")
|
|
assert.NotContains(t, contributor.Achievements, "issue-close-10", "Should not earn issue-close-10 for <10 issues")
|
|
})
|
|
|
|
t.Run("earns issue comment achievements", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "discusser",
|
|
IssueComments: 30, // Should earn issue-comment-5, issue-comment-10, issue-comment-25
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-5", "Should earn issue-comment-5 for 5+ comments")
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-10", "Should earn issue-comment-10 for 10+ comments")
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-25", "Should earn issue-comment-25 for 25+ comments")
|
|
assert.NotContains(t, contributor.Achievements, "issue-comment-50", "Should not earn issue-comment-50 for <50 comments")
|
|
})
|
|
|
|
t.Run("earns issue reference achievements", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "linker",
|
|
IssueReferencesInCommits: 15, // Should earn issue-ref-5, issue-ref-10
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
assert.Contains(t, contributor.Achievements, "issue-ref-5", "Should earn issue-ref-5 for 5+ references")
|
|
assert.Contains(t, contributor.Achievements, "issue-ref-10", "Should earn issue-ref-10 for 10+ references")
|
|
assert.NotContains(t, contributor.Achievements, "issue-ref-25", "Should not earn issue-ref-25 for <25 references")
|
|
})
|
|
|
|
t.Run("earns all issue achievement tiers", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Scoring.Enabled = true
|
|
calc := NewCalculator(cfg)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
FullName: "owner/repo",
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "super-issue-worker",
|
|
IssuesOpened: 100,
|
|
IssuesClosed: 100,
|
|
IssueComments: 150,
|
|
IssueReferencesInCommits: 150,
|
|
RepositoriesContributed: []string{"owner/repo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := calc.Calculate(metrics)
|
|
|
|
contributor := result.Repositories[0].Contributors[0]
|
|
// Should have all issue opened achievements
|
|
assert.Contains(t, contributor.Achievements, "issue-1")
|
|
assert.Contains(t, contributor.Achievements, "issue-5")
|
|
assert.Contains(t, contributor.Achievements, "issue-10")
|
|
assert.Contains(t, contributor.Achievements, "issue-25")
|
|
assert.Contains(t, contributor.Achievements, "issue-50")
|
|
// Should have all issue closed achievements
|
|
assert.Contains(t, contributor.Achievements, "issue-close-1")
|
|
assert.Contains(t, contributor.Achievements, "issue-close-5")
|
|
assert.Contains(t, contributor.Achievements, "issue-close-10")
|
|
assert.Contains(t, contributor.Achievements, "issue-close-25")
|
|
assert.Contains(t, contributor.Achievements, "issue-close-50")
|
|
// Should have all issue comment achievements
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-5")
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-10")
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-25")
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-50")
|
|
assert.Contains(t, contributor.Achievements, "issue-comment-100")
|
|
// Should have all issue reference achievements
|
|
assert.Contains(t, contributor.Achievements, "issue-ref-5")
|
|
assert.Contains(t, contributor.Achievements, "issue-ref-10")
|
|
assert.Contains(t, contributor.Achievements, "issue-ref-25")
|
|
assert.Contains(t, contributor.Achievements, "issue-ref-50")
|
|
assert.Contains(t, contributor.Achievements, "issue-ref-100")
|
|
})
|
|
}
|