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