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 }