mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
568 lines
17 KiB
Go
568 lines
17 KiB
Go
package quality
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewScorer(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
targetScore int
|
|
expected int
|
|
}{
|
|
{"default target", 0, 95},
|
|
{"negative target", -10, 95},
|
|
{"zero target", 0, 95},
|
|
{"custom target", 85, 85},
|
|
{"high target", 100, 100},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
scorer := NewScorer(tt.targetScore)
|
|
if scorer.targetScore != tt.expected {
|
|
t.Errorf("NewScorer() targetScore = %v, want %v", scorer.targetScore, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_CalculateScore(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
findings []Finding
|
|
totalScore int
|
|
strictScore int
|
|
}{
|
|
{
|
|
name: "no findings",
|
|
findings: []Finding{},
|
|
totalScore: 0,
|
|
strictScore: 0,
|
|
},
|
|
{
|
|
name: "open findings only",
|
|
findings: []Finding{
|
|
{Score: 5, Severity: SeverityT1, Status: StatusOpen},
|
|
{Score: 10, Severity: SeverityT2, Status: StatusOpen},
|
|
{Score: 15, Severity: SeverityT3, Status: StatusOpen},
|
|
{Score: 20, Severity: SeverityT4, Status: StatusOpen},
|
|
},
|
|
totalScore: 150, // 5*1 + 10*2 + 15*3 + 20*4
|
|
strictScore: 580, // (5*1)*1 + (10*2)*2 + (15*3)*3 + (20*4)*5
|
|
},
|
|
{
|
|
name: "mixed statuses",
|
|
findings: []Finding{
|
|
{Score: 5, Severity: SeverityT1, Status: StatusOpen},
|
|
{Score: 10, Severity: SeverityT2, Status: StatusFixed},
|
|
{Score: 15, Severity: SeverityT3, Status: StatusFalsePositive},
|
|
{Score: 20, Severity: SeverityT4, Status: StatusIgnored},
|
|
{Score: 25, Severity: SeverityT1, Status: StatusWontfix},
|
|
},
|
|
totalScore: 175, // All included with severity weighting
|
|
strictScore: 30, // Open T1 + unjustified wontfix T1
|
|
},
|
|
{
|
|
name: "justified wontfix",
|
|
findings: []Finding{
|
|
{Score: 10, Severity: SeverityT2, Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "legacy code"}},
|
|
{Score: 15, Severity: SeverityT3, Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "third-party"}},
|
|
},
|
|
totalScore: 65, // All included in total with severity weighting
|
|
strictScore: 0, // All wontfix are justified
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
total, strict := scorer.CalculateScore(tt.findings)
|
|
if total != tt.totalScore {
|
|
t.Errorf("CalculateScore() total = %v, want %v", total, tt.totalScore)
|
|
}
|
|
if strict != tt.strictScore {
|
|
t.Errorf("CalculateScore() strict = %v, want %v", strict, tt.strictScore)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_GenerateScorecard(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
findings := []Finding{
|
|
{Type: "dead_code", Severity: SeverityT2, Status: StatusOpen, Score: 10},
|
|
{Type: "naming", Severity: SeverityT1, Status: StatusFixed, Score: 5},
|
|
{Type: "complexity", Severity: SeverityT3, Status: StatusOpen, Score: 15},
|
|
}
|
|
lastScan := time.Now()
|
|
|
|
card := scorer.GenerateScorecard(findings, lastScan)
|
|
|
|
if card == nil {
|
|
t.Error("GenerateScorecard() should not return nil")
|
|
}
|
|
|
|
if card.TargetScore != 95 {
|
|
t.Errorf("GenerateScorecard() TargetScore = %v, want 95", card.TargetScore)
|
|
}
|
|
|
|
if card.TotalScore != 70 { // 10*2 + 5*1 + 15*3
|
|
t.Errorf("GenerateScorecard() TotalScore = %v, want 70", card.TotalScore)
|
|
}
|
|
|
|
if card.LastScan != lastScan {
|
|
t.Error("GenerateScorecard() LastScan not set correctly")
|
|
}
|
|
|
|
// Check findings by type
|
|
if card.FindingsByType["dead_code"] != 1 {
|
|
t.Errorf("GenerateScorecard() dead_code count = %v, want 1", card.FindingsByType["dead_code"])
|
|
}
|
|
if card.FindingsByType["naming"] != 1 {
|
|
t.Errorf("GenerateScorecard() naming count = %v, want 1", card.FindingsByType["naming"])
|
|
}
|
|
|
|
// Check findings by tier
|
|
if card.FindingsByTier[SeverityT1] != 1 {
|
|
t.Errorf("GenerateScorecard() T1 count = %v, want 1", card.FindingsByTier[SeverityT1])
|
|
}
|
|
if card.FindingsByTier[SeverityT2] != 1 {
|
|
t.Errorf("GenerateScorecard() T2 count = %v, want 1", card.FindingsByTier[SeverityT2])
|
|
}
|
|
|
|
// Check status by type
|
|
if card.StatusByType["open"] != 2 {
|
|
t.Errorf("GenerateScorecard() open count = %v, want 2", card.StatusByType["open"])
|
|
}
|
|
if card.StatusByType["fixed"] != 1 {
|
|
t.Errorf("GenerateScorecard() fixed count = %v, want 1", card.StatusByType["fixed"])
|
|
}
|
|
}
|
|
|
|
func TestScorer_isStrictlyRelevant(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
finding Finding
|
|
expected bool
|
|
}{
|
|
{"open issue", Finding{Status: StatusOpen}, true},
|
|
{"fixed issue", Finding{Status: StatusFixed}, false},
|
|
{"false positive", Finding{Status: StatusFalsePositive}, false},
|
|
{"ignored", Finding{Status: StatusIgnored}, false},
|
|
{"unjustified wontfix", Finding{Status: StatusWontfix}, true},
|
|
{"justified wontfix", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "legacy"}}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scorer.isStrictlyRelevant(tt.finding)
|
|
if result != tt.expected {
|
|
t.Errorf("isStrictlyRelevant() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_isJustifiedWontfix(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
finding Finding
|
|
expected bool
|
|
}{
|
|
{"no metadata", Finding{Status: StatusWontfix}, false},
|
|
{"no resolution note", Finding{Status: StatusWontfix, Metadata: map[string]string{}}, false},
|
|
{"empty resolution note", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": ""}}, false},
|
|
{"legacy justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "legacy code"}}, true},
|
|
{"deprecated justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "deprecated API"}}, true},
|
|
{"external justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "external dependency"}}, true},
|
|
{"third-party justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "third-party library"}}, true},
|
|
{"temporary justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "temporary fix"}}, true},
|
|
{"documentation justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "documentation only"}}, true},
|
|
{"test-only justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "test-only code"}}, true},
|
|
{"example justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "example code"}}, true},
|
|
{"sample justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "sample code"}}, true},
|
|
{"invalid justification", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "needs fixing"}}, false},
|
|
{"case insensitive", Finding{Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "LEGACY CODE"}}, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scorer.isJustifiedWontfix(tt.finding)
|
|
if result != tt.expected {
|
|
t.Errorf("isJustifiedWontfix() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_getStrictMultiplier(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
finding Finding
|
|
expected int
|
|
}{
|
|
{"T1 severity", Finding{Severity: SeverityT1}, 1},
|
|
{"T2 severity", Finding{Severity: SeverityT2}, 2},
|
|
{"T3 severity", Finding{Severity: SeverityT3}, 3},
|
|
{"T4 severity", Finding{Severity: SeverityT4}, 5},
|
|
{"unknown severity", Finding{Severity: Severity(99)}, 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scorer.getStrictMultiplier(tt.finding)
|
|
if result != tt.expected {
|
|
t.Errorf("getStrictMultiplier() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_GetHealthGrade(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
score int
|
|
expected string
|
|
}{
|
|
{"perfect score", 0, "A"},
|
|
{"excellent score", 500, "C"},
|
|
{"good score", 1000, "F"},
|
|
{"very good score", 2000, "B"},
|
|
{"good score", 3000, "C"},
|
|
{"fair score", 4000, "D"},
|
|
{"poor score", 5000, "F"},
|
|
{"very poor score", 10000, "F"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
grade := scorer.GetHealthGrade(tt.score)
|
|
if grade != tt.expected {
|
|
t.Errorf("GetHealthGrade(%d) = %v, want %v", tt.score, grade, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_getScorePercentage(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
score int
|
|
expected int
|
|
}{
|
|
{"zero score", 0, 100},
|
|
{"low score", 100, 95},
|
|
{"medium score", 1000, 50},
|
|
{"high score", 5000, 50},
|
|
{"very high score", 10000, 50},
|
|
{"extreme score", 20000, 55},
|
|
{"negative score", -100, 100},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
percentage := scorer.getScorePercentage(tt.score)
|
|
if percentage != tt.expected {
|
|
t.Errorf("getScorePercentage(%d) = %v, want %v", tt.score, percentage, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_GetStrictHealthMetrics(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
findings := []Finding{
|
|
{Score: 10, Severity: SeverityT4, Status: StatusOpen},
|
|
{Score: 5, Severity: SeverityT3, Status: StatusOpen},
|
|
{Score: 3, Severity: SeverityT2, Status: StatusOpen},
|
|
{Score: 1, Severity: SeverityT1, Status: StatusOpen},
|
|
{Score: 15, Severity: SeverityT2, Status: StatusFixed},
|
|
{Score: 8, Severity: SeverityT3, Status: StatusIgnored},
|
|
{Score: 6, Severity: SeverityT4, Status: StatusWontfix},
|
|
}
|
|
|
|
metrics := scorer.GetStrictHealthMetrics(findings)
|
|
|
|
// Verify required fields
|
|
requiredFields := []string{
|
|
"total_issues", "open_issues", "critical_issues", "high_issues",
|
|
"medium_issues", "low_issues", "resolved_issues", "ignored_issues",
|
|
"open_percentage", "critical_percentage", "resolution_rate",
|
|
"strict_score", "total_score", "health_score", "grade",
|
|
}
|
|
|
|
for _, field := range requiredFields {
|
|
if _, exists := metrics[field]; !exists {
|
|
t.Errorf("GetStrictHealthMetrics() missing required field: %s", field)
|
|
}
|
|
}
|
|
|
|
// Verify specific values
|
|
if metrics["total_issues"] != 7 {
|
|
t.Errorf("GetStrictHealthMetrics() total_issues = %v, want 7", metrics["total_issues"])
|
|
}
|
|
if metrics["open_issues"] != 4 {
|
|
t.Errorf("GetStrictHealthMetrics() open_issues = %v, want 4", metrics["open_issues"])
|
|
}
|
|
if metrics["critical_issues"] != 2 {
|
|
t.Errorf("GetStrictHealthMetrics() critical_issues = %v, want 2", metrics["critical_issues"])
|
|
}
|
|
}
|
|
|
|
func TestScorer_GetStrictGrade(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
healthScore float64
|
|
expected string
|
|
}{
|
|
{"perfect", 100.0, "A+"},
|
|
{"excellent", 95.0, "A+"},
|
|
{"very good", 90.0, "A"},
|
|
{"good plus", 85.0, "A-"},
|
|
{"good", 80.0, "B+"},
|
|
{"good minus", 75.0, "B"},
|
|
{"fair plus", 70.0, "B-"},
|
|
{"fair", 65.0, "C+"},
|
|
{"fair minus", 60.0, "C"},
|
|
{"poor plus", 55.0, "C-"},
|
|
{"poor", 50.0, "D+"},
|
|
{"poor minus", 45.0, "D"},
|
|
{"very poor", 40.0, "D-"},
|
|
{"failing", 35.0, "F"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
grade := scorer.GetStrictGrade(tt.healthScore)
|
|
if grade != tt.expected {
|
|
t.Errorf("GetStrictGrade(%.1f) = %v, want %v", tt.healthScore, grade, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_FormatScorecard(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
card := &Scorecard{
|
|
TotalScore: 100,
|
|
StrictScore: 50,
|
|
TargetScore: 95,
|
|
FindingsByType: map[string]int{
|
|
"dead_code": 5,
|
|
"naming": 3,
|
|
},
|
|
FindingsByTier: map[Severity]int{
|
|
SeverityT1: 2,
|
|
SeverityT2: 4,
|
|
SeverityT3: 1,
|
|
SeverityT4: 1,
|
|
},
|
|
StatusByType: map[string]int{
|
|
"open": 6,
|
|
"fixed": 2,
|
|
},
|
|
LastScan: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
|
|
output := scorer.FormatScorecard(card)
|
|
|
|
if output == "" {
|
|
t.Error("FormatScorecard() should not return empty string")
|
|
}
|
|
|
|
// Check that key elements are present
|
|
requiredElements := []string{
|
|
"STRICT Code Quality Scorecard",
|
|
"Overall Health",
|
|
"Target Score: 95",
|
|
"Current Score: 100 (strict: 50)",
|
|
"Findings by Type",
|
|
"dead_code: 5",
|
|
"naming: 3",
|
|
"Findings by Severity",
|
|
"T1 (Auto-fixable): 2",
|
|
"T2 (Quick manual): 4",
|
|
"T3 (Needs judgment): 1",
|
|
"T4 (Major refactor): 1",
|
|
"Status Breakdown",
|
|
"open: 6",
|
|
"fixed: 2",
|
|
"Last Scan: 2024-01-01 12:00:00",
|
|
}
|
|
|
|
for _, element := range requiredElements {
|
|
if !strings.Contains(output, element) {
|
|
t.Errorf("FormatScorecard() missing element: %s", element)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScorer_getSeverityEmoji(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
severity Severity
|
|
expected string
|
|
}{
|
|
{"T1", SeverityT1, "🟢"},
|
|
{"T2", SeverityT2, "🟡"},
|
|
{"T3", SeverityT3, "🟠"},
|
|
{"T4", SeverityT4, "🔴"},
|
|
{"unknown", Severity(99), "⚪"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
emoji := scorer.getSeverityEmoji(tt.severity)
|
|
if emoji != tt.expected {
|
|
t.Errorf("getSeverityEmoji() = %v, want %v", emoji, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_GetNextPriority(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
tests := []struct {
|
|
name string
|
|
findings []Finding
|
|
expected *Finding
|
|
}{
|
|
{"no findings", []Finding{}, nil},
|
|
{
|
|
name: "single finding",
|
|
findings: []Finding{{Score: 10, Severity: SeverityT2, Status: StatusOpen}},
|
|
expected: &Finding{Score: 10, Severity: SeverityT2, Status: StatusOpen},
|
|
},
|
|
{
|
|
name: "multiple findings",
|
|
findings: []Finding{
|
|
{Score: 5, Severity: SeverityT1, Status: StatusOpen},
|
|
{Score: 10, Severity: SeverityT2, Status: StatusOpen},
|
|
{Score: 15, Severity: SeverityT3, Status: StatusOpen},
|
|
{Score: 20, Severity: SeverityT4, Status: StatusFixed},
|
|
},
|
|
expected: &Finding{Score: 15, Severity: SeverityT3, Status: StatusOpen},
|
|
},
|
|
{
|
|
name: "highest weight",
|
|
findings: []Finding{
|
|
{Score: 5, Severity: SeverityT1, Status: StatusOpen},
|
|
{Score: 25, Severity: SeverityT4, Status: StatusOpen},
|
|
},
|
|
expected: &Finding{Score: 25, Severity: SeverityT4, Status: StatusOpen},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scorer.GetNextPriority(tt.findings)
|
|
|
|
if tt.expected == nil {
|
|
if result != nil {
|
|
t.Errorf("GetNextPriority() expected nil, got %v", result)
|
|
}
|
|
} else {
|
|
if result == nil {
|
|
t.Errorf("GetNextPriority() expected finding, got nil")
|
|
} else {
|
|
weight := int(result.Severity) * result.Score
|
|
expectedWeight := int(tt.expected.Severity) * tt.expected.Score
|
|
if weight != expectedWeight {
|
|
t.Errorf("GetNextPriority() weight = %v, want %v", weight, expectedWeight)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScorer_GetFindingsByTier(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
findings := []Finding{
|
|
{Score: 5, Severity: SeverityT1, Status: StatusOpen},
|
|
{Score: 10, Severity: SeverityT2, Status: StatusOpen},
|
|
{Score: 15, Severity: SeverityT3, Status: StatusOpen},
|
|
{Score: 20, Severity: SeverityT4, Status: StatusOpen},
|
|
{Score: 25, Severity: SeverityT1, Status: StatusFixed},
|
|
{Score: 30, Severity: SeverityT2, Status: StatusIgnored},
|
|
}
|
|
|
|
result := scorer.GetFindingsByTier(findings)
|
|
|
|
// Should only include open findings
|
|
if len(result[SeverityT1]) != 1 {
|
|
t.Errorf("GetFindingsByTier() T1 count = %v, want 1", len(result[SeverityT1]))
|
|
}
|
|
if len(result[SeverityT2]) != 1 {
|
|
t.Errorf("GetFindingsByTier() T2 count = %v, want 1", len(result[SeverityT2]))
|
|
}
|
|
if len(result[SeverityT3]) != 1 {
|
|
t.Errorf("GetFindingsByTier() T3 count = %v, want 1", len(result[SeverityT3]))
|
|
}
|
|
if len(result[SeverityT4]) != 1 {
|
|
t.Errorf("GetFindingsByTier() T4 count = %v, want 1", len(result[SeverityT4]))
|
|
}
|
|
}
|
|
|
|
func TestScorer_GetProgressMetrics(t *testing.T) {
|
|
scorer := NewScorer(95)
|
|
|
|
findings := []Finding{
|
|
{Status: StatusOpen},
|
|
{Status: StatusOpen},
|
|
{Status: StatusFixed},
|
|
{Status: StatusFixed},
|
|
{Status: StatusWontfix},
|
|
}
|
|
|
|
metrics := scorer.GetProgressMetrics(findings)
|
|
|
|
// Verify required fields
|
|
requiredFields := []string{"total", "open", "fixed", "wontfix", "progress"}
|
|
|
|
for _, field := range requiredFields {
|
|
if _, exists := metrics[field]; !exists {
|
|
t.Errorf("GetProgressMetrics() missing required field: %s", field)
|
|
}
|
|
}
|
|
|
|
// Verify specific values
|
|
if metrics["total"] != 5 {
|
|
t.Errorf("GetProgressMetrics() total = %v, want 5", metrics["total"])
|
|
}
|
|
if metrics["open"] != 2 {
|
|
t.Errorf("GetProgressMetrics() open = %v, want 2", metrics["open"])
|
|
}
|
|
if metrics["fixed"] != 2 {
|
|
t.Errorf("GetProgressMetrics() fixed = %v, want 2", metrics["fixed"])
|
|
}
|
|
if metrics["wontfix"] != 1 {
|
|
t.Errorf("GetProgressMetrics() wontfix = %v, want 1", metrics["wontfix"])
|
|
}
|
|
if metrics["progress"] != 40.0 {
|
|
t.Errorf("GetProgressMetrics() progress = %v, want 40.0", metrics["progress"])
|
|
}
|
|
}
|