// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using System.Data;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using AutomationTool;
using UnrealBuildTool;
using UnrealBuildBase;
namespace Gauntlet
{
///
/// Utility functions for Unreal Automation Telemetry
///
static class UnrealAutomationTelemetry
{
///
/// Gather csv telemetry outputs generated by UE automated tests and add them to a test report
///
///
///
///
static public void LoadOutputsIntoReport(string TelemetryDirectory, ITelemetryReport TelemetryReport, Dictionary Mapping = null)
{
// Scan for csv files
foreach (string File in Directory.GetFiles(TelemetryDirectory))
{
if (Path.GetExtension(File) != ".csv")
{
continue;
}
LoadCSVOutputIntoReport(File, TelemetryReport, Mapping);
}
}
///
/// Gather telemetry data from csv file generated by UE automated tests and add them to a test report
///
///
///
///
static public void LoadCSVOutputIntoReport(string File, ITelemetryReport TelemetryReport, Dictionary Mapping = null)
{
List> CSVData = null;
try
{
CSVData = CSVParser.Load(File);
}
catch (Exception Ex)
{
Log.Error("Telemetry - Failed to read CSV file '{0}'. {1}", File, Ex);
}
if (CSVData == null)
{
return;
}
string TestNameKey = CSVColumns.TestName.ToString();
string DataPointKey = CSVColumns.DataPoint.ToString();
string MeasurementKey = CSVColumns.Measurement.ToString();
string ContextKey = CSVColumns.Context.ToString();
string UnitKey = CSVColumns.Unit.ToString();
string BaselineKey = CSVColumns.Baseline.ToString();
if (Mapping != null)
{
if (Mapping.ContainsKey(TestNameKey))
Mapping.TryGetValue(TestNameKey, out TestNameKey);
if (Mapping.ContainsKey(DataPointKey))
Mapping.TryGetValue(DataPointKey, out DataPointKey);
if (Mapping.ContainsKey(MeasurementKey))
Mapping.TryGetValue(MeasurementKey, out MeasurementKey);
if (Mapping.ContainsKey(ContextKey))
Mapping.TryGetValue(ContextKey, out ContextKey);
if (Mapping.ContainsKey(UnitKey))
Mapping.TryGetValue(UnitKey, out UnitKey);
if (Mapping.ContainsKey(BaselineKey))
Mapping.TryGetValue(BaselineKey, out BaselineKey);
}
// Populate telemetry report
foreach (Dictionary Row in CSVData)
{
string TestName;
Row.TryGetValue(TestNameKey, out TestName);
string DataPoint;
Row.TryGetValue(DataPointKey, out DataPoint);
string Measurement;
Row.TryGetValue(MeasurementKey, out Measurement);
string Context;
Row.TryGetValue(ContextKey, out Context);
string Unit;
Row.TryGetValue(UnitKey, out Unit);
string Baseline;
Row.TryGetValue(BaselineKey, out Baseline);
double BaselineFloat = 0;
double.TryParse(Baseline, out BaselineFloat);
if (string.IsNullOrEmpty(TestName) || string.IsNullOrEmpty(DataPoint) || string.IsNullOrEmpty(Measurement))
{
Log.Warning("Telemetry - Missing data in CSV file '{0}':\n TestName='{1}', DataPoint='{2}', Measurement='{3}'", File, TestName, DataPoint, Measurement);
continue;
}
try
{
TelemetryReport.AddTelemetry(TestName, DataPoint, double.Parse(Measurement), Context, Unit, BaselineFloat);
}
catch (FormatException)
{
Log.Error("Telemetry - Failed to parse Measurement value('{0}') in CSV file '{1}'.", Measurement, File);
}
}
}
public enum CSVColumns
{
TestName,
DataPoint,
Measurement,
Context,
Unit,
Baseline
}
static public void WriteDataTableToCSV(DataTable Data, string OutputFile)
{
StringBuilder OutputBuffer = new StringBuilder();
IEnumerable ColumnNames = Data.Columns.Cast().Select(C => C.ColumnName);
OutputBuffer.AppendLine(string.Join(",", ColumnNames));
foreach (DataRow Row in Data.Rows)
{
IEnumerable Fields = Row.ItemArray.Select(F => F.ToString());
OutputBuffer.AppendLine(string.Join(",", Fields));
}
string CSVFolder = Path.GetDirectoryName(OutputFile);
if (!Directory.Exists(CSVFolder))
{
Directory.CreateDirectory(CSVFolder);
}
File.WriteAllText(OutputFile, OutputBuffer.ToString());
}
}
public class UnrealTelemetryContext : ITelemetryContext
{
public object GetProperty(string Name)
{
object Value;
Properties.TryGetValue(Name, out Value);
return Value;
}
Dictionary Properties;
public UnrealTelemetryContext()
{
Properties = new Dictionary();
}
public void SetProperty(string Name, object Value)
{
Properties[Name] = Value;
}
}
///
/// Build command to read Unreal Automation telemetry data saved in csv format and then publish it to a database.
///
class PublishUnrealAutomationTelemetry : BuildCommand
{
[Help("CSVFile", "Path to the csv file to parse.")]
[Help("CSVDirectory", "Path to a folder containing csv files to parse.")]
[Help("CSVMapping", "Optional CSV column mapping. Format is: :,...")]
[Help("TelemetryConfig", "Telemetry configuration to use to publish to Database. Default: UETelemetryStaging.")]
[Help("DatabaseConfigPath", "Path to alternate Database config. Default is TelemetryConfig default.")]
[Help("Project", "Target Project name.")]
[Help("Platform", "Target platform name. Default: current environment platform.")]
[Help("Role", "Target Role name. Default: Editor.")]
[Help("Branch", "Target Branch name. Default: Unknown.")]
[Help("Changelist", "Target Changelist number. Default: 0.")]
[Help("Configuration", "Target Configuration name. Default: Development.")]
[Help("JobLink", "Http Link to Build Job.")]
public override ExitCode Execute()
{
string CSVFile = ParseParamValue("CSVFile=", "");
string CSVDirectory = ParseParamValue("CSVDirectory=", "");
string Config = ParseParamValue("TelemetryConfig=", "UETelemetryStaging");
string DatabaseConfigPath = ParseParamValue("DatabaseConfigPath=", "");
List CSVMappingStrings = Globals.Params.ParseValues("CSVMapping", true);
string ProjectString = ParseParamValue("Project=", "");
string PlatformString = ParseParamValue("Platform=", "");
string BranchString = ParseParamValue("Branch=", "Unknown");
string ChangelistString = ParseParamValue("Changelist=", "0");
string RoleString = ParseParamValue("Role=", "Editor");
string ConfigurationString = ParseParamValue("Configuration=", "Development");
string JobLink = ParseParamValue("JobLink=", "");
if (string.IsNullOrEmpty(CSVDirectory) && string.IsNullOrEmpty(CSVFile))
{
throw new AutomationException("No telemetry files specified. Use -CSVDirectory= or -CSVFile=.csv");
}
if (!string.IsNullOrEmpty(CSVDirectory) && !Directory.Exists(CSVDirectory))
{
throw new AutomationException(string.Format("CSVDirectory '{0}' is missing.", CSVDirectory));
}
if (!string.IsNullOrEmpty(CSVFile) && !File.Exists(CSVFile))
{
throw new AutomationException(string.Format("CSVFile '{0}' is missing.", CSVFile));
}
// CSV Mapping
Dictionary CSVMapping = null;
if (CSVMappingStrings.Count > 0)
{
CSVMapping = new Dictionary();
foreach (string StringLine in CSVMappingStrings)
{
var SplitLine = StringLine.Split(":");
if (SplitLine.Length != 2)
{
throw new AutomationException("CSVMapping entry must be in the form :.");
}
string TargetKey = SplitLine[0];
string SourceKey = SplitLine[1];
object Target;
if (!Enum.TryParse(typeof(UnrealAutomationTelemetry.CSVColumns), TargetKey, true, out Target))
{
string AllKeys = string.Join(", ", Enum.GetNames(typeof(UnrealAutomationTelemetry.CSVColumns)));
throw new AutomationException(string.Format("Unknown target key '{0}', CSVMapping target key must be one of the values: {1}.", TargetKey, AllKeys));
}
TargetKey = ((UnrealAutomationTelemetry.CSVColumns)Target).ToString();
CSVMapping[TargetKey] = SourceKey;
}
}
// Get Context
UnrealTelemetryContext Context = new UnrealTelemetryContext();
if (string.IsNullOrEmpty(ProjectString))
{
throw new AutomationException("No project specified. Use -Project=ShooterGame etc");
}
Context.SetProperty("ProjectName", ProjectString);
object Role;
if (!Enum.TryParse(typeof(UnrealTargetRole), RoleString, true, out Role))
{
string AllKeys = string.Join(", ", Enum.GetNames(typeof(UnrealTargetRole)));
throw new AutomationException(string.Format("Unknown Role '{0}', it must be one of the values: {1}.", RoleString, AllKeys));
}
RoleString = ((UnrealTargetRole)Role).ToString();
object Configuration;
if (!Enum.TryParse(typeof(UnrealTargetConfiguration), ConfigurationString, true, out Configuration))
{
string AllKeys = string.Join(", ", Enum.GetNames(typeof(UnrealTargetConfiguration)));
throw new AutomationException(string.Format("Unknown Configuration '{0}', it must be one of the values: {1}.", ConfigurationString, AllKeys));
}
ConfigurationString = ((UnrealTargetConfiguration)Configuration).ToString();
Context.SetProperty("Configuration", string.Format("{0} {1}", RoleString, ConfigurationString));
Context.SetProperty("Platform", string.IsNullOrEmpty(PlatformString) ? BuildHostPlatform.Current.Platform : UnrealTargetPlatform.Parse(PlatformString));
Context.SetProperty("Branch", BranchString);
Context.SetProperty("Changelist", ChangelistString);
Context.SetProperty("ChangelistDateTime", GetChangelistDateTime(int.Parse(ChangelistString)));
Context.SetProperty("JobLink", JobLink);
// Create a report
HordeReport.SimpleTestReport Report = new HordeReport.SimpleTestReport();
if (Report is ITelemetryReport TelemetryReport)
{
// Parse CSV
if (!string.IsNullOrEmpty(CSVDirectory))
{
UnrealAutomationTelemetry.LoadOutputsIntoReport(CSVDirectory, TelemetryReport, CSVMapping);
}
else
{
UnrealAutomationTelemetry.LoadCSVOutputIntoReport(CSVFile, TelemetryReport, CSVMapping);
}
// Publish Telemetry Data
var DataRows = TelemetryReport.GetAllTelemetryData();
if (DataRows != null)
{
IDatabaseConfig DBConfig = DatabaseConfigManager.GetConfigByName(Config);
if (DBConfig != null)
{
DBConfig.LoadConfig(DatabaseConfigPath);
IDatabaseDriver DB = DBConfig.GetDriver();
Log.Info("Submitting telemetry data to {0}", DB.ToString());
DB.SubmitDataItems(DataRows, Context);
}
else
{
Log.Warning("Got telemetry data, but database configuration is unknown '{0}'.", Config);
}
}
else
{
Log.Error("Failed to retrieve any data from telemetry report.");
}
}
return 0;
}
///
/// Get the DateTime of the given changelist
///
///
///
private DateTime GetChangelistDateTime(int Changelist)
{
try
{
P4Connection.DescribeRecord Record = new P4Connection.DescribeRecord();
P4.DescribeChangelist(Changelist, out Record, false);
Regex Regx = new Regex(@" on ([0-9/]+ [0-9:]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
Match DateMatch = Regx.Match(Record.Header);
return DateTime.Parse(DateMatch.Groups[1].Value);
}
catch
{
return DateTime.Now;
}
}
}
///
/// Build command to fetch Unreal Automation telemetry data from a database and save it to csv.
///
class FetchUnrealAutomationTelemetry : BuildCommand
{
[Help("CSVFile", "Path of the csv file to save.")]
[Help("TelemetryConfig", "Telemetry configuration to use to publish to Database. Default: UETelemetryStaging.")]
[Help("DatabaseConfigPath", "Path to alternate Database config. Default is TelemetryConfig default.")]
[Help("Project", "Target Project name.")]
[Help("Platform", "Target platform name. Default: current environment platform.")]
[Help("Role", "Target Role name. Default: Editor.")]
[Help("Branch", "Target Branch name. Default: Unknown.")]
[Help("Configuration", "Target Configuration name. Default: Development.")]
[Help("Since", "Filter fetch data from the last 'Since' time. Default: 1month.")]
[Help("TestName", "Filter fetch by TestName. Support coma separated list.")]
[Help("DataPoint", "Filter fetch by DataPoint. Support coma separated list.")]
[Help("Context", "Filter fetch by Context. Support coma separated list.")]
public override ExitCode Execute()
{
string CSVFile = ParseParamValue("CSVFile=", "");
string Config = ParseParamValue("TelemetryConfig=", "UETelemetryStaging");
string DatabaseConfigPath = ParseParamValue("DatabaseConfigPath=", "");
string ProjectString = ParseParamValue("Project=", "");
string PlatformString = ParseParamValue("Platform=", "");
string BranchString = ParseParamValue("Branch=", "Unknown");
string RoleString = ParseParamValue("Role=", "Editor");
string ConfigurationString = ParseParamValue("Configuration=", "Development");
string Since = ParseParamValue("Since=", "1month");
string TestName = ParseParamValue("TestName=", "");
string DataPoint = ParseParamValue("DataPoint=", "");
string Context = ParseParamValue("Context=", "");
if (string.IsNullOrEmpty(CSVFile))
{
throw new AutomationException("CSVFile argument is missing.");
}
UnrealTelemetryContext Conditions = new UnrealTelemetryContext();
if (string.IsNullOrEmpty(ProjectString))
{
throw new AutomationException("No project specified. Use -Project=ShooterGame etc");
}
Conditions.SetProperty("ProjectName", ProjectString);
object Role;
if (!Enum.TryParse(typeof(UnrealTargetRole), RoleString, true, out Role))
{
string AllKeys = string.Join(", ", Enum.GetNames(typeof(UnrealTargetRole)));
throw new AutomationException(string.Format("Unknown Role '{0}', it must be one of the values: {1}.", RoleString, AllKeys));
}
RoleString = ((UnrealTargetRole)Role).ToString();
object Configuration;
if (!Enum.TryParse(typeof(UnrealTargetConfiguration), ConfigurationString, true, out Configuration))
{
string AllKeys = string.Join(", ", Enum.GetNames(typeof(UnrealTargetConfiguration)));
throw new AutomationException(string.Format("Unknown Configuration '{0}', it must be one of the values: {1}.", ConfigurationString, AllKeys));
}
ConfigurationString = ((UnrealTargetConfiguration)Configuration).ToString();
Conditions.SetProperty("Configuration", string.Format("{0} {1}", RoleString, ConfigurationString));
UnrealTargetPlatform Platform = string.IsNullOrEmpty(PlatformString) ? BuildHostPlatform.Current.Platform : UnrealTargetPlatform.Parse(PlatformString);
Conditions.SetProperty("Platform", Platform.ToString());
Conditions.SetProperty("Branch", BranchString);
if (!string.IsNullOrEmpty(TestName))
{
Conditions.SetProperty("TestName", TestName);
}
if (!string.IsNullOrEmpty(DataPoint))
{
Conditions.SetProperty("DataPoint", DataPoint);
}
if (!string.IsNullOrEmpty(Context))
{
Conditions.SetProperty("Context", Context);
}
Conditions.SetProperty("ChangelistDateTime", SinceStringToDate(Since));
IDatabaseConfig DBConfig = DatabaseConfigManager.GetConfigByName(Config);
if (DBConfig != null)
{
DBConfig.LoadConfig(DatabaseConfigPath);
IDatabaseDriver DB = DBConfig.GetDriver();
var Results = DB.FetchData(Conditions);
if(Results is null || Results.Tables.Count == 0)
{
throw new AutomationException("No result returned from the DB with the requested conditions.");
}
Log.Info("Successfully retrived {0} rows from the last {1}.", Results.Tables[0].Rows.Count, Since);
UnrealAutomationTelemetry.WriteDataTableToCSV(Results.Tables[0], CSVFile);
}
return 0;
}
private DateTime SinceStringToDate(string SinceString)
{
Regex Regx = new Regex(@"([0-9]+)\s*([dwmy])", RegexOptions.Compiled | RegexOptions.IgnoreCase);
Match DateMatch = Regx.Match(SinceString);
DateTime SinceDate = DateTime.Now;
if (DateMatch.Success)
{
string NumberString = DateMatch.Groups[1].Value;
string TimeSpanString = DateMatch.Groups[2].Value.Substring(0, 1).ToLower();
int Factor = Int32.Parse(NumberString);
switch(TimeSpanString)
{
case "d":
SinceDate = SinceDate.AddDays(-Factor);
break;
case "w":
SinceDate = SinceDate.AddDays(-Factor * 7);
break;
case "m":
SinceDate = SinceDate.AddMonths(-Factor);
break;
case "y":
SinceDate = SinceDate.AddYears(-Factor);
break;
}
}
return SinceDate;
}
}
}