github.com/masakurapa/gover-html/
91.6%
internal/
91.6%
cover/
89.6%
filter/
100%
filter.go
100%
cover.go
90.6%
func.go
82.5%
html/
97.2%
tree/
100%
tree.go
100%
html.go
95.4%
template.go
0%
option/
93.4%
error.go
0%
option.go
96.6%
profile/
89.2%
profile.go
92.3%
reader.go
87.5%
reader/
0%
reader.go
0%
Function | Coverage |
---|---|
Total Coverage |
90.6%
|
func ReadProfile(r io.Reader, f filter.Filter) (profile.Profiles, error) |
100%
|
func toInt(s string) int |
75%
|
func toProfiles(files map[string]*profile.Profile, f filter.Filter) (profile.Profiles, error) |
90.5%
|
func filterBlocks(blocks []profile.Block) []profile.Block |
100%
|
func makeImportDirMap(files map[string]*profile.Profile) (map[string]importDir, error) |
78.9%
|
func execGoList(files map[string]*profile.Profile) ([]byte, error) |
86.7%
|
- package cover
- import (
- "bufio"
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "os/exec"
- "path"
- "path/filepath"
- "regexp"
- "runtime"
- "sort"
- "strconv"
- "strings"
- "github.com/masakurapa/gover-html/internal/cover/filter"
- "github.com/masakurapa/gover-html/internal/profile"
- )
- var reg = regexp.MustCompile(`^(.+):([0-9]+)\.([0-9]+),([0-9]+)\.([0-9]+) ([0-9]+) ([0-9]+)$`)
- type importDir struct {
- modulePath string
- relative string
- dir string
- }
- // ReadProfile is reads profiling data
- func ReadProfile(r io.Reader, f filter.Filter) (profile.Profiles, error) {
- files := make(map[string]*profile.Profile)
- modeOk := false
- id := 1
- s := bufio.NewScanner(r)
- for s.Scan() {
- line := s.Text()
- // first line must be "mode: xxx"
- if !modeOk {
- const p = "mode: "
- if !strings.HasPrefix(line, p) || line == p {
- return nil, fmt.Errorf("first line must be mode: %q", line)
- }
- modeOk = true
- continue
- }
- matches := reg.FindStringSubmatch(line)
- if matches == nil {
- return nil, fmt.Errorf("%q does not match expected format: %v", line, reg)
- }
- fileName := matches[1]
- p := files[fileName]
- if p == nil {
- p = &profile.Profile{ID: id, FileName: fileName}
- files[fileName] = p
- id++
- }
- p.Blocks = append(p.Blocks, profile.Block{
- StartLine: toInt(matches[2]),
- StartCol: toInt(matches[3]),
- EndLine: toInt(matches[4]),
- EndCol: toInt(matches[5]),
- NumState: toInt(matches[6]),
- Count: toInt(matches[7]),
- })
- }
- return toProfiles(files, f)
- }
- func toInt(s string) int {
- i, err := strconv.Atoi(s)
- if err != nil {
- panic(err)
- }
- return i
- }
- func toProfiles(files map[string]*profile.Profile, f filter.Filter) (profile.Profiles, error) {
- dirs, err := makeImportDirMap(files)
- if err != nil {
- return nil, err
- }
- profiles := make(profile.Profiles, 0, len(files))
- for _, p := range files {
- p.Blocks = filterBlocks(p.Blocks)
- sort.SliceStable(p.Blocks, func(i, j int) bool {
- bi, bj := p.Blocks[i], p.Blocks[j]
- return bi.StartLine < bj.StartLine || bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol
- })
- d := dirs[path.Dir(p.FileName)]
- if !f.IsOutputTarget(d.relative, filepath.Base(p.FileName)) {
- continue
- }
- p.Dir = d.dir
- p.ModulePath = d.modulePath
- pp, err := makeNewProfile(p, f)
- if err != nil {
- return nil, err
- }
- profiles = append(profiles, *pp)
- }
- sort.SliceStable(profiles, func(i, j int) bool {
- return profiles[i].FileName < profiles[j].FileName
- })
- return profiles, nil
- }
- func filterBlocks(blocks []profile.Block) []profile.Block {
- index := func(bs []profile.Block, b *profile.Block) int {
- for i, bb := range bs {
- if bb.StartLine == b.StartLine &&
- bb.StartCol == b.StartCol &&
- bb.EndLine == b.EndLine &&
- bb.EndCol == b.EndCol {
- return i
- }
- }
- return -1
- }
- newBlocks := make([]profile.Block, 0, len(blocks))
- for _, b := range blocks {
- i := index(newBlocks, &b)
- if i == -1 {
- newBlocks = append(newBlocks, b)
- continue
- }
- if b.Count > 0 {
- newBlocks[i] = b
- }
- }
- return newBlocks
- }
- func makeImportDirMap(files map[string]*profile.Profile) (map[string]importDir, error) {
- stdout, err := execGoList(files)
- if err != nil {
- return nil, err
- }
- pkgs := make(map[string]importDir)
- if len(stdout) == 0 {
- return pkgs, nil
- }
- type pkg struct {
- Dir string
- Module *struct {
- Path string
- Dir string
- }
- ImportPath string
- Error *struct {
- Err string
- }
- }
- dec := json.NewDecoder(bytes.NewReader(stdout))
- for {
- var p pkg
- err := dec.Decode(&p)
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, fmt.Errorf("decoding go list json: %v", err)
- }
- if p.Error != nil {
- return nil, fmt.Errorf(p.Error.Err)
- }
- // should have the same result for "pkg.ImportPath" and "path.Path(Profile.FileName)"
- pkgs[p.ImportPath] = importDir{
- modulePath: p.Module.Path,
- relative: strings.TrimPrefix(p.Dir, p.Module.Dir+"/"),
- dir: p.Dir,
- }
- }
- return pkgs, nil
- }
- // execute "go list" command
- func execGoList(files map[string]*profile.Profile) ([]byte, error) {
- dirs := make([]string, 0, len(files))
- m := make(map[string]struct{})
- for _, p := range files {
- if p.IsRelativeOrAbsolute() {
- continue
- }
- dir := path.Dir(p.FileName)
- if _, ok := m[dir]; !ok {
- m[dir] = struct{}{}
- dirs = append(dirs, dir)
- }
- }
- if len(dirs) == 0 {
- return make([]byte, 0), nil
- }
- cmdName := filepath.Join(runtime.GOROOT(), "bin/go")
- args := append([]string{"list", "-e", "-json"}, dirs...)
- cmd := exec.Command(cmdName, args...)
- return cmd.Output()
- }
Function | Coverage |
---|---|
Total Coverage |
100%
|
func New(opt option.Option) Filter |
100%
|
func (f *filter) IsOutputTarget(relativePath, fileName string) bool |
100%
|
func (f *filter) IsOutputTargetFunc(relativePath, structName, funcName string) bool |
100%
|
func (f *filter) hasPrefix(path, fileName, prefix string) bool |
100%
|
func (f *filter) convertPathForValidate(relativePath string) string |
100%
|
- package filter
- import (
- "path/filepath"
- "strings"
- "github.com/masakurapa/gover-html/internal/option"
- )
- // Filter is filter the output directory
- type Filter interface {
- IsOutputTarget(string, string) bool
- IsOutputTargetFunc(string, string, string) bool
- }
- type filter struct {
- opt option.Option
- }
- // New is initialize the filter
- func New(opt option.Option) Filter {
- return &filter{opt: opt}
- }
- // IsOutputTarget returns true if output target
- // The "relativePath" must be relative to the base path
- func (f *filter) IsOutputTarget(relativePath, fileName string) bool {
- // absolute path is always NG
- if strings.HasPrefix(relativePath, "/") {
- return false
- }
- path := f.convertPathForValidate(relativePath)
- for _, s := range f.opt.Exclude {
- if f.hasPrefix(path, fileName, s) {
- return false
- }
- }
- if len(f.opt.Include) == 0 {
- return true
- }
- for _, s := range f.opt.Include {
- if f.hasPrefix(path, fileName, s) {
- return true
- }
- }
- return false
- }
- func (f *filter) IsOutputTargetFunc(relativePath, structName, funcName string) bool {
- path := f.convertPathForValidate(relativePath)
- dir := filepath.Dir(path)
- for _, opt := range f.opt.ExcludeFunc {
- if opt.Path != "" && opt.Path != path && opt.Path != dir {
- continue
- }
- if opt.Struct != "" && opt.Struct != structName {
- continue
- }
- if opt.Func != funcName {
- continue
- }
- return false
- }
- return true
- }
- func (f *filter) hasPrefix(path, fileName, prefix string) bool {
- if path == prefix || strings.HasPrefix(path, prefix+"/") {
- return true
- }
- return path+"/"+fileName == prefix
- }
- func (f *filter) convertPathForValidate(relativePath string) string {
- path := strings.TrimPrefix(relativePath, "./")
- return strings.TrimSuffix(path, "/")
- }
Function | Coverage |
---|---|
Total Coverage |
82.5%
|
func (v *funcVisitor) Visit(node ast.Node) ast.Visitor |
85.7%
|
func makeNewProfile(prof *profile.Profile, f filter.Filter) (*profile.Profile, error) |
75%
|
func findFuncs(name string) ([]*funcExtent, error) |
85.7%
|
func newProfile(prof *profile.Profile, exts []*funcExtent, f filter.Filter) *profile.Profile |
80%
|
- package cover
- import (
- "bytes"
- "go/ast"
- "go/format"
- "go/parser"
- "go/token"
- "github.com/masakurapa/gover-html/internal/cover/filter"
- "github.com/masakurapa/gover-html/internal/profile"
- )
- type funcExtent struct {
- structName string
- funcName string
- name string
- startLine int
- startCol int
- endLine int
- endCol int
- }
- type funcVisitor struct {
- fset *token.FileSet
- name string
- astFile *ast.File
- funcs []*funcExtent
- }
- func (v *funcVisitor) Visit(node ast.Node) ast.Visitor {
- switch n := node.(type) {
- case *ast.FuncDecl:
- if n.Body == nil {
- break
- }
- start := v.fset.Position(n.Pos())
- end := v.fset.Position(n.End())
- var buf bytes.Buffer
- err := format.Node(&buf, token.NewFileSet(), &ast.FuncDecl{
- Name: n.Name,
- Recv: n.Recv,
- Type: n.Type,
- })
- if err != nil {
- panic(err)
- }
- src, err := format.Source(buf.Bytes())
- if err != nil {
- panic(err)
- }
- var structName string
- if n.Recv != nil && len(n.Recv.List) > 0 {
- if t, ok := n.Recv.List[0].Type.(*ast.Ident); ok {
- structName = t.Name
- }
- if t, ok := n.Recv.List[0].Type.(*ast.StarExpr); ok {
- structName = t.X.(*ast.Ident).Name
- }
- }
- fe := &funcExtent{
- structName: structName,
- funcName: n.Name.Name,
- name: string(src),
- startLine: start.Line,
- startCol: start.Column,
- endLine: end.Line,
- endCol: end.Column,
- }
- v.funcs = append(v.funcs, fe)
- }
- return v
- }
- func makeNewProfile(prof *profile.Profile, f filter.Filter) (*profile.Profile, error) {
- exts, err := findFuncs(prof.FilePath())
- if err != nil {
- return nil, err
- }
- return newProfile(prof, exts, f), nil
- }
- func findFuncs(name string) ([]*funcExtent, error) {
- fset := token.NewFileSet()
- parsedFile, err := parser.ParseFile(fset, name, nil, 0)
- if err != nil {
- return nil, err
- }
- visitor := &funcVisitor{
- fset: fset,
- name: name,
- astFile: parsedFile,
- }
- ast.Walk(visitor, visitor.astFile)
- return visitor.funcs, nil
- }
- func newProfile(prof *profile.Profile, exts []*funcExtent, f filter.Filter) *profile.Profile {
- fncs := make(profile.Functions, 0, len(exts))
- blocks := make(profile.Blocks, 0, len(prof.Blocks))
- bi := 0
- for _, e := range exts {
- isCoverageBlock := f.IsOutputTargetFunc(prof.RemoveModulePathFromFileName(), e.structName, e.funcName)
- fnc := profile.Function{
- Name: e.name,
- StartLine: e.startLine,
- StartCol: e.startCol,
- }
- for bi < len(prof.Blocks) {
- b := prof.Blocks[bi]
- if b.StartLine < e.startLine {
- if isCoverageBlock {
- blocks = append(blocks, b)
- }
- bi++
- continue
- }
- if b.StartLine >= e.startLine &&
- b.EndLine <= e.endLine {
- fnc.Blocks = append(fnc.Blocks, b)
- if isCoverageBlock {
- blocks = append(blocks, b)
- }
- bi++
- continue
- }
- break
- }
- if isCoverageBlock {
- fncs = append(fncs, fnc)
- }
- }
- prof.Blocks = blocks
- prof.Functions = fncs
- return prof
- }
Function | Coverage |
---|---|
Total Coverage |
95.4%
|
func WriteTreeView(out io.Writer, profiles profile.Profiles, opt option.Option) error |
87.5%
|
func makeTemplateTree(tree *[]templateTree, nodes []tree.Node, indent int) |
100%
|
func writeSource(buf *bytes.Buffer, src []byte, prof *profile.Profile) |
97.4%
|
func writeChar(buf *bytes.Buffer, b byte) |
100%
|
- package html
- import (
- "bytes"
- "fmt"
- "html/template"
- "io"
- "os"
- "path"
- "github.com/masakurapa/gover-html/internal/html/tree"
- "github.com/masakurapa/gover-html/internal/option"
- "github.com/masakurapa/gover-html/internal/profile"
- )
- var (
- escapeChar = map[byte]string{
- '<': "<",
- '>': ">",
- '&': "&",
- '\t': " ",
- }
- )
- type templateData struct {
- Theme string
- Tree []templateTree
- Files []templateFile
- }
- type templateTree struct {
- ID int
- Name string
- Indent int
- Coverage float64
- IsFile bool
- }
- type templateFile struct {
- ID int
- Name string
- Body template.HTML
- Coverage float64
- Functions []templateFunc
- }
- type templateFunc struct {
- Name string
- Line int
- Coverage float64
- }
- // WriteTreeView outputs coverage as a tree view HTML file
- func WriteTreeView(out io.Writer, profiles profile.Profiles, opt option.Option) error {
- nodes := tree.Create(profiles)
- tmpTree := make([]templateTree, 0)
- makeTemplateTree(&tmpTree, nodes, 0)
- data := templateData{
- Theme: opt.Theme,
- Tree: tmpTree,
- Files: make([]templateFile, 0, len(profiles)),
- }
- var buf bytes.Buffer
- for _, p := range profiles {
- b, err := os.ReadFile(p.FilePath())
- if err != nil {
- return fmt.Errorf("can't read %q: %v", p.FileName, err)
- }
- writeSource(&buf, b, &p)
- f := templateFile{
- ID: p.ID,
- Name: p.FileName,
- Body: template.HTML(buf.String()),
- Coverage: p.Blocks.Coverage(),
- Functions: make([]templateFunc, 0, len(p.Functions)),
- }
- buf.Reset()
- for _, fn := range p.Functions {
- f.Functions = append(f.Functions, templateFunc{
- Name: fn.Name,
- Line: fn.StartLine,
- Coverage: fn.Blocks.Coverage(),
- })
- }
- data.Files = append(data.Files, f)
- }
- return parsedTreeTemplate.Execute(out, data)
- }
- func makeTemplateTree(tree *[]templateTree, nodes []tree.Node, indent int) {
- for _, node := range nodes {
- childBlocks := node.ChildBlocks()
- *tree = append(*tree, templateTree{
- Name: node.Name,
- Indent: indent,
- Coverage: childBlocks.Coverage(),
- })
- makeTemplateTree(tree, node.Dirs, indent+1)
- for _, p := range node.Files {
- *tree = append(*tree, templateTree{
- ID: p.ID,
- Name: path.Base(p.FileName),
- Indent: indent + 1,
- Coverage: p.Blocks.Coverage(),
- IsFile: true,
- })
- }
- }
- }
- func writeSource(buf *bytes.Buffer, src []byte, prof *profile.Profile) {
- bi := 0
- si := 0
- line := 1
- col := 1
- cov0 := false
- cov1 := false
- buf.WriteString("<ol>")
- for si < len(src) {
- if col == 1 {
- buf.WriteString(fmt.Sprintf(`<li id="file%d-%d">`, prof.ID, line))
- if cov0 {
- buf.WriteString(`<span class="cov0">`)
- }
- if cov1 {
- buf.WriteString(`<span class="cov1">`)
- }
- }
- if len(prof.Blocks) > bi {
- block := prof.Blocks[bi]
- if block.StartLine == line && block.StartCol == col {
- if block.Count == 0 {
- buf.WriteString(`<span class="cov0">`)
- cov0 = true
- } else {
- buf.WriteString(`<span class="cov1">`)
- cov1 = true
- }
- }
- if block.EndLine == line && block.EndCol == col || line > block.EndLine {
- buf.WriteString(`</span>`)
- bi++
- cov0 = false
- cov1 = false
- continue
- }
- }
- b := src[si]
- writeChar(buf, b)
- if b == '\n' {
- if cov0 || cov1 {
- buf.WriteString("</span>")
- }
- buf.WriteString("</li>")
- line++
- col = 0
- }
- si++
- col++
- }
- buf.WriteString("</ol>")
- }
- func writeChar(buf *bytes.Buffer, b byte) {
- if s, ok := escapeChar[b]; ok {
- buf.WriteString(s)
- return
- }
- buf.WriteByte(b)
- }
Function | Coverage |
---|---|
Total Coverage |
0%
|
- package html
- import (
- "fmt"
- "html/template"
- )
- var parsedTreeTemplate = template.
- Must(template.New("html").
- Funcs(template.FuncMap{
- "indent": func(i int) int { return i*24 + 12 },
- "coverageColor": func(coverage float64) template.CSS {
- // Professional color gradient with proper opacity
- var h float64
- if coverage < 50 {
- // Red to orange gradient
- h = coverage * 0.6 // 0-30 degrees (red to orange)
- } else if coverage < 80 {
- // Orange to yellow gradient
- h = 30 + ((coverage - 50) / 30 * 30) // 30-60 degrees
- } else {
- // Yellow to green gradient
- h = 60 + ((coverage - 80) / 20 * 60) // 60-120 degrees
- }
- return template.CSS(fmt.Sprintf("hsla(%.0f, 70%%, 50%%, 0.4)", h))
- },
- }).
- Parse(treeTemplate))
- const treeTemplate = `<!DOCTYPE html>
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Coverage Report</title>
- <style>
- * {
- box-sizing: border-box;
- }
- body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- font-size: 14px;
- line-height: 1.6;
- }
- /* Custom scrollbar */
- ::-webkit-scrollbar {
- width: 8px;
- height: 8px;
- }
- ::-webkit-scrollbar-track {
- background: transparent;
- }
- ::-webkit-scrollbar-thumb {
- background: rgba(156, 163, 175, 0.3);
- border-radius: 4px;
- }
- ::-webkit-scrollbar-thumb:hover {
- background: rgba(156, 163, 175, 0.5);
- }
- .dark ::-webkit-scrollbar-thumb {
- background: rgba(71, 85, 105, 0.5);
- }
- .dark ::-webkit-scrollbar-thumb:hover {
- background: rgba(71, 85, 105, 0.7);
- }
- .main {
- width: 100%;
- min-height: 100vh;
- display: flex;
- }
- .light {
- background: #f8fafc;
- color: #1e293b;
- }
- .dark {
- background: #0f172a;
- color: #e2e8f0;
- }
- #tree {
- width: 280px;
- min-width: 280px;
- height: 100vh;
- padding: 24px 16px;
- white-space: nowrap;
- overflow-x: hidden;
- overflow-y: auto;
- position: sticky;
- top: 0;
- left: 0;
- background: #ffffff;
- box-shadow: 1px 0 0 rgba(0, 0, 0, 0.05);
- }
- .dark #tree {
- background: #1e293b;
- box-shadow: 1px 0 0 rgba(255, 255, 255, 0.05);
- }
- #tree > div {
- padding: 0;
- position: relative;
- margin: 4px 0;
- }
- .tree-item {
- position: relative;
- padding: 8px 12px;
- border-radius: 6px;
- transition: all 0.2s ease;
- background: transparent;
- }
- .tree-item:hover {
- background-color: rgba(59, 130, 246, 0.05);
- }
- .dark .tree-item:hover {
- background-color: rgba(96, 165, 250, 0.1);
- }
- .tree-item-bg {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- z-index: 0;
- border-radius: 6px;
- }
- .tree-item-content {
- position: relative;
- z-index: 1;
- font-weight: 500;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .tree-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .tree-coverage {
- margin-left: 12px;
- font-size: 12px;
- font-weight: 600;
- color: #64748b;
- }
- .dark .tree-coverage {
- color: #94a3b8;
- }
- .clickable {
- cursor: pointer;
- }
- .clickable .tree-item-content {
- color: inherit;
- text-decoration: none;
- }
- .current .tree-item {
- background-color: rgba(59, 130, 246, 0.1);
- box-shadow: inset 3px 0 0 #3b82f6;
- }
- .dark .current .tree-item {
- background-color: rgba(96, 165, 250, 0.15);
- box-shadow: inset 3px 0 0 #60a5fa;
- }
- #coverage {
- flex: 1;
- padding: 24px;
- overflow-y: auto;
- }
- .source {
- white-space: nowrap;
- background: #ffffff;
- border-radius: 8px;
- padding: 24px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- overflow-x: auto;
- margin-top: 24px;
- }
- .dark .source {
- background: #1e293b;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
- }
- pre {
- counter-reset: line;
- font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
- font-size: 13px;
- line-height: 1.6;
- margin: 0;
- }
- ol {
- list-style: none;
- counter-reset: number;
- margin: 0;
- padding: 0;
- }
- li {
- padding: 2px 0;
- transition: background-color 0.2s ease;
- }
- li:hover {
- background-color: rgba(59, 130, 246, 0.05);
- }
- .dark li:hover {
- background-color: rgba(96, 165, 250, 0.05);
- }
- li:before {
- counter-increment: number;
- content: counter(number);
- margin-right: 24px;
- display: inline-block;
- width: 50px;
- text-align: right;
- color: #94a3b8;
- font-weight: normal;
- cursor: pointer;
- transition: color 0.2s ease;
- }
- li:hover:before {
- color: #3b82f6;
- }
- .dark li:hover:before {
- color: #60a5fa;
- }
- .dark li:before {
- color: #64748b;
- }
- .range-highlight {
- background-color: rgba(59, 130, 246, 0.15) !important;
- }
- .dark .range-highlight {
- background-color: rgba(96, 165, 250, 0.15) !important;
- }
- .cov0 {
- background-color: rgba(239, 68, 68, 0.1);
- font-weight: 600;
- }
- .cov1 {
- background-color: rgba(34, 197, 94, 0.1);
- font-weight: normal;
- }
- table {
- width: 100%;
- background: #ffffff;
- border-radius: 8px;
- overflow: hidden;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- }
- .dark table {
- background: #1e293b;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
- }
- tr {
- border-bottom: 1px solid rgba(148, 163, 184, 0.1);
- }
- .dark tr {
- border-bottom: 1px solid rgba(51, 65, 85, 0.5);
- }
- tr:last-child {
- border-bottom: none;
- }
- th {
- background: #f1f5f9;
- font-weight: 600;
- text-align: left;
- padding: 10px 16px;
- font-size: 13px;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: #64748b;
- }
- .dark th {
- background: #0f172a;
- color: #94a3b8;
- }
- td {
- padding: 8px 16px;
- }
- table .total {
- font-weight: 600;
- }
- table .fnc {
- font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
- font-size: 13px;
- }
- table .fnc .clickable {
- color: #3b82f6;
- text-decoration: none;
- transition: color 0.2s ease;
- }
- table .fnc .clickable:hover {
- color: #2563eb;
- text-decoration: underline;
- }
- .dark table .fnc .clickable {
- color: #60a5fa;
- }
- .dark table .fnc .clickable:hover {
- color: #93bbfc;
- }
- table .cov {
- width: 120px;
- text-align: right;
- padding: 0;
- }
- .cov-cell {
- position: relative;
- padding: 8px 16px;
- background-color: transparent;
- }
- .cov-text {
- position: relative;
- z-index: 1;
- font-weight: 600;
- font-size: 13px;
- }
- .cov-bg {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- z-index: 0;
- }
- </style>
- </head>
- <body>
- <div class="main {{.Theme}}">
- <div id="tree">
- {{range $i, $t := .Tree}}
- {{if $t.IsFile}}
- <div class="file" id="tree{{$t.ID}}" onclick="change({{$t.ID}}, {{$t.Indent}});">
- <div class="tree-item clickable" style="margin-inline-start: {{indent $t.Indent}}px;">
- <div class="tree-item-bg" style="width: {{if eq $t.Coverage 0.0}}100{{else}}{{$t.Coverage}}{{end}}%; background-color: {{coverageColor $t.Coverage}}"></div>
- <div class="tree-item-content">
- <span class="tree-name">{{$t.Name}}</span>
- <span class="tree-coverage">{{$t.Coverage}}%</span>
- </div>
- </div>
- </div>
- {{else}}
- <div>
- <div class="tree-item" style="margin-inline-start: {{indent $t.Indent}}px">
- <div class="tree-item-bg" style="width: {{if eq $t.Coverage 0.0}}100{{else}}{{$t.Coverage}}{{end}}%; background-color: {{coverageColor $t.Coverage}}"></div>
- <div class="tree-item-content">
- <span class="tree-name">{{$t.Name}}/</span>
- <span class="tree-coverage">{{$t.Coverage}}%</span>
- </div>
- </div>
- </div>
- {{end}}
- {{end}}
- </div>
- <div id="coverage">
- {{range $i, $f := .Files}}
- <div id="file{{$f.ID}}" style="display: none">
- <table>
- <tr><th>Function</th><th style="text-align: right">Coverage</th></tr>
- <tr>
- <td class="total">Total Coverage</td>
- <td class="cov">
- <div class="cov-cell">
- <div class="cov-bg" style="width: {{if eq $f.Coverage 0.0}}100{{else}}{{$f.Coverage}}{{end}}%; background-color: {{coverageColor $f.Coverage}}"></div>
- <div class="cov-text">{{$f.Coverage}}%</div>
- </div>
- </td>
- </tr>
- {{range $j, $fn := .Functions}}
- <tr>
- <td class="fnc"><span class="clickable" onclick="scrollToLine({{$f.ID}}, {{$fn.Line}});">{{$fn.Name}}</span></td>
- <td class="cov">
- <div class="cov-cell">
- <div class="cov-bg" style="width: {{if eq $fn.Coverage 0.0}}100{{else}}{{$fn.Coverage}}{{end}}%; background-color: {{coverageColor $fn.Coverage}}"></div>
- <div class="cov-text">{{$fn.Coverage}}%</div>
- </div>
- </td>
- </tr>
- {{end}}
- </table>
- <div class="source">
- <pre>{{$f.Body}}</pre>
- </div>
- </div>
- {{end}}
- </div>
- </div>
- <script>
- let current;
- let currentTree;
- updateByQuery();
- window.addEventListener('popstate', function(e) {
- updateByQuery();
- })
- window.addEventListener('hashchange', function(e) {
- handleHashChange();
- })
- window.addEventListener('load', function() {
- handleHashChange();
- setupLineNumberClicks();
- })
- function updateByQuery() {
- const searchParams = new URLSearchParams(window.location.search);
- const n = searchParams.get('n');
- const i = searchParams.get('i');
- if (n && i) {
- change(n, i);
- }
- // Check for hash after loading the file
- setTimeout(handleHashChange, 100);
- }
- function handleHashChange() {
- const hash = window.location.hash;
- if (hash) {
- // Check if it's a range (e.g., #file1-L10-L20)
- const rangeMatch = hash.match(/#file(\d+)-L(\d+)-L(\d+)/);
- if (rangeMatch) {
- const fileId = rangeMatch[1];
- const startLine = parseInt(rangeMatch[2]);
- const endLine = parseInt(rangeMatch[3]);
- // Highlight the range
- highlightLineRange(fileId, startLine, endLine);
- // Scroll to start of range
- const startElm = document.getElementById('file' + fileId + '-' + startLine);
- if (startElm) {
- startElm.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }
- } else {
- // Single line
- const elm = document.getElementById(hash.substring(1));
- if (elm) {
- elm.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Clear previous highlights
- clearHighlights();
- // Highlight the line briefly
- elm.style.backgroundColor = 'rgba(59, 130, 246, 0.2)';
- setTimeout(() => {
- elm.style.backgroundColor = '';
- }, 1500);
- }
- }
- }
- }
- function select(n) {
- if (current) {
- current.style.display = 'none';
- }
- current = document.getElementById('file' + n);
- if (!current) {
- return;
- }
- current.style.display = 'block';
- // Setup line number clicks for the newly displayed file
- setTimeout(setupLineNumberClicks, 100);
- }
- function selectTree(n, indent) {
- if (currentTree) {
- currentTree.classList.remove('current');
- }
- currentTree = document.getElementById('tree' + n);
- if (!currentTree) {
- return;
- }
- currentTree.classList.add('current');
- }
- function scrollById(id) {
- const elm = document.getElementById(id);
- if (elm) {
- elm.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }
- }
- function scrollToLine(fileId, lineNum) {
- const lineId = 'file' + fileId + '-' + lineNum;
- // Update URL with hash
- const url = new URL(window.location.href);
- url.hash = lineId;
- history.pushState("", "", url);
- // Scroll to the line
- scrollById(lineId);
- // Highlight the line
- const elm = document.getElementById(lineId);
- if (elm) {
- elm.style.backgroundColor = 'rgba(59, 130, 246, 0.2)';
- setTimeout(() => {
- elm.style.backgroundColor = '';
- }, 1500);
- }
- }
- function change(n, i) {
- select(n);
- selectTree(n, i);
- updateUrl(n, i)
- }
- function updateUrl(n, i) {
- const url = new URL(window.location.href);
- if( !url.searchParams.get('n') ) {
- url.searchParams.append('n',n);
- url.searchParams.append('i',i);
- location.href = url;
- } else {
- if (url.searchParams.get('n') != n || url.searchParams.get('i') != i) {
- url.searchParams.set('n',n);
- url.searchParams.set('i',i);
- history.pushState("", "", url);
- }
- }
- }
- let lastClickedLine = null;
- function setupLineNumberClicks() {
- // Wait for content to be loaded
- setTimeout(() => {
- document.querySelectorAll('li').forEach(li => {
- const id = li.id;
- if (id && id.includes('-')) {
- // Create clickable line number
- li.addEventListener('click', function(e) {
- if (e.target === li || window.getComputedStyle(e.target, ':before').getPropertyValue('content')) {
- const parts = id.split('-');
- const fileId = parts[0].replace('file', '');
- const lineNum = parseInt(parts[1]);
- if (e.shiftKey && lastClickedLine && lastClickedLine.fileId === fileId) {
- // Range selection
- const startLine = Math.min(lastClickedLine.lineNum, lineNum);
- const endLine = Math.max(lastClickedLine.lineNum, lineNum);
- // Update URL with range
- const url = new URL(window.location.href);
- url.hash = 'file' + fileId + '-L' + startLine + '-L' + endLine;
- history.pushState("", "", url);
- // Highlight range
- highlightLineRange(fileId, startLine, endLine);
- } else {
- // Single line selection
- const url = new URL(window.location.href);
- url.hash = id;
- history.pushState("", "", url);
- // Clear previous highlights
- clearHighlights();
- // Highlight the line
- li.style.backgroundColor = 'rgba(59, 130, 246, 0.2)';
- setTimeout(() => {
- if (!li.classList.contains('range-highlight')) {
- li.style.backgroundColor = '';
- }
- }, 1500);
- lastClickedLine = { fileId, lineNum };
- }
- }
- });
- }
- });
- }, 100);
- }
- function highlightLineRange(fileId, startLine, endLine) {
- clearHighlights();
- for (let i = startLine; i <= endLine; i++) {
- const li = document.getElementById('file' + fileId + '-' + i);
- if (li) {
- li.classList.add('range-highlight');
- li.style.backgroundColor = 'rgba(59, 130, 246, 0.15)';
- }
- }
- }
- function clearHighlights() {
- document.querySelectorAll('.range-highlight').forEach(li => {
- li.classList.remove('range-highlight');
- li.style.backgroundColor = '';
- });
- }
- </script>
- </body>
- </html>
- `
Function | Coverage |
---|---|
Total Coverage |
100%
|
func (n *Node) ChildBlocks() profile.Blocks |
100%
|
func Create(profiles profile.Profiles) []Node |
100%
|
func addNode(nodes *[]Node, paths []string, p *profile.Profile) |
100%
|
func index(nodes []Node, name string) int |
100%
|
func mergeSingreDir(nodes []Node) []Node |
100%
|
- package tree
- import (
- "path/filepath"
- "strings"
- "github.com/masakurapa/gover-html/internal/profile"
- )
- // Node is single node of directory tree
- type Node struct {
- Name string
- Files profile.Profiles
- Dirs []Node
- }
- // ChildBlocks returns all child Blocks for Node
- func (n *Node) ChildBlocks() profile.Blocks {
- blocks := make(profile.Blocks, 0)
- for _, f := range n.Files {
- blocks = append(blocks, f.Blocks...)
- }
- for _, ch := range n.Dirs {
- blocks = append(blocks, ch.ChildBlocks()...)
- }
- return blocks
- }
- // Create returns directory tree
- func Create(profiles profile.Profiles) []Node {
- nodes := make([]Node, 0)
- for _, p := range profiles {
- idx := index(nodes, p.ModulePath)
- if idx == -1 {
- nodes = append(nodes, Node{Name: p.ModulePath})
- idx = len(nodes) - 1
- }
- addNode(&nodes[idx].Dirs, strings.Split(p.RemoveModulePathFromFileName(), "/"), &p)
- }
- for i, node := range nodes {
- nodes[i].Dirs = mergeSingreDir(node.Dirs)
- }
- return nodes
- }
- func addNode(nodes *[]Node, paths []string, p *profile.Profile) {
- name := paths[0]
- nextPaths := paths[1:]
- idx := index(*nodes, name)
- if idx == -1 {
- *nodes = append(*nodes, Node{Name: name})
- idx = len(*nodes) - 1
- }
- n := *nodes
- if len(nextPaths) == 1 {
- n[idx].Files = append(n[idx].Files, *p)
- return
- }
- addNode(&n[idx].Dirs, nextPaths, p)
- }
- func index(nodes []Node, name string) int {
- for i, t := range nodes {
- if t.Name == name {
- return i
- }
- }
- return -1
- }
- // merge directories with no files and only one child directory
- //
- // path/
- // to/
- // file.go
- //
- // to
- //
- // path/to/
- // file.go
- func mergeSingreDir(nodes []Node) []Node {
- for i, n := range nodes {
- if len(n.Dirs) == 0 {
- continue
- }
- mergeSingreDir(n.Dirs)
- if len(n.Files) > 0 || len(n.Dirs) != 1 {
- continue
- }
- sub := n.Dirs[0]
- nodes[i].Name = filepath.Join(n.Name, sub.Name)
- nodes[i].Files = sub.Files
- nodes[i].Dirs = sub.Dirs
- }
- return nodes
- }
Function | Coverage |
---|---|
Total Coverage |
0%
|
func (e *optionErrors) Error() string |
0%
|
- package option
- import "strings"
- type optionErrors []error
- func (e *optionErrors) Error() string {
- messages := make([]string, 0, len(*e))
- for _, err := range *e {
- messages = append(messages, err.Error())
- }
- return strings.Join(messages, "\n")
- }
Function | Coverage |
---|---|
Total Coverage |
96.6%
|
func New(r reader.Reader) *Generator |
100%
|
func (g *Generator) Generate(input *string, inputFiles *string, output *string, theme *string, include *string, exclude *string, excludeFunc *string) (*Option, error) |
92.9%
|
func (g *Generator) readOptionFile() (*optionConfig, error) |
70%
|
func (g *Generator) stringValue(arg *string, opt string) string |
100%
|
func (g *Generator) stringsValue(arg *string, opt []string) []string |
100%
|
func (g *Generator) getValidatedOption(opt *optionConfig) (*Option, error) |
100%
|
func (g *Generator) validate(opt *optionConfig) error |
100%
|
func (g *Generator) validateFilter(f string, values []string) optionErrors |
100%
|
func (g *Generator) validateExcludeFunc(values []string) optionErrors |
100%
|
func (g *Generator) getOptionWithDefaultValue(opt *optionConfig) *Option |
100%
|
func (g *Generator) isEmpty(s string) bool |
100%
|
func (g *Generator) convertInputFilesOption(values []string) []string |
100%
|
func (g *Generator) convertFilterValue(values []string) []string |
100%
|
func (g *Generator) convertExcludeFuncOption(values []string) []ExcludeFuncOption |
100%
|
- package option
- import (
- "fmt"
- "io"
- "regexp"
- "strings"
- "github.com/masakurapa/gover-html/internal/reader"
- "gopkg.in/yaml.v2"
- )
- const (
- fileName = ".gover.yml"
- optionSeparator = ","
- inputDefault = "coverage.out"
- outputDefault = "coverage.html"
- themeDark = "dark"
- themeLight = "light"
- themeDefault = themeDark
- )
- var (
- // 関数除外設定の検証用の正規表現(ここでは緩い検証にする)
- excludeFuncFormat = regexp.MustCompile(`^\((.+)\)\.([a-zA-Z].+)$`)
- )
- type optionConfig struct {
- Input string
- InputFiles []string `yaml:"input-files"`
- Output string
- Theme string
- Include []string // include fire or directories
- Exclude []string // exclude fire or directories
- ExcludeFunc []string `yaml:"exclude-func"` // exclude functions
- }
- type Option struct {
- Input string
- InputFiles []string
- Output string
- Theme string
- Include []string // include fire or directories
- Exclude []string // exclude fire or directories
- ExcludeFunc []ExcludeFuncOption // exclude functions
- }
- type ExcludeFuncOption struct {
- Path string
- Struct string
- Func string
- }
- type Generator struct {
- r reader.Reader
- }
- func New(r reader.Reader) *Generator {
- return &Generator{r: r}
- }
- func (g *Generator) Generate(
- input *string,
- inputFiles *string,
- output *string,
- theme *string,
- include *string,
- exclude *string,
- excludeFunc *string,
- ) (*Option, error) {
- opt := &optionConfig{}
- if g.r.Exists(fileName) {
- fileOpt, err := g.readOptionFile()
- if err != nil {
- return nil, err
- }
- opt = fileOpt
- }
- opt.Input = g.stringValue(input, opt.Input)
- opt.InputFiles = g.stringsValue(inputFiles, opt.InputFiles)
- opt.Output = g.stringValue(output, opt.Output)
- opt.Theme = g.stringValue(theme, opt.Theme)
- opt.Include = g.stringsValue(include, opt.Include)
- opt.Exclude = g.stringsValue(exclude, opt.Exclude)
- opt.ExcludeFunc = g.stringsValue(excludeFunc, opt.ExcludeFunc)
- return g.getValidatedOption(opt)
- }
- func (g *Generator) readOptionFile() (*optionConfig, error) {
- r, err := g.r.Read(fileName)
- if err != nil {
- return nil, err
- }
- b, err := io.ReadAll(r)
- if err != nil {
- return nil, err
- }
- opt := optionConfig{}
- if err := yaml.Unmarshal(b, &opt); err != nil {
- return nil, err
- }
- return &opt, nil
- }
- func (g *Generator) stringValue(arg *string, opt string) string {
- if arg == nil {
- return opt
- }
- return *arg
- }
- func (g *Generator) stringsValue(arg *string, opt []string) []string {
- if arg == nil {
- return opt
- }
- return strings.Split(*arg, optionSeparator)
- }
- func (g *Generator) getValidatedOption(opt *optionConfig) (*Option, error) {
- if err := g.validate(opt); err != nil {
- return nil, err
- }
- return g.getOptionWithDefaultValue(opt), nil
- }
- func (g *Generator) validate(opt *optionConfig) error {
- errs := make(optionErrors, 0)
- if !g.isEmpty(opt.Theme) && opt.Theme != themeDark && opt.Theme != themeLight {
- errs = append(errs, fmt.Errorf("theme must be %q or %q", themeDark, themeLight))
- }
- if es := g.validateFilter("include", opt.Include); len(es) > 0 {
- errs = append(errs, es...)
- }
- if es := g.validateFilter("exclude", opt.Exclude); len(es) > 0 {
- errs = append(errs, es...)
- }
- if es := g.validateExcludeFunc(opt.ExcludeFunc); len(es) > 0 {
- errs = append(errs, es...)
- }
- if len(errs) > 0 {
- return &errs
- }
- return nil
- }
- func (g *Generator) validateFilter(f string, values []string) optionErrors {
- errs := make(optionErrors, 0)
- for _, v := range values {
- if g.isEmpty(v) {
- continue
- }
- if strings.HasPrefix(v, "/") {
- errs = append(errs, fmt.Errorf("%s value %q must not be an absolute path", f, v))
- }
- }
- return errs
- }
- func (g *Generator) validateExcludeFunc(values []string) optionErrors {
- errs := make(optionErrors, 0)
- for _, v := range values {
- if g.isEmpty(v) {
- continue
- }
- if strings.HasPrefix(v, "(/") {
- errs = append(errs, fmt.Errorf("exclude-func value %q must not be an absolute path", v))
- continue
- }
- // ()が含まれない場合は関数名のみとみなしてOK
- if !strings.Contains(v, "(") && !strings.Contains(v, ")") {
- continue
- }
- if !excludeFuncFormat.MatchString(v) {
- errs = append(errs, fmt.Errorf("exclude-func value %q format is invalid", v))
- }
- }
- return errs
- }
- func (g *Generator) getOptionWithDefaultValue(opt *optionConfig) *Option {
- newOpt := &Option{
- Input: opt.Input,
- Output: opt.Output,
- Theme: opt.Theme,
- Include: opt.Include,
- Exclude: opt.Exclude,
- }
- if g.isEmpty(newOpt.Input) {
- newOpt.Input = inputDefault
- }
- if g.isEmpty(newOpt.Output) {
- newOpt.Output = outputDefault
- }
- if g.isEmpty(newOpt.Theme) {
- newOpt.Theme = themeDefault
- }
- newOpt.InputFiles = g.convertInputFilesOption(opt.InputFiles)
- newOpt.Include = g.convertFilterValue(newOpt.Include)
- newOpt.Exclude = g.convertFilterValue(newOpt.Exclude)
- newOpt.ExcludeFunc = g.convertExcludeFuncOption(opt.ExcludeFunc)
- return newOpt
- }
- func (g *Generator) isEmpty(s string) bool {
- return s == ""
- }
- func (g *Generator) convertInputFilesOption(values []string) []string {
- ret := make([]string, 0, len(values))
- for _, v := range values {
- s := strings.TrimSpace(v)
- if g.isEmpty(s) {
- continue
- }
- ret = append(ret, s)
- }
- return ret
- }
- func (g *Generator) convertFilterValue(values []string) []string {
- ret := make([]string, 0, len(values))
- for _, v := range values {
- s := strings.TrimSpace(v)
- if g.isEmpty(s) {
- continue
- }
- s = strings.TrimPrefix(s, "./")
- s = strings.TrimSuffix(s, "/")
- ret = append(ret, s)
- }
- return ret
- }
- func (g *Generator) convertExcludeFuncOption(values []string) []ExcludeFuncOption {
- ret := make([]ExcludeFuncOption, 0, len(values))
- for _, v := range values {
- s := strings.TrimSpace(v)
- if g.isEmpty(s) {
- continue
- }
- // ()が含まれない場合は関数名のみとみなして終了
- if !strings.Contains(s, "(") && !strings.Contains(s, ")") {
- ret = append(ret, ExcludeFuncOption{Func: s})
- continue
- }
- // excludeFuncFormat = regexp.MustCompile(`^\(.+\)\.([a-zA-Z].+)$`)
- matches := excludeFuncFormat.FindStringSubmatch(s)
- optPath := strings.TrimSuffix(strings.TrimPrefix(matches[1], "./"), "/")
- var path, structName string
- idx := strings.LastIndex(optPath, ".")
- if idx == -1 {
- // ファイル名で指定していない or 構造体名未指定
- path = optPath
- } else {
- s := optPath[idx+1:]
- if s == "go" {
- // ファイル名のみ指定している
- path = optPath
- } else {
- path = optPath[:idx]
- structName = s
- }
- }
- if path == "*" {
- path = ""
- }
- ret = append(ret, ExcludeFuncOption{
- Path: strings.TrimSuffix(strings.TrimPrefix(path, "./"), "/"),
- Struct: structName,
- Func: matches[2],
- })
- }
- return ret
- }
Function | Coverage |
---|---|
Total Coverage |
92.3%
|
func (prof *Profile) IsRelativeOrAbsolute() bool |
100%
|
func (prof *Profile) RemoveModulePathFromFileName() string |
0%
|
func (prof *Profile) FilePath() string |
100%
|
func (blocks *Blocks) Coverage() float64 |
100%
|
- package profile
- import (
- "math"
- "path"
- "path/filepath"
- "strings"
- )
- // Profiles is a type that represents a slice of Profile
- type Profiles []Profile
- // Profile is profiling data for each file
- type Profile struct {
- ID int
- ModulePath string
- Dir string
- FileName string
- Blocks Blocks
- Functions Functions
- }
- // Functions is a type that represents a slice of Function
- type Functions []Function
- // Function is single func of profiling data
- type Function struct {
- Name string
- StartLine int
- StartCol int
- Blocks Blocks
- }
- // Blocks is a type that represents a slice of Block
- type Blocks []Block
- // Block is single block of profiling data
- type Block struct {
- StartLine int
- StartCol int
- EndLine int
- EndCol int
- NumState int
- Count int
- }
- // IsRelativeOrAbsolute returns true if FileName is relative path or absolute path
- func (prof *Profile) IsRelativeOrAbsolute() bool {
- return strings.HasPrefix(prof.FileName, ".") || filepath.IsAbs(prof.FileName)
- }
- // RemoveModulePathFromFileName returns FileName with ModulePath removed
- func (prof *Profile) RemoveModulePathFromFileName() string {
- return strings.TrimPrefix(strings.TrimPrefix(prof.FileName, prof.ModulePath), "/")
- }
- // FilePath returns readable file path
- func (prof *Profile) FilePath() string {
- if prof.IsRelativeOrAbsolute() {
- return prof.FileName
- }
- return filepath.Join(prof.Dir, path.Base(prof.FileName))
- }
- // Coverage returns covered ratio for file
- func (blocks *Blocks) Coverage() float64 {
- var total, covered int64
- for _, b := range *blocks {
- total += int64(b.NumState)
- if b.Count > 0 {
- covered += int64(b.NumState)
- }
- }
- if total == 0 {
- return 0
- }
- return math.Round((float64(covered)/float64(total)*100)*10) / 10
- }
Function | Coverage |
---|---|
Total Coverage |
87.5%
|
func Read(opt option.Option) (io.Reader, error) |
81.8%
|
func read(buf *bytes.Buffer, in string) error |
92.3%
|
- package profile
- import (
- "bufio"
- "bytes"
- "io"
- "os"
- "github.com/masakurapa/gover-html/internal/option"
- )
- var (
- newline = []byte("\n")
- )
- func Read(opt option.Option) (io.Reader, error) {
- // InputFilesが指定されていない場合だけInputを使う
- if len(opt.InputFiles) == 0 {
- b, err := os.ReadFile(opt.Input)
- if err != nil {
- return nil, err
- }
- return bytes.NewReader(b), nil
- }
- // 1行目は "mode: set" を固定にする
- buf := bytes.NewBuffer([]byte("mode: set"))
- buf.Write(newline)
- for _, in := range opt.InputFiles {
- if err := read(buf, in); err != nil {
- return nil, err
- }
- }
- return buf, nil
- }
- func read(buf *bytes.Buffer, in string) error {
- f, err := os.Open(in)
- if err != nil {
- return err
- }
- defer f.Close()
- // 1行目には "mode: xxx" が入っているはずなので無視して2行目から読み込む
- skip := true
- r := bufio.NewScanner(f)
- for r.Scan() {
- if skip {
- skip = false
- continue
- }
- buf.Write(r.Bytes())
- buf.Write(newline)
- }
- return nil
- }
Function | Coverage |
---|---|
Total Coverage |
0%
|
func New() Reader |
0%
|
func (r *fileReader) Read(file string) (io.Reader, error) |
0%
|
func (r *fileReader) Exists(file string) bool |
0%
|
- package reader
- import (
- "io"
- "os"
- )
- // Reader is file reader
- type Reader interface {
- Read(string) (io.Reader, error)
- Exists(string) bool
- }
- type fileReader struct{}
- // New is initialization the file reader
- func New() Reader {
- return &fileReader{}
- }
- func (r *fileReader) Read(file string) (io.Reader, error) {
- return os.Open(file)
- }
- func (r *fileReader) Exists(file string) bool {
- _, err := os.Stat(file)
- return err == nil
- }