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