github.com/saurabh-prakash/go-cover-view@v0.0.1/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"log"
    11  	"math"
    12  	"os"
    13  	"os/exec"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"golang.org/x/mod/modfile"
    18  	"golang.org/x/tools/cover"
    19  )
    20  
    21  var (
    22  	modFile   string
    23  	report    string
    24  	covered   string
    25  	uncovered string
    26  
    27  	output string
    28  	ci     string
    29  
    30  	gitDiffOnly bool
    31  	gitDiffBase string
    32  
    33  	writer io.Writer = os.Stdout
    34  )
    35  
    36  type _modfile interface {
    37  	Path() string
    38  }
    39  
    40  type modfileFromJSON struct {
    41  	Module struct {
    42  		Path string
    43  	}
    44  }
    45  
    46  func (m *modfileFromJSON) Path() string {
    47  	return m.Module.Path
    48  }
    49  
    50  type xmodfile struct {
    51  	*modfile.File
    52  }
    53  
    54  func (m *xmodfile) Path() string {
    55  	return m.Module.Mod.Path
    56  }
    57  
    58  func init() {
    59  	flag.StringVar(&modFile, "modfile", "", "go.mod path")
    60  	flag.StringVar(&report, "report", "coverage.txt", "coverage report path")
    61  	flag.StringVar(&covered, "covered", "O", "prefix for covered line")
    62  	flag.StringVar(&uncovered, "uncovered", "X", "prefix for uncovered line")
    63  
    64  	flag.StringVar(&output, "output", "simple", `output type: available values "simple", "json", "markdown"`)
    65  	flag.StringVar(&ci, "ci", "", strings.TrimSpace(`
    66  ci type: available values "", "github-actions"
    67  github-actions:
    68  	Comment the markdown report to Pull Request on GitHub.
    69  `))
    70  
    71  	flag.BoolVar(&gitDiffOnly, "git-diff-only", false, "only files with git diff")
    72  	flag.StringVar(&gitDiffBase, "git-diff-base", "origin/master", "git diff base")
    73  }
    74  
    75  func main() {
    76  	flag.Parse()
    77  	if err := _main(); err != nil {
    78  		log.Fatal(err)
    79  	}
    80  }
    81  
    82  func _main() error {
    83  	m, err := parseModfile()
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	profiles, err := cover.ParseProfiles(report)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	switch ci {
    94  	case "github-actions":
    95  		gitDiffOnly = true
    96  		return upsertGitHubPullRequestComment(profiles, m.Path())
    97  	}
    98  
    99  	var r renderer
   100  	switch output {
   101  	case "", "simple":
   102  		r = &simpleRenderer{}
   103  	case "json":
   104  		r = &jsonRenderer{}
   105  	case "markdown":
   106  		r = &markdownRenderer{}
   107  	default:
   108  		return fmt.Errorf("invalid output type %s", output)
   109  	}
   110  
   111  	return r.Render(writer, profiles, m.Path())
   112  }
   113  
   114  func parseModfile() (_modfile, error) {
   115  	if modFile == "" {
   116  		output, err := exec.Command("go", "mod", "edit", "-json").Output()
   117  		if err != nil {
   118  			return nil, fmt.Errorf("go mod edit -json: %w", err)
   119  		}
   120  		var m modfileFromJSON
   121  		if err := json.Unmarshal(output, &m); err != nil {
   122  			return nil, err
   123  		}
   124  		return &m, nil
   125  	}
   126  
   127  	data, err := ioutil.ReadFile(modFile)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	f, err := modfile.Parse(modFile, data, nil)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	return &xmodfile{File: f}, nil
   137  }
   138  
   139  func getLines(profile *cover.Profile, module string) ([]string, error) {
   140  	// github.com/saurabh-prakash/go-cover-view/main.go -> ./main.go
   141  	p := strings.ReplaceAll(profile.FileName, module, ".")
   142  	f, err := os.Open(p)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	defer f.Close()
   147  
   148  	scanner := bufio.NewScanner(f)
   149  	lines := make([]string, 0, 1000)
   150  	for scanner.Scan() {
   151  		line := scanner.Text()
   152  		lines = append(lines, line)
   153  	}
   154  
   155  	width := int(math.Log10(float64(len(lines)))) + 1
   156  	if len(covered) > len(uncovered) {
   157  		width += len(covered) + 1
   158  	} else {
   159  		width += len(uncovered) + 1
   160  	}
   161  	w := strconv.Itoa(width)
   162  	for i, line := range lines {
   163  		var newLine string
   164  		if len(line) == 0 {
   165  			format := "%" + w + "d:"
   166  			newLine = fmt.Sprintf(format, i+1)
   167  		} else {
   168  			format := "%" + w + "d: %s"
   169  			newLine = fmt.Sprintf(format, i+1, line)
   170  		}
   171  		lines[i] = newLine
   172  	}
   173  
   174  	for _, block := range profile.Blocks {
   175  		var prefix string
   176  		if block.Count > 0 {
   177  			prefix = covered
   178  		} else {
   179  			prefix = uncovered
   180  		}
   181  		for i := block.StartLine - 1; i <= block.EndLine-1; i++ {
   182  			if i >= len(lines) {
   183  				return nil, fmt.Errorf("invalid line length: index=%d, len(lines)=%d", i, len(lines))
   184  			}
   185  			line := lines[i]
   186  			newLine := prefix + line[len(prefix):]
   187  			lines[i] = newLine
   188  		}
   189  	}
   190  
   191  	return lines, nil
   192  }
   193  
   194  func getDiffs() ([]string, error) {
   195  	if !gitDiffOnly {
   196  		return []string{}, nil
   197  	}
   198  	args := []string{"diff", "--name-only"}
   199  	if gitDiffBase != "" {
   200  		args = append(args, gitDiffBase)
   201  	}
   202  	_out, err := exec.Command("git", args...).Output()
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  	out := strings.TrimSpace(string(_out))
   207  	diffs := strings.Split(out, "\n")
   208  	return diffs, nil
   209  }
   210  
   211  func containsDiff(filename, path string, diffs []string) bool {
   212  	for _, diff := range diffs {
   213  		name := fmt.Sprintf("%s/%s", path, diff)
   214  		if filename == name {
   215  			return true
   216  		}
   217  	}
   218  	return false
   219  }
   220  
   221  type renderer interface {
   222  	Render(w io.Writer, profiles []*cover.Profile, path string) error
   223  }
   224  
   225  type simpleRenderer struct{}
   226  
   227  var _ renderer = (*simpleRenderer)(nil)
   228  
   229  func (r *simpleRenderer) Render(w io.Writer, profiles []*cover.Profile, path string) error {
   230  	reports, err := getSimpleReports(profiles, path)
   231  	if err != nil {
   232  		return err
   233  	}
   234  	bw := bufio.NewWriter(w)
   235  	for _, r := range reports {
   236  		fmt.Fprintln(bw, r.FileName)
   237  		fmt.Fprintln(bw, r.Report)
   238  		fmt.Fprintln(bw)
   239  	}
   240  	return bw.Flush()
   241  }
   242  
   243  type simpleReport struct {
   244  	FileName string
   245  	Report   string
   246  }
   247  
   248  func newSimpleReport(fileName string, lines []string) *simpleReport {
   249  	return &simpleReport{
   250  		FileName: fileName,
   251  		Report:   strings.Join(lines, "\n"),
   252  	}
   253  }
   254  
   255  func getSimpleReports(profiles []*cover.Profile, path string) ([]*simpleReport, error) {
   256  	diffs, err := getDiffs()
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	results := make([]*simpleReport, 0, len(profiles))
   261  	for _, profile := range profiles {
   262  		lines, err := getLines(profile, path)
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  		if gitDiffOnly {
   267  			if containsDiff(profile.FileName, path, diffs) {
   268  				results = append(results, newSimpleReport(profile.FileName, lines))
   269  			}
   270  			continue
   271  		}
   272  		results = append(results, newSimpleReport(profile.FileName, lines))
   273  	}
   274  	return results, nil
   275  }