// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Net; using System.Web; using System.Web.Mvc; using Microsoft.Practices.ObjectBuilder2; using Newtonsoft.Json.Linq; using Tools.CrashReporter.CrashReportWebSite.DataModels; using Tools.CrashReporter.CrashReportWebSite.DataModels.Repositories; using Tools.CrashReporter.CrashReportWebSite.ViewModels; namespace Tools.CrashReporter.CrashReportWebSite.Controllers { /// /// A controller to handle the Bugg data. /// public class BuggsController : Controller { private int pageSize = 50; /// /// An empty constructor. /// public BuggsController() { } /// /// The Index action. /// /// The form of user data passed up from the client. /// The view to display a list of Buggs on the client. public ActionResult Index( FormCollection BuggsForm ) { using (var logTimer = new FAutoScopedLogTimer( this.GetType().ToString())) { var formData = new FormHelper( Request, BuggsForm, "CrashInTimeFrameGroup" ); var results = GetResults( formData ); results.GenerationTime = logTimer.GetElapsedSeconds().ToString( "F2" ); return View( "Index", results ); } } /// /// The Show action. /// /// The form of user data passed up from the client. /// The unique id of the Bugg. /// The view to display a Bugg on the client. public ActionResult Show( FormCollection buggsForm, int id, int page = 0 ) { using( var logTimer = new FAutoScopedLogTimer( this.GetType().ToString() + "(BuggId=" + id + ")") ) { using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { // Set the display properties based on the radio buttons var displayModuleNames = buggsForm["DisplayModuleNames"] == "true"; var displayFunctionNames = buggsForm["DisplayFunctionNames"] == "true"; var displayFileNames = buggsForm["DisplayFileNames"] == "true"; var displayFilePathNames = false; // Set up the view model with the Crash data if(buggsForm["Page"] != null) int.TryParse(buggsForm["Page"], out page); if (buggsForm["DisplayFilePathNames"] == "true") { displayFilePathNames = true; displayFileNames = false; } var displayUnformattedCallStack = buggsForm["DisplayUnformattedCallStack"] == "true"; var model = GetResult(id, page, pageSize, unitOfWork); model.SourceContext = model.CrashData.First().SourceContext; model.Bugg.PrepareBuggForJira(model.CrashData); // Handle 'CopyToJira' button var buggIdToBeAddedToJira = 0; foreach (var entry in buggsForm.Cast().Where(entry => entry.ToString().Contains("CopyToJira-"))) { int.TryParse(entry.ToString().Substring("CopyToJira-".Length), out buggIdToBeAddedToJira); break; } if (buggIdToBeAddedToJira != 0) { model.Bugg.JiraProject = buggsForm["JiraProject"]; model.Bugg.CopyToJira(); } var jc = JiraConnection.Get(); var bValidJira = false; // Verify valid JiraID, this may be still a TTP if (!string.IsNullOrEmpty(model.Bugg.TTPID)) { var jira = 0; int.TryParse(model.Bugg.TTPID, out jira); if (jira == 0) { bValidJira = true; } } if (jc.CanBeUsed() && bValidJira) { // Grab the data form JIRA. var jiraSearchQuery = "key = " + model.Bugg.TTPID; var jiraResults = new Dictionary>(); try { jiraResults = jc.SearchJiraTickets( jiraSearchQuery, new string[] { "key", // string "summary", // string "components", // System.Collections.ArrayList, Dictionary, name "resolution", // System.Collections.Generic.Dictionary`2[System.String,System.Object], name "fixVersions", // System.Collections.ArrayList, Dictionary, name "customfield_11200" // string }); } catch (System.Exception) { model.Bugg.JiraSummary = "JIRA MISMATCH"; model.Bugg.JiraComponentsText = "JIRA MISMATCH"; model.Bugg.JiraResolution = "JIRA MISMATCH"; model.Bugg.JiraFixVersionsText = "JIRA MISMATCH"; model.Bugg.JiraFixCL = "JIRA MISMATCH"; } // Jira Key, Summary, Components, Resolution, Fix version, Fix changelist if (jiraResults.Any()) { var jira = jiraResults.First(); var summary = (string) jira.Value["summary"]; var componentsText = ""; var components = (System.Collections.ArrayList) jira.Value["components"]; foreach (Dictionary component in components) { componentsText += (string) component["name"]; componentsText += " "; } var resolutionFields = (Dictionary) jira.Value["resolution"]; var resolution = resolutionFields != null ? (string) resolutionFields["name"] : ""; var fixVersionsText = ""; var fixVersions = (System.Collections.ArrayList) jira.Value["fixVersions"]; foreach (Dictionary fixVersion in fixVersions) { fixVersionsText += (string) fixVersion["name"]; fixVersionsText += " "; } var fixCl = jira.Value["customfield_11200"] != null ? (int) (decimal) jira.Value["customfield_11200"] : 0; //Conversion to ado.net entity framework model.Bugg.JiraSummary = summary; model.Bugg.JiraComponentsText = componentsText; model.Bugg.JiraResolution = resolution; model.Bugg.JiraFixVersionsText = fixVersionsText; if (fixCl != 0) { model.Bugg.FixedChangeList = fixCl.ToString(); } } } // Apply any user settings if (buggsForm.Count > 0) { if (!string.IsNullOrEmpty(buggsForm["SetStatus"])) { model.Bugg.Status = buggsForm["SetStatus"]; unitOfWork.CrashRepository.SetStatusByBuggId(model.Bugg.Id, buggsForm["SetFixedIn"]); } if (!string.IsNullOrEmpty(buggsForm["SetFixedIn"])) { model.Bugg.FixedChangeList = buggsForm["SetFixedIn"]; unitOfWork.CrashRepository.SetFixedCLByBuggId(model.Bugg.Id, buggsForm["SetFixedIn"]); } if (!string.IsNullOrEmpty(buggsForm["SetTTP"])) { model.Bugg.TTPID = buggsForm["SetTTP"]; unitOfWork.CrashRepository.SetJiraByBuggId(model.Bugg.Id, buggsForm["SetTTP"]); } unitOfWork.BuggRepository.Update(model.Bugg); unitOfWork.Save(); } var newCrash = model.CrashData.FirstOrDefault(); if (newCrash != null) { var callStack = new CallStackContainer(newCrash.CrashType, newCrash.RawCallStack, newCrash.PlatformName); // Set callstack properties callStack.bDisplayModuleNames = displayModuleNames; callStack.bDisplayFunctionNames = displayFunctionNames; callStack.bDisplayFileNames = displayFileNames; callStack.bDisplayFilePathNames = displayFilePathNames; callStack.bDisplayUnformattedCallStack = displayUnformattedCallStack; model.CallStack = callStack; // Shorten very long function names. foreach (var entry in model.CallStack.CallStackEntries) { entry.FunctionName = entry.GetTrimmedFunctionName(128); } model.SourceContext = newCrash.SourceContext; model.LatestCrashSummary = newCrash.Summary; } model.LatestCrashSummary = newCrash.Summary; model.Bugg.LatestCrashSummary = newCrash.Summary; model.GenerationTime = logTimer.GetElapsedSeconds().ToString("F2"); //Populate Jira Projects var jiraConnection = JiraConnection.Get(); var response = jiraConnection.JiraRequest("/issue/createmeta", JiraConnection.JiraMethod.GET, null, HttpStatusCode.OK); using (var responseReader = new StreamReader(response.GetResponseStream())) { var responseText = responseReader.ReadToEnd(); JObject jsonObject = JObject.Parse(responseText); JToken fields = jsonObject["projects"]; foreach (var project in fields) { model.JiraProjects.Add(new SelectListItem() { Text = project["name"].ToObject(), Value = project["key"].ToObject() }); } } model.PagingInfo = new PagingInfo { CurrentPage = page, PageSize = pageSize, TotalResults = model.Bugg.NumberOfCrashes.Value }; return View("Show", model); } } } #region Private Methods /// /// Retrieve all Buggs matching the search criteria. /// /// The incoming form of search criteria from the client. /// A view to display the filtered Buggs. private BuggsViewModel GetResults(FormHelper FormData) { // Right now we take a Result IQueryable starting with ListAll() Buggs then we widdle away the result set by tacking on // Linq queries. Essentially it's Results.ListAll().Where().Where().Where().Where().Where().Where() // Could possibly create more optimized queries when we know exactly what we're querying // The downside is that if we add more parameters each query may need to be updated.... Or we just add new case statements // The other downside is that there's less code reuse, but that may be worth it. var fromDate = FormData.DateFrom; var toDate = FormData.DateTo.AddDays(1); IQueryable results = null; IQueryable buggResults = null; var skip = (FormData.Page - 1) * FormData.PageSize; var take = FormData.PageSize; var sortedResultsList = new System.Collections.Generic.List(); var groupCounts = new System.Collections.Generic.SortedDictionary(); int totalCountedRecords = 0; using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { var userGroupId = unitOfWork.UserGroupRepository.First(data => data.Name == FormData.UserGroup).Id; // Get every Bugg. var resultsAll = unitOfWork.CrashRepository.ListAll(); // Look at all Buggs that are still 'open' i.e. the last Crash occurred in our date range. results = FilterByDate(resultsAll, FormData.DateFrom, FormData.DateTo); // Filter results by build version. if (!string.IsNullOrEmpty(FormData.VersionName)) { results = results.Where(data => data.BuildVersion == FormData.VersionName); } // Filter by BranchName if (!string.IsNullOrEmpty(FormData.BranchName)) { results = results.Where(da => da.Branch.Equals(FormData.BranchName)); } // Filter by PlatformName if (!string.IsNullOrEmpty(FormData.PlatformName)) { results = results.Where(da => da.PlatformName.Equals(FormData.PlatformName)); } // Run at the end if (!string.IsNullOrEmpty(FormData.SearchQuery)) { try { var queryString = HttpUtility.HtmlDecode(FormData.SearchQuery.ToString()) ?? ""; // Take out terms starting with a - var terms = queryString.Split("-, ;+".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var allFuncionCallIds = new HashSet(); foreach (var term in terms) { var functionCallIds = unitOfWork.FunctionRepository.Get(functionCallInstance => functionCallInstance.Call.Contains(term)).Select(x => x.Id).ToList(); foreach (var id in functionCallIds) { allFuncionCallIds.Add(id); } } // Search for all function ids. OR operation, not very efficient, but for searching for one function should be ok. results = allFuncionCallIds.Aggregate(results, (current, id) => current.Where(x => x.Pattern.Contains(id + "+") || x.Pattern.Contains("+" + id))); } catch (Exception ex) { Debug.WriteLine("Exception in Search: " + ex.ToString()); } } // Filter by Crash Type if (FormData.CrashType != "All") { switch (FormData.CrashType) { case "Crash": results = results.Where(buggInstance => buggInstance.CrashType == 1); break; case "Assert": results = results.Where(buggInstance => buggInstance.CrashType == 2); break; case "Ensure": results = results.Where(buggInstance => buggInstance.CrashType == 3); break; case "CrashAsserts": results = results.Where(buggInstance => buggInstance.CrashType == 1 || buggInstance.CrashType == 2); break; } } // Filter by user group if present if (!string.IsNullOrEmpty(FormData.UserGroup)) results = FilterByUserGroup(results, userGroupId); if (!string.IsNullOrEmpty(FormData.JiraId)) { results = results.Where(data => data.Jira == FormData.JiraId); } var CrashByUserGroupCounts = unitOfWork.CrashRepository.ListAll().Where(data => data.User.UserGroupId == userGroupId && data.TimeOfCrash >= fromDate && data.TimeOfCrash <= toDate && data.BuggId != null) .GroupBy(data => data.BuggId) .Select(data => new {data.Key, Count = data.Count()}) .OrderByDescending(data => data.Count) .ToDictionary(data => data.Key, data => data.Count); var CrashInTimeFrameCounts = unitOfWork.CrashRepository.ListAll().Where(data => data.TimeOfCrash >= fromDate && data.TimeOfCrash <= toDate && data.BuggId != null) .GroupBy(data => data.BuggId) .Select(data => new {data.Key, Count = data.Count()}) .OrderByDescending(data => data.Count) .ToDictionary(data => data.Key, data => data.Count); var affectedUsers = unitOfWork.CrashRepository.ListAll().Where(data => data.User.UserGroupId == userGroupId && data.TimeOfCrash >= fromDate && data.TimeOfCrash <= toDate && data.BuggId != null) .Select(data => new { BuggId = data.BuggId, ComputerName = data.ComputerName }) .Distinct() .GroupBy(data => data.BuggId) .Select(data => new {data.Key, Count = data.Count()}) .OrderByDescending(data => data.Count) .ToDictionary(data => data.Key, data => data.Count); var ids = results.Where(data => data.BuggId != null).Select(data => data.BuggId).Distinct().ToList(); buggResults = unitOfWork.BuggRepository.ListAll().Where(data => ids.Contains(data.Id)); //buggResults = results.Where(data => data.BuggId != null).Select(data => data.Bugg).Distinct(); // Grab just the results we want to display on this page totalCountedRecords = buggResults.Count(); buggResults = GetSortedResults(buggResults, FormData.SortTerm, FormData.SortTerm == "descending", FormData.DateFrom, FormData.DateTo, FormData.UserGroup); sortedResultsList = buggResults.ToList(); // Get UserGroup ResultCounts var groups = buggResults.SelectMany(data => data.UserGroups) .GroupBy(data => data.Name) .Select(data => new {Key = data.Key, Count = data.Count()}) .ToDictionary(x => x.Key, y => y.Count); groupCounts = new SortedDictionary(groups); foreach (var bugg in sortedResultsList) { if (CrashByUserGroupCounts.ContainsKey(bugg.Id)) { bugg.CrashesInTimeFrameGroup = CrashByUserGroupCounts[bugg.Id]; } else bugg.CrashesInTimeFrameGroup = 0; if (CrashInTimeFrameCounts.ContainsKey(bugg.Id)) { bugg.CrashesInTimeFrameAll = CrashInTimeFrameCounts[bugg.Id]; } else bugg.CrashesInTimeFrameAll = 0; if (affectedUsers.ContainsKey(bugg.Id)) { bugg.NumberOfUniqueMachines = affectedUsers[bugg.Id]; } else bugg.NumberOfUniqueMachines = 0; } sortedResultsList = sortedResultsList.OrderByDescending(data => data.CrashesInTimeFrameGroup).Skip(skip) .Take(totalCountedRecords >= skip + take ? take : totalCountedRecords).ToList(); foreach (var bugg in sortedResultsList) { var Crash = unitOfWork.CrashRepository.First(data => data.BuggId == bugg.Id); bugg.FunctionCalls = Crash.GetCallStack().GetFunctionCalls(); } } System.Collections.Generic.List branchNames; System.Collections.Generic.List versionNames; System.Collections.Generic.List platformNames; using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { branchNames = unitOfWork.CrashRepository.GetBranchesAsListItems(); versionNames = unitOfWork.CrashRepository.GetVersionsAsListItems(); platformNames = unitOfWork.CrashRepository.GetPlatformsAsListItems(); } return new BuggsViewModel() { Results = sortedResultsList, PagingInfo = new PagingInfo { CurrentPage = FormData.Page, PageSize = FormData.PageSize, TotalResults = totalCountedRecords }, SortTerm = FormData.SortTerm, SortOrder = FormData.SortOrder, UserGroup = FormData.UserGroup, CrashType = FormData.CrashType, SearchQuery = FormData.SearchQuery, BranchNames = branchNames, VersionNames = versionNames, PlatformNames = platformNames, DateFrom = (long)(FormData.DateFrom - CrashesViewModel.Epoch).TotalMilliseconds, DateTo = (long)(FormData.DateTo - CrashesViewModel.Epoch).TotalMilliseconds, VersionName = FormData.VersionName, PlatformName = FormData.PlatformName, BranchName = FormData.BranchName, GroupCounts = groupCounts, Jira = FormData.JiraId, }; } /// /// Retrieve all Data for a single bug given by the id /// /// /// /// private BuggViewModel GetResult(int id, int page, int pageSize, UnitOfWork unitOfWork) { var model = new BuggViewModel(); model.Bugg = unitOfWork.BuggRepository.GetById(id); model.Bugg.CrashesInTimeFrameAll = 0; model.Bugg.CrashesInTimeFrameGroup = 0; model.Bugg.NumberOfCrashes = unitOfWork.CrashRepository.Count(data => data.BuggId == id); model.Bugg.NumberOfUniqueMachines = unitOfWork.CrashRepository.ListAll() .Where(data => data.BuggId == id) .GroupBy(data => data.ComputerName) .Count(); model.CrashData = unitOfWork.CrashRepository.ListAll().Where(data => data.BuggId == id) .OrderByDescending(data => data.TimeOfCrash).Skip(page * pageSize).Take(pageSize).Select(data => new CrashDataModel() { Id = data.Id, ChangelistVersion = data.ChangelistVersion, GameName = data.GameName, EngineMode = data.EngineMode, PlatformName = data.PlatformName, TimeOfCrash = data.TimeOfCrash, Description = data.Description, RawCallStack = data.RawCallStack, CrashType = data.CrashType, Summary = data.Summary, SourceContext = data.SourceContext, BuildVersion = data.BuildVersion, ComputerName = data.ComputerName, Branch = data.Branch }).ToList(); return model; } /// /// Filter a set of Buggs by a jira ticket. /// /// The unfiltered set of Buggs /// The ticket by which to filter the list of buggs /// private IQueryable FilterByJira(IQueryable results, string jira) { return results.Where(bugg => bugg.TTPID == jira); } /// /// Filter a set of Buggs to a date range. /// /// The unfiltered set of Buggs. /// The earliest date to filter by. /// The latest date to filter by. /// The set of Buggs between the earliest and latest date. private IQueryable FilterByDate(IQueryable results, DateTime dateFrom, DateTime dateTo) { dateTo = dateTo.AddDays(1); //var buggsInTimeFrame = // results.Where(bugg => (bugg.TimeOfFirstCrash >= dateFrom && bugg.TimeOfFirstCrash <= dateTo) || // (bugg.TimeOfLastCrash >= dateFrom && bugg.TimeOfLastCrash <= dateTo) || (bugg.TimeOfFirstCrash <= dateFrom && bugg.TimeOfLastCrash >= dateTo)); var buggsInTimeFrame = results.Where(MyCrash => MyCrash.TimeOfCrash >= dateFrom && MyCrash.TimeOfCrash <= dateTo); return buggsInTimeFrame; } /// /// Filter a set of Buggs by build version. /// /// The unfiltered set of Buggs. /// The build version to filter by. /// The set of Buggs that matches specified build version private IQueryable FilterByBuildVersion(IQueryable results, string buildVersion) { if (!string.IsNullOrEmpty(buildVersion)) { results = results.Where(data => data.BuildVersion.Contains(buildVersion)); } return results; } /// /// Filter a set of Buggs by user group name. /// /// The unfiltered set of Buggs. /// The user group name to filter by. /// The set of Buggs by users in the requested user group. private IQueryable FilterByUserGroup(IQueryable setOfBuggs, int groupName) { return setOfBuggs.Where(data => data.User.UserGroup.Id == groupName); } /// /// Sort the container of Buggs by the requested criteria. /// /// A container of unsorted Buggs. /// The term to sort by. /// Whether to sort by descending or ascending. /// The date of the earliest Bugg to examine. /// The date of the most recent Bugg to examine. /// The user group name to filter by. /// A sorted container of Buggs. private IOrderedQueryable GetSortedResults(IQueryable results, string sortTerm, bool bSortDescending, DateTime dateFrom, DateTime dateTo, string groupName) { IOrderedQueryable orderedResults = null; try { switch (sortTerm) { case "Id": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Id, bSortDescending); break; case "BuildVersion": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.BuildVersion, bSortDescending); break; case "LatestCrash": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.TimeOfLastCrash, bSortDescending); break; case "FirstCrash": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.TimeOfFirstCrash, bSortDescending); break; case "NumberOfCrash": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.NumberOfCrashes, bSortDescending); break; case "NumberOfUsers": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.NumberOfUniqueMachines, bSortDescending); break; case "Pattern": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Pattern, bSortDescending); break; case "CrashType": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.CrashType, bSortDescending); break; case "Status": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Status, bSortDescending); break; case "FixedChangeList": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.FixedChangeList, bSortDescending); break; case "TTPID": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.TTPID, bSortDescending); break; default: orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Id, bSortDescending); break; } return orderedResults; } catch (Exception ex) { Debug.WriteLine("Exception in GetSortedResults: " + ex.ToString()); } return orderedResults; } /// /// /// /// /// /// /// /// private IOrderedQueryable EnumerableOrderBy(IQueryable query, Expression> predicate, bool bDescending) { return bDescending ? query.OrderByDescending(predicate) : query.OrderBy(predicate); } /// Dispose the controller - safely getting rid of database connections /// protected override void Dispose(bool disposing) { base.Dispose(disposing); } #endregion } }