Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions internal/mutation/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ func (o Options) workers() int {
// concurrently; temp-copy runners for other languages must serialize
// per-file internally).
func Analyze(repoPath string, d *diff.Result, l lang.Language, opts Options) (report.Section, error) {
allMutants := collectMutants(repoPath, d, l)
allMutants, err := collectMutants(repoPath, d, l)
if err != nil {
return report.Section{}, err
}

if len(allMutants) == 0 {
return report.Section{
Expand All @@ -124,7 +127,7 @@ func Analyze(repoPath string, d *diff.Result, l lang.Language, opts Options) (re
// collectMutants gathers mutation sites for every changed file, honoring
// the language's annotation scanner so lines marked
// `// mutator-disable-*` never produce mutants.
func collectMutants(repoPath string, d *diff.Result, l lang.Language) []Mutant {
func collectMutants(repoPath string, d *diff.Result, l lang.Language) ([]Mutant, error) {
gen := l.MutantGenerator()
scanner := l.AnnotationScanner()

Expand All @@ -133,11 +136,11 @@ func collectMutants(repoPath string, d *diff.Result, l lang.Language) []Mutant {
absPath := filepath.Join(repoPath, fc.Path)
disabled, err := scanner.ScanAnnotations(absPath)
if err != nil {
continue
return nil, fmt.Errorf("scanning mutation annotations for %s: %w", fc.Path, err)
}
sites, err := gen.GenerateMutants(absPath, fc, disabled)
if err != nil {
continue
return nil, fmt.Errorf("generating mutants for %s: %w", fc.Path, err)
}
for _, s := range sites {
all = append(all, Mutant{
Expand All @@ -148,7 +151,7 @@ func collectMutants(repoPath string, d *diff.Result, l lang.Language) []Mutant {
})
}
}
return all
return all, nil
}

// runMutantsParallel processes mutants in two phases:
Expand Down
78 changes: 78 additions & 0 deletions internal/mutation/mutation_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package mutation

import (
"errors"
"runtime"
"testing"

"github.com/0xPolygon/diffguard/internal/diff"
"github.com/0xPolygon/diffguard/internal/lang"
)

// Most of what used to be tested here was the Go AST machinery:
Expand Down Expand Up @@ -73,3 +77,77 @@ func TestOptionsTiers(t *testing.T) {
t.Errorf("tier2 explicit = %v, want 50", got)
}
}

func TestAnalyze_PropagatesMutationCollectionErrors(t *testing.T) {
scanErr := errors.New("synthetic annotation scan failure")
generateErr := errors.New("synthetic mutant generation failure")

tests := []struct {
name string
scannerErr error
generatorErr error
wantErr error
}{
{
name: "annotation scanner failure",
scannerErr: scanErr,
wantErr: scanErr,
},
{
name: "mutant generator failure",
generatorErr: generateErr,
wantErr: generateErr,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &analysisErrorLanguage{
scanner: stubAnnotationScanner{err: tt.scannerErr},
generator: stubMutantGenerator{err: tt.generatorErr},
}
d := &diff.Result{Files: []diff.FileChange{{Path: "changed.go"}}}

_, err := Analyze(t.TempDir(), d, l, Options{})
if !errors.Is(err, tt.wantErr) {
t.Fatalf("Analyze() error = %v, want error wrapping %v", err, tt.wantErr)
}
})
}
}

type stubAnnotationScanner struct {
err error
}

func (s stubAnnotationScanner) ScanAnnotations(string) (map[int]bool, error) {
return nil, s.err
}

type stubMutantGenerator struct {
err error
}

func (g stubMutantGenerator) GenerateMutants(string, diff.FileChange, map[int]bool) ([]lang.MutantSite, error) {
return nil, g.err
}

// analysisErrorLanguage exposes only the mutation collection dependencies.
// Every other capability is intentionally nil because Analyze returns before
// it can use them when collection fails.
type analysisErrorLanguage struct {
scanner lang.AnnotationScanner
generator lang.MutantGenerator
}

func (l *analysisErrorLanguage) Name() string { return "analysis-error-test" }
func (l *analysisErrorLanguage) FileFilter() lang.FileFilter { return lang.FileFilter{} }
func (l *analysisErrorLanguage) ComplexityCalculator() lang.ComplexityCalculator { return nil }
func (l *analysisErrorLanguage) FunctionExtractor() lang.FunctionExtractor { return nil }
func (l *analysisErrorLanguage) ImportResolver() lang.ImportResolver { return nil }
func (l *analysisErrorLanguage) ComplexityScorer() lang.ComplexityScorer { return nil }
func (l *analysisErrorLanguage) MutantGenerator() lang.MutantGenerator { return l.generator }
func (l *analysisErrorLanguage) MutantApplier() lang.MutantApplier { return nil }
func (l *analysisErrorLanguage) AnnotationScanner() lang.AnnotationScanner { return l.scanner }
func (l *analysisErrorLanguage) TestRunner() lang.TestRunner { return nil }
func (l *analysisErrorLanguage) DeadCodeDetector() lang.DeadCodeDetector { return nil }