// Copyright Epic Games, Inc. All Rights Reserved.
using Gauntlet;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnrealBuildTool;
using Log = Gauntlet.Log;
namespace UE
{
///
/// Implements the functionality of testing the cooking process in the editor
///
public class CookByTheBookEditor : UnrealTestNode
{
private const string CookingIsStartedKey = "The cooking process is started";
private const string CookingStartedPattern = "CookSettings for Memory";
private const string CookingInProgressPattern = @"Cooked\s+packages\s+\d+\s+Packages\s+Remain\s+\d+\s+Total\s+\d+";
private const string CookingCompletePattern = "Cook by the book total time in tick";
private string[] CookErrors;
protected const string CookingCompleteKey = "The cooking process is complete";
protected const string CookingInProgressKey = "The cooking process is in progress";
protected const string CookingContentIsPlacedCorrectlyKey = "The cooking content is placed correctly";
protected string BaseEditorCommandLine = @" -run=cook -log -ini:Game:[/Script/UnrealEd.ProjectPackagingSettings]:bUseZenStore=false";
protected UnrealLogStreamParser EditorLogParser;
protected Checker Checker;
public CookByTheBookEditor(UnrealTestContext InContext) : base(InContext)
{
CleanContentDir();
}
public override bool StartTest(int Pass, int InNumPasses)
{
bool IsStarted = base.StartTest(Pass, InNumPasses);
InitTest();
return IsStarted;
}
public override UnrealTestConfiguration GetConfiguration()
{
UnrealTestConfiguration Config = base.GetConfiguration();
Config.AllRolesExit = true;
SetEditorRole(Config);
return Config;
}
public override void TickTest()
{
EditorLogParser.ReadStream();
CookErrors = FindCookErrors();
if (CookErrors.Any())
{
Log.Error("There are cook errors. Ending test");
CompleteTest(TestResult.Failed);
}
if (Checker.PerformValidations())
{
Log.Info("The cooking process was successful, the cooking content is correct. Ending test");
CompleteTest(TestResult.Passed);
}
base.TickTest();
}
protected virtual void InitTest()
{
CookErrors = [];
Checker = new Checker();
Checker.AddValidation(CookingIsStartedKey, IsCookingStarted);
Checker.AddValidation(CookingInProgressKey, IsCookingInProgress);
Checker.AddValidation(CookingCompleteKey, IsCookingComplete);
Checker.AddValidation(CookingContentIsPlacedCorrectlyKey, IsCookedContentPlacedCorrectly);
InitEditorLogStreamParser();
}
protected override UnrealProcessResult GetExitCodeAndReason(StopReason InReason, UnrealLog InLog, UnrealRoleArtifacts InArtifacts,
out string ExitReason, out int ExitCode)
{
if (InArtifacts.SessionRole.RoleType == UnrealTargetRole.Editor)
{
InLog.EngineInitializedPattern = CookingStartedPattern;
}
string ErrorDescription = GetExitReason();
if (!string.IsNullOrEmpty(ErrorDescription))
{
ExitCode = -1;
ExitReason = ErrorDescription;
return UnrealProcessResult.TestFailure;
}
return base.GetExitCodeAndReason(InReason, InLog, InArtifacts, out ExitReason, out ExitCode);
}
protected virtual void SetEditorRole(UnrealTestConfiguration Config)
{
string TargetPlatformName = GetTargetPlatformName();
UnrealTestRole EditorRole = Config.RequireRole(UnrealTargetRole.Editor);
EditorRole.CommandLine += $@" {BaseEditorCommandLine} -targetplatform={TargetPlatformName}";
}
private void InitEditorLogStreamParser()
{
if (GetRunningEditor() is not { } EditorApp)
{
return;
}
ILogStreamReader LogStreamReader = EditorApp.GetLogBufferReader();
EditorLogParser = new UnrealLogStreamParser(LogStreamReader);
}
private bool IsCookingStarted()
{
return EditorLogParser.GetLogLinesContaining(CookingStartedPattern).Any();
}
private bool IsCookingInProgress()
{
return EditorLogParser.GetLogLinesMatchingPattern(CookingInProgressPattern).Any();
}
protected virtual bool IsCookingComplete()
{
return EditorLogParser.GetLogLinesContaining(CookingCompletePattern).Any();
}
protected virtual bool IsCookedContentPlacedCorrectly()
{
if (!Checker.HasValidated(CookingCompleteKey))
{
return false;
}
ContentFolder ActualCookedContent = GetActualCookedContent();
ContentFolder ExpectedCookedContent = GetExpectedCookedContent();
ContentFolderEqualityComparer Comparer = new ContentFolderEqualityComparer();
bool IsCookedContentPlacedCorrectly = Comparer.Equals(ExpectedCookedContent, ActualCookedContent);
Log.Info(ActualCookedContent.ToString());
if (!IsCookedContentPlacedCorrectly)
{
Log.Error("The cooked content is not valid");
CompleteTest(TestResult.Failed);
}
return IsCookedContentPlacedCorrectly;
}
protected ContentFolder GetActualCookedContent()
{
string SavedCookedPlatformPath = GetSavedCookedPlatformPath();
ContentFolder CookedContent = ContentFolderReader.ReadFolderStructure(SavedCookedPlatformPath, 1);
return CookedContent;
}
protected virtual ContentFolder GetExpectedCookedContent()
{
string TargetPlatformName = GetTargetPlatformName();
ContentFolder CookedContent = new ContentFolder(TargetPlatformName)
{
SubFolders =
[
new ContentFolder("Engine")
{
SubFolders = new List
{
new("Content"),
new("Plugins")
},
Files = ["GlobalShaderCache-PCD3D_SM5.bin", "GlobalShaderCache-VULKAN_SM6.bin", "GlobalShaderCache-PCD3D_SM6.bin"]
},
new ContentFolder(Context.Options.Project)
{
SubFolders = new List
{
new("Content"),
new("Metadata"),
new("Plugins")
},
Files = new List{ "AssetRegistry.bin" }
}
]
};
return CookedContent;
}
private string[] FindCookErrors()
{
string[] CookErrors = EditorLogParser
.GetEventsFromChannels(["LogCook"])
.Where(E => E.Level == UnrealLog.LogLevel.Error)
.Select(E => E.Message)
.ToArray();
return CookErrors;
}
protected void CleanContentDir()
{
string SavedCookedPath = GetSavedCookedPath();
if (!Directory.Exists(SavedCookedPath))
{
return;
}
Directory.Delete(SavedCookedPath, true);
}
private string GetExitReason()
{
StringBuilder ErrorBuilder = new StringBuilder();
if (CookErrors.Any())
{
ErrorBuilder.Append($"There are cook errors: {string.Join("\n", CookErrors)}");
}
string[] FailedEvents = GetFailedValidations();
if (FailedEvents.Any())
{
ErrorBuilder.Append($"The expected events in the logs have not occurred: {string.Join("\n", FailedEvents)}");
}
return ErrorBuilder.ToString();
}
private string[] GetFailedValidations()
{
return Checker.Validations.Values
.Where(I => !I.IsValidated)
.Select(I => I.ActionKey)
.ToArray();
}
protected string GetSavedPath()
{
string ProjectPath = GetProjectPath();
string SavedPath = Path.Combine(ProjectPath, "Saved");
return SavedPath;
}
protected string GetProjectPath()
{
string NormalizedProjectFilePath = Context.Options.ProjectPath.ToNormalizedPath();
string ProjectPath = Path.GetDirectoryName(NormalizedProjectFilePath) ?? string.Empty;
return ProjectPath;
}
protected string GetSavedCookedPlatformPath()
{
string SavedCookedPath = GetSavedCookedPath();
string TargetPlatformName = GetTargetPlatformName();
string SavedCookedPlatformPath = Path.Combine(SavedCookedPath, TargetPlatformName);
return SavedCookedPlatformPath;
}
private string GetSavedCookedPath()
{
string SavedPath = GetSavedPath();
string SavedCookedPath = Path.Combine(SavedPath, "Cooked");
return SavedCookedPath;
}
private UnrealTargetPlatform GetTargetPlatform()
{
UnrealTargetPlatform TargetPlatform = Context.GetRoleContext(UnrealTargetRole.Editor).Platform;
return TargetPlatform;
}
protected string GetTargetPlatformName()
{
UnrealTargetPlatform TargetPlatform = GetTargetPlatform();
string TargetPlatformName = UnrealHelpers.GetPlatformName(TargetPlatform, UnrealTargetRole.Editor, false);
return TargetPlatformName;
}
protected void CompleteTest(TestResult TestResult)
{
MarkTestComplete();
SetUnrealTestResult(TestResult);
}
protected IAppInstance GetRunningEditor()
{
return UnrealApp
.SessionInstance
?.RunningRoles
?.FirstOrDefault(R => R.Role.RoleType.IsEditor() && !R.AppInstance.WasKilled)
?.AppInstance;
}
}
}