github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/service/task_history.go (about)

     1  package service
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"sort"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/evergreen-ci/evergreen"
    12  	"github.com/evergreen-ci/evergreen/apimodels"
    13  	"github.com/evergreen-ci/evergreen/db"
    14  	"github.com/evergreen-ci/evergreen/model"
    15  	"github.com/evergreen-ci/evergreen/model/task"
    16  	"github.com/evergreen-ci/evergreen/model/user"
    17  	"github.com/evergreen-ci/evergreen/model/version"
    18  	"github.com/gorilla/mux"
    19  	"github.com/mongodb/grip"
    20  	"github.com/pkg/errors"
    21  	"gopkg.in/mgo.v2/bson"
    22  )
    23  
    24  const (
    25  	beforeWindow = "before"
    26  	afterWindow  = "after"
    27  
    28  	// Initial number of revisions to return on first page load
    29  	InitRevisionsBefore = 50
    30  	InitRevisionsAfter  = 100
    31  
    32  	// Number of revisions to return on subsequent requests
    33  	NoRevisions     = 0
    34  	MaxNumRevisions = 50
    35  
    36  	// this regex either matches against the exact 'test' string, or
    37  	// against the 'test' string at the end of some kind of filepath.
    38  	testMatchRegex = `(\Q%s\E|.*(\\|/)\Q%s\E)$`
    39  )
    40  
    41  // Representation of a group of tasks with the same display name and revision,
    42  // but different build variants.
    43  type taskDrawerItem struct {
    44  	Revision string    `json:"revision"`
    45  	Message  string    `json:"message"`
    46  	PushTime time.Time `json:"push_time"`
    47  	// small amount of info about each task in this group
    48  	TaskBlurb taskBlurb `json:"task"`
    49  }
    50  
    51  type versionDrawerItem struct {
    52  	Revision string    `json:"revision"`
    53  	Message  string    `json:"message"`
    54  	PushTime time.Time `json:"push_time"`
    55  	Id       string    `json:"version_id"`
    56  	Errors   []string  `json:"errors"`
    57  	Warnings []string  `json:"warnings"`
    58  	Ignored  bool      `json:"ignored"`
    59  }
    60  
    61  // Represents a small amount of information about a task - used as part of the
    62  // task history to display a visual blurb.
    63  type taskBlurb struct {
    64  	Id       string                  `json:"id"`
    65  	Variant  string                  `json:"variant"`
    66  	Status   string                  `json:"status"`
    67  	Details  apimodels.TaskEndDetail `json:"task_end_details"`
    68  	Failures []string                `json:"failures"`
    69  }
    70  
    71  // Serves the task history page itself.
    72  func (uis *UIServer) taskHistoryPage(w http.ResponseWriter, r *http.Request) {
    73  	projCtx := MustHaveProjectContext(r)
    74  
    75  	if projCtx.Project == nil {
    76  		http.Error(w, "not found", http.StatusNotFound)
    77  		return
    78  	}
    79  	taskName := mux.Vars(r)["task_name"]
    80  
    81  	var chunk model.TaskHistoryChunk
    82  	var v *version.Version
    83  	var before bool
    84  	var err error
    85  
    86  	if strBefore := r.FormValue("before"); strBefore != "" {
    87  		if before, err = strconv.ParseBool(strBefore); err != nil {
    88  			http.Error(w, err.Error(), http.StatusInternalServerError)
    89  			return
    90  		}
    91  	}
    92  	buildVariants := projCtx.Project.GetVariantsWithTask(taskName)
    93  
    94  	if revision := r.FormValue("revision"); revision != "" {
    95  		v, err = version.FindOne(version.ByProjectIdAndRevision(projCtx.Project.Identifier, revision))
    96  		if err != nil {
    97  			http.Error(w, err.Error(), http.StatusInternalServerError)
    98  			return
    99  		}
   100  	}
   101  
   102  	taskHistoryIterator := model.NewTaskHistoryIterator(taskName, buildVariants, projCtx.Project.Identifier)
   103  
   104  	if r.FormValue("format") == "" {
   105  		if v != nil {
   106  			chunk, err = taskHistoryIterator.GetChunk(v, InitRevisionsBefore, InitRevisionsAfter, true)
   107  		} else {
   108  			// Load the most recent MaxNumRevisions if a particular
   109  			// version was unspecified
   110  			chunk, err = taskHistoryIterator.GetChunk(v, MaxNumRevisions, NoRevisions, false)
   111  		}
   112  	} else if before {
   113  		chunk, err = taskHistoryIterator.GetChunk(v, MaxNumRevisions, NoRevisions, false)
   114  	} else {
   115  		chunk, err = taskHistoryIterator.GetChunk(v, NoRevisions, MaxNumRevisions, false)
   116  	}
   117  	if err != nil {
   118  		http.Error(w, err.Error(), http.StatusInternalServerError)
   119  		return
   120  	}
   121  
   122  	data := taskHistoryPageData{
   123  		TaskName:         taskName,
   124  		Tasks:            chunk.Tasks,
   125  		Variants:         buildVariants,
   126  		FailedTests:      chunk.FailedTests,
   127  		Versions:         chunk.Versions,
   128  		ExhaustedBefore:  chunk.Exhausted.Before,
   129  		ExhaustedAfter:   chunk.Exhausted.After,
   130  		SelectedRevision: r.FormValue("revision"),
   131  	}
   132  
   133  	switch r.FormValue("format") {
   134  	case "json":
   135  		uis.WriteJSON(w, http.StatusOK, data)
   136  		return
   137  	default:
   138  		uis.WriteHTML(w, http.StatusOK, struct {
   139  			ProjectData projectContext
   140  			User        *user.DBUser
   141  			Flashes     []interface{}
   142  			Data        taskHistoryPageData
   143  		}{projCtx, GetUser(r), []interface{}{}, data}, "base",
   144  			"task_history.html", "base_angular.html", "menu.html")
   145  	}
   146  }
   147  
   148  func (uis *UIServer) variantHistory(w http.ResponseWriter, r *http.Request) {
   149  	projCtx := MustHaveProjectContext(r)
   150  	variant := mux.Vars(r)["variant"]
   151  	beforeCommitId := r.FormValue("before")
   152  	isJson := (r.FormValue("format") == "json")
   153  
   154  	var beforeCommit *version.Version
   155  	var err error
   156  	beforeCommit = nil
   157  	if beforeCommitId != "" {
   158  		beforeCommit, err = version.FindOne(version.ById(beforeCommitId))
   159  		if err != nil {
   160  			uis.LoggedError(w, r, http.StatusInternalServerError, err)
   161  			return
   162  		}
   163  		grip.WarningWhen(beforeCommit == nil, "'before' was specified but query returned nil")
   164  	}
   165  
   166  	project, err := model.FindProject("", projCtx.ProjectRef)
   167  	if err != nil {
   168  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   169  		return
   170  	}
   171  
   172  	bv := project.FindBuildVariant(variant)
   173  	if bv == nil {
   174  		http.Error(w, "variant not found", http.StatusNotFound)
   175  		return
   176  	}
   177  
   178  	iter := model.NewBuildVariantHistoryIterator(variant, bv.Name, projCtx.Project.Identifier)
   179  	tasks, versions, err := iter.GetItems(beforeCommit, 50)
   180  	if err != nil {
   181  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   182  		return
   183  	}
   184  
   185  	var suites []string
   186  	for _, task := range bv.Tasks {
   187  		suites = append(suites, task.Name)
   188  	}
   189  
   190  	sort.Strings(suites)
   191  
   192  	data := struct {
   193  		Variant   string
   194  		Tasks     []bson.M
   195  		TaskNames []string
   196  		Versions  []version.Version
   197  		Project   string
   198  	}{variant, tasks, suites, versions, projCtx.Project.Identifier}
   199  	if isJson {
   200  		uis.WriteJSON(w, http.StatusOK, data)
   201  		return
   202  	}
   203  	uis.WriteHTML(w, http.StatusOK, struct {
   204  		ProjectData projectContext
   205  		User        *user.DBUser
   206  		Flashes     []interface{}
   207  		Data        interface{}
   208  	}{projCtx, GetUser(r), []interface{}{}, data}, "base",
   209  		"build_variant_history.html", "base_angular.html", "menu.html")
   210  }
   211  
   212  func (uis *UIServer) taskHistoryPickaxe(w http.ResponseWriter, r *http.Request) {
   213  	projCtx := MustHaveProjectContext(r)
   214  
   215  	if projCtx.Project == nil {
   216  		http.Error(w, "not found", http.StatusNotFound)
   217  		return
   218  	}
   219  
   220  	taskName := mux.Vars(r)["task_name"]
   221  
   222  	highOrder, err := strconv.ParseInt(r.FormValue("high"), 10, 64)
   223  	if err != nil {
   224  		http.Error(w, fmt.Sprintf("Error parsing high: `%s`", err.Error()), http.StatusBadRequest)
   225  		return
   226  	}
   227  	lowOrder, err := strconv.ParseInt(r.FormValue("low"), 10, 64)
   228  	if err != nil {
   229  		http.Error(w, fmt.Sprintf("Error parsing low: `%s`", err.Error()), http.StatusBadRequest)
   230  		return
   231  	}
   232  
   233  	filter := struct {
   234  		BuildVariants []string          `json:"buildVariants"`
   235  		Tests         map[string]string `json:"tests"`
   236  	}{}
   237  
   238  	err = json.Unmarshal([]byte(r.FormValue("filter")), &filter)
   239  	if err != nil {
   240  		http.Error(w, fmt.Sprintf("Error in filter: %v", err.Error()), http.StatusBadRequest)
   241  		return
   242  	}
   243  	buildVariants := projCtx.Project.GetVariantsWithTask(taskName)
   244  
   245  	onlyMatchingTasks := (r.FormValue("only_matching_tasks") == "true")
   246  
   247  	// If there are no build variants, use all of them for the given task name.
   248  	// Need this because without the build_variant specified, no amount of hinting
   249  	// will get sort to use the proper index
   250  	query := bson.M{
   251  		"build_variant": bson.M{
   252  			"$in": buildVariants,
   253  		},
   254  		"display_name": taskName,
   255  		"order": bson.M{
   256  			"$gte": lowOrder,
   257  			"$lte": highOrder,
   258  		},
   259  		"branch": projCtx.Project.Identifier,
   260  	}
   261  
   262  	// If there are build variants, use them instead
   263  	if len(filter.BuildVariants) > 0 {
   264  		query["build_variant"] = bson.M{
   265  			"$in": filter.BuildVariants,
   266  		}
   267  	}
   268  
   269  	// If there are tests to filter by, create a big $elemMatch $or in the
   270  	// projection to make sure we only get the tests we care about.
   271  	elemMatchOr := make([]bson.M, 0)
   272  	for test, result := range filter.Tests {
   273  		regexp := fmt.Sprintf(testMatchRegex, test, test)
   274  		if result == "ran" {
   275  			// Special case: if asking for tasks where the test ran, don't care
   276  			// about the test status
   277  			elemMatchOr = append(elemMatchOr, bson.M{
   278  				"test_file": bson.RegEx{regexp, ""},
   279  			})
   280  		} else {
   281  			elemMatchOr = append(elemMatchOr, bson.M{
   282  				"test_file": bson.RegEx{regexp, ""},
   283  				"status":    result,
   284  			})
   285  		}
   286  	}
   287  
   288  	elemMatch := bson.M{"$or": elemMatchOr}
   289  
   290  	// Special case: if only one test filter, don't need to use a $or
   291  	if 1 == len(elemMatchOr) {
   292  		elemMatch = elemMatchOr[0]
   293  	}
   294  
   295  	projection := bson.M{
   296  		"_id":           1,
   297  		"status":        1,
   298  		"activated":     1,
   299  		"time_taken":    1,
   300  		"build_variant": 1,
   301  	}
   302  
   303  	if len(elemMatchOr) > 0 {
   304  		projection["test_results"] = bson.M{
   305  			"$elemMatch": elemMatch,
   306  		}
   307  
   308  		// If we only care about matching tasks, put the elemMatch in the query too
   309  		if onlyMatchingTasks {
   310  			query["test_results"] = bson.M{
   311  				"$elemMatch": elemMatch,
   312  			}
   313  		}
   314  	}
   315  
   316  	last, err := task.Find(db.Query(query).Project(projection))
   317  
   318  	if err != nil {
   319  		http.Error(w, fmt.Sprintf("Error querying tasks: `%s`", err.Error()), http.StatusInternalServerError)
   320  		return
   321  	}
   322  
   323  	uis.WriteJSON(w, http.StatusOK, last)
   324  }
   325  
   326  func (uis *UIServer) taskHistoryTestNames(w http.ResponseWriter, r *http.Request) {
   327  	taskName := mux.Vars(r)["task_name"]
   328  
   329  	projCtx := MustHaveProjectContext(r)
   330  
   331  	if projCtx.Project == nil {
   332  		http.Error(w, "not found", http.StatusNotFound)
   333  		return
   334  	}
   335  
   336  	buildVariants := projCtx.Project.GetVariantsWithTask(taskName)
   337  
   338  	taskHistoryIterator := model.NewTaskHistoryIterator(taskName, buildVariants,
   339  		projCtx.Project.Identifier)
   340  
   341  	results, err := taskHistoryIterator.GetDistinctTestNames(NumTestsToSearchForTestNames)
   342  
   343  	if err != nil {
   344  		http.Error(w, fmt.Sprintf("Error finding test names: `%v`", err.Error()), http.StatusInternalServerError)
   345  		return
   346  	}
   347  
   348  	uis.WriteJSON(w, http.StatusOK, results)
   349  }
   350  
   351  // drawerParams contains the parameters from a request to populate a task or version history drawer.
   352  type drawerParams struct {
   353  	anchorId string
   354  	window   string
   355  	radius   int
   356  }
   357  
   358  func validateDrawerParams(r *http.Request) (drawerParams, error) {
   359  	requestVars := mux.Vars(r)
   360  	anchorId := requestVars["anchor"] // id of the item serving as reference point in history
   361  	window := requestVars["window"]
   362  
   363  	// do some validation on the window of tasks requested
   364  	if window != "surround" && window != "before" && window != "after" {
   365  		return drawerParams{}, errors.Errorf("invalid value %v for window", window)
   366  	}
   367  
   368  	// the 'radius' of the history we want (how many tasks on each side of the anchor task)
   369  	radius := r.FormValue("radius")
   370  	if radius == "" {
   371  		radius = "5"
   372  	}
   373  	historyRadius, err := strconv.Atoi(radius)
   374  	if err != nil {
   375  		return drawerParams{}, errors.Errorf("invalid value %v for radius", radius)
   376  	}
   377  	return drawerParams{anchorId, window, historyRadius}, nil
   378  }
   379  
   380  // Handler for serving the data used to populate the task history drawer.
   381  func (uis *UIServer) versionHistoryDrawer(w http.ResponseWriter, r *http.Request) {
   382  	projCtx := MustHaveProjectContext(r)
   383  
   384  	drawerInfo, err := validateDrawerParams(r)
   385  	if err != nil {
   386  		http.Error(w, err.Error(), http.StatusBadRequest)
   387  		return
   388  	}
   389  
   390  	// get the versions in the requested window
   391  	versions, err := getVersionsInWindow(drawerInfo.window, projCtx.Version.Identifier,
   392  		projCtx.Version.RevisionOrderNumber, drawerInfo.radius, projCtx.Version)
   393  
   394  	if err != nil {
   395  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   396  		return
   397  	}
   398  
   399  	versionDrawerItems := []versionDrawerItem{}
   400  	for _, v := range versions {
   401  		versionDrawerItems = append(versionDrawerItems, versionDrawerItem{
   402  			v.Revision, v.Message, v.CreateTime, v.Id, v.Errors, v.Warnings, v.Ignored})
   403  	}
   404  
   405  	uis.WriteJSON(w, http.StatusOK, struct {
   406  		Revisions []versionDrawerItem `json:"revisions"`
   407  	}{versionDrawerItems})
   408  }
   409  
   410  // Handler for serving the data used to populate the task history drawer.
   411  func (uis *UIServer) taskHistoryDrawer(w http.ResponseWriter, r *http.Request) {
   412  	projCtx := MustHaveProjectContext(r)
   413  
   414  	drawerInfo, err := validateDrawerParams(r)
   415  	if err != nil {
   416  		http.Error(w, err.Error(), http.StatusBadRequest)
   417  		return
   418  	}
   419  
   420  	if projCtx.Version == nil {
   421  		http.Error(w, "no version available", http.StatusBadRequest)
   422  		return
   423  	}
   424  	// get the versions in the requested window
   425  	versions, err := getVersionsInWindow(drawerInfo.window, projCtx.Version.Identifier,
   426  		projCtx.Version.RevisionOrderNumber, drawerInfo.radius, projCtx.Version)
   427  
   428  	if err != nil {
   429  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   430  		return
   431  	}
   432  
   433  	// populate task groups for the versions in the window
   434  	taskGroups, err := getTaskDrawerItems(projCtx.Task.DisplayName, projCtx.Task.BuildVariant, false, versions)
   435  	if err != nil {
   436  		uis.LoggedError(w, r, http.StatusInternalServerError, err)
   437  		return
   438  	}
   439  
   440  	uis.WriteJSON(w, http.StatusOK, struct {
   441  		Revisions []taskDrawerItem `json:"revisions"`
   442  	}{taskGroups})
   443  }
   444  
   445  func getVersionsInWindow(wt, projectId string, anchorOrderNum, radius int,
   446  	center *version.Version) ([]version.Version, error) {
   447  	if wt == beforeWindow {
   448  		return makeVersionsQuery(anchorOrderNum, projectId, radius, true)
   449  	} else if wt == afterWindow {
   450  		after, err := makeVersionsQuery(anchorOrderNum, projectId, radius, false)
   451  		if err != nil {
   452  			return nil, err
   453  		}
   454  		// reverse the versions in "after" so that they're ordered backwards in time
   455  		for i, j := 0, len(after)-1; i < j; i, j = i+1, j-1 {
   456  			after[i], after[j] = after[j], after[i]
   457  		}
   458  		return after, nil
   459  	}
   460  	before, err := makeVersionsQuery(anchorOrderNum, projectId, radius, true)
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	after, err := makeVersionsQuery(anchorOrderNum, projectId, radius, false)
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  	// reverse the versions in "after" so that they're ordered backwards in time
   469  	for i, j := 0, len(after)-1; i < j; i, j = i+1, j-1 {
   470  		after[i], after[j] = after[j], after[i]
   471  	}
   472  	after = append(after, *center)
   473  	after = append(after, before...)
   474  	return after, nil
   475  }
   476  
   477  // Helper to make the appropriate query to the versions collection for what
   478  // we will need.  "before" indicates whether to fetch versions before or
   479  // after the passed-in task.
   480  func makeVersionsQuery(anchorOrderNum int, projectId string, versionsToFetch int, before bool) ([]version.Version, error) {
   481  	// decide how the versions we want relative to the task's revision order number
   482  	ronQuery := bson.M{"$gt": anchorOrderNum}
   483  	if before {
   484  		ronQuery = bson.M{"$lt": anchorOrderNum}
   485  	}
   486  
   487  	// switch how to sort the versions
   488  	sortVersions := []string{version.RevisionOrderNumberKey}
   489  	if before {
   490  		sortVersions = []string{"-" + version.RevisionOrderNumberKey}
   491  	}
   492  
   493  	// fetch the versions
   494  	return version.Find(
   495  		db.Query(bson.M{
   496  			version.IdentifierKey:          projectId,
   497  			version.RevisionOrderNumberKey: ronQuery,
   498  		}).WithFields(
   499  			version.RevisionOrderNumberKey,
   500  			version.RevisionKey,
   501  			version.MessageKey,
   502  			version.CreateTimeKey,
   503  			version.ErrorsKey,
   504  			version.WarningsKey,
   505  			version.IgnoredKey,
   506  		).Sort(sortVersions).Limit(versionsToFetch))
   507  }
   508  
   509  // Given a task name and a slice of versions, return the appropriate sibling
   510  // groups of tasks.  They will be sorted by ascending revision order number,
   511  // unless reverseOrder is true, in which case they will be sorted
   512  // descending.
   513  func getTaskDrawerItems(displayName string, variant string, reverseOrder bool, versions []version.Version) ([]taskDrawerItem, error) {
   514  
   515  	orderNumbers := make([]int, 0, len(versions))
   516  	for _, v := range versions {
   517  		orderNumbers = append(orderNumbers, v.RevisionOrderNumber)
   518  	}
   519  
   520  	revisionSort := task.RevisionOrderNumberKey
   521  	if reverseOrder {
   522  		revisionSort = "-" + revisionSort
   523  	}
   524  
   525  	tasks, err := task.Find(task.ByOrderNumbersForNameAndVariant(orderNumbers, displayName, variant).Sort([]string{revisionSort}))
   526  
   527  	if err != nil {
   528  		return nil, errors.Wrap(err, "error getting sibling tasks")
   529  	}
   530  	return createSiblingTaskGroups(tasks, versions), nil
   531  }
   532  
   533  // Given versions and the appropriate tasks within them, sorted by build
   534  // variant, create sibling groups for the tasks.
   535  func createSiblingTaskGroups(tasks []task.Task, versions []version.Version) []taskDrawerItem {
   536  	// version id -> group
   537  	groupsByVersion := map[string]taskDrawerItem{}
   538  
   539  	// create a group for each version
   540  	for _, v := range versions {
   541  		group := taskDrawerItem{
   542  			Revision: v.Revision,
   543  			Message:  v.Message,
   544  			PushTime: v.CreateTime,
   545  		}
   546  		groupsByVersion[v.Id] = group
   547  	}
   548  
   549  	// go through the tasks, adding blurbs for them to the appropriate versions
   550  	for _, task := range tasks {
   551  		blurb := taskBlurb{
   552  			Id:      task.Id,
   553  			Variant: task.BuildVariant,
   554  			Status:  task.Status,
   555  			Details: task.Details,
   556  		}
   557  
   558  		for _, result := range task.TestResults {
   559  			if result.Status == evergreen.TestFailedStatus {
   560  				blurb.Failures = append(blurb.Failures, result.TestFile)
   561  			}
   562  		}
   563  
   564  		// add the blurb to the appropriate group
   565  		groupForVersion := groupsByVersion[task.Version]
   566  		groupForVersion.TaskBlurb = blurb
   567  		groupsByVersion[task.Version] = groupForVersion
   568  	}
   569  
   570  	// create a slice of the sibling groups, in the appropriate order
   571  	orderedGroups := make([]taskDrawerItem, 0, len(versions))
   572  	for _, version := range versions {
   573  		orderedGroups = append(orderedGroups, groupsByVersion[version.Id])
   574  	}
   575  
   576  	return orderedGroups
   577  
   578  }