diff --git a/main.go b/main.go new file mode 100644 index 0000000..8e24313 --- /dev/null +++ b/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path" + "strings" + + "git.xdrm.io/go/cliverage/coverage" +) + +func main() { + var codepath string + var coverfile string + var filepath string + var showUsage bool + + flag.StringVar(&codepath, "d", "", "path to where lives your code") + flag.StringVar(&coverfile, "c", "", "path to the coverage file") + flag.StringVar(&filepath, "f", ".", "path to the file to print relative to -d option") + flag.BoolVar(&showUsage, "h", false, "") + flag.Usage = usage + flag.Parse() + + // 1. show usage + if showUsage { + flag.Usage() + os.Exit(0) + } + + // 2. missing required arguments + if len(codepath) < 1 || len(coverfile) < 1 { + fmt.Printf("%s missing arguments.\n\n", warning("/!\\")) + fmt.Printf("run -h to show usage.\n") + os.Exit(1) + } + + // 3. check codepath + if info, err := os.Stat(codepath); true { + if err != nil && os.IsNotExist(err) { + fmt.Printf("%s codepath does not exist.\n\n", warning("/!\\")) + os.Exit(1) + } + + if !info.IsDir() { + fmt.Printf("%s codepath is not a directory.\n\n", warning("/!\\")) + os.Exit(1) + } + + if err != nil { + fmt.Printf("%s invalid codepath: %s.\n\n", warning("/!\\"), err) + os.Exit(1) + } + } + + // 4. check coverfile + if info, err := os.Stat(coverfile); true { + + if err != nil && os.IsNotExist(err) { + fmt.Printf("%s coverfile does not exist.\n\n", warning("/!\\")) + os.Exit(1) + } + + if info.IsDir() { + fmt.Printf("%s coverfile is not a file.\n\n", warning("/!\\")) + os.Exit(1) + } + + if err != nil { + fmt.Printf("%s invalid coverfile: %s.\n\n", warning("/!\\"), err) + os.Exit(1) + } + } + + // 5. extract package path + os.Chdir(codepath) + var getGoPkgPath = exec.Command("go", "list", ".") + pkgPath, err := getGoPkgPath.Output() + if err != nil { + fmt.Printf("%s cannot get go package path: %s.\n\n", warning("/!\\"), err) + os.Exit(1) + } + + // 6. parse coverfile + fdCoverfile, err := os.OpenFile(coverfile, os.O_RDONLY, 0755) + if err != nil { + fmt.Printf("%s cannot open coverfile: %s.\n\n", warning("/!\\"), err) + os.Exit(1) + } + defer fdCoverfile.Close() + parsedCoverage, err := coverage.Parse(fdCoverfile, strings.Trim(string(pkgPath), " \t\r\n")) + if err != nil { + fmt.Printf("%s error reading coverfile: %s.\n\n", warning("/!\\"), err) + os.Exit(1) + } + + // 7. iterate over files + coveredFiles := parsedCoverage.GetFile(filepath) + for fname, coveredChunks := range coveredFiles.Files { + path := path.Join(codepath, fname) + fmt.Printf("----- %s -----\n", fname) + + fdFile, err := os.OpenFile(path, os.O_RDONLY, 0755) + if err != nil { + fmt.Printf("%s cannot open file '%s': %s.\n\n", warning("/!\\"), path, err) + continue + } + defer fdFile.Close() + printCoverage(fdFile, os.Stdout, coveredChunks) + fmt.Printf("\n\n") + } + + os.Exit(0) +} diff --git a/print.go b/print.go index f1a577e..9ba7b42 100644 --- a/print.go +++ b/print.go @@ -1,6 +1,13 @@ package main -import "fmt" +import ( + "bufio" + "fmt" + "io" + "sort" + + "git.xdrm.io/go/cliverage/coverage" +) func bold(str string) string { return fmt.Sprintf("\x1b[1m%s\x1b[0m", str) @@ -10,6 +17,128 @@ func success(str string) string { return fmt.Sprintf("\x1b[38;2;26;221;120m%s\x1b[0m", str) } -func error(str string) string { +func warning(str string) string { return fmt.Sprintf("\x1b[38;2;221;26;120m%s\x1b[0m", str) } + +func printCoverage(r io.Reader, w io.Writer, chunks []coverage.Coverage) error { + reader := bufio.NewReader(r) + linen := uint64(0) + eof := false + + coverPrefix := "\x1b[38;2;26;221;120m" + coverSuffix := "\x1b[0m" + // coverPrefix := "<<<" + // coverSuffix := ">>>" + + for true { + linen++ + if eof { + break + } + + line, err := reader.ReadString('\n') + if err == io.EOF { + if len(line) < 1 { + break + } else { + eof = true + } + } else if err != nil { + return err + } + + // check if we got a chunk start + concerned := make([]coverage.Coverage, 0) + for _, c := range chunks { + if c.StartLine == linen || c.EndLine == linen { + concerned = append(concerned, c) + } + } + // sort by max column before + sort.Slice(concerned, sortChunksByColumn(concerned, linen)) + + // if no coverage, write directly + if len(concerned) < 1 { + _, err := w.Write([]byte(fmt.Sprintf("%d\t| %s", linen, line))) + if err != nil { + return err + } + continue + } + + // position each column with its prefix/suffix + for _, chunk := range concerned { + isStart := chunk.StartLine == linen + isEnd := chunk.EndLine == linen + + if isStart && isEnd { + firstColumn := chunk.StartColumn + prefix := coverPrefix + lastColumn := chunk.EndColumn + suffix := coverSuffix + if chunk.EndColumn > chunk.StartColumn { + firstColumn = chunk.EndColumn + prefix = coverSuffix + lastColumn = chunk.StartColumn + suffix = coverPrefix + } + + line = line[0:firstColumn-1] + prefix + line[firstColumn-1:] + if firstColumn != lastColumn { + line = line[0:lastColumn-1] + suffix + line[lastColumn-1:] + } + + continue + } + + if isEnd { + line = line[0:chunk.EndColumn-1] + coverSuffix + line[chunk.EndColumn-1:] + } else if isStart { + line = line[0:chunk.StartColumn-1] + coverPrefix + line[chunk.StartColumn-1:] + continue + } + } + + _, err = w.Write([]byte(fmt.Sprintf("%d\t| %s", linen, line))) + if err != nil { + return err + } + + } + return nil + +} + +func sortChunksByColumn(chunks []coverage.Coverage, lineNumber uint64) func(i, j int) bool { + return func(i, j int) bool { + // if start is concerned + iConcernedColumn := chunks[i].StartColumn + if chunks[i].EndLine == lineNumber { + iConcernedColumn = chunks[i].EndColumn + if chunks[i].StartLine == lineNumber && chunks[i].StartColumn > iConcernedColumn { + iConcernedColumn = chunks[i].StartColumn + } + } + + jConcernedColumn := chunks[j].StartColumn + if chunks[j].EndLine == lineNumber { + jConcernedColumn = chunks[j].EndColumn + if chunks[j].StartLine == lineNumber && chunks[j].StartColumn > jConcernedColumn { + jConcernedColumn = chunks[j].StartColumn + } + } + + if iConcernedColumn == jConcernedColumn { + // return the closing chunk before + if chunks[i].StartLine == lineNumber && chunks[j].EndLine == lineNumber { + return true + } + if chunks[i].EndLine == lineNumber && chunks[j].StartLine == lineNumber { + return false + } + } + + return iConcernedColumn >= jConcernedColumn + } +}