From 94e28d260d5a14813b388f1b85bf1e73fff28c08 Mon Sep 17 00:00:00 2001 From: Kanan <93033289+kriss39@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:02:35 +0400 Subject: [PATCH 1/2] test(mutation): expose swallowed collection errors --- internal/mutation/mutation_test.go | 78 ++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/internal/mutation/mutation_test.go b/internal/mutation/mutation_test.go index 7a1b4c5..a8a1dd9 100644 --- a/internal/mutation/mutation_test.go +++ b/internal/mutation/mutation_test.go @@ -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: @@ -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 } From 53b0305713a99945504a354a274f2ba36ebce379 Mon Sep 17 00:00:00 2001 From: Kanan <93033289+kriss39@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:03:58 +0400 Subject: [PATCH 2/2] fix(mutation): propagate collection errors --- internal/mutation/mutation.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/mutation/mutation.go b/internal/mutation/mutation.go index aa3af2b..a72af05 100644 --- a/internal/mutation/mutation.go +++ b/internal/mutation/mutation.go @@ -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{ @@ -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() @@ -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{ @@ -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: