Files
git-velocity/internal/generator/site/generator_test.go
T

603 lines
17 KiB
Go

package site
import (
"os"
"path/filepath"
"testing"
"time"
json "github.com/goccy/go-json"
"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 TestNewGenerator(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
gen, err := NewGenerator("/tmp/output", cfg)
require.NoError(t, err)
assert.NotNil(t, gen)
assert.Equal(t, "/tmp/output", gen.outputDir)
assert.Equal(t, cfg, gen.config)
}
func TestGenerator_GenerateCreatesOutputDir(t *testing.T) {
tempDir := t.TempDir()
outputDir := filepath.Join(tempDir, "new-output")
cfg := config.DefaultConfig()
gen, err := NewGenerator(outputDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{
Period: models.Period{
Start: time.Now().Add(-24 * time.Hour),
End: time.Now(),
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Verify output directory was created
info, err := os.Stat(outputDir)
require.NoError(t, err)
assert.True(t, info.IsDir())
}
func TestGenerator_GenerateCreatesDataDir(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{}
err = gen.Generate(metrics)
require.NoError(t, err)
// Verify data directory was created
dataDir := filepath.Join(tempDir, "data")
info, err := os.Stat(dataDir)
require.NoError(t, err)
assert.True(t, info.IsDir())
}
func TestGenerator_GenerateGlobalJSON(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{
TotalContributors: 5,
TotalCommits: 100,
TotalPRs: 50,
TotalReviews: 75,
TotalLinesAdded: 10000,
TotalLinesDeleted: 5000,
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Read and verify global.json
globalPath := filepath.Join(tempDir, "data", "global.json")
data, err := os.ReadFile(globalPath)
require.NoError(t, err)
var result struct {
TotalContributors int `json:"total_contributors"`
TotalCommits int `json:"total_commits"`
TotalPRs int `json:"total_prs"`
TotalReviews int `json:"total_reviews"`
TotalLinesAdded int `json:"total_lines_added"`
TotalLinesDeleted int `json:"total_lines_deleted"`
GeneratedAt time.Time `json:"generated_at"`
}
err = json.Unmarshal(data, &result)
require.NoError(t, err)
assert.Equal(t, 5, result.TotalContributors)
assert.Equal(t, 100, result.TotalCommits)
assert.Equal(t, 50, result.TotalPRs)
assert.Equal(t, 75, result.TotalReviews)
assert.Equal(t, 10000, result.TotalLinesAdded)
assert.Equal(t, 5000, result.TotalLinesDeleted)
assert.False(t, result.GeneratedAt.IsZero())
}
func TestGenerator_GenerateLeaderboardJSON(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{
Leaderboard: []models.LeaderboardEntry{
{Rank: 1, Login: "user1", Score: 1000},
{Rank: 2, Login: "user2", Score: 800},
{Rank: 3, Login: "user3", Score: 600},
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Read and verify leaderboard.json
leaderboardPath := filepath.Join(tempDir, "data", "leaderboard.json")
data, err := os.ReadFile(leaderboardPath)
require.NoError(t, err)
var result []models.LeaderboardEntry
err = json.Unmarshal(data, &result)
require.NoError(t, err)
require.Len(t, result, 3)
assert.Equal(t, "user1", result[0].Login)
assert.Equal(t, 1000, result[0].Score)
assert.Equal(t, "user2", result[1].Login)
assert.Equal(t, 800, result[1].Score)
}
func TestGenerator_GenerateRepositoryJSON(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
Owner: "myorg",
Name: "myrepo",
TotalCommits: 42,
TotalPRs: 10,
},
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Read and verify repository metrics
repoPath := filepath.Join(tempDir, "data", "repos", "myorg", "myrepo", "metrics.json")
data, err := os.ReadFile(repoPath)
require.NoError(t, err)
var result models.RepositoryMetrics
err = json.Unmarshal(data, &result)
require.NoError(t, err)
assert.Equal(t, "myorg", result.Owner)
assert.Equal(t, "myrepo", result.Name)
assert.Equal(t, 42, result.TotalCommits)
assert.Equal(t, 10, result.TotalPRs)
}
func TestGenerator_GenerateMultipleRepositories(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{Owner: "org1", Name: "repo1", TotalCommits: 100},
{Owner: "org1", Name: "repo2", TotalCommits: 200},
{Owner: "org2", Name: "repo3", TotalCommits: 300},
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Verify all repository files exist
_, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org1", "repo1", "metrics.json"))
assert.NoError(t, err)
_, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org1", "repo2", "metrics.json"))
assert.NoError(t, err)
_, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org2", "repo3", "metrics.json"))
assert.NoError(t, err)
}
func TestGenerator_GenerateTeamJSON(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{
Teams: []models.TeamMetrics{
{
Name: "Backend Team",
Color: "#ff0000",
Members: []string{"user1", "user2"},
TotalScore: 1500,
},
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Read and verify team JSON (slugified name)
teamPath := filepath.Join(tempDir, "data", "teams", "backend-team.json")
data, err := os.ReadFile(teamPath)
require.NoError(t, err)
var result models.TeamMetrics
err = json.Unmarshal(data, &result)
require.NoError(t, err)
assert.Equal(t, "Backend Team", result.Name)
assert.Equal(t, "#ff0000", result.Color)
assert.Equal(t, 1500, result.TotalScore)
assert.Len(t, result.Members, 2)
}
func TestGenerator_GenerateContributorJSON(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
// Generator now uses global Contributors, not per-repo Contributors
metrics := &models.GlobalMetrics{
Contributors: []models.ContributorMetrics{
{
Login: "john-doe",
Name: "John Doe",
CommitCount: 50,
PRsOpened: 10,
},
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Read and verify contributor JSON
contributorPath := filepath.Join(tempDir, "data", "contributors", "john-doe.json")
data, err := os.ReadFile(contributorPath)
require.NoError(t, err)
var result models.ContributorMetrics
err = json.Unmarshal(data, &result)
require.NoError(t, err)
assert.Equal(t, "john-doe", result.Login)
assert.Equal(t, "John Doe", result.Name)
assert.Equal(t, 50, result.CommitCount)
assert.Equal(t, 10, result.PRsOpened)
}
func TestGenerator_UsesGlobalContributorsNotPerRepo(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
// Same contributor in multiple repos with different per-repo stats
// But GlobalMetrics.Contributors should have AGGREGATED stats
metrics := &models.GlobalMetrics{
// Per-repo data (used for repository-specific pages)
Repositories: []models.RepositoryMetrics{
{
Owner: "org",
Name: "repo1",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 50, PRsOpened: 5},
},
},
{
Owner: "org",
Name: "repo2",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 75, PRsOpened: 10}, // Same user, different count
},
},
},
// Global aggregated data (this is what the generator should use for contributor files)
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 125, PRsOpened: 15}, // Sum: 50+75=125, 5+10=15
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Contributor file should have AGGREGATED data from GlobalMetrics.Contributors
contributorPath := filepath.Join(tempDir, "data", "contributors", "user1.json")
data, err := os.ReadFile(contributorPath)
require.NoError(t, err)
var result models.ContributorMetrics
err = json.Unmarshal(data, &result)
require.NoError(t, err)
// Should be the aggregated count (125 commits, 15 PRs), NOT 50 or 75
assert.Equal(t, 125, result.CommitCount, "Should use aggregated commits from GlobalMetrics.Contributors")
assert.Equal(t, 15, result.PRsOpened, "Should use aggregated PRs from GlobalMetrics.Contributors")
}
func TestGenerator_MultipleContributorsAcrossRepos(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
// Multiple contributors across multiple repos with aggregated global data
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
Owner: "org",
Name: "repo1",
Contributors: []models.ContributorMetrics{
{Login: "alice", CommitCount: 100, LinesAdded: 5000},
{Login: "bob", CommitCount: 50, LinesAdded: 2000},
},
},
{
Owner: "org",
Name: "repo2",
Contributors: []models.ContributorMetrics{
{Login: "alice", CommitCount: 50, LinesAdded: 3000},
{Login: "charlie", CommitCount: 75, LinesAdded: 4000},
},
},
},
// Aggregated global contributors
Contributors: []models.ContributorMetrics{
{Login: "alice", CommitCount: 150, LinesAdded: 8000}, // 100+50, 5000+3000
{Login: "bob", CommitCount: 50, LinesAdded: 2000}, // Only in repo1
{Login: "charlie", CommitCount: 75, LinesAdded: 4000}, // Only in repo2
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Verify alice has aggregated data
alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json")
aliceData, err := os.ReadFile(alicePath)
require.NoError(t, err)
var aliceResult models.ContributorMetrics
err = json.Unmarshal(aliceData, &aliceResult)
require.NoError(t, err)
assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits")
assert.Equal(t, 8000, aliceResult.LinesAdded, "Alice should have aggregated lines added")
// Verify bob exists with his data
bobPath := filepath.Join(tempDir, "data", "contributors", "bob.json")
bobData, err := os.ReadFile(bobPath)
require.NoError(t, err)
var bobResult models.ContributorMetrics
err = json.Unmarshal(bobData, &bobResult)
require.NoError(t, err)
assert.Equal(t, 50, bobResult.CommitCount)
assert.Equal(t, 2000, bobResult.LinesAdded)
// Verify charlie exists with his data
charliePath := filepath.Join(tempDir, "data", "contributors", "charlie.json")
charlieData, err := os.ReadFile(charliePath)
require.NoError(t, err)
var charlieResult models.ContributorMetrics
err = json.Unmarshal(charlieData, &charlieResult)
require.NoError(t, err)
assert.Equal(t, 75, charlieResult.CommitCount)
assert.Equal(t, 4000, charlieResult.LinesAdded)
}
func TestGenerator_NoTeamsDoesNotCreateTeamDir(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
metrics := &models.GlobalMetrics{
Teams: []models.TeamMetrics{}, // Empty teams
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Team directory should not exist
teamDir := filepath.Join(tempDir, "data", "teams")
_, err = os.Stat(teamDir)
assert.True(t, os.IsNotExist(err))
}
func TestSlugify(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expected string
}{
{"Backend Team", "backend-team"},
{"Frontend_Team", "frontend-team"},
{"UPPER CASE", "upper-case"},
{"already-slug", "already-slug"},
{"Multiple Spaces", "multiple---spaces"},
{"Mixed_And Spaced", "mixed-and-spaced"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
result := slugify(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestWriteJSON(t *testing.T) {
tempDir := t.TempDir()
testData := map[string]interface{}{
"key": "value",
"number": 42,
"nested": map[string]string{
"inner": "data",
},
}
path := filepath.Join(tempDir, "test.json")
err := writeJSON(path, testData)
require.NoError(t, err)
// Verify file was created and is valid JSON
data, err := os.ReadFile(path)
require.NoError(t, err)
var result map[string]interface{}
err = json.Unmarshal(data, &result)
require.NoError(t, err)
assert.Equal(t, "value", result["key"])
assert.Equal(t, float64(42), result["number"]) // JSON numbers are float64
}
func TestWriteJSON_Indented(t *testing.T) {
tempDir := t.TempDir()
testData := map[string]string{"key": "value"}
path := filepath.Join(tempDir, "test.json")
err := writeJSON(path, testData)
require.NoError(t, err)
data, err := os.ReadFile(path)
require.NoError(t, err)
// Should be formatted with indentation
assert.Contains(t, string(data), "\n")
assert.Contains(t, string(data), " ") // 2-space indent
}
func TestWriteJSON_ErrorOnInvalidPath(t *testing.T) {
// Try to write to a path that doesn't exist
path := "/nonexistent/directory/test.json"
err := writeJSON(path, "data")
assert.Error(t, err)
}
func TestGenerator_GenerateWithFullMetrics(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
// Create comprehensive metrics
metrics := &models.GlobalMetrics{
Period: models.Period{
Start: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
End: time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
Granularity: "monthly",
Label: "2024",
},
TotalContributors: 10,
TotalCommits: 500,
TotalPRs: 100,
TotalReviews: 200,
TotalLinesAdded: 50000,
TotalLinesDeleted: 25000,
Repositories: []models.RepositoryMetrics{
{
Owner: "org",
Name: "repo1",
TotalCommits: 300,
TotalPRs: 60,
ActiveContributors: 5,
Contributors: []models.ContributorMetrics{
{Login: "alice", Name: "Alice", CommitCount: 100},
{Login: "bob", Name: "Bob", CommitCount: 200},
},
},
{
Owner: "org",
Name: "repo2",
TotalCommits: 200,
TotalPRs: 40,
ActiveContributors: 5,
Contributors: []models.ContributorMetrics{
{Login: "alice", Name: "Alice", CommitCount: 50},
{Login: "charlie", Name: "Charlie", CommitCount: 150},
},
},
},
// Global aggregated contributors (used for individual contributor files)
Contributors: []models.ContributorMetrics{
{Login: "alice", Name: "Alice", CommitCount: 150}, // 100+50 aggregated
{Login: "bob", Name: "Bob", CommitCount: 200}, // Only in repo1
{Login: "charlie", Name: "Charlie", CommitCount: 150}, // Only in repo2
},
Teams: []models.TeamMetrics{
{
Name: "Core Team",
Members: []string{"alice", "bob"},
TotalScore: 5000,
},
},
Leaderboard: []models.LeaderboardEntry{
{Rank: 1, Login: "alice", Score: 3000},
{Rank: 2, Login: "bob", Score: 2000},
{Rank: 3, Login: "charlie", Score: 1500},
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Verify all expected files exist
expectedPaths := []string{
filepath.Join(tempDir, "data", "global.json"),
filepath.Join(tempDir, "data", "leaderboard.json"),
filepath.Join(tempDir, "data", "repos", "org", "repo1", "metrics.json"),
filepath.Join(tempDir, "data", "repos", "org", "repo2", "metrics.json"),
filepath.Join(tempDir, "data", "teams", "core-team.json"),
filepath.Join(tempDir, "data", "contributors", "alice.json"),
filepath.Join(tempDir, "data", "contributors", "bob.json"),
filepath.Join(tempDir, "data", "contributors", "charlie.json"),
}
for _, path := range expectedPaths {
_, err := os.Stat(path)
assert.NoError(t, err, "Expected file to exist: %s", path)
}
// Verify alice's file has aggregated data (150 commits, not 100 from first repo)
alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json")
aliceData, err := os.ReadFile(alicePath)
require.NoError(t, err)
var aliceResult models.ContributorMetrics
err = json.Unmarshal(aliceData, &aliceResult)
require.NoError(t, err)
assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits from global Contributors")
}