github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/plugin/builtin/gotest/parser.go (about)

     1  package gotest
     2  
     3  import (
     4  	"bufio"
     5  	"io"
     6  	"regexp"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  )
    12  
    13  const (
    14  	PASS = "PASS"
    15  	FAIL = "FAIL"
    16  	SKIP = "SKIP"
    17  
    18  	// Match the start prefix and save the group of non-space characters following the word "RUN"
    19  	StartRegexString = `=== RUN\s+(\S+)`
    20  
    21  	// Match the end prefix, save PASS/FAIL/SKIP, save the decimal value for number of seconds
    22  	EndRegexString = `--- (PASS|SKIP|FAIL): (\S+) \(([0-9.]+[ ]*s)`
    23  
    24  	// Match the start prefix and save the group of non-space characters following the word "RUN"
    25  	GocheckStartRegexString = `START: .*.go:[0-9]+: (\S+)`
    26  
    27  	// Match the end prefix, save PASS/FAIL/SKIP, save the decimal value for number of seconds
    28  	GocheckEndRegexString = `(PASS|SKIP|FAIL): .*.go:[0-9]+: (\S+)\s*([0-9.]+[ ]*s)?`
    29  )
    30  
    31  var startRegex = regexp.MustCompile(StartRegexString)
    32  var endRegex = regexp.MustCompile(EndRegexString)
    33  var gocheckStartRegex = regexp.MustCompile(GocheckStartRegexString)
    34  var gocheckEndRegex = regexp.MustCompile(GocheckEndRegexString)
    35  
    36  // Parser is an interface for parsing go test output, producing
    37  // test logs and test results
    38  type Parser interface {
    39  	// Parse takes a reader for test output, and reads
    40  	// until the reader is exhausted. Any parsing erros
    41  	// are returned.
    42  	Parse(io.Reader) error
    43  
    44  	// Logs returns an array of strings, each entry a line in
    45  	// the test output.
    46  	Logs() []string
    47  
    48  	// Results returns an array of test results. Parse() must be called
    49  	// before this.
    50  	Results() []*TestResult
    51  }
    52  
    53  // This test result implementation maps more idiomatically to Go's test output
    54  // than the TestResult type in the model package. Results are converted to the
    55  // model type before being sent to the server.
    56  type TestResult struct {
    57  	// The name of the test
    58  	Name string
    59  	// The name of the test suite the test is a part of.
    60  	// Currently, for this plugin, this is the name of the package
    61  	// being tested, prefixed with a unique number to avoid
    62  	// collisions when packages have the same name
    63  	SuiteName string
    64  	// The result status of the test
    65  	Status string
    66  	// How long the test took to run
    67  	RunTime time.Duration
    68  	// Number representing the starting log line number of the test
    69  	// in the test's logged output
    70  	StartLine int
    71  	// Number representing the last line of the test in log output
    72  	EndLine int
    73  
    74  	// Can be set to mark the id of the server-side log that this
    75  	// results corresponds to
    76  	LogId string
    77  }
    78  
    79  // VanillaParser parses tests following go test output format.
    80  // This should cover regular go tests as well as those written with the
    81  // popular testing packages goconvey and gocheck.
    82  type VanillaParser struct {
    83  	Suite string
    84  	logs  []string
    85  	// map for storing tests during parsing
    86  	tests map[string]*TestResult
    87  	order []string
    88  }
    89  
    90  // Logs returns an array of logs captured during test execution.
    91  func (vp *VanillaParser) Logs() []string {
    92  	return vp.logs
    93  }
    94  
    95  // Results returns an array of test results parsed during test execution.
    96  func (vp *VanillaParser) Results() []*TestResult {
    97  	out := []*TestResult{}
    98  
    99  	for _, name := range vp.order {
   100  		out = append(out, vp.tests[name])
   101  	}
   102  
   103  	return out
   104  }
   105  
   106  // Parse reads in a test's output and stores the results and logs.
   107  func (vp *VanillaParser) Parse(testOutput io.Reader) error {
   108  	testScanner := bufio.NewScanner(testOutput)
   109  	vp.tests = map[string]*TestResult{}
   110  	for testScanner.Scan() {
   111  		if err := testScanner.Err(); err != nil {
   112  			return errors.Wrap(err, "error reading test output")
   113  		}
   114  		// logs are appended at the start of the loop, allowing
   115  		// len(vp.logs) to represent the current line number [1...]
   116  		logLine := testScanner.Text()
   117  		vp.logs = append(vp.logs, logLine)
   118  		if err := vp.handleLine(logLine); err != nil {
   119  			return errors.WithStack(err)
   120  		}
   121  	}
   122  	return nil
   123  }
   124  
   125  // handleLine attempts to parse and store any test updates from the given line.
   126  func (vp *VanillaParser) handleLine(line string) error {
   127  	// This is gross, and could all go away with the resolution of
   128  	// https://code.google.com/p/go/issues/detail?id=2981
   129  	switch {
   130  	case startRegex.MatchString(line):
   131  		return vp.handleStart(line, startRegex, true)
   132  	case gocheckStartRegex.MatchString(line):
   133  		return vp.handleStart(line, gocheckStartRegex, false)
   134  	case endRegex.MatchString(line):
   135  		return vp.handleEnd(line, endRegex)
   136  	case gocheckEndRegex.MatchString(line):
   137  		return vp.handleEnd(line, gocheckEndRegex)
   138  	}
   139  	return nil
   140  }
   141  
   142  // handleEnd gets the end data from an ending line and stores it.
   143  func (vp *VanillaParser) handleEnd(line string, rgx *regexp.Regexp) error {
   144  	name, status, duration, err := endInfoFromLogLine(line, rgx)
   145  	if err != nil {
   146  		return errors.Wrapf(err, "error parsing end line '%s'", line)
   147  	}
   148  	t := vp.tests[name]
   149  	if t == nil || t.Name == "" {
   150  		// if there's no existing test, just stub one out
   151  		t = vp.newTestResult(name)
   152  		vp.order = append(vp.order, name)
   153  		vp.tests[name] = t
   154  	}
   155  	t.Status = status
   156  	t.RunTime = duration
   157  	t.EndLine = len(vp.logs)
   158  
   159  	return nil
   160  }
   161  
   162  // handleStart gets the data from a start line and stores it.
   163  func (vp *VanillaParser) handleStart(line string, rgx *regexp.Regexp, defaultFail bool) error {
   164  	name, err := startInfoFromLogLine(line, rgx)
   165  	if err != nil {
   166  		return errors.Wrapf(err, "error parsing start line '%s'", line)
   167  	}
   168  	t := vp.newTestResult(name)
   169  
   170  	// tasks should start out failed unless they're marked
   171  	// passing/skipped, although gocheck can't support this
   172  	if defaultFail {
   173  		t.Status = FAIL
   174  	} else {
   175  		t.Status = PASS
   176  	}
   177  
   178  	vp.tests[name] = t
   179  	vp.order = append(vp.order, name)
   180  
   181  	return nil
   182  }
   183  
   184  // newTestResult populates a test result type with the given
   185  // test name, current suite, and current line number.
   186  func (vp *VanillaParser) newTestResult(name string) *TestResult {
   187  	return &TestResult{
   188  		Name:      name,
   189  		SuiteName: vp.Suite,
   190  		StartLine: len(vp.logs),
   191  	}
   192  }
   193  
   194  // startInfoFromLogLine gets the test name from a log line
   195  // indicating the start of a test. Returns test name
   196  // and an error if one occurs.
   197  func startInfoFromLogLine(line string, rgx *regexp.Regexp) (string, error) {
   198  	matches := rgx.FindStringSubmatch(line)
   199  	if len(matches) < 2 {
   200  		// futureproofing -- this can't happen as long as we
   201  		// check Match() before calling startInfoFromLogLine
   202  		return "", errors.Errorf(
   203  			"unable to match start line regular expression on line: %s", line)
   204  	}
   205  	return matches[1], nil
   206  }
   207  
   208  // endInfoFromLogLine gets the test name, result status, and Duration
   209  // from a log line. Returns those matched elements, as well as any error
   210  // in regex or duration parsing.
   211  func endInfoFromLogLine(line string, rgx *regexp.Regexp) (string, string, time.Duration, error) {
   212  	matches := rgx.FindStringSubmatch(line)
   213  	if len(matches) < 4 {
   214  		// this block should never be reached if we call endRegex.Match()
   215  		// before entering this function
   216  		return "", "", 0, errors.Errorf(
   217  			"unable to match end line regular expression on line: %s", line)
   218  	}
   219  	status := matches[1]
   220  	name := matches[2]
   221  	var duration time.Duration
   222  	if matches[3] != "" {
   223  		var err error
   224  		duration, err = time.ParseDuration(strings.Replace(matches[3], " ", "", -1))
   225  		if err != nil {
   226  			return "", "", 0, errors.Wrap(err, "error parsing test runtime")
   227  		}
   228  	}
   229  	return name, status, duration, nil
   230  }