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