// 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)) } }) } }