返回 DeepSeek-Reasonix
mutation.go
根目录 / cmd / e2ebench / mutation.go
1 package main
2
3 import (
4 "go/ast"
5 "go/parser"
6 "go/printer"
7 "go/token"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strings"
12 "time"
13 )
14
15 const maxMutants = 15
16
17 type mutationResult struct {
18 caught, total int
19 survivors []string
20 }
21
22 // runMutation gives a behavioral signal that the differential can't for additive
23 // PRs: it replaces each changed function's body with a zero-value return (which
24 // compiles for any signature via *new(T)), runs only the agent's new tests for
25 // that package, and records whether they catch the mutation. A caught mutant
26 // means a test actually asserts that function's output; a survivor means the new
27 // tests don't check it.
28 func runMutation(repo, base string, srcFiles []string, refs []testRef) mutationResult {
29 changed := changedLineSet(repo, base, srcFiles)
30 byPkg := map[string][]string{}
31 for _, r := range refs {
32 byPkg[r.pkg] = append(byPkg[r.pkg], r.name)
33 }
34
35 var res mutationResult
36 for _, file := range srcFiles {
37 if res.total >= maxMutants {
38 break
39 }
40 pkg := "./" + filepath.ToSlash(filepath.Dir(file))
41 tests := byPkg[pkg]
42 if len(tests) == 0 {
43 continue // no new tests to attribute a catch to
44 }
45 runRe := "^(" + strings.Join(tests, "|") + ")$"
46
47 abs := filepath.Join(repo, filepath.FromSlash(file))
48 srcB, err := os.ReadFile(abs)
49 if err != nil {
50 continue
51 }
52 fset := token.NewFileSet()
53 f, err := parser.ParseFile(fset, abs, srcB, 0)
54 if err != nil {
55 continue
56 }
57 src := string(srcB)
58 for _, fd := range changedFuncs(fset, f, changed[file]) {
59 if res.total >= maxMutants {
60 break
61 }
62 res.total++
63 lb := fset.Position(fd.Body.Lbrace).Offset
64 rb := fset.Position(fd.Body.Rbrace).Offset
65 mutated := src[:lb] + mutantBody(fset, fd.Type.Results) + src[rb+1:]
66 if os.WriteFile(abs, []byte(mutated), 0o644) != nil {
67 res.total--
68 continue
69 }
70 cmd := exec.Command("go", "test", "-run", runRe, pkg)
71 cmd.Dir = repo
72 cmd.WaitDelay = 2 * time.Minute // bound the wait for a mutant that wedges a test
73 // Restore source even on panic; a file left mutated would corrupt the next mutant.
74 restored := false
75 defer func() {
76 if !restored {
77 _ = os.WriteFile(abs, srcB, 0o644)
78 }
79 }()
80 caught := cmd.Run() != nil
81 _ = os.WriteFile(abs, srcB, 0o644)
82 restored = true
83 if caught {
84 res.caught++
85 } else {
86 res.survivors = append(res.survivors, fd.Name.Name)
87 }
88 }
89 }
90 return res
91 }
92
93 // changedFuncs returns the funcs in f whose line range overlaps a changed line.
94 // main/init and nil-named decls are skipped (no meaningful return to mutate).
95 func changedFuncs(fset *token.FileSet, f *ast.File, lines map[int]bool) []*ast.FuncDecl {
96 var out []*ast.FuncDecl
97 for _, decl := range f.Decls {
98 fd, ok := decl.(*ast.FuncDecl)
99 if !ok || fd.Body == nil || fd.Name == nil {
100 continue
101 }
102 name := fd.Name.Name
103 if name == "main" || name == "init" {
104 continue
105 }
106 start := fset.Position(fd.Pos()).Line
107 end := fset.Position(fd.End()).Line
108 for ln := range lines {
109 if ln >= start && ln <= end {
110 out = append(out, fd)
111 break
112 }
113 }
114 }
115 return out
116 }
117
118 // mutantBody returns a replacement body that returns the zero value for each
119 // result. *new(T) is the zero value of any type T, so this compiles for every
120 // signature without naming the results or knowing their concrete types.
121 func mutantBody(fset *token.FileSet, results *ast.FieldList) string {
122 if results == nil || len(results.List) == 0 {
123 return "{\n}"
124 }
125 var rets []string
126 for _, field := range results.List {
127 t := "*new(" + printType(fset, field.Type) + ")"
128 n := len(field.Names)
129 if n == 0 {
130 n = 1
131 }
132 for i := 0; i < n; i++ {
133 rets = append(rets, t)
134 }
135 }
136 return "{\n\treturn " + strings.Join(rets, ", ") + "\n}"
137 }
138
139 func printType(fset *token.FileSet, e ast.Expr) string {
140 var b strings.Builder
141 _ = printer.Fprint(&b, fset, e)
142 return b.String()
143 }
144
144 lines GO