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"
|
2017-12-12 08:13:35 +00:00
|
|
|
"path/filepath"
|
2015-10-12 08:16:57 +00:00
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2018-06-21 12:38:08 +00:00
|
|
|
"sync"
|
2017-11-16 11:42:30 +00:00
|
|
|
"time"
|
2015-10-13 12:58:50 +00:00
|
|
|
|
2017-06-17 10:40:18 +00:00
|
|
|
"github.com/google/syzkaller/pkg/cover"
|
2017-12-12 08:13:35 +00:00
|
|
|
"github.com/google/syzkaller/pkg/hash"
|
2017-11-16 11:42:30 +00:00
|
|
|
"github.com/google/syzkaller/pkg/osutil"
|
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 coverage struct {
|
|
|
|
line int
|
|
|
|
covered bool
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2018-06-21 12:38:08 +00:00
|
|
|
initCoverOnce sync.Once
|
|
|
|
initCoverError error
|
|
|
|
initCoverSymbols []symbol
|
|
|
|
initCoverPCs []uint64
|
|
|
|
initCoverVMOffset uint32
|
2016-09-06 17:35:48 +00:00
|
|
|
)
|
|
|
|
|
2018-06-21 12:38:08 +00:00
|
|
|
func initCover(kernelObj, arch string) error {
|
|
|
|
if kernelObj == "" {
|
|
|
|
return fmt.Errorf("kernel_obj is not specified")
|
|
|
|
}
|
|
|
|
vmlinux := filepath.Join(kernelObj, "vmlinux")
|
|
|
|
symbols, err := symbolizer.ReadSymbols(vmlinux)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to run nm on %v: %v", vmlinux, err)
|
|
|
|
}
|
|
|
|
for name, ss := range symbols {
|
|
|
|
for _, s := range ss {
|
|
|
|
initCoverSymbols = append(initCoverSymbols, symbol{s.Addr, s.Addr + uint64(s.Size), name})
|
2017-05-23 15:18:01 +00:00
|
|
|
}
|
2018-06-21 12:38:08 +00:00
|
|
|
}
|
|
|
|
sort.Slice(initCoverSymbols, func(i, j int) bool {
|
|
|
|
return initCoverSymbols[i].start < initCoverSymbols[j].start
|
|
|
|
})
|
|
|
|
initCoverPCs, err = coveredPCs(arch, vmlinux)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to run objdump on %v: %v", vmlinux, err)
|
|
|
|
}
|
|
|
|
sort.Slice(initCoverPCs, func(i, j int) bool {
|
|
|
|
return initCoverPCs[i] < initCoverPCs[j]
|
|
|
|
})
|
|
|
|
initCoverVMOffset, err = getVMOffset(vmlinux)
|
|
|
|
return err
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
|
|
|
|
2018-07-23 06:11:45 +00:00
|
|
|
func generateCoverHTML(w io.Writer, kernelObj, kernelSrc, arch string, cov cover.Cover) error {
|
2016-02-18 20:48:45 +00:00
|
|
|
if len(cov) == 0 {
|
2018-06-21 12:38:08 +00:00
|
|
|
return fmt.Errorf("no coverage data available")
|
2016-02-18 20:48:45 +00:00
|
|
|
}
|
2018-06-21 12:38:08 +00:00
|
|
|
initCoverOnce.Do(func() { initCoverError = initCover(kernelObj, arch) })
|
|
|
|
if initCoverError != nil {
|
|
|
|
return initCoverError
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
2018-06-21 12:38:08 +00:00
|
|
|
|
2018-02-20 19:44:04 +00:00
|
|
|
pcs := make([]uint64, 0, len(cov))
|
|
|
|
for pc := range cov {
|
2018-06-21 12:38:08 +00:00
|
|
|
fullPC := cover.RestorePC(pc, initCoverVMOffset)
|
2018-04-27 12:14:03 +00:00
|
|
|
prevPC := previousInstructionPC(arch, fullPC)
|
|
|
|
pcs = append(pcs, prevPC)
|
2016-09-06 17:35:48 +00:00
|
|
|
}
|
2018-06-21 12:38:08 +00:00
|
|
|
vmlinux := filepath.Join(kernelObj, "vmlinux")
|
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
|
|
|
|
}
|
|
|
|
|
2018-03-08 17:48:26 +00:00
|
|
|
coveredFrames, _, 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) {
|
2018-07-23 06:11:45 +00:00
|
|
|
remain := strings.TrimPrefix(f, prefix)
|
|
|
|
if kernelSrc != "" {
|
|
|
|
f = filepath.Join(kernelSrc, remain)
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
}
|
2018-07-23 06:11:45 +00:00
|
|
|
f = filepath.Clean(remain)
|
2015-10-12 08:16:57 +00:00
|
|
|
d.Files = append(d.Files, &templateFile{
|
2017-12-12 08:13:35 +00:00
|
|
|
ID: hash.String([]byte(f)),
|
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))
|
2018-03-08 17:48:26 +00:00
|
|
|
return coverTemplate.Execute(w, d)
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
2018-06-21 12:38:08 +00:00
|
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
|
|
return sorted[i].line < sorted[j].line
|
|
|
|
})
|
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
|
|
|
}
|
|
|
|
|
2018-03-08 17:48:26 +00:00
|
|
|
func getVMOffset(vmlinux string) (uint32, error) {
|
2017-11-23 08:21:00 +00:00
|
|
|
out, err := osutil.RunCmd(time.Hour, "", "readelf", "-SW", vmlinux)
|
2016-04-27 15:37:54 +00:00
|
|
|
if err != nil {
|
2017-11-16 11:42:30 +00:00
|
|
|
return 0, err
|
2016-04-27 15:37:54 +00:00
|
|
|
}
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
handledFuncs := make(map[uint64]bool)
|
|
|
|
uncovered := make(map[uint64]bool)
|
2016-09-06 17:35:48 +00:00
|
|
|
for _, pc := range pcs {
|
2018-06-21 12:38:08 +00:00
|
|
|
idx := sort.Search(len(initCoverSymbols), func(i int) bool {
|
|
|
|
return pc < initCoverSymbols[i].end
|
2016-09-06 17:35:48 +00:00
|
|
|
})
|
2018-06-21 12:38:08 +00:00
|
|
|
if idx == len(initCoverSymbols) {
|
2016-09-06 17:35:48 +00:00
|
|
|
continue
|
|
|
|
}
|
2018-06-21 12:38:08 +00:00
|
|
|
s := initCoverSymbols[idx]
|
2016-09-06 17:35:48 +00:00
|
|
|
if pc < s.start || pc > s.end {
|
|
|
|
continue
|
|
|
|
}
|
2017-01-17 10:48:54 +00:00
|
|
|
if !handledFuncs[s.start] {
|
|
|
|
handledFuncs[s.start] = true
|
2018-06-21 12:38:08 +00:00
|
|
|
startPC := sort.Search(len(initCoverPCs), func(i int) bool {
|
|
|
|
return s.start <= initCoverPCs[i]
|
2017-01-17 10:48:54 +00:00
|
|
|
})
|
2018-06-21 12:38:08 +00:00
|
|
|
endPC := sort.Search(len(initCoverPCs), func(i int) bool {
|
|
|
|
return s.end < initCoverPCs[i]
|
2017-01-17 10:48:54 +00:00
|
|
|
})
|
2018-06-21 12:38:08 +00:00
|
|
|
for _, pc1 := range initCoverPCs[startPC:endPC] {
|
2017-01-17 10:48:54 +00:00
|
|
|
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.
|
2018-04-20 17:19:09 +00:00
|
|
|
func coveredPCs(arch, bin string) ([]uint64, error) {
|
2017-11-16 11:42:30 +00:00
|
|
|
cmd := osutil.Command("objdump", "-d", "--no-show-raw-insn", bin)
|
2016-09-06 17:35:48 +00:00
|
|
|
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)
|
|
|
|
traceFunc := []byte(" <__sanitizer_cov_trace_pc>")
|
2018-04-20 17:19:09 +00:00
|
|
|
var callInsn []byte
|
|
|
|
switch arch {
|
|
|
|
case "amd64":
|
|
|
|
// ffffffff8100206a: callq ffffffff815cc1d0 <__sanitizer_cov_trace_pc>
|
|
|
|
callInsn = []byte("\tcallq ")
|
|
|
|
case "386":
|
|
|
|
// c1000102: call c10001f0 <__sanitizer_cov_trace_pc>
|
|
|
|
callInsn = []byte("\tcall ")
|
|
|
|
case "arm64":
|
|
|
|
// ffff0000080d9cc0: bl ffff00000820f478 <__sanitizer_cov_trace_pc>
|
|
|
|
callInsn = []byte("\tbl\t")
|
|
|
|
case "arm":
|
|
|
|
// 8010252c: bl 801c3280 <__sanitizer_cov_trace_pc>
|
|
|
|
callInsn = []byte("\tbl\t")
|
|
|
|
case "ppc64le":
|
|
|
|
// c00000000006d904: bl c000000000350780 <.__sanitizer_cov_trace_pc>
|
|
|
|
callInsn = []byte("\tbl ")
|
|
|
|
traceFunc = []byte(" <.__sanitizer_cov_trace_pc>")
|
|
|
|
default:
|
|
|
|
panic("unknown arch")
|
|
|
|
}
|
2016-09-06 17:35:48 +00:00
|
|
|
for s.Scan() {
|
|
|
|
ln := s.Bytes()
|
|
|
|
if pos := bytes.Index(ln, callInsn); pos == -1 {
|
|
|
|
continue
|
2018-03-08 17:48:26 +00:00
|
|
|
} else if !bytes.Contains(ln[pos:], traceFunc) {
|
2016-09-06 17:35:48 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-04-27 12:14:03 +00:00
|
|
|
func previousInstructionPC(arch string, pc uint64) uint64 {
|
|
|
|
switch arch {
|
|
|
|
case "amd64":
|
|
|
|
return pc - 5
|
|
|
|
case "386":
|
|
|
|
return pc - 1
|
|
|
|
case "arm64":
|
|
|
|
return pc - 4
|
|
|
|
case "arm":
|
|
|
|
// THUMB instructions are 2 or 4 bytes with low bit set.
|
|
|
|
// ARM instructions are always 4 bytes.
|
|
|
|
return (pc - 3) & ^uint64(1)
|
|
|
|
case "ppc64le":
|
|
|
|
return pc - 4
|
|
|
|
default:
|
|
|
|
panic("unknown arch")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-12 08:16:57 +00:00
|
|
|
type templateData struct {
|
|
|
|
Files []*templateFile
|
|
|
|
}
|
|
|
|
|
|
|
|
type templateFile struct {
|
2017-12-12 08:13:35 +00:00
|
|
|
ID string
|
2015-10-12 08:16:57 +00:00
|
|
|
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
|
|
|
|
2017-12-12 08:13:35 +00:00
|
|
|
var coverTemplate = template.Must(template.New("").Parse(`
|
2015-10-12 08:16:57 +00:00
|
|
|
<!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">
|
2017-12-12 08:13:35 +00:00
|
|
|
{{range $f := .Files}}
|
|
|
|
<option value="{{$f.ID}}">{{$f.Name}} ({{$f.Coverage}})</option>
|
2015-10-12 08:16:57 +00:00
|
|
|
{{end}}
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="content">
|
|
|
|
{{range $i, $f := .Files}}
|
2017-12-12 08:13:35 +00:00
|
|
|
<pre class="file" id="{{$f.ID}}" {{if $i}}style="display: none;"{{end}}>{{$f.Body}}</pre>{{end}}
|
2015-10-12 08:16:57 +00:00
|
|
|
</div>
|
|
|
|
</body>
|
|
|
|
<script>
|
|
|
|
(function() {
|
|
|
|
var files = document.getElementById('files');
|
2017-12-12 08:13:35 +00:00
|
|
|
var visible = document.getElementById(files.value);
|
|
|
|
if (window.location.hash) {
|
|
|
|
var hash = window.location.hash.substring(1);
|
|
|
|
for (var i = 0; i < files.options.length; i++) {
|
2018-01-24 10:25:14 +00:00
|
|
|
if (files.options[i].value === hash) {
|
|
|
|
files.selectedIndex = i;
|
|
|
|
visible.style.display = 'none';
|
|
|
|
visible = document.getElementById(files.value);
|
|
|
|
visible.style.display = 'block';
|
|
|
|
break;
|
|
|
|
}
|
2017-12-12 08:13:35 +00:00
|
|
|
}
|
|
|
|
}
|
2015-10-12 08:16:57 +00:00
|
|
|
files.addEventListener('change', onChange, false);
|
|
|
|
function onChange() {
|
|
|
|
visible.style.display = 'none';
|
|
|
|
visible = document.getElementById(files.value);
|
|
|
|
visible.style.display = 'block';
|
|
|
|
window.scrollTo(0, 0);
|
2017-12-12 08:13:35 +00:00
|
|
|
window.location.hash = files.value;
|
2015-10-12 08:16:57 +00:00
|
|
|
}
|
|
|
|
})();
|
|
|
|
</script>
|
|
|
|
</html>
|
|
|
|
`))
|