github.com/april1989/origin-go-tools@v0.0.32/cmd/cover/html.go (about) 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 "html/template" 12 "io" 13 "io/ioutil" 14 "math" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "runtime" 19 20 "github.com/april1989/origin-go-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 `