// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using EpicGames.Core; using UnrealBuildBase; namespace UnrealBuildTool { /// /// ModuleRules extension for low level tests. /// public class TestModuleRules : ModuleRules { private readonly XNamespace BuildGraphNamespace = XNamespace.Get("http://www.epicgames.com/BuildGraph"); private readonly XNamespace SchemaInstance = XNamespace.Get("http://www.w3.org/2001/XMLSchema-instance"); private readonly XNamespace SchemaLocation = XNamespace.Get("http://www.epicgames.com/BuildGraph ../../Build/Graph/Schema.xsd"); private bool bUsesCatch2 = true; /// /// Associated tested module of this test module. /// public ModuleRules? TestedModule { get; private set; } /// /// Constructs a TestModuleRules object as its own test module. /// /// public TestModuleRules(ReadOnlyTargetRules Target) : base(Target) { SetupCommonProperties(Target); } /// /// Constructs a TestModuleRules object as its own test module. /// Sets value of bUsesCatch2. /// public TestModuleRules(ReadOnlyTargetRules Target, bool InUsesCatch2) : base(Target) { bUsesCatch2 = InUsesCatch2; if (bUsesCatch2) { SetupCommonProperties(Target); } } /// /// Constructs a TestModuleRules object with an associated tested module. /// public TestModuleRules(ModuleRules TestedModule) : base(TestedModule.Target) { this.TestedModule = TestedModule; Name = TestedModule.Name + "Tests"; if (!String.IsNullOrEmpty(TestedModule.ShortName)) { ShortName = TestedModule.ShortName + "Tests"; } File = TestedModule.File; Directory = DirectoryReference.Combine(TestedModule.Directory, "Tests"); Context = TestedModule.Context; PrivateDependencyModuleNames.AddRange(TestedModule.PrivateDependencyModuleNames); PublicDependencyModuleNames.AddRange(TestedModule.PublicDependencyModuleNames); // Tests can refer to tested module's Public and Private paths string ModulePublicDir = Path.Combine(TestedModule.ModuleDirectory, "Public"); if (System.IO.Directory.Exists(ModulePublicDir)) { PublicIncludePaths.Add(ModulePublicDir); } string ModulePrivateDir = Path.Combine(TestedModule.ModuleDirectory, "Private"); if (System.IO.Directory.Exists(ModulePrivateDir)) { PrivateIncludePaths.Add(ModulePrivateDir); } SetupCommonProperties(Target); } private void SetupCommonProperties(ReadOnlyTargetRules Target) { bIsTestModuleOverride = true; PCHUsage = PCHUsageMode.NoPCHs; PrecompileForTargets = PrecompileTargetsType.None; if (Target.Configuration == UnrealTargetConfiguration.Debug && Target.Platform == UnrealTargetPlatform.Linux) { OptimizeCode = CodeOptimization.Never; } bAllowConfidentialPlatformDefines = true; bLegalToDistributeObjectCode = true; // Required false for catch.hpp bUseUnity = false; // Disable exception handling so that tests can assert for exceptions bEnableObjCExceptions = false; bEnableExceptions = false; SetResourcesFolder("Resources"); if (!PublicDependencyModuleNames.Contains("Catch2")) { PublicDependencyModuleNames.Add("Catch2"); } if (!PrivateDependencyModuleNames.Contains("LowLevelTestsRunner")) { PrivateDependencyModuleNames.Add("LowLevelTestsRunner"); } // Platforms specific setup if (Target.Platform == UnrealTargetPlatform.Android) { PublicDefinitions.Add("CATCH_CONFIG_NOSTDOUT"); } if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.TVOS) { // Fix missing frameworks from ApplicationCore // Needed for CADisplayLink PublicFrameworks.Add("QuartzCore"); // Needed for MTLCreateSystemDefaultDevice PublicWeakFrameworks.Add("Metal"); } } /// /// Set test-specific resources folder relative to module directory. /// This will be copied to the binaries path during deployment. /// protected void SetResourcesFolder(string ResourcesRelativeFolder) { AdditionalPropertiesForReceipt.Inner.RemoveAll(Prop => Prop.Name == "ResourcesFolder"); foreach (DirectoryReference Directory in GetAllModuleDirectories()) { string TestResourcesDir = Path.Combine(Directory.FullName, ResourcesRelativeFolder); if (System.IO.Directory.Exists(TestResourcesDir)) { AdditionalPropertiesForReceipt.Add("ResourcesFolder", TestResourcesDir); } } } #pragma warning disable 8602 #pragma warning disable 8604 /// /// Generates or updates metadata file for LowLevelTests.xml containing test flags: name, short name, target name, relative binaries path, supported platforms etc. /// The test metadata specifying name, short name etc used to populate the BuildGraph properties file. /// protected void UpdateBuildGraphPropertiesFile(Metadata TestMetadata) { bool bUpdateBuildGraphPropertiesFile = false; TestTargetRules? TestTargetRules = Target.InnerTestTargetRules; if (TestTargetRules != null) { bUpdateBuildGraphPropertiesFile = TestTargetRules.bUpdateBuildGraphPropertiesFile; } bool bIsBuildMachine = Unreal.IsBuildMachine(); if (bIsBuildMachine || !bUpdateBuildGraphPropertiesFile || TestMetadata == null) { return; } string BaseFolder = GetBaseFolder(); string GeneratedPropertiesScriptFile; string NonPublicPath = Path.Combine(BaseFolder, "Restricted", "NotForLicensees", "Build", "LowLevelTests", $"{TestMetadata.TestName}.xml"); bool ModuleInRestrictedPath = IsRestrictedPath(ModuleDirectory); if (ModuleInRestrictedPath) { GeneratedPropertiesScriptFile = NonPublicPath; } else { GeneratedPropertiesScriptFile = Path.Combine(BaseFolder, "Build", "LowLevelTests", $"{TestMetadata.TestName}.xml"); } if (!System.IO.File.Exists(GeneratedPropertiesScriptFile)) { string? DirGenProps = Path.GetDirectoryName(GeneratedPropertiesScriptFile); if (DirGenProps != null && !System.IO.Directory.Exists(DirGenProps)) { System.IO.Directory.CreateDirectory(DirGenProps); } using (FileStream FileStream = System.IO.File.Create(GeneratedPropertiesScriptFile)) { XDocument XInitFile = new XDocument(new XElement(BuildGraphNamespace + "BuildGraph", new XAttribute(XNamespace.Xmlns + "xsi", SchemaInstance), new XAttribute(SchemaInstance + "schemaLocation", SchemaLocation))); XInitFile.Root?.Add( new XElement( BuildGraphNamespace + "Property", new XAttribute("Name", "TestNames"), new XAttribute("Value", "$(TestNames);" + TestMetadata.TestName))); XInitFile.Save(FileStream); } } // All relevant properties string TestTargetName = Target.LaunchModuleName ?? "Launch"; string TestBinariesPath = TryGetBinariesPath(); // Do not save full paths if (Path.IsPathRooted(TestBinariesPath)) { TestBinariesPath = Path.GetRelativePath(Unreal.RootDirectory.FullName, TestBinariesPath); } MakeFileWriteable(GeneratedPropertiesScriptFile); XDocument GenPropsDoc = XDocument.Load(GeneratedPropertiesScriptFile); XElement? Root = GenPropsDoc.Root; // First descendant must be TestNames if (Root != null && Root.FirstNode != null) { XElement TestNames = (XElement)Root.FirstNode; if (TestNames != null) { XElement lastUpdatedNode = TestNames; InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "Disabled", Convert.ToString(TestMetadata.Disabled)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "Short", Convert.ToString(TestMetadata.TestShortName)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "StagesWithProjectFile", Convert.ToString(TestMetadata.StagesWithProjectFile)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "Target", Convert.ToString(TestTargetName)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "BinariesRelative", Convert.ToString(TestBinariesPath)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "ReportType", Convert.ToString(TestMetadata.ReportType)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "GauntletArgs", Convert.ToString(TestMetadata.InitialExtraArgs) + Convert.ToString(TestMetadata.GauntletArgs)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "ExtraArgs", Convert.ToString(TestMetadata.ExtraArgs)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "HasAfterSteps", Convert.ToString(TestMetadata.HasAfterSteps)); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "UsesCatch2", Convert.ToString(TestMetadata.UsesCatch2)); InsertOrUpdateTestOption(ref lastUpdatedNode, TestMetadata.TestName, $"Run {TestMetadata.TestShortName} Tests", "Run", "Tests", false.ToString()); InsertOrUpdateTestFlagProperty(ref lastUpdatedNode, TestMetadata.TestName, "SupportedPlatforms", TestMetadata.SupportedPlatforms.Aggregate("", (current, next) => (String.IsNullOrEmpty(current) ? next.ToString() : current + ";" + next.ToString()))); } } GenPropsDoc.Save(GeneratedPropertiesScriptFile); // Platform-specific configurations string GeneratedPropertiesPlatformFile; string NonPublicPathPlatform; // Generate peroperty file for each platform foreach (UnrealTargetPlatform ValidPlatform in UnrealTargetPlatform.GetValidPlatforms()) { bool IsRestrictedPlatformName = IsPlatformRestricted(ValidPlatform); if (IsRestrictedPlatformName) { NonPublicPathPlatform = Path.Combine(BaseFolder, "Restricted", "NotForLicensees", "Platforms", ValidPlatform.ToString(), "Build", "LowLevelTests", $"{TestMetadata.TestName}.xml"); } else { NonPublicPathPlatform = Path.Combine(BaseFolder, "Restricted", "NotForLicensees", "Build", "LowLevelTests", $"{TestMetadata.TestName}.xml"); } if (ModuleInRestrictedPath) { GeneratedPropertiesPlatformFile = NonPublicPathPlatform; } else { if (IsRestrictedPlatformName) { GeneratedPropertiesPlatformFile = Path.Combine(BaseFolder, "Platforms", ValidPlatform.ToString(), "Build", "LowLevelTests", $"{TestMetadata.TestName}.xml"); } else { GeneratedPropertiesPlatformFile = Path.Combine(BaseFolder, "Build", "LowLevelTests", $"{TestMetadata.TestName}.xml"); } } if (!System.IO.File.Exists(GeneratedPropertiesPlatformFile)) { string? DirGenPropsPlatforms = Path.GetDirectoryName(GeneratedPropertiesPlatformFile); if (DirGenPropsPlatforms != null && !System.IO.Directory.Exists(DirGenPropsPlatforms)) { System.IO.Directory.CreateDirectory(DirGenPropsPlatforms); } using (FileStream FileStream = System.IO.File.Create(GeneratedPropertiesPlatformFile)) { new XDocument(new XElement(BuildGraphNamespace + "BuildGraph", new XAttribute(XNamespace.Xmlns + "xsi", SchemaInstance), new XAttribute(SchemaInstance + "schemaLocation", SchemaLocation))).Save(FileStream); } } MakeFileWriteable(GeneratedPropertiesPlatformFile); XDocument XInitPlatformFile = XDocument.Load(GeneratedPropertiesPlatformFile); // Adding per-test and per-platform tags string TagsValue = TestMetadata.PlatformTags.ContainsKey(ValidPlatform) ? TestMetadata.PlatformTags[ValidPlatform] : String.Empty; AppendOrUpdateTestFlagProperty(ref XInitPlatformFile, TestMetadata.TestName, ValidPlatform.ToString(), "Tags", TagsValue); string ExtraCompilationArgsValue = TestMetadata.PlatformCompilationExtraArgs.ContainsKey(ValidPlatform) ? TestMetadata.PlatformCompilationExtraArgs[ValidPlatform] : String.Empty; AppendOrUpdateTestFlagProperty(ref XInitPlatformFile, TestMetadata.TestName, ValidPlatform.ToString(), "ExtraCompilationArgs", ExtraCompilationArgsValue); string RunSupportedValue = TestMetadata.PlatformsRunUnsupported.Contains(ValidPlatform) ? "False" : "True"; AppendOrUpdateTestFlagProperty(ref XInitPlatformFile, TestMetadata.TestName, ValidPlatform.ToString(), "RunSupported", RunSupportedValue); string RunContainerizedValue = TestMetadata.PlatformRunContainerized.ContainsKey(ValidPlatform) ? "True" : "False"; AppendOrUpdateTestFlagProperty(ref XInitPlatformFile, TestMetadata.TestName, ValidPlatform.ToString(), "RunContainerized", RunContainerizedValue); XInitPlatformFile.Save(GeneratedPropertiesPlatformFile); } } private string GetBaseFolder() { string RelativeModulePath = Path.GetRelativePath(Unreal.RootDirectory.FullName, ModuleDirectory); string[] BreadCrumbs = RelativeModulePath.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); if (BreadCrumbs.Length > 0) { return Path.Combine(Unreal.RootDirectory.FullName, BreadCrumbs[0]); } return Unreal.EngineDirectory.FullName; } private bool IsPlatformRestricted(UnrealTargetPlatform Platform) { return RestrictedFolder.GetNames().Contains(Platform.ToString()); } private bool IsRestrictedPath(string ModuleDirectory) { foreach (string RestrictedFolderName in RestrictedFolder.GetNames()) { if (ModuleDirectory.Contains(RestrictedFolderName)) { return true; } } return false; } private string TryGetBinariesPath() { int SourceFolderIndex = ModuleDirectory.IndexOf("Source"); if (SourceFolderIndex < 0) { int PluginFolderIndex = ModuleDirectory.IndexOf("Plugins"); if (PluginFolderIndex >= 0) { return ModuleDirectory.Substring(0, PluginFolderIndex) + "Binaries"; } throw new Exception("Could not detect source folder path for module " + GetType()); } return ModuleDirectory.Substring(0, SourceFolderIndex) + "Binaries"; } private void AppendOrUpdateTestFlagProperty(ref XDocument Document, string FlagRadix, string FlagPrefix, string FlagSuffix, string FlagValue) { XElement? Existing = Document.Root!.Elements().Where(element => element.Attribute("Name").Value == FlagPrefix + FlagRadix + FlagSuffix).FirstOrDefault(); if (Existing != null) { Existing!.SetAttributeValue("Value", FlagValue); } else { XElement ElementAppend = new XElement(BuildGraphNamespace + "Property"); ElementAppend.SetAttributeValue("Name", FlagPrefix + FlagRadix + FlagSuffix); ElementAppend.SetAttributeValue("Value", FlagValue); Document.Root!.Add(ElementAppend); } } private void InsertOrUpdateTestFlagProperty(ref XElement ElementUpsertAfter, string TestName, string FlagSuffix, string FlagValue) { IEnumerable NextChunk = ElementUpsertAfter.ElementsAfterSelf(BuildGraphNamespace + "Property") .Where(prop => prop.Attribute("Name").Value.EndsWith(FlagSuffix)); if (!NextChunk .Where(prop => prop.Attribute("Name").Value == TestName + FlagSuffix).Any()) { XElement ElementInsert = new XElement(BuildGraphNamespace + "Property"); ElementInsert.SetAttributeValue("Name", TestName + FlagSuffix); ElementInsert.SetAttributeValue("Value", FlagValue); ElementUpsertAfter.AddAfterSelf(ElementInsert); } else { NextChunk .Where(prop => prop.Attribute("Name").Value == TestName + FlagSuffix).First().SetAttributeValue("Value", FlagValue); } ElementUpsertAfter = NextChunk.Last(); } private void InsertOrUpdateTestOption(ref XElement ElementUpsertAfter, string OptionRadix, string Description, string OptionPrefix, string OptionSuffix, string DefaultValue) { IEnumerable NextChunk = ElementUpsertAfter.ElementsAfterSelf(BuildGraphNamespace + "Option") .Where(prop => prop.Attribute("Name").Value.StartsWith(OptionPrefix) && prop.Attribute("Name").Value.EndsWith(OptionSuffix)); if (!NextChunk .Where(prop => prop.Attribute("Name").Value == OptionPrefix + OptionRadix + OptionSuffix).Any()) { XElement ElementInsert = new XElement(BuildGraphNamespace + "Option"); ElementInsert.SetAttributeValue("Name", OptionPrefix + OptionRadix + OptionSuffix); ElementInsert.SetAttributeValue("DefaultValue", DefaultValue); ElementInsert.SetAttributeValue("Description", Description); ElementUpsertAfter.AddAfterSelf(ElementInsert); } else { XElement ElementUpdate = NextChunk .Where(prop => prop.Attribute("Name").Value == OptionPrefix + OptionRadix + OptionSuffix).First(); ElementUpdate.SetAttributeValue("Description", Description); ElementUpdate.SetAttributeValue("DefaultValue", DefaultValue); } ElementUpsertAfter = NextChunk.Last(); } #pragma warning restore 8604 #pragma warning restore 8602 private void MakeFileWriteable(string InFilePath) { System.IO.File.SetAttributes(InFilePath, System.IO.File.GetAttributes(InFilePath) & ~FileAttributes.ReadOnly); } #pragma warning disable 8618 /// /// Test metadata class. /// public class Metadata { /// /// Test long name. /// public string TestName { get; set; } /// /// Test short name used for display in build system. /// public string TestShortName { get; set; } private string ReportTypePrivate = "console"; /// /// Type of Catch2 report, defaults to console. /// public string ReportType { get => ReportTypePrivate; set => ReportTypePrivate = value; } /// /// Does this test use project files for staging additional files /// and cause the build to use BuildCookRun instead of a Compile step /// public bool StagesWithProjectFile { get; set; } /// /// Is this test disabled? /// public bool Disabled { get; set; } /// /// Depercated, use GauntletArgs or ExtraArgs instead to help indicate arguments to launch the test under. /// public string InitialExtraArgs { get; [Obsolete] set; } /// /// Any initial Gauntlet args to be passed to the test executable /// public string GauntletArgs { get; set; } /// /// Any extra args to be passed to the test executable as --extra-args /// public string ExtraArgs { get; set; } /// /// Whether there's a step that gets executed after the tests have finished. /// Typically used for cleanup of resources. /// public bool HasAfterSteps { get; set; } private bool UsesCatch2Private = true; /// /// Test built with a frakework other than Catch2 /// public bool UsesCatch2 { get => UsesCatch2Private; set => UsesCatch2Private = value; } /// /// Set of supported platforms. /// public HashSet SupportedPlatforms { get; set; } = new HashSet() { UnrealTargetPlatform.Win64 }; private Dictionary PlatformTagsPrivate = new Dictionary(); /// /// Per-platform tags. /// public Dictionary PlatformTags { get => PlatformTagsPrivate; set => PlatformTagsPrivate = value; } private Dictionary PlatformCompilationExtraArgsPrivate = new Dictionary(); /// /// Per-platform extra compilation arguments. /// public Dictionary PlatformCompilationExtraArgs { get => PlatformCompilationExtraArgsPrivate; set => PlatformCompilationExtraArgsPrivate = value; } private List PlatformsRunUnsupportedPrivate = new List() { UnrealTargetPlatform.Android, UnrealTargetPlatform.IOS, UnrealTargetPlatform.TVOS, UnrealTargetPlatform.VisionOS }; /// /// List of platforms that cannot run tests. /// public List PlatformsRunUnsupported { get => PlatformsRunUnsupportedPrivate; set => PlatformsRunUnsupportedPrivate = value; } private Dictionary PlatformRunContainerizedPrivate = new Dictionary(); /// /// Whether or not the test is run inside a Docker container for a given platform. /// public Dictionary PlatformRunContainerized { get { return PlatformRunContainerizedPrivate; } set { PlatformRunContainerizedPrivate = value; } } } #pragma warning restore 8618 } }