mcubik/goverreport

View on GitHub
report/report.go

Summary

Maintainability
A
1 hr
Test Coverage
package report

import (
    "errors"
    "fmt"
    "path/filepath"
    "sort"
    "strings"

    "golang.org/x/tools/cover"
)

// Coverage summary for a file or module
type Summary struct {
    Name                                       string
    Blocks, Stmts, MissingBlocks, MissingStmts int
    BlockCoverage, StmtCoverage                float64
}

// Report of the coverage results
type Report struct {
    Total Summary   // Global coverage
    Files []Summary // Coverage by file
}

// Generates a coverage report given the coverage profile file, and the following configurations:
// exclusions: packages to be excluded (if a package is excluded, all its subpackages are excluded as well)
// sortBy: the order in which the files will be sorted in the report (see sortResults)
// order: the direction of the the sorting
func GenerateReport(coverprofile string, root string, exclusions []string, sortBy, order string, packages bool) (Report, error) {
    profiles, err := cover.ParseProfiles(coverprofile)
    if err != nil {
        return Report{}, fmt.Errorf("Invalid coverprofile: '%s'", err)
    }
    total := &accumulator{name: "Total"}
    files := make(map[string]*accumulator)
    for _, profile := range profiles {
        fileName := normalizeName(profile.FileName, root, packages)
        if isExcluded(fileName, exclusions) {
            continue
        }
        fileCover, ok := files[fileName]
        if !ok {
            // Create new accumulator
            fileCover = &accumulator{name: fileName}
            files[fileName] = fileCover
        }
        total.addAll(profile.Blocks)
        fileCover.addAll(profile.Blocks)
    }
    return makeReport(total, files, sortBy, order)
}

// Removes root dir part if configured to do so
func normalizeName(fileName string, root string, packages bool) string {
    if packages {
        fileName = filepath.Dir(fileName)
    }

    if root == "" {
        return fileName
    }
    if packages {
        return "." + strings.Replace(fileName, root, "", -1)
    }
    return strings.Replace(fileName, root, "", -1)
}

func isExcluded(fileName string, exclusions []string) bool {
    for _, exclusion := range exclusions {
        if strings.HasPrefix(fileName, exclusion) {
            return true
        }
    }
    return false
}

// Creates a Report struct from the coverage sumarization results
func makeReport(total *accumulator, files map[string]*accumulator, sortBy, order string) (Report, error) {
    fileReports := make([]Summary, 0, len(files))
    for _, fileCover := range files {
        fileReports = append(fileReports, fileCover.results())
    }
    if err := sortResults(fileReports, sortBy, order); err != nil {
        return Report{}, err
    }
    return Report{
        Total: total.results(),
        Files: fileReports}, nil
}

// Accumulates the coverage of a file and returns a summary
type accumulator struct {
    name                                       string
    blocks, stmts, coveredBlocks, coveredStmts int
}

// Accumulates a profile block
func (a *accumulator) add(block cover.ProfileBlock) {
    a.blocks++
    a.stmts += block.NumStmt
    if block.Count > 0 {
        a.coveredBlocks++
        a.coveredStmts += block.NumStmt
    }
}

func (a *accumulator) addAll(blocks []cover.ProfileBlock) {
    for _, block := range blocks {
        a.add(block)
    }
}

// Creates a summary with the accumulated values
func (a *accumulator) results() Summary {
    return Summary{
        Name:          a.name,
        Blocks:        a.blocks,
        Stmts:         a.stmts,
        MissingBlocks: a.blocks - a.coveredBlocks,
        MissingStmts:  a.stmts - a.coveredStmts,
        BlockCoverage: float64(a.coveredBlocks) / float64(a.blocks) * 100,
        StmtCoverage:  float64(a.coveredStmts) / float64(a.stmts) * 100}
}

// Sorts the individual coverage reports by a given column
// (block --block coverage--, stmt --stmt coverage--, missing-blocks or missing-stmts)
// and a sorting direction (asc or desc)
func sortResults(reports []Summary, mode string, order string) error {
    var reverse bool
    var cmp func(i, j int) bool
    switch order {
    case "asc":
        reverse = false
    case "desc":
        reverse = true
    default:
        return errors.New("Order must be either asc or desc")
    }
    switch mode {
    case "filename", "package":
        cmp = func(i, j int) bool {
            return reports[i].Name < reports[j].Name
        }
    case "block":
        cmp = func(i, j int) bool {
            return reports[i].BlockCoverage < reports[j].BlockCoverage
        }
    case "stmt":
        cmp = func(i, j int) bool {
            return reports[i].StmtCoverage < reports[j].StmtCoverage
        }
    case "missing-blocks":
        cmp = func(i, j int) bool {
            return reports[i].MissingBlocks < reports[j].MissingBlocks
        }
    case "missing-stmts":
        cmp = func(i, j int) bool {
            return reports[i].MissingStmts < reports[j].MissingStmts
        }
    default:
        return errors.New("Invalid sort colum, must be one of filename, package, block, stmt, missing-blocks or missing-stmts")
    }
    sort.Slice(reports, func(i, j int) bool {
        if reverse {
            return !cmp(i, j)
        } else {
            return cmp(i, j)
        }
    })
    return nil
}