| package main |
| |
| import ( |
| "encoding/xml" |
| "fmt" |
| "go/ast" |
| "go/build" |
| "go/parser" |
| "go/token" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strings" |
| "time" |
| ) |
| |
| func main() { |
| convert(os.Stdin, os.Stdout) |
| } |
| |
| func convert(in io.Reader, out io.Writer) { |
| profiles, err := ParseProfiles(in) |
| if err != nil { |
| panic("Can't parse profiles") |
| } |
| |
| srcDirs := build.Default.SrcDirs() |
| sources := make([]*Source, len(srcDirs)) |
| for i, dir := range srcDirs { |
| sources[i] = &Source{dir} |
| } |
| |
| coverage := Coverage{Sources: sources, Packages: nil, Timestamp: time.Now().UnixNano() / int64(time.Millisecond)} |
| coverage.parseProfiles(profiles) |
| |
| fmt.Fprintf(out, xml.Header) |
| fmt.Fprintf(out, "<!DOCTYPE coverage SYSTEM \"http://cobertura.sourceforge.net/xml/coverage-03.dtd\">\n") |
| |
| encoder := xml.NewEncoder(out) |
| encoder.Indent("", "\t") |
| err = encoder.Encode(coverage) |
| if err != nil { |
| panic(err) |
| } |
| |
| fmt.Fprintln(out) |
| } |
| |
| func (cov *Coverage) parseProfiles(profiles []*Profile) error { |
| cov.Packages = []*Package{} |
| for _, profile := range profiles { |
| cov.parseProfile(profile) |
| } |
| return nil |
| } |
| |
| func (cov *Coverage) parseProfile(profile *Profile) error { |
| fileName := profile.FileName |
| absFilePath, err := findFile(fileName) |
| if err != nil { |
| return err |
| } |
| fset := token.NewFileSet() |
| parsed, err := parser.ParseFile(fset, absFilePath, nil, 0) |
| if err != nil { |
| return err |
| } |
| data, err := ioutil.ReadFile(absFilePath) |
| if err != nil { |
| return err |
| } |
| |
| pkgPath, _ := filepath.Split(fileName) |
| pkgPath = strings.TrimRight(pkgPath, string(os.PathSeparator)) |
| |
| var pkg *Package |
| for _, p := range cov.Packages { |
| if p.Name == pkgPath { |
| pkg = p |
| } |
| } |
| if pkg == nil { |
| pkg = &Package{Name: pkgPath, Classes: []*Class{}} |
| cov.Packages = append(cov.Packages, pkg) |
| } |
| visitor := &fileVisitor{ |
| fset: fset, |
| fileName: fileName, |
| fileData: data, |
| classes: make(map[string]*Class), |
| pkg: pkg, |
| profile: profile, |
| } |
| ast.Walk(visitor, parsed) |
| return nil |
| } |
| |
| type fileVisitor struct { |
| fset *token.FileSet |
| fileName string |
| fileData []byte |
| pkg *Package |
| classes map[string]*Class |
| profile *Profile |
| } |
| |
| func (v *fileVisitor) Visit(node ast.Node) ast.Visitor { |
| switch n := node.(type) { |
| case *ast.FuncDecl: |
| class := v.class(n) |
| method := v.method(n) |
| class.Methods = append(class.Methods, method) |
| for _, line := range method.Lines { |
| class.Lines = append(class.Lines, line) |
| } |
| } |
| return v |
| } |
| |
| func (v *fileVisitor) method(n *ast.FuncDecl) *Method { |
| method := &Method{Name: n.Name.Name} |
| method.Lines = []*Line{} |
| |
| start := v.fset.Position(n.Pos()) |
| end := v.fset.Position(n.End()) |
| startLine := start.Line |
| startCol := start.Column |
| endLine := end.Line |
| endCol := end.Column |
| // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. |
| for _, b := range v.profile.Blocks { |
| if b.StartLine > endLine || (b.StartLine == endLine && b.StartCol >= endCol) { |
| // Past the end of the function. |
| break |
| } |
| if b.EndLine < startLine || (b.EndLine == startLine && b.EndCol <= startCol) { |
| // Before the beginning of the function |
| continue |
| } |
| for i := b.StartLine; i <= b.EndLine; i++ { |
| method.Lines = append(method.Lines, &Line{Number: i, Hits: int64(b.Count)}) |
| } |
| } |
| return method |
| } |
| |
| func (v *fileVisitor) class(n *ast.FuncDecl) *Class { |
| className := v.recvName(n) |
| var class *Class = v.classes[className] |
| if class == nil { |
| class = &Class{Name: className, Filename: v.fileName, Methods: []*Method{}, Lines: []*Line{}} |
| v.classes[className] = class |
| v.pkg.Classes = append(v.pkg.Classes, class) |
| } |
| return class |
| } |
| |
| func (v *fileVisitor) recvName(n *ast.FuncDecl) string { |
| if n.Recv == nil { |
| return "-" |
| } |
| recv := n.Recv.List[0].Type |
| start := v.fset.Position(recv.Pos()) |
| end := v.fset.Position(recv.End()) |
| name := string(v.fileData[start.Offset:end.Offset]) |
| return strings.TrimSpace(strings.TrimLeft(name, "*")) |
| } |