// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Tools.DotNETCommon.Perforce; namespace MetadataTool { /// /// Class that implements a pattern matcher for a particular class of errors /// abstract class PatternMatcher { /// /// The category name /// public abstract string Category { get; } /// /// Creates fingerprints from any matching diagnostics /// /// The job that was run /// The job step that was run /// List of diagnostics that were produced by the build. Items should be removed from this list if they match. /// List which receives all the matched issues. public virtual void Match(InputJob Job, InputJobStep JobStep, List Diagnostics, List Issues) { for (int Idx = 0; Idx < Diagnostics.Count; Idx++) { InputDiagnostic Diagnostic = Diagnostics[Idx]; if(TryMatch(Job, JobStep, Diagnostic, Issues)) { Diagnostics.RemoveAt(Idx); Idx--; } } } /// /// Tries to create a fingerprint from an individual diagnostic. /// /// The job that was run /// The job step that was run /// A diagnostic from the given job step /// List which receives all the matched issues. /// True if this diagnostic should be removed (usually because a fingerprint was created) public abstract bool TryMatch(InputJob Job, InputJobStep JobStep, InputDiagnostic Diagnostic, List Issues); /// /// Determines if one issue can be merged into another /// /// The source issue /// The target issue public virtual bool CanMerge(BuildHealthIssue Source, BuildHealthIssue Target) { // Make sure the categories match if (Source.Category != Target.Category) { return false; } // Check that a filename or message matches if (!Source.FileNames.Any(x => Target.FileNames.Contains(x)) && !Source.Identifiers.Any(x => Target.Identifiers.Contains(x))) { return false; } return true; } /// /// Determines if an issue can be merged into another issue that occurred at the same initial job /// /// The source issue /// The target issue /// True if the two new issues can be merged public virtual bool CanMergeInitialJob(BuildHealthIssue Source, BuildHealthIssue Target) { return Source.Category == Target.Category; } /// /// Merge one fingerprint with another /// /// The source fingerprint /// The fingerprint to merge into public virtual void Merge(BuildHealthIssue Source, BuildHealthIssue Target) { HashSet TargetMessages = new HashSet(Target.Diagnostics.Select(x => x.Message), StringComparer.Ordinal); foreach(BuildHealthDiagnostic SourceDiagnostic in Source.Diagnostics) { if(Target.Diagnostics.Count >= 50) { break; } if(!TargetMessages.Contains(SourceDiagnostic.Message)) { Target.Diagnostics.Add(SourceDiagnostic); } } Target.FileNames.UnionWith(Source.FileNames); Target.Identifiers.UnionWith(Source.Identifiers); Target.References.UnionWith(Source.References); } /// /// Filters all the likely causers from the list of changes since an issue was created /// /// The perforce connection /// The build issue /// List of changes since the issue first occurred. /// List of changes which are causers for the issue public virtual List FindCausers(PerforceConnection Perforce, BuildHealthIssue Issue, IReadOnlyList Changes) { List Causers = new List(); SortedSet FileNamesWithoutPath = GetFileNamesWithoutPath(Issue.FileNames); if (FileNamesWithoutPath.Count > 0) { foreach (ChangeInfo Change in Changes) { DescribeRecord DescribeRecord = GetDescribeRecord(Perforce, Change); if (ContainsFileNames(DescribeRecord, FileNamesWithoutPath)) { Causers.Add(Change); } } } if(Causers.Count > 0) { return Causers; } else { return new List(Changes); } } /// /// Utility method to get the describe record for a change. Caches it on the ChangeInfo object as necessary. /// /// The Perforce connection /// The change to query public DescribeRecord GetDescribeRecord(PerforceConnection Perforce, ChangeInfo Change) { if(Change.CachedDescribeRecord == null) { Change.CachedDescribeRecord = Perforce.Describe(Change.Record.Number).Data; } return Change.CachedDescribeRecord; } /// /// Tests whether a change is a code change /// /// The Perforce connection /// The change to query /// True if the change is a code change public bool ContainsAnyFileWithExtension(PerforceConnection Perforce, ChangeInfo Change, string[] Extensions) { DescribeRecord Record = GetDescribeRecord(Perforce, Change); foreach(DescribeFileRecord File in Record.Files) { foreach(string Extension in Extensions) { if(File.DepotFile.EndsWith(Extension, StringComparison.OrdinalIgnoreCase)) { return true; } } } return false; } /// /// Determines if this change is a likely causer for an issue /// /// The change describe record /// Fingerprint for the issue /// True if the change is a likely culprit protected static bool ContainsFileNames(DescribeRecord DescribeRecord, SortedSet FileNamesWithoutPath) { foreach (DescribeFileRecord File in DescribeRecord.Files) { int Idx = File.DepotFile.LastIndexOf('/'); if (Idx != -1) { string FileName = File.DepotFile.Substring(Idx + 1); if (FileNamesWithoutPath.Contains(FileName)) { return true; } } } return false; } /// /// Normalizes a filename to a path within the workspace /// /// Filename to normalize /// Base directory containing the workspace /// Normalized filename protected string GetNormalizedFileName(string FileName, string BaseDirectory) { string NormalizedFileName = FileName.Replace('\\', '/'); if (!String.IsNullOrEmpty(BaseDirectory)) { // Normalize the expected base directory for errors in this build, and attempt to strip it from the file name string NormalizedBaseDirectory = BaseDirectory; if (NormalizedBaseDirectory != null && NormalizedBaseDirectory.Length > 0) { NormalizedBaseDirectory = NormalizedBaseDirectory.Replace('\\', '/').TrimEnd('/') + "/"; } if (NormalizedFileName.StartsWith(NormalizedBaseDirectory, StringComparison.OrdinalIgnoreCase)) { NormalizedFileName = NormalizedFileName.Substring(NormalizedBaseDirectory.Length); } } else { // Try to match anything under a 'Sync' folder. Match FallbackRegex = Regex.Match(NormalizedFileName, "/Sync/(.*)"); if (FallbackRegex.Success) { NormalizedFileName = FallbackRegex.Groups[1].Value; } } return NormalizedFileName; } /// /// Finds all the unique filenames without their path components /// /// Set of sorted filenames protected static SortedSet GetFileNamesWithoutPath(IEnumerable FileNames) { SortedSet FileNamesWithoutPath = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (string FileName in FileNames) { int Idx = FileName.LastIndexOf('/'); if (Idx != -1) { FileNamesWithoutPath.Add(FileName.Substring(Idx + 1)); } } return FileNamesWithoutPath; } /// /// Gets a set of unique source file names that relate to this issue /// /// Set of source file names protected static SortedSet GetSourceFileNames(IEnumerable FileNames) { SortedSet ShortFileNames = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (string FileName in FileNames) { int Idx = FileName.LastIndexOfAny(new char[] { '/', '\\' }); if (Idx != -1) { string ShortFileName = FileName.Substring(Idx + 1); if (!ShortFileName.StartsWith("Module.", StringComparison.OrdinalIgnoreCase)) { ShortFileNames.Add(ShortFileName); } } } return ShortFileNames; } /// /// Gets a set of unique asset filenames that relate to this issue /// /// Set of asset names protected static SortedSet GetAssetNames(IEnumerable FileNames) { SortedSet ShortFileNames = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (string FileName in FileNames) { int Idx = FileName.LastIndexOfAny(new char[] { '/', '\\' }); if (Idx != -1) { string AssetName = FileName.Substring(Idx + 1); int DotIdx = AssetName.LastIndexOf('.'); if (DotIdx != -1) { AssetName = AssetName.Substring(0, DotIdx); } ShortFileNames.Add(AssetName); } } return ShortFileNames; } /// /// Gets the summary for an issue /// /// The issue to summarize /// The summary text for this issue public abstract string GetSummary(BuildHealthIssue Issue); } }