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 }