github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/model/task_history.go (about) 1 package model 2 3 import ( 4 "fmt" 5 "strings" 6 "time" 7 8 "github.com/evergreen-ci/evergreen" 9 "github.com/evergreen-ci/evergreen/apimodels" 10 "github.com/evergreen-ci/evergreen/db" 11 "github.com/evergreen-ci/evergreen/db/bsonutil" 12 "github.com/evergreen-ci/evergreen/model/task" 13 "github.com/evergreen-ci/evergreen/model/version" 14 "github.com/evergreen-ci/evergreen/util" 15 "github.com/mongodb/grip" 16 "github.com/pkg/errors" 17 "gopkg.in/mgo.v2" 18 "gopkg.in/mgo.v2/bson" 19 ) 20 21 const ( 22 TaskTimeout = "timeout" 23 TaskSystemFailure = "sysfail" 24 ) 25 26 type taskHistoryIterator struct { 27 TaskName string 28 BuildVariants []string 29 ProjectName string 30 } 31 32 type TaskHistoryChunk struct { 33 Tasks []bson.M 34 Versions []version.Version 35 FailedTests map[string][]task.TestResult 36 Exhausted ExhaustedIterator 37 } 38 39 type ExhaustedIterator struct { 40 Before, After bool 41 } 42 43 type TaskHistory struct { 44 Id string `bson:"_id" json:"_id"` 45 Order string `bson:"order" json:"order"` 46 Tasks []aggregatedTaskHistory `bson:"tasks" json:"tasks"` 47 } 48 49 type aggregatedTaskHistory struct { 50 Id string `bson:"_id" json:"_id"` 51 Status string `bson:"status" json:"status"` 52 Activated bool `bson:"activated" json:"activated"` 53 TimeTaken time.Duration `bson:"time_taken" json:"time_taken"` 54 BuildVariant string `bson:"build_variant" json:"build_variant"` 55 TestResults apimodels.TaskEndDetails `bson:"status_details" json:"status_details"` 56 } 57 type TaskDetails struct { 58 TimedOut bool `bson:"timed_out"` 59 Status string `bson:"st"` 60 } 61 62 // TestHistoryResult represents what is returned by the aggregation 63 type TestHistoryResult struct { 64 TestFile string `bson:"tf"` 65 TaskName string `bson:"tn"` 66 TaskStatus string `bson:"task_status"` 67 TestStatus string `bson:"test_status"` 68 Revision string `bson:"r"` 69 Project string `bson:"p"` 70 TaskId string `bson:"tid"` 71 BuildVariant string `bson:"bv"` 72 StartTime float64 `bson:"st"` 73 EndTime float64 `bson:"et"` 74 Execution int `bson:"ex"` 75 Url string `bson:"url"` 76 UrlRaw string `bson:"url_r"` 77 OldTaskId string `bson:"otid"` 78 TaskTimedOut bool `bson:"to"` 79 TaskDetailsType string `bson:"tdt"` 80 LogId string `bson:"lid"` 81 } 82 83 // TestHistoryResult bson tags 84 var ( 85 TestFileKey = bsonutil.MustHaveTag(TestHistoryResult{}, "TestFile") 86 TaskNameKey = bsonutil.MustHaveTag(TestHistoryResult{}, "TaskName") 87 TaskStatusKey = bsonutil.MustHaveTag(TestHistoryResult{}, "TaskStatus") 88 TestStatusKey = bsonutil.MustHaveTag(TestHistoryResult{}, "TestStatus") 89 RevisionKey = bsonutil.MustHaveTag(TestHistoryResult{}, "Revision") 90 ProjectKey = bsonutil.MustHaveTag(TestHistoryResult{}, "Project") 91 TaskIdKey = bsonutil.MustHaveTag(TestHistoryResult{}, "TaskId") 92 BuildVariantKey = bsonutil.MustHaveTag(TestHistoryResult{}, "BuildVariant") 93 EndTimeKey = bsonutil.MustHaveTag(TestHistoryResult{}, "EndTime") 94 StartTimeKey = bsonutil.MustHaveTag(TestHistoryResult{}, "StartTime") 95 ExecutionKey = bsonutil.MustHaveTag(TestHistoryResult{}, "Execution") 96 OldTaskIdKey = bsonutil.MustHaveTag(TestHistoryResult{}, "OldTaskId") 97 UrlKey = bsonutil.MustHaveTag(TestHistoryResult{}, "Url") 98 UrlRawKey = bsonutil.MustHaveTag(TestHistoryResult{}, "UrlRaw") 99 TaskTimedOutKey = bsonutil.MustHaveTag(TestHistoryResult{}, "TaskTimedOut") 100 TaskDetailsTypeKey = bsonutil.MustHaveTag(TestHistoryResult{}, "TaskDetailsType") 101 LogIdKey = bsonutil.MustHaveTag(TestHistoryResult{}, "LogId") 102 ) 103 104 // TestHistoryParameters are the parameters that are used 105 // to retrieve Test Results. 106 type TestHistoryParameters struct { 107 Project string `json:"project"` 108 TestNames []string `json:"test_names"` 109 TaskNames []string `json:"task_names"` 110 BuildVariants []string `json:"variants"` 111 TaskStatuses []string `json:"task_statuses"` 112 TestStatuses []string `json:"test_statuses"` 113 BeforeRevision string `json:"before_revision"` 114 AfterRevision string `json:"after_revision"` 115 BeforeDate time.Time `json:"before_date"` 116 AfterDate time.Time `json:"after_date"` 117 Sort int `json:"sort"` 118 Limit int `json:"limit"` 119 } 120 121 type TaskHistoryIterator interface { 122 GetChunk(version *version.Version, numBefore, numAfter int, include bool) (TaskHistoryChunk, error) 123 GetDistinctTestNames(numCommits int) ([]string, error) 124 } 125 126 func NewTaskHistoryIterator(name string, buildVariants []string, projectName string) TaskHistoryIterator { 127 return TaskHistoryIterator(&taskHistoryIterator{TaskName: name, BuildVariants: buildVariants, ProjectName: projectName}) 128 } 129 130 func (iter *taskHistoryIterator) findAllVersions(v *version.Version, numRevisions int, before, include bool) ([]version.Version, bool, error) { 131 versionQuery := bson.M{ 132 version.RequesterKey: evergreen.RepotrackerVersionRequester, 133 version.IdentifierKey: iter.ProjectName, 134 } 135 136 // If including the specified version in the result, then should 137 // get an additional revision 138 if include { 139 numRevisions++ 140 } 141 142 // Determine the comparator to use based on whether the revisions 143 // come before/after the specified version 144 compare, order := "$gt", version.RevisionOrderNumberKey 145 if before { 146 compare, order = "$lt", fmt.Sprintf("-%v", version.RevisionOrderNumberKey) 147 if include { 148 compare = "$lte" 149 } 150 } else if include { 151 compare = "$gte" 152 } 153 154 if v != nil { 155 versionQuery[version.RevisionOrderNumberKey] = bson.M{compare: v.RevisionOrderNumber} 156 } 157 158 // Get the next numRevisions, plus an additional one to check if have 159 // reached the beginning/end of history 160 versions, err := version.Find( 161 db.Query(versionQuery).WithFields( 162 version.IdKey, 163 version.RevisionOrderNumberKey, 164 version.RevisionKey, 165 version.MessageKey, 166 version.CreateTimeKey, 167 ).Sort([]string{order}).Limit(numRevisions + 1)) 168 169 // Check if there were fewer results returned by the query than what 170 // the limit was set as 171 exhausted := len(versions) <= numRevisions 172 if !exhausted { 173 // Exclude the last version because we actually only wanted 174 // `numRevisions` number of commits 175 versions = versions[:len(versions)-1] 176 } 177 178 // The iterator can only be exhausted if an actual version was specified 179 exhausted = exhausted || (v == nil && numRevisions == 0) 180 181 if !before { 182 // Reverse the order so that the most recent version is first 183 for i, j := 0, len(versions)-1; i < j; i, j = i+1, j-1 { 184 versions[i], versions[j] = versions[j], versions[i] 185 } 186 } 187 return versions, exhausted, err 188 } 189 190 // Returns tasks grouped by their versions, and sorted with the most 191 // recent first (i.e. descending commit order number). 192 func (iter *taskHistoryIterator) GetChunk(v *version.Version, numBefore, numAfter int, include bool) (TaskHistoryChunk, error) { 193 chunk := TaskHistoryChunk{ 194 Tasks: []bson.M{}, 195 Versions: []version.Version{}, 196 FailedTests: map[string][]task.TestResult{}, 197 } 198 199 session, database, err := db.GetGlobalSessionFactory().GetSession() 200 if err != nil { 201 return chunk, errors.Wrap(err, "problem getting database session") 202 } 203 204 defer session.Close() 205 206 versionsBefore, exhausted, err := iter.findAllVersions(v, numBefore, true, include) 207 if err != nil { 208 return chunk, errors.WithStack(err) 209 } 210 chunk.Exhausted.Before = exhausted 211 212 versionsAfter, exhausted, err := iter.findAllVersions(v, numAfter, false, false) 213 if err != nil { 214 return chunk, errors.WithStack(err) 215 } 216 chunk.Exhausted.After = exhausted 217 218 versions := append(versionsAfter, versionsBefore...) 219 if len(versions) == 0 { 220 return chunk, nil 221 } 222 chunk.Versions = versions 223 224 // versionStartBoundary is the most recent version (i.e. newest) that 225 // should be included in the results. 226 // 227 // versionEndBoundary is the least recent version (i.e. oldest) that 228 // should be included in the results. 229 versionStartBoundary, versionEndBoundary := versions[0], versions[len(versions)-1] 230 231 pipeline := database.C(task.Collection).Pipe( 232 []bson.M{ 233 {"$match": bson.M{ 234 task.RequesterKey: evergreen.RepotrackerVersionRequester, 235 task.ProjectKey: iter.ProjectName, 236 task.DisplayNameKey: iter.TaskName, 237 task.BuildVariantKey: bson.M{"$in": iter.BuildVariants}, 238 task.RevisionOrderNumberKey: bson.M{ 239 "$gte": versionEndBoundary.RevisionOrderNumber, 240 "$lte": versionStartBoundary.RevisionOrderNumber, 241 }, 242 }}, 243 {"$project": bson.M{ 244 task.IdKey: 1, 245 task.StatusKey: 1, 246 task.DetailsKey: 1, 247 task.ActivatedKey: 1, 248 task.TimeTakenKey: 1, 249 task.BuildVariantKey: 1, 250 task.RevisionKey: 1, 251 task.RevisionOrderNumberKey: 1, 252 }}, 253 {"$group": bson.M{ 254 "_id": fmt.Sprintf("$%v", task.RevisionKey), 255 "order": bson.M{"$first": fmt.Sprintf("$%v", task.RevisionOrderNumberKey)}, 256 "tasks": bson.M{ 257 "$push": bson.M{ 258 task.IdKey: fmt.Sprintf("$%v", task.IdKey), 259 task.StatusKey: fmt.Sprintf("$%v", task.StatusKey), 260 task.DetailsKey: fmt.Sprintf("$%v", task.DetailsKey), 261 task.ActivatedKey: fmt.Sprintf("$%v", task.ActivatedKey), 262 task.TimeTakenKey: fmt.Sprintf("$%v", task.TimeTakenKey), 263 task.BuildVariantKey: fmt.Sprintf("$%v", task.BuildVariantKey), 264 }, 265 }, 266 }}, 267 {"$sort": bson.M{task.RevisionOrderNumberKey: -1}}, 268 }, 269 ) 270 271 var aggregatedTasks []bson.M 272 if err = pipeline.All(&aggregatedTasks); err != nil { 273 return chunk, errors.WithStack(err) 274 } 275 chunk.Tasks = aggregatedTasks 276 277 failedTests, err := iter.GetFailedTests(pipeline) 278 if err != nil { 279 return chunk, errors.WithStack(err) 280 } 281 282 chunk.FailedTests = failedTests 283 return chunk, nil 284 } 285 286 func (self *taskHistoryIterator) GetDistinctTestNames(numCommits int) ([]string, error) { 287 session, db, err := db.GetGlobalSessionFactory().GetSession() 288 if err != nil { 289 return nil, errors.Wrap(err, "problem getting database session") 290 } 291 defer session.Close() 292 293 pipeline := db.C(task.Collection).Pipe( 294 []bson.M{ 295 { 296 "$match": bson.M{ 297 task.BuildVariantKey: bson.M{"$in": self.BuildVariants}, 298 task.DisplayNameKey: self.TaskName, 299 }, 300 }, 301 {"$sort": bson.D{{task.RevisionOrderNumberKey, -1}}}, 302 {"$limit": numCommits}, 303 {"$unwind": fmt.Sprintf("$%v", task.TestResultsKey)}, 304 {"$group": bson.M{"_id": fmt.Sprintf("$%v.%v", task.TestResultsKey, task.TestResultTestFileKey)}}, 305 }, 306 ) 307 308 var output []bson.M 309 310 if err = pipeline.All(&output); err != nil { 311 return nil, errors.WithStack(err) 312 } 313 314 names := make([]string, 0) 315 for _, doc := range output { 316 names = append(names, doc["_id"].(string)) 317 } 318 319 return names, nil 320 } 321 322 // GetFailedTests returns a mapping of task id to a slice of failed tasks 323 // extracted from a pipeline of aggregated tasks 324 func (self *taskHistoryIterator) GetFailedTests(aggregatedTasks *mgo.Pipe) (map[string][]task.TestResult, error) { 325 // get the ids of the failed task 326 var failedTaskIds []string 327 var taskHistory TaskHistory 328 iter := aggregatedTasks.Iter() 329 for { 330 if iter.Next(&taskHistory) { 331 for _, task := range taskHistory.Tasks { 332 if task.Status == evergreen.TaskFailed { 333 failedTaskIds = append(failedTaskIds, task.Id) 334 } 335 } 336 } else { 337 break 338 } 339 } 340 341 if err := iter.Err(); err != nil { 342 return nil, err 343 } 344 345 // find all the relevant failed tests 346 failedTestsMap := make(map[string][]task.TestResult) 347 tasks, err := task.Find(task.ByIds(failedTaskIds).WithFields(task.IdKey, task.TestResultsKey)) 348 if err != nil { 349 return nil, err 350 } 351 352 // create the mapping of the task id to the list of failed tasks 353 for _, task := range tasks { 354 for _, test := range task.TestResults { 355 if test.Status == evergreen.TestFailedStatus { 356 failedTestsMap[task.Id] = append(failedTestsMap[task.Id], test) 357 } 358 } 359 } 360 return failedTestsMap, nil 361 } 362 363 // validate returns a list of validation error messages if there are any validation errors 364 // and an empty list if there are none. 365 // It checks that there is not both a date and revision time range, 366 // checks that sort is either -1 or 1, 367 // checks that the test statuses and task statuses are valid test or task statuses, 368 // checks that there is a project id and either a list of test names or task names. 369 func (t *TestHistoryParameters) validate() []string { 370 validationErrors := []string{} 371 if t.Project == "" { 372 validationErrors = append(validationErrors, "no project id specified") 373 } 374 375 if len(t.TestNames) == 0 && len(t.TaskNames) == 0 { 376 validationErrors = append(validationErrors, "must include test names or task names") 377 } 378 // A test can either have failed, silently failed, got skipped, or passed. 379 validTestStatuses := []string{ 380 evergreen.TestFailedStatus, 381 evergreen.TestSilentlyFailedStatus, 382 evergreen.TestSkippedStatus, 383 evergreen.TestSucceededStatus, 384 } 385 for _, status := range t.TestStatuses { 386 if !util.SliceContains(validTestStatuses, status) { 387 validationErrors = append(validationErrors, fmt.Sprintf("invalid test status in parameters: %v", status)) 388 } 389 } 390 391 // task statuses can be fail, pass, or timeout. 392 validTaskStatuses := []string{evergreen.TaskFailed, evergreen.TaskSucceeded, TaskTimeout, TaskSystemFailure} 393 for _, status := range t.TaskStatuses { 394 if !util.SliceContains(validTaskStatuses, status) { 395 validationErrors = append(validationErrors, fmt.Sprintf("invalid task status in parameters: %v", status)) 396 } 397 } 398 399 if (!util.IsZeroTime(t.AfterDate) || !util.IsZeroTime(t.BeforeDate)) && 400 (t.AfterRevision != "" || t.BeforeRevision != "") { 401 validationErrors = append(validationErrors, "cannot have both date and revision time range parameter") 402 } 403 404 if t.Sort != -1 && t.Sort != 1 { 405 validationErrors = append(validationErrors, "sort parameter can only be -1 or 1") 406 } 407 return validationErrors 408 } 409 410 // setDefaultsAndValidate sets the default for test history parameters that do not have values 411 // and validates the test parameters. 412 func (thp *TestHistoryParameters) SetDefaultsAndValidate() error { 413 if len(thp.TestStatuses) == 0 { 414 thp.TestStatuses = []string{evergreen.TestFailedStatus} 415 } 416 if len(thp.TaskStatuses) == 0 { 417 thp.TaskStatuses = []string{evergreen.TaskFailed} 418 } 419 if thp.Sort == 0 { 420 thp.Sort = -1 421 } 422 validationErrors := thp.validate() 423 if len(validationErrors) > 0 { 424 return errors.Errorf("validation error on test history parameters: %s", 425 strings.Join(validationErrors, ", ")) 426 } 427 return nil 428 } 429 430 // mergeResults merges the test results from the old tests and current tests so that all test results with the same 431 // test file name and task id are adjacent to each other. 432 // Since the tests results returned in the aggregation are sorted in the same way for both the tasks and old_tasks collection, 433 // the sorted format should be the same - this is assuming that currentTestHistory and oldTestHistory are both sorted. 434 func mergeResults(currentTestHistory []TestHistoryResult, oldTestHistory []TestHistoryResult) []TestHistoryResult { 435 if len(oldTestHistory) == 0 { 436 return currentTestHistory 437 } 438 if len(currentTestHistory) == 0 { 439 return oldTestHistory 440 } 441 442 allResults := []TestHistoryResult{} 443 oldIndex := 0 444 445 for _, testResult := range currentTestHistory { 446 // first add the element of the latest execution 447 allResults = append(allResults, testResult) 448 449 // check that there are more test results in oldTestHistory; 450 // check if the old task id, is the same as the original task id of the current test result 451 // and that the test file is the same. 452 for oldIndex < len(oldTestHistory) && 453 oldTestHistory[oldIndex].OldTaskId == testResult.TaskId && 454 oldTestHistory[oldIndex].TestFile == testResult.TestFile { 455 allResults = append(allResults, oldTestHistory[oldIndex]) 456 457 oldIndex += 1 458 } 459 } 460 return allResults 461 } 462 463 // buildTestHistoryQuery returns the aggregation pipeline that is executed given the test history parameters. 464 func buildTestHistoryQuery(testHistoryParameters *TestHistoryParameters) ([]bson.M, error) { 465 // construct the task match query 466 taskMatchQuery := bson.M{ 467 task.ProjectKey: testHistoryParameters.Project, 468 } 469 470 // construct the test match query 471 testMatchQuery := bson.M{ 472 task.TestResultsKey + "." + task.TestResultStatusKey: bson.M{"$in": testHistoryParameters.TestStatuses}, 473 } 474 475 // separate out pass/fail from timeouts and system failures 476 isTimeout := false 477 isSysFail := false 478 taskStatuses := []string{} 479 for _, status := range testHistoryParameters.TaskStatuses { 480 switch status { 481 case TaskTimeout: 482 isTimeout = true 483 case TaskSystemFailure: 484 isSysFail = true 485 default: 486 taskStatuses = append(taskStatuses, status) 487 } 488 } 489 statusQuery := []bson.M{} 490 491 // if there are any pass/fail tasks create a query that isn't a timeout or a system failure. 492 if len(taskStatuses) > 0 { 493 statusQuery = append(statusQuery, 494 bson.M{ 495 task.StatusKey: bson.M{"$in": taskStatuses}, 496 task.DetailsKey + "." + task.TaskEndDetailTimedOut: bson.M{ 497 "$ne": true, 498 }, 499 task.DetailsKey + "." + task.TaskEndDetailType: bson.M{ 500 "$ne": "system", 501 }, 502 }) 503 } 504 505 if isTimeout { 506 statusQuery = append(statusQuery, bson.M{ 507 task.StatusKey: evergreen.TaskFailed, 508 task.DetailsKey + "." + task.TaskEndDetailTimedOut: true, 509 }) 510 } 511 if isSysFail { 512 statusQuery = append(statusQuery, bson.M{ 513 task.StatusKey: evergreen.TaskFailed, 514 task.DetailsKey + "." + task.TaskEndDetailType: "system", 515 }) 516 } 517 518 taskMatchQuery["$or"] = statusQuery 519 520 // check task, test, and build variants and add them to the task query if necessary 521 if len(testHistoryParameters.TaskNames) > 0 { 522 taskMatchQuery[task.DisplayNameKey] = bson.M{"$in": testHistoryParameters.TaskNames} 523 } 524 if len(testHistoryParameters.BuildVariants) > 0 { 525 taskMatchQuery[task.BuildVariantKey] = bson.M{"$in": testHistoryParameters.BuildVariants} 526 } 527 if len(testHistoryParameters.TestNames) > 0 { 528 taskMatchQuery[task.TestResultsKey+"."+task.TestResultTestFileKey] = bson.M{"$in": testHistoryParameters.TestNames} 529 testMatchQuery[task.TestResultsKey+"."+task.TestResultTestFileKey] = bson.M{"$in": testHistoryParameters.TestNames} 530 } 531 532 // add in date to task query if necessary 533 if !util.IsZeroTime(testHistoryParameters.BeforeDate) || !util.IsZeroTime(testHistoryParameters.AfterDate) { 534 startTimeClause := bson.M{} 535 if !util.IsZeroTime(testHistoryParameters.BeforeDate) { 536 startTimeClause["$lte"] = testHistoryParameters.BeforeDate 537 } 538 if !util.IsZeroTime(testHistoryParameters.AfterDate) { 539 startTimeClause["$gte"] = testHistoryParameters.AfterDate 540 } 541 taskMatchQuery[task.StartTimeKey] = startTimeClause 542 } 543 544 var pipeline []bson.M 545 546 // we begin to build the pipeline here. This if/else clause 547 // builds the initial match and limit. This returns early if 548 // you do not specify a revision range or a limit; and issues 549 // a warning if you specify only *one* bound without a limit. 550 // 551 // This operation will return an error if the before or after 552 // revision are empty. 553 if testHistoryParameters.BeforeRevision == "" && testHistoryParameters.AfterRevision == "" { 554 if testHistoryParameters.Limit == 0 { 555 return nil, errors.New("must specify a range of revisions *or* a limit") 556 } 557 558 pipeline = append(pipeline, 559 bson.M{"$match": taskMatchQuery}, 560 bson.M{"$limit": testHistoryParameters.Limit}) 561 } else { 562 // add in revision to task query if necessary 563 564 revisionOrderNumberClause := bson.M{} 565 if testHistoryParameters.BeforeRevision != "" { 566 v, err := version.FindOne(version.ByProjectIdAndRevision(testHistoryParameters.Project, 567 testHistoryParameters.BeforeRevision).WithFields(version.RevisionOrderNumberKey)) 568 if err != nil { 569 return nil, err 570 } 571 if v == nil { 572 return nil, errors.Errorf("invalid revision : %v", testHistoryParameters.BeforeRevision) 573 } 574 revisionOrderNumberClause["$lte"] = v.RevisionOrderNumber 575 } 576 577 if testHistoryParameters.AfterRevision != "" { 578 v, err := version.FindOne(version.ByProjectIdAndRevision(testHistoryParameters.Project, 579 testHistoryParameters.AfterRevision).WithFields(version.RevisionOrderNumberKey)) 580 if err != nil { 581 return nil, err 582 } 583 if v == nil { 584 return nil, errors.Errorf("invalid revision : %v", testHistoryParameters.AfterRevision) 585 } 586 revisionOrderNumberClause["$gt"] = v.RevisionOrderNumber 587 } 588 taskMatchQuery[task.RevisionOrderNumberKey] = revisionOrderNumberClause 589 590 pipeline = append(pipeline, bson.M{"$match": taskMatchQuery}) 591 592 if testHistoryParameters.Limit > 0 { 593 pipeline = append(pipeline, bson.M{"$limit": testHistoryParameters.Limit}) 594 } else if len(revisionOrderNumberClause) != 2 { 595 grip.Notice("task history query contains a potentially unbounded range of revisions") 596 } 597 } 598 599 pipeline = append(pipeline, 600 bson.M{"$project": bson.M{ 601 task.DisplayNameKey: 1, 602 task.BuildVariantKey: 1, 603 task.StatusKey: 1, 604 task.TestResultsKey: 1, 605 task.RevisionKey: 1, 606 task.IdKey: 1, 607 task.ExecutionKey: 1, 608 task.RevisionOrderNumberKey: 1, 609 task.OldTaskIdKey: 1, 610 task.StartTimeKey: 1, 611 task.ProjectKey: 1, 612 task.DetailsKey: 1, 613 }}, 614 bson.M{"$unwind": "$" + task.TestResultsKey}, 615 bson.M{"$match": testMatchQuery}, 616 bson.M{"$sort": bson.D{ 617 {task.RevisionOrderNumberKey, testHistoryParameters.Sort}, 618 {task.TestResultsKey + "." + task.TestResultTestFileKey, testHistoryParameters.Sort}, 619 }}, 620 bson.M{"$project": bson.M{ 621 TestFileKey: "$" + task.TestResultsKey + "." + task.TestResultTestFileKey, 622 TaskIdKey: "$" + task.IdKey, 623 TestStatusKey: "$" + task.TestResultsKey + "." + task.TestResultStatusKey, 624 TaskStatusKey: "$" + task.StatusKey, 625 RevisionKey: "$" + task.RevisionKey, 626 ProjectKey: "$" + task.ProjectKey, 627 TaskNameKey: "$" + task.DisplayNameKey, 628 BuildVariantKey: "$" + task.BuildVariantKey, 629 StartTimeKey: "$" + task.TestResultsKey + "." + task.TestResultStartTimeKey, 630 EndTimeKey: "$" + task.TestResultsKey + "." + task.TestResultEndTimeKey, 631 ExecutionKey: "$" + task.ExecutionKey + "." + task.ExecutionKey, 632 OldTaskIdKey: "$" + task.OldTaskIdKey, 633 UrlKey: "$" + task.TestResultsKey + "." + task.TestResultURLKey, 634 UrlRawKey: "$" + task.TestResultsKey + "." + task.TestResultURLRawKey, 635 LogIdKey: "$" + task.TestResultsKey + "." + task.TestResultLogIdKey, 636 TaskTimedOutKey: "$" + task.DetailsKey + "." + task.TaskEndDetailTimedOut, 637 TaskDetailsTypeKey: "$" + task.DetailsKey + "." + task.TaskEndDetailType, 638 }}) 639 640 return pipeline, nil 641 } 642 643 // GetTestHistory takes in test history parameters, validates them, and returns the test results according to those parameters. 644 // It sets tasks failed and tests failed as default statuses if none are provided, and defaults to all tasks, tests, 645 // and variants if those are not set. 646 func GetTestHistory(testHistoryParameters *TestHistoryParameters) ([]TestHistoryResult, error) { 647 pipeline, err := buildTestHistoryQuery(testHistoryParameters) 648 if err != nil { 649 return nil, err 650 } 651 aggTestResults := []TestHistoryResult{} 652 err = db.Aggregate(task.Collection, pipeline, &aggTestResults) 653 if err != nil { 654 return nil, err 655 } 656 aggOldTestResults := []TestHistoryResult{} 657 err = db.Aggregate(task.OldCollection, pipeline, &aggOldTestResults) 658 if err != nil { 659 return nil, err 660 } 661 return mergeResults(aggTestResults, aggOldTestResults), nil 662 }