Golang程序  |  287行  |  7.67 KB

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