blob: 7012b54f22753c576c5ca17240a8fbbf97d3ad10 [file] [log] [blame]
// 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
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:
const (
titlePrefix = "TITLE: "
startPrefix = "START: "
endPrefix = "END: "
corruptedPrefix = "CORRUPTED: "
suppressedPrefix = "SUPPRESSED: "
)
switch ln := s.Text(); {
case strings.HasPrefix(ln, "#"):
case strings.HasPrefix(ln, titlePrefix):
test.Title = ln[len(titlePrefix):]
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)
}
case ln == "":
phase = phaseLog
default:
t.Fatalf("unknown header field %q", 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 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 := "", false, "", false
if rep != nil {
title = rep.Title
corrupted = rep.Corrupted
corruptedReason = rep.CorruptedReason
suppressed = rep.Suppressed
}
if title != test.Title || corrupted != test.Corrupted || suppressed != test.Suppressed {
if *flagUpdate && test.StartLine == "" && test.EndLine == "" {
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "TITLE: %v\n", title)
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)
}
}
t.Fatalf("want:\nTITLE: %s\nCORRUPTED: %v\nSUPPRESSED: %v\n"+
"got:\nTITLE: %s\nCORRUPTED: %v (%v)\nSUPPRESSED: %v\n",
test.Title, test.Corrupted, test.Suppressed,
title, corrupted, corruptedReason, suppressed)
}
if title != "" && len(rep.Report) == 0 {
t.Fatalf("found crash message but report is empty")
}
if rep != nil {
checkReport(t, rep, test)
}
}
func checkReport(t *testing.T, 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 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]))
}
}
}
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:]
if guilty := reporter.(guilter).extractGuiltyFile(report); guilty != file {
t.Fatalf("got guilty %q, want %q", guilty, file)
}
}
func forEachFile(t *testing.T, dir string, fn func(t *testing.T, reporter Reporter, fn string)) {
testFilenameRe := regexp.MustCompile("^[0-9]+$")
for os := range ctors {
path := filepath.Join("testdata", os, dir)
if !osutil.IsExist(path) {
continue
}
files, err := ioutil.ReadDir(path)
if err != nil {
t.Fatal(err)
}
cfg := &mgrconfig.Config{
TargetOS: os,
}
reporter, err := NewReporter(cfg)
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if !testFilenameRe.MatchString(file.Name()) {
continue
}
t.Run(fmt.Sprintf("%v/%v", os, file.Name()), func(t *testing.T) {
fn(t, reporter, filepath.Join(path, file.Name()))
})
}
}
}
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))
}
})
}
}