github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/service/waterfall.go (about) 1 package service 2 3 import ( 4 "net/http" 5 "sort" 6 "strconv" 7 "time" 8 9 "github.com/evergreen-ci/evergreen" 10 "github.com/evergreen-ci/evergreen/apimodels" 11 "github.com/evergreen-ci/evergreen/model" 12 "github.com/evergreen-ci/evergreen/model/build" 13 "github.com/evergreen-ci/evergreen/model/task" 14 "github.com/evergreen-ci/evergreen/model/user" 15 "github.com/evergreen-ci/evergreen/model/version" 16 "github.com/pkg/errors" 17 ) 18 19 const ( 20 // VersionItemsToCreate is the number of waterfall versions to create, 21 // including rolled-up ones. 22 VersionItemsToCreate = 5 23 24 // SkipQueryParam is the string field for the skip value in the URL 25 // (how many versions to skip). 26 SkipQueryParam = "skip" 27 28 InactiveStatus = "inactive" 29 ) 30 31 // Pull the skip value out of the http request 32 func skipValue(r *http.Request) (int, error) { 33 // determine how many versions to skip 34 toSkipStr := r.FormValue(SkipQueryParam) 35 if toSkipStr == "" { 36 toSkipStr = "0" 37 } 38 return strconv.Atoi(toSkipStr) 39 } 40 41 // uiStatus determines task status label. 42 func uiStatus(task waterfallTask) string { 43 switch task.Status { 44 case evergreen.TaskStarted, evergreen.TaskSucceeded, 45 evergreen.TaskFailed, evergreen.TaskDispatched: 46 return task.Status 47 case evergreen.TaskUndispatched: 48 if task.Activated { 49 return evergreen.TaskUndispatched 50 } else { 51 return evergreen.TaskInactive 52 } 53 default: 54 return "" 55 } 56 } 57 58 type versionVariantData struct { 59 Rows map[string]waterfallRow `json:"rows"` 60 Versions []waterfallVersion `json:"versions"` 61 BuildVariants waterfallBuildVariants `json:"build_variants"` 62 } 63 64 // waterfallData is all of the data that gets sent to the waterfall page on load 65 type waterfallData struct { 66 Rows []waterfallRow `json:"rows"` 67 Versions []waterfallVersion `json:"versions"` 68 TotalVersions int `json:"total_versions"` // total number of versions (for pagination) 69 CurrentSkip int `json:"current_skip"` // number of versions skipped so far 70 PreviousPageCount int `json:"previous_page_count"` // number of versions on previous page 71 CurrentTime int64 `json:"current_time"` // time used to calculate the eta of started task 72 } 73 74 // waterfallBuildVariant stores the Id and DisplayName for a given build 75 // This struct is associated with one waterfallBuild 76 type waterfallBuildVariant struct { 77 Id string `json:"id"` 78 DisplayName string `json:"display_name"` 79 } 80 81 // waterfallRow represents one row associated with a build variant. 82 type waterfallRow struct { 83 BuildVariant waterfallBuildVariant `json:"build_variant"` 84 Builds map[string]waterfallBuild `json:"builds"` 85 } 86 87 // waterfallBuild represents one set of tests for a given build variant and version 88 type waterfallBuild struct { 89 Id string `json:"id"` 90 Active bool `json:"active"` 91 Version string `json:"version"` 92 Tasks []waterfallTask `json:"tasks"` 93 TaskStatusCount taskStatusCount `json:"taskStatusCount"` 94 } 95 96 // waterfallTask represents one task in the waterfall UI. 97 type waterfallTask struct { 98 Id string `json:"id"` 99 Status string `json:"status"` 100 StatusDetails apimodels.TaskEndDetail `json:"task_end_details"` 101 DisplayName string `json:"display_name"` 102 TimeTaken time.Duration `json:"time_taken"` 103 Activated bool `json:"activated"` 104 FailedTestNames []string `json:"failed_test_names,omitempty"` 105 ExpectedDuration time.Duration `json:"expected_duration,omitempty"` 106 StartTime int64 `json:"start_time"` 107 } 108 109 // failedTest holds all the information for displaying context about tests that failed in a 110 // waterfall page tooltip. 111 112 // waterfallVersion holds the waterfall UI representation of a single version (column) 113 // If the RolledUp field is false, then it contains information about 114 // a single version and the metadata fields will be of length 1. 115 // If the RolledUp field is true, this represents multiple inactive versions, with each element 116 // in the metadata arrays corresponding to one inactive version, 117 // ordered from most recent inactive version to earliest. 118 type waterfallVersion struct { 119 120 // whether or not the version element actually consists of multiple inactive 121 // versions rolled up into one 122 RolledUp bool `json:"rolled_up"` 123 124 // metadata about the enclosed versions. if this version does not consist 125 // of multiple rolled-up versions, these will each only have length 1 126 Ids []string `json:"ids"` 127 Messages []string `json:"messages"` 128 Authors []string `json:"authors"` 129 CreateTimes []time.Time `json:"create_times"` 130 Revisions []string `json:"revisions"` 131 RevisionOrderNumber int `json:"revision_order"` 132 133 // used to hold any errors that were found in creating the version 134 Errors []waterfallVersionError `json:"errors"` 135 Warnings []waterfallVersionError `json:"warnings"` 136 Ignoreds []bool `json:"ignoreds"` 137 } 138 139 type waterfallVersionError struct { 140 Messages []string `json:"messages"` 141 } 142 143 // waterfallBuildVariants implements the sort interface to allow backend sorting. 144 type waterfallBuildVariants []waterfallBuildVariant 145 146 func (wfbv waterfallBuildVariants) Len() int { 147 return len(wfbv) 148 } 149 150 func (wfbv waterfallBuildVariants) Less(i, j int) bool { 151 return wfbv[i].DisplayName < wfbv[j].DisplayName 152 } 153 154 func (wfbv waterfallBuildVariants) Swap(i, j int) { 155 wfbv[i], wfbv[j] = wfbv[j], wfbv[i] 156 } 157 158 // waterfallVersions implements the sort interface to allow backend sorting. 159 type waterfallVersions []waterfallVersion 160 161 func (wfv waterfallVersions) Len() int { 162 return len(wfv) 163 } 164 165 func (wfv waterfallVersions) Less(i, j int) bool { 166 return wfv[i].RevisionOrderNumber > wfv[j].RevisionOrderNumber 167 } 168 169 func (wfv waterfallVersions) Swap(i, j int) { 170 wfv[i], wfv[j] = wfv[j], wfv[i] 171 } 172 173 // createWaterfallTasks takes ina build's task cache returns a list of waterfallTasks. 174 func createWaterfallTasks(tasks []build.TaskCache) ([]waterfallTask, taskStatusCount) { 175 //initialize and set TaskStatusCount fields to zero 176 statusCount := taskStatusCount{} 177 waterfallTasks := []waterfallTask{} 178 179 // add the tasks to the build 180 for _, t := range tasks { 181 taskForWaterfall := waterfallTask{ 182 Id: t.Id, 183 Status: t.Status, 184 StatusDetails: t.StatusDetails, 185 DisplayName: t.DisplayName, 186 Activated: t.Activated, 187 TimeTaken: t.TimeTaken, 188 StartTime: t.StartTime.UnixNano(), 189 } 190 taskForWaterfall.Status = uiStatus(taskForWaterfall) 191 192 statusCount.incrementStatus(taskForWaterfall.Status, taskForWaterfall.StatusDetails) 193 194 waterfallTasks = append(waterfallTasks, taskForWaterfall) 195 } 196 return waterfallTasks, statusCount 197 } 198 199 // Fetch versions until 'numVersionElements' elements are created, including 200 // elements consisting of multiple versions rolled-up into one. 201 // The skip value indicates how many versions back in time should be skipped 202 // before starting to fetch versions, the project indicates which project the 203 // returned versions should be a part of. 204 func getVersionsAndVariants(skip, numVersionElements int, project *model.Project) (versionVariantData, error) { 205 // the final array of versions to return 206 finalVersions := []waterfallVersion{} 207 208 // keep track of the build variants we see 209 bvSet := map[string]bool{} 210 211 waterfallRows := map[string]waterfallRow{} 212 213 // build variant mappings - used so we can store the display name as 214 // the build variant field of a build 215 buildVariantMappings := project.GetVariantMappings() 216 217 // keep track of the last rolled-up version, so inactive versions can 218 // be added 219 var lastRolledUpVersion *waterfallVersion 220 221 // loop until we have enough from the db 222 for len(finalVersions) < numVersionElements { 223 224 // fetch the versions and associated builds 225 versionsFromDB, buildsByVersion, err := 226 fetchVersionsAndAssociatedBuilds(project, skip, numVersionElements) 227 228 if err != nil { 229 return versionVariantData{}, errors.Wrap(err, 230 "error fetching versions and builds:") 231 } 232 233 // if we've reached the beginning of all versions 234 if len(versionsFromDB) == 0 { 235 break 236 } 237 238 // to fetch started tasks and failed tests for providing additional context 239 // in a tooltip 240 failedAndStartedTaskIds := []string{} 241 242 // update the amount skipped 243 skip += len(versionsFromDB) 244 245 // create the necessary versions, rolling up inactive ones 246 for _, versionFromDB := range versionsFromDB { 247 248 // if we have hit enough versions, break out 249 if len(finalVersions) == numVersionElements { 250 break 251 } 252 253 // the builds for the version 254 buildsInVersion := buildsByVersion[versionFromDB.Id] 255 256 // see if there are any active tasks in the version 257 versionActive := anyActiveTasks(buildsInVersion) 258 259 // add any represented build variants to the set and initialize rows 260 for _, b := range buildsInVersion { 261 bvSet[b.BuildVariant] = true 262 263 buildVariant := waterfallBuildVariant{ 264 Id: b.BuildVariant, 265 DisplayName: buildVariantMappings[b.BuildVariant], 266 } 267 268 if buildVariant.DisplayName == "" { 269 buildVariant.DisplayName = b.BuildVariant + 270 " (removed)" 271 } 272 273 if _, ok := waterfallRows[b.BuildVariant]; !ok { 274 waterfallRows[b.BuildVariant] = waterfallRow{ 275 Builds: map[string]waterfallBuild{}, 276 BuildVariant: buildVariant, 277 } 278 } 279 280 } 281 282 // if it is inactive, roll up the version and don't create any 283 // builds for it 284 if !versionActive { 285 if lastRolledUpVersion == nil { 286 lastRolledUpVersion = &waterfallVersion{RolledUp: true, RevisionOrderNumber: versionFromDB.RevisionOrderNumber} 287 } 288 289 // add the version metadata into the last rolled-up version 290 lastRolledUpVersion.Ids = append(lastRolledUpVersion.Ids, 291 versionFromDB.Id) 292 lastRolledUpVersion.Authors = append(lastRolledUpVersion.Authors, 293 versionFromDB.Author) 294 lastRolledUpVersion.Errors = append( 295 lastRolledUpVersion.Errors, waterfallVersionError{versionFromDB.Errors}) 296 lastRolledUpVersion.Warnings = append( 297 lastRolledUpVersion.Warnings, waterfallVersionError{versionFromDB.Warnings}) 298 lastRolledUpVersion.Messages = append( 299 lastRolledUpVersion.Messages, versionFromDB.Message) 300 lastRolledUpVersion.Ignoreds = append( 301 lastRolledUpVersion.Ignoreds, versionFromDB.Ignored) 302 lastRolledUpVersion.CreateTimes = append( 303 lastRolledUpVersion.CreateTimes, versionFromDB.CreateTime) 304 lastRolledUpVersion.Revisions = append( 305 lastRolledUpVersion.Revisions, versionFromDB.Revision) 306 307 // move on to the next version 308 continue 309 } 310 311 // add a pending rolled-up version, if it exists 312 if lastRolledUpVersion != nil { 313 finalVersions = append(finalVersions, *lastRolledUpVersion) 314 lastRolledUpVersion = nil 315 } 316 317 // if we have hit enough versions, break out 318 if len(finalVersions) == numVersionElements { 319 break 320 } 321 322 // if the version can not be rolled up, create a fully fledged 323 // version for it 324 activeVersion := waterfallVersion{ 325 Ids: []string{versionFromDB.Id}, 326 Messages: []string{versionFromDB.Message}, 327 Authors: []string{versionFromDB.Author}, 328 CreateTimes: []time.Time{versionFromDB.CreateTime}, 329 Revisions: []string{versionFromDB.Revision}, 330 Errors: []waterfallVersionError{{versionFromDB.Errors}}, 331 Warnings: []waterfallVersionError{{versionFromDB.Warnings}}, 332 Ignoreds: []bool{versionFromDB.Ignored}, 333 RevisionOrderNumber: versionFromDB.RevisionOrderNumber, 334 } 335 336 // add the builds to the waterfall row 337 for _, b := range buildsInVersion { 338 currentRow := waterfallRows[b.BuildVariant] 339 buildForWaterfall := waterfallBuild{ 340 Id: b.Id, 341 Version: versionFromDB.Id, 342 } 343 344 tasks, statusCount := createWaterfallTasks(b.Tasks) 345 buildForWaterfall.Tasks = tasks 346 buildForWaterfall.TaskStatusCount = statusCount 347 currentRow.Builds[versionFromDB.Id] = buildForWaterfall 348 waterfallRows[b.BuildVariant] = currentRow 349 for _, task := range buildForWaterfall.Tasks { 350 if task.Status == evergreen.TaskFailed || task.Status == evergreen.TaskStarted { 351 failedAndStartedTaskIds = append(failedAndStartedTaskIds, task.Id) 352 } 353 } 354 } 355 356 // add the version 357 finalVersions = append(finalVersions, activeVersion) 358 359 } 360 361 failedAndStartedTasks, err := task.Find(task.ByIds(failedAndStartedTaskIds)) 362 if err != nil { 363 return versionVariantData{}, errors.Wrap(err, "error fetching failed tasks") 364 365 } 366 addFailedAndStartedTests(waterfallRows, failedAndStartedTasks) 367 } 368 369 // if the last version was rolled-up, add it 370 if lastRolledUpVersion != nil { 371 finalVersions = append(finalVersions, *lastRolledUpVersion) 372 } 373 374 // create the list of display names for the build variants represented 375 buildVariants := waterfallBuildVariants{} 376 for name := range bvSet { 377 displayName := buildVariantMappings[name] 378 if displayName == "" { 379 displayName = name + " (removed)" 380 } 381 buildVariants = append(buildVariants, waterfallBuildVariant{Id: name, DisplayName: displayName}) 382 } 383 384 return versionVariantData{ 385 Rows: waterfallRows, 386 Versions: finalVersions, 387 BuildVariants: buildVariants, 388 }, nil 389 390 } 391 392 // addFailedTests adds all of the failed tests associated with a task to its entry in the waterfallRow. 393 // addFailedAndStartedTests adds all of the failed tests associated with a task to its entry in the waterfallRow 394 // and adds the estimated duration to started tasks. 395 func addFailedAndStartedTests(waterfallRows map[string]waterfallRow, failedAndStartedTasks []task.Task) { 396 failedTestsByTaskId := map[string][]string{} 397 expectedDurationByTaskId := map[string]time.Duration{} 398 for _, t := range failedAndStartedTasks { 399 failedTests := []string{} 400 for _, r := range t.TestResults { 401 if r.Status == evergreen.TestFailedStatus { 402 failedTests = append(failedTests, r.TestFile) 403 } 404 } 405 if t.Status == evergreen.TaskStarted { 406 expectedDurationByTaskId[t.Id] = t.ExpectedDuration 407 } 408 failedTestsByTaskId[t.Id] = failedTests 409 } 410 for buildVariant, row := range waterfallRows { 411 for versionId, build := range row.Builds { 412 for i, task := range build.Tasks { 413 if len(failedTestsByTaskId[task.Id]) != 0 { 414 waterfallRows[buildVariant].Builds[versionId].Tasks[i].FailedTestNames = append( 415 waterfallRows[buildVariant].Builds[versionId].Tasks[i].FailedTestNames, 416 failedTestsByTaskId[task.Id]...) 417 sort.Strings(waterfallRows[buildVariant].Builds[versionId].Tasks[i].FailedTestNames) 418 } 419 if duration, ok := expectedDurationByTaskId[task.Id]; ok { 420 waterfallRows[buildVariant].Builds[versionId].Tasks[i].ExpectedDuration = duration 421 } 422 } 423 } 424 } 425 } 426 427 // Helper function to fetch a group of versions and their associated builds. 428 // Returns the versions themselves, as well as a map of version id -> the 429 // builds that are a part of the version (unsorted). 430 func fetchVersionsAndAssociatedBuilds(project *model.Project, skip int, numVersions int) ([]version.Version, map[string][]build.Build, error) { 431 432 // fetch the versions from the db 433 versionsFromDB, err := version.Find(version.ByProjectId(project.Identifier). 434 WithFields( 435 version.RevisionKey, 436 version.ErrorsKey, 437 version.WarningsKey, 438 version.IgnoredKey, 439 version.MessageKey, 440 version.AuthorKey, 441 version.RevisionOrderNumberKey, 442 version.CreateTimeKey, 443 ).Sort([]string{"-" + version.RevisionOrderNumberKey}).Skip(skip).Limit(numVersions)) 444 445 if err != nil { 446 return nil, nil, errors.Wrap(err, "error fetching versions from database") 447 } 448 449 // create a slice of the version ids (used to fetch the builds) 450 versionIds := make([]string, 0, len(versionsFromDB)) 451 for _, v := range versionsFromDB { 452 versionIds = append(versionIds, v.Id) 453 } 454 455 // fetch all of the builds (with only relevant fields) 456 buildsFromDb, err := build.Find( 457 build.ByVersions(versionIds). 458 WithFields(build.BuildVariantKey, build.TasksKey, build.VersionKey)) 459 if err != nil { 460 return nil, nil, errors.Wrap(err, "error fetching builds from database") 461 } 462 463 // group the builds by version 464 buildsByVersion := map[string][]build.Build{} 465 for _, build := range buildsFromDb { 466 buildsByVersion[build.Version] = append(buildsByVersion[build.Version], build) 467 } 468 469 return versionsFromDB, buildsByVersion, nil 470 } 471 472 // Takes in a slice of tasks, and determines whether any of the tasks in 473 // any of the builds are active. 474 func anyActiveTasks(builds []build.Build) bool { 475 for _, build := range builds { 476 for _, task := range build.Tasks { 477 if task.Activated { 478 return true 479 } 480 } 481 } 482 return false 483 } 484 485 // Calculates how many actual versions would appear on the previous page, given 486 // the starting skip for the current page as well as the number of version 487 // elements per page (including elements containing rolled-up versions). 488 func countOnPreviousPage(skip int, numVersionElements int, 489 project *model.Project) (int, error) { 490 491 // if there is no previous page 492 if skip == 0 { 493 return 0, nil 494 } 495 496 // the initial number of versions to be fetched per iteration 497 toFetch := numVersionElements 498 499 // the initial number of elements to step back from the current point 500 // (capped to 0) 501 stepBack := skip - numVersionElements 502 if stepBack < 0 { 503 toFetch = skip // only fetch up to the current point 504 stepBack = 0 505 } 506 507 // bookkeeping: the number of version elements represented so far, as well 508 // as the total number of versions fetched 509 elementsCreated := 0 510 versionsFetched := 0 511 // bookkeeping: whether the previous version was active 512 prevActive := true 513 514 for { 515 516 // fetch the versions and builds 517 versionsFromDB, buildsByVersion, err := 518 fetchVersionsAndAssociatedBuilds(project, stepBack, toFetch) 519 520 if err != nil { 521 return 0, errors.Wrap(err, "error fetching versions and builds") 522 } 523 524 // for each of the versions fetched (iterating backwards), calculate 525 // how much it contributes to the version elements that would be 526 // created 527 for i := len(versionsFromDB) - 1; i >= 0; i-- { 528 529 // increment the versions we've fetched 530 versionsFetched += 1 531 // if there are any active tasks 532 if anyActiveTasks(buildsByVersion[versionsFromDB[i].Id]) { 533 534 // we may have stepped one over where the versions end, if 535 // the last was inactive 536 if elementsCreated == numVersionElements { 537 return versionsFetched - 1, nil 538 } 539 540 // the active version would get its own element 541 elementsCreated += 1 542 prevActive = true 543 544 // see if it's the last 545 if elementsCreated == numVersionElements { 546 return versionsFetched, nil 547 } 548 } else if prevActive { 549 550 // only record a rolled-up version when we hit the first version 551 // in it (walking backwards) 552 elementsCreated += 1 553 prevActive = false 554 } 555 556 } 557 558 // if we've hit the most recent versions (can't step back farther) 559 if stepBack == 0 { 560 return versionsFetched, nil 561 } 562 563 // recalculate where to skip to and how many to fetch 564 stepBack -= numVersionElements 565 if stepBack < 0 { 566 toFetch = stepBack + numVersionElements 567 stepBack = 0 568 } 569 570 } 571 } 572 573 // Create and return the waterfall data we need to render the page. 574 // Http handler for the waterfall page 575 func (uis *UIServer) waterfallPage(w http.ResponseWriter, r *http.Request) { 576 projCtx := MustHaveProjectContext(r) 577 if projCtx.Project == nil { 578 uis.ProjectNotFound(projCtx, w, r) 579 return 580 } 581 582 skip, err := skipValue(r) 583 if err != nil { 584 skip = 0 585 } 586 587 finalData := waterfallData{} 588 589 // first, get all of the versions and variants we will need 590 vvData, err := getVersionsAndVariants(skip, 591 VersionItemsToCreate, projCtx.Project) 592 593 if err != nil { 594 uis.LoggedError(w, r, http.StatusInternalServerError, err) 595 return 596 } 597 var wfv waterfallVersions = vvData.Versions 598 599 sort.Sort(wfv) 600 finalData.Versions = wfv 601 602 sort.Sort(vvData.BuildVariants) 603 rows := []waterfallRow{} 604 for _, bv := range vvData.BuildVariants { 605 rows = append(rows, vvData.Rows[bv.Id]) 606 } 607 finalData.Rows = rows 608 609 // compute the total number of versions that exist 610 finalData.TotalVersions, err = version.Count(version.ByProjectId(projCtx.Project.Identifier)) 611 if err != nil { 612 uis.LoggedError(w, r, http.StatusInternalServerError, err) 613 return 614 } 615 616 // compute the number of versions on the previous page 617 finalData.PreviousPageCount, err = countOnPreviousPage(skip, VersionItemsToCreate, projCtx.Project) 618 if err != nil { 619 uis.LoggedError(w, r, http.StatusInternalServerError, err) 620 return 621 } 622 623 // add in the skip value 624 finalData.CurrentSkip = skip 625 626 // pass it the current time 627 finalData.CurrentTime = time.Now().UnixNano() 628 629 uis.WriteHTML(w, http.StatusOK, struct { 630 ProjectData projectContext 631 User *user.DBUser 632 Data waterfallData 633 JiraHost string 634 }{projCtx, GetUser(r), finalData, uis.Settings.Jira.Host}, "base", "waterfall.html", "base_angular.html", "menu.html") 635 }