github.com/darmach/terratest@v0.34.8-0.20210517103231-80931f95e3ff/modules/logger/parser/parser.go (about)

     1  // Package logger/parser contains methods to parse and restructure log output from go testing and terratest
     2  package parser
     3  
     4  import (
     5  	"bufio"
     6  	"io"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  	"sync"
    11  
    12  	junitparser "github.com/jstemmer/go-junit-report/parser"
    13  	"github.com/sirupsen/logrus"
    14  )
    15  
    16  // SpawnParsers will spawn the log parser and junit report parsers off of a single reader.
    17  func SpawnParsers(logger *logrus.Logger, reader io.Reader, outputDir string) {
    18  	forkedReader, forkedWriter := io.Pipe()
    19  	teedReader := io.TeeReader(reader, forkedWriter)
    20  	var waitForParsers sync.WaitGroup
    21  	waitForParsers.Add(2)
    22  	go func() {
    23  		// close pipe writer, because this section drains the tee reader indicating reader is done draining
    24  		defer forkedWriter.Close()
    25  		defer waitForParsers.Done()
    26  		parseAndStoreTestOutput(logger, teedReader, outputDir)
    27  	}()
    28  	go func() {
    29  		defer waitForParsers.Done()
    30  		report, err := junitparser.Parse(forkedReader, "")
    31  		if err == nil {
    32  			storeJunitReport(logger, outputDir, report)
    33  		} else {
    34  			logger.Errorf("Error parsing test output into junit report: %s", err)
    35  		}
    36  	}()
    37  	waitForParsers.Wait()
    38  }
    39  
    40  // RegEx for parsing test status lines. Pulled from jstemmer/go-junit-report
    41  var (
    42  	regexResult  = regexp.MustCompile(`--- (PASS|FAIL|SKIP): (.+) \((\d+\.\d+)(?: ?seconds|s)\)`)
    43  	regexStatus  = regexp.MustCompile(`=== (RUN|PAUSE|CONT)\s+(.+)`)
    44  	regexSummary = regexp.MustCompile(`(^FAIL$)|(^(ok|FAIL)\s+([^ ]+)\s+(?:(\d+\.\d+)s|\(cached\)|(\[\w+ failed]))(?:\s+coverage:\s+(\d+\.\d+)%\sof\sstatements(?:\sin\s.+)?)?$)`)
    45  	regexPanic   = regexp.MustCompile(`^panic:`)
    46  )
    47  
    48  // getIndent takes a line and returns the indent string
    49  // Example:
    50  //   in:  "    --- FAIL: TestSnafu"
    51  //   out: "    "
    52  func getIndent(data string) string {
    53  	re := regexp.MustCompile(`^\s+`)
    54  	indent := re.FindString(data)
    55  	return indent
    56  }
    57  
    58  // getTestNameFromResultLine takes a go testing result line and extracts out the test name
    59  // Example:
    60  //   in:  --- FAIL: TestSnafu
    61  //   out: TestSnafu
    62  func getTestNameFromResultLine(text string) string {
    63  	m := regexResult.FindStringSubmatch(text)
    64  	return m[2]
    65  }
    66  
    67  // isResultLine checks if a line of text matches a test result (begins with "--- FAIL" or "--- PASS")
    68  func isResultLine(text string) bool {
    69  	return regexResult.MatchString(text)
    70  }
    71  
    72  // getTestNameFromStatusLine takes a go testing status line and extracts out the test name
    73  // Example:
    74  //   in:  === RUN  TestSnafu
    75  //   out: TestSnafu
    76  func getTestNameFromStatusLine(text string) string {
    77  	m := regexStatus.FindStringSubmatch(text)
    78  	return m[2]
    79  }
    80  
    81  // isStatusLine checks if a line of text matches a test status
    82  func isStatusLine(text string) bool {
    83  	return regexStatus.MatchString(text)
    84  }
    85  
    86  // isSummaryLine checks if a line of text matches the test summary
    87  func isSummaryLine(text string) bool {
    88  	return regexSummary.MatchString(text)
    89  }
    90  
    91  // isPanicLine checks if a line of text matches a panic
    92  func isPanicLine(text string) bool {
    93  	return regexPanic.MatchString(text)
    94  }
    95  
    96  // parseAndStoreTestOutput will take test log entries from terratest and aggregate the output by test. Takes advantage
    97  // of the fact that terratest logs are prefixed by the test name. This will store the broken out logs into files under
    98  // the outputDir, named by test name.
    99  // Additionally will take test result lines and collect them under a summary log file named `summary.log`.
   100  // See the `fixtures` directory for some examples.
   101  func parseAndStoreTestOutput(
   102  	logger *logrus.Logger,
   103  	read io.Reader,
   104  	outputDir string,
   105  ) {
   106  	logWriter := LogWriter{
   107  		lookup:    make(map[string]*os.File),
   108  		outputDir: outputDir,
   109  	}
   110  	defer logWriter.closeFiles(logger)
   111  
   112  	// Track some state that persists across lines
   113  	testResultMarkers := TestResultMarkerStack{}
   114  	previousTestName := ""
   115  
   116  	var err error
   117  	reader := bufio.NewReader(read)
   118  	for {
   119  		var data string
   120  		data, err = reader.ReadString('\n')
   121  		if len(data) == 0 && err == io.EOF {
   122  			break
   123  		}
   124  
   125  		data = strings.TrimSuffix(data, "\n")
   126  
   127  		// separate block so that we do not overwrite the err variable that we need afterwards to check if we're done
   128  		{
   129  			indentLevel := len(getIndent(data))
   130  			isIndented := indentLevel > 0
   131  
   132  			// Garbage collection of test result markers. Primary purpose is to detect when we dedent out, which can only be
   133  			// detected when we reach a dedented line.
   134  			testResultMarkers = testResultMarkers.removeDedentedTestResultMarkers(indentLevel)
   135  
   136  			// Handle each possible category of test lines
   137  			switch {
   138  			case isSummaryLine(data):
   139  				logWriter.writeLog(logger, "summary", data)
   140  
   141  			case isStatusLine(data):
   142  				testName := getTestNameFromStatusLine(data)
   143  				previousTestName = testName
   144  				logWriter.writeLog(logger, testName, data)
   145  
   146  			case strings.HasPrefix(data, "Test"):
   147  				// Heuristic: `go test` will only execute test functions named `Test.*`, so we assume any line prefixed
   148  				// with `Test` is a test output for a named test. Also assume that test output will be space delimeted and
   149  				// test names can't contain spaces (because they are function names).
   150  				// This must be modified when `logger.DoLog` changes.
   151  				vals := strings.Split(data, " ")
   152  				testName := vals[0]
   153  				previousTestName = testName
   154  				logWriter.writeLog(logger, testName, data)
   155  
   156  			case isIndented && isResultLine(data):
   157  				// In a nested test result block, so collect the line into all the test results we have seen so far.
   158  				for _, marker := range testResultMarkers {
   159  					logWriter.writeLog(logger, marker.TestName, data)
   160  				}
   161  
   162  			case isPanicLine(data):
   163  				// When panic, we want all subsequent nonstandard test lines to roll up to the summary
   164  				previousTestName = "summary"
   165  				logWriter.writeLog(logger, "summary", data)
   166  
   167  			case isResultLine(data):
   168  				// We ignore result lines, because that is handled specially below.
   169  
   170  			case previousTestName != "":
   171  				// Base case: roll up to the previous test line, if it exists.
   172  				// Handles case where terratest log has entries with newlines in them.
   173  				logWriter.writeLog(logger, previousTestName, data)
   174  
   175  			default:
   176  				logger.Warnf("Found test line that does not match known cases: %s", data)
   177  			}
   178  
   179  			// This has to happen separately from main if block to handle the special case of nested tests (e.g table driven
   180  			// tests). For those result lines, we want it to roll up to the parent test, so we need to run the handler in
   181  			// the `isIndented` section. But for both root and indented result lines, we want to execute the following code,
   182  			// hence this special block.
   183  			if isResultLine(data) {
   184  				testName := getTestNameFromResultLine(data)
   185  				logWriter.writeLog(logger, testName, data)
   186  				logWriter.writeLog(logger, "summary", data)
   187  
   188  				marker := TestResultMarker{
   189  					TestName:    testName,
   190  					IndentLevel: indentLevel,
   191  				}
   192  				testResultMarkers = testResultMarkers.push(marker)
   193  			}
   194  		}
   195  
   196  		if err != nil {
   197  			break
   198  		}
   199  	}
   200  	if err != io.EOF {
   201  		logger.Fatalf("Error reading from Reader: %s", err)
   202  	}
   203  }