2015-10-12 08:16:57 +00:00
|
|
|
// Copyright 2015 syzkaller project authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os/exec"
|
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2015-10-13 12:58:50 +00:00
|
|
|
|
2017-06-17 10:40:18 +00:00
|
|
|
"github.com/google/syzkaller/pkg/cover"
|
2017-06-01 16:52:11 +00:00
|
|
|
. "github.com/google/syzkaller/pkg/log"
|
2017-06-17 10:53:47 +00:00
|
|
|
"github.com/google/syzkaller/pkg/symbolizer"
|
2015-10-12 08:16:57 +00:00
|
|
|
)
|
|
|
|
|
2016-09-06 17:35:48 +00:00
|
|
|
type symbol struct {
|
|
|
|
start uint64
|
|
|
|
end uint64
|
|
|
|
name string
|
|
|
|
}
|
|
|
|
|
|
|
|
type symbolArray []symbol
|
|
|
|
|
|
|
|
func (a symbolArray) Len() int { return len(a) }
|
|
|
|
func (a symbolArray) Less(i, j int) bool { return a[i].start < a[j].start }
|
|
|
|
func (a symbolArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
|
|
|
|
type coverage struct {
|
|
|
|
line int
|
|
|
|
covered bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type coverageArray []coverage
|
|
|
|
|
|
|
|
func (a coverageArray) Len() int { return len(a) }
|
|
|
|
func (a coverageArray) Less(i, j int) bool { return a[i].line < a[j].line }
|
|
|
|
func (a coverageArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
|
|
|
|
type uint64Array []uint64
|
|
|
|
|
|
|
|
func (a uint64Array) Len() int { return len(a) }
|
|
|
|
func (a uint64Array) Less(i, j int) bool { return a[i] < a[j] }
|
|
|
|
func (a uint64Array) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
|
|
|
|
var (
|
2017-05-23 15:18:01 +00:00
|
|
|
allCoverPCs []uint64
|
|
|
|
allCoverReady = make(chan bool)
|
|
|
|
allSymbols map[string][]symbolizer.Symbol
|
|
|
|
allSymbolsReady = make(chan bool)
|
2017-05-25 09:36:19 +00:00
|
|
|
vmOffsets = make(map[string]uint32)
|
2016-09-06 17:35:48 +00:00
|
|
|
)
|
|
|
|
|
2017-01-17 10:48:54 +00:00
|
|
|
const (
|
|
|
|
callLen = 5 // length of a call instruction, x86-ism
|
|
|
|
)
|
|
|
|
|
2016-09-06 17:35:48 +00:00
|
|
|
func initAllCover(vmlinux string) {
|
|
|
|
// Running objdump on vmlinux takes 20-30 seconds, so we do it asynchronously on start.
|
2017-05-23 15:18:01 +00:00
|
|
|
// Running nm on vmlinux may takes 200 microsecond and being called during symbolization of every crash,
|
|
|
|
// so also do it asynchronously on start and reuse the value during each crash.
|
2016-09-06 17:35:48 +00:00
|
|
|
go func() {
|
|
|
|
pcs, err := coveredPCs(vmlinux)
|
|
|
|
if err == nil {
|
|
|
|
sort.Sort(uint64Array(pcs))
|
|
|
|
allCoverPCs = pcs
|
|
|
|
} else {
|
2016-10-09 08:15:57 +00:00
|
|
|
Logf(0, "failed to run objdump on %v: %v", vmlinux, err)
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
2017-05-23 15:18:01 +00:00
|
|
|
|
|
|
|
allSymbols, err = symbolizer.ReadSymbols(vmlinux)
|
|
|
|
if err != nil {
|
|
|
|
Logf(0, "failed to run nm on %v: %v", vmlinux, err)
|
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
close(allCoverReady)
|
2017-05-23 15:18:01 +00:00
|
|
|
close(allSymbolsReady)
|
2016-09-06 17:35:48 +00:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2015-10-12 08:16:57 +00:00
|
|
|
func generateCoverHtml(w io.Writer, vmlinux string, cov []uint32) error {
|
2016-02-18 20:48:45 +00:00
|
|
|
if len(cov) == 0 {
|
|
|
|
return fmt.Errorf("No coverage data available")
|
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
|
|
|
|
base, err := getVmOffset(vmlinux)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pcs := make([]uint64, len(cov))
|
|
|
|
for i, pc := range cov {
|
2017-01-17 10:48:54 +00:00
|
|
|
pcs[i] = cover.RestorePC(pc, base) - callLen
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
2017-01-17 10:48:54 +00:00
|
|
|
uncovered, err := uncoveredPcsInFuncs(vmlinux, pcs)
|
2016-09-06 17:35:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-01-17 10:48:54 +00:00
|
|
|
coveredFrames, prefix, err := symbolize(vmlinux, pcs)
|
2015-10-12 08:16:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-01-17 10:48:54 +00:00
|
|
|
if len(coveredFrames) == 0 {
|
2016-02-18 20:48:45 +00:00
|
|
|
return fmt.Errorf("'%s' does not have debug info (set CONFIG_DEBUG_INFO=y)", vmlinux)
|
2016-02-16 14:10:24 +00:00
|
|
|
}
|
2015-10-12 08:16:57 +00:00
|
|
|
|
2017-01-17 10:48:54 +00:00
|
|
|
uncoveredFrames, prefix, err := symbolize(vmlinux, uncovered)
|
2016-09-06 17:35:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-10-12 08:16:57 +00:00
|
|
|
var d templateData
|
2017-01-17 10:48:54 +00:00
|
|
|
for f, covered := range fileSet(coveredFrames, uncoveredFrames) {
|
2016-02-16 14:06:24 +00:00
|
|
|
lines, err := parseFile(f)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
coverage := 0
|
2015-10-12 08:16:57 +00:00
|
|
|
var buf bytes.Buffer
|
|
|
|
for i, ln := range lines {
|
2016-09-06 17:35:48 +00:00
|
|
|
if len(covered) > 0 && covered[0].line == i+1 {
|
|
|
|
if covered[0].covered {
|
|
|
|
buf.Write([]byte("<span id='covered'>"))
|
|
|
|
buf.Write(ln)
|
|
|
|
buf.Write([]byte("</span> /*covered*/\n"))
|
|
|
|
coverage++
|
|
|
|
} else {
|
|
|
|
buf.Write([]byte("<span id='uncovered'>"))
|
|
|
|
buf.Write(ln)
|
|
|
|
buf.Write([]byte("</span>\n"))
|
|
|
|
}
|
2015-10-12 08:16:57 +00:00
|
|
|
covered = covered[1:]
|
|
|
|
} else {
|
|
|
|
buf.Write(ln)
|
2016-09-06 17:35:48 +00:00
|
|
|
buf.Write([]byte{'\n'})
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
|
|
|
}
|
2016-02-16 14:06:24 +00:00
|
|
|
if len(f) > len(prefix) {
|
|
|
|
f = f[len(prefix):]
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
|
|
|
d.Files = append(d.Files, &templateFile{
|
2016-02-16 14:06:24 +00:00
|
|
|
Name: f,
|
2015-10-12 08:16:57 +00:00
|
|
|
Body: template.HTML(buf.String()),
|
|
|
|
Coverage: coverage,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Sort(templateFileArray(d.Files))
|
|
|
|
if err := coverTemplate.Execute(w, d); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-01-17 10:48:54 +00:00
|
|
|
func fileSet(covered, uncovered []symbolizer.Frame) map[string][]coverage {
|
2016-09-06 17:35:48 +00:00
|
|
|
files := make(map[string]map[int]bool)
|
|
|
|
funcs := make(map[string]bool)
|
2017-01-17 10:48:54 +00:00
|
|
|
for _, frame := range covered {
|
2016-08-31 15:00:55 +00:00
|
|
|
if files[frame.File] == nil {
|
2016-09-06 17:35:48 +00:00
|
|
|
files[frame.File] = make(map[int]bool)
|
|
|
|
}
|
|
|
|
files[frame.File][frame.Line] = true
|
|
|
|
funcs[frame.Func] = true
|
|
|
|
}
|
2017-01-17 10:48:54 +00:00
|
|
|
for _, frame := range uncovered {
|
2016-09-06 17:35:48 +00:00
|
|
|
if !funcs[frame.Func] {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if files[frame.File] == nil {
|
|
|
|
files[frame.File] = make(map[int]bool)
|
|
|
|
}
|
|
|
|
if !files[frame.File][frame.Line] {
|
|
|
|
files[frame.File][frame.Line] = false
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
res := make(map[string][]coverage)
|
2015-10-12 08:16:57 +00:00
|
|
|
for f, lines := range files {
|
2016-09-06 17:35:48 +00:00
|
|
|
sorted := make([]coverage, 0, len(lines))
|
|
|
|
for ln, covered := range lines {
|
|
|
|
sorted = append(sorted, coverage{ln, covered})
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
sort.Sort(coverageArray(sorted))
|
2015-10-12 08:16:57 +00:00
|
|
|
res[f] = sorted
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2016-02-16 14:06:24 +00:00
|
|
|
func parseFile(fn string) ([][]byte, error) {
|
2015-10-12 08:16:57 +00:00
|
|
|
data, err := ioutil.ReadFile(fn)
|
|
|
|
if err != nil {
|
2016-02-16 14:06:24 +00:00
|
|
|
return nil, err
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
2016-02-16 14:06:24 +00:00
|
|
|
htmlReplacer := strings.NewReplacer(">", ">", "<", "<", "&", "&", "\t", " ")
|
2015-10-12 08:16:57 +00:00
|
|
|
var lines [][]byte
|
|
|
|
for {
|
|
|
|
idx := bytes.IndexByte(data, '\n')
|
|
|
|
if idx == -1 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
lines = append(lines, []byte(htmlReplacer.Replace(string(data[:idx]))))
|
|
|
|
data = data[idx+1:]
|
|
|
|
}
|
|
|
|
if len(data) != 0 {
|
|
|
|
lines = append(lines, data)
|
|
|
|
}
|
2016-02-16 14:06:24 +00:00
|
|
|
return lines, nil
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
|
|
|
|
2016-04-27 15:37:54 +00:00
|
|
|
func getVmOffset(vmlinux string) (uint32, error) {
|
2017-05-23 15:18:01 +00:00
|
|
|
if v, ok := vmOffsets[vmlinux]; ok {
|
|
|
|
return v, nil
|
|
|
|
}
|
2016-04-27 15:37:54 +00:00
|
|
|
out, err := exec.Command("readelf", "-SW", vmlinux).CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("readelf failed: %v\n%s", err, out)
|
|
|
|
}
|
|
|
|
s := bufio.NewScanner(bytes.NewReader(out))
|
|
|
|
var addr uint32
|
|
|
|
for s.Scan() {
|
|
|
|
ln := s.Text()
|
|
|
|
pieces := strings.Fields(ln)
|
|
|
|
for i := 0; i < len(pieces); i++ {
|
|
|
|
if pieces[i] != "PROGBITS" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
v, err := strconv.ParseUint("0x"+pieces[i+1], 0, 64)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("failed to parse addr in readelf output: %v", err)
|
|
|
|
}
|
|
|
|
if v == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
v32 := (uint32)(v >> 32)
|
|
|
|
if addr == 0 {
|
|
|
|
addr = v32
|
|
|
|
}
|
|
|
|
if addr != v32 {
|
|
|
|
return 0, fmt.Errorf("different section offsets in a single binary")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-05-23 15:18:01 +00:00
|
|
|
vmOffsets[vmlinux] = addr
|
2016-04-27 15:37:54 +00:00
|
|
|
return addr, nil
|
|
|
|
}
|
|
|
|
|
2017-01-17 10:48:54 +00:00
|
|
|
// uncoveredPcsInFuncs returns uncovered PCs with __sanitizer_cov_trace_pc calls in functions containing pcs.
|
|
|
|
func uncoveredPcsInFuncs(vmlinux string, pcs []uint64) ([]uint64, error) {
|
2017-05-23 15:18:01 +00:00
|
|
|
<-allSymbolsReady
|
|
|
|
if allSymbols == nil {
|
|
|
|
return nil, fmt.Errorf("failed to run nm on vmlinux")
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
|
|
|
var symbols symbolArray
|
|
|
|
for name, ss := range allSymbols {
|
|
|
|
for _, s := range ss {
|
|
|
|
symbols = append(symbols, symbol{s.Addr, s.Addr + uint64(s.Size), name})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sort.Sort(symbols)
|
|
|
|
|
|
|
|
<-allCoverReady
|
|
|
|
if len(allCoverPCs) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2017-01-17 10:48:54 +00:00
|
|
|
handledFuncs := make(map[uint64]bool)
|
|
|
|
uncovered := make(map[uint64]bool)
|
2016-09-06 17:35:48 +00:00
|
|
|
for _, pc := range pcs {
|
|
|
|
idx := sort.Search(len(symbols), func(i int) bool {
|
|
|
|
return pc < symbols[i].end
|
|
|
|
})
|
|
|
|
if idx == len(symbols) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
s := symbols[idx]
|
|
|
|
if pc < s.start || pc > s.end {
|
|
|
|
continue
|
|
|
|
}
|
2017-01-17 10:48:54 +00:00
|
|
|
if !handledFuncs[s.start] {
|
|
|
|
handledFuncs[s.start] = true
|
|
|
|
startPC := sort.Search(len(allCoverPCs), func(i int) bool {
|
|
|
|
return s.start <= allCoverPCs[i]
|
|
|
|
})
|
|
|
|
endPC := sort.Search(len(allCoverPCs), func(i int) bool {
|
|
|
|
return s.end < allCoverPCs[i]
|
|
|
|
})
|
|
|
|
for _, pc1 := range allCoverPCs[startPC:endPC] {
|
|
|
|
uncovered[pc1] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
delete(uncovered, pc)
|
|
|
|
}
|
|
|
|
uncoveredPCs := make([]uint64, 0, len(uncovered))
|
|
|
|
for pc := range uncovered {
|
|
|
|
uncoveredPCs = append(uncoveredPCs, pc)
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
2017-01-17 10:48:54 +00:00
|
|
|
return uncoveredPCs, nil
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// coveredPCs returns list of PCs of __sanitizer_cov_trace_pc calls in binary bin.
|
|
|
|
func coveredPCs(bin string) ([]uint64, error) {
|
|
|
|
cmd := exec.Command("objdump", "-d", "--no-show-raw-insn", bin)
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer stdout.Close()
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer cmd.Wait()
|
|
|
|
var pcs []uint64
|
|
|
|
s := bufio.NewScanner(stdout)
|
|
|
|
// A line looks as: "ffffffff8100206a: callq ffffffff815cc1d0 <__sanitizer_cov_trace_pc>"
|
|
|
|
callInsn := []byte("callq ")
|
|
|
|
traceFunc := []byte(" <__sanitizer_cov_trace_pc>")
|
|
|
|
for s.Scan() {
|
|
|
|
ln := s.Bytes()
|
|
|
|
if pos := bytes.Index(ln, callInsn); pos == -1 {
|
|
|
|
continue
|
|
|
|
} else if bytes.Index(ln[pos:], traceFunc) == -1 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
colon := bytes.IndexByte(ln, ':')
|
|
|
|
if colon == -1 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
pc, err := strconv.ParseUint(string(ln[:colon]), 16, 64)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
pcs = append(pcs, pc)
|
2016-04-27 15:37:54 +00:00
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
if err := s.Err(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return pcs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func symbolize(vmlinux string, pcs []uint64) ([]symbolizer.Frame, string, error) {
|
2016-08-31 15:00:55 +00:00
|
|
|
symb := symbolizer.NewSymbolizer()
|
|
|
|
defer symb.Close()
|
|
|
|
|
|
|
|
frames, err := symb.SymbolizeArray(vmlinux, pcs)
|
2015-10-12 08:16:57 +00:00
|
|
|
if err != nil {
|
2016-02-16 14:06:24 +00:00
|
|
|
return nil, "", err
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
2016-08-31 15:00:55 +00:00
|
|
|
|
2016-02-16 14:06:24 +00:00
|
|
|
prefix := ""
|
2016-08-31 15:00:55 +00:00
|
|
|
for i := range frames {
|
|
|
|
frame := &frames[i]
|
|
|
|
frame.PC--
|
2016-02-16 14:06:24 +00:00
|
|
|
if prefix == "" {
|
2016-08-31 15:00:55 +00:00
|
|
|
prefix = frame.File
|
2016-02-16 14:06:24 +00:00
|
|
|
} else {
|
|
|
|
i := 0
|
2016-08-31 15:00:55 +00:00
|
|
|
for ; i < len(prefix) && i < len(frame.File); i++ {
|
|
|
|
if prefix[i] != frame.File[i] {
|
2016-02-16 14:06:24 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prefix = prefix[:i]
|
|
|
|
}
|
2016-08-31 15:00:55 +00:00
|
|
|
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
2016-08-31 15:00:55 +00:00
|
|
|
return frames, prefix, nil
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type templateData struct {
|
|
|
|
Files []*templateFile
|
|
|
|
}
|
|
|
|
|
|
|
|
type templateFile struct {
|
|
|
|
Name string
|
|
|
|
Body template.HTML
|
|
|
|
Coverage int
|
|
|
|
}
|
|
|
|
|
|
|
|
type templateFileArray []*templateFile
|
|
|
|
|
2017-01-08 13:08:49 +00:00
|
|
|
func (a templateFileArray) Len() int { return len(a) }
|
|
|
|
func (a templateFileArray) Less(i, j int) bool {
|
|
|
|
n1 := a[i].Name
|
|
|
|
n2 := a[j].Name
|
|
|
|
// Move include files to the bottom.
|
|
|
|
if len(n1) != 0 && len(n2) != 0 {
|
|
|
|
if n1[0] != '.' && n2[0] == '.' {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if n1[0] == '.' && n2[0] != '.' {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return n1 < n2
|
|
|
|
}
|
|
|
|
func (a templateFileArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
2015-10-12 08:16:57 +00:00
|
|
|
|
|
|
|
var coverTemplate = template.Must(template.New("").Parse(
|
|
|
|
`
|
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
|
|
<style>
|
|
|
|
body {
|
|
|
|
background: white;
|
|
|
|
}
|
|
|
|
#topbar {
|
|
|
|
background: black;
|
|
|
|
position: fixed;
|
|
|
|
top: 0; left: 0; right: 0;
|
|
|
|
height: 42px;
|
|
|
|
border-bottom: 1px solid rgb(70, 70, 70);
|
|
|
|
}
|
|
|
|
#nav {
|
|
|
|
float: left;
|
|
|
|
margin-left: 10px;
|
|
|
|
margin-top: 10px;
|
|
|
|
}
|
|
|
|
#content {
|
|
|
|
font-family: 'Courier New', Courier, monospace;
|
|
|
|
color: rgb(70, 70, 70);
|
|
|
|
margin-top: 50px;
|
|
|
|
}
|
|
|
|
#covered {
|
|
|
|
color: rgb(0, 0, 0);
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
#uncovered {
|
|
|
|
color: rgb(255, 0, 0);
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
2015-10-12 08:16:57 +00:00
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="topbar">
|
|
|
|
<div id="nav">
|
|
|
|
<select id="files">
|
|
|
|
{{range $i, $f := .Files}}
|
|
|
|
<option value="file{{$i}}">{{$f.Name}} ({{$f.Coverage}})</option>
|
|
|
|
{{end}}
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="content">
|
|
|
|
{{range $i, $f := .Files}}
|
|
|
|
<pre class="file" id="file{{$i}}" {{if $i}}style="display: none"{{end}}>{{$f.Body}}</pre>
|
|
|
|
{{end}}
|
|
|
|
</div>
|
|
|
|
</body>
|
|
|
|
<script>
|
|
|
|
(function() {
|
|
|
|
var files = document.getElementById('files');
|
|
|
|
var visible = document.getElementById('file0');
|
|
|
|
files.addEventListener('change', onChange, false);
|
|
|
|
function onChange() {
|
|
|
|
visible.style.display = 'none';
|
|
|
|
visible = document.getElementById(files.value);
|
|
|
|
visible.style.display = 'block';
|
|
|
|
window.scrollTo(0, 0);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
</script>
|
|
|
|
</html>
|
|
|
|
`))
|