diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 78144b2..9b52f2e 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,6 +21,12 @@ jobs: with: go-version: 1.11 + - name: Build code generation tool + run: go build -v ./cmd/generator + + - name: Generate code + run: go generate ./... + - name: Build run: go build -v ./cmd/gocov-html diff --git a/.gitignore b/.gitignore index c7abeee..0ff6f2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /gocov-html +/generator *.swp /*.css +// Generated Go files +*_gen.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d4ff1f..761eb0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ v 1.3 + - refac: generate Go code to render themes. #38 - doc: fix semver in README. #36 v 1.2 diff --git a/Makefile b/Makefile index ff030f1..6c18c7d 100644 --- a/Makefile +++ b/Makefile @@ -4,5 +4,8 @@ BIN=gocov-html MAIN_CMD=github.com/matm/${BIN}/cmd/${BIN} +GENERATOR_BIN=generator +GENERATOR_CMD=github.com/matm/${BIN}/cmd/${GENERATOR_BIN} + include version.mk include build.mk diff --git a/build.mk b/build.mk index 2146598..7c571e2 100644 --- a/build.mk +++ b/build.mk @@ -71,10 +71,13 @@ cleardist: @rm -rf ${DISTDIR} && mkdir -p ${BINDIR} && mkdir -p ${BUILDDIR} build: + @go build ${GENERATOR_CMD} + @go generate ./... @go build -ldflags "all=$(GO_LDFLAGS)" ${MAIN_CMD} test: @go test ./... clean: - @rm -rf ${BIN} ${BUILDDIR} ${DISTDIR} + @find pkg -name \*_gen.go -delete + @rm -rf ${BIN} ${GENERATOR_BIN} ${BUILDDIR} ${DISTDIR} diff --git a/cmd/generator/main.go b/cmd/generator/main.go new file mode 100644 index 0000000..a0ac54e --- /dev/null +++ b/cmd/generator/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "log" + "os" + "path" + "strings" + "text/template" + + "github.com/matm/gocov-html/pkg/types" +) + +const tmpl = `// Code generated by "go run generator.go". DO NOT EDIT. + +package themes + +import ( + "text/template" + "time" + + "github.com/matm/gocov-html/pkg/types" +) + +func (t defaultTheme) Data() *types.TemplateData { + td:= &types.TemplateData{ + When: time.Now().Format(time.RFC822Z), + ProjectURL: types.ProjectURL, + } + {{if .Style}} + td.Style = {{.Style}} + {{end}} + {{if .Script}} + td.Script = {{.Script}} + {{end}} + return td +} + +func (t defaultTheme) Template() *template.Template { + tmpl := {{.Template}} + p := template.Must(template.New("theme").Parse(tmpl)) + return p +} +` + +func inspect(name string, theme *string, assets *types.StaticAssets) error { + fset := token.NewFileSet() + token, err := parser.ParseFile(fset, name, nil, parser.ParseComments) + if err != nil { + return err + } + ast.Inspect(token, func(n ast.Node) bool { + fn, ok := n.(*ast.FuncDecl) + if ok { + switch fn.Name.Name { + case "Name": + *theme = fn.Body.List[0].(*ast.ReturnStmt).Results[0].(*ast.BasicLit).Value + *theme = strings.Replace(*theme, `"`, "", -1) + case "Assets": + es := fn.Body.List[0].(*ast.ReturnStmt).Results[0].(*ast.CompositeLit).Elts + for _, e := range es { + kv := e.(*ast.KeyValueExpr) + id := kv.Key.(*ast.Ident) + switch id.Name { + case "Stylesheets": + elems := kv.Value.(*ast.CompositeLit).Elts + for _, elem := range elems { + sheet := elem.(*ast.BasicLit).Value + assets.Stylesheets = append(assets.Stylesheets, strings.Replace(sheet, `"`, "", -1)) + } + case "Index": + tmplName := kv.Value.(*ast.BasicLit).Value + assets.Index = strings.Replace(tmplName, `"`, "", -1) + case "Scripts": + elems := kv.Value.(*ast.CompositeLit).Elts + for _, elem := range elems { + script := elem.(*ast.BasicLit).Value + assets.Scripts = append(assets.Scripts, strings.Replace(script, `"`, "", -1)) + } + } + } + } + return false + } + return true + }) + return nil +} + +func render(name, theme string, assets types.StaticAssets) error { + baseThemeDir := path.Join("..", "..", "themes", theme) + out := strings.Replace(name, ".go", "_gen.go", 1) + outFile, err := os.Create(out) + if err != nil { + return err + } + defer outFile.Close() + index, err := ioutil.ReadFile(path.Join(baseThemeDir, assets.Index)) + if err != nil { + return err + } + // Contains all stylesheets' data. + var allStyles bytes.Buffer + for _, css := range assets.Stylesheets { + style, err := ioutil.ReadFile(path.Join(baseThemeDir, css)) + if err != nil { + return err + } + fmt.Fprintf(&allStyles, "`%s`", style) + } + + // Contains all scripts' data. + var allScripts bytes.Buffer + for _, script := range assets.Scripts { + js, err := ioutil.ReadFile(path.Join(baseThemeDir, script)) + if err != nil { + return err + } + fmt.Fprintf(&allScripts, "`%s`", js) + } + t, err := template.New("").Parse(tmpl) + if err != nil { + return err + } + type data struct { + Script string + Style string + Template string + } + err = t.Execute(outFile, &data{ + Script: allScripts.String(), + Style: allStyles.String(), + Template: "`" + string(index) + "`"}, + ) + return err +} + +func main() { + name := os.Getenv("GOFILE") + if name == "" { + fmt.Println("Must be run by the \"go generate\" tool, like \"go generate ./...\"") + os.Exit(1) + } + assets := new(types.StaticAssets) + theme := new(string) + err := inspect(name, theme, assets) + if err != nil { + log.Fatal(err) + } + if err := render(name, *theme, *assets); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/gocov-html/main.go b/cmd/gocov-html/main.go index f0aceaa..ea6f785 100644 --- a/cmd/gocov-html/main.go +++ b/cmd/gocov-html/main.go @@ -68,7 +68,7 @@ func main() { } if *showDefaultCSS { - fmt.Println(themes.Current().Data().CSS) + fmt.Println(themes.Current().Data().Style) return } diff --git a/pkg/cov/report.go b/pkg/cov/report.go index 15d9b26..32d1f77 100644 --- a/pkg/cov/report.go +++ b/pkg/cov/report.go @@ -106,7 +106,7 @@ func printReport(w io.Writer, r *report) error { theme := themes.Current() data := theme.Data() - css := data.CSS + css := data.Style if len(r.stylesheet) > 0 { // Inline CSS. f, err := os.Open(r.stylesheet) @@ -124,7 +124,7 @@ func printReport(w io.Writer, r *report) error { reportPackages[i] = buildReportPackage(pkg) } - data.CSS = css + data.Style = css data.Packages = reportPackages if len(reportPackages) > 1 { diff --git a/pkg/themes/default.go b/pkg/themes/default.go index 00b3dd1..62b7d08 100644 --- a/pkg/themes/default.go +++ b/pkg/themes/default.go @@ -1,143 +1,17 @@ package themes -import ( - "text/template" - "time" +//go:generate ../../generator +import ( "github.com/matm/gocov-html/pkg/types" ) type defaultTheme struct{} -func (t defaultTheme) Data() *types.TemplateData { - css := `` - return &types.TemplateData{ - CSS: css, - When: time.Now().Format(time.RFC822Z), - ProjectURL: types.ProjectURL, +func (t defaultTheme) Assets() types.StaticAssets { + return types.StaticAssets{ + Stylesheets: []string{"style.css"}, + Index: "index.html", } } @@ -148,104 +22,3 @@ func (t defaultTheme) Name() string { func (t defaultTheme) Description() string { return "original golang theme (default)" } - -func (t defaultTheme) Template() *template.Template { - tmpl := `{{define "theme"}} - - - Coverage Report - - {{.CSS}} - - -
Coverage Report
- {{if not .Packages}} -

no test files in package.

" - {{else}} -
Generated on {{.When}} with gocov-html
- {{/* Report overview/summary available? */}} - {{if .Overview}} -
Report Overview
- - {{range $k,$rp := .Packages}} - - - - - - {{end}} -
{{$rp.Pkg.Name}}{{printf "%.2f%%" $rp.PercentageReached}}{{printf "%d" $rp.ReachedStatements}}/{{printf "%d" $rp.TotalStatements}}
- - {{end}} - {{range $k,$rp := .Packages}} -
- Package Overview: {{$rp.Pkg.Name}} - {{printf "%.2f%%" $rp.PercentageReached}} -
-

- This is a coverage report created after analysis of the {{$rp.Pkg.Name}} package. It - has been generated with the following command: -

-
gocov test {{$rp.Pkg.Name}} | gocov-html
-

Here are the stats. Please select a function name to view its implementation and see what's left for testing.

- - - {{range $k,$f := $rp.Functions}} - - - - - - - {{end}} -
- {{$f.Name}}(...) - - {{$rp.Pkg.Name}}/{{$f.ShortFileName}} - - {{printf "%.2f%%" $f.CoveragePercent}} - - {{$f.StatementsReached}}/{{len $f.Statements}} -
- - {{/* Functions source code here */}} - {{range $k,$f := $rp.Functions}} -
func {{$f.Name}}
-
- Back -

In {{$f.File}}:

-
- - {{range $p,$info := $f.Lines}} - - - - - {{end}} -
{{$info.LineNumber}} -
{{$info.Code}}
-
- {{end}} {{/* range function lines */}} - - - {{end}} {{/* range Packages end */}} - -
- {{if not .Overview}} - {{$rp := index .Packages 0}} -
{{$rp.Pkg.Name}}
-
{{printf "%.2f%%" $rp.PercentageReached}}
- {{else}} -
{{.Overview.Pkg.Name}}
-
{{printf "%.2f%%" .Overview.PercentageReached}}
- {{end}} {{/* if overview end */}} -
- {{end}} {{/* range if end */}} - - -{{end}}` - p := template.Must(template.New("theme").Parse(tmpl)) - return p -} diff --git a/pkg/types/theme.go b/pkg/types/theme.go index 596fbc3..7a3d30a 100644 --- a/pkg/types/theme.go +++ b/pkg/types/theme.go @@ -8,15 +8,21 @@ type Beautifier interface { Name() string // Description is a single line comment about the theme. Description() string - // Template is the content that will be rendered. + Assets() StaticAssets + // Template is the structure of the page that will be rendered. + // This code is generated by pkg/theme/generator.go. Template() *template.Template + // Data is the content used by the template. + // This code is generated by pkg/theme/generator.go. Data() *TemplateData } // TemplateData has all the fields needed by the the HTML template for rendering. type TemplateData struct { - // CSS is the stylesheet content that will be embedded in the HTML page. - CSS string + // Style is the stylesheet content that will be embedded in the HTML page. + Style string + // Script is the javascript content that will be embedded in the HTML page. + Script string // When is the date time of report generation. When string // Overview holds data used for an additional header in case of multiple Go packages @@ -28,3 +34,10 @@ type TemplateData struct { // ProjectURL is the project's site on GitHub. ProjectURL string } + +// StaticAssets sets all assets required for a theme. +type StaticAssets struct { + Stylesheets []string + Scripts []string + Index string +} diff --git a/themes/golang/index.html b/themes/golang/index.html new file mode 100644 index 0000000..fb4bb83 --- /dev/null +++ b/themes/golang/index.html @@ -0,0 +1,105 @@ +{{define "theme"}} + + + Coverage Report + + {{if .Style}} + + {{end}} + + +
Coverage Report
+ {{if not .Packages}} +

no test files in package.

" + {{else}} +
Generated on {{.When}} with gocov-html
+ {{/* Report overview/summary available? */}} + {{if .Overview}} +
Report Overview
+ + {{range $k,$rp := .Packages}} + + + + + + {{end}} +
{{$rp.Pkg.Name}}{{printf "%.2f%%" $rp.PercentageReached}}{{printf "%d" $rp.ReachedStatements}}/{{printf "%d" $rp.TotalStatements}}
+ + {{end}} + {{range $k,$rp := .Packages}} +
+ Package Overview: {{$rp.Pkg.Name}} + {{printf "%.2f%%" $rp.PercentageReached}} +
+

+ This is a coverage report created after analysis of the {{$rp.Pkg.Name}} package. It + has been generated with the following command: +

+
gocov test {{$rp.Pkg.Name}} | gocov-html
+

Here are the stats. Please select a function name to view its implementation and see what's left for testing.

+ + + {{range $k,$f := $rp.Functions}} + + + + + + + {{end}} +
+ {{$f.Name}}(...) + + {{$rp.Pkg.Name}}/{{$f.ShortFileName}} + + {{printf "%.2f%%" $f.CoveragePercent}} + + {{$f.StatementsReached}}/{{len $f.Statements}} +
+ + {{/* Functions source code here */}} + {{range $k,$f := $rp.Functions}} +
func {{$f.Name}}
+
+ Back +

In {{$f.File}}:

+
+ + {{range $p,$info := $f.Lines}} + + + + + {{end}} +
{{$info.LineNumber}} +
{{$info.Code}}
+
+ {{end}} {{/* range function lines */}} + + + {{end}} {{/* range Packages end */}} + +
+ {{if not .Overview}} + {{$rp := index .Packages 0}} +
{{$rp.Pkg.Name}}
+
{{printf "%.2f%%" $rp.PercentageReached}}
+ {{else}} +
{{.Overview.Pkg.Name}}
+
{{printf "%.2f%%" .Overview.PercentageReached}}
+ {{end}} {{/* if overview end */}} +
+ {{end}} {{/* range if end */}} + {{if .Script}} + + {{end}} + + +{{end}} \ No newline at end of file diff --git a/themes/golang/style.css b/themes/golang/style.css new file mode 100644 index 0000000..cfd822a --- /dev/null +++ b/themes/golang/style.css @@ -0,0 +1,160 @@ +body { + background-color: #fff; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +table { + margin-left: 10px; + border-collapse: collapse; +} + +td { + background-color: #fff; + padding: 2px; +} + +table.overview td { + padding-right: 20px; +} + +td.percent, +td.linecount { + text-align: right; +} + +div.package, +#totalcov { + color: #fff; + background-color: #375eab; + font-size: 16px; + font-weight: bold; + padding: 10px; + border-radius: 5px 5px 5px 5px; +} + +div.package, +#totalcov { + float: right; + right: 10px; +} + +#totalcov { + top: 10px; + position: relative; + background-color: #fff; + color: #000; + border: 1px solid #375eab; + clear: both; +} + +#summaryWrapper { + position: fixed; + top: 10px; + float: right; + right: 10px; + +} + +span.packageTotal { + float: right; + color: #000; +} + +#doctitle { + background-color: #fff; + font-size: 24px; + margin-top: 20px; + margin-left: 10px; + color: #375eab; + font-weight: bold; +} + +#about { + margin-left: 18px; + font-size: 10px; +} + +table tr:last-child td { + font-weight: bold; +} + +.functitle, +.funcname { + text-align: center; + font-size: 20px; + font-weight: bold; + color: #375eab; +} + +.funcname { + text-align: left; + margin-top: 20px; + margin-left: 10px; + margin-bottom: 20px; + padding: 2px 5px 5px; + background-color: #e0ebf5; +} + +table.listing { + margin-left: 10px; +} + +table.listing td { + padding: 0px; + font-size: 12px; + background-color: #eee; + vertical-align: top; + padding-left: 10px; + border-bottom: 1px solid #fff; +} + +table.listing td:first-child { + text-align: right; + font-weight: bold; + vertical-align: center; +} + +table.listing tr.miss td { + background-color: #FFBBB8; +} + +table.listing tr:last-child td { + font-weight: normal; + color: #000; +} + +table.listing tr:last-child td:first-child { + font-weight: bold; +} + +.info { + margin-left: 10px; +} + +.info code {} + +pre { + margin: 1px; +} + +pre.cmd { + background-color: #e9e9e9; + border-radius: 5px 5px 5px 5px; + padding: 10px; + margin: 20px; + line-height: 18px; + font-size: 14px; +} + +a { + text-decoration: none; + color: #375eab; +} + +a:hover { + text-decoration: underline; +} + +p { + margin-left: 10px; +} \ No newline at end of file