You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Update PluginDescriptor to have parity with FPluginDescriptor in terms of handling custom JSON fields. The order in which the original fields are written to disk is preserved. When saving the PluginDescriptor to disk, the same order is followed and any new entries are added to the end of the plugin file. Note: PluginDescriptor.Save and PluginDescriptor.Save2 will exist concurrently for now. There are still parts of UBT that use PluginDescriptor.Write and the refactor to create a unified Save function will come later as a lot of testing still needs to be done. JsonObject: - Introduced AddOrSetFieldValue() methods overloaded on value types to add/set fields in JsonObject. - Changed the underlying data structure of JsonObject from Dictionary<string,Object?> to OrderedDictionary to guarantee that order of all .uplugin fields (default and custom) are preserved - Made the JsonObject constructor that takes an OrderedDictionary private to ensure type safety of values passed to JsonObject - Introduced ToJsonString() for JsonObject that serializes the JsonObject to match the formatting of .uplugin IMPORTANT: The ToJsonString() method uses the JavaScriptEncoder.UnsafeRelaxedJsonEscaping encoder for Utf8JsonWriter. This allows +,<,>,&,` etc characters to be written properly for .uplugins. As per MS docs, with this encoding, none of the contents should be written to HTML or a script. - Introduced JsonObjectTests that unit tested JsonObject to ensure previous behavior was maintained. - Ensured that keys for the JsonObject are processed in a case insensitive and culture independent way. PluginDescriptor - Added a JsonObject field that acts as the cache of all fields that were read from a file or parsed from a json string. - Cached JsonObject records the order of all fields that were read and captures custom fields in the .uplugin - Introduced Save2(), the new save algorithm that writes all fields (default and custom) to disk. - Deleted the private default constructor because it didn't seem to be used anywhere. - Introduced PluginDescriptorTests to ensure previous behavior is maintained. In particular when dealing with a .uplugin with only default fields, results of Save() and Save2() are the same. Descriptor Files: - Introduced ToJsonObject() methods to replace the previous JsonWriter patterns of writing the descriptor data. - Introduced the UpdateJsonObject pattern which mimics the old WriteArray pattern of writing data to a JsonWriter. #rb: Mark.Winter, Joe.Kirchoff #jira: none #test: Developed this in a TDD style with unit tests for all the major functions. Also reviewed output of created .uplugins and diffed them. [CL 26920712 by leon huang in ue5-main branch]
287 lines
8.5 KiB
C#
287 lines
8.5 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using EpicGames.Core;
|
|
using UnrealBuildTool;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
namespace UnrealBuildToolTests
|
|
{
|
|
[TestClass]
|
|
public class PluginDescriptorTests
|
|
{
|
|
private static DirectoryReference s_tempDirectory;
|
|
|
|
private static DirectoryReference CreateTempDir()
|
|
{
|
|
string tempDir = Path.Join(Path.GetTempPath(), "epicgames-core-tests-" + Guid.NewGuid().ToString()[..8]);
|
|
Directory.CreateDirectory(tempDir);
|
|
return new DirectoryReference(tempDir);
|
|
}
|
|
|
|
[ClassInitialize]
|
|
public static void Setup(TestContext testContext)
|
|
{
|
|
PluginDescriptorTests.s_tempDirectory = CreateTempDir();
|
|
}
|
|
|
|
[ClassCleanup]
|
|
public static void TearDown()
|
|
{
|
|
if (Directory.Exists(PluginDescriptorTests.s_tempDirectory.FullName))
|
|
{
|
|
Directory.Delete(PluginDescriptorTests.s_tempDirectory.FullName, true);
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ReadPluginCustomFields()
|
|
{
|
|
string json = CreateUPluginWithCustomFields();
|
|
string filename = "CustomFields.uplugin";
|
|
FileReference fileToWrite = CreateTempUPluginFile(filename, json);
|
|
PluginDescriptor.FromFile(fileToWrite);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ReadUpluginMissingFileVersion()
|
|
{
|
|
string json = CreateUpluginWithMissingVersion();
|
|
string filename = "MissingFileVersionField.uplugin";
|
|
FileReference fileToWrite = CreateTempUPluginFile(filename, json);
|
|
Assert.ThrowsException<BuildException>(() => PluginDescriptor.FromFile(fileToWrite));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ReadUpluginInvalidVersion()
|
|
{
|
|
string json = CreateUpluginWithInvalidVersion();
|
|
string filename = "InvalidFileVersionField.uplugin";
|
|
FileReference fileToWrite = CreateTempUPluginFile(filename, json);
|
|
Assert.ThrowsException<BuildException>(() => PluginDescriptor.FromFile(fileToWrite));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Save2CustomFieldsMissingDefaults()
|
|
{
|
|
string json = CreateUPluginWithCustomFields();
|
|
string filename = "CustomFields-Save.uplugin";
|
|
FileReference fileToWrite = CreateTempUPluginFile(filename, json);
|
|
PluginDescriptor descriptor = PluginDescriptor.FromFile(fileToWrite);
|
|
string fileToSave = GetAbsolutePathToTempFile("CustomFields-SaveResults.uplugin");
|
|
descriptor.Save2(fileToSave);
|
|
|
|
string saveContents = File.ReadAllText(fileToSave);
|
|
JsonObject writtenObject = JsonObject.Parse(json);
|
|
JsonObject savedObject = JsonObject.Parse(saveContents);
|
|
IEnumerable<string> writtenObjectKeys = writtenObject.KeyNames;
|
|
// At this point, the written object should contain all the custom fields
|
|
// The saved object should have all the custom fields as well as the missing default fields appended
|
|
// This means we can't do a simple equality check to see if the objects are the same
|
|
// We check to make sure that all of the keys in the written object exist in the saved object as a short hand to check that all the custom fields have been saved and carried over to the saved object
|
|
foreach (string key in writtenObjectKeys)
|
|
{
|
|
Assert.IsTrue(savedObject.ContainsField(key));
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Save2DefaultFields()
|
|
{
|
|
string json = CreateUpluginWithDefaultFields();
|
|
string filename = "DefaultFields-Save.uplugin";
|
|
FileReference fileToWrite = CreateTempUPluginFile(filename, json);
|
|
PluginDescriptor descriptor = PluginDescriptor.FromFile(fileToWrite);
|
|
string fileToSave = GetAbsolutePathToTempFile("DefaultFields-SaveResults.uplugin");
|
|
descriptor.Save2(fileToSave);
|
|
string saveContents = File.ReadAllText(fileToSave);
|
|
|
|
// Nothing should change between the format of the json and what was written to disk
|
|
Assert.AreEqual(json, saveContents);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Save2CompareWithSave()
|
|
{
|
|
string json = CreateUpluginWithDefaultFields();
|
|
string filename = "DefaultFields-Save2.uplugin";
|
|
FileReference fileToWrite = CreateTempUPluginFile(filename, json);
|
|
|
|
PluginDescriptor descriptor = PluginDescriptor.FromFile(fileToWrite);
|
|
string fileToSave = GetAbsolutePathToTempFile("DefaultFields-SaveResults.uplugin");
|
|
string fileToSave2 = GetAbsolutePathToTempFile("DefaultFields-Save2Results.uplugin");
|
|
descriptor.Save2(fileToSave);
|
|
descriptor.Save2(fileToSave2);
|
|
|
|
string saveContents = File.ReadAllText(fileToSave);
|
|
string save2Contents = File.ReadAllText(fileToSave2);
|
|
|
|
// Nothing should change between the format of the json and what was written to disk
|
|
Assert.AreEqual(saveContents, save2Contents);
|
|
}
|
|
|
|
private static string GetAbsolutePathToTempFile(string fileName)
|
|
{
|
|
if (String.IsNullOrEmpty(fileName))
|
|
{
|
|
return "";
|
|
}
|
|
return Path.Join(s_tempDirectory.FullName, fileName);
|
|
}
|
|
|
|
private static FileReference CreateTempUPluginFile(string fileName, string fileContent)
|
|
{
|
|
string extension = Path.GetExtension(fileName);
|
|
Debug.Assert(!String.IsNullOrEmpty(extension));
|
|
Debug.Assert(extension == ".uplugin");
|
|
string inputFile = Path.Join(s_tempDirectory.FullName, fileName);
|
|
FileReference inputFileReference = new FileReference(inputFile);
|
|
File.WriteAllText(inputFile, fileContent);
|
|
return inputFileReference;
|
|
}
|
|
|
|
private static string CreateUPluginWithCustomFields()
|
|
{
|
|
string json = @"
|
|
{
|
|
""FileVersion"": 3,
|
|
""CanContainContent"": true,
|
|
""ExplicitlyLoaded"": true,
|
|
""EnabledByDefault"": false,
|
|
""EditorCustomVirtualPath"": ""MyPlugin"",
|
|
""CustomField1"": ""400.0"",
|
|
""CustomField2"": ""CustomValue2"",
|
|
""CustomField3"": false,
|
|
""Modules"": [
|
|
{
|
|
""Name"": ""Module1"",
|
|
""Type"": ""Runtime"",
|
|
""LoadingPhase"": ""Default""
|
|
},
|
|
{
|
|
""Name"": ""Module2"",
|
|
""Type"": ""ClientOnly"",
|
|
""LoadingPhase"": ""Default""
|
|
}
|
|
],
|
|
""Plugins"": [
|
|
{
|
|
""Name"": ""Plugin1"",
|
|
""Enabled"": true
|
|
},
|
|
{
|
|
""Name"": ""Plugin2"",
|
|
""Enabled"": true
|
|
},
|
|
{
|
|
""Name"": ""Plugin3"",
|
|
""Enabled"": true
|
|
},
|
|
{
|
|
""Name"": ""Plugin4"",
|
|
""Enabled"": true
|
|
},
|
|
{
|
|
""Name"": ""Plugin5"",
|
|
""Enabled"": true
|
|
}
|
|
]
|
|
}";
|
|
|
|
return FixVerbatimStringIdentation(json);
|
|
}
|
|
|
|
private static string CreateUpluginWithDefaultFields()
|
|
{
|
|
string json = @"
|
|
{
|
|
""FileVersion"": 3,
|
|
""Version"": 1,
|
|
""VersionName"": ""1.0"",
|
|
""FriendlyName"": ""Enhanced Input"",
|
|
""Description"": ""Input handling that allows for contextual and dynamic mappings."",
|
|
""Category"": ""Input"",
|
|
""CreatedBy"": ""Epic Games, Inc."",
|
|
""CreatedByURL"": ""https://epicgames.com"",
|
|
""DocsURL"": ""https://docs.unrealengine.com/en-US/enhanced-input-in-unreal-engine/"",
|
|
""MarketplaceURL"": """",
|
|
""SupportURL"": """",
|
|
""EnabledByDefault"": true,
|
|
""CanContainContent"": false,
|
|
""IsBetaVersion"": false,
|
|
""Installed"": false,
|
|
""Modules"": [
|
|
{
|
|
""Name"": ""EnhancedInput"",
|
|
""Type"": ""Runtime"",
|
|
""LoadingPhase"": ""PreDefault""
|
|
},
|
|
{
|
|
""Name"": ""InputBlueprintNodes"",
|
|
""Type"": ""UncookedOnly"",
|
|
""LoadingPhase"": ""Default""
|
|
},
|
|
{
|
|
""Name"": ""InputEditor"",
|
|
""Type"": ""Editor"",
|
|
""LoadingPhase"": ""Default""
|
|
}
|
|
]
|
|
}";
|
|
|
|
return FixVerbatimStringIdentation(json);
|
|
}
|
|
|
|
private static string CreateUpluginWithMissingVersion()
|
|
{
|
|
string json = @"
|
|
{
|
|
""CustomField1"": ""Value1""
|
|
}";
|
|
return FixVerbatimStringIdentation(json);
|
|
}
|
|
|
|
private static string CreateUpluginWithInvalidVersion()
|
|
{
|
|
string json = @"
|
|
{
|
|
""FileVersion"": 4
|
|
}";
|
|
return FixVerbatimStringIdentation(json);
|
|
}
|
|
|
|
private static string FixVerbatimStringIdentation(string verbatimString)
|
|
{
|
|
if (String.IsNullOrEmpty(verbatimString))
|
|
{
|
|
return verbatimString;
|
|
}
|
|
string[] lines = verbatimString.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
|
StringBuilder jsonStringBuilder = new StringBuilder();
|
|
int numberOfTabs = 0;
|
|
bool bFoundTabLevel = false;
|
|
foreach (string line in lines)
|
|
{
|
|
// Should only apply to the first line. Skip
|
|
if (String.IsNullOrEmpty(line))
|
|
{
|
|
continue;
|
|
}
|
|
// Now we're on the first line with the { we're just going to assume that this is the first line
|
|
if (!bFoundTabLevel)
|
|
{
|
|
numberOfTabs= line.TakeWhile(x => x == '\t').Count();
|
|
bFoundTabLevel = true;
|
|
}
|
|
jsonStringBuilder.AppendLine(line.Substring(numberOfTabs));
|
|
}
|
|
return jsonStringBuilder.ToString();
|
|
}
|
|
}
|
|
}
|