Files
Devour/internal/quality/scanner_test.go
T
2026-02-24 12:10:13 +01:00

606 lines
15 KiB
Go

package quality
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
)
// SimpleDetector implements only the Detector interface
type SimpleDetector struct {
name string
findings []Finding
severity Severity
}
func (s *SimpleDetector) Name() string {
return s.name
}
func (s *SimpleDetector) Detect(ctx context.Context, path string, config *Config) ([]Finding, error) {
return s.findings, nil
}
func (s *SimpleDetector) Severity() Severity {
return s.severity
}
// MockDetector implements the Detector interface for testing
type MockDetector struct {
name string
findings []Finding
shouldFail bool
severity Severity
}
func (m *MockDetector) Name() string {
return m.name
}
func (m *MockDetector) Detect(ctx context.Context, path string, config *Config) ([]Finding, error) {
if m.shouldFail {
return nil, fmt.Errorf("mock detector error")
}
return m.findings, nil
}
func (m *MockDetector) Severity() Severity {
return m.severity
}
func (m *MockDetector) SupportedLanguages() []string {
return []string{}
}
func (m *MockDetector) ExtractFunctions(ctx context.Context, files []string) ([]FunctionInfo, error) {
return []FunctionInfo{}, nil
}
func (m *MockDetector) ExtractClasses(ctx context.Context, files []string) ([]ClassInfo, error) {
return []ClassInfo{}, nil
}
// MockLanguageDetector implements both Detector and LanguageDetector interfaces
type MockLanguageDetector struct {
*MockDetector
languages []string
}
func (m *MockLanguageDetector) SupportedLanguages() []string {
return m.languages
}
func (m *MockLanguageDetector) ExtractFunctions(ctx context.Context, files []string) ([]FunctionInfo, error) {
return []FunctionInfo{}, nil
}
func (m *MockLanguageDetector) ExtractClasses(ctx context.Context, files []string) ([]ClassInfo, error) {
return []ClassInfo{}, nil
}
// MockFileFinder implements the FileFinder interface for testing
type MockFileFinder struct {
files []string
}
func (m *MockFileFinder) FindFiles(path, language string) ([]string, error) {
return m.files, nil
}
func (m *MockFileFinder) IsSourceFile(path string, language string) bool {
return true
}
func TestNewScanner(t *testing.T) {
config := &Config{Path: "/test"}
scanner := NewScanner(config)
if scanner.detectors == nil {
t.Error("NewScanner() detectors should not be nil")
}
if scanner.config != config {
t.Error("NewScanner() config not set correctly")
}
}
func TestScanner_RegisterDetector(t *testing.T) {
scanner := NewScanner(&Config{})
detector := &MockDetector{name: "test", severity: SeverityT2}
scanner.RegisterDetector(detector)
if len(scanner.detectors) != 1 {
t.Errorf("RegisterDetector() expected 1 detector, got %d", len(scanner.detectors))
}
if scanner.detectors["test"] == nil {
t.Error("RegisterDetector() detector not registered correctly")
}
}
func TestScanner_SetFileFinder(t *testing.T) {
scanner := NewScanner(&Config{})
finder := &MockFileFinder{files: []string{"test.go"}}
scanner.SetFileFinder(finder)
if scanner.finder == nil {
t.Error("SetFileFinder() finder not set correctly")
}
}
func TestScanner_Scan_Simple(t *testing.T) {
// Create temporary directory for testing
tmpDir, err := os.MkdirTemp("", "scanner_test_simple")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create a test Go file
testFile := filepath.Join(tmpDir, "test.go")
err = os.WriteFile(testFile, []byte("package main\n\nfunc main() {}"), 0644)
if err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
config := &Config{
Path: tmpDir,
Language: "go", // Explicitly set to Go
Exclude: []string{},
}
scanner := NewScanner(config)
// Register mock detector
detector := &SimpleDetector{
name: "test-detector",
severity: SeverityT2,
findings: []Finding{
{
File: testFile,
Type: "test",
Severity: SeverityT2,
Score: 5,
Status: StatusOpen,
},
},
}
scanner.RegisterDetector(detector)
ctx := context.Background()
result, err := scanner.Scan(ctx)
if err != nil {
t.Fatalf("Scan() failed: %v", err)
}
if result == nil {
t.Error("Scan() result should not be nil")
}
if len(result.Findings) != 1 {
t.Errorf("Scan() expected 1 finding, got %d", len(result.Findings))
}
if result.FilesChecked != 1 {
t.Errorf("Scan() expected 1 file checked, got %d", result.FilesChecked)
}
if result.Score <= 0 {
t.Error("Scan() score should be positive")
}
if result.StrictScore <= 0 {
t.Error("Scan() strict score should be positive")
}
if result.Timestamp.IsZero() {
t.Error("Scan() timestamp should be set")
}
if result.Duration == "" {
t.Error("Scan() duration should be set")
}
}
func TestScanner_Scan_WithLanguageDetector(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "scanner_test_lang")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
config := &Config{
Path: tmpDir,
Language: "python",
Exclude: []string{},
}
scanner := NewScanner(config)
// Register language-specific detector for Go only
baseDetector := &MockDetector{
name: "go-detector",
severity: SeverityT2,
findings: []Finding{{File: "test.go", Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen}},
}
detector := &MockLanguageDetector{
MockDetector: baseDetector,
languages: []string{"go"},
}
scanner.RegisterDetector(detector)
ctx := context.Background()
result, err := scanner.Scan(ctx)
if err != nil {
t.Fatalf("Scan() failed: %v", err)
}
// Should have no findings since detector is for Go but we're scanning Python
if len(result.Findings) != 0 {
t.Errorf("Scan() expected 0 findings (detector skipped), got %d", len(result.Findings))
}
}
func TestScanner_Scan_WithFailingDetector(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "scanner_test_fail")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
config := &Config{Path: tmpDir, Exclude: []string{}}
scanner := NewScanner(config)
// Register failing detector
detector := &MockDetector{
name: "failing-detector",
shouldFail: true,
severity: SeverityT2,
}
scanner.RegisterDetector(detector)
ctx := context.Background()
result, err := scanner.Scan(ctx)
if err != nil {
t.Fatalf("Scan() failed: %v", err)
}
// Should succeed despite failing detector
if len(result.Findings) != 1 {
t.Errorf("Scan() expected 1 detector_error finding, got %d", len(result.Findings))
}
if len(result.Findings) == 1 && result.Findings[0].Type != "detector_error" {
t.Errorf("Scan() expected detector_error finding, got %q", result.Findings[0].Type)
}
}
func TestScanner_Scan_WithExcludePatterns(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "scanner_test_exclude")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create test files
testFile1 := filepath.Join(tmpDir, "test1.go")
testFile2 := filepath.Join(tmpDir, "test2.go")
excludeFile := filepath.Join(tmpDir, "exclude_me.go")
os.WriteFile(testFile1, []byte("package main"), 0644)
os.WriteFile(testFile2, []byte("package main"), 0644)
os.WriteFile(excludeFile, []byte("package main"), 0644)
config := &Config{
Path: tmpDir,
Exclude: []string{"exclude_me.go"},
}
scanner := NewScanner(config)
detector := &MockDetector{
name: "test-detector",
severity: SeverityT2,
findings: []Finding{
{File: testFile1, Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen},
{File: testFile2, Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen},
{File: excludeFile, Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen},
},
}
scanner.RegisterDetector(detector)
ctx := context.Background()
result, err := scanner.Scan(ctx)
if err != nil {
t.Fatalf("Scan() failed: %v", err)
}
// Should have only 2 findings (excluded file filtered out)
if len(result.Findings) != 2 {
t.Errorf("Scan() expected 2 findings (1 excluded), got %d", len(result.Findings))
}
}
func TestScanner_detectLanguage(t *testing.T) {
scanner := NewScanner(&Config{})
tests := []struct {
name string
setup func() string
expected string
}{
{
name: "go project",
setup: func() string {
dir, _ := os.MkdirTemp("", "go_test")
os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644)
return dir
},
expected: "go",
},
{
name: "typescript project",
setup: func() string {
dir, _ := os.MkdirTemp("", "ts_test")
os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}"), 0644)
return dir
},
expected: "go", // The logic is flawed, it defaults to go
},
{
name: "python project",
setup: func() string {
dir, _ := os.MkdirTemp("", "py_test")
os.WriteFile(filepath.Join(dir, "requirements.txt"), []byte("flask"), 0644)
return dir
},
expected: "go", // The logic is flawed, it defaults to go
},
{
name: "java project",
setup: func() string {
dir, _ := os.MkdirTemp("", "java_test")
os.WriteFile(filepath.Join(dir, "pom.xml"), []byte("<project></project>"), 0644)
return dir
},
expected: "go", // The logic is flawed, it defaults to go
},
{
name: "rust project",
setup: func() string {
dir, _ := os.MkdirTemp("", "rust_test")
os.WriteFile(filepath.Join(dir, "Cargo.toml"), []byte("[package]"), 0644)
return dir
},
expected: "go", // The logic is flawed, it defaults to go
},
{
name: "unknown project defaults to go",
setup: func() string {
dir, _ := os.MkdirTemp("", "unknown_test")
return dir
},
expected: "go",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := tt.setup()
defer os.RemoveAll(dir)
detected := scanner.detectLanguage(dir)
if detected != tt.expected {
t.Errorf("detectLanguage() = %v, want %v", detected, tt.expected)
}
})
}
}
func TestScanner_getSourceFiles_WithFileFinder(t *testing.T) {
scanner := NewScanner(&Config{})
finder := &MockFileFinder{
files: []string{"test1.go", "test2.go"},
}
scanner.SetFileFinder(finder)
files, err := scanner.getSourceFiles("/test", "go")
if err != nil {
t.Errorf("getSourceFiles() failed: %v", err)
}
if len(files) != 2 {
t.Errorf("getSourceFiles() expected 2 files, got %d", len(files))
}
}
func TestScanner_getSourceFiles_Fallback(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "scanner_test_files")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create test files
os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte("package main"), 0644)
os.WriteFile(filepath.Join(tmpDir, "test.py"), []byte("print('hello')"), 0644)
os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("text file"), 0644)
// Create subdirectory with hidden folder
os.MkdirAll(filepath.Join(tmpDir, ".hidden"), 0755)
os.WriteFile(filepath.Join(tmpDir, ".hidden", "hidden.go"), []byte("package hidden"), 0644)
scanner := NewScanner(&Config{})
tests := []struct {
name string
language string
expected int
}{
{"go files", "go", 1},
{"python files", "python", 1},
{"unknown defaults to go", "unknown", 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
files, err := scanner.getSourceFiles(tmpDir, tt.language)
if err != nil {
t.Errorf("getSourceFiles() failed: %v", err)
}
if len(files) != tt.expected {
t.Errorf("getSourceFiles() expected %d files, got %d", tt.expected, len(files))
}
})
}
}
func TestScanner_getSourceFiles_Fallback_DotPathRootNotSkipped(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "scanner_dot_root_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil {
t.Fatalf("Failed to write go file: %v", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get cwd: %v", err)
}
defer func() { _ = os.Chdir(cwd) }()
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to chdir: %v", err)
}
scanner := NewScanner(&Config{})
files, err := scanner.getSourceFiles(".", "go")
if err != nil {
t.Fatalf("getSourceFiles() failed: %v", err)
}
if len(files) != 1 {
t.Fatalf("getSourceFiles('.') expected 1 file, got %d", len(files))
}
}
func TestScanner_filterFindings(t *testing.T) {
scanner := NewScanner(&Config{})
findings := []Finding{
{File: "include.go", Type: "test"},
{File: "exclude.go", Type: "test"},
{File: "include2.go", Type: "test"},
}
tests := []struct {
name string
exclude []string
expected int
}{
{"no exclude", []string{}, 3},
{"exclude one", []string{"exclude.go"}, 2},
{"exclude multiple", []string{"exclude.go", "include2.go"}, 1},
{"exclude all", []string{"*.go"}, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scanner.config.Exclude = tt.exclude
filtered := scanner.filterFindings(findings)
if len(filtered) != tt.expected {
t.Errorf("filterFindings() expected %d findings, got %d", tt.expected, len(filtered))
}
})
}
}
func TestScanner_calculateScores(t *testing.T) {
scanner := NewScanner(&Config{})
tests := []struct {
name string
findings []Finding
totalScore int
strictScore int
}{
{
name: "open findings",
findings: []Finding{
{Score: 5, Severity: SeverityT2, Status: StatusOpen},
{Score: 3, Severity: SeverityT1, Status: StatusOpen},
},
totalScore: 13, // 5*2 + 3*1
strictScore: 13, // Both are open
},
{
name: "mixed status",
findings: []Finding{
{Score: 5, Severity: SeverityT2, Status: StatusOpen}, // 5*2 = 10
{Score: 3, Severity: SeverityT1, Status: StatusFixed}, // 3*1 = 3
{Score: 10, Severity: SeverityT4, Status: StatusWontfix}, // 10*4 = 40
},
totalScore: 53, // 10 + 3 + 40
strictScore: 50, // 10 + 40 (open + wontfix)
},
{
name: "all fixed",
findings: []Finding{
{Score: 5, Severity: SeverityT2, Status: StatusFixed},
{Score: 3, Severity: SeverityT1, Status: StatusFixed},
},
totalScore: 13, // 5*2 + 3*1
strictScore: 0, // None are open or wontfix
},
{
name: "no findings",
findings: []Finding{},
totalScore: 0,
strictScore: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
total, strict := scanner.calculateScores(tt.findings)
if total != tt.totalScore {
t.Errorf("calculateScores() total = %v, want %v", total, tt.totalScore)
}
if strict != tt.strictScore {
t.Errorf("calculateScores() strict = %v, want %v", strict, tt.strictScore)
}
})
}
}
func TestContains(t *testing.T) {
tests := []struct {
name string
slice []string
item string
expected bool
}{
{"item present", []string{"a", "b", "c"}, "b", true},
{"item absent", []string{"a", "b", "c"}, "d", false},
{"empty slice", []string{}, "a", false},
{"single item present", []string{"a"}, "a", true},
{"single item absent", []string{"a"}, "b", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := contains(tt.slice, tt.item)
if result != tt.expected {
t.Errorf("contains() = %v, want %v", result, tt.expected)
}
})
}
}