Files
2025-05-18 13:04:45 +08:00

210 lines
8.0 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using EpicGames.Horde.Issues.Handlers;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Text.RegularExpressions;
#nullable enable
#pragma warning disable MA0016
namespace AutomationUtils.Matchers
{
/// <summary>
/// Matcher for LLVM Sanitizer reports
///
/// </summary>
public class SanitizerEventMatcher : ILogEventMatcher
{
/// <summary>
/// Regex pattern that matches Report Start
/// </summary>
// e.g.
// ASAN: =================================================================
// TSAN: ==================
public static readonly Regex ReportStartPattern = new Regex(@"(^=================================================================$)|(^==================$)", RegexOptions.Multiline | RegexOptions.ExplicitCapture);
/// <summary>
/// Regex pattern that matches Report End
/// </summary>
// e.g.
// ASAN: ==30562==ABORTING
// TSAN: ==================
public static readonly Regex ReportEndPattern = new Regex(@"(End of Address Sanitizer report)|(^==\d+==(\s*\w+)?$)|(^==================$)", RegexOptions.Multiline | RegexOptions.ExplicitCapture);
/// <summary>
/// Regex pattern that matches Report Level
/// </summary>
// e.g.
// ASAN: ==30562==ERROR: AddressSanitizer: heap-use-after-free on address 0x617002aa8418 at pc 0x7f98a08bd090 bp 0x7ffc30203af0 sp 0x7ffc30203ae8
// TSAN: WARNING: ThreadSanitizer: data race (pid=6049)
public static readonly Regex ReportLevelPattern = new Regex(@"^[\s\t]*(==\d+(?:\[0x\w+\])?==\s*)?(?<ReportLevel>WARNING|ERROR):\s+(?<SanitizerName>\w+)Sanitizer:\s*(?<Summary>.+)$", RegexOptions.Multiline | RegexOptions.ExplicitCapture);
/// <summary>
/// Regex pattern that matches Stack Trace
/// </summary>
// e.g. Note line number, column number and BuildId are optional
// ASAN: #0 0x7f98a08bd08f in UObjectBase::GetFName() const /mnt/somepath/file.h:90:10
// TSAN: #0 MyType<Type, OtherType>::Function(unsigned long*&) const /mnt/somepath/file.h:90:10 (BinaryName+0x2bc102e0) (BuildId: 8b17597a9f5444a0)
public static readonly Regex StackTracePattern = new Regex(
@"^[\s\t]+\#[\d]+\s+(0x[0-9a-fA-F]+\s+in\s+)?" + // The optional capture group is to handle ASAN's slightly different stack trace syntax from TSAN
@"(?<Symbol>.+?)\s" +
@"(?<SourceFile>(\/|\w:).+?)" +
@"(:(?<Line>\d+?))?" +
@"(:(?<Column>\d+?))?" +
@"(\s+\(.+?\+(?<Address>0x[0-9a-fA-F]+?)\))?" +
@"(\s+?\(BuildId:\s+?(?<BuildId>.+?)\))?$", RegexOptions.Multiline | RegexOptions.ExplicitCapture);
/// <summary>
/// Regex pattern that matches Summary
/// </summary>
// e.g.
// ASAN: SUMMARY: AddressSanitizer: heap-use-after-free /some/path.cpp:169:33 in UObjectBase::GetFName() const
// TSAN: SUMMARY: ThreadSanitizer: data race /some/path.cpp:169:33 in MyType::Foo()
public static readonly Regex SummaryPattern = new Regex(
@"^[\s\t]*SUMMARY:\s+(?<SanitizerName>\w+)Sanitizer:\s*" +
@"(?<SummaryReason>.+)\s" +
@"((\(?(?<SourceFile>(\/|\w:|eboot).+?)\)?" +
@"(\s+\(BuildId:\s.+\))?" +
@"(:(?<Line>\d+?))?" +
@"(:(?<Column>\d+?))?))\s+?" +
@"(in\s+?(?<Symbol>.+)|at\s+?\(?(?<Symbol>.+)\)?.+)$", RegexOptions.Multiline | RegexOptions.ExplicitCapture);
/// <summary>
/// Convert SanitizerName to EventId
/// </summary>
/// <param name="sanitizerName"></param>
/// <returns></returns>
public static EventId ConvertSanitizerNameToEventId(string sanitizerName)
{
switch (sanitizerName)
{
case "Thread": return KnownLogEvents.Sanitizer_Thread;
case "Address": return KnownLogEvents.Sanitizer_Address;
}
return KnownLogEvents.Sanitizer;
}
/// <summary>
/// Convert ReportLevel string to LogLevel enum
/// </summary>
/// <param name="reportLevel"></param>
/// <returns></returns>
public static LogLevel ConvertReportLevel(string reportLevel)
{
switch (reportLevel)
{
case "WARNING": return LogLevel.Warning;
case "ERROR": return LogLevel.Error;
}
return LogLevel.Error;
}
/// <summary>
/// Add Value to Properties using (Regex) Group match and update Message with the corresponding key instead of value from Group.
/// </summary>
/// <param name="Value"></param>
/// <param name="Group"></param>
/// <param name="Properties"></param>
/// <param name="Message"></param>
/// <param name="Offset">Optional argument to offset Group index from Message if Message changed since Group was initialy matched</param>
private static void AddPropertyAndReplace(LogValue Value, Group Group, Dictionary<string, object> Properties, ref string Message, int Offset = 0)
{
string Key = Value.Type.ToString();
Value.Text = Group.Value;
Properties.Add(Key, Value);
int Index = Group.Index + Offset;
Message = $"{Message.Substring(0, Index)}{{{Key}}}{Message.Substring(Index + Group.Length)}";
}
/// <summary>
/// Add Sanitizer Summary information to input Key/Value Pair Properties and update Message with Key markers
/// </summary>
/// <param name="Message"></param>
/// <param name="Properties"></param>
/// <returns></returns>
public static bool AddSanitizerSummaryProperties(ref string Message, Dictionary<string, object> Properties)
{
Match Match = SummaryPattern.Match(Message);
if (Match.Success)
{
int InitialLength = Message.Length;
AddPropertyAndReplace(SanitizerIssueHandler.SanitizerName, Match.Groups["SanitizerName"], Properties, ref Message);
AddPropertyAndReplace(SanitizerIssueHandler.SummaryReason, Match.Groups["SummaryReason"], Properties, ref Message, Message.Length - InitialLength);
AddPropertyAndReplace(SanitizerIssueHandler.SummarySourceFile, Match.Groups["SourceFile"], Properties, ref Message, Message.Length - InitialLength);
return true;
}
return false;
}
/// <inheritdoc/>
public LogEventMatch? Match(ILogCursor cursor)
{
Match? match;
if (cursor.TryMatch(ReportStartPattern, out match))
{
EventId sanitizerId = KnownLogEvents.Sanitizer;
LogLevel reportLevel = LogLevel.Information;
LogEventBuilder builder = new LogEventBuilder(cursor);
builder.MoveNext();
while (builder.Current.CurrentLine != null)
{
if (builder.Current.TryMatch(StackTracePattern, out match))
{
do
{
builder.AnnotateSymbol(match!.Groups["Symbol"]);
builder.AnnotateSourceFile(match.Groups["SourceFile"], "");
builder.TryAnnotate(match.Groups["Line"], LogEventMarkup.LineNumber);
builder.TryAnnotate(match.Groups["Column"], LogEventMarkup.ColumnNumber);
builder.TryAnnotate(match.Groups["Address"], "");
builder.TryAnnotate(match.Groups["BuildId"], "");
builder.MoveNext();
} while (builder.Current.TryMatch(StackTracePattern, out match));
}
else
{
if (builder.Current.TryMatch(ReportLevelPattern, out match))
{
sanitizerId = ConvertSanitizerNameToEventId(match.Groups["SanitizerName"].Value);
reportLevel = ConvertReportLevel(match.Groups["ReportLevel"].Value);
// Used by IssueHandler
builder.Annotate(match.Groups["SanitizerName"], SanitizerIssueHandler.SanitizerName);
}
if (builder.Current.TryMatch(SummaryPattern, out match))
{
// Used by IssueHandler
builder.Annotate(match.Groups["SummaryReason"], SanitizerIssueHandler.SummaryReason);
builder.Annotate("SummarySourceFile", match.Groups["SourceFile"], SanitizerIssueHandler.SummarySourceFile);
// Annotate the source file again so that we may get UGS link resolution
builder.AnnotateSourceFile(match.Groups["SourceFile"], "");
builder.TryAnnotate(match.Groups["Line"], LogEventMarkup.LineNumber);
builder.TryAnnotate(match.Groups["Column"], LogEventMarkup.ColumnNumber);
builder.AnnotateSymbol(match.Groups["Symbol"]);
}
else if (builder.Current.IsMatch(ReportEndPattern))
{
return builder.ToMatch(LogEventPriority.AboveNormal, reportLevel, sanitizerId);
}
builder.MoveNext();
}
}
}
return null;
}
}
}