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

     1  package gotest
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/evergreen-ci/evergreen"
    10  	"github.com/evergreen-ci/evergreen/model"
    11  	"github.com/evergreen-ci/evergreen/model/task"
    12  	"github.com/evergreen-ci/evergreen/plugin"
    13  	"github.com/mitchellh/mapstructure"
    14  	"github.com/mongodb/grip/slogger"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  // ParseFilesCommand is a struct implementing plugin.Command. It is used to parse a file or
    19  // series of files containing the output of go tests, and send the results back to the server.
    20  type ParseFilesCommand struct {
    21  	// a list of filename blobs to include
    22  	// e.g. "monitor.suite", "output/*"
    23  	Files []string `mapstructure:"files" plugin:"expand"`
    24  }
    25  
    26  // Name returns the string name for the parse files command.
    27  func (pfCmd *ParseFilesCommand) Name() string {
    28  	return ParseFilesCommandName
    29  }
    30  
    31  func (pfCmd *ParseFilesCommand) Plugin() string {
    32  	return GotestPluginName
    33  }
    34  
    35  // ParseParams reads the specified map of parameters into the ParseFilesCommand struct, and
    36  // validates that at least one file pattern is specified.
    37  func (pfCmd *ParseFilesCommand) ParseParams(params map[string]interface{}) error {
    38  	if err := mapstructure.Decode(params, pfCmd); err != nil {
    39  		return errors.Wrapf(err, "error decoding '%v' params", pfCmd.Name())
    40  	}
    41  
    42  	if len(pfCmd.Files) == 0 {
    43  		return errors.Errorf("error validating params: must specify at least one "+
    44  			"file pattern to parse: '%+v'", params)
    45  	}
    46  	return nil
    47  }
    48  
    49  // Execute parses the specified output files and sends the test results found in them
    50  // back to the server.
    51  func (pfCmd *ParseFilesCommand) Execute(pluginLogger plugin.Logger,
    52  	pluginCom plugin.PluginCommunicator, taskConfig *model.TaskConfig,
    53  	stop chan bool) error {
    54  
    55  	if err := plugin.ExpandValues(pfCmd, taskConfig.Expansions); err != nil {
    56  		err = errors.Wrap(err, "error expanding params")
    57  		pluginLogger.LogTask(slogger.ERROR, "Error parsing gotest files: %+v", err)
    58  		return err
    59  	}
    60  
    61  	// make sure the file patterns are relative to the task's working directory
    62  	for idx, file := range pfCmd.Files {
    63  		pfCmd.Files[idx] = filepath.Join(taskConfig.WorkDir, file)
    64  	}
    65  
    66  	// will be all files containing test results
    67  	outputFiles, err := pfCmd.AllOutputFiles()
    68  	if err != nil {
    69  		return errors.Wrap(err, "error obtaining names of output files")
    70  	}
    71  
    72  	// make sure we're parsing something
    73  	if len(outputFiles) == 0 {
    74  		return errors.New("no files found to be parsed")
    75  	}
    76  
    77  	// parse all of the files
    78  	logs, results, err := ParseTestOutputFiles(outputFiles, stop, pluginLogger, taskConfig)
    79  	if err != nil {
    80  		return errors.Wrap(err, "error parsing output results")
    81  	}
    82  
    83  	// ship all of the test logs off to the server
    84  	pluginLogger.LogTask(slogger.INFO, "Sending test logs to server...")
    85  	allResults := []*TestResult{}
    86  	for idx, log := range logs {
    87  		var logId string
    88  
    89  		if logId, err = pluginCom.TaskPostTestLog(&log); err != nil {
    90  			// continue on error to let the other logs be posted
    91  			pluginLogger.LogTask(slogger.ERROR, "Error posting log: %v", err)
    92  		}
    93  
    94  		// add all of the test results that correspond to that log to the
    95  		// full list of results
    96  		for _, result := range results[idx] {
    97  			result.LogId = logId
    98  			allResults = append(allResults, result)
    99  		}
   100  
   101  	}
   102  	pluginLogger.LogTask(slogger.INFO, "Finished posting logs to server")
   103  
   104  	// convert everything
   105  	resultsAsModel := ToModelTestResults(taskConfig.Task, allResults)
   106  
   107  	// ship the parsed results off to the server
   108  	pluginLogger.LogTask(slogger.INFO, "Sending parsed results to server...")
   109  	if err := pluginCom.TaskPostResults(&resultsAsModel); err != nil {
   110  		return errors.Wrap(err, "error posting parsed results to server")
   111  	}
   112  	pluginLogger.LogTask(slogger.INFO, "Successfully sent parsed results to server")
   113  
   114  	return nil
   115  
   116  }
   117  
   118  // AllOutputFiles creates a list of all test output files that will be parsed, by expanding
   119  // all of the file patterns specified to the command.
   120  func (pfCmd *ParseFilesCommand) AllOutputFiles() ([]string, error) {
   121  
   122  	outputFiles := []string{}
   123  
   124  	// walk through all specified file patterns
   125  	for _, pattern := range pfCmd.Files {
   126  		matches, err := filepath.Glob(pattern)
   127  		if err != nil {
   128  			return nil, errors.Wrap(err, "error expanding file patterns")
   129  		}
   130  		outputFiles = append(outputFiles, matches...)
   131  	}
   132  
   133  	// uniquify the list
   134  	asSet := map[string]bool{}
   135  	for _, file := range outputFiles {
   136  		asSet[file] = true
   137  	}
   138  	outputFiles = []string{}
   139  	for file := range asSet {
   140  		outputFiles = append(outputFiles, file)
   141  	}
   142  
   143  	return outputFiles, nil
   144  
   145  }
   146  
   147  // ParseTestOutputFiles parses all of the files that are passed in, and returns the
   148  // test logs and test results found within.
   149  func ParseTestOutputFiles(outputFiles []string, stop chan bool,
   150  	pluginLogger plugin.Logger, taskConfig *model.TaskConfig) ([]model.TestLog,
   151  	[][]*TestResult, error) {
   152  
   153  	var results [][]*TestResult
   154  	var logs []model.TestLog
   155  
   156  	// now, open all the files, and parse the test results
   157  	for _, outputFile := range outputFiles {
   158  		// kill the execution if API server requests
   159  		select {
   160  		case <-stop:
   161  			return nil, nil, errors.New("command was stopped")
   162  		default:
   163  			// no stop signal
   164  		}
   165  
   166  		// assume that the name of the file, stripping off the ".suite" extension if present,
   167  		// is the name of the suite being tested
   168  		_, suiteName := filepath.Split(outputFile)
   169  		suiteName = strings.TrimSuffix(suiteName, ".suite")
   170  
   171  		// open the file
   172  		fileReader, err := os.Open(outputFile)
   173  		if err != nil {
   174  			// don't bomb out on a single bad file
   175  			pluginLogger.LogTask(slogger.ERROR, "Unable to open file '%v' for parsing: %v",
   176  				outputFile, err)
   177  			continue
   178  		}
   179  		defer fileReader.Close()
   180  
   181  		// parse the output logs
   182  		parser := &VanillaParser{Suite: suiteName}
   183  		if err := parser.Parse(fileReader); err != nil {
   184  			// continue on error
   185  			pluginLogger.LogTask(slogger.ERROR, "Error parsing file '%v': %v",
   186  				outputFile, err)
   187  			continue
   188  		}
   189  
   190  		// build up the test logs
   191  		logLines := parser.Logs()
   192  		testLog := model.TestLog{
   193  			Name:          suiteName,
   194  			Task:          taskConfig.Task.Id,
   195  			TaskExecution: taskConfig.Task.Execution,
   196  			Lines:         logLines,
   197  		}
   198  		// save the results
   199  		results = append(results, parser.Results())
   200  		logs = append(logs, testLog)
   201  
   202  	}
   203  	return logs, results, nil
   204  }
   205  
   206  // ToModelTestResults converts the implementation of TestResults native
   207  // to the gotest plugin to the implementation used by MCI tasks
   208  func ToModelTestResults(_ *task.Task, results []*TestResult) task.TestResults {
   209  	var modelResults []task.TestResult
   210  	for _, res := range results {
   211  		// start and end are times that we don't know,
   212  		// represented as a 64bit floating point (epoch time fraction)
   213  		var start float64 = float64(time.Now().Unix())
   214  		var end float64 = start + res.RunTime.Seconds()
   215  		var status string
   216  		switch res.Status {
   217  		// as long as we use a regex, it should be impossible to
   218  		// get an incorrect status code
   219  		case PASS:
   220  			status = evergreen.TestSucceededStatus
   221  		case SKIP:
   222  			status = evergreen.TestSkippedStatus
   223  		case FAIL:
   224  			status = evergreen.TestFailedStatus
   225  		}
   226  		convertedResult := task.TestResult{
   227  			TestFile:  res.Name,
   228  			Status:    status,
   229  			StartTime: start,
   230  			EndTime:   end,
   231  			LineNum:   res.StartLine - 1,
   232  			LogId:     res.LogId,
   233  		}
   234  		modelResults = append(modelResults, convertedResult)
   235  	}
   236  	return task.TestResults{modelResults}
   237  }