syzkaller/pkg/report/report_test.go
Dmitry Vyukov 3d789641a3 pkg/report: add Report.SkipPos
SkipPos is what pkg/instance needs,
but also will be needed for ParseAll.
2020-05-13 20:37:26 +02:00

397 lines
11 KiB
Go

// 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 report
import (
"bufio"
"bytes"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/google/syzkaller/pkg/mgrconfig"
"github.com/google/syzkaller/pkg/osutil"
)
var flagUpdate = flag.Bool("update", false, "update test files accordingly to current results")
func TestParse(t *testing.T) {
forEachFile(t, "report", testParseFile)
}
type ParseTest struct {
FileName string
Log []byte
Title string
Type Type
Frame string
StartLine string
EndLine string
Corrupted bool
Suppressed bool
HasReport bool
Report []byte
}
func testParseFile(t *testing.T, reporter Reporter, fn string) {
input, err := os.Open(fn)
if err != nil {
t.Fatal(err)
}
defer input.Close()
const (
phaseHeaders = iota
phaseLog
phaseReport
)
phase := phaseHeaders
test := &ParseTest{
FileName: fn,
}
prevEmptyLine := false
s := bufio.NewScanner(input)
for s.Scan() {
switch phase {
case phaseHeaders:
ln := s.Text()
if ln == "" {
phase = phaseLog
continue
}
parseHeaderLine(t, test, ln)
case phaseLog:
if prevEmptyLine && string(s.Bytes()) == "REPORT:" {
test.HasReport = true
phase = phaseReport
} else {
test.Log = append(test.Log, s.Bytes()...)
test.Log = append(test.Log, '\n')
}
case phaseReport:
test.Report = append(test.Report, s.Bytes()...)
test.Report = append(test.Report, '\n')
}
prevEmptyLine = len(s.Bytes()) == 0
}
if s.Err() != nil {
t.Fatalf("file scanning error: %v", s.Err())
}
if len(test.Log) == 0 {
t.Fatalf("can't find log in input file")
}
testParseImpl(t, reporter, test)
// In some cases we get output with \r\n for line endings,
// ensure that regexps are not confused by this.
bytes.Replace(test.Log, []byte{'\n'}, []byte{'\r', '\n'}, -1)
testParseImpl(t, reporter, test)
}
func parseHeaderLine(t *testing.T, test *ParseTest, ln string) {
const (
titlePrefix = "TITLE: "
typePrefix = "TYPE: "
framePrefix = "FRAME: "
startPrefix = "START: "
endPrefix = "END: "
corruptedPrefix = "CORRUPTED: "
suppressedPrefix = "SUPPRESSED: "
)
switch {
case strings.HasPrefix(ln, "#"):
case strings.HasPrefix(ln, titlePrefix):
test.Title = ln[len(titlePrefix):]
case strings.HasPrefix(ln, typePrefix):
switch v := ln[len(typePrefix):]; v {
case Hang.String():
test.Type = Hang
case MemoryLeak.String():
test.Type = MemoryLeak
case DataRace.String():
test.Type = DataRace
case UnexpectedReboot.String():
test.Type = UnexpectedReboot
default:
t.Fatalf("unknown TYPE value %q", v)
}
case strings.HasPrefix(ln, framePrefix):
test.Frame = ln[len(framePrefix):]
case strings.HasPrefix(ln, startPrefix):
test.StartLine = ln[len(startPrefix):]
case strings.HasPrefix(ln, endPrefix):
test.EndLine = ln[len(endPrefix):]
case strings.HasPrefix(ln, corruptedPrefix):
switch v := ln[len(corruptedPrefix):]; v {
case "Y":
test.Corrupted = true
case "N":
test.Corrupted = false
default:
t.Fatalf("unknown CORRUPTED value %q", v)
}
case strings.HasPrefix(ln, suppressedPrefix):
switch v := ln[len(suppressedPrefix):]; v {
case "Y":
test.Suppressed = true
case "N":
test.Suppressed = false
default:
t.Fatalf("unknown SUPPRESSED value %q", v)
}
default:
t.Fatalf("unknown header field %q", ln)
}
}
func testParseImpl(t *testing.T, reporter Reporter, test *ParseTest) {
rep := reporter.Parse(test.Log)
containsCrash := reporter.ContainsCrash(test.Log)
expectCrash := (test.Title != "")
if expectCrash && !containsCrash {
t.Fatalf("ContainsCrash did not find crash")
}
if !expectCrash && containsCrash {
t.Fatalf("ContainsCrash found unexpected crash")
}
if rep != nil && rep.Title == "" {
t.Fatalf("found crash, but title is empty")
}
title, corrupted, corruptedReason, suppressed, typ, frame := "", false, "", false, Unknown, ""
if rep != nil {
title = rep.Title
corrupted = rep.Corrupted
corruptedReason = rep.CorruptedReason
suppressed = rep.Suppressed
typ = rep.Type
frame = rep.Frame
}
if title != test.Title || corrupted != test.Corrupted || suppressed != test.Suppressed ||
typ != test.Type || test.Frame != "" && frame != test.Frame {
if *flagUpdate && test.StartLine+test.EndLine == "" {
updateReportTest(t, test, title, corrupted, suppressed, typ, frame)
}
t.Fatalf("want:\nTITLE: %s\nTYPE: %v\nFRAME: %v\nCORRUPTED: %v\nSUPPRESSED: %v\n"+
"got:\nTITLE: %s\nTYPE: %v\nFRAME: %v\nCORRUPTED: %v (%v)\nSUPPRESSED: %v\n",
test.Title, test.Type, test.Frame, test.Corrupted, test.Suppressed,
title, typ, frame, corrupted, corruptedReason, suppressed)
}
if title != "" && len(rep.Report) == 0 {
t.Fatalf("found crash message but report is empty")
}
if rep == nil {
return
}
checkReport(t, reporter, rep, test)
}
func checkReport(t *testing.T, reporter Reporter, rep *Report, test *ParseTest) {
if test.HasReport && !bytes.Equal(rep.Report, test.Report) {
t.Fatalf("extracted wrong report:\n%s\nwant:\n%s", rep.Report, test.Report)
}
if !bytes.Equal(rep.Output, test.Log) {
t.Fatalf("bad Output:\n%s", rep.Output)
}
if rep.StartPos != 0 && rep.EndPos != 0 && rep.StartPos >= rep.EndPos {
t.Fatalf("StartPos=%v >= EndPos=%v", rep.StartPos, rep.EndPos)
}
if rep.EndPos > len(rep.Output) {
t.Fatalf("EndPos=%v > len(Output)=%v", rep.EndPos, len(rep.Output))
}
if rep.SkipPos <= rep.StartPos || rep.SkipPos > rep.EndPos {
t.Fatalf("bad SkipPos=%v: StartPos=%v EndPos=%v", rep.SkipPos, rep.StartPos, rep.EndPos)
}
if test.StartLine != "" {
if test.EndLine == "" {
test.EndLine = test.StartLine
}
startPos := bytes.Index(test.Log, []byte(test.StartLine))
endPos := bytes.Index(test.Log, []byte(test.EndLine)) + len(test.EndLine)
if rep.StartPos != startPos || rep.EndPos != endPos {
t.Fatalf("bad start/end pos %v-%v, want %v-%v, line %q",
rep.StartPos, rep.EndPos, startPos, endPos,
string(test.Log[rep.StartPos:rep.EndPos]))
}
}
if rep.StartPos != 0 {
// If we parse from StartPos, we must find the same report.
rep1 := reporter.Parse(test.Log[rep.StartPos:])
if rep1 == nil || rep1.Title != rep.Title || rep1.StartPos != 0 {
t.Fatalf("did not find the same report from rep.StartPos=%v", rep.StartPos)
}
// If we parse from EndPos, we must not find the same report.
rep2 := reporter.Parse(test.Log[rep.EndPos:])
if rep2 != nil && rep2.Title == rep.Title {
t.Fatalf("found the same report after rep.EndPos=%v", rep.EndPos)
}
}
}
func updateReportTest(t *testing.T, test *ParseTest, title string, corrupted, suppressed bool,
typ Type, frame string) {
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "TITLE: %v\n", title)
if typ != Unknown {
fmt.Fprintf(buf, "TYPE: %v\n", typ)
}
if test.Frame != "" {
fmt.Fprintf(buf, "FRAME: %v\n", frame)
}
if corrupted {
fmt.Fprintf(buf, "CORRUPTED: Y\n")
}
if suppressed {
fmt.Fprintf(buf, "SUPPRESSED: Y\n")
}
fmt.Fprintf(buf, "\n%s", test.Log)
if test.HasReport {
fmt.Fprintf(buf, "REPORT:\n%s", test.Report)
}
if err := ioutil.WriteFile(test.FileName, buf.Bytes(), 0640); err != nil {
t.Logf("failed to update test file: %v", err)
}
}
func TestGuiltyFile(t *testing.T) {
forEachFile(t, "guilty", testGuiltyFile)
}
func testGuiltyFile(t *testing.T, reporter Reporter, fn string) {
data, err := ioutil.ReadFile(fn)
if err != nil {
t.Fatal(err)
}
for bytes.HasPrefix(data, []byte{'#'}) {
nl := bytes.Index(data, []byte{'\n'})
if nl == -1 {
t.Fatalf("unterminated comment in file")
}
data = data[nl+1:]
}
const prefix = "FILE: "
if !bytes.HasPrefix(data, []byte(prefix)) {
t.Fatalf("no %v prefix in file", prefix)
}
nlnl := bytes.Index(data[len(prefix):], []byte{'\n', '\n'})
if nlnl == -1 {
t.Fatalf("no \\n\\n in file")
}
file := string(data[len(prefix) : len(prefix)+nlnl])
report := data[len(prefix)+nlnl:]
rep := reporter.Parse(report)
if rep == nil {
t.Fatalf("did not find crash in the input")
}
// Parse doesn't generally run on already symbolized output,
// but here we run it on symbolized output because we can't symbolize in tests.
// The problem is with duplicated lines due to inlined frames,
// Parse can strip such report after first title line because it thinks
// that the duplicated title line is beginning on another report.
// In such case we restore whole report, but still keep StartPos that
// Parse produces at least in some cases.
if !bytes.HasSuffix(report, rep.Report) {
rep.Report = report
rep.StartPos = 0
}
if err := reporter.Symbolize(rep); err != nil {
t.Fatalf("failed to symbolize report: %v", err)
}
if rep.guiltyFile != file {
t.Fatalf("got guilty %q, want %q", rep.guiltyFile, file)
}
}
func forEachFile(t *testing.T, dir string, fn func(t *testing.T, reporter Reporter, fn string)) {
for os := range ctors {
if os == "windows" {
continue // not implemented
}
cfg := &mgrconfig.Config{
TargetOS: os,
TargetArch: "amd64",
}
reporter, err := NewReporter(cfg)
if err != nil {
t.Fatal(err)
}
for _, file := range readDir(t, filepath.Join("testdata", os, dir)) {
t.Run(fmt.Sprintf("%v/%v", os, filepath.Base(file)), func(t *testing.T) {
fn(t, reporter, file)
})
}
for _, file := range readDir(t, filepath.Join("testdata", "all", dir)) {
t.Run(fmt.Sprintf("%v/all/%v", os, filepath.Base(file)), func(t *testing.T) {
fn(t, reporter, file)
})
}
}
}
func readDir(t *testing.T, dir string) (files []string) {
if !osutil.IsExist(dir) {
return nil
}
entries, err := ioutil.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
testFilenameRe := regexp.MustCompile("^[0-9]+$")
for _, ent := range entries {
if !testFilenameRe.MatchString(ent.Name()) {
continue
}
files = append(files, filepath.Join(dir, ent.Name()))
}
return
}
func TestReplace(t *testing.T) {
tests := []struct {
where string
start int
end int
what string
result string
}{
{"0123456789", 3, 5, "abcdef", "012abcdef56789"},
{"0123456789", 3, 5, "ab", "012ab56789"},
{"0123456789", 3, 3, "abcd", "012abcd3456789"},
{"0123456789", 0, 2, "abcd", "abcd23456789"},
{"0123456789", 0, 0, "ab", "ab0123456789"},
{"0123456789", 10, 10, "ab", "0123456789ab"},
{"0123456789", 8, 10, "ab", "01234567ab"},
{"0123456789", 5, 5, "", "0123456789"},
{"0123456789", 3, 8, "", "01289"},
{"0123456789", 3, 8, "ab", "012ab89"},
{"0123456789", 0, 5, "a", "a56789"},
{"0123456789", 5, 10, "ab", "01234ab"},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
result := replace([]byte(test.where), test.start, test.end, []byte(test.what))
if test.result != string(result) {
t.Errorf("want '%v', got '%v'", test.result, string(result))
}
})
}
}
func TestFuzz(t *testing.T) {
for _, data := range []string{
"kernel panicType 'help' for a list of commands",
"0000000000000000000\n\n\n\n\n\nBooting the kernel.",
"ZIRCON KERNEL PANICHalted",
"BUG:Disabling lock debugging due to kernel taint",
"[0.0] WARNING: ? 0+0x0/0",
"BUG: login: [0.0] ",
"cleaned vnod\re",
"kernel\r:",
} {
Fuzz([]byte(data)[:len(data):len(data)])
}
}