mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
603 lines
15 KiB
Go
603 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) != 0 {
|
|
t.Errorf("Scan() expected 0 findings, got %d", len(result.Findings))
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|