// 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);
}
}