github.com/kjda/gocoverview@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/kjda/gocoverview/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 }