Home | History | Annotate | Download | only in cover
      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("&gt;")
    125 		case '<':
    126 			dst.WriteString("&lt;")
    127 		case '&':
    128 			dst.WriteString("&amp;")
    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