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  }