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