diff options
| author | Franck Cuny <franck@fcuny.net> | 2025-09-27 15:10:13 -0700 |
|---|---|---|
| committer | Franck Cuny <franck@fcuny.net> | 2025-09-27 15:16:40 -0700 |
| commit | 7095a53bf173ca6680c7888c8d1df4afb1ad29ae (patch) | |
| tree | 63ce5037b38ed8704fffc362c7541ebef311b74c /cmd/pr-analyzer/analyzer_test.go | |
| parent | fix sha for goget (diff) | |
| download | x-7095a53bf173ca6680c7888c8d1df4afb1ad29ae.tar.gz | |
add pr-analyzer
A tool to analyze pull request review patterns in a GitHub repository, focusing on team members' review behaviors and habits. This tool helps teams understand and improve their code review processes by providing insights into review frequency, speed, thoroughness, and approval patterns.
Diffstat (limited to 'cmd/pr-analyzer/analyzer_test.go')
| -rw-r--r-- | cmd/pr-analyzer/analyzer_test.go | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/cmd/pr-analyzer/analyzer_test.go b/cmd/pr-analyzer/analyzer_test.go new file mode 100644 index 0000000..d2c3041 --- /dev/null +++ b/cmd/pr-analyzer/analyzer_test.go @@ -0,0 +1,233 @@ +package main + +import ( + "testing" + "time" + + "go.fcuny.net/x/internal/github" +) + +func TestAnalyzePRs(t *testing.T) { + startDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC) + a := NewAnalyzer(startDate, endDate) + + tests := []struct { + name string + prs []github.PullRequest + reviews map[int][]github.Review + comments map[int][]github.ReviewComment + want []ReviewerMetrics + }{ + { + name: "single PR with immediate approval", + prs: []github.PullRequest{ + { + Number: 1, + CreatedAt: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), + User: github.User{ + Login: "author1", + }, + }, + }, + reviews: map[int][]github.Review{ + 1: { + { + ID: 1, + User: github.User{Login: "reviewer1"}, + State: "APPROVED", + SubmittedAt: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC), + }, + }, + }, + comments: map[int][]github.ReviewComment{}, + want: []ReviewerMetrics{ + { + Username: "reviewer1", + PRsReviewed: 1, + AverageReviewSpeed: 0.5, // 30 minutes + AverageComments: 0, + ImmediateApprovals: 100.0, // 100% immediate approvals + }, + }, + }, + { + name: "PR with comments and changes requested", + prs: []github.PullRequest{ + { + Number: 2, + CreatedAt: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), + User: github.User{ + Login: "author2", + }, + }, + }, + reviews: map[int][]github.Review{ + 2: { + { + ID: 2, + User: github.User{Login: "reviewer2"}, + State: "CHANGES_REQUESTED", + SubmittedAt: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC), + }, + }, + }, + comments: map[int][]github.ReviewComment{ + 2: { + { + ID: 3, + ReviewID: 2, + User: github.User{Login: "reviewer2"}, + CreatedAt: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC), + Path: "file1.go", + Line: 10, + }, + { + ID: 4, + ReviewID: 2, + User: github.User{Login: "reviewer2"}, + CreatedAt: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC), + Path: "file2.go", + Line: 20, + }, + }, + }, + want: []ReviewerMetrics{ + { + Username: "reviewer2", + PRsReviewed: 1, + AverageReviewSpeed: 1.0, // 1 hour + AverageComments: 2.0, + ImmediateApprovals: 0.0, // 0% immediate approvals + }, + }, + }, + { + name: "multiple PRs with same reviewer", + prs: []github.PullRequest{ + { + Number: 3, + CreatedAt: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), + User: github.User{ + Login: "author3", + }, + }, + { + Number: 4, + CreatedAt: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC), + User: github.User{ + Login: "author4", + }, + }, + }, + reviews: map[int][]github.Review{ + 3: { + { + ID: 5, + User: github.User{Login: "reviewer3"}, + State: "APPROVED", + SubmittedAt: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC), + }, + }, + 4: { + { + ID: 6, + User: github.User{Login: "reviewer3"}, + State: "APPROVED", + SubmittedAt: time.Date(2024, 1, 15, 11, 30, 0, 0, time.UTC), + }, + }, + }, + comments: map[int][]github.ReviewComment{}, + want: []ReviewerMetrics{ + { + Username: "reviewer3", + PRsReviewed: 2, + AverageReviewSpeed: 0.5, // 30 minutes average + AverageComments: 0, + ImmediateApprovals: 100.0, // 100% immediate approvals + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prData := make([]PRData, len(tt.prs)) + for i, pr := range tt.prs { + reviews := tt.reviews[pr.Number] + comments := tt.comments[pr.Number] + + commentsByReview := make(map[int64][]CommentData) + for _, comment := range comments { + commentsByReview[comment.ReviewID] = append( + commentsByReview[comment.ReviewID], + CommentData{ + Reviewer: comment.User.Login, + CreatedAt: comment.CreatedAt, + Path: comment.Path, + Line: comment.Line, + }, + ) + } + + reviewData := make([]ReviewData, len(reviews)) + for j, review := range reviews { + reviewData[j] = ReviewData{ + Reviewer: review.User.Login, + State: review.State, + SubmittedAt: review.SubmittedAt, + Comments: commentsByReview[review.ID], + } + } + + prData[i] = PRData{ + Number: pr.Number, + CreatedAt: pr.CreatedAt, + Reviews: reviewData, + } + } + + got, err := a.AnalyzePRs(prData) + if err != nil { + t.Errorf("AnalyzePRs() error = %v", err) + return + } + + if len(got) != len(tt.want) { + t.Errorf("AnalyzePRs() returned %d metrics, want %d", len(got), len(tt.want)) + return + } + + for i, m := range got { + if m.Username != tt.want[i].Username { + t.Errorf("Username = %v, want %v", m.Username, tt.want[i].Username) + } + if m.PRsReviewed != tt.want[i].PRsReviewed { + t.Errorf("PRsReviewed = %v, want %v", m.PRsReviewed, tt.want[i].PRsReviewed) + } + if m.AverageReviewSpeed != tt.want[i].AverageReviewSpeed { + t.Errorf( + "AverageReviewSpeed = %v, want %v", + m.AverageReviewSpeed, + tt.want[i].AverageReviewSpeed, + ) + } + if m.AverageComments != tt.want[i].AverageComments { + t.Errorf( + "AverageComments = %v, want %v", + m.AverageComments, + tt.want[i].AverageComments, + ) + } + if m.ImmediateApprovals != tt.want[i].ImmediateApprovals { + t.Errorf( + "ImmediateApprovals = %v, want %v", + m.ImmediateApprovals, + tt.want[i].ImmediateApprovals, + ) + } + } + }) + } +} |
