github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/report/report_test.go (about) 1 // Copyright 2015 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package report 5 6 import ( 7 "bufio" 8 "bytes" 9 "flag" 10 "fmt" 11 "os" 12 "path/filepath" 13 "reflect" 14 "regexp" 15 "sort" 16 "strings" 17 "testing" 18 19 "github.com/google/syzkaller/pkg/mgrconfig" 20 "github.com/google/syzkaller/pkg/osutil" 21 "github.com/google/syzkaller/pkg/report/crash" 22 "github.com/google/syzkaller/pkg/testutil" 23 "github.com/google/syzkaller/sys/targets" 24 "github.com/stretchr/testify/assert" 25 ) 26 27 var flagUpdate = flag.Bool("update", false, "update test files accordingly to current results") 28 29 func TestParse(t *testing.T) { 30 forEachFile(t, "report", testParseFile) 31 } 32 33 type ParseTest struct { 34 FileName string 35 Log []byte 36 Title string 37 AltTitles []string 38 Type crash.Type 39 Frame string 40 StartLine string 41 EndLine string 42 Corrupted bool 43 Suppressed bool 44 HasReport bool 45 Report []byte 46 } 47 48 func testParseFile(t *testing.T, reporter *Reporter, fn string) { 49 data, err := os.ReadFile(fn) 50 if err != nil { 51 t.Fatal(err) 52 } 53 // Strip all \r from reports because the merger removes it. 54 data = bytes.ReplaceAll(data, []byte{'\r'}, nil) 55 const ( 56 phaseHeaders = iota 57 phaseLog 58 phaseReport 59 ) 60 phase := phaseHeaders 61 test := &ParseTest{ 62 FileName: fn, 63 } 64 prevEmptyLine := false 65 s := bufio.NewScanner(bytes.NewReader(data)) 66 for s.Scan() { 67 switch phase { 68 case phaseHeaders: 69 ln := s.Text() 70 if ln == "" { 71 phase = phaseLog 72 continue 73 } 74 parseHeaderLine(t, test, ln) 75 case phaseLog: 76 if prevEmptyLine && string(s.Bytes()) == "REPORT:" { 77 test.HasReport = true 78 phase = phaseReport 79 } else { 80 test.Log = append(test.Log, s.Bytes()...) 81 test.Log = append(test.Log, '\n') 82 } 83 case phaseReport: 84 test.Report = append(test.Report, s.Bytes()...) 85 test.Report = append(test.Report, '\n') 86 } 87 prevEmptyLine = len(s.Bytes()) == 0 88 } 89 if s.Err() != nil { 90 t.Fatalf("file scanning error: %v", s.Err()) 91 } 92 if len(test.Log) == 0 { 93 t.Fatalf("can't find log in input file") 94 } 95 testParseImpl(t, reporter, test) 96 } 97 98 func parseHeaderLine(t *testing.T, test *ParseTest, ln string) { 99 const ( 100 titlePrefix = "TITLE: " 101 altTitlePrefix = "ALT: " 102 typePrefix = "TYPE: " 103 framePrefix = "FRAME: " 104 startPrefix = "START: " 105 endPrefix = "END: " 106 corruptedPrefix = "CORRUPTED: " 107 suppressedPrefix = "SUPPRESSED: " 108 ) 109 switch { 110 case strings.HasPrefix(ln, "#"): 111 case strings.HasPrefix(ln, titlePrefix): 112 test.Title = ln[len(titlePrefix):] 113 case strings.HasPrefix(ln, altTitlePrefix): 114 test.AltTitles = append(test.AltTitles, ln[len(altTitlePrefix):]) 115 case strings.HasPrefix(ln, typePrefix): 116 test.Type = crash.Type(ln[len(typePrefix):]) 117 case strings.HasPrefix(ln, framePrefix): 118 test.Frame = ln[len(framePrefix):] 119 case strings.HasPrefix(ln, startPrefix): 120 test.StartLine = ln[len(startPrefix):] 121 case strings.HasPrefix(ln, endPrefix): 122 test.EndLine = ln[len(endPrefix):] 123 case strings.HasPrefix(ln, corruptedPrefix): 124 switch v := ln[len(corruptedPrefix):]; v { 125 case "Y": 126 test.Corrupted = true 127 case "N": 128 test.Corrupted = false 129 default: 130 t.Fatalf("unknown CORRUPTED value %q", v) 131 } 132 case strings.HasPrefix(ln, suppressedPrefix): 133 switch v := ln[len(suppressedPrefix):]; v { 134 case "Y": 135 test.Suppressed = true 136 case "N": 137 test.Suppressed = false 138 default: 139 t.Fatalf("unknown SUPPRESSED value %q", v) 140 } 141 default: 142 t.Fatalf("unknown header field %q", ln) 143 } 144 } 145 146 func testParseImpl(t *testing.T, reporter *Reporter, test *ParseTest) { 147 rep := reporter.Parse(test.Log) 148 containsCrash := reporter.ContainsCrash(test.Log) 149 expectCrash := (test.Title != "") 150 if expectCrash && !containsCrash { 151 t.Fatalf("did not find crash") 152 } 153 if !expectCrash && containsCrash { 154 t.Fatalf("found unexpected crash") 155 } 156 if rep != nil && rep.Title == "" { 157 t.Fatalf("found crash, but title is empty") 158 } 159 if rep != nil && rep.Type == unspecifiedType { 160 t.Fatalf("unspecifiedType leaked outside") 161 } 162 title, corrupted, corruptedReason, suppressed, typ, frame := "", false, "", false, crash.UnknownType, "" 163 var altTitles []string 164 if rep != nil { 165 title = rep.Title 166 altTitles = rep.AltTitles 167 corrupted = rep.Corrupted 168 corruptedReason = rep.CorruptedReason 169 suppressed = rep.Suppressed 170 typ = rep.Type 171 frame = rep.Frame 172 } 173 sort.Strings(altTitles) 174 sort.Strings(test.AltTitles) 175 if title != test.Title || !reflect.DeepEqual(altTitles, test.AltTitles) || corrupted != test.Corrupted || 176 suppressed != test.Suppressed || typ != test.Type || test.Frame != "" && frame != test.Frame { 177 if *flagUpdate && test.StartLine+test.EndLine == "" { 178 updateReportTest(t, test, title, altTitles, corrupted, suppressed, typ, frame) 179 } 180 gotAltTitles, wantAltTitles := "", "" 181 for _, t := range altTitles { 182 gotAltTitles += "ALT: " + t + "\n" 183 } 184 for _, t := range test.AltTitles { 185 wantAltTitles += "ALT: " + t + "\n" 186 } 187 t.Fatalf("want:\nTITLE: %s\n%sTYPE: %v\nFRAME: %v\nCORRUPTED: %v\nSUPPRESSED: %v\n"+ 188 "got:\nTITLE: %s\n%sTYPE: %v\nFRAME: %v\nCORRUPTED: %v (%v)\nSUPPRESSED: %v", 189 test.Title, wantAltTitles, test.Type, test.Frame, test.Corrupted, test.Suppressed, 190 title, gotAltTitles, typ, frame, corrupted, corruptedReason, suppressed) 191 } 192 if title != "" && len(rep.Report) == 0 { 193 t.Fatalf("found crash message but report is empty") 194 } 195 if rep == nil { 196 return 197 } 198 checkReport(t, reporter, rep, test) 199 } 200 201 func checkReport(t *testing.T, reporter *Reporter, rep *Report, test *ParseTest) { 202 if test.HasReport && !bytes.Equal(rep.Report, test.Report) { 203 t.Fatalf("extracted wrong report:\n%s\nwant:\n%s", rep.Report, test.Report) 204 } 205 if !bytes.Equal(rep.Output, test.Log) { 206 t.Fatalf("bad Output:\n%s", rep.Output) 207 } 208 if rep.StartPos != 0 && rep.EndPos != 0 && rep.StartPos >= rep.EndPos { 209 t.Fatalf("StartPos %v >= EndPos %v", rep.StartPos, rep.EndPos) 210 } 211 if rep.EndPos > len(rep.Output) { 212 t.Fatalf("EndPos %v > len(Output) %v", rep.EndPos, len(rep.Output)) 213 } 214 if rep.SkipPos <= rep.StartPos || rep.SkipPos > rep.EndPos { 215 t.Fatalf("bad SkipPos %v: StartPos %v EndPos %v", rep.SkipPos, rep.StartPos, rep.EndPos) 216 } 217 if test.StartLine != "" { 218 if test.EndLine == "" { 219 test.EndLine = test.StartLine 220 } 221 startPos := bytes.Index(test.Log, []byte(test.StartLine)) 222 endPos := bytes.Index(test.Log, []byte(test.EndLine)) + len(test.EndLine) 223 if rep.StartPos != startPos || rep.EndPos != endPos { 224 t.Fatalf("bad start/end pos %v-%v, want %v-%v, line %q", 225 rep.StartPos, rep.EndPos, startPos, endPos, 226 string(test.Log[rep.StartPos:rep.EndPos])) 227 } 228 } 229 if rep.StartPos != 0 { 230 // If we parse from StartPos, we must find the same report. 231 rep1 := reporter.ParseFrom(test.Log, rep.StartPos) 232 if rep1 == nil || rep1.Title != rep.Title || rep1.StartPos != rep.StartPos { 233 t.Fatalf("did not find the same report from rep.StartPos=%v", rep.StartPos) 234 } 235 // If we parse from EndPos, we must not find the same report. 236 rep2 := reporter.ParseFrom(test.Log, rep.EndPos) 237 if rep2 != nil && rep2.Title == rep.Title { 238 t.Fatalf("found the same report after rep.EndPos=%v", rep.EndPos) 239 } 240 } 241 } 242 243 func updateReportTest(t *testing.T, test *ParseTest, title string, altTitles []string, corrupted, suppressed bool, 244 typ crash.Type, frame string) { 245 buf := new(bytes.Buffer) 246 fmt.Fprintf(buf, "TITLE: %v\n", title) 247 for _, t := range altTitles { 248 fmt.Fprintf(buf, "ALT: %v\n", t) 249 } 250 if typ != crash.UnknownType { 251 fmt.Fprintf(buf, "TYPE: %v\n", typ) 252 } 253 if test.Frame != "" { 254 fmt.Fprintf(buf, "FRAME: %v\n", frame) 255 } 256 if corrupted { 257 fmt.Fprintf(buf, "CORRUPTED: Y\n") 258 } 259 if suppressed { 260 fmt.Fprintf(buf, "SUPPRESSED: Y\n") 261 } 262 fmt.Fprintf(buf, "\n%s", test.Log) 263 if test.HasReport { 264 fmt.Fprintf(buf, "REPORT:\n%s", test.Report) 265 } 266 if err := os.WriteFile(test.FileName, buf.Bytes(), 0640); err != nil { 267 t.Logf("failed to update test file: %v", err) 268 } 269 } 270 271 func TestGuiltyFile(t *testing.T) { 272 forEachFile(t, "guilty", testGuiltyFile) 273 } 274 275 func testGuiltyFile(t *testing.T, reporter *Reporter, fn string) { 276 vars, report := parseGuiltyTest(t, fn) 277 file := vars["FILE"] 278 rep := reporter.Parse(report) 279 if rep == nil { 280 t.Fatalf("did not find crash in the input") 281 } 282 // Parse doesn't generally run on already symbolized output, 283 // but here we run it on symbolized output because we can't symbolize in tests. 284 // The problem is with duplicated lines due to inlined frames, 285 // Parse can strip such report after first title line because it thinks 286 // that the duplicated title line is beginning on another report. 287 // In such case we restore whole report, but still keep StartPos that 288 // Parse produces at least in some cases. 289 if !bytes.HasSuffix(report, rep.Report) { 290 rep.Report = report 291 rep.StartPos = 0 292 } 293 if err := reporter.Symbolize(rep); err != nil { 294 t.Fatalf("failed to symbolize report: %v", err) 295 } 296 if rep.GuiltyFile != file { 297 t.Fatalf("got guilty %q, want %q", rep.GuiltyFile, file) 298 } 299 } 300 301 func TestRawGuiltyFile(t *testing.T) { 302 forEachFile(t, "guilty_raw", testRawGuiltyFile) 303 } 304 305 func testRawGuiltyFile(t *testing.T, reporter *Reporter, fn string) { 306 vars, report := parseGuiltyTest(t, fn) 307 outFile := reporter.ReportToGuiltyFile(vars["TITLE"], report) 308 if outFile != vars["FILE"] { 309 t.Fatalf("expected %#v, got %#v", vars["FILE"], outFile) 310 } 311 } 312 313 func parseGuiltyTest(t *testing.T, fn string) (map[string]string, []byte) { 314 data, err := os.ReadFile(fn) 315 if err != nil { 316 t.Fatal(err) 317 } 318 nlnl := bytes.Index(data, []byte{'\n', '\n'}) 319 if nlnl == -1 { 320 t.Fatalf("no \\n\\n in file") 321 } 322 vars := map[string]string{} 323 s := bufio.NewScanner(bytes.NewReader(data[:nlnl])) 324 for s.Scan() { 325 ln := strings.TrimSpace(s.Text()) 326 if ln == "" || ln[0] == '#' { 327 continue 328 } 329 colon := strings.IndexByte(ln, ':') 330 if colon == -1 { 331 t.Fatalf("no : in %s", ln) 332 } 333 vars[strings.TrimSpace(ln[:colon])] = strings.TrimSpace(ln[colon+1:]) 334 } 335 return vars, data[nlnl+2:] 336 } 337 338 func forEachFile(t *testing.T, dir string, fn func(t *testing.T, reporter *Reporter, fn string)) { 339 for os := range ctors { 340 if os == targets.Windows { 341 continue // not implemented 342 } 343 cfg := &mgrconfig.Config{ 344 Derived: mgrconfig.Derived{ 345 TargetOS: os, 346 TargetArch: targets.AMD64, 347 SysTarget: targets.Get(os, targets.AMD64), 348 }, 349 } 350 reporter, err := NewReporter(cfg) 351 if err != nil { 352 t.Fatal(err) 353 } 354 // There is little point in re-parsing all test files in race mode. 355 // Just make sure there are no obvious races by running few reports from "all" dir. 356 if !testutil.RaceEnabled { 357 for _, file := range readDir(t, filepath.Join("testdata", os, dir)) { 358 t.Run(fmt.Sprintf("%v/%v", os, filepath.Base(file)), func(t *testing.T) { 359 fn(t, reporter, file) 360 }) 361 } 362 } 363 for _, file := range readDir(t, filepath.Join("testdata", "all", dir)) { 364 t.Run(fmt.Sprintf("%v/all/%v", os, filepath.Base(file)), func(t *testing.T) { 365 fn(t, reporter, file) 366 }) 367 } 368 } 369 } 370 371 func readDir(t *testing.T, dir string) (files []string) { 372 if !osutil.IsExist(dir) { 373 return nil 374 } 375 entries, err := os.ReadDir(dir) 376 if err != nil { 377 t.Fatal(err) 378 } 379 testFilenameRe := regexp.MustCompile("^[0-9]+$") 380 for _, ent := range entries { 381 if !testFilenameRe.MatchString(ent.Name()) { 382 continue 383 } 384 files = append(files, filepath.Join(dir, ent.Name())) 385 } 386 return 387 } 388 389 func TestReplace(t *testing.T) { 390 tests := []struct { 391 where string 392 start int 393 end int 394 what string 395 result string 396 }{ 397 {"0123456789", 3, 5, "abcdef", "012abcdef56789"}, 398 {"0123456789", 3, 5, "ab", "012ab56789"}, 399 {"0123456789", 3, 3, "abcd", "012abcd3456789"}, 400 {"0123456789", 0, 2, "abcd", "abcd23456789"}, 401 {"0123456789", 0, 0, "ab", "ab0123456789"}, 402 {"0123456789", 10, 10, "ab", "0123456789ab"}, 403 {"0123456789", 8, 10, "ab", "01234567ab"}, 404 {"0123456789", 5, 5, "", "0123456789"}, 405 {"0123456789", 3, 8, "", "01289"}, 406 {"0123456789", 3, 8, "ab", "012ab89"}, 407 {"0123456789", 0, 5, "a", "a56789"}, 408 {"0123456789", 5, 10, "ab", "01234ab"}, 409 } 410 for _, test := range tests { 411 t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) { 412 result := replace([]byte(test.where), test.start, test.end, []byte(test.what)) 413 if test.result != string(result) { 414 t.Errorf("want '%v', got '%v'", test.result, string(result)) 415 } 416 }) 417 } 418 } 419 420 func TestFuzz(t *testing.T) { 421 for _, data := range []string{ 422 "kernel panicType 'help' for a list of commands", 423 "0000000000000000000\n\n\n\n\n\nBooting the kernel.", 424 "ZIRCON KERNEL PANICHalted", 425 "BUG:Disabling lock debugging due to kernel taint", 426 "[0.0] WARNING: ? 0+0x0/0", 427 "BUG: login: [0.0] ", 428 "cleaned vnode", 429 "kernel:", 430 } { 431 Fuzz([]byte(data)[:len(data):len(data)]) 432 } 433 } 434 435 func TestTruncate(t *testing.T) { 436 assert.Equal(t, []byte(`01234 437 438 <<cut 11 bytes out>>`), Truncate([]byte(`0123456789ABCDEF`), 5, 0)) 439 assert.Equal(t, []byte(`<<cut 11 bytes out>> 440 441 BCDEF`), Truncate([]byte(`0123456789ABCDEF`), 0, 5)) 442 assert.Equal(t, []byte(`0123 443 444 <<cut 9 bytes out>> 445 446 DEF`), Truncate([]byte(`0123456789ABCDEF`), 4, 3)) 447 }