You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
6.2 KiB

  1. // Copyright 2013 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package main
  5. import (
  6. "bufio"
  7. "bytes"
  8. "fmt"
  9. "html/template"
  10. "io"
  11. "io/ioutil"
  12. "math"
  13. "os"
  14. "os/exec"
  15. "path/filepath"
  16. "runtime"
  17. "golang.org/x/tools/cover"
  18. )
  19. // htmlOutput reads the profile data from profile and generates an HTML
  20. // coverage report, writing it to outfile. If outfile is empty,
  21. // it writes the report to a temporary file and opens it in a web browser.
  22. func htmlOutput(profile, outfile string) error {
  23. profiles, err := cover.ParseProfiles(profile)
  24. if err != nil {
  25. return err
  26. }
  27. var d templateData
  28. for _, profile := range profiles {
  29. fn := profile.FileName
  30. if profile.Mode == "set" {
  31. d.Set = true
  32. }
  33. file, err := findFile(fn)
  34. if err != nil {
  35. return err
  36. }
  37. src, err := ioutil.ReadFile(file)
  38. if err != nil {
  39. return fmt.Errorf("can't read %q: %v", fn, err)
  40. }
  41. var buf bytes.Buffer
  42. err = htmlGen(&buf, src, profile.Boundaries(src))
  43. if err != nil {
  44. return err
  45. }
  46. d.Files = append(d.Files, &templateFile{
  47. Name: fn,
  48. Body: template.HTML(buf.String()),
  49. Coverage: percentCovered(profile),
  50. })
  51. }
  52. var out *os.File
  53. if outfile == "" {
  54. var dir string
  55. dir, err = ioutil.TempDir("", "cover")
  56. if err != nil {
  57. return err
  58. }
  59. out, err = os.Create(filepath.Join(dir, "coverage.html"))
  60. } else {
  61. out, err = os.Create(outfile)
  62. }
  63. if err != nil {
  64. return err
  65. }
  66. err = htmlTemplate.Execute(out, d)
  67. if err == nil {
  68. err = out.Close()
  69. }
  70. if err != nil {
  71. return err
  72. }
  73. if outfile == "" {
  74. if !startBrowser("file://" + out.Name()) {
  75. fmt.Fprintf(os.Stderr, "HTML output written to %s\n", out.Name())
  76. }
  77. }
  78. return nil
  79. }
  80. // percentCovered returns, as a percentage, the fraction of the statements in
  81. // the profile covered by the test run.
  82. // In effect, it reports the coverage of a given source file.
  83. func percentCovered(p *cover.Profile) float64 {
  84. var total, covered int64
  85. for _, b := range p.Blocks {
  86. total += int64(b.NumStmt)
  87. if b.Count > 0 {
  88. covered += int64(b.NumStmt)
  89. }
  90. }
  91. if total == 0 {
  92. return 0
  93. }
  94. return float64(covered) / float64(total) * 100
  95. }
  96. // htmlGen generates an HTML coverage report with the provided filename,
  97. // source code, and tokens, and writes it to the given Writer.
  98. func htmlGen(w io.Writer, src []byte, boundaries []cover.Boundary) error {
  99. dst := bufio.NewWriter(w)
  100. for i := range src {
  101. for len(boundaries) > 0 && boundaries[0].Offset == i {
  102. b := boundaries[0]
  103. if b.Start {
  104. n := 0
  105. if b.Count > 0 {
  106. n = int(math.Floor(b.Norm*9)) + 1
  107. }
  108. fmt.Fprintf(dst, `<span class="cov%v" title="%v">`, n, b.Count)
  109. } else {
  110. dst.WriteString("</span>")
  111. }
  112. boundaries = boundaries[1:]
  113. }
  114. switch b := src[i]; b {
  115. case '>':
  116. dst.WriteString("&gt;")
  117. case '<':
  118. dst.WriteString("&lt;")
  119. case '&':
  120. dst.WriteString("&amp;")
  121. case '\t':
  122. dst.WriteString(" ")
  123. default:
  124. dst.WriteByte(b)
  125. }
  126. }
  127. return dst.Flush()
  128. }
  129. // startBrowser tries to open the URL in a browser
  130. // and reports whether it succeeds.
  131. func startBrowser(url string) bool {
  132. // try to start the browser
  133. var args []string
  134. switch runtime.GOOS {
  135. case "darwin":
  136. args = []string{"open"}
  137. case "windows":
  138. args = []string{"cmd", "/c", "start"}
  139. default:
  140. args = []string{"xdg-open"}
  141. }
  142. cmd := exec.Command(args[0], append(args[1:], url)...)
  143. return cmd.Start() == nil
  144. }
  145. // rgb returns an rgb value for the specified coverage value
  146. // between 0 (no coverage) and 10 (max coverage).
  147. func rgb(n int) string {
  148. if n == 0 {
  149. return "rgb(192, 0, 0)" // Red
  150. }
  151. // Gradient from gray to green.
  152. r := 128 - 12*(n-1)
  153. g := 128 + 12*(n-1)
  154. b := 128 + 3*(n-1)
  155. return fmt.Sprintf("rgb(%v, %v, %v)", r, g, b)
  156. }
  157. // colors generates the CSS rules for coverage colors.
  158. func colors() template.CSS {
  159. var buf bytes.Buffer
  160. for i := 0; i < 11; i++ {
  161. fmt.Fprintf(&buf, ".cov%v { color: %v }\n", i, rgb(i))
  162. }
  163. return template.CSS(buf.String())
  164. }
  165. var htmlTemplate = template.Must(template.New("html").Funcs(template.FuncMap{
  166. "colors": colors,
  167. }).Parse(tmplHTML))
  168. type templateData struct {
  169. Files []*templateFile
  170. Set bool
  171. }
  172. type templateFile struct {
  173. Name string
  174. Body template.HTML
  175. Coverage float64
  176. }
  177. const tmplHTML = `
  178. <!DOCTYPE html>
  179. <html>
  180. <head>
  181. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  182. <style>
  183. body {
  184. background: black;
  185. color: rgb(80, 80, 80);
  186. }
  187. body, pre, #legend span {
  188. font-family: Menlo, monospace;
  189. font-weight: bold;
  190. }
  191. #topbar {
  192. background: black;
  193. position: fixed;
  194. top: 0; left: 0; right: 0;
  195. height: 42px;
  196. border-bottom: 1px solid rgb(80, 80, 80);
  197. }
  198. #content {
  199. margin-top: 50px;
  200. }
  201. #nav, #legend {
  202. float: left;
  203. margin-left: 10px;
  204. }
  205. #legend {
  206. margin-top: 12px;
  207. }
  208. #nav {
  209. margin-top: 10px;
  210. }
  211. #legend span {
  212. margin: 0 5px;
  213. }
  214. {{colors}}
  215. </style>
  216. </head>
  217. <body>
  218. <div id="topbar">
  219. <div id="nav">
  220. <select id="files">
  221. {{range $i, $f := .Files}}
  222. <option value="file{{$i}}">{{$f.Name}} ({{printf "%.1f" $f.Coverage}}%)</option>
  223. {{end}}
  224. </select>
  225. </div>
  226. <div id="legend">
  227. <span>not tracked</span>
  228. {{if .Set}}
  229. <span class="cov0">not covered</span>
  230. <span class="cov8">covered</span>
  231. {{else}}
  232. <span class="cov0">no coverage</span>
  233. <span class="cov1">low coverage</span>
  234. <span class="cov2">*</span>
  235. <span class="cov3">*</span>
  236. <span class="cov4">*</span>
  237. <span class="cov5">*</span>
  238. <span class="cov6">*</span>
  239. <span class="cov7">*</span>
  240. <span class="cov8">*</span>
  241. <span class="cov9">*</span>
  242. <span class="cov10">high coverage</span>
  243. {{end}}
  244. </div>
  245. </div>
  246. <div id="content">
  247. {{range $i, $f := .Files}}
  248. <pre class="file" id="file{{$i}}" {{if $i}}style="display: none"{{end}}>{{$f.Body}}</pre>
  249. {{end}}
  250. </div>
  251. </body>
  252. <script>
  253. (function() {
  254. var files = document.getElementById('files');
  255. var visible = document.getElementById('file0');
  256. files.addEventListener('change', onChange, false);
  257. function onChange() {
  258. visible.style.display = 'none';
  259. visible = document.getElementById(files.value);
  260. visible.style.display = 'block';
  261. window.scrollTo(0, 0);
  262. }
  263. })();
  264. </script>
  265. </html>
  266. `