144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/token"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const maxMutants = 15
|
|
|
|
type mutationResult struct {
|
|
caught, total int
|
|
survivors []string
|
|
}
|
|
|
|
// runMutation gives a behavioral signal that the differential can't for additive
|
|
// PRs: it replaces each changed function's body with a zero-value return (which
|
|
// compiles for any signature via *new(T)), runs only the agent's new tests for
|
|
// that package, and records whether they catch the mutation. A caught mutant
|
|
// means a test actually asserts that function's output; a survivor means the new
|
|
// tests don't check it.
|
|
func runMutation(repo, base string, srcFiles []string, refs []testRef) mutationResult {
|
|
changed := changedLineSet(repo, base, srcFiles)
|
|
byPkg := map[string][]string{}
|
|
for _, r := range refs {
|
|
byPkg[r.pkg] = append(byPkg[r.pkg], r.name)
|
|
}
|
|
|
|
var res mutationResult
|
|
for _, file := range srcFiles {
|
|
if res.total >= maxMutants {
|
|
break
|
|
}
|
|
pkg := "./" + filepath.ToSlash(filepath.Dir(file))
|
|
tests := byPkg[pkg]
|
|
if len(tests) == 0 {
|
|
continue // no new tests to attribute a catch to
|
|
}
|
|
runRe := "^(" + strings.Join(tests, "|") + ")$"
|
|
|
|
abs := filepath.Join(repo, filepath.FromSlash(file))
|
|
srcB, err := os.ReadFile(abs)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, abs, srcB, 0)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
src := string(srcB)
|
|
for _, fd := range changedFuncs(fset, f, changed[file]) {
|
|
if res.total >= maxMutants {
|
|
break
|
|
}
|
|
res.total++
|
|
lb := fset.Position(fd.Body.Lbrace).Offset
|
|
rb := fset.Position(fd.Body.Rbrace).Offset
|
|
mutated := src[:lb] + mutantBody(fset, fd.Type.Results) + src[rb+1:]
|
|
if os.WriteFile(abs, []byte(mutated), 0o644) != nil {
|
|
res.total--
|
|
continue
|
|
}
|
|
cmd := exec.Command("go", "test", "-run", runRe, pkg)
|
|
cmd.Dir = repo
|
|
cmd.WaitDelay = 2 * time.Minute // bound the wait for a mutant that wedges a test
|
|
// Restore source even on panic; a file left mutated would corrupt the next mutant.
|
|
restored := false
|
|
defer func() {
|
|
if !restored {
|
|
_ = os.WriteFile(abs, srcB, 0o644)
|
|
}
|
|
}()
|
|
caught := cmd.Run() != nil
|
|
_ = os.WriteFile(abs, srcB, 0o644)
|
|
restored = true
|
|
if caught {
|
|
res.caught++
|
|
} else {
|
|
res.survivors = append(res.survivors, fd.Name.Name)
|
|
}
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// changedFuncs returns the funcs in f whose line range overlaps a changed line.
|
|
// main/init and nil-named decls are skipped (no meaningful return to mutate).
|
|
func changedFuncs(fset *token.FileSet, f *ast.File, lines map[int]bool) []*ast.FuncDecl {
|
|
var out []*ast.FuncDecl
|
|
for _, decl := range f.Decls {
|
|
fd, ok := decl.(*ast.FuncDecl)
|
|
if !ok || fd.Body == nil || fd.Name == nil {
|
|
continue
|
|
}
|
|
name := fd.Name.Name
|
|
if name == "main" || name == "init" {
|
|
continue
|
|
}
|
|
start := fset.Position(fd.Pos()).Line
|
|
end := fset.Position(fd.End()).Line
|
|
for ln := range lines {
|
|
if ln >= start && ln <= end {
|
|
out = append(out, fd)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// mutantBody returns a replacement body that returns the zero value for each
|
|
// result. *new(T) is the zero value of any type T, so this compiles for every
|
|
// signature without naming the results or knowing their concrete types.
|
|
func mutantBody(fset *token.FileSet, results *ast.FieldList) string {
|
|
if results == nil || len(results.List) == 0 {
|
|
return "{\n}"
|
|
}
|
|
var rets []string
|
|
for _, field := range results.List {
|
|
t := "*new(" + printType(fset, field.Type) + ")"
|
|
n := len(field.Names)
|
|
if n == 0 {
|
|
n = 1
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
rets = append(rets, t)
|
|
}
|
|
}
|
|
return "{\n\treturn " + strings.Join(rets, ", ") + "\n}"
|
|
}
|
|
|
|
func printType(fset *token.FileSet, e ast.Expr) string {
|
|
var b strings.Builder
|
|
_ = printer.Fprint(&b, fset, e)
|
|
return b.String()
|
|
}
|