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  }