From 4dcf1092f50b58011dd8572c20817d4602ff7736 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 8 Oct 2018 22:14:05 -0700 Subject: [PATCH 001/140] Remove unused variable --- ModuleManager/Cats/CatMover.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ModuleManager/Cats/CatMover.cs b/ModuleManager/Cats/CatMover.cs index 91987e22..bf1833a1 100644 --- a/ModuleManager/Cats/CatMover.cs +++ b/ModuleManager/Cats/CatMover.cs @@ -21,8 +21,6 @@ public class CatMover : MonoBehaviour private float time = 5; private float trailTime = 0.5f; - private int frames = 1; - // Use this for initialization void Start() { From 38cbaa10836ae50948e20989dd7354ab6b5f0763 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 8 Oct 2018 22:16:28 -0700 Subject: [PATCH 002/140] Make const stuff const --- ModuleManager/Cats/CatMover.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ModuleManager/Cats/CatMover.cs b/ModuleManager/Cats/CatMover.cs index bf1833a1..f5dd7678 100644 --- a/ModuleManager/Cats/CatMover.cs +++ b/ModuleManager/Cats/CatMover.cs @@ -18,8 +18,8 @@ public class CatMover : MonoBehaviour public float scale = 2; - private float time = 5; - private float trailTime = 0.5f; + private const float time = 5; + private const float trailTime = time / 4; // Use this for initialization void Start() @@ -32,8 +32,6 @@ void Start() offsetY = Mathf.FloorToInt(0.2f * Screen.height); spos.z = -1; - - trailTime = time / 4; totalLenth = (int) (Screen.width / time * trail.time) + 150; trail.time = trailTime; From d6edd62cb52b93e3233d4e8cd204f3f6ff3f1e39 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 8 Oct 2018 22:30:32 -0700 Subject: [PATCH 003/140] Fix typo in test --- ModuleManagerTests/PatchExtractorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 453d56e2..2a56c11a 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -186,14 +186,14 @@ public void TestSortAndExtractPatches() AssertUrlCorrect("@NADE", beforeMod2Configs[3], currentPatches[3]); currentPatches = list.modPasses["mod2"].forPatches; - Assert.Equal(forMod1Configs.Length, currentPatches.Count); + Assert.Equal(forMod2Configs.Length, currentPatches.Count); AssertUrlCorrect("@NODE", forMod2Configs[0], currentPatches[0]); AssertUrlCorrect("@NODE[foo]:HAS[#bar]", forMod2Configs[1], currentPatches[1]); AssertUrlCorrect("@NADE", forMod2Configs[2], currentPatches[2]); AssertUrlCorrect("@NADE", forMod2Configs[3], currentPatches[3]); currentPatches = list.modPasses["mod2"].afterPatches; - Assert.Equal(afterMod1Configs.Length, currentPatches.Count); + Assert.Equal(afterMod2Configs.Length, currentPatches.Count); AssertUrlCorrect("@NODE", afterMod2Configs[0], currentPatches[0]); AssertUrlCorrect("@NODE[foo]:HAS[#bar]", afterMod2Configs[1], currentPatches[1]); AssertUrlCorrect("@NADE", afterMod2Configs[2], currentPatches[2]); From e6746b440f39c04a0c320ca502aeb7bdaabd81a1 Mon Sep 17 00:00:00 2001 From: sarbian Date: Mon, 15 Oct 2018 23:37:31 +0200 Subject: [PATCH 004/140] v3.1.0 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index 233161a6..a27f4b16 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("3.0.7")] +[assembly: AssemblyVersion("3.1.0")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 7ea256db1aa3c2e961d0178307fe9561f7206d0d Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 24 Mar 2018 12:20:04 -0700 Subject: [PATCH 005/140] Make patch stopwatch local It's not needed outside of ProcessPatch(), which contains everything it needs to time --- ModuleManager/MMPatchLoader.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index adcf1999..45e91e8e 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -57,8 +57,6 @@ public class MMPatchLoader : LoadingSystem private bool useCache = false; - private readonly Stopwatch patchSw = new Stopwatch(); - private static readonly List postPatchCallbacks = new List(); private const float yieldInterval = 1f/30f; // Patch at ~30fps @@ -95,12 +93,6 @@ private void Awake() public override bool IsReady() { - //return false; - if (ready) - { - patchSw.Stop(); - logger.Info("Ran in " + ((float)patchSw.ElapsedMilliseconds / 1000).ToString("F3") + "s"); - } return ready; } @@ -113,9 +105,6 @@ public override string ProgressTitle() public override void StartLoad() { - patchSw.Reset(); - patchSw.Start(); - ready = false; // DB check used to track the now fixed TextureReplacer corruption @@ -132,6 +121,9 @@ public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) private IEnumerator ProcessPatch() { + Stopwatch patchSw = new Stopwatch(); + patchSw.Start(); + status = "Checking Cache"; logger.Info(status); yield return null; @@ -390,6 +382,9 @@ float updateTimeRemaining() yield return null; + patchSw.Stop(); + logger.Info("Ran in " + ((float)patchSw.ElapsedMilliseconds / 1000).ToString("F3") + "s"); + ready = true; } From 8056f597afe14208b0ff9dd133cf9b8eb9b7c341 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 24 Mar 2018 12:32:11 -0700 Subject: [PATCH 006/140] Make useCache local Really only needed for a few lines in ProcessPatch() --- ModuleManager/MMPatchLoader.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 45e91e8e..c786220f 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -55,8 +55,6 @@ public class MMPatchLoader : LoadingSystem private string configSha; private Dictionary filesSha = new Dictionary(); - private bool useCache = false; - private static readonly List postPatchCallbacks = new List(); private const float yieldInterval = 1f/30f; // Patch at ~30fps @@ -127,15 +125,15 @@ private IEnumerator ProcessPatch() status = "Checking Cache"; logger.Info(status); yield return null; - + + bool useCache = false; try { - IsCacheUpToDate(); + useCache = IsCacheUpToDate(); } catch (Exception ex) { logger.Exception("Exception in IsCacheUpToDate", ex); - useCache = false; } #if DEBUG @@ -422,7 +420,7 @@ private void SaveModdedPhysics() configs[0].config.Save(physicsPath); } - private void IsCacheUpToDate() + private bool IsCacheUpToDate() { Stopwatch sw = new Stopwatch(); sw.Start(); @@ -471,7 +469,7 @@ private void IsCacheUpToDate() logger.Info("SHA generated in " + ((float)sw.ElapsedMilliseconds / 1000).ToString("F3") + "s"); logger.Info(" SHA = " + configSha); - useCache = false; + bool useCache = false; if (File.Exists(shaPath)) { ConfigNode shaConfigNode = ConfigNode.Load(shaPath); @@ -492,6 +490,7 @@ private void IsCacheUpToDate() logger.Info("useCache = " + useCache); } } + return useCache; } private bool CheckFilesChange(UrlDir.UrlFile[] files, ConfigNode shaConfigNode) From 02bd5817ec393dbecc9cc1eb0143aed2b5389723 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 9 Dec 2017 23:15:59 -0800 Subject: [PATCH 007/140] Make Patch an object Saves having to parse things multiple times, error/info messages can now show the original name (except NEEDS, will be worked on). More functionality will be added to this class in future commits. --- ModuleManager/ModuleManager.csproj | 1 + ModuleManager/Patch.cs | 21 ++ ModuleManager/PatchApplier.cs | 45 ++-- ModuleManager/PatchExtractor.cs | 33 +-- ModuleManager/PatchList.cs | 12 +- ModuleManagerTests/ModuleManagerTests.csproj | 1 + ModuleManagerTests/PatchApplierTest.cs | 231 ++++++------------- ModuleManagerTests/PatchExtractorTest.cs | 142 ++++++------ ModuleManagerTests/PatchTest.cs | 76 ++++++ 9 files changed, 284 insertions(+), 278 deletions(-) create mode 100644 ModuleManager/Patch.cs create mode 100644 ModuleManagerTests/PatchTest.cs diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 61344d57..c0f51ded 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -59,6 +59,7 @@ + diff --git a/ModuleManager/Patch.cs b/ModuleManager/Patch.cs new file mode 100644 index 00000000..ba443205 --- /dev/null +++ b/ModuleManager/Patch.cs @@ -0,0 +1,21 @@ +using System; + +namespace ModuleManager +{ + public class Patch + { + public readonly UrlDir.UrlConfig urlConfig; + public readonly Command command; + public readonly ConfigNode node; + + public Patch(UrlDir.UrlConfig urlConfig, Command command, ConfigNode node) + { + if (command != Command.Edit && command != Command.Copy && command != Command.Delete) + throw new ArgumentException($"Must be Edit, Copy, or Delete (got {command})", nameof(command)); + + this.urlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + this.command = command; + this.node = node ?? throw new ArgumentNullException(nameof(node)); + } + } +} diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index 5e30ad02..3e981653 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -49,31 +49,30 @@ public void ApplyPatches() ApplyPatches(":FINAL", patchList.finalPatches); } - private void ApplyPatches(string stage, IEnumerable patches) + private void ApplyPatches(string stage, IEnumerable patches) { logger.Info(stage + " pass"); Activity = "ModuleManager " + stage; - foreach (UrlDir.UrlConfig mod in patches) + foreach (Patch patch in patches) { try { - string name = mod.type.RemoveWS(); - Command cmd = CommandParser.Parse(name, out string tmp); + string name = patch.node.name.RemoveWS(); - if (cmd == Command.Insert) + if (patch.command == Command.Insert) { - logger.Warning("Warning - Encountered insert node that should not exist at this stage: " + mod.SafeUrl()); + logger.Warning("Warning - Encountered insert node that should not exist at this stage: " + patch.urlConfig.SafeUrl()); continue; } - else if (cmd != Command.Edit && cmd != Command.Copy && cmd != Command.Delete) + else if (patch.command != Command.Edit && patch.command != Command.Copy && patch.command != Command.Delete) { - logger.Warning("Invalid command encountered on a patch: " + mod.SafeUrl()); + logger.Warning("Invalid command encountered on a patch: " + patch.urlConfig.SafeUrl()); continue; } string upperName = name.ToUpper(); - PatchContext context = new PatchContext(mod, databaseRoot, logger, progress); + PatchContext context = new PatchContext(patch.urlConfig, databaseRoot, logger, progress); char[] sep = { '[', ']' }; string condition = ""; @@ -86,29 +85,29 @@ private void ApplyPatches(string stage, IEnumerable patches) string[] splits = name.Split(sep, 3); string[] patterns = splits.Length > 1 ? splits[1].Split(',', '|') : null; - string type = splits[0].Substring(1); + string type = splits[0]; - bool loop = mod.config.HasNode("MM_PATCH_LOOP"); + bool loop = patch.node.HasNode("MM_PATCH_LOOP"); foreach (UrlDir.UrlFile file in allConfigFiles) { - if (cmd == Command.Edit) + if (patch.command == Command.Edit) { foreach (UrlDir.UrlConfig url in file.configs) { if (!IsMatch(url, type, patterns, condition)) continue; - if (loop) logger.Info("Looping on " + mod.SafeUrl() + " to " + url.SafeUrl()); + if (loop) logger.Info($"Looping on {patch.urlConfig.SafeUrl()} to {url.SafeUrl()}"); do { - progress.ApplyingUpdate(url, mod); - url.config = MMPatchLoader.ModifyNode(new NodeStack(url.config), mod.config, context); + progress.ApplyingUpdate(url, patch.urlConfig); + url.config = MMPatchLoader.ModifyNode(new NodeStack(url.config), patch.node, context); } while (loop && IsMatch(url, type, patterns, condition)); if (loop) url.config.RemoveNodes("MM_PATCH_LOOP"); } } - else if (cmd == Command.Copy) + else if (patch.command == Command.Copy) { // Avoid checking the new configs we are creating int count = file.configs.Count; @@ -117,19 +116,19 @@ private void ApplyPatches(string stage, IEnumerable patches) UrlDir.UrlConfig url = file.configs[i]; if (!IsMatch(url, type, patterns, condition)) continue; - ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(url.config), mod.config, context); + ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(url.config), patch.node, context); if (url.config.HasValue("name") && url.config.GetValue("name") == clone.GetValue("name")) { - progress.Error(mod, $"Error - when applying copy {mod.SafeUrl()} to {url.SafeUrl()} - the copy needs to have a different name than the parent (use @name = xxx)"); + progress.Error(patch.urlConfig, $"Error - when applying copy {patch.urlConfig.SafeUrl()} to {url.SafeUrl()} - the copy needs to have a different name than the parent (use @name = xxx)"); } else { - progress.ApplyingCopy(url, mod); + progress.ApplyingCopy(url, patch.urlConfig); file.AddConfig(clone); } } } - else if (cmd == Command.Delete) + else if (patch.command == Command.Delete) { int i = 0; while (i < file.configs.Count) @@ -138,7 +137,7 @@ private void ApplyPatches(string stage, IEnumerable patches) if (IsMatch(url, type, patterns, condition)) { - progress.ApplyingDelete(url, mod); + progress.ApplyingDelete(url, patch.urlConfig); file.configs.RemoveAt(i); } else @@ -156,11 +155,11 @@ private void ApplyPatches(string stage, IEnumerable patches) } catch (Exception e) { - progress.Exception(mod, "Exception while processing node : " + mod.SafeUrl(), e); + progress.Exception(patch.urlConfig, "Exception while processing node : " + patch.urlConfig.SafeUrl(), e); try { - logger.Error("Processed node was\n" + mod.PrettyPrint()); + logger.Error("Processed node was\n" + patch.urlConfig.PrettyPrint()); } catch (Exception ex2) { diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index d3425918..93210f2d 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -31,13 +31,13 @@ public static PatchList SortAndExtractPatches(UrlDir databaseRoot, IEnumerable thePass = null; + List thePass = null; bool modNotFound = false; if (firstMatch.Success) @@ -176,16 +176,17 @@ public static PatchList SortAndExtractPatches(UrlDir databaseRoot, IEnumerable beforePatches = new List(0); - public readonly List forPatches = new List(0); - public readonly List afterPatches = new List(0); + public readonly List beforePatches = new List(0); + public readonly List forPatches = new List(0); + public readonly List afterPatches = new List(0); public readonly string name; @@ -52,9 +52,9 @@ public ModPassCollection(IEnumerable modList) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - public readonly List firstPatches = new List(); - public readonly List legacyPatches = new List(); - public readonly List finalPatches = new List(); + public readonly List firstPatches = new List (); + public readonly List legacyPatches = new List(); + public readonly List finalPatches = new List(); public readonly ModPassCollection modPasses; diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 1909a654..72faea68 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -71,6 +71,7 @@ + diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index 729de882..1370095a 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -5,6 +5,7 @@ using UnityEngine; using TestUtils; using ModuleManager; +using ModuleManager.Extensions; using ModuleManager.Logging; using ModuleManager.Progress; @@ -50,7 +51,7 @@ public void TestApplyPatches__Edit() { "jkl", "mno" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART") + Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART") { { "@foo", "baz" }, { "pqr", "stw" }, @@ -63,8 +64,8 @@ public void TestApplyPatches__Edit() EnsureNoErrors(); progress.Received(1).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingUpdate(config2, patch1); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config2, patch1.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(3, allConfigs.Length); @@ -109,7 +110,7 @@ public void TestApplyPatches__Copy() { "bbb", "004" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART") + Patch patch1 = CreatePatch(Command.Copy, new TestConfigNode("+PART") { { "@name ^", ":^00:01:" }, { "@aaa", "011" }, @@ -123,79 +124,8 @@ public void TestApplyPatches__Copy() EnsureNoErrors(); progress.Received(1).PatchApplied(); - progress.Received().ApplyingCopy(config1, patch1); - progress.Received().ApplyingCopy(config2, patch1); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(5, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, allConfigs[0].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "002" }, - }, allConfigs[1].config); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "003" }, - { "bbb", "004" }, - }, allConfigs[2].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "010" }, - { "aaa", "011" }, - { "ccc", "005" }, - }, allConfigs[3].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "012" }, - { "ccc", "005" }, - }, allConfigs[4].config); - } - - [Fact] - public void TestApplyPatches__Copy__AlternateCommand() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "002" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "003" }, - { "bbb", "004" }, - }, file); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("$PART") - { - { "@name ^", ":^00:01:" }, - { "@aaa", "011" }, - { "ccc", "005" }, - }); - - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(1).PatchApplied(); - progress.Received().ApplyingCopy(config1, patch1); - progress.Received().ApplyingCopy(config2, patch1); + progress.Received().ApplyingCopy(config1, patch1.urlConfig); + progress.Received().ApplyingCopy(config2, patch1.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(5, allConfigs.Length); @@ -251,49 +181,7 @@ public void TestApplyPatches__Delete() { "jkl", "mno" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("-PART")); - - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(1).PatchApplied(); - progress.Received().ApplyingDelete(config1, patch1); - progress.Received().ApplyingDelete(config2, patch1); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(1, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "ghi" }, - { "jkl", "mno" }, - }, allConfigs[0].config); - } - - [Fact] - public void TestApplyPatches__Delete__AlternateCommand() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "abc" }, - { "foo", "bar" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "def" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "ghi" }, - { "jkl", "mno" }, - }, file); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("-PART")); + Patch patch1 = CreatePatch(Command.Delete, new TestConfigNode("-PART")); patchList.firstPatches.Add(patch1); @@ -302,8 +190,8 @@ public void TestApplyPatches__Delete__AlternateCommand() EnsureNoErrors(); progress.Received(1).PatchApplied(); - progress.Received().ApplyingDelete(config1, patch1); - progress.Received().ApplyingDelete(config2, patch1); + progress.Received().ApplyingDelete(config1, patch1.urlConfig); + progress.Received().ApplyingDelete(config2, patch1.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(1, allConfigs.Length); @@ -342,20 +230,20 @@ public void TestApplyPatches__Name() { "ddd", "007" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000]") + Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000]") { { "@aaa", "011" }, { "eee", "012" }, }); - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART[002]") + Patch patch2 = CreatePatch(Command.Copy, new TestConfigNode("+PART[002]") { { "@name", "022" }, { "@bbb", "013" }, { "fff", "014" }, }); - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new TestConfigNode("!PART[004]")); + Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART[004]")); patchList.firstPatches.Add(patch1); patchList.firstPatches.Add(patch2); @@ -366,9 +254,9 @@ public void TestApplyPatches__Name() EnsureNoErrors(); progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingCopy(config2, patch2); - progress.Received().ApplyingDelete(config3, patch3); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingCopy(config2, patch2.urlConfig); + progress.Received().ApplyingDelete(config3, patch3.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(4, allConfigs.Length); @@ -427,20 +315,20 @@ public void TestApplyPatches__Name__Wildcard() { "ddd", "007" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[0*0]") + Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[0*0]") { { "@aaa", "011" }, { "eee", "012" }, }); - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART[0*2]") + Patch patch2 = CreatePatch(Command.Copy, new TestConfigNode("+PART[0*2]") { { "@name", "022" }, { "@bbb", "013" }, { "fff", "014" }, }); - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new TestConfigNode("!PART[0*4]")); + Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART[0*4]")); patchList.firstPatches.Add(patch1); patchList.firstPatches.Add(patch2); @@ -451,9 +339,9 @@ public void TestApplyPatches__Name__Wildcard() EnsureNoErrors(); progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingCopy(config2, patch2); - progress.Received().ApplyingDelete(config3, patch3); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingCopy(config2, patch2.urlConfig); + progress.Received().ApplyingDelete(config3, patch3.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(4, allConfigs.Length); @@ -506,7 +394,7 @@ public void TestApplyPatches__Name__Or() { "ccc", "005" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "@aaa", "011" }, { "ddd", "006" }, @@ -519,8 +407,8 @@ public void TestApplyPatches__Name__Or() EnsureNoErrors(); progress.Received(1).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingUpdate(config2, patch1); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config2, patch1.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(3, allConfigs.Length); @@ -555,47 +443,47 @@ public void TestApplyPatches__Order() { "aaa", "001" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "bbb", "002" }, }); - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch2 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "ccc", "003" }, }); - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch3 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "ddd", "004" }, }); - UrlDir.UrlConfig patch4 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch4 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "eee", "005" }, }); - UrlDir.UrlConfig patch5 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch5 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "fff", "006" }, }); - UrlDir.UrlConfig patch6 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch6 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "ggg", "007" }, }); - UrlDir.UrlConfig patch7 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch7 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "hhh", "008" }, }); - UrlDir.UrlConfig patch8 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch8 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "iii", "009" }, }); - UrlDir.UrlConfig patch9 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") + Patch patch9 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") { { "jjj", "010" }, }); @@ -615,15 +503,15 @@ public void TestApplyPatches__Order() EnsureNoErrors(); progress.Received(9).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingUpdate(config1, patch2); - progress.Received().ApplyingUpdate(config1, patch3); - progress.Received().ApplyingUpdate(config1, patch4); - progress.Received().ApplyingUpdate(config1, patch5); - progress.Received().ApplyingUpdate(config1, patch6); - progress.Received().ApplyingUpdate(config1, patch7); - progress.Received().ApplyingUpdate(config1, patch8); - progress.Received().ApplyingUpdate(config1, patch9); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config1, patch2.urlConfig); + progress.Received().ApplyingUpdate(config1, patch3.urlConfig); + progress.Received().ApplyingUpdate(config1, patch4.urlConfig); + progress.Received().ApplyingUpdate(config1, patch5.urlConfig); + progress.Received().ApplyingUpdate(config1, patch6.urlConfig); + progress.Received().ApplyingUpdate(config1, patch7.urlConfig); + progress.Received().ApplyingUpdate(config1, patch8.urlConfig); + progress.Received().ApplyingUpdate(config1, patch9.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(1, allConfigs.Length); @@ -671,20 +559,20 @@ public void TestApplyPatches__Constraints() { "ddd", "007" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART:HAS[#aaa[001]]") + Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART:HAS[#aaa[001]]") { { "@aaa", "011" }, { "eee", "012" }, }); - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART:HAS[#bbb[003]]") + Patch patch2 = CreatePatch(Command.Copy, new TestConfigNode("+PART:HAS[#bbb[003]]") { { "@name", "012" }, { "@bbb", "013" }, { "fff", "014" }, }); - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new TestConfigNode("!PART:HAS[#ccc[005]]")); + Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART:HAS[#ccc[005]]")); patchList.firstPatches.Add(patch1); patchList.firstPatches.Add(patch2); @@ -695,9 +583,9 @@ public void TestApplyPatches__Constraints() EnsureNoErrors(); progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingCopy(config2, patch2); - progress.Received().ApplyingDelete(config3, patch3); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingCopy(config2, patch2.urlConfig); + progress.Received().ApplyingDelete(config3, patch3.urlConfig); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(4, allConfigs.Length); @@ -738,7 +626,7 @@ public void TestApplyPatches__Loop() { "aaa", "1" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART:HAS[~aaa[>10]]") + Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART:HAS[~aaa[>10]]") { { "@aaa *", "2" }, { "bbb", "002" }, @@ -752,7 +640,7 @@ public void TestApplyPatches__Loop() EnsureNoErrors(); progress.Received(1).PatchApplied(); - progress.Received(4).ApplyingUpdate(config1, patch1); + progress.Received(4).ApplyingUpdate(config1, patch1.urlConfig); logger.Received().Log(LogType.Log, "Looping on abc/def/@PART:HAS[~aaa[>10]] to abc/def/PART"); @@ -770,6 +658,7 @@ public void TestApplyPatches__Loop() }, allConfigs[0].config); } + /* [Fact] public void TestApplyPatches__InvalidOperator() { @@ -816,6 +705,7 @@ public void TestApplyPatches__InvalidOperator() }, allConfigs[0].config); } + */ [Fact] public void TestApplyPatches__Copy__NameNotChanged() @@ -826,7 +716,7 @@ public void TestApplyPatches__Copy__NameNotChanged() { "aaa", "001" }, }, file); - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART") + Patch patch1 = CreatePatch(Command.Copy, new TestConfigNode("+PART") { { "@aaa", "011" }, { "bbb", "012" }, @@ -839,7 +729,7 @@ public void TestApplyPatches__Copy__NameNotChanged() progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.Received().Error(patch1, "Error - when applying copy abc/def/+PART to abc/def/PART - the copy needs to have a different name than the parent (use @name = xxx)"); + progress.Received().Error(patch1.urlConfig, "Error - when applying copy abc/def/+PART to abc/def/PART - the copy needs to have a different name than the parent (use @name = xxx)"); logger.DidNotReceive().Log(LogType.Warning, Arg.Any()); logger.DidNotReceive().Log(LogType.Error, Arg.Any()); @@ -873,5 +763,16 @@ private void AssertNodesEqual(ConfigNode expected, ConfigNode actual) { Assert.Equal(expected.ToString(), actual.ToString()); } + + private Patch CreatePatch(Command command, ConfigNode node) + { + ConfigNode newNode = node; + if (command != Command.Insert) + { + newNode = new ConfigNode(node.name.Substring(1)); + newNode.ShallowCopyFrom(node); + } + return new Patch(new UrlDir.UrlConfig(file, node), command, newNode); + } } } diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 2a56c11a..3f9005b1 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -139,65 +139,68 @@ public void TestSortAndExtractPatches() Assert.Equal(insertConfigs, root.AllConfigs); - Assert.Equal(legacyConfigs, list.legacyPatches); + List currentPatches; - List currentPatches; + currentPatches = list.legacyPatches; + Assert.Equal(legacyConfigs.Length, currentPatches.Count); + AssertPatchCorrect(currentPatches[0], legacyConfigs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], legacyConfigs[1], Command.Edit, "NADE[foo]:HAS[#bar]"); currentPatches = list.firstPatches; Assert.Equal(firstConfigs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", firstConfigs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", firstConfigs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", firstConfigs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", firstConfigs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], firstConfigs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], firstConfigs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], firstConfigs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], firstConfigs[3], Command.Edit, "NADE"); currentPatches = list.finalPatches; Assert.Equal(finalConfigs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", finalConfigs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", finalConfigs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", finalConfigs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", finalConfigs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], finalConfigs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], finalConfigs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], finalConfigs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], finalConfigs[3], Command.Edit, "NADE"); currentPatches = list.modPasses["mod1"].beforePatches; Assert.Equal(beforeMod1Configs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", beforeMod1Configs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", beforeMod1Configs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", beforeMod1Configs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", beforeMod1Configs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], beforeMod1Configs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], beforeMod1Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], beforeMod1Configs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], beforeMod1Configs[3], Command.Edit, "NADE"); currentPatches = list.modPasses["mod1"].forPatches; Assert.Equal(forMod1Configs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", forMod1Configs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", forMod1Configs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", forMod1Configs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", forMod1Configs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], forMod1Configs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], forMod1Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], forMod1Configs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], forMod1Configs[3], Command.Edit, "NADE"); currentPatches = list.modPasses["mod1"].afterPatches; Assert.Equal(afterMod1Configs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", afterMod1Configs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", afterMod1Configs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", afterMod1Configs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", afterMod1Configs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], afterMod1Configs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], afterMod1Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], afterMod1Configs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], afterMod1Configs[3], Command.Edit, "NADE"); currentPatches = list.modPasses["mod2"].beforePatches; Assert.Equal(beforeMod2Configs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", beforeMod2Configs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", beforeMod2Configs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", beforeMod2Configs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", beforeMod2Configs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], beforeMod2Configs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], beforeMod2Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], beforeMod2Configs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], beforeMod2Configs[3], Command.Edit, "NADE"); currentPatches = list.modPasses["mod2"].forPatches; Assert.Equal(forMod2Configs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", forMod2Configs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", forMod2Configs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", forMod2Configs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", forMod2Configs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], forMod2Configs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], forMod2Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], forMod2Configs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], forMod2Configs[3], Command.Edit, "NADE"); currentPatches = list.modPasses["mod2"].afterPatches; Assert.Equal(afterMod2Configs.Length, currentPatches.Count); - AssertUrlCorrect("@NODE", afterMod2Configs[0], currentPatches[0]); - AssertUrlCorrect("@NODE[foo]:HAS[#bar]", afterMod2Configs[1], currentPatches[1]); - AssertUrlCorrect("@NADE", afterMod2Configs[2], currentPatches[2]); - AssertUrlCorrect("@NADE", afterMod2Configs[3], currentPatches[3]); + AssertPatchCorrect(currentPatches[0], afterMod2Configs[0], Command.Edit, "NODE"); + AssertPatchCorrect(currentPatches[1], afterMod2Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(currentPatches[2], afterMod2Configs[2], Command.Edit, "NADE"); + AssertPatchCorrect(currentPatches[3], afterMod2Configs[3], Command.Edit, "NADE"); progress.Received(34).PatchAdded(); @@ -260,7 +263,7 @@ public void TestSortAndExtractPatches__MoreThanOnePass() progress.Received().Error(config3, "Error - more than one pass specifier on a node: abc/def/@NODE:FIRST:FOR[mod1]"); Assert.Equal(1, list.firstPatches.Count); - AssertUrlCorrect("@NODE", config1, list.firstPatches[0]); + AssertPatchCorrect(list.firstPatches[0], config1, Command.Edit, "NODE"); Assert.Empty(list.legacyPatches); Assert.Empty(list.finalPatches); Assert.Empty(list.modPasses["mod1"].beforePatches); @@ -285,9 +288,10 @@ public void TestSortAndExtractPatches__Exception() progress.Received().Exception(config2, "Exception while parsing pass for config: abc/def/@NODE:FIRST:FIRST", e); - Assert.Equal(new[] { config1 }, list.legacyPatches); + Assert.Equal(1, list.legacyPatches.Count); + AssertPatchCorrect(list.legacyPatches[0], config1, Command.Edit, "NODE"); Assert.Equal(1, list.firstPatches.Count); - AssertUrlCorrect("@NADE", config3, list.firstPatches[0]); + AssertPatchCorrect(list.firstPatches[0], config3, Command.Edit, "NADE"); progress.Received(2).PatchAdded(); } @@ -312,7 +316,7 @@ public void TestSortAndExtractPatches__NotBracketBalanced() Assert.Empty(list.finalPatches); Assert.Empty(list.modPasses["mod1"].beforePatches); Assert.Equal(1, list.modPasses["mod1"].forPatches.Count); - AssertUrlCorrect("@NODE", config1, list.modPasses["mod1"].forPatches[0]); + AssertPatchCorrect(list.modPasses["mod1"].forPatches[0], config1, Command.Edit, "NODE"); Assert.Empty(list.modPasses["mod1"].afterPatches); progress.Received(1).PatchAdded(); @@ -341,42 +345,50 @@ public void TestSortAndExtractPatches__BadlyFormed() Assert.Empty(list.finalPatches); Assert.Empty(list.modPasses["mod1"].beforePatches); Assert.Equal(1, list.modPasses["mod1"].forPatches.Count); - AssertUrlCorrect("@NODE", config1, list.modPasses["mod1"].forPatches[0]); + AssertPatchCorrect(list.modPasses["mod1"].forPatches[0], config1, Command.Edit, "NODE"); Assert.Empty(list.modPasses["mod1"].afterPatches); progress.Received(1).PatchAdded(); } [Fact] - public void TestSortAndExtractPatches__InvalidCommand() + public void TestSortAndExtractPatches__Command() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR[mod1]"); - UrlDir.UrlConfig config2 = CreateConfig("%NODE:FOR[mod1]"); - UrlDir.UrlConfig config3 = CreateConfig("&NODE:FOR[mod1]"); - UrlDir.UrlConfig config4 = CreateConfig("|NODE:FOR[mod1]"); - UrlDir.UrlConfig config5 = CreateConfig("#NODE:FOR[mod1]"); - UrlDir.UrlConfig config6 = CreateConfig("*NODE:FOR[mod1]"); + UrlDir.UrlConfig config01 = CreateConfig("@NODE:FOR[mod1]"); + UrlDir.UrlConfig config02 = CreateConfig("+NODE:FOR[mod1]"); + UrlDir.UrlConfig config03 = CreateConfig("$NODE:FOR[mod1]"); + UrlDir.UrlConfig config04 = CreateConfig("!NODE:FOR[mod1]"); + UrlDir.UrlConfig config05 = CreateConfig("-NODE:FOR[mod1]"); + UrlDir.UrlConfig config06 = CreateConfig("%NODE:FOR[mod1]"); + UrlDir.UrlConfig config07 = CreateConfig("&NODE:FOR[mod1]"); + UrlDir.UrlConfig config08 = CreateConfig("|NODE:FOR[mod1]"); + UrlDir.UrlConfig config09 = CreateConfig("#NODE:FOR[mod1]"); + UrlDir.UrlConfig config10 = CreateConfig("*NODE:FOR[mod1]"); string[] modList = { "mod1" }; PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); Assert.Empty(root.AllConfigs); - progress.Received().Error(config2, "Error - replace command (%) is not valid on a root node: abc/def/%NODE:FOR[mod1]"); - progress.Received().Error(config3, "Error - create command (&) is not valid on a root node: abc/def/&NODE:FOR[mod1]"); - progress.Received().Error(config4, "Error - rename command (|) is not valid on a root node: abc/def/|NODE:FOR[mod1]"); - progress.Received().Error(config5, "Error - paste command (#) is not valid on a root node: abc/def/#NODE:FOR[mod1]"); - progress.Received().Error(config6, "Error - special command (*) is not valid on a root node: abc/def/*NODE:FOR[mod1]"); + progress.Received().Error(config06, "Error - replace command (%) is not valid on a root node: abc/def/%NODE:FOR[mod1]"); + progress.Received().Error(config07, "Error - create command (&) is not valid on a root node: abc/def/&NODE:FOR[mod1]"); + progress.Received().Error(config08, "Error - rename command (|) is not valid on a root node: abc/def/|NODE:FOR[mod1]"); + progress.Received().Error(config09, "Error - paste command (#) is not valid on a root node: abc/def/#NODE:FOR[mod1]"); + progress.Received().Error(config10, "Error - special command (*) is not valid on a root node: abc/def/*NODE:FOR[mod1]"); Assert.Empty(list.firstPatches); Assert.Empty(list.legacyPatches); Assert.Empty(list.finalPatches); Assert.Empty(list.modPasses["mod1"].beforePatches); - Assert.Equal(1, list.modPasses["mod1"].forPatches.Count); - AssertUrlCorrect("@NODE", config1, list.modPasses["mod1"].forPatches[0]); + Assert.Equal(5, list.modPasses["mod1"].forPatches.Count); + AssertPatchCorrect(list.modPasses["mod1"].forPatches[0], config01, Command.Edit, "NODE"); + AssertPatchCorrect(list.modPasses["mod1"].forPatches[1], config02, Command.Copy, "NODE"); + AssertPatchCorrect(list.modPasses["mod1"].forPatches[2], config03, Command.Copy, "NODE"); + AssertPatchCorrect(list.modPasses["mod1"].forPatches[3], config04, Command.Delete, "NODE"); + AssertPatchCorrect(list.modPasses["mod1"].forPatches[4], config05, Command.Delete, "NODE"); Assert.Empty(list.modPasses["mod1"].afterPatches); - progress.Received(1).PatchAdded(); + progress.Received(5).PatchAdded(); } private UrlDir.UrlConfig CreateConfig(string name) @@ -395,31 +407,25 @@ private UrlDir.UrlConfig CreateConfig(string name) return UrlBuilder.CreateConfig(node, file); } - private void AssertUrlCorrect(string expectedNodeName, UrlDir.UrlConfig originalUrl, UrlDir.UrlConfig observedUrl) + private void AssertPatchCorrect(Patch patch, UrlDir.UrlConfig originalUrl, Command expectedCommand, string expectedNodeName) { - Assert.Equal(expectedNodeName, observedUrl.type); + Assert.Same(originalUrl, patch.urlConfig); + Assert.Equal(expectedNodeName, patch.node.name); ConfigNode originalNode = originalUrl.config; - ConfigNode observedNode = observedUrl.config; - - Assert.Equal(expectedNodeName, observedNode.name); - - if (originalNode.HasValue("name")) Assert.Equal(originalNode.GetValue("name"), observedUrl.name); - - Assert.Same(originalUrl.parent, observedUrl.parent); - Assert.Equal(originalNode.id, observedNode.id); - Assert.Equal(originalNode.values.Count, observedNode.values.Count); - Assert.Equal(originalNode.nodes.Count, observedNode.nodes.Count); + Assert.Equal(originalNode.id, patch.node.id); + Assert.Equal(originalNode.values.Count, patch.node.values.Count); + Assert.Equal(originalNode.nodes.Count, patch.node.nodes.Count); for (int i = 0; i < originalNode.values.Count; i++) { - Assert.Same(originalNode.values[i], observedNode.values[i]); + Assert.Same(originalNode.values[i], patch.node.values[i]); } for (int i = 0; i < originalNode.nodes.Count; i++) { - Assert.Same(originalNode.nodes[i], observedNode.nodes[i]); + Assert.Same(originalNode.nodes[i], patch.node.nodes[i]); } } } diff --git a/ModuleManagerTests/PatchTest.cs b/ModuleManagerTests/PatchTest.cs new file mode 100644 index 00000000..c8bda099 --- /dev/null +++ b/ModuleManagerTests/PatchTest.cs @@ -0,0 +1,76 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; + +namespace ModuleManagerTests +{ + public class PatchTest + { + [Fact] + public void TestConstructor() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + ConfigNode node = new ConfigNode("NADE"); + Patch patch = new Patch(urlConfig, Command.Edit, node); + + Assert.Same(urlConfig, patch.urlConfig); + Assert.Equal(Command.Edit, patch.command); + Assert.Same(node, patch.node); + } + + [Fact] + public void TestConstructor__ValidCommands() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + ConfigNode node = new ConfigNode("NADE"); + + Patch patch1 = new Patch(urlConfig, Command.Edit, node); + Assert.Same(urlConfig, patch1.urlConfig); + Assert.Equal(Command.Edit, patch1.command); + Assert.Same(node, patch1.node); + + Patch patch2 = new Patch(urlConfig, Command.Copy, node); + Assert.Same(urlConfig, patch2.urlConfig); + Assert.Equal(Command.Copy, patch2.command); + Assert.Same(node, patch2.node); + + Patch patch3 = new Patch(urlConfig, Command.Delete, node); + Assert.Same(urlConfig, patch3.urlConfig); + Assert.Equal(Command.Delete, patch3.command); + Assert.Same(node, patch3.node); + + ArgumentException ex1 = Assert.Throws(() => new Patch(urlConfig, Command.Create, node)); + Assert.Equal("Must be Edit, Copy, or Delete (got Create)\r\nParameter name: command", ex1.Message); + + ArgumentException ex2 = Assert.Throws(() => new Patch(urlConfig, Command.Insert, node)); + Assert.Equal("Must be Edit, Copy, or Delete (got Insert)\r\nParameter name: command", ex2.Message); + + ArgumentException ex3 = Assert.Throws(() => new Patch(urlConfig, Command.Paste, node)); + Assert.Equal("Must be Edit, Copy, or Delete (got Paste)\r\nParameter name: command", ex3.Message); + + ArgumentException ex4 = Assert.Throws(() => new Patch(urlConfig, Command.Rename, node)); + Assert.Equal("Must be Edit, Copy, or Delete (got Rename)\r\nParameter name: command", ex4.Message); + + ArgumentException ex5 = Assert.Throws(() => new Patch(urlConfig, Command.Replace, node)); + Assert.Equal("Must be Edit, Copy, or Delete (got Replace)\r\nParameter name: command", ex5.Message); + + ArgumentException ex6 = Assert.Throws(() => new Patch(urlConfig, Command.Special, node)); + Assert.Equal("Must be Edit, Copy, or Delete (got Special)\r\nParameter name: command", ex6.Message); + } + + [Fact] + public void TestConstructor__NullUrlConfig() + { + Assert.Throws(() => new Patch(null, Command.Edit, new ConfigNode("BLAH"))); + } + + [Fact] + public void TestConstructor__NullNode() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + Assert.Throws(() => new Patch(urlConfig, Command.Edit, null)); + } + } +} From 9d37955474438a38696ca0b15ae8dc8c451982d1 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 10 Dec 2017 20:57:59 -0800 Subject: [PATCH 008/140] Simplify a bit --- ModuleManager/PatchExtractor.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index 93210f2d..6eb02440 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -118,7 +118,6 @@ public static PatchList SortAndExtractPatches(UrlDir databaseRoot, IEnumerable thePass = null; - bool modNotFound = false; if (firstMatch.Success) { @@ -139,8 +138,8 @@ public static PatchList SortAndExtractPatches(UrlDir databaseRoot, IEnumerable Date: Sun, 10 Dec 2017 21:24:46 -0800 Subject: [PATCH 009/140] Extra semicolon --- ModuleManager/PatchExtractor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index 6eb02440..e327daea 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -31,7 +31,7 @@ public static PatchList SortAndExtractPatches(UrlDir databaseRoot, IEnumerable Date: Wed, 7 Feb 2018 21:29:32 -0800 Subject: [PATCH 010/140] Extract interface for PatchList, modify PatchExtractor PatchExtractor did too much at once, it shouldn't enumerate and extract at the same time (still some work to be done there...). PatchList now has a clearer interface and obscures more of the details - iterating through patches is now handled by it rater than PatchApplier --- ModuleManager/Collections/ArrayEnumerator.cs | 2 +- ModuleManager/MMPatchLoader.cs | 11 +- ModuleManager/ModuleManager.csproj | 1 + ModuleManager/Pass.cs | 31 + ModuleManager/PatchApplier.cs | 28 +- ModuleManager/PatchExtractor.cs | 305 ++++--- ModuleManager/PatchList.cs | 119 ++- ModuleManagerTests/ModuleManagerTests.csproj | 1 + ModuleManagerTests/PassTest.cs | 73 ++ ModuleManagerTests/PatchApplierTest.cs | 264 +++--- ModuleManagerTests/PatchExtractorTest.cs | 884 +++++++++++++------ ModuleManagerTests/PatchListTest.cs | 312 ++++++- 12 files changed, 1411 insertions(+), 620 deletions(-) create mode 100644 ModuleManager/Pass.cs create mode 100644 ModuleManagerTests/PassTest.cs diff --git a/ModuleManager/Collections/ArrayEnumerator.cs b/ModuleManager/Collections/ArrayEnumerator.cs index d002b2ad..c965b9d3 100644 --- a/ModuleManager/Collections/ArrayEnumerator.cs +++ b/ModuleManager/Collections/ArrayEnumerator.cs @@ -9,7 +9,7 @@ public struct ArrayEnumerator : IEnumerator private readonly T[] array; private int index; - public ArrayEnumerator(T[] array) + public ArrayEnumerator(params T[] array) { this.array = array; index = -1; diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index c786220f..f713c0c3 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -175,7 +175,16 @@ private IEnumerator ProcessPatch() yield return null; - PatchList patchList = PatchExtractor.SortAndExtractPatches(GameDatabase.Instance.root, mods, progress); + // PatchList patchList = PatchExtractor.SortAndExtractPatches(GameDatabase.Instance.root, mods, progress); + + PatchList patchList = new PatchList(mods); + PatchExtractor extractor = new PatchExtractor(patchList, progress, logger); + + // Have to convert to an array because we will be removing patches + foreach (UrlDir.UrlConfig urlConfig in GameDatabase.Instance.root.AllConfigs.ToArray()) + { + extractor.ExtractPatch(urlConfig); + } #endregion diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index c0f51ded..4e0bd9da 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -59,6 +59,7 @@ + diff --git a/ModuleManager/Pass.cs b/ModuleManager/Pass.cs new file mode 100644 index 00000000..241c88b5 --- /dev/null +++ b/ModuleManager/Pass.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ModuleManager +{ + public interface IPass : IEnumerable + { + string Name { get; } + } + + public class Pass : IPass + { + private readonly string name; + private readonly List patches = new List(0); + + public Pass(string name) + { + this.name = name ?? throw new ArgumentNullException(nameof(name)); + if (name == string.Empty) throw new ArgumentException("can't be empty", nameof(name)); + } + + public string Name => name; + + public void Add(Patch patch) => patches.Add(patch); + + public List.Enumerator GetEnumerator() => patches.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index 3e981653..c82ee866 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using ModuleManager.Logging; using ModuleManager.Extensions; @@ -14,13 +13,13 @@ public class PatchApplier private readonly IPatchProgress progress; private readonly UrlDir databaseRoot; - private readonly PatchList patchList; + private readonly IPatchList patchList; private readonly UrlDir.UrlFile[] allConfigFiles; public string Activity { get; private set; } - public PatchApplier(PatchList patchList, UrlDir databaseRoot, IPatchProgress progress, IBasicLogger logger) + public PatchApplier(IPatchList patchList, UrlDir databaseRoot, IPatchProgress progress, IBasicLogger logger) { this.patchList = patchList; this.databaseRoot = databaseRoot; @@ -32,29 +31,18 @@ public PatchApplier(PatchList patchList, UrlDir databaseRoot, IPatchProgress pro public void ApplyPatches() { - ApplyPatches(":FIRST", patchList.firstPatches); - - // any node without a :pass - ApplyPatches(":LEGACY (default)", patchList.legacyPatches); - - foreach (PatchList.ModPass pass in patchList.modPasses) + foreach (IPass pass in patchList) { - string upperModName = pass.name.ToUpper(); - ApplyPatches($":BEFORE[{upperModName}]", pass.beforePatches); - ApplyPatches($":FOR[{upperModName}]", pass.forPatches); - ApplyPatches($":AFTER[{upperModName}]", pass.afterPatches); + ApplyPatches(pass); } - - // :Final node - ApplyPatches(":FINAL", patchList.finalPatches); } - private void ApplyPatches(string stage, IEnumerable patches) + private void ApplyPatches(IPass pass) { - logger.Info(stage + " pass"); - Activity = "ModuleManager " + stage; + logger.Info(pass.Name + " pass"); + Activity = "ModuleManager " + pass.Name; - foreach (Patch patch in patches) + foreach (Patch patch in pass) { try { diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index e327daea..5a583ad4 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -1,13 +1,12 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using ModuleManager.Extensions; +using ModuleManager.Logging; using ModuleManager.Progress; namespace ModuleManager { - public static class PatchExtractor + public class PatchExtractor { private static readonly Regex firstRegex = new Regex(@":FIRST", RegexOptions.IgnoreCase); private static readonly Regex finalRegex = new Regex(@":FINAL", RegexOptions.IgnoreCase); @@ -15,190 +14,202 @@ public static class PatchExtractor private static readonly Regex forRegex = new Regex(@":FOR(?:\[([^\[\]]+)\])?", RegexOptions.IgnoreCase); private static readonly Regex afterRegex = new Regex(@":AFTER(?:\[([^\[\]]+)\])?", RegexOptions.IgnoreCase); - public static PatchList SortAndExtractPatches(UrlDir databaseRoot, IEnumerable modList, IPatchProgress progress) + private readonly IPatchList patchList; + private readonly IPatchProgress progress; + private readonly IBasicLogger logger; + + public PatchExtractor(IPatchList patchList, IPatchProgress progress, IBasicLogger logger) { - PatchList list = new PatchList(modList); + this.patchList = patchList ?? throw new ArgumentNullException(nameof(patchList)); + this.progress = progress ?? throw new ArgumentNullException(nameof(progress)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } - // Have to convert to an array because we will be removing patches - foreach (UrlDir.UrlConfig url in databaseRoot.AllConfigs.ToArray()) + public void ExtractPatch(UrlDir.UrlConfig urlConfig) + { + try { - try + if (!urlConfig.type.IsBracketBalanced()) { - if (!url.type.IsBracketBalanced()) - { - progress.Error(url, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\n" + url.SafeUrl()); - url.parent.configs.Remove(url); - continue; - } + progress.Error(urlConfig, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\n" + urlConfig.SafeUrl()); + urlConfig.parent.configs.Remove(urlConfig); + return; + } - Command command = CommandParser.Parse(url.type, out string name); + Command command = CommandParser.Parse(urlConfig.type, out string name); - Match firstMatch = firstRegex.Match(name); - Match finalMatch = finalRegex.Match(name); - Match beforeMatch = beforeRegex.Match(name); - Match forMatch = forRegex.Match(name); - Match afterMatch = afterRegex.Match(name); + Match firstMatch = firstRegex.Match(name); + Match finalMatch = finalRegex.Match(name); + Match beforeMatch = beforeRegex.Match(name); + Match forMatch = forRegex.Match(name); + Match afterMatch = afterRegex.Match(name); - int matchCount = 0; + int matchCount = 0; - if (firstMatch.Success) matchCount++; - if (finalMatch.Success) matchCount++; - if (beforeMatch.Success) matchCount++; - if (forMatch.Success) matchCount++; - if (afterMatch.Success) matchCount++; + if (firstMatch.Success) matchCount++; + if (finalMatch.Success) matchCount++; + if (beforeMatch.Success) matchCount++; + if (forMatch.Success) matchCount++; + if (afterMatch.Success) matchCount++; - if (firstMatch.NextMatch().Success) matchCount++; - if (finalMatch.NextMatch().Success) matchCount++; - if (beforeMatch.NextMatch().Success) matchCount++; - if (forMatch.NextMatch().Success) matchCount++; - if (afterMatch.NextMatch().Success) matchCount++; + if (firstMatch.NextMatch().Success) matchCount++; + if (finalMatch.NextMatch().Success) matchCount++; + if (beforeMatch.NextMatch().Success) matchCount++; + if (forMatch.NextMatch().Success) matchCount++; + if (afterMatch.NextMatch().Success) matchCount++; - bool error = false; + bool error = false; - if (command == Command.Insert && matchCount > 0) - { - progress.Error(url, $"Error - pass specifier detected on an insert node (not a patch): {url.SafeUrl()}"); - error = true; - } - else if (command == Command.Replace) - { - progress.Error(url, $"Error - replace command (%) is not valid on a root node: {url.SafeUrl()}"); - error = true; - } - else if (command == Command.Create) - { - progress.Error(url, $"Error - create command (&) is not valid on a root node: {url.SafeUrl()}"); - error = true; - } - else if (command == Command.Rename) - { - progress.Error(url, $"Error - rename command (|) is not valid on a root node: {url.SafeUrl()}"); - error = true; - } - else if (command == Command.Paste) - { - progress.Error(url, $"Error - paste command (#) is not valid on a root node: {url.SafeUrl()}"); - error = true; - } - else if (command == Command.Special) - { - progress.Error(url, $"Error - special command (*) is not valid on a root node: {url.SafeUrl()}"); - error = true; - } + if (command == Command.Insert && matchCount > 0) + { + progress.Error(urlConfig, $"Error - pass specifier detected on an insert node (not a patch): {urlConfig.SafeUrl()}"); + error = true; + } + else if (command == Command.Replace) + { + progress.Error(urlConfig, $"Error - replace command (%) is not valid on a root node: {urlConfig.SafeUrl()}"); + error = true; + } + else if (command == Command.Create) + { + progress.Error(urlConfig, $"Error - create command (&) is not valid on a root node: {urlConfig.SafeUrl()}"); + error = true; + } + else if (command == Command.Rename) + { + progress.Error(urlConfig, $"Error - rename command (|) is not valid on a root node: {urlConfig.SafeUrl()}"); + error = true; + } + else if (command == Command.Paste) + { + progress.Error(urlConfig, $"Error - paste command (#) is not valid on a root node: {urlConfig.SafeUrl()}"); + error = true; + } + else if (command == Command.Special) + { + progress.Error(urlConfig, $"Error - special command (*) is not valid on a root node: {urlConfig.SafeUrl()}"); + error = true; + } - if (matchCount > 1) - { - progress.Error(url, $"Error - more than one pass specifier on a node: {url.SafeUrl()}"); - error = true; - } - if (beforeMatch.Success && !beforeMatch.Groups[1].Success) - { - progress.Error(url, "Error - malformed :BEFORE patch specifier detected: " + url.SafeUrl()); - error = true; - } - if (forMatch.Success && !forMatch.Groups[1].Success) - { - progress.Error(url, "Error - malformed :FOR patch specifier detected: " + url.SafeUrl()); - error = true; - } - if (afterMatch.Success && !afterMatch.Groups[1].Success) - { - progress.Error(url, "Error - malformed :AFTER patch specifier detected: " + url.SafeUrl()); - error = true; - } - if (error) - { - url.parent.configs.Remove(url); - continue; - } + if (matchCount > 1) + { + progress.Error(urlConfig, $"Error - more than one pass specifier on a node: {urlConfig.SafeUrl()}"); + error = true; + } + if (beforeMatch.Success && !beforeMatch.Groups[1].Success) + { + progress.Error(urlConfig, "Error - malformed :BEFORE patch specifier detected: " + urlConfig.SafeUrl()); + error = true; + } + if (forMatch.Success && !forMatch.Groups[1].Success) + { + progress.Error(urlConfig, "Error - malformed :FOR patch specifier detected: " + urlConfig.SafeUrl()); + error = true; + } + if (afterMatch.Success && !afterMatch.Groups[1].Success) + { + progress.Error(urlConfig, "Error - malformed :AFTER patch specifier detected: " + urlConfig.SafeUrl()); + error = true; + } + if (error) + { + urlConfig.parent.configs.Remove(urlConfig); + return; + } - if (command == Command.Insert) continue; + if (command == Command.Insert) return; - url.parent.configs.Remove(url); + urlConfig.parent.configs.Remove(urlConfig); - Match theMatch = null; - List thePass = null; + Match theMatch = null; + Action addPatch = null; - if (firstMatch.Success) + if (firstMatch.Success) + { + theMatch = firstMatch; + addPatch = patchList.AddFirstPatch; + } + else if (finalMatch.Success) + { + theMatch = finalMatch; + addPatch = patchList.AddFinalPatch; + } + else if (beforeMatch.Success) + { + if (CheckMod(beforeMatch, patchList, out string theMod)) { - theMatch = firstMatch; - thePass = list.firstPatches; + theMatch = beforeMatch; + addPatch = p => patchList.AddBeforePatch(theMod, p); } - else if (finalMatch.Success) + else { - theMatch = finalMatch; - thePass = list.finalPatches; + progress.NeedsUnsatisfiedBefore(urlConfig); + return; } - else if (beforeMatch.Success) + } + else if (forMatch.Success) + { + if (CheckMod(forMatch, patchList, out string theMod)) { - if (CheckMod(beforeMatch, list.modPasses, out string theMod)) - { - theMatch = beforeMatch; - thePass = list.modPasses[theMod].beforePatches; - } - else - { - progress.NeedsUnsatisfiedBefore(url); - continue; - } + theMatch = forMatch; + addPatch = p => patchList.AddForPatch(theMod, p); } - else if (forMatch.Success) + else { - if (CheckMod(forMatch, list.modPasses, out string theMod)) - { - theMatch = forMatch; - thePass = list.modPasses[theMod].forPatches; - } - else - { - progress.NeedsUnsatisfiedFor(url); - continue; - } + progress.NeedsUnsatisfiedFor(urlConfig); + return; } - else if (afterMatch.Success) + } + else if (afterMatch.Success) + { + if (CheckMod(afterMatch, patchList, out string theMod)) { - if (CheckMod(afterMatch, list.modPasses, out string theMod)) - { - theMatch = afterMatch; - thePass = list.modPasses[theMod].afterPatches; - } - else - { - progress.NeedsUnsatisfiedAfter(url); - continue; - } + theMatch = afterMatch; + addPatch = p => patchList.AddAfterPatch(theMod, p); } else { - thePass = list.legacyPatches; + progress.NeedsUnsatisfiedAfter(urlConfig); + return; } + } + else + { + addPatch = patchList.AddLegacyPatch; + } - string newName; - if (theMatch == null) - newName = name; - else - newName = name.Remove(theMatch.Index, theMatch.Length); + string newName; + if (theMatch == null) + newName = name; + else + newName = name.Remove(theMatch.Index, theMatch.Length); - ConfigNode node = new ConfigNode(newName) { id = url.config.id }; - node.ShallowCopyFrom(url.config); - Patch patch = new Patch(url, command, node); + ConfigNode node = new ConfigNode(newName) { id = urlConfig.config.id }; + node.ShallowCopyFrom(urlConfig.config); + Patch patch = new Patch(urlConfig, command, node); - thePass.Add(patch); - progress.PatchAdded(); + addPatch(patch); + progress.PatchAdded(); + } + catch(Exception e) + { + progress.Exception(urlConfig, $"Exception while parsing pass for config: {urlConfig.SafeUrl()}", e); + + try + { + urlConfig.parent.configs.Remove(urlConfig); } - catch(Exception e) + catch (Exception ex) { - progress.Exception(url, $"Exception while parsing pass for config: {url.SafeUrl()}", e); + logger.Exception("Exception while attempting to clean up bad config", ex); } } - - return list; } - private static bool CheckMod(Match match, PatchList.ModPassCollection modPasses, out string theMod) + private static bool CheckMod(Match match, IPatchList patchList, out string theMod) { theMod = match.Groups[1].Value.Trim().ToLower(); - return modPasses.HasMod(theMod); + return patchList.HasMod(theMod); } } } diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index 974d1d1e..dff2fdb5 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -6,23 +6,43 @@ namespace ModuleManager { - public class PatchList + public interface IPatchList : IEnumerable { - public class ModPass - { - public readonly List beforePatches = new List(0); - public readonly List forPatches = new List(0); - public readonly List afterPatches = new List(0); + bool HasMod(string mod); + void AddFirstPatch(Patch patch); + void AddLegacyPatch(Patch patch); + void AddBeforePatch(string mod, Patch patch); + void AddForPatch(string mod, Patch patch); + void AddAfterPatch(string mod, Patch patch); + void AddFinalPatch(Patch patch); + } + public class PatchList : IPatchList + { + private class ModPass + { public readonly string name; - + public readonly Pass beforePass; + public readonly Pass forPass; + public readonly Pass afterPass; + public ModPass(string name) { - this.name = name; + if (name == null) throw new ArgumentNullException(nameof(name)); + if (name == string.Empty) throw new ArgumentException("can't be blank", nameof(name)); + this.name = name.ToUpperInvariant(); + + beforePass = new Pass($":BEFORE[{this.name}]"); + forPass = new Pass($":FOR[{this.name}]"); + afterPass = new Pass($":AFTER[{this.name}]"); } + + public void AddBeforePatch(Patch patch) => beforePass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + public void AddForPatch(Patch patch) => forPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + public void AddAfterPatch(Patch patch) => afterPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); } - public class ModPassCollection : IEnumerable + private class ModPassCollection : IEnumerable { private readonly ModPass[] passesArray; private readonly Dictionary passesDict; @@ -44,23 +64,96 @@ public ModPassCollection(IEnumerable modList) } public ModPass this[string name] => passesDict[name.ToLowerInvariant()]; + public ModPass this[int index] => passesArray[index]; public bool HasMod(string name) => passesDict.ContainsKey(name.ToLowerInvariant()); + public int Count => passesArray.Length; + public ArrayEnumerator GetEnumerator() => new ArrayEnumerator(passesArray); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - public readonly List firstPatches = new List (); - public readonly List legacyPatches = new List(); - public readonly List finalPatches = new List(); + private readonly Pass firstPatches = new Pass(":FIRST"); + private readonly Pass legacyPatches = new Pass(":LEGACY (default)"); + private readonly Pass finalPatches = new Pass(":FINAL"); - public readonly ModPassCollection modPasses; + private readonly ModPassCollection modPasses; public PatchList(IEnumerable modList) { modPasses = new ModPassCollection(modList); } + + public ArrayEnumerator GetEnumerator() => new ArrayEnumerator(EnumeratePasses()); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool HasMod(string mod) + { + if (mod == null) throw new ArgumentNullException(nameof(mod)); + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + return modPasses.HasMod(mod); + } + + public void AddFirstPatch(Patch patch) + { + firstPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + } + + public void AddLegacyPatch(Patch patch) + { + legacyPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + } + + public void AddBeforePatch(string mod, Patch patch) + { + EnsureMod(mod); + modPasses[mod].AddBeforePatch(patch ?? throw new ArgumentNullException(nameof(patch))); + } + + public void AddForPatch(string mod, Patch patch) + { + EnsureMod(mod); + modPasses[mod].AddForPatch(patch ?? throw new ArgumentNullException(nameof(patch))); + } + + public void AddAfterPatch(string mod, Patch patch) + { + EnsureMod(mod); + modPasses[mod].AddAfterPatch(patch ?? throw new ArgumentNullException(nameof(patch))); + } + + public void AddFinalPatch(Patch patch) + { + finalPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + } + + private IPass[] EnumeratePasses() + { + IPass[] result = new IPass[modPasses.Count * 3 + 3]; + + result[0] = firstPatches; + result[1] = legacyPatches; + + for (int i = 0; i < modPasses.Count; i++) + { + result[i * 3 + 2] = modPasses[i].beforePass; + result[i * 3 + 3] = modPasses[i].forPass; + result[i * 3 + 4] = modPasses[i].afterPass; + } + + result[result.Length - 1] = finalPatches; + + return result; + } + + private void EnsureMod(string mod) + { + if (mod == null) throw new ArgumentNullException(nameof(mod)); + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + if (!HasMod(mod)) throw new KeyNotFoundException($"Mod '{mod}' not found"); + } } } diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 72faea68..23ad13a9 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -61,6 +61,7 @@ + diff --git a/ModuleManagerTests/PassTest.cs b/ModuleManagerTests/PassTest.cs new file mode 100644 index 00000000..85300ef5 --- /dev/null +++ b/ModuleManagerTests/PassTest.cs @@ -0,0 +1,73 @@ +using System; +using Xunit; +using TestUtils; +using ModuleManager; + +namespace ModuleManagerTests +{ + public class PassTest + { + private UrlDir.UrlFile file; + + public PassTest() + { + file = UrlBuilder.CreateFile("abc/def.cfg"); + } + + [Fact] + public void TestConstructor__NameNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new Pass(null); + }); + + Assert.Equal("name", ex.ParamName); + } + + [Fact] + public void TestConstructor__NameEmpty() + { + ArgumentException ex = Assert.Throws(delegate + { + new Pass(""); + }); + + Assert.Equal("can't be empty\r\nParameter name: name", ex.Message); + Assert.Equal("name", ex.ParamName); + } + + [Fact] + public void TestName() + { + Pass pass = new Pass(":NOTINAMILLIONYEARS"); + + Assert.Equal(":NOTINAMILLIONYEARS", pass.Name); + } + + [Fact] + public void Test__Add__Enumerator() + { + Patch[] patches = + { + CreatePatch(Command.Edit, new ConfigNode()), + CreatePatch(Command.Copy, new ConfigNode()), + CreatePatch(Command.Delete, new ConfigNode()), + }; + + Pass pass = new Pass("blah") + { + patches[0], + patches[1], + patches[2], + }; + + Assert.Equal(patches, pass); + } + + private Patch CreatePatch(Command command, ConfigNode node) + { + return new Patch(new UrlDir.UrlConfig(file, node), command, node); + } + } +} diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index 1370095a..5929ddf5 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -5,6 +5,7 @@ using UnityEngine; using TestUtils; using ModuleManager; +using ModuleManager.Collections; using ModuleManager.Extensions; using ModuleManager.Logging; using ModuleManager.Progress; @@ -18,7 +19,10 @@ public class PatchApplierTest private readonly string[] modList = new[] { "mod1", "mod2" }; private UrlDir databaseRoot; private UrlDir.UrlFile file; - private readonly PatchList patchList; + private readonly IPass pass1; + private readonly IPass pass2; + private readonly IPass pass3; + private readonly IPatchList patchList; private readonly PatchApplier patchApplier; public PatchApplierTest() @@ -27,7 +31,14 @@ public PatchApplierTest() progress = Substitute.For(); databaseRoot = UrlBuilder.CreateRoot(); file = UrlBuilder.CreateFile("abc/def.cfg", databaseRoot); - patchList = new PatchList(modList); + pass1 = Substitute.For(); + pass2 = Substitute.For(); + pass3 = Substitute.For(); + pass1.Name.Returns(":PASS1"); + pass2.Name.Returns(":PASS2"); + pass3.Name.Returns(":PASS3"); + patchList = Substitute.For(); + patchList.GetEnumerator().Returns(new ArrayEnumerator(pass1, pass2, pass3)); patchApplier = new PatchApplier(patchList, databaseRoot, progress, logger); } @@ -57,15 +68,21 @@ public void TestApplyPatches__Edit() { "pqr", "stw" }, }); - patchList.firstPatches.Add(patch1); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(1).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config2, patch1.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config2, patch1.urlConfig); + progress.Received(1).PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(3, allConfigs.Length); @@ -117,15 +134,21 @@ public void TestApplyPatches__Copy() { "ccc", "005" }, }); - patchList.firstPatches.Add(patch1); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(1).PatchApplied(); - progress.Received().ApplyingCopy(config1, patch1.urlConfig); - progress.Received().ApplyingCopy(config2, patch1.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingCopy(config1, patch1.urlConfig); + progress.Received().ApplyingCopy(config2, patch1.urlConfig); + progress.Received(1).PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(5, allConfigs.Length); @@ -183,15 +206,21 @@ public void TestApplyPatches__Delete() Patch patch1 = CreatePatch(Command.Delete, new TestConfigNode("-PART")); - patchList.firstPatches.Add(patch1); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(1).PatchApplied(); - progress.Received().ApplyingDelete(config1, patch1.urlConfig); - progress.Received().ApplyingDelete(config2, patch1.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingDelete(config1, patch1.urlConfig); + progress.Received().ApplyingDelete(config2, patch1.urlConfig); + progress.Received(1).PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(1, allConfigs.Length); @@ -245,18 +274,24 @@ public void TestApplyPatches__Name() Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART[004]")); - patchList.firstPatches.Add(patch1); - patchList.firstPatches.Add(patch2); - patchList.firstPatches.Add(patch3); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingCopy(config2, patch2.urlConfig); - progress.Received().ApplyingDelete(config3, patch3.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingCopy(config2, patch2.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingDelete(config3, patch3.urlConfig); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(4, allConfigs.Length); @@ -330,18 +365,24 @@ public void TestApplyPatches__Name__Wildcard() Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART[0*4]")); - patchList.firstPatches.Add(patch1); - patchList.firstPatches.Add(patch2); - patchList.firstPatches.Add(patch3); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingCopy(config2, patch2.urlConfig); - progress.Received().ApplyingDelete(config3, patch3.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingCopy(config2, patch2.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingDelete(config3, patch3.urlConfig); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(4, allConfigs.Length); @@ -400,15 +441,21 @@ public void TestApplyPatches__Name__Or() { "ddd", "006" }, }); - patchList.firstPatches.Add(patch1); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(1).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config2, patch1.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config2, patch1.urlConfig); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(3, allConfigs.Length); @@ -488,30 +535,38 @@ public void TestApplyPatches__Order() { "jjj", "010" }, }); - patchList.firstPatches.Add(patch1); - patchList.legacyPatches.Add(patch2); - patchList.modPasses["mod1"].beforePatches.Add(patch3); - patchList.modPasses["mod1"].forPatches.Add(patch4); - patchList.modPasses["mod1"].afterPatches.Add(patch5); - patchList.modPasses["mod2"].beforePatches.Add(patch6); - patchList.modPasses["mod2"].forPatches.Add(patch7); - patchList.modPasses["mod2"].afterPatches.Add(patch8); - patchList.finalPatches.Add(patch9); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); + pass2.GetEnumerator().Returns(new ArrayEnumerator(patch4, patch5, patch6)); + pass3.GetEnumerator().Returns(new ArrayEnumerator(patch7, patch8, patch9)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(9).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config1, patch2.urlConfig); - progress.Received().ApplyingUpdate(config1, patch3.urlConfig); - progress.Received().ApplyingUpdate(config1, patch4.urlConfig); - progress.Received().ApplyingUpdate(config1, patch5.urlConfig); - progress.Received().ApplyingUpdate(config1, patch6.urlConfig); - progress.Received().ApplyingUpdate(config1, patch7.urlConfig); - progress.Received().ApplyingUpdate(config1, patch8.urlConfig); - progress.Received().ApplyingUpdate(config1, patch9.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingUpdate(config1, patch2.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingUpdate(config1, patch3.urlConfig); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + progress.Received().ApplyingUpdate(config1, patch4.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingUpdate(config1, patch5.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingUpdate(config1, patch6.urlConfig); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + progress.Received().ApplyingUpdate(config1, patch7.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingUpdate(config1, patch8.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingUpdate(config1, patch9.urlConfig); + progress.Received().PatchApplied(); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(1, allConfigs.Length); @@ -574,18 +629,24 @@ public void TestApplyPatches__Constraints() Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART:HAS[#ccc[005]]")); - patchList.firstPatches.Add(patch1); - patchList.firstPatches.Add(patch2); - patchList.firstPatches.Add(patch3); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingCopy(config2, patch2.urlConfig); - progress.Received().ApplyingDelete(config3, patch3.urlConfig); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingCopy(config2, patch2.urlConfig); + progress.Received().PatchApplied(); + progress.Received().ApplyingDelete(config3, patch3.urlConfig); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(4, allConfigs.Length); @@ -633,16 +694,24 @@ public void TestApplyPatches__Loop() new ConfigNode("MM_PATCH_LOOP"), }); - patchList.firstPatches.Add(patch1); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); patchApplier.ApplyPatches(); EnsureNoErrors(); - progress.Received(1).PatchApplied(); - progress.Received(4).ApplyingUpdate(config1, patch1.urlConfig); - - logger.Received().Log(LogType.Log, "Looping on abc/def/@PART:HAS[~aaa[>10]] to abc/def/PART"); + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + logger.Received().Log(LogType.Log, "Looping on abc/def/@PART:HAS[~aaa[>10]] to abc/def/PART"); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().ApplyingUpdate(config1, patch1.urlConfig); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(1, allConfigs.Length); @@ -658,55 +727,6 @@ public void TestApplyPatches__Loop() }, allConfigs[0].config); } - /* - [Fact] - public void TestApplyPatches__InvalidOperator() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "1" }, - }, file); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new ConfigNode("%PART")); - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new ConfigNode("|PART")); - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new ConfigNode("#PART")); - UrlDir.UrlConfig patch4 = new UrlDir.UrlConfig(file, new ConfigNode("*PART")); - UrlDir.UrlConfig patch5 = new UrlDir.UrlConfig(file, new ConfigNode("&PART")); - - patchList.firstPatches.Add(patch1); - patchList.firstPatches.Add(patch2); - patchList.firstPatches.Add(patch3); - patchList.firstPatches.Add(patch4); - patchList.firstPatches.Add(patch5); - - patchApplier.ApplyPatches(); - - progress.DidNotReceiveWithAnyArgs().Error(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - - logger.DidNotReceive().Log(LogType.Error, Arg.Any()); - logger.DidNotReceiveWithAnyArgs().Exception(null, null); - - logger.Received().Log(LogType.Warning, "Invalid command encountered on a patch: abc/def/%PART"); - logger.Received().Log(LogType.Warning, "Invalid command encountered on a patch: abc/def/|PART"); - logger.Received().Log(LogType.Warning, "Invalid command encountered on a patch: abc/def/#PART"); - logger.Received().Log(LogType.Warning, "Invalid command encountered on a patch: abc/def/*PART"); - logger.Received().Log(LogType.Warning, "Invalid command encountered on a patch: abc/def/&PART"); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(1, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "1" }, - }, allConfigs[0].config); - - } - */ - [Fact] public void TestApplyPatches__Copy__NameNotChanged() { @@ -722,21 +742,27 @@ public void TestApplyPatches__Copy__NameNotChanged() { "bbb", "012" }, }); - patchList.firstPatches.Add(patch1); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); patchApplier.ApplyPatches(); progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.Received().Error(patch1.urlConfig, "Error - when applying copy abc/def/+PART to abc/def/PART - the copy needs to have a different name than the parent (use @name = xxx)"); - - logger.DidNotReceive().Log(LogType.Warning, Arg.Any()); - logger.DidNotReceive().Log(LogType.Error, Arg.Any()); logger.DidNotReceiveWithAnyArgs().Exception(null, null); - - progress.Received(1).PatchApplied(); + + progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); progress.DidNotReceiveWithAnyArgs().ApplyingCopy(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); + + Received.InOrder(delegate + { + logger.Received().Log(LogType.Log, ":PASS1 pass"); + progress.Received().Error(patch1.urlConfig, "Error - when applying copy abc/def/+PART to abc/def/PART - the copy needs to have a different name than the parent (use @name = xxx)"); + progress.Received().PatchApplied(); + logger.Received().Log(LogType.Log, ":PASS2 pass"); + logger.Received().Log(LogType.Log, ":PASS3 pass"); + }); UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); Assert.Equal(1, allConfigs.Length); diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 3f9005b1..7871f5e8 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -4,6 +4,7 @@ using NSubstitute; using TestUtils; using ModuleManager; +using ModuleManager.Logging; using ModuleManager.Progress; namespace ModuleManagerTests @@ -14,346 +15,640 @@ public class PatchExtractorTest private UrlDir.UrlFile file; private IPatchProgress progress; + private IPatchList patchList; + private IBasicLogger logger; + private PatchExtractor extractor; public PatchExtractorTest() { root = UrlBuilder.CreateRoot(); file = UrlBuilder.CreateFile("abc/def.cfg", root); + patchList = Substitute.For(); progress = Substitute.For(); + logger = Substitute.For(); + extractor = new PatchExtractor(patchList, progress, logger); } [Fact] - public void TestSortAndExtractPatches() + public void TestConstructor__PatchListNull() { - UrlDir.UrlConfig[] insertConfigs = + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("NODE"), - CreateConfig("NADE"), - }; + new PatchExtractor(null, progress, logger); + }); - UrlDir.UrlConfig[] legacyConfigs = - { - CreateConfig("@NODE"), - CreateConfig("@NADE[foo]:HAS[#bar]"), - }; + Assert.Equal("patchList", ex.ParamName); + } - UrlDir.UrlConfig[] firstConfigs = + [Fact] + public void TestConstructor__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("@NODE:FIRST"), - CreateConfig("@NODE[foo]:HAS[#bar]:FIRST"), - CreateConfig("@NADE:First"), - CreateConfig("@NADE:first"), - }; + new PatchExtractor(patchList, null, logger); + }); - UrlDir.UrlConfig[] finalConfigs = - { - CreateConfig("@NODE:FINAL"), - CreateConfig("@NODE[foo]:HAS[#bar]:FINAL"), - CreateConfig("@NADE:Final"), - CreateConfig("@NADE:final"), - }; + Assert.Equal("progress", ex.ParamName); + } - UrlDir.UrlConfig[] beforeMod1Configs = + [Fact] + public void TestConstructor__LoggerNull() + { + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("@NODE:BEFORE[mod1]"), - CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod1]"), - CreateConfig("@NADE:before[mod1]"), - CreateConfig("@NADE:BEFORE[MOD1]"), - }; + new PatchExtractor(patchList, progress, null); + }); + + Assert.Equal("logger", ex.ParamName); + } + + [Fact] + public void TestExtractPatch__Insert() + { + UrlDir.UrlConfig patchConfig = CreateConfig("NODE"); + + extractor.ExtractPatch(patchConfig); + + AssertNoErrors(); + + Assert.Equal(new[] { patchConfig }, root.AllConfigs); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + progress.DidNotReceive().PatchAdded(); + + EnsureNeedsSatisfied(); + } + + [Fact] + public void TestExtractPatch__First() + { + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FIRST"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FIRST"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:First"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:first"); + + List patches = new List(); + patchList.AddFirstPatch(Arg.Do(patch => patches.Add(patch))); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); + + AssertNoErrors(); + + Assert.Empty(root.AllConfigs); + + Assert.Equal(4, patches.Count); + AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); + AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); + AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); - UrlDir.UrlConfig[] forMod1Configs = + Received.InOrder(delegate { - CreateConfig("@NODE:FOR[mod1]"), - CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod1]"), - CreateConfig("@NADE:for[mod1]"), - CreateConfig("@NADE:FOR[MOD1]"), - }; + patchList.Received().AddFirstPatch(patches[0]); + progress.Received().PatchAdded(); + patchList.Received().AddFirstPatch(patches[1]); + progress.Received().PatchAdded(); + patchList.Received().AddFirstPatch(patches[2]); + progress.Received().PatchAdded(); + patchList.Received().AddFirstPatch(patches[3]); + progress.Received().PatchAdded(); + }); + + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + EnsureNeedsSatisfied(); + } + + [Fact] + public void TestExtractPatch__Legacy() + { + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE"); + + List patches = new List(); + patchList.AddLegacyPatch(Arg.Do(patch => patches.Add(patch))); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + + AssertNoErrors(); + + Assert.Empty(root.AllConfigs); + + Assert.Equal(2, patches.Count); + AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); - UrlDir.UrlConfig[] afterMod1Configs = + Received.InOrder(delegate { - CreateConfig("@NODE:AFTER[mod1]"), - CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod1]"), - CreateConfig("@NADE:after[mod1]"), - CreateConfig("@NADE:AFTER[MOD1]"), - }; + patchList.Received().AddLegacyPatch(patches[0]); + progress.Received().PatchAdded(); + patchList.Received().AddLegacyPatch(patches[1]); + progress.Received().PatchAdded(); + }); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + EnsureNeedsSatisfied(); + } + + [Fact] + public void TestExtractPatch__BeforeMod() + { + patchList.HasMod("mod1").Returns(true); + + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod1]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:BEFORE[mod1]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Before[mod1]"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:before[MOD1]"); + + List patches = new List(); + patchList.AddBeforePatch("mod1", Arg.Do(patch => patches.Add(patch))); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); + + AssertNoErrors(); + + Assert.Empty(root.AllConfigs); + + Assert.Equal(4, patches.Count); + AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); + AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); + AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); - UrlDir.UrlConfig[] beforeMod2Configs = + Received.InOrder(delegate { - CreateConfig("@NODE:BEFORE[mod2]"), - CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod2]"), - CreateConfig("@NADE:before[mod2]"), - CreateConfig("@NADE:BEFORE[MOD2]"), - }; + patchList.Received().AddBeforePatch("mod1", patches[0]); + progress.Received().PatchAdded(); + patchList.Received().AddBeforePatch("mod1", patches[1]); + progress.Received().PatchAdded(); + patchList.Received().AddBeforePatch("mod1", patches[2]); + progress.Received().PatchAdded(); + patchList.Received().AddBeforePatch("mod1", patches[3]); + progress.Received().PatchAdded(); + }); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + EnsureNeedsSatisfied(); + } + + [Fact] + public void TestExtractPatch__BeforeMod__ModDoesNotExist() + { + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod3]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:BEFORE[mod3]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Before[mod3]"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:before[MOD3]"); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); - UrlDir.UrlConfig[] forMod2Configs = + AssertNoErrors(); + + Assert.Empty(root.AllConfigs); + + Received.InOrder(delegate { - CreateConfig("@NODE:FOR[mod2]"), - CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod2]"), - CreateConfig("@NADE:for[mod2]"), - CreateConfig("@NADE:FOR[MOD2]"), - }; + progress.Received().NeedsUnsatisfiedBefore(patchConfig1); + progress.Received().NeedsUnsatisfiedBefore(patchConfig2); + progress.Received().NeedsUnsatisfiedBefore(patchConfig3); + progress.Received().NeedsUnsatisfiedBefore(patchConfig4); + }); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - UrlDir.UrlConfig[] afterMod2Configs = + progress.DidNotReceive().PatchAdded(); + } + + [Fact] + public void TestExtractPatch__ForMod() + { + patchList.HasMod("mod1").Returns(true); + + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod1]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FOR[mod1]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:For[mod1]"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:for[MOD1]"); + + List patches = new List(); + patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); + + AssertNoErrors(); + + Assert.Empty(root.AllConfigs); + + Assert.Equal(4, patches.Count); + AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); + AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); + AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); + + + Received.InOrder(delegate { - CreateConfig("@NODE:AFTER[mod2]"), - CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod2]"), - CreateConfig("@NADE:after[mod2]"), - CreateConfig("@NADE:AFTER[MOD2]"), - }; + patchList.Received().AddForPatch("mod1", patches[0]); + progress.Received().PatchAdded(); + patchList.Received().AddForPatch("mod1", patches[1]); + progress.Received().PatchAdded(); + patchList.Received().AddForPatch("mod1", patches[2]); + progress.Received().PatchAdded(); + patchList.Received().AddForPatch("mod1", patches[3]); + progress.Received().PatchAdded(); + }); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + EnsureNeedsSatisfied(); + } + + [Fact] + public void TestExtractPatch__ForMod__ModDoesNotExist() + { + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod3]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FOR[mod3]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:For[mod3]"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:for[MOD3]"); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); + + AssertNoErrors(); + + Assert.Empty(root.AllConfigs); - UrlDir.UrlConfig[] beforeMod3Configs = + Received.InOrder(delegate { - CreateConfig("@NODE:BEFORE[mod3]"), - CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod3]"), - CreateConfig("@NADE:before[mod3]"), - CreateConfig("@NADE:BEFORE[MOD3]"), - }; + progress.Received().NeedsUnsatisfiedFor(patchConfig1); + progress.Received().NeedsUnsatisfiedFor(patchConfig2); + progress.Received().NeedsUnsatisfiedFor(patchConfig3); + progress.Received().NeedsUnsatisfiedFor(patchConfig4); + }); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + progress.DidNotReceive().PatchAdded(); + } + + [Fact] + public void TestExtractPatch__AfterMod() + { + patchList.HasMod("mod1").Returns(true); + + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod1]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:AFTER[mod1]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:After[mod1]"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:after[MOD1]"); + + List patches = new List(); + patchList.AddAfterPatch("mod1", Arg.Do(patch => patches.Add(patch))); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); + + AssertNoErrors(); + + Assert.Equal(4, patches.Count); + AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); + AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); + AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); - UrlDir.UrlConfig[] forMod3Configs = + Assert.Empty(root.AllConfigs); + + Received.InOrder(delegate { - CreateConfig("@NODE:FOR[mod3]"), - CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod3]"), - CreateConfig("@NADE:for[mod3]"), - CreateConfig("@NADE:FOR[MOD3]"), - }; + patchList.Received().AddAfterPatch("mod1", patches[0]); + progress.Received().PatchAdded(); + patchList.Received().AddAfterPatch("mod1", patches[1]); + progress.Received().PatchAdded(); + patchList.Received().AddAfterPatch("mod1", patches[2]); + progress.Received().PatchAdded(); + patchList.Received().AddAfterPatch("mod1", patches[3]); + progress.Received().PatchAdded(); + }); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + EnsureNeedsSatisfied(); + } + + [Fact] + public void TestExtractPatch__AfterMod__ModDoesNotExist() + { + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod3]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:AFTER[mod3]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:After[mod3]"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:after[MOD3]"); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); + + AssertNoErrors(); + + Assert.Empty(root.AllConfigs); - UrlDir.UrlConfig[] afterMod3Configs = + Received.InOrder(delegate { - CreateConfig("@NODE:AFTER[mod3]"), - CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod3]"), - CreateConfig("@NADE:after[mod3]"), - CreateConfig("@NADE:AFTER[MOD3]"), - }; + progress.Received().NeedsUnsatisfiedAfter(patchConfig1); + progress.Received().NeedsUnsatisfiedAfter(patchConfig2); + progress.Received().NeedsUnsatisfiedAfter(patchConfig3); + progress.Received().NeedsUnsatisfiedAfter(patchConfig4); + }); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - string[] modList = { "mod1", "mod2" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + progress.DidNotReceive().PatchAdded(); + } - progress.DidNotReceiveWithAnyArgs().Error(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + [Fact] + public void TestExtractPatch__Final() + { + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FINAL"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FINAL"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Final"); + UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:final"); + + List patches = new List(); + patchList.AddFinalPatch(Arg.Do(patch => patches.Add(patch))); + + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); + extractor.ExtractPatch(patchConfig4); + + AssertNoErrors(); - Assert.True(list.modPasses.HasMod("mod1")); - Assert.True(list.modPasses.HasMod("mod2")); - Assert.False(list.modPasses.HasMod("mod3")); - - Assert.Equal(insertConfigs, root.AllConfigs); - - List currentPatches; - - currentPatches = list.legacyPatches; - Assert.Equal(legacyConfigs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], legacyConfigs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], legacyConfigs[1], Command.Edit, "NADE[foo]:HAS[#bar]"); - - currentPatches = list.firstPatches; - Assert.Equal(firstConfigs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], firstConfigs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], firstConfigs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], firstConfigs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], firstConfigs[3], Command.Edit, "NADE"); - - currentPatches = list.finalPatches; - Assert.Equal(finalConfigs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], finalConfigs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], finalConfigs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], finalConfigs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], finalConfigs[3], Command.Edit, "NADE"); - - currentPatches = list.modPasses["mod1"].beforePatches; - Assert.Equal(beforeMod1Configs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], beforeMod1Configs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], beforeMod1Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], beforeMod1Configs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], beforeMod1Configs[3], Command.Edit, "NADE"); - - currentPatches = list.modPasses["mod1"].forPatches; - Assert.Equal(forMod1Configs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], forMod1Configs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], forMod1Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], forMod1Configs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], forMod1Configs[3], Command.Edit, "NADE"); - - currentPatches = list.modPasses["mod1"].afterPatches; - Assert.Equal(afterMod1Configs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], afterMod1Configs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], afterMod1Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], afterMod1Configs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], afterMod1Configs[3], Command.Edit, "NADE"); - - currentPatches = list.modPasses["mod2"].beforePatches; - Assert.Equal(beforeMod2Configs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], beforeMod2Configs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], beforeMod2Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], beforeMod2Configs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], beforeMod2Configs[3], Command.Edit, "NADE"); - - currentPatches = list.modPasses["mod2"].forPatches; - Assert.Equal(forMod2Configs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], forMod2Configs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], forMod2Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], forMod2Configs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], forMod2Configs[3], Command.Edit, "NADE"); - - currentPatches = list.modPasses["mod2"].afterPatches; - Assert.Equal(afterMod2Configs.Length, currentPatches.Count); - AssertPatchCorrect(currentPatches[0], afterMod2Configs[0], Command.Edit, "NODE"); - AssertPatchCorrect(currentPatches[1], afterMod2Configs[1], Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(currentPatches[2], afterMod2Configs[2], Command.Edit, "NADE"); - AssertPatchCorrect(currentPatches[3], afterMod2Configs[3], Command.Edit, "NADE"); - - progress.Received(34).PatchAdded(); - - progress.Received().NeedsUnsatisfiedBefore(beforeMod3Configs[0]); - progress.Received().NeedsUnsatisfiedBefore(beforeMod3Configs[1]); - progress.Received().NeedsUnsatisfiedBefore(beforeMod3Configs[2]); - progress.Received().NeedsUnsatisfiedBefore(beforeMod3Configs[3]); - - progress.Received().NeedsUnsatisfiedFor(forMod3Configs[0]); - progress.Received().NeedsUnsatisfiedFor(forMod3Configs[1]); - progress.Received().NeedsUnsatisfiedFor(forMod3Configs[2]); - progress.Received().NeedsUnsatisfiedFor(forMod3Configs[3]); - - progress.Received().NeedsUnsatisfiedAfter(afterMod3Configs[0]); - progress.Received().NeedsUnsatisfiedAfter(afterMod3Configs[1]); - progress.Received().NeedsUnsatisfiedAfter(afterMod3Configs[2]); - progress.Received().NeedsUnsatisfiedAfter(afterMod3Configs[3]); + Assert.Empty(root.AllConfigs); + + Assert.Equal(4, patches.Count); + AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); + AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); + AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); + AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); + + Received.InOrder(delegate + { + patchList.Received().AddFinalPatch(patches[0]); + progress.Received().PatchAdded(); + patchList.Received().AddFinalPatch(patches[1]); + progress.Received().PatchAdded(); + patchList.Received().AddFinalPatch(patches[2]); + progress.Received().PatchAdded(); + patchList.Received().AddFinalPatch(patches[3]); + progress.Received().PatchAdded(); + }); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + + EnsureNeedsSatisfied(); } [Fact] - public void TestSortAndExtractPatches__InsertWithPass() + public void TestExtractPatch__InsertWithPass() { - UrlDir.UrlConfig config1 = CreateConfig("NODE"); - UrlDir.UrlConfig config2 = CreateConfig("NODE:FOR[mod1]"); - UrlDir.UrlConfig config3 = CreateConfig("NODE:FOR[mod2]"); - UrlDir.UrlConfig config4 = CreateConfig("NODE:FINAL"); + UrlDir.UrlConfig patchConfig1 = CreateConfig("NODE:FOR[mod1]"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("NODE:FOR[mod2]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("NODE:FINAL"); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); - Assert.Equal(new[] { config1 }, root.AllConfigs); + Assert.Empty(root.AllConfigs); - progress.Received().Error(config2, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FOR[mod1]"); - progress.Received().Error(config3, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FOR[mod2]"); - progress.Received().Error(config4, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FINAL"); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - Assert.Empty(list.firstPatches); - Assert.Empty(list.legacyPatches); - Assert.Empty(list.finalPatches); - Assert.Empty(list.modPasses["mod1"].beforePatches); - Assert.Empty(list.modPasses["mod1"].forPatches); - Assert.Empty(list.modPasses["mod1"].afterPatches); + Received.InOrder(delegate + { + progress.Received().Error(patchConfig1, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FOR[mod1]"); + progress.Received().Error(patchConfig2, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FOR[mod2]"); + progress.Received().Error(patchConfig3, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FINAL"); + }); + + EnsureNeedsSatisfied(); progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestSortAndExtractPatches__MoreThanOnePass() + public void TestExtractPatch__MoreThanOnePass() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE:FIRST"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:FIRST:FIRST"); - UrlDir.UrlConfig config3 = CreateConfig("@NODE:FIRST:FOR[mod1]"); + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE:FIRST:FIRST"); + UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FIRST:FOR[mod1]"); + UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:FOR[mod1]:AFTER[mod2]"); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + extractor.ExtractPatch(patchConfig1); + extractor.ExtractPatch(patchConfig2); + extractor.ExtractPatch(patchConfig3); Assert.Empty(root.AllConfigs); - progress.Received().Error(config2, "Error - more than one pass specifier on a node: abc/def/@NODE:FIRST:FIRST"); - progress.Received().Error(config3, "Error - more than one pass specifier on a node: abc/def/@NODE:FIRST:FOR[mod1]"); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + Received.InOrder(delegate + { + progress.Received().Error(patchConfig1, "Error - more than one pass specifier on a node: abc/def/@NODE:FIRST:FIRST"); + progress.Received().Error(patchConfig2, "Error - more than one pass specifier on a node: abc/def/@NODE:FIRST:FOR[mod1]"); + progress.Received().Error(patchConfig3, "Error - more than one pass specifier on a node: abc/def/@NODE:FOR[mod1]:AFTER[mod2]"); + }); - Assert.Equal(1, list.firstPatches.Count); - AssertPatchCorrect(list.firstPatches[0], config1, Command.Edit, "NODE"); - Assert.Empty(list.legacyPatches); - Assert.Empty(list.finalPatches); - Assert.Empty(list.modPasses["mod1"].beforePatches); - Assert.Empty(list.modPasses["mod1"].forPatches); - Assert.Empty(list.modPasses["mod1"].afterPatches); + EnsureNeedsSatisfied(); - progress.Received(1).PatchAdded(); + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestSortAndExtractPatches__Exception() + public void TestExtractPatch__Exception() { Exception e = new Exception("an exception was thrown"); progress.WhenForAnyArgs(p => p.Error(null, null)).Throw(e); - UrlDir.UrlConfig config1 = CreateConfig("@NODE"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:FIRST:FIRST"); - UrlDir.UrlConfig config3 = CreateConfig("@NADE:FIRST"); + UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE:FIRST:FIRST"); + + extractor.ExtractPatch(patchConfig1); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + Assert.Empty(root.AllConfigs); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.Received().Exception(config2, "Exception while parsing pass for config: abc/def/@NODE:FIRST:FIRST", e); + progress.Received().Exception(patchConfig1, "Exception while parsing pass for config: abc/def/@NODE:FIRST:FIRST", e); - Assert.Equal(1, list.legacyPatches.Count); - AssertPatchCorrect(list.legacyPatches[0], config1, Command.Edit, "NODE"); - Assert.Equal(1, list.firstPatches.Count); - AssertPatchCorrect(list.firstPatches[0], config3, Command.Edit, "NADE"); + EnsureNeedsSatisfied(); - progress.Received(2).PatchAdded(); + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestSortAndExtractPatches__NotBracketBalanced() + public void TestExtractPatch__NotBracketBalanced() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR[mod1]"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:FOR["); - UrlDir.UrlConfig config3 = CreateConfig("NODE:HAS[#foo[]"); + UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR["); + UrlDir.UrlConfig config2 = CreateConfig("NODE:HAS[#foo[]"); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + extractor.ExtractPatch(config1); + extractor.ExtractPatch(config2); Assert.Empty(root.AllConfigs); - progress.Received().Error(config2, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/@NODE:FOR["); - progress.Received().Error(config3, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/NODE:HAS[#foo[]"); - - Assert.Empty(list.firstPatches); - Assert.Empty(list.legacyPatches); - Assert.Empty(list.finalPatches); - Assert.Empty(list.modPasses["mod1"].beforePatches); - Assert.Equal(1, list.modPasses["mod1"].forPatches.Count); - AssertPatchCorrect(list.modPasses["mod1"].forPatches[0], config1, Command.Edit, "NODE"); - Assert.Empty(list.modPasses["mod1"].afterPatches); - - progress.Received(1).PatchAdded(); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + Received.InOrder(delegate + { + progress.Received().Error(config1, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/@NODE:FOR["); + progress.Received().Error(config2, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/NODE:HAS[#foo[]"); + }); + + EnsureNeedsSatisfied(); + + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestSortAndExtractPatches__BadlyFormed() + public void TestExtractPatch__BadlyFormed() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR[mod1]"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:FOR[]"); - UrlDir.UrlConfig config3 = CreateConfig("@NADE:FIRST:BEFORE"); - UrlDir.UrlConfig config4 = CreateConfig("@NADE:AFTER"); - - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + UrlDir.UrlConfig config1 = CreateConfig("@NODE[foo]:HAS[#bar]:FOR[]"); + UrlDir.UrlConfig config2 = CreateConfig("@NODE:BEFORE"); + UrlDir.UrlConfig config3 = CreateConfig("@NODE:AFTER"); + + extractor.ExtractPatch(config1); + extractor.ExtractPatch(config2); + extractor.ExtractPatch(config3); Assert.Empty(root.AllConfigs); - progress.Received().Error(config2, "Error - malformed :FOR patch specifier detected: abc/def/@NODE:FOR[]"); - progress.Received().Error(config3, "Error - more than one pass specifier on a node: abc/def/@NADE:FIRST:BEFORE"); - progress.Received().Error(config3, "Error - malformed :BEFORE patch specifier detected: abc/def/@NADE:FIRST:BEFORE"); - progress.Received().Error(config4, "Error - malformed :AFTER patch specifier detected: abc/def/@NADE:AFTER"); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + + Received.InOrder(delegate + { + progress.Received().Error(config1, "Error - malformed :FOR patch specifier detected: abc/def/@NODE[foo]:HAS[#bar]:FOR[]"); + progress.Received().Error(config2, "Error - malformed :BEFORE patch specifier detected: abc/def/@NODE:BEFORE"); + progress.Received().Error(config3, "Error - malformed :AFTER patch specifier detected: abc/def/@NODE:AFTER"); + }); - Assert.Empty(list.firstPatches); - Assert.Empty(list.legacyPatches); - Assert.Empty(list.finalPatches); - Assert.Empty(list.modPasses["mod1"].beforePatches); - Assert.Equal(1, list.modPasses["mod1"].forPatches.Count); - AssertPatchCorrect(list.modPasses["mod1"].forPatches[0], config1, Command.Edit, "NODE"); - Assert.Empty(list.modPasses["mod1"].afterPatches); + EnsureNeedsSatisfied(); - progress.Received(1).PatchAdded(); + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestSortAndExtractPatches__Command() + public void TestExtractPatch__Command() { + patchList.HasMod("mod1").Returns(true); + UrlDir.UrlConfig config01 = CreateConfig("@NODE:FOR[mod1]"); UrlDir.UrlConfig config02 = CreateConfig("+NODE:FOR[mod1]"); UrlDir.UrlConfig config03 = CreateConfig("$NODE:FOR[mod1]"); @@ -365,30 +660,59 @@ public void TestSortAndExtractPatches__Command() UrlDir.UrlConfig config09 = CreateConfig("#NODE:FOR[mod1]"); UrlDir.UrlConfig config10 = CreateConfig("*NODE:FOR[mod1]"); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + List patches = new List(); + patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); + + extractor.ExtractPatch(config01); + extractor.ExtractPatch(config02); + extractor.ExtractPatch(config03); + extractor.ExtractPatch(config04); + extractor.ExtractPatch(config05); + extractor.ExtractPatch(config06); + extractor.ExtractPatch(config07); + extractor.ExtractPatch(config08); + extractor.ExtractPatch(config09); + extractor.ExtractPatch(config10); + + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); Assert.Empty(root.AllConfigs); - progress.Received().Error(config06, "Error - replace command (%) is not valid on a root node: abc/def/%NODE:FOR[mod1]"); - progress.Received().Error(config07, "Error - create command (&) is not valid on a root node: abc/def/&NODE:FOR[mod1]"); - progress.Received().Error(config08, "Error - rename command (|) is not valid on a root node: abc/def/|NODE:FOR[mod1]"); - progress.Received().Error(config09, "Error - paste command (#) is not valid on a root node: abc/def/#NODE:FOR[mod1]"); - progress.Received().Error(config10, "Error - special command (*) is not valid on a root node: abc/def/*NODE:FOR[mod1]"); - - Assert.Empty(list.firstPatches); - Assert.Empty(list.legacyPatches); - Assert.Empty(list.finalPatches); - Assert.Empty(list.modPasses["mod1"].beforePatches); - Assert.Equal(5, list.modPasses["mod1"].forPatches.Count); - AssertPatchCorrect(list.modPasses["mod1"].forPatches[0], config01, Command.Edit, "NODE"); - AssertPatchCorrect(list.modPasses["mod1"].forPatches[1], config02, Command.Copy, "NODE"); - AssertPatchCorrect(list.modPasses["mod1"].forPatches[2], config03, Command.Copy, "NODE"); - AssertPatchCorrect(list.modPasses["mod1"].forPatches[3], config04, Command.Delete, "NODE"); - AssertPatchCorrect(list.modPasses["mod1"].forPatches[4], config05, Command.Delete, "NODE"); - Assert.Empty(list.modPasses["mod1"].afterPatches); - - progress.Received(5).PatchAdded(); + Assert.Equal(5, patches.Count); + AssertPatchCorrect(patches[0], config01, Command.Edit, "NODE"); + AssertPatchCorrect(patches[1], config02, Command.Copy, "NODE"); + AssertPatchCorrect(patches[2], config03, Command.Copy, "NODE"); + AssertPatchCorrect(patches[3], config04, Command.Delete, "NODE"); + AssertPatchCorrect(patches[4], config05, Command.Delete, "NODE"); + progress.Received().PatchAdded(); + + Received.InOrder(delegate + { + patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[0]); + progress.Received().PatchAdded(); + patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[1]); + progress.Received().PatchAdded(); + patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[2]); + progress.Received().PatchAdded(); + patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[3]); + progress.Received().PatchAdded(); + patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[4]); + progress.Received().PatchAdded(); + progress.Received().Error(config06, "Error - replace command (%) is not valid on a root node: abc/def/%NODE:FOR[mod1]"); + progress.Received().Error(config07, "Error - create command (&) is not valid on a root node: abc/def/&NODE:FOR[mod1]"); + progress.Received().Error(config08, "Error - rename command (|) is not valid on a root node: abc/def/|NODE:FOR[mod1]"); + progress.Received().Error(config09, "Error - paste command (#) is not valid on a root node: abc/def/#NODE:FOR[mod1]"); + progress.Received().Error(config10, "Error - special command (*) is not valid on a root node: abc/def/*NODE:FOR[mod1]"); + }); + + EnsureNeedsSatisfied(); + + patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); + patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); + patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); } private UrlDir.UrlConfig CreateConfig(string name) @@ -410,6 +734,7 @@ private UrlDir.UrlConfig CreateConfig(string name) private void AssertPatchCorrect(Patch patch, UrlDir.UrlConfig originalUrl, Command expectedCommand, string expectedNodeName) { Assert.Same(originalUrl, patch.urlConfig); + Assert.Equal(expectedCommand, patch.command); Assert.Equal(expectedNodeName, patch.node.name); ConfigNode originalNode = originalUrl.config; @@ -427,6 +752,21 @@ private void AssertPatchCorrect(Patch patch, UrlDir.UrlConfig originalUrl, Comma { Assert.Same(originalNode.nodes[i], patch.node.nodes[i]); } + + } + + private void AssertNoErrors() + { + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + private void EnsureNeedsSatisfied() + { + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); } } } diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index dc19b426..ce252c13 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -1,86 +1,304 @@ using System; using System.Collections.Generic; +using System.Linq; using Xunit; +using TestUtils; using ModuleManager; namespace ModuleManagerTests { public class PatchListTest { + private UrlDir databaseRoot; + private UrlDir.UrlFile file; + private PatchList patchList; + + public PatchListTest() + { + databaseRoot = UrlBuilder.CreateRoot(); + file = UrlBuilder.CreateFile("abc/def.cfg", databaseRoot); + + patchList = new PatchList(new[] { "mod1", "mod2" }); + } + + [Fact] + public void Test__Lifecycle() + { + Patch patch01 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch02 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch03 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch04 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch05 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch06 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch07 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch08 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch09 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch10 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch11 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch12 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch13 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch14 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch15 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch16 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch17 = CreatePatch(Command.Edit, new ConfigNode("blah")); + Patch patch18 = CreatePatch(Command.Edit, new ConfigNode("blah")); + + patchList.AddFirstPatch(patch01); + patchList.AddFirstPatch(patch02); + patchList.AddLegacyPatch(patch03); + patchList.AddLegacyPatch(patch04); + patchList.AddBeforePatch("mod1", patch05); + patchList.AddBeforePatch("MOD1", patch06); + patchList.AddForPatch("mod1", patch07); + patchList.AddForPatch("MOD1", patch08); + patchList.AddAfterPatch("mod1", patch09); + patchList.AddAfterPatch("MOD1", patch10); + patchList.AddBeforePatch("mod2", patch11); + patchList.AddBeforePatch("MOD2", patch12); + patchList.AddForPatch("mod2", patch13); + patchList.AddForPatch("MOD2", patch14); + patchList.AddAfterPatch("mod2", patch15); + patchList.AddAfterPatch("MOD2", patch16); + patchList.AddFinalPatch(patch17); + patchList.AddFinalPatch(patch18); + + IPass[] passes = patchList.ToArray(); + + Assert.Equal(":FIRST", passes[0].Name); + Assert.Equal(new[] { patch01, patch02 }, passes[0]); + + Assert.Equal(":LEGACY (default)", passes[1].Name); + Assert.Equal(new[] { patch03, patch04 }, passes[1]); + + Assert.Equal(":BEFORE[MOD1]", passes[2].Name); + Assert.Equal(new[] { patch05, patch06 }, passes[2]); + + Assert.Equal(":FOR[MOD1]", passes[3].Name); + Assert.Equal(new[] { patch07, patch08 }, passes[3]); + + Assert.Equal(":AFTER[MOD1]", passes[4].Name); + Assert.Equal(new[] { patch09, patch10 }, passes[4]); + + Assert.Equal(":BEFORE[MOD2]", passes[5].Name); + Assert.Equal(new[] { patch11, patch12 }, passes[5]); + + Assert.Equal(":FOR[MOD2]", passes[6].Name); + Assert.Equal(new[] { patch13, patch14 }, passes[6]); + + Assert.Equal(":AFTER[MOD2]", passes[7].Name); + Assert.Equal(new[] { patch15, patch16 }, passes[7]); + + Assert.Equal(":FINAL", passes[8].Name); + Assert.Equal(new[] { patch17, patch18 }, passes[8]); + } + + [Fact] + public void TestHasMod__True() + { + patchList = new PatchList(new[] { "mod1", "Mod2", "MOD3" }); + + Assert.True(patchList.HasMod("mod1")); + Assert.True(patchList.HasMod("Mod1")); + Assert.True(patchList.HasMod("MOD1")); + Assert.True(patchList.HasMod("mod2")); + Assert.True(patchList.HasMod("Mod2")); + Assert.True(patchList.HasMod("MOD2")); + Assert.True(patchList.HasMod("mod3")); + Assert.True(patchList.HasMod("Mod3")); + Assert.True(patchList.HasMod("MOD3")); + } + + [Fact] + public void TestHasMod__False() + { + Assert.False(patchList.HasMod("mod3")); + Assert.False(patchList.HasMod("Mod3")); + Assert.False(patchList.HasMod("MOD3")); + } + + [Fact] + public void TestHasMod__Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + Assert.True(patchList.HasMod(null)); + }); + + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestHasMod__Blank() + { + ArgumentException ex = Assert.Throws(delegate + { + Assert.True(patchList.HasMod("")); + }); + + Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestAddLegacyPatch__Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddLegacyPatch(null); + }); + + Assert.Equal("patch", ex.ParamName); + } + + [Fact] + public void TestAddBeforePatch__ModNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddBeforePatch(null, CreatePatch(Command.Edit, new ConfigNode())); + }); + + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestAddBeforePatch__ModBlank() + { + ArgumentException ex = Assert.Throws(delegate + { + patchList.AddBeforePatch("", CreatePatch(Command.Edit, new ConfigNode())); + }); + + Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Equal("mod", ex.ParamName); + } + [Fact] - public void TestConstructor() + public void TestAddBeforePatch__PatchNull() { - PatchList list = new PatchList(new string[0]); + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddBeforePatch("mod1", null); + }); - Assert.NotNull(list.firstPatches); - Assert.NotNull(list.legacyPatches); - Assert.NotNull(list.finalPatches); - Assert.NotNull(list.modPasses); + Assert.Equal("patch", ex.ParamName); } [Fact] - public void TestModPasses__HasMod() + public void TestAddBeforePatch__ModDoesNotExist() { - PatchList list = new PatchList(new[] { "mod1", "Mod2", "MOD3" }); + KeyNotFoundException ex = Assert.Throws(delegate + { + patchList.AddBeforePatch("mod3", CreatePatch(Command.Edit, new ConfigNode())); + }); - PatchList.ModPassCollection collection = list.modPasses; + Assert.Equal("Mod 'mod3' not found", ex.Message); + } - Assert.True(collection.HasMod("mod1")); - Assert.True(collection.HasMod("Mod1")); - Assert.True(collection.HasMod("MOD1")); + [Fact] + public void TestAddForPatch__ModNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddForPatch(null, CreatePatch(Command.Edit, new ConfigNode())); + }); - Assert.True(collection.HasMod("mod2")); - Assert.True(collection.HasMod("Mod2")); - Assert.True(collection.HasMod("MOD2")); + Assert.Equal("mod", ex.ParamName); + } - Assert.True(collection.HasMod("mod3")); - Assert.True(collection.HasMod("Mod3")); - Assert.True(collection.HasMod("MOD3")); + [Fact] + public void TestAddForPatch__ModBlank() + { + ArgumentException ex = Assert.Throws(delegate + { + patchList.AddForPatch("", CreatePatch(Command.Edit, new ConfigNode())); + }); - Assert.False(collection.HasMod("mod4")); - Assert.False(collection.HasMod("Mod4")); - Assert.False(collection.HasMod("MOD4")); + Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Equal("mod", ex.ParamName); } [Fact] - public void TestModPasses__Accessor() + public void TestAddForPatch__PatchNull() { - PatchList list = new PatchList(new[] { "mod1", "mod2" }); + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddForPatch("mod1", null); + }); - PatchList.ModPass pass1 = list.modPasses["mod1"]; - Assert.NotNull(pass1); - Assert.Equal("mod1", pass1.name); - Assert.NotNull(pass1.beforePatches); - Assert.Equal(0, pass1.beforePatches.Capacity); - Assert.NotNull(pass1.forPatches); - Assert.Equal(0, pass1.forPatches.Capacity); - Assert.NotNull(pass1.afterPatches); - Assert.Equal(0, pass1.afterPatches.Capacity); + Assert.Equal("patch", ex.ParamName); + } - PatchList.ModPass pass2 = list.modPasses["mod2"]; - Assert.NotNull(pass2); - Assert.Equal("mod2", pass2.name); - Assert.NotNull(pass2.beforePatches); - Assert.Equal(0, pass2.beforePatches.Capacity); - Assert.NotNull(pass2.forPatches); - Assert.Equal(0, pass2.forPatches.Capacity); - Assert.NotNull(pass2.afterPatches); - Assert.Equal(0, pass2.afterPatches.Capacity); + [Fact] + public void TestAddForPatch__ModDoesNotExist() + { + KeyNotFoundException ex = Assert.Throws(delegate + { + patchList.AddForPatch("mod3", CreatePatch(Command.Edit, new ConfigNode())); + }); + + Assert.Equal("Mod 'mod3' not found", ex.Message); + } - Assert.Throws(delegate + [Fact] + public void TestAddAfterPatch__ModNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddAfterPatch(null, CreatePatch(Command.Edit, new ConfigNode())); + }); + + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestAddAfterPatch__ModBlank() + { + ArgumentException ex = Assert.Throws(delegate + { + patchList.AddAfterPatch("", CreatePatch(Command.Edit, new ConfigNode())); + }); + + Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestAddAfterPatch__PatchNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddAfterPatch("mod1", null); + }); + + Assert.Equal("patch", ex.ParamName); + } + + [Fact] + public void TestAddAddafterPatch__ModDoesNotExist() + { + KeyNotFoundException ex = Assert.Throws(delegate { - PatchList.ModPass mod3 = list.modPasses["mod3"]; + patchList.AddAfterPatch("mod3", CreatePatch(Command.Edit, new ConfigNode())); }); + + Assert.Equal("Mod 'mod3' not found", ex.Message); } [Fact] - public void TestModPasses__Enumeration() + public void TestAddFinalPatch__Null() { - PatchList list = new PatchList(new[] { "mod1", "mod2" }); + ArgumentNullException ex = Assert.Throws(delegate + { + patchList.AddFinalPatch(null); + }); - PatchList.ModPass[] passes = new PatchList.ModPass[] { list.modPasses["mod1"], list.modPasses["mod2"] }; + Assert.Equal("patch", ex.ParamName); + } - Assert.Equal(passes, list.modPasses); + private Patch CreatePatch(Command command, ConfigNode node) + { + return new Patch(new UrlDir.UrlConfig(file, node), command, node); } } } From fef311054bbd8b0298e7af79e09acd935a4409d3 Mon Sep 17 00:00:00 2001 From: blowfish Date: Wed, 11 Apr 2018 22:18:41 -0700 Subject: [PATCH 011/140] Allow null value in wildcard match Allows for instance :HAS[#someValue] instead of :HAS[#someValue[*]] --- ModuleManager/MMPatchLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index f713c0c3..5da80d98 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -1799,7 +1799,7 @@ public static bool CheckConstraints(ConfigNode node, string constraints) public static bool WildcardMatchValues(ConfigNode node, string type, string value) { double val = 0; - bool compare = value.Length > 1 && (value[0] == '<' || value[0] == '>'); + bool compare = value != null && value.Length > 1 && (value[0] == '<' || value[0] == '>'); compare = compare && double.TryParse(value.Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out val); string[] values = node.GetValues(type); From e134c5049ff49671538e6a64e9e494bf3f241a0b Mon Sep 17 00:00:00 2001 From: blowfish Date: Wed, 11 Apr 2018 23:02:38 -0700 Subject: [PATCH 012/140] Extract node matcher Will eventually be useful in modifying nodes too, but not without some refactoring. For now it is only used on root patches. --- ModuleManager/ModuleManager.csproj | 1 + ModuleManager/NodeMatcher.cs | 88 +++++ ModuleManager/Patch.cs | 3 + ModuleManager/PatchApplier.cs | 32 +- ModuleManagerTests/ModuleManagerTests.csproj | 1 + ModuleManagerTests/NodeMatcherTest.cs | 333 +++++++++++++++++++ ModuleManagerTests/PatchExtractorTest.cs | 53 +++ ModuleManagerTests/PatchTest.cs | 58 ++++ 8 files changed, 541 insertions(+), 28 deletions(-) create mode 100644 ModuleManager/NodeMatcher.cs create mode 100644 ModuleManagerTests/NodeMatcherTest.cs diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 4e0bd9da..c73adca4 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -57,6 +57,7 @@ + diff --git a/ModuleManager/NodeMatcher.cs b/ModuleManager/NodeMatcher.cs new file mode 100644 index 00000000..cef87865 --- /dev/null +++ b/ModuleManager/NodeMatcher.cs @@ -0,0 +1,88 @@ +using System; +using ModuleManager.Extensions; + +namespace ModuleManager +{ + public interface INodeMatcher + { + bool IsMatch(ConfigNode node); + } + + public class NodeMatcher : INodeMatcher + { + private static readonly char[] sep = { '[', ']' }; + + private string type; + private string[] namePatterns = null; + private string constraints = ""; + + public NodeMatcher(string nodeName) + { + if (nodeName == null) throw new ArgumentNullException(nameof(nodeName)); + if (nodeName == "") throw new ArgumentException("can't be empty", nameof(nodeName)); + if (!nodeName.IsBracketBalanced()) throw new FormatException("node name is not bracket balanced: " + nodeName); + string name = nodeName; + + int indexOfHas = name.IndexOf(":HAS[", StringComparison.InvariantCultureIgnoreCase); + + if (indexOfHas == 0) + { + throw new FormatException("node name cannot begin with :HAS : " + nodeName); + } + else if (indexOfHas > 0) + { + int closingBracketIndex = name.LastIndexOf(']', name.Length - 1, name.Length - indexOfHas - 1); + // Really shouldn't happen if we're bracket balanced but just in case + if (closingBracketIndex == -1) throw new FormatException("Malformed :HAS[] block detected: " + nodeName); + + constraints = name.Substring(indexOfHas + 5, closingBracketIndex - indexOfHas - 5); + name = name.Substring(0, indexOfHas); + } + + int bracketIndex = name.IndexOf('['); + if (bracketIndex == 0) + { + throw new FormatException("node name cannot begin with a bracket: " + nodeName); + } + else if (bracketIndex > 0) + { + int closingBracketIndex = name.LastIndexOf(']', name.Length - 1, name.Length - bracketIndex - 1); + // Really shouldn't happen if we're bracket balanced but just in case + if (closingBracketIndex == -1) throw new FormatException("Malformed brackets detected: " + nodeName); + string patterns = name.Substring(bracketIndex + 1, closingBracketIndex - bracketIndex - 1); + namePatterns = patterns.Split(',', '|'); + type = name.Substring(0, bracketIndex); + } + else + { + type = name; + namePatterns = null; + } + } + + public bool IsMatch(ConfigNode node) + { + if (node.name != type) return false; + + if (namePatterns != null) + { + string name = node.GetValue("name"); + if (name == null) return false; + + bool match = false; + foreach (string pattern in namePatterns) + { + if (MMPatchLoader.WildcardMatch(name, pattern)) + { + match = true; + break; + } + } + + if (!match) return false; + } + + return MMPatchLoader.CheckConstraints(node, constraints); + } + } +} diff --git a/ModuleManager/Patch.cs b/ModuleManager/Patch.cs index ba443205..1eb488f7 100644 --- a/ModuleManager/Patch.cs +++ b/ModuleManager/Patch.cs @@ -7,6 +7,7 @@ public class Patch public readonly UrlDir.UrlConfig urlConfig; public readonly Command command; public readonly ConfigNode node; + public readonly INodeMatcher nodeMatcher; public Patch(UrlDir.UrlConfig urlConfig, Command command, ConfigNode node) { @@ -16,6 +17,8 @@ public Patch(UrlDir.UrlConfig urlConfig, Command command, ConfigNode node) this.urlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); this.command = command; this.node = node ?? throw new ArgumentNullException(nameof(node)); + + nodeMatcher = new NodeMatcher(node.name); } } } diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index c82ee866..68de463f 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -83,14 +83,14 @@ private void ApplyPatches(IPass pass) { foreach (UrlDir.UrlConfig url in file.configs) { - if (!IsMatch(url, type, patterns, condition)) continue; + if (!patch.nodeMatcher.IsMatch(url.config)) continue; if (loop) logger.Info($"Looping on {patch.urlConfig.SafeUrl()} to {url.SafeUrl()}"); do { progress.ApplyingUpdate(url, patch.urlConfig); url.config = MMPatchLoader.ModifyNode(new NodeStack(url.config), patch.node, context); - } while (loop && IsMatch(url, type, patterns, condition)); + } while (loop && patch.nodeMatcher.IsMatch(url.config)); if (loop) url.config.RemoveNodes("MM_PATCH_LOOP"); } @@ -102,7 +102,7 @@ private void ApplyPatches(IPass pass) for (int i = 0; i < count; i++) { UrlDir.UrlConfig url = file.configs[i]; - if (!IsMatch(url, type, patterns, condition)) continue; + if (!patch.nodeMatcher.IsMatch(url.config)) continue; ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(url.config), patch.node, context); if (url.config.HasValue("name") && url.config.GetValue("name") == clone.GetValue("name")) @@ -123,7 +123,7 @@ private void ApplyPatches(IPass pass) { UrlDir.UrlConfig url = file.configs[i]; - if (IsMatch(url, type, patterns, condition)) + if (patch.nodeMatcher.IsMatch(url.config)) { progress.ApplyingDelete(url, patch.urlConfig); file.configs.RemoveAt(i); @@ -156,29 +156,5 @@ private void ApplyPatches(IPass pass) } } } - - private static bool IsMatch(UrlDir.UrlConfig url, string type, string[] namePatterns, string constraints) - { - if (url.type != type) return false; - - if (namePatterns != null) - { - if (url.name == url.type) return false; - - bool match = false; - foreach (string pattern in namePatterns) - { - if (MMPatchLoader.WildcardMatch(url.name, pattern)) - { - match = true; - break; - } - } - - if (!match) return false; - } - - return MMPatchLoader.CheckConstraints(url.config, constraints); - } } } diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 23ad13a9..fe099e9c 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -60,6 +60,7 @@ + diff --git a/ModuleManagerTests/NodeMatcherTest.cs b/ModuleManagerTests/NodeMatcherTest.cs new file mode 100644 index 00000000..d1f1baf3 --- /dev/null +++ b/ModuleManagerTests/NodeMatcherTest.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit; +using TestUtils; +using ModuleManager; + +namespace ModuleManagerTests +{ + public class NodeMatcherTest + { + #region Constructor + + [Fact] + public void TestConstructor__Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new NodeMatcher(null); + }); + + Assert.Equal("nodeName", ex.ParamName); + } + + [Fact] + public void TestConstructor__Blank() + { + ArgumentException ex = Assert.Throws(delegate + { + new NodeMatcher(""); + }); + + Assert.Equal("nodeName", ex.ParamName); + Assert.Equal("can't be empty\r\nParameter name: nodeName", ex.Message); + } + + [Fact] + public void TestConstructor__NotBracketBalanced() + { + FormatException ex = Assert.Throws(delegate + { + new NodeMatcher("NODE[stuff"); + }); + + Assert.Equal("node name is not bracket balanced: NODE[stuff", ex.Message); + } + + [Fact] + public void TestConstructor__StartsWithHas() + { + FormatException ex = Assert.Throws(delegate + { + new NodeMatcher(":HAS[#blah]"); + }); + + Assert.Equal("node name cannot begin with :HAS : :HAS[#blah]", ex.Message); + } + + [Fact] + public void TestConstructor__StartsWithBracket() + { + FormatException ex = Assert.Throws(delegate + { + new NodeMatcher("[#blah]"); + }); + + Assert.Equal("node name cannot begin with a bracket: [#blah]", ex.Message); + } + + #endregion + + #region IsMatch + + [Fact] + public void TestIsMatch() + { + NodeMatcher matcher = new NodeMatcher("NODE"); + + Assert.True(matcher.IsMatch(new ConfigNode("NODE"))); + Assert.False(matcher.IsMatch(new ConfigNode("PART"))); + } + + [Fact] + public void TestIsMatch__Name() + { + NodeMatcher matcher = new NodeMatcher("NODE[blah]"); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("NODE"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "bleh" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("PART"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "blah" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "bleh" }, + })); + } + + [Fact] + public void TestIsMatch__Name__Wildcard() + { + NodeMatcher matcher = new NodeMatcher("NODE[bl*h]"); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + })); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blablah" }, + })); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "bleh" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("NODE"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blue" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("PART"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "blah" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "blue" }, + })); + } + + [Fact] + public void TestIsMatch__Name__Multiple() + { + NodeMatcher matcher = new NodeMatcher("NODE[blah|bleh|blih*]"); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + })); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "bleh" }, + })); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blih" }, + })); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blihblih" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("NODE"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "bloh" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("PART"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "blah" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "bleh" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "blih" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("PART") + { + { "name", "bloh" }, + })); + } + + [Fact] + public void TestIsMatch__Constraints() + { + NodeMatcher matcher = new NodeMatcher("NODE[blah]:HAS[@FOO[bar*],#something[else]]"); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + { "something", "else" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "bleh" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NADE") + { + { "name", "blah" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + } + + [Fact] + public void TestIsMatch__Constraints_Open() + { + NodeMatcher matcher = new NodeMatcher("NODE[blah]:HAS[@FOO,#something]"); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + { "something", "else" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "bleh" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NADE") + { + { "name", "blah" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + } + + #endregion + } +} diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 7871f5e8..3fce6b64 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -753,6 +753,12 @@ private void AssertPatchCorrect(Patch patch, UrlDir.UrlConfig originalUrl, Comma Assert.Same(originalNode.nodes[i], patch.node.nodes[i]); } + if (expectedNodeName == "NODE") + AssertNodeMatcher__Bare(patch.nodeMatcher); + else if (expectedNodeName == "NODE[foo]:HAS[#bar]") + AssertNodeMatcher__Name__Has(patch.nodeMatcher); + else + throw new NotImplementedException(); } private void AssertNoErrors() @@ -768,5 +774,52 @@ private void EnsureNeedsSatisfied() progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); } + + private void AssertNodeMatcher__Bare(INodeMatcher matcher) + { + Assert.True(matcher.IsMatch(new ConfigNode("NODE"))); + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "boo" }, + { "bar", "baz" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("NADE"))); + } + + private void AssertNodeMatcher__Name__Has(INodeMatcher matcher) + { + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "foo" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "boo" }, + { "bar", "baz" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("NODE"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("NADE") + { + { "name", "foo" }, + { "bar", "baz" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "boo" }, + { "bar", "baz" }, + })); + } } } diff --git a/ModuleManagerTests/PatchTest.cs b/ModuleManagerTests/PatchTest.cs index c8bda099..a20ebc1b 100644 --- a/ModuleManagerTests/PatchTest.cs +++ b/ModuleManagerTests/PatchTest.cs @@ -72,5 +72,63 @@ public void TestConstructor__NullNode() UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); Assert.Throws(() => new Patch(urlConfig, Command.Edit, null)); } + + [Fact] + public void TestNodeMatcher() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE[blah]:HAS[@FOO[bar*],#something[else]]")); + Patch patch = new Patch(urlConfig, Command.Edit, urlConfig.config); + INodeMatcher matcher = patch.nodeMatcher; + + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + { "something", "else" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "blah" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "bleh" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NADE") + { + { "name", "blah" }, + { "something", "else" }, + new TestConfigNode("FOO") + { + { "name", "barbar" }, + }, + })); + } } } From 53d238a7ccc585c66ef21536fa5dd62fbea70328 Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 19 Apr 2018 00:07:26 -0700 Subject: [PATCH 013/140] Extract IPatch interface, split up root patches --- ModuleManager/ModuleManager.csproj | 6 +- ModuleManager/Pass.cs | 11 +- ModuleManager/Patch.cs | 24 - ModuleManager/PatchApplier.cs | 103 +-- ModuleManager/PatchExtractor.cs | 9 +- ModuleManager/PatchList.cs | 31 +- ModuleManager/Patches/CopyPatch.cs | 55 ++ ModuleManager/Patches/DeletePatch.cs | 48 ++ ModuleManager/Patches/EditPatch.cs | 54 ++ ModuleManager/Patches/IPatch.cs | 13 + ModuleManager/Patches/PatchCompiler.cs | 31 + ModuleManagerTests/ModuleManagerTests.csproj | 5 +- ModuleManagerTests/PassTest.cs | 22 +- ModuleManagerTests/PatchApplierTest.cs | 773 +----------------- ModuleManagerTests/PatchExtractorTest.cs | 113 ++- ModuleManagerTests/PatchListTest.cs | 61 +- ModuleManagerTests/PatchTest.cs | 134 --- ModuleManagerTests/Patches/CopyPatchTest.cs | 274 +++++++ ModuleManagerTests/Patches/DeletePatchTest.cs | 130 +++ ModuleManagerTests/Patches/EditPatchTest.cs | 235 ++++++ .../Patches/PatchCompilerTest.cs | 257 ++++++ 21 files changed, 1261 insertions(+), 1128 deletions(-) delete mode 100644 ModuleManager/Patch.cs create mode 100644 ModuleManager/Patches/CopyPatch.cs create mode 100644 ModuleManager/Patches/DeletePatch.cs create mode 100644 ModuleManager/Patches/EditPatch.cs create mode 100644 ModuleManager/Patches/IPatch.cs create mode 100644 ModuleManager/Patches/PatchCompiler.cs delete mode 100644 ModuleManagerTests/PatchTest.cs create mode 100644 ModuleManagerTests/Patches/CopyPatchTest.cs create mode 100644 ModuleManagerTests/Patches/DeletePatchTest.cs create mode 100644 ModuleManagerTests/Patches/EditPatchTest.cs create mode 100644 ModuleManagerTests/Patches/PatchCompilerTest.cs diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index c73adca4..0bb5a934 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -61,9 +61,13 @@ - + + + + + diff --git a/ModuleManager/Pass.cs b/ModuleManager/Pass.cs index 241c88b5..c7db8684 100644 --- a/ModuleManager/Pass.cs +++ b/ModuleManager/Pass.cs @@ -1,10 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using ModuleManager.Patches; namespace ModuleManager { - public interface IPass : IEnumerable + public interface IPass : IEnumerable { string Name { get; } } @@ -12,7 +13,7 @@ public interface IPass : IEnumerable public class Pass : IPass { private readonly string name; - private readonly List patches = new List(0); + private readonly List patches = new List(0); public Pass(string name) { @@ -22,10 +23,10 @@ public Pass(string name) public string Name => name; - public void Add(Patch patch) => patches.Add(patch); + public void Add(IPatch patch) => patches.Add(patch); - public List.Enumerator GetEnumerator() => patches.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public List.Enumerator GetEnumerator() => patches.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/ModuleManager/Patch.cs b/ModuleManager/Patch.cs deleted file mode 100644 index 1eb488f7..00000000 --- a/ModuleManager/Patch.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace ModuleManager -{ - public class Patch - { - public readonly UrlDir.UrlConfig urlConfig; - public readonly Command command; - public readonly ConfigNode node; - public readonly INodeMatcher nodeMatcher; - - public Patch(UrlDir.UrlConfig urlConfig, Command command, ConfigNode node) - { - if (command != Command.Edit && command != Command.Copy && command != Command.Delete) - throw new ArgumentException($"Must be Edit, Copy, or Delete (got {command})", nameof(command)); - - this.urlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); - this.command = command; - this.node = node ?? throw new ArgumentNullException(nameof(node)); - - nodeMatcher = new NodeMatcher(node.name); - } - } -} diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index 68de463f..f98b7c49 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -2,8 +2,8 @@ using System.Linq; using ModuleManager.Logging; using ModuleManager.Extensions; +using ModuleManager.Patches; using ModuleManager.Progress; -using NodeStack = ModuleManager.Collections.ImmutableStack; namespace ModuleManager { @@ -11,8 +11,7 @@ public class PatchApplier { private readonly IBasicLogger logger; private readonly IPatchProgress progress; - - private readonly UrlDir databaseRoot; + private readonly IPatchList patchList; private readonly UrlDir.UrlFile[] allConfigFiles; @@ -22,7 +21,6 @@ public class PatchApplier public PatchApplier(IPatchList patchList, UrlDir databaseRoot, IPatchProgress progress, IBasicLogger logger) { this.patchList = patchList; - this.databaseRoot = databaseRoot; this.progress = progress; this.logger = logger; @@ -42,112 +40,23 @@ private void ApplyPatches(IPass pass) logger.Info(pass.Name + " pass"); Activity = "ModuleManager " + pass.Name; - foreach (Patch patch in pass) + foreach (IPatch patch in pass) { try { - string name = patch.node.name.RemoveWS(); - - if (patch.command == Command.Insert) - { - logger.Warning("Warning - Encountered insert node that should not exist at this stage: " + patch.urlConfig.SafeUrl()); - continue; - } - else if (patch.command != Command.Edit && patch.command != Command.Copy && patch.command != Command.Delete) - { - logger.Warning("Invalid command encountered on a patch: " + patch.urlConfig.SafeUrl()); - continue; - } - - string upperName = name.ToUpper(); - PatchContext context = new PatchContext(patch.urlConfig, databaseRoot, logger, progress); - char[] sep = { '[', ']' }; - string condition = ""; - - if (upperName.Contains(":HAS[")) - { - int start = upperName.IndexOf(":HAS["); - condition = name.Substring(start + 5, name.LastIndexOf(']') - start - 5); - name = name.Substring(0, start); - } - - string[] splits = name.Split(sep, 3); - string[] patterns = splits.Length > 1 ? splits[1].Split(',', '|') : null; - string type = splits[0]; - - bool loop = patch.node.HasNode("MM_PATCH_LOOP"); - foreach (UrlDir.UrlFile file in allConfigFiles) { - if (patch.command == Command.Edit) - { - foreach (UrlDir.UrlConfig url in file.configs) - { - if (!patch.nodeMatcher.IsMatch(url.config)) continue; - if (loop) logger.Info($"Looping on {patch.urlConfig.SafeUrl()} to {url.SafeUrl()}"); - - do - { - progress.ApplyingUpdate(url, patch.urlConfig); - url.config = MMPatchLoader.ModifyNode(new NodeStack(url.config), patch.node, context); - } while (loop && patch.nodeMatcher.IsMatch(url.config)); - - if (loop) url.config.RemoveNodes("MM_PATCH_LOOP"); - } - } - else if (patch.command == Command.Copy) - { - // Avoid checking the new configs we are creating - int count = file.configs.Count; - for (int i = 0; i < count; i++) - { - UrlDir.UrlConfig url = file.configs[i]; - if (!patch.nodeMatcher.IsMatch(url.config)) continue; - - ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(url.config), patch.node, context); - if (url.config.HasValue("name") && url.config.GetValue("name") == clone.GetValue("name")) - { - progress.Error(patch.urlConfig, $"Error - when applying copy {patch.urlConfig.SafeUrl()} to {url.SafeUrl()} - the copy needs to have a different name than the parent (use @name = xxx)"); - } - else - { - progress.ApplyingCopy(url, patch.urlConfig); - file.AddConfig(clone); - } - } - } - else if (patch.command == Command.Delete) - { - int i = 0; - while (i < file.configs.Count) - { - UrlDir.UrlConfig url = file.configs[i]; - - if (patch.nodeMatcher.IsMatch(url.config)) - { - progress.ApplyingDelete(url, patch.urlConfig); - file.configs.RemoveAt(i); - } - else - { - i++; - } - } - } - else - { - throw new NotImplementedException("This code should not be reachable"); - } + patch.Apply(file, progress, logger); } progress.PatchApplied(); } catch (Exception e) { - progress.Exception(patch.urlConfig, "Exception while processing node : " + patch.urlConfig.SafeUrl(), e); + progress.Exception(patch.UrlConfig, "Exception while processing node : " + patch.UrlConfig.SafeUrl(), e); try { - logger.Error("Processed node was\n" + patch.urlConfig.PrettyPrint()); + logger.Error("Processed node was\n" + patch.UrlConfig.PrettyPrint()); } catch (Exception ex2) { diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index 5a583ad4..06de7182 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using ModuleManager.Extensions; using ModuleManager.Logging; +using ModuleManager.Patches; using ModuleManager.Progress; namespace ModuleManager @@ -122,7 +123,7 @@ public void ExtractPatch(UrlDir.UrlConfig urlConfig) urlConfig.parent.configs.Remove(urlConfig); Match theMatch = null; - Action addPatch = null; + Action addPatch = null; if (firstMatch.Success) { @@ -184,11 +185,7 @@ public void ExtractPatch(UrlDir.UrlConfig urlConfig) else newName = name.Remove(theMatch.Index, theMatch.Length); - ConfigNode node = new ConfigNode(newName) { id = urlConfig.config.id }; - node.ShallowCopyFrom(urlConfig.config); - Patch patch = new Patch(urlConfig, command, node); - - addPatch(patch); + addPatch(PatchCompiler.CompilePatch(urlConfig, command, newName)); progress.PatchAdded(); } catch(Exception e) diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index dff2fdb5..e79f9570 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -3,18 +3,19 @@ using System.Collections.Generic; using System.Linq; using ModuleManager.Collections; +using ModuleManager.Patches; namespace ModuleManager { public interface IPatchList : IEnumerable { bool HasMod(string mod); - void AddFirstPatch(Patch patch); - void AddLegacyPatch(Patch patch); - void AddBeforePatch(string mod, Patch patch); - void AddForPatch(string mod, Patch patch); - void AddAfterPatch(string mod, Patch patch); - void AddFinalPatch(Patch patch); + void AddFirstPatch(IPatch patch); + void AddLegacyPatch(IPatch patch); + void AddBeforePatch(string mod, IPatch patch); + void AddForPatch(string mod, IPatch patch); + void AddAfterPatch(string mod, IPatch patch); + void AddFinalPatch(IPatch patch); } public class PatchList : IPatchList @@ -37,9 +38,9 @@ public ModPass(string name) afterPass = new Pass($":AFTER[{this.name}]"); } - public void AddBeforePatch(Patch patch) => beforePass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); - public void AddForPatch(Patch patch) => forPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); - public void AddAfterPatch(Patch patch) => afterPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + public void AddBeforePatch(IPatch patch) => beforePass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + public void AddForPatch(IPatch patch) => forPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + public void AddAfterPatch(IPatch patch) => afterPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); } private class ModPassCollection : IEnumerable @@ -97,35 +98,35 @@ public bool HasMod(string mod) return modPasses.HasMod(mod); } - public void AddFirstPatch(Patch patch) + public void AddFirstPatch(IPatch patch) { firstPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); } - public void AddLegacyPatch(Patch patch) + public void AddLegacyPatch(IPatch patch) { legacyPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); } - public void AddBeforePatch(string mod, Patch patch) + public void AddBeforePatch(string mod, IPatch patch) { EnsureMod(mod); modPasses[mod].AddBeforePatch(patch ?? throw new ArgumentNullException(nameof(patch))); } - public void AddForPatch(string mod, Patch patch) + public void AddForPatch(string mod, IPatch patch) { EnsureMod(mod); modPasses[mod].AddForPatch(patch ?? throw new ArgumentNullException(nameof(patch))); } - public void AddAfterPatch(string mod, Patch patch) + public void AddAfterPatch(string mod, IPatch patch) { EnsureMod(mod); modPasses[mod].AddAfterPatch(patch ?? throw new ArgumentNullException(nameof(patch))); } - public void AddFinalPatch(Patch patch) + public void AddFinalPatch(IPatch patch) { finalPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); } diff --git a/ModuleManager/Patches/CopyPatch.cs b/ModuleManager/Patches/CopyPatch.cs new file mode 100644 index 00000000..e8fa295f --- /dev/null +++ b/ModuleManager/Patches/CopyPatch.cs @@ -0,0 +1,55 @@ +using System; +using NodeStack = ModuleManager.Collections.ImmutableStack; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + public class CopyPatch : IPatch + { + public UrlDir.UrlConfig UrlConfig { get; } + public INodeMatcher NodeMatcher { get; } + + public CopyPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher) + { + UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + } + + public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) + { + if (file == null) throw new ArgumentNullException(nameof(file)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + PatchContext context = new PatchContext(UrlConfig, file.root, logger, progress); + + // Avoid checking the new configs we are creating + int count = file.configs.Count; + for (int i = 0; i < count; i++) + { + UrlDir.UrlConfig url = file.configs[i]; + try + { + if (!NodeMatcher.IsMatch(url.config)) continue; + + ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(url.config), UrlConfig.config, context); + if (url.config.HasValue("name") && url.config.GetValue("name") == clone.GetValue("name")) + { + progress.Error(UrlConfig, $"Error - when applying copy {UrlConfig.SafeUrl()} to {url.SafeUrl()} - the copy needs to have a different name than the parent (use @name = xxx)"); + } + else + { + progress.ApplyingCopy(url, UrlConfig); + file.AddConfig(clone); + } + } + catch (Exception ex) + { + progress.Exception(UrlConfig, $"Exception while applying copy {UrlConfig.SafeUrl()} to {url.SafeUrl()}", ex); + } + } + } + } +} diff --git a/ModuleManager/Patches/DeletePatch.cs b/ModuleManager/Patches/DeletePatch.cs new file mode 100644 index 00000000..2fbcd879 --- /dev/null +++ b/ModuleManager/Patches/DeletePatch.cs @@ -0,0 +1,48 @@ +using System; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + public class DeletePatch : IPatch + { + public UrlDir.UrlConfig UrlConfig { get; } + public INodeMatcher NodeMatcher { get; } + + public DeletePatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher) + { + UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + } + + public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) + { + if (file == null) throw new ArgumentNullException(nameof(file)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + int i = 0; + while (i < file.configs.Count) + { + UrlDir.UrlConfig url = file.configs[i]; + try + { + if (NodeMatcher.IsMatch(url.config)) + { + progress.ApplyingDelete(url, UrlConfig); + file.configs.RemoveAt(i); + } + else + { + i++; + } + } + catch (Exception ex) + { + progress.Exception(UrlConfig, $"Exception while applying delete {UrlConfig.SafeUrl()} to {url.SafeUrl()}", ex); + } + } + } + } +} diff --git a/ModuleManager/Patches/EditPatch.cs b/ModuleManager/Patches/EditPatch.cs new file mode 100644 index 00000000..4dfcc22f --- /dev/null +++ b/ModuleManager/Patches/EditPatch.cs @@ -0,0 +1,54 @@ +using System; +using NodeStack = ModuleManager.Collections.ImmutableStack; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + public class EditPatch : IPatch + { + private readonly bool loop; + + public UrlDir.UrlConfig UrlConfig { get; } + public INodeMatcher NodeMatcher { get; } + + public EditPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher) + { + UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + + loop = urlConfig.config.HasNode("MM_PATCH_LOOP"); + } + + public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) + { + if (file == null) throw new ArgumentNullException(nameof(file)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + PatchContext context = new PatchContext(UrlConfig, file.root, logger, progress); + for (int i = 0; i < file.configs.Count; i++) + { + UrlDir.UrlConfig urlConfig = file.configs[i]; + try + { + if (!NodeMatcher.IsMatch(urlConfig.config)) continue; + if (loop) logger.Info($"Looping on {UrlConfig.SafeUrl()} to {urlConfig.SafeUrl()}"); + + do + { + progress.ApplyingUpdate(urlConfig, UrlConfig); + file.configs[i] = urlConfig = new UrlDir.UrlConfig(file, MMPatchLoader.ModifyNode(new NodeStack(urlConfig.config), UrlConfig.config, context)); + } while (loop && NodeMatcher.IsMatch(urlConfig.config)); + + if (loop) file.configs[i].config.RemoveNodes("MM_PATCH_LOOP"); + } + catch (Exception ex) + { + progress.Exception(UrlConfig, $"Exception while applying update {UrlConfig.SafeUrl()} to {urlConfig.SafeUrl()}", ex); + } + } + } + } +} diff --git a/ModuleManager/Patches/IPatch.cs b/ModuleManager/Patches/IPatch.cs new file mode 100644 index 00000000..57ae2cfa --- /dev/null +++ b/ModuleManager/Patches/IPatch.cs @@ -0,0 +1,13 @@ +using System; +using ModuleManager.Logging; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + public interface IPatch + { + UrlDir.UrlConfig UrlConfig { get; } + INodeMatcher NodeMatcher { get; } + void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger); + } +} diff --git a/ModuleManager/Patches/PatchCompiler.cs b/ModuleManager/Patches/PatchCompiler.cs new file mode 100644 index 00000000..3e71a3ce --- /dev/null +++ b/ModuleManager/Patches/PatchCompiler.cs @@ -0,0 +1,31 @@ +using System; + +namespace ModuleManager.Patches +{ + public static class PatchCompiler + { + public static IPatch CompilePatch(UrlDir.UrlConfig urlConfig, Command command, string name) + { + if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); + if (name == null) throw new ArgumentNullException(nameof(name)); + if (name == string.Empty) throw new ArgumentException("can't be empty", nameof(name)); + + INodeMatcher nodeMatcher = new NodeMatcher(name); + + switch (command) + { + case Command.Edit: + return new EditPatch(urlConfig, nodeMatcher); + + case Command.Copy: + return new CopyPatch(urlConfig, nodeMatcher); + + case Command.Delete: + return new DeletePatch(urlConfig, nodeMatcher); + + default: + throw new ArgumentException("invalid command for a root node: " + command, nameof(command)); + } + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index fe099e9c..87d7ec27 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -64,6 +64,10 @@ + + + + @@ -73,7 +77,6 @@ - diff --git a/ModuleManagerTests/PassTest.cs b/ModuleManagerTests/PassTest.cs index 85300ef5..9e712e49 100644 --- a/ModuleManagerTests/PassTest.cs +++ b/ModuleManagerTests/PassTest.cs @@ -1,7 +1,10 @@ using System; +using System.Linq; using Xunit; +using NSubstitute; using TestUtils; using ModuleManager; +using ModuleManager.Patches; namespace ModuleManagerTests { @@ -48,11 +51,11 @@ public void TestName() [Fact] public void Test__Add__Enumerator() { - Patch[] patches = + IPatch[] patches = { - CreatePatch(Command.Edit, new ConfigNode()), - CreatePatch(Command.Copy, new ConfigNode()), - CreatePatch(Command.Delete, new ConfigNode()), + Substitute.For(), + Substitute.For(), + Substitute.For(), }; Pass pass = new Pass("blah") @@ -62,12 +65,13 @@ public void Test__Add__Enumerator() patches[2], }; - Assert.Equal(patches, pass); - } + IPatch[] passPatches = pass.ToArray(); + Assert.Equal(patches.Length, passPatches.Length); - private Patch CreatePatch(Command command, ConfigNode node) - { - return new Patch(new UrlDir.UrlConfig(file, node), command, node); + for (int i = 0; i < patches.Length; i++) + { + Assert.Same(patches[i], passPatches[i]); + } } } } diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index 5929ddf5..8d07bd3c 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -1,13 +1,12 @@ using System; -using System.Linq; using Xunit; using NSubstitute; using UnityEngine; using TestUtils; using ModuleManager; using ModuleManager.Collections; -using ModuleManager.Extensions; using ModuleManager.Logging; +using ModuleManager.Patches; using ModuleManager.Progress; namespace ModuleManagerTests @@ -43,739 +42,30 @@ public PatchApplierTest() } [Fact] - public void TestApplyPatches__Edit() + public void TestApplyPatches() { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "abc" }, - { "foo", "bar" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "def" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "ghi" }, - { "jkl", "mno" }, - }, file); - - Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART") - { - { "@foo", "baz" }, - { "pqr", "stw" }, - }); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config2, patch1.urlConfig); - progress.Received(1).PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(3, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "abc" }, - { "foo", "baz" }, - { "pqr", "stw" }, - }, allConfigs[0].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "def" }, - { "pqr", "stw" }, - }, allConfigs[1].config); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "ghi" }, - { "jkl", "mno" }, - }, allConfigs[2].config); - } - - [Fact] - public void TestApplyPatches__Copy() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "002" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "003" }, - { "bbb", "004" }, - }, file); - - Patch patch1 = CreatePatch(Command.Copy, new TestConfigNode("+PART") - { - { "@name ^", ":^00:01:" }, - { "@aaa", "011" }, - { "ccc", "005" }, - }); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingCopy(config1, patch1.urlConfig); - progress.Received().ApplyingCopy(config2, patch1.urlConfig); - progress.Received(1).PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(5, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, allConfigs[0].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "002" }, - }, allConfigs[1].config); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "003" }, - { "bbb", "004" }, - }, allConfigs[2].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "010" }, - { "aaa", "011" }, - { "ccc", "005" }, - }, allConfigs[3].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "012" }, - { "ccc", "005" }, - }, allConfigs[4].config); - } - - [Fact] - public void TestApplyPatches__Delete() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "abc" }, - { "foo", "bar" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "def" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "ghi" }, - { "jkl", "mno" }, - }, file); - - Patch patch1 = CreatePatch(Command.Delete, new TestConfigNode("-PART")); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingDelete(config1, patch1.urlConfig); - progress.Received().ApplyingDelete(config2, patch1.urlConfig); - progress.Received(1).PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(1, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "ghi" }, - { "jkl", "mno" }, - }, allConfigs[0].config); - } - - [Fact] - public void TestApplyPatches__Name() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "004" }, - { "ccc", "005" }, - }, file); - - UrlDir.UrlConfig config4 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "006" }, - { "ddd", "007" }, - }, file); - - Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000]") - { - { "@aaa", "011" }, - { "eee", "012" }, - }); - - Patch patch2 = CreatePatch(Command.Copy, new TestConfigNode("+PART[002]") - { - { "@name", "022" }, - { "@bbb", "013" }, - { "fff", "014" }, - }); - - Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART[004]")); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingCopy(config2, patch2.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingDelete(config3, patch3.urlConfig); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(4, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "011" }, - { "eee", "012" }, - }, allConfigs[0].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - }, allConfigs[1].config); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "006" }, - { "ddd", "007" }, - }, allConfigs[2].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "022" }, - { "bbb", "013" }, - { "fff", "014" }, - }, allConfigs[3].config); - } - - [Fact] - public void TestApplyPatches__Name__Wildcard() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "004" }, - { "ccc", "005" }, - }, file); - - UrlDir.UrlConfig config4 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "006" }, - { "ddd", "007" }, - }, file); - - Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[0*0]") - { - { "@aaa", "011" }, - { "eee", "012" }, - }); - - Patch patch2 = CreatePatch(Command.Copy, new TestConfigNode("+PART[0*2]") - { - { "@name", "022" }, - { "@bbb", "013" }, - { "fff", "014" }, - }); - - Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART[0*4]")); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingCopy(config2, patch2.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingDelete(config3, patch3.urlConfig); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(4, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "011" }, - { "eee", "012" }, - }, allConfigs[0].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - }, allConfigs[1].config); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "006" }, - { "ddd", "007" }, - }, allConfigs[2].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "022" }, - { "bbb", "013" }, - { "fff", "014" }, - }, allConfigs[3].config); - } - - [Fact] - public void TestApplyPatches__Name__Or() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "004" }, - { "ccc", "005" }, - }, file); - - Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "@aaa", "011" }, - { "ddd", "006" }, - }); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config2, patch1.urlConfig); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(3, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "011" }, - { "ddd", "006" }, - }, allConfigs[0].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - { "ddd", "006" }, - }, allConfigs[1].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "004" }, - { "ccc", "005" }, - }, allConfigs[2].config); - } - - [Fact] - public void TestApplyPatches__Order() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "bbb", "002" }, - }); - - Patch patch2 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "ccc", "003" }, - }); - - Patch patch3 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "ddd", "004" }, - }); - - Patch patch4 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "eee", "005" }, - }); - - Patch patch5 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "fff", "006" }, - }); - - Patch patch6 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "ggg", "007" }, - }); - - Patch patch7 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "hhh", "008" }, - }); - - Patch patch8 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "iii", "009" }, - }); - - Patch patch9 = CreatePatch(Command.Edit, new TestConfigNode("@PART[000|0*2]") - { - { "jjj", "010" }, - }); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); - pass2.GetEnumerator().Returns(new ArrayEnumerator(patch4, patch5, patch6)); - pass3.GetEnumerator().Returns(new ArrayEnumerator(patch7, patch8, patch9)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch2.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch3.urlConfig); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - progress.Received().ApplyingUpdate(config1, patch4.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch5.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch6.urlConfig); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - progress.Received().ApplyingUpdate(config1, patch7.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch8.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch9.urlConfig); - progress.Received().PatchApplied(); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(1, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - { "bbb", "002" }, - { "ccc", "003" }, - { "ddd", "004" }, - { "eee", "005" }, - { "fff", "006" }, - { "ggg", "007" }, - { "hhh", "008" }, - { "iii", "009" }, - { "jjj", "010" }, - }, allConfigs[0].config); - } - - [Fact] - public void TestApplyPatches__Constraints() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("PART") { { "name", "000" }, { "aaa", "001" }, }, file); - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - }, file); - - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "004" }, - { "ccc", "005" }, - }, file); - - UrlDir.UrlConfig config4 = UrlBuilder.CreateConfig(new TestConfigNode("PORT") - { - { "name", "006" }, - { "ddd", "007" }, - }, file); - - Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART:HAS[#aaa[001]]") - { - { "@aaa", "011" }, - { "eee", "012" }, - }); - - Patch patch2 = CreatePatch(Command.Copy, new TestConfigNode("+PART:HAS[#bbb[003]]") - { - { "@name", "012" }, - { "@bbb", "013" }, - { "fff", "014" }, - }); - - Patch patch3 = CreatePatch(Command.Delete, new TestConfigNode("!PART:HAS[#ccc[005]]")); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1, patch2, patch3)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingCopy(config2, patch2.urlConfig); - progress.Received().PatchApplied(); - progress.Received().ApplyingDelete(config3, patch3.urlConfig); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(4, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "011" }, - { "eee", "012" }, - }, allConfigs[0].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "002" }, - { "bbb", "003" }, - }, allConfigs[1].config); - - AssertNodesEqual(new TestConfigNode("PORT") - { - { "name", "006" }, - { "ddd", "007" }, - }, allConfigs[2].config); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "012" }, - { "bbb", "013" }, - { "fff", "014" }, - }, allConfigs[3].config); - } - - [Fact] - public void TestApplyPatches__Loop() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "1" }, - }, file); - - Patch patch1 = CreatePatch(Command.Edit, new TestConfigNode("@PART:HAS[~aaa[>10]]") - { - { "@aaa *", "2" }, - { "bbb", "002" }, - new ConfigNode("MM_PATCH_LOOP"), - }); - - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - logger.Received().Log(LogType.Log, "Looping on abc/def/@PART:HAS[~aaa[>10]] to abc/def/PART"); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().ApplyingUpdate(config1, patch1.urlConfig); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(1, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "16" }, - { "bbb", "002" }, - { "bbb", "002" }, - { "bbb", "002" }, - { "bbb", "002" }, - }, allConfigs[0].config); - } + UrlDir.UrlConfig[] patchUrlConfigs = new UrlDir.UrlConfig[9]; + IPatch[] patches = new IPatch[9]; - [Fact] - public void TestApplyPatches__Copy__NameNotChanged() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") + for (int i = 0; i < 9; i++) { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - Patch patch1 = CreatePatch(Command.Copy, new TestConfigNode("+PART") - { - { "@aaa", "011" }, - { "bbb", "012" }, - }); + patchUrlConfigs[i] = UrlBuilder.CreateConfig(new ConfigNode(), file); + patches[i] = Substitute.For(); + patches[i].UrlConfig.Returns(patchUrlConfigs[i]); + } - pass1.GetEnumerator().Returns(new ArrayEnumerator(patch1)); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patches[0], patches[1], patches[2])); + pass2.GetEnumerator().Returns(new ArrayEnumerator(patches[3], patches[4], patches[5])); + pass3.GetEnumerator().Returns(new ArrayEnumerator(patches[6], patches[7], patches[8])); patchApplier.ApplyPatches(); - - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - logger.DidNotReceiveWithAnyArgs().Exception(null, null); - - progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); - progress.DidNotReceiveWithAnyArgs().ApplyingCopy(null, null); - progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); - - Received.InOrder(delegate - { - logger.Received().Log(LogType.Log, ":PASS1 pass"); - progress.Received().Error(patch1.urlConfig, "Error - when applying copy abc/def/+PART to abc/def/PART - the copy needs to have a different name than the parent (use @name = xxx)"); - progress.Received().PatchApplied(); - logger.Received().Log(LogType.Log, ":PASS2 pass"); - logger.Received().Log(LogType.Log, ":PASS3 pass"); - }); - - UrlDir.UrlConfig[] allConfigs = databaseRoot.AllConfigs.ToArray(); - Assert.Equal(1, allConfigs.Length); - - AssertNodesEqual(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, allConfigs[0].config); - } - - private void EnsureNoErrors() - { progress.DidNotReceiveWithAnyArgs().Error(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); @@ -783,22 +73,31 @@ private void EnsureNoErrors() logger.DidNotReceive().Log(LogType.Warning, Arg.Any()); logger.DidNotReceive().Log(LogType.Error, Arg.Any()); logger.DidNotReceiveWithAnyArgs().Exception(null, null); - } - - private void AssertNodesEqual(ConfigNode expected, ConfigNode actual) - { - Assert.Equal(expected.ToString(), actual.ToString()); - } - private Patch CreatePatch(Command command, ConfigNode node) - { - ConfigNode newNode = node; - if (command != Command.Insert) + Received.InOrder(delegate { - newNode = new ConfigNode(node.name.Substring(1)); - newNode.ShallowCopyFrom(node); - } - return new Patch(new UrlDir.UrlConfig(file, node), command, newNode); + logger.Log(LogType.Log, ":PASS1 pass"); + patches[0].Apply(file, progress, logger); + progress.PatchApplied(); + patches[1].Apply(file, progress, logger); + progress.PatchApplied(); + patches[2].Apply(file, progress, logger); + progress.PatchApplied(); + logger.Log(LogType.Log, ":PASS2 pass"); + patches[3].Apply(file, progress, logger); + progress.PatchApplied(); + patches[4].Apply(file, progress, logger); + progress.PatchApplied(); + patches[5].Apply(file, progress, logger); + progress.PatchApplied(); + logger.Log(LogType.Log, ":PASS3 pass"); + patches[6].Apply(file, progress, logger); + progress.PatchApplied(); + patches[7].Apply(file, progress, logger); + progress.PatchApplied(); + patches[8].Apply(file, progress, logger); + progress.PatchApplied(); + }); } } } diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 3fce6b64..0adcbe90 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -5,6 +5,7 @@ using TestUtils; using ModuleManager; using ModuleManager.Logging; +using ModuleManager.Patches; using ModuleManager.Progress; namespace ModuleManagerTests @@ -94,8 +95,8 @@ public void TestExtractPatch__First() UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:First"); UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:first"); - List patches = new List(); - patchList.AddFirstPatch(Arg.Do(patch => patches.Add(patch))); + List patches = new List(); + patchList.AddFirstPatch(Arg.Do(patch => patches.Add(patch))); extractor.ExtractPatch(patchConfig1); extractor.ExtractPatch(patchConfig2); @@ -107,10 +108,10 @@ public void TestExtractPatch__First() Assert.Empty(root.AllConfigs); Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); - AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); - AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); + AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); + AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); Received.InOrder(delegate { @@ -139,8 +140,8 @@ public void TestExtractPatch__Legacy() UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]"); UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE"); - List patches = new List(); - patchList.AddLegacyPatch(Arg.Do(patch => patches.Add(patch))); + List patches = new List(); + patchList.AddLegacyPatch(Arg.Do(patch => patches.Add(patch))); extractor.ExtractPatch(patchConfig1); extractor.ExtractPatch(patchConfig2); @@ -150,8 +151,8 @@ public void TestExtractPatch__Legacy() Assert.Empty(root.AllConfigs); Assert.Equal(2, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); + AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); + AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); Received.InOrder(delegate { @@ -180,8 +181,8 @@ public void TestExtractPatch__BeforeMod() UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Before[mod1]"); UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:before[MOD1]"); - List patches = new List(); - patchList.AddBeforePatch("mod1", Arg.Do(patch => patches.Add(patch))); + List patches = new List(); + patchList.AddBeforePatch("mod1", Arg.Do(patch => patches.Add(patch))); extractor.ExtractPatch(patchConfig1); extractor.ExtractPatch(patchConfig2); @@ -193,10 +194,10 @@ public void TestExtractPatch__BeforeMod() Assert.Empty(root.AllConfigs); Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); - AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); - AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); + AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); + AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); Received.InOrder(delegate { @@ -267,8 +268,8 @@ public void TestExtractPatch__ForMod() UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:For[mod1]"); UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:for[MOD1]"); - List patches = new List(); - patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); + List patches = new List(); + patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); extractor.ExtractPatch(patchConfig1); extractor.ExtractPatch(patchConfig2); @@ -280,10 +281,10 @@ public void TestExtractPatch__ForMod() Assert.Empty(root.AllConfigs); Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); - AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); - AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); + AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); + AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); Received.InOrder(delegate @@ -355,8 +356,8 @@ public void TestExtractPatch__AfterMod() UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:After[mod1]"); UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:after[MOD1]"); - List patches = new List(); - patchList.AddAfterPatch("mod1", Arg.Do(patch => patches.Add(patch))); + List patches = new List(); + patchList.AddAfterPatch("mod1", Arg.Do(patch => patches.Add(patch))); extractor.ExtractPatch(patchConfig1); extractor.ExtractPatch(patchConfig2); @@ -366,10 +367,10 @@ public void TestExtractPatch__AfterMod() AssertNoErrors(); Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); - AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); - AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); + AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); + AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); Assert.Empty(root.AllConfigs); @@ -440,8 +441,8 @@ public void TestExtractPatch__Final() UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Final"); UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:final"); - List patches = new List(); - patchList.AddFinalPatch(Arg.Do(patch => patches.Add(patch))); + List patches = new List(); + patchList.AddFinalPatch(Arg.Do(patch => patches.Add(patch))); extractor.ExtractPatch(patchConfig1); extractor.ExtractPatch(patchConfig2); @@ -453,10 +454,10 @@ public void TestExtractPatch__Final() Assert.Empty(root.AllConfigs); Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, Command.Edit, "NODE[foo]:HAS[#bar]"); - AssertPatchCorrect(patches[1], patchConfig2, Command.Edit, "NODE"); - AssertPatchCorrect(patches[2], patchConfig3, Command.Edit, "NODE"); - AssertPatchCorrect(patches[3], patchConfig4, Command.Edit, "NODE"); + AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); + AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); Received.InOrder(delegate { @@ -660,8 +661,8 @@ public void TestExtractPatch__Command() UrlDir.UrlConfig config09 = CreateConfig("#NODE:FOR[mod1]"); UrlDir.UrlConfig config10 = CreateConfig("*NODE:FOR[mod1]"); - List patches = new List(); - patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); + List patches = new List(); + patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); extractor.ExtractPatch(config01); extractor.ExtractPatch(config02); @@ -680,11 +681,11 @@ public void TestExtractPatch__Command() Assert.Empty(root.AllConfigs); Assert.Equal(5, patches.Count); - AssertPatchCorrect(patches[0], config01, Command.Edit, "NODE"); - AssertPatchCorrect(patches[1], config02, Command.Copy, "NODE"); - AssertPatchCorrect(patches[2], config03, Command.Copy, "NODE"); - AssertPatchCorrect(patches[3], config04, Command.Delete, "NODE"); - AssertPatchCorrect(patches[4], config05, Command.Delete, "NODE"); + AssertPatchCorrect(patches[0], config01, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[1], config02, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[2], config03, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[3], config04, AssertNodeMatcher__Bare); + AssertPatchCorrect(patches[4], config05, AssertNodeMatcher__Bare); progress.Received().PatchAdded(); Received.InOrder(delegate @@ -731,34 +732,12 @@ private UrlDir.UrlConfig CreateConfig(string name) return UrlBuilder.CreateConfig(node, file); } - private void AssertPatchCorrect(Patch patch, UrlDir.UrlConfig originalUrl, Command expectedCommand, string expectedNodeName) + private void AssertPatchCorrect(IPatch patch, UrlDir.UrlConfig originalUrl, Action assertNodeMatcher) where T : IPatch { - Assert.Same(originalUrl, patch.urlConfig); - Assert.Equal(expectedCommand, patch.command); - Assert.Equal(expectedNodeName, patch.node.name); + Assert.IsType(patch); + Assert.Same(originalUrl, patch.UrlConfig); - ConfigNode originalNode = originalUrl.config; - - Assert.Equal(originalNode.id, patch.node.id); - Assert.Equal(originalNode.values.Count, patch.node.values.Count); - Assert.Equal(originalNode.nodes.Count, patch.node.nodes.Count); - - for (int i = 0; i < originalNode.values.Count; i++) - { - Assert.Same(originalNode.values[i], patch.node.values[i]); - } - - for (int i = 0; i < originalNode.nodes.Count; i++) - { - Assert.Same(originalNode.nodes[i], patch.node.nodes[i]); - } - - if (expectedNodeName == "NODE") - AssertNodeMatcher__Bare(patch.nodeMatcher); - else if (expectedNodeName == "NODE[foo]:HAS[#bar]") - AssertNodeMatcher__Name__Has(patch.nodeMatcher); - else - throw new NotImplementedException(); + assertNodeMatcher(patch.NodeMatcher); } private void AssertNoErrors() diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index ce252c13..474ef64d 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using Xunit; +using NSubstitute; using TestUtils; using ModuleManager; +using ModuleManager.Patches; namespace ModuleManagerTests { @@ -24,24 +26,24 @@ public PatchListTest() [Fact] public void Test__Lifecycle() { - Patch patch01 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch02 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch03 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch04 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch05 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch06 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch07 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch08 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch09 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch10 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch11 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch12 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch13 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch14 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch15 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch16 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch17 = CreatePatch(Command.Edit, new ConfigNode("blah")); - Patch patch18 = CreatePatch(Command.Edit, new ConfigNode("blah")); + IPatch patch01 = Substitute.For(); + IPatch patch02 = Substitute.For(); + IPatch patch03 = Substitute.For(); + IPatch patch04 = Substitute.For(); + IPatch patch05 = Substitute.For(); + IPatch patch06 = Substitute.For(); + IPatch patch07 = Substitute.For(); + IPatch patch08 = Substitute.For(); + IPatch patch09 = Substitute.For(); + IPatch patch10 = Substitute.For(); + IPatch patch11 = Substitute.For(); + IPatch patch12 = Substitute.For(); + IPatch patch13 = Substitute.For(); + IPatch patch14 = Substitute.For(); + IPatch patch15 = Substitute.For(); + IPatch patch16 = Substitute.For(); + IPatch patch17 = Substitute.For(); + IPatch patch18 = Substitute.For(); patchList.AddFirstPatch(patch01); patchList.AddFirstPatch(patch02); @@ -155,7 +157,7 @@ public void TestAddBeforePatch__ModNull() { ArgumentNullException ex = Assert.Throws(delegate { - patchList.AddBeforePatch(null, CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddBeforePatch(null, Substitute.For()); }); Assert.Equal("mod", ex.ParamName); @@ -166,7 +168,7 @@ public void TestAddBeforePatch__ModBlank() { ArgumentException ex = Assert.Throws(delegate { - patchList.AddBeforePatch("", CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddBeforePatch("", Substitute.For()); }); Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); @@ -189,7 +191,7 @@ public void TestAddBeforePatch__ModDoesNotExist() { KeyNotFoundException ex = Assert.Throws(delegate { - patchList.AddBeforePatch("mod3", CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddBeforePatch("mod3", Substitute.For()); }); Assert.Equal("Mod 'mod3' not found", ex.Message); @@ -200,7 +202,7 @@ public void TestAddForPatch__ModNull() { ArgumentNullException ex = Assert.Throws(delegate { - patchList.AddForPatch(null, CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddForPatch(null, Substitute.For()); }); Assert.Equal("mod", ex.ParamName); @@ -211,7 +213,7 @@ public void TestAddForPatch__ModBlank() { ArgumentException ex = Assert.Throws(delegate { - patchList.AddForPatch("", CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddForPatch("", Substitute.For()); }); Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); @@ -234,7 +236,7 @@ public void TestAddForPatch__ModDoesNotExist() { KeyNotFoundException ex = Assert.Throws(delegate { - patchList.AddForPatch("mod3", CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddForPatch("mod3", Substitute.For()); }); Assert.Equal("Mod 'mod3' not found", ex.Message); @@ -245,7 +247,7 @@ public void TestAddAfterPatch__ModNull() { ArgumentNullException ex = Assert.Throws(delegate { - patchList.AddAfterPatch(null, CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddAfterPatch(null, Substitute.For()); }); Assert.Equal("mod", ex.ParamName); @@ -256,7 +258,7 @@ public void TestAddAfterPatch__ModBlank() { ArgumentException ex = Assert.Throws(delegate { - patchList.AddAfterPatch("", CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddAfterPatch("", Substitute.For()); }); Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); @@ -279,7 +281,7 @@ public void TestAddAddafterPatch__ModDoesNotExist() { KeyNotFoundException ex = Assert.Throws(delegate { - patchList.AddAfterPatch("mod3", CreatePatch(Command.Edit, new ConfigNode())); + patchList.AddAfterPatch("mod3", Substitute.For()); }); Assert.Equal("Mod 'mod3' not found", ex.Message); @@ -295,10 +297,5 @@ public void TestAddFinalPatch__Null() Assert.Equal("patch", ex.ParamName); } - - private Patch CreatePatch(Command command, ConfigNode node) - { - return new Patch(new UrlDir.UrlConfig(file, node), command, node); - } } } diff --git a/ModuleManagerTests/PatchTest.cs b/ModuleManagerTests/PatchTest.cs deleted file mode 100644 index a20ebc1b..00000000 --- a/ModuleManagerTests/PatchTest.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using Xunit; -using NSubstitute; -using TestUtils; -using ModuleManager; - -namespace ModuleManagerTests -{ - public class PatchTest - { - [Fact] - public void TestConstructor() - { - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); - ConfigNode node = new ConfigNode("NADE"); - Patch patch = new Patch(urlConfig, Command.Edit, node); - - Assert.Same(urlConfig, patch.urlConfig); - Assert.Equal(Command.Edit, patch.command); - Assert.Same(node, patch.node); - } - - [Fact] - public void TestConstructor__ValidCommands() - { - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); - ConfigNode node = new ConfigNode("NADE"); - - Patch patch1 = new Patch(urlConfig, Command.Edit, node); - Assert.Same(urlConfig, patch1.urlConfig); - Assert.Equal(Command.Edit, patch1.command); - Assert.Same(node, patch1.node); - - Patch patch2 = new Patch(urlConfig, Command.Copy, node); - Assert.Same(urlConfig, patch2.urlConfig); - Assert.Equal(Command.Copy, patch2.command); - Assert.Same(node, patch2.node); - - Patch patch3 = new Patch(urlConfig, Command.Delete, node); - Assert.Same(urlConfig, patch3.urlConfig); - Assert.Equal(Command.Delete, patch3.command); - Assert.Same(node, patch3.node); - - ArgumentException ex1 = Assert.Throws(() => new Patch(urlConfig, Command.Create, node)); - Assert.Equal("Must be Edit, Copy, or Delete (got Create)\r\nParameter name: command", ex1.Message); - - ArgumentException ex2 = Assert.Throws(() => new Patch(urlConfig, Command.Insert, node)); - Assert.Equal("Must be Edit, Copy, or Delete (got Insert)\r\nParameter name: command", ex2.Message); - - ArgumentException ex3 = Assert.Throws(() => new Patch(urlConfig, Command.Paste, node)); - Assert.Equal("Must be Edit, Copy, or Delete (got Paste)\r\nParameter name: command", ex3.Message); - - ArgumentException ex4 = Assert.Throws(() => new Patch(urlConfig, Command.Rename, node)); - Assert.Equal("Must be Edit, Copy, or Delete (got Rename)\r\nParameter name: command", ex4.Message); - - ArgumentException ex5 = Assert.Throws(() => new Patch(urlConfig, Command.Replace, node)); - Assert.Equal("Must be Edit, Copy, or Delete (got Replace)\r\nParameter name: command", ex5.Message); - - ArgumentException ex6 = Assert.Throws(() => new Patch(urlConfig, Command.Special, node)); - Assert.Equal("Must be Edit, Copy, or Delete (got Special)\r\nParameter name: command", ex6.Message); - } - - [Fact] - public void TestConstructor__NullUrlConfig() - { - Assert.Throws(() => new Patch(null, Command.Edit, new ConfigNode("BLAH"))); - } - - [Fact] - public void TestConstructor__NullNode() - { - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); - Assert.Throws(() => new Patch(urlConfig, Command.Edit, null)); - } - - [Fact] - public void TestNodeMatcher() - { - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE[blah]:HAS[@FOO[bar*],#something[else]]")); - Patch patch = new Patch(urlConfig, Command.Edit, urlConfig.config); - INodeMatcher matcher = patch.nodeMatcher; - - Assert.True(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "blah" }, - { "something", "else" }, - new TestConfigNode("FOO") - { - { "name", "barbar" }, - }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "blah" }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "blah" }, - { "something", "else" }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "blah" }, - new TestConfigNode("FOO") - { - { "name", "barbar" }, - }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "bleh" }, - { "something", "else" }, - new TestConfigNode("FOO") - { - { "name", "barbar" }, - }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NADE") - { - { "name", "blah" }, - { "something", "else" }, - new TestConfigNode("FOO") - { - { "name", "barbar" }, - }, - })); - } - } -} diff --git a/ModuleManagerTests/Patches/CopyPatchTest.cs b/ModuleManagerTests/Patches/CopyPatchTest.cs new file mode 100644 index 00000000..3b5e1b1e --- /dev/null +++ b/ModuleManagerTests/Patches/CopyPatchTest.cs @@ -0,0 +1,274 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Logging; +using ModuleManager.Patches; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class CopyPatchTest + { + [Fact] + public void TestConstructor__urlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new CopyPatch(null, Substitute.For()); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestConstructor__nodeMatcherNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null); + }); + + Assert.Equal("nodeMatcher", ex.ParamName); + } + + [Fact] + public void TestUrlConfig() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode()); + CopyPatch patch = new CopyPatch(urlConfig, Substitute.For()); + + Assert.Same(urlConfig, patch.UrlConfig); + } + + [Fact] + public void TestNodeMatcher() + { + INodeMatcher nodeMatcher = Substitute.For(); + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher); + + Assert.Same(nodeMatcher, patch.NodeMatcher); + } + + [Fact] + public void TestApply() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + UrlDir.UrlConfig urlConfig1 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, file); + + UrlDir.UrlConfig urlConfig2 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, file); + + UrlDir.UrlConfig urlConfig3 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + UrlDir.UrlConfig urlConfig4 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(urlConfig1.config).Returns(false); + nodeMatcher.IsMatch(urlConfig2.config).Returns(true); + nodeMatcher.IsMatch(urlConfig3.config).Returns(false); + nodeMatcher.IsMatch(urlConfig4.config).Returns(true); + + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") + { + { "@foo", "baz" }, + { "pqr", "stw" }, + }), nodeMatcher); + + IPatchProgress progress = Substitute.For(); + IBasicLogger logger = Substitute.For(); + + patch.Apply(file, progress, logger); + + Assert.Equal(6, file.configs.Count); + + Assert.Same(urlConfig1, file.configs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, file.configs[0].config); + + Assert.Same(urlConfig2, file.configs[1]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, file.configs[1].config); + + Assert.Same(urlConfig3, file.configs[2]); + AssertNodesEqual(new ConfigNode("NODE"), file.configs[2].config); + + Assert.Same(urlConfig4, file.configs[3]); + AssertNodesEqual(new ConfigNode("NODE"), file.configs[3].config); + + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "baz" }, + { "pqr", "stw" }, + }, file.configs[4].config); + + AssertNodesEqual(new TestConfigNode("NODE") + { + { "pqr", "stw" }, + }, file.configs[5].config); + + Received.InOrder(delegate + { + progress.ApplyingCopy(urlConfig2, patch.UrlConfig); + progress.ApplyingCopy(urlConfig4, patch.UrlConfig); + }); + + progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); + + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + [Fact] + public void TestApply__NameChanged() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }, file); + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(urlConfig.config).Returns(true); + + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") + { + { "@name", "001" }, + { "@foo", "baz" }, + { "pqr", "stw" }, + }), nodeMatcher); + + IPatchProgress progress = Substitute.For(); + IBasicLogger logger = Substitute.For(); + + patch.Apply(file, progress, logger); + + Assert.Equal(2, file.configs.Count); + + Assert.Same(urlConfig, file.configs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }, file.configs[0].config); + + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "001" }, + { "foo", "baz" }, + { "pqr", "stw" }, + }, file.configs[1].config); + + progress.Received().ApplyingCopy(urlConfig, patch.UrlConfig); + + progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); + + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + [Fact] + public void TestApply__NameNotChanged() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }, file); + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(urlConfig.config).Returns(true); + + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("+NODE") + { + { "@foo", "baz" }, + { "pqr", "stw" }, + }), nodeMatcher); + + IPatchProgress progress = Substitute.For(); + IBasicLogger logger = Substitute.For(); + + patch.Apply(file, progress, logger); + + Assert.Equal(1, file.configs.Count); + + Assert.Same(urlConfig, file.configs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }, file.configs[0].config); + + progress.Received().Error(patch.UrlConfig, "Error - when applying copy ghi/jkl/+NODE to abc/def/NODE - the copy needs to have a different name than the parent (use @name = xxx)"); + + progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingCopy(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); + + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + [Fact] + public void TestApply__FileNull() + { + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(null, Substitute.For(), Substitute.For()); + }); + + Assert.Equal("file", ex.ParamName); + } + + [Fact] + public void TestApply__ProgressNull() + { + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestApply__LoggerNull() + { + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); + }); + + Assert.Equal("logger", ex.ParamName); + } + + private void AssertNodesEqual(ConfigNode expected, ConfigNode actual) + { + Assert.Equal(expected.ToString(), actual.ToString()); + } + } +} diff --git a/ModuleManagerTests/Patches/DeletePatchTest.cs b/ModuleManagerTests/Patches/DeletePatchTest.cs new file mode 100644 index 00000000..b75576b7 --- /dev/null +++ b/ModuleManagerTests/Patches/DeletePatchTest.cs @@ -0,0 +1,130 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Logging; +using ModuleManager.Patches; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class DeletePatchTest + { + [Fact] + public void TestConstructor__urlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new DeletePatch(null, Substitute.For()); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestConstructor__nodeMatcherNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null); + }); + + Assert.Equal("nodeMatcher", ex.ParamName); + } + + [Fact] + public void TestUrlConfig() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode()); + DeletePatch patch = new DeletePatch(urlConfig, Substitute.For()); + + Assert.Same(urlConfig, patch.UrlConfig); + } + + [Fact] + public void TestNodeMatcher() + { + INodeMatcher nodeMatcher = Substitute.For(); + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher); + + Assert.Same(nodeMatcher, patch.NodeMatcher); + } + + [Fact] + public void TestApply() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + UrlDir.UrlConfig urlConfig1 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + UrlDir.UrlConfig urlConfig2 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + UrlDir.UrlConfig urlConfig3 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + UrlDir.UrlConfig urlConfig4 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(urlConfig1.config).Returns(false); + nodeMatcher.IsMatch(urlConfig2.config).Returns(true); + nodeMatcher.IsMatch(urlConfig3.config).Returns(false); + nodeMatcher.IsMatch(urlConfig4.config).Returns(true); + + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("!NODE")), nodeMatcher); + + IPatchProgress progress = Substitute.For(); + IBasicLogger logger = Substitute.For(); + + patch.Apply(file, progress, logger); + + Assert.Equal(new[] { urlConfig1, urlConfig3 }, file.configs); + + Received.InOrder(delegate + { + progress.ApplyingDelete(urlConfig2, patch.UrlConfig); + progress.ApplyingDelete(urlConfig4, patch.UrlConfig); + }); + + progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingCopy(null, null); + + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + [Fact] + public void TestApply__FileNull() + { + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(null, Substitute.For(), Substitute.For()); + }); + + Assert.Equal("file", ex.ParamName); + } + + [Fact] + public void TestApply__ProgressNull() + { + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestApply__LoggerNull() + { + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); + }); + + Assert.Equal("logger", ex.ParamName); + } + } +} diff --git a/ModuleManagerTests/Patches/EditPatchTest.cs b/ModuleManagerTests/Patches/EditPatchTest.cs new file mode 100644 index 00000000..7db368e3 --- /dev/null +++ b/ModuleManagerTests/Patches/EditPatchTest.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using Xunit; +using NSubstitute; +using UnityEngine; +using TestUtils; +using ModuleManager; +using ModuleManager.Logging; +using ModuleManager.Patches; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class EditPatchTest + { + [Fact] + public void TestConstructor__urlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new EditPatch(null, Substitute.For()); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestConstructor__nodeMatcherNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null); + }); + + Assert.Equal("nodeMatcher", ex.ParamName); + } + + [Fact] + public void TestUrlConfig() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode()); + EditPatch patch = new EditPatch(urlConfig, Substitute.For()); + + Assert.Same(urlConfig, patch.UrlConfig); + } + + [Fact] + public void TestNodeMatcher() + { + INodeMatcher nodeMatcher = Substitute.For(); + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher); + + Assert.Same(nodeMatcher, patch.NodeMatcher); + } + + [Fact] + public void TestApply() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + UrlDir.UrlConfig urlConfig1 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, file); + + UrlDir.UrlConfig urlConfig2 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, file); + + UrlDir.UrlConfig urlConfig3 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + UrlDir.UrlConfig urlConfig4 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(urlConfig1.config).Returns(false); + nodeMatcher.IsMatch(urlConfig2.config).Returns(true); + nodeMatcher.IsMatch(urlConfig3.config).Returns(false); + nodeMatcher.IsMatch(urlConfig4.config).Returns(true); + + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") + { + { "@foo", "baz" }, + { "pqr", "stw" }, + }), nodeMatcher); + + IPatchProgress progress = Substitute.For(); + IBasicLogger logger = Substitute.For(); + + patch.Apply(file, progress, logger); + + Assert.Equal(4, file.configs.Count); + + Assert.Same(urlConfig1, file.configs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, file.configs[0].config); + + Assert.NotSame(urlConfig2, file.configs[1]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "baz" }, + { "pqr", "stw" }, + }, file.configs[1].config); + + Assert.Same(urlConfig3, file.configs[2]); + AssertNodesEqual(new ConfigNode("NODE"), file.configs[2].config); + + Assert.NotSame(urlConfig4, file.configs[3]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "pqr", "stw" }, + }, file.configs[3].config); + + Received.InOrder(delegate + { + progress.ApplyingUpdate(urlConfig2, patch.UrlConfig); + progress.ApplyingUpdate(urlConfig4, patch.UrlConfig); + }); + + progress.DidNotReceiveWithAnyArgs().ApplyingCopy(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); + + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + [Fact] + public void TestApply__Loop() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "name", "000" }, + { "aaa", "1" }, + }, file); + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(Arg.Is(node => int.Parse(node.GetValue("aaa")) < 10)).Returns(true); + + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") + { + { "@aaa *", "2" }, + { "bbb", "002" }, + new ConfigNode("MM_PATCH_LOOP"), + }), nodeMatcher); + + IPatchProgress progress = Substitute.For(); + IBasicLogger logger = Substitute.For(); + + List modifiedUrlConfigs = new List(); + progress.ApplyingUpdate(Arg.Do(url => modifiedUrlConfigs.Add(url)), patch.UrlConfig); + + patch.Apply(file, progress, logger); + + Assert.Equal(1, file.configs.Count); + Assert.NotSame(urlConfig, file.configs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "000" }, + { "aaa", "16" }, + { "bbb", "002" }, + { "bbb", "002" }, + { "bbb", "002" }, + { "bbb", "002" }, + }, file.configs[0].config); + + Assert.Same(urlConfig, modifiedUrlConfigs[0]); + Assert.NotSame(urlConfig, modifiedUrlConfigs[1]); + Assert.NotSame(urlConfig, modifiedUrlConfigs[2]); + Assert.NotSame(urlConfig, modifiedUrlConfigs[3]); + + Received.InOrder(delegate + { + logger.Log(LogType.Log, "Looping on ghi/jkl/@NODE to abc/def/NODE"); + progress.ApplyingUpdate(urlConfig, patch.UrlConfig); + progress.ApplyingUpdate(modifiedUrlConfigs[1], patch.UrlConfig); + progress.ApplyingUpdate(modifiedUrlConfigs[2], patch.UrlConfig); + progress.ApplyingUpdate(modifiedUrlConfigs[3], patch.UrlConfig); + }); + + progress.DidNotReceiveWithAnyArgs().ApplyingCopy(null, null); + progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); + + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + [Fact] + public void TestApply__FileNull() + { + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(null, Substitute.For(), Substitute.For()); + }); + + Assert.Equal("file", ex.ParamName); + } + + [Fact] + public void TestApply__ProgressNull() + { + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestApply__LoggerNull() + { + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); + }); + + Assert.Equal("logger", ex.ParamName); + } + + private void AssertNodesEqual(ConfigNode expected, ConfigNode actual) + { + Assert.Equal(expected.ToString(), actual.ToString()); + } + } +} diff --git a/ModuleManagerTests/Patches/PatchCompilerTest.cs b/ModuleManagerTests/Patches/PatchCompilerTest.cs new file mode 100644 index 00000000..178a6d7a --- /dev/null +++ b/ModuleManagerTests/Patches/PatchCompilerTest.cs @@ -0,0 +1,257 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Logging; +using ModuleManager.Patches; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class PatchCompilerTest + { + private readonly IPatchProgress progress = Substitute.For(); + private readonly IBasicLogger logger = Substitute.For(); + private readonly UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + [Fact] + public void TestCompilePatch__Edit() + { + UrlDir.UrlConfig patchUrlConfig = UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") + { + { "@bar", "bleh" }, + }); + + EditPatch patch = Assert.IsType(PatchCompiler.CompilePatch(patchUrlConfig, Command.Edit, "NODE[foo]:HAS[#bar]")); + + Assert.Same(patchUrlConfig, patch.UrlConfig); + AssertNodeMatcher(patch.NodeMatcher); + + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }, file); + + patch.Apply(file, progress, logger); + + AssertNoErrors(); + + progress.Received().ApplyingUpdate(urlConfig, patchUrlConfig); + + Assert.Equal(1, file.configs.Count); + Assert.NotSame(urlConfig, file.configs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "bleh" }, + }, file.configs[0].config); + } + + [Fact] + public void TestCompilePatch__Copy() + { + UrlDir.UrlConfig patchUrlConfig = UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("+NODE") + { + { "@name", "boo" }, + { "@bar", "bleh" }, + }); + + CopyPatch patch = Assert.IsType(PatchCompiler.CompilePatch(patchUrlConfig, Command.Copy, "NODE[foo]:HAS[#bar]")); + + Assert.Same(patchUrlConfig, patch.UrlConfig); + AssertNodeMatcher(patch.NodeMatcher); + + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }, file); + + patch.Apply(file, progress, logger); + + AssertNoErrors(); + + progress.Received().ApplyingCopy(urlConfig, patchUrlConfig); + + Assert.Equal(2, file.configs.Count); + Assert.Same(urlConfig, file.configs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }, file.configs[0].config); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "boo" }, + { "bar", "bleh" }, + }, file.configs[1].config); + } + + [Fact] + public void TestCompilePatch__Delete() + { + UrlDir.UrlConfig patchUrlConfig = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("-NODE")); + + DeletePatch patch = Assert.IsType(PatchCompiler.CompilePatch(patchUrlConfig, Command.Delete, "NODE[foo]:HAS[#bar]")); + + Assert.Same(patchUrlConfig, patch.UrlConfig); + AssertNodeMatcher(patch.NodeMatcher); + + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }, file); + + patch.Apply(file, progress, logger); + + AssertNoErrors(); + + progress.Received().ApplyingDelete(urlConfig, patchUrlConfig); + + Assert.Equal(0, file.configs.Count); + } + + [Fact] + public void TestCompilePatch__NullUrlConfig() + { + ArgumentNullException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(null, Command.Edit, "NODE[foo]:HAS[#bar]"); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestCompilePatch__NullName() + { + ArgumentNullException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Edit, null); + }); + + Assert.Equal("name", ex.ParamName); + } + + [Fact] + public void TestCompilePatch__BlankName() + { + ArgumentException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Edit, ""); + }); + + Assert.Equal("name", ex.ParamName); + Assert.Equal("can't be empty\r\nParameter name: name", ex.Message); + } + + [Fact] + public void TestCompilePatch__InvalidCommand__Replace() + { + ArgumentException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Replace, "NODE[foo]:HAS[#bar]"); + }); + + Assert.Equal("command", ex.ParamName); + Assert.Equal("invalid command for a root node: Replace\r\nParameter name: command", ex.Message); + } + + [Fact] + public void TestCompilePatch__InvalidCommand__Create() + { + ArgumentException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Create, "NODE[foo]:HAS[#bar]"); + }); + + Assert.Equal("command", ex.ParamName); + Assert.Equal("invalid command for a root node: Create\r\nParameter name: command", ex.Message); + } + + [Fact] + public void TestCompilePatch__InvalidCommand__Rename() + { + ArgumentException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Rename, "NODE[foo]:HAS[#bar]"); + }); + + Assert.Equal("command", ex.ParamName); + Assert.Equal("invalid command for a root node: Rename\r\nParameter name: command", ex.Message); + } + + [Fact] + public void TestCompilePatch__InvalidCommand__Paste() + { + ArgumentException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Paste, "NODE[foo]:HAS[#bar]"); + }); + + Assert.Equal("command", ex.ParamName); + Assert.Equal("invalid command for a root node: Paste\r\nParameter name: command", ex.Message); + } + + [Fact] + public void TestCompilePatch__InvalidCommand__Special() + { + ArgumentException ex = Assert.Throws(delegate + { + PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Special, "NODE[foo]:HAS[#bar]"); + }); + + Assert.Equal("command", ex.ParamName); + Assert.Equal("invalid command for a root node: Special\r\nParameter name: command", ex.Message); + } + + private void AssertNodeMatcher(INodeMatcher matcher) + { + Assert.True(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "foo" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "boo" }, + { "bar", "baz" }, + })); + + Assert.False(matcher.IsMatch(new ConfigNode("NODE"))); + + Assert.False(matcher.IsMatch(new TestConfigNode("NADE") + { + { "name", "foo" }, + { "bar", "baz" }, + })); + + Assert.False(matcher.IsMatch(new TestConfigNode("NODE") + { + { "name", "boo" }, + { "bar", "baz" }, + })); + } + + private void AssertNodesEqual(ConfigNode expected, ConfigNode actual) + { + Assert.Equal(expected.ToString(), actual.ToString()); + } + + private void AssertNoErrors() + { + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + } +} From ffbabbbdbeb035211853584a4d7ed43832c58dd0 Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 19 Apr 2018 00:43:24 -0700 Subject: [PATCH 014/140] Fix tests depending on line endings Since we're already asserting on the ParamName, we can trust that the 2nd part of the message will be there and not test it. --- ModuleManagerTests/NodeMatcherTest.cs | 5 +---- ModuleManagerTests/PassTest.cs | 2 +- ModuleManagerTests/PatchListTest.cs | 8 ++++---- ModuleManagerTests/Patches/PatchCompilerTest.cs | 12 ++++++------ 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/ModuleManagerTests/NodeMatcherTest.cs b/ModuleManagerTests/NodeMatcherTest.cs index d1f1baf3..036770cb 100644 --- a/ModuleManagerTests/NodeMatcherTest.cs +++ b/ModuleManagerTests/NodeMatcherTest.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using Xunit; using TestUtils; using ModuleManager; @@ -32,7 +29,7 @@ public void TestConstructor__Blank() }); Assert.Equal("nodeName", ex.ParamName); - Assert.Equal("can't be empty\r\nParameter name: nodeName", ex.Message); + Assert.Contains("can't be empty", ex.Message); } [Fact] diff --git a/ModuleManagerTests/PassTest.cs b/ModuleManagerTests/PassTest.cs index 9e712e49..d2cce882 100644 --- a/ModuleManagerTests/PassTest.cs +++ b/ModuleManagerTests/PassTest.cs @@ -36,7 +36,7 @@ public void TestConstructor__NameEmpty() new Pass(""); }); - Assert.Equal("can't be empty\r\nParameter name: name", ex.Message); + Assert.Contains("can't be empty", ex.Message); Assert.Equal("name", ex.ParamName); } diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index 474ef64d..0de0a2ea 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -137,7 +137,7 @@ public void TestHasMod__Blank() Assert.True(patchList.HasMod("")); }); - Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Contains("can't be empty", ex.Message); Assert.Equal("mod", ex.ParamName); } @@ -171,7 +171,7 @@ public void TestAddBeforePatch__ModBlank() patchList.AddBeforePatch("", Substitute.For()); }); - Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Contains("can't be empty", ex.Message); Assert.Equal("mod", ex.ParamName); } @@ -216,7 +216,7 @@ public void TestAddForPatch__ModBlank() patchList.AddForPatch("", Substitute.For()); }); - Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Contains("can't be empty", ex.Message); Assert.Equal("mod", ex.ParamName); } @@ -261,7 +261,7 @@ public void TestAddAfterPatch__ModBlank() patchList.AddAfterPatch("", Substitute.For()); }); - Assert.Equal("can't be empty\r\nParameter name: mod", ex.Message); + Assert.Contains("can't be empty", ex.Message); Assert.Equal("mod", ex.ParamName); } diff --git a/ModuleManagerTests/Patches/PatchCompilerTest.cs b/ModuleManagerTests/Patches/PatchCompilerTest.cs index 178a6d7a..0b401ef3 100644 --- a/ModuleManagerTests/Patches/PatchCompilerTest.cs +++ b/ModuleManagerTests/Patches/PatchCompilerTest.cs @@ -145,7 +145,7 @@ public void TestCompilePatch__BlankName() }); Assert.Equal("name", ex.ParamName); - Assert.Equal("can't be empty\r\nParameter name: name", ex.Message); + Assert.Contains("can't be empty", ex.Message); } [Fact] @@ -157,7 +157,7 @@ public void TestCompilePatch__InvalidCommand__Replace() }); Assert.Equal("command", ex.ParamName); - Assert.Equal("invalid command for a root node: Replace\r\nParameter name: command", ex.Message); + Assert.Contains("invalid command for a root node: Replace", ex.Message); } [Fact] @@ -169,7 +169,7 @@ public void TestCompilePatch__InvalidCommand__Create() }); Assert.Equal("command", ex.ParamName); - Assert.Equal("invalid command for a root node: Create\r\nParameter name: command", ex.Message); + Assert.Contains("invalid command for a root node: Create", ex.Message); } [Fact] @@ -181,7 +181,7 @@ public void TestCompilePatch__InvalidCommand__Rename() }); Assert.Equal("command", ex.ParamName); - Assert.Equal("invalid command for a root node: Rename\r\nParameter name: command", ex.Message); + Assert.Contains("invalid command for a root node: Rename", ex.Message); } [Fact] @@ -193,7 +193,7 @@ public void TestCompilePatch__InvalidCommand__Paste() }); Assert.Equal("command", ex.ParamName); - Assert.Equal("invalid command for a root node: Paste\r\nParameter name: command", ex.Message); + Assert.Contains("invalid command for a root node: Paste", ex.Message); } [Fact] @@ -205,7 +205,7 @@ public void TestCompilePatch__InvalidCommand__Special() }); Assert.Equal("command", ex.ParamName); - Assert.Equal("invalid command for a root node: Special\r\nParameter name: command", ex.Message); + Assert.Contains("invalid command for a root node: Special", ex.Message); } private void AssertNodeMatcher(INodeMatcher matcher) From c91258096cc0c5d3fb79a08ccfa30eee950e8939 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 3 Jul 2018 23:33:31 -0700 Subject: [PATCH 015/140] Whitespace --- ModuleManager/Progress/ProgressCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Progress/ProgressCounter.cs b/ModuleManager/Progress/ProgressCounter.cs index 1400ad4f..c62d5c81 100644 --- a/ModuleManager/Progress/ProgressCounter.cs +++ b/ModuleManager/Progress/ProgressCounter.cs @@ -12,7 +12,7 @@ public class ProgressCounter public readonly Counter errors = new Counter(); public readonly Counter exceptions = new Counter(); public readonly Counter needsUnsatisfied = new Counter(); - + public readonly Dictionary errorFiles = new Dictionary(); } } From 7b230976b88f11380d84ad3d6f224d0c6a9042b5 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 3 Jul 2018 23:38:45 -0700 Subject: [PATCH 016/140] Allow warnings Cache generation can proceed but the user will be alerted. --- ModuleManager/MMPatchLoader.cs | 8 ++++++++ ModuleManager/Progress/IPatchProgress.cs | 1 + ModuleManager/Progress/PatchProgress.cs | 19 +++++++++++++++++++ ModuleManager/Progress/ProgressCounter.cs | 2 ++ .../Progress/PatchProgressTest.cs | 19 +++++++++++++++++++ 5 files changed, 49 insertions(+) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 5da80d98..7c17bd27 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -258,6 +258,11 @@ float updateTimeRemaining() #region Saving Cache + foreach (KeyValuePair item in progress.Counter.warningFiles) + { + logger.Warning(item.Value + " warning" + (item.Value > 1 ? "s" : "") + " related to GameData/" + item.Key); + } + if (progress.Counter.errors > 0 || progress.Counter.exceptions > 0) { foreach (KeyValuePair item in progress.Counter.errorFiles) @@ -693,6 +698,9 @@ private void StatusUpdate(IPatchProgress progress) status = "ModuleManager: " + progress.Counter.patchedNodes + " patch" + (progress.Counter.patchedNodes != 1 ? "es" : "") + " applied"; + if (progress.Counter.warnings > 0) + status += ", found " + progress.Counter.warnings + " warning" + (progress.Counter.warnings != 1 ? "s" : "") + ""; + if (progress.Counter.errors > 0) status += ", found " + progress.Counter.errors + " error" + (progress.Counter.errors != 1 ? "s" : "") + ""; diff --git a/ModuleManager/Progress/IPatchProgress.cs b/ModuleManager/Progress/IPatchProgress.cs index 5eabfc3c..3b6f13df 100644 --- a/ModuleManager/Progress/IPatchProgress.cs +++ b/ModuleManager/Progress/IPatchProgress.cs @@ -9,6 +9,7 @@ public interface IPatchProgress float ProgressFraction { get; } + void Warning(UrlDir.UrlConfig url, string message); void Error(UrlDir.UrlConfig url, string message); void Exception(string message, Exception exception); void Exception(UrlDir.UrlConfig url, string message, Exception exception); diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index 4939ffc6..9f1f6cfa 100644 --- a/ModuleManager/Progress/PatchProgress.cs +++ b/ModuleManager/Progress/PatchProgress.cs @@ -95,6 +95,13 @@ public void NeedsUnsatisfiedAfter(UrlDir.UrlConfig url) Counter.needsUnsatisfied.Increment(); } + public void Warning(UrlDir.UrlConfig url, string message) + { + Counter.warnings.Increment(); + logger.Warning(message); + RecordWarningFile(url); + } + public void Error(UrlDir.UrlConfig url, string message) { Counter.errors.Increment(); @@ -114,6 +121,18 @@ public void Exception(UrlDir.UrlConfig url, string message, Exception exception) RecordErrorFile(url); } + private void RecordWarningFile(UrlDir.UrlConfig url) + { + string key = url.parent.url + "." + url.parent.fileExtension; + if (key[0] == '/') + key = key.Substring(1); + + if (Counter.warningFiles.ContainsKey(key)) + Counter.warningFiles[key] += 1; + else + Counter.warningFiles[key] = 1; + } + private void RecordErrorFile(UrlDir.UrlConfig url) { string key = url.parent.url + "." + url.parent.fileExtension; diff --git a/ModuleManager/Progress/ProgressCounter.cs b/ModuleManager/Progress/ProgressCounter.cs index c62d5c81..7fc16627 100644 --- a/ModuleManager/Progress/ProgressCounter.cs +++ b/ModuleManager/Progress/ProgressCounter.cs @@ -9,10 +9,12 @@ public class ProgressCounter public readonly Counter totalPatches = new Counter(); public readonly Counter appliedPatches = new Counter(); public readonly Counter patchedNodes = new Counter(); + public readonly Counter warnings = new Counter(); public readonly Counter errors = new Counter(); public readonly Counter exceptions = new Counter(); public readonly Counter needsUnsatisfied = new Counter(); + public readonly Dictionary warningFiles = new Dictionary(); public readonly Dictionary errorFiles = new Dictionary(); } } diff --git a/ModuleManagerTests/Progress/PatchProgressTest.cs b/ModuleManagerTests/Progress/PatchProgressTest.cs index d94ae87e..cf650285 100644 --- a/ModuleManagerTests/Progress/PatchProgressTest.cs +++ b/ModuleManagerTests/Progress/PatchProgressTest.cs @@ -219,6 +219,25 @@ public void TestNeedsUnsatisfiedAfter() logger.Received().Log(LogType.Log, "Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its AFTER"); } + [Fact] + public void TestWarning() + { + UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); + UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_OTHER_NODE")); + + Assert.Equal(0, progress.Counter.warnings); + + progress.Warning(config1, "I'm warning you"); + Assert.Equal(1, progress.Counter.warnings); + Assert.Equal(1, progress.Counter.warningFiles["abc/def.cfg"]); + logger.Received().Log(LogType.Warning, "I'm warning you"); + + progress.Warning(config2, "You should probably pay attention to this"); + Assert.Equal(2, progress.Counter.warnings); + Assert.Equal(2, progress.Counter.warningFiles["abc/def.cfg"]); + logger.Received().Log(LogType.Warning, "You should probably pay attention to this"); + } + [Fact] public void TestError() { From 99984913ac564b5a06860a4094bbafe4ff335450 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 3 Jul 2018 23:39:52 -0700 Subject: [PATCH 017/140] Make ArrayEnumerator more versatile Now optionally accepts start index and length --- ModuleManager/Collections/ArrayEnumerator.cs | 35 ++++++- .../Collections/ArrayEnumeratorTest.cs | 91 +++++++++++++++++++ 2 files changed, 121 insertions(+), 5 deletions(-) diff --git a/ModuleManager/Collections/ArrayEnumerator.cs b/ModuleManager/Collections/ArrayEnumerator.cs index c965b9d3..775a35dd 100644 --- a/ModuleManager/Collections/ArrayEnumerator.cs +++ b/ModuleManager/Collections/ArrayEnumerator.cs @@ -7,12 +7,37 @@ namespace ModuleManager.Collections public struct ArrayEnumerator : IEnumerator { private readonly T[] array; + private readonly int startIndex; + private readonly int length; + private int index; - public ArrayEnumerator(params T[] array) + public ArrayEnumerator(params T[] array) : this(array, 0) { } + + public ArrayEnumerator(T[] array, int startIndex) : this(array, startIndex, (array?.Length ?? -1) - startIndex) { } + + public ArrayEnumerator(T[] array, int startIndex, int length) { - this.array = array; - index = -1; + this.array = array ?? throw new ArgumentNullException(nameof(array)); + + if (startIndex < 0) + throw new ArgumentException($"must be non-negative (got {startIndex})", nameof(startIndex)); + if (startIndex > array.Length) + throw new ArgumentException( + $"must be less than or equal to array length (array length {array.Length}, startIndex {startIndex})", + nameof(startIndex) + ); + if (length < 0) + throw new ArgumentException($"must be non-negative (got {length})", nameof(length)); + if (startIndex + length > array.Length) + throw new ArgumentException( + $"must fit within the string (array length {array.Length}, startIndex {startIndex}, length {length})", + nameof(length) + ); + + this.startIndex = startIndex; + this.length = length; + index = startIndex - 1; } public T Current => array[index]; @@ -23,12 +48,12 @@ public void Dispose() { } public bool MoveNext() { index++; - return index < array.Length; + return index < startIndex + length; } public void Reset() { - index = -1; + index = startIndex - 1; } } } diff --git a/ModuleManagerTests/Collections/ArrayEnumeratorTest.cs b/ModuleManagerTests/Collections/ArrayEnumeratorTest.cs index 34eac1ed..7ba5cf64 100644 --- a/ModuleManagerTests/Collections/ArrayEnumeratorTest.cs +++ b/ModuleManagerTests/Collections/ArrayEnumeratorTest.cs @@ -7,6 +7,69 @@ namespace ModuleManagerTests.Collections { public class ArrayEnumeratorTest { + [Fact] + public void Test__Constructor__ArrayNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new ArrayEnumerator(null); + }); + + Assert.Equal("array", ex.ParamName); + } + + [Fact] + public void Test__Constructor__StartIndex__Negative() + { + string[] arr = { "abc", "def", "ghi" }; + ArgumentException ex = Assert.Throws(delegate + { + new ArrayEnumerator(arr, -1); + }); + + Assert.Equal("startIndex", ex.ParamName); + Assert.Contains("must be non-negative (got -1)", ex.Message); + } + + [Fact] + public void Test__Constructor__StartIndex__TooLarge() + { + string[] arr = { "abc", "def", "ghi" }; + ArgumentException ex = Assert.Throws(delegate + { + new ArrayEnumerator(arr, 4); + }); + + Assert.Equal("startIndex", ex.ParamName); + Assert.Contains("must be less than or equal to array length (array length 3, startIndex 4)", ex.Message); + } + + [Fact] + public void Test__Constructor__Length__Negative() + { + string[] arr = { "abc", "def", "ghi" }; + ArgumentException ex = Assert.Throws(delegate + { + new ArrayEnumerator(arr, 1, -1); + }); + + Assert.Equal("length", ex.ParamName); + Assert.Contains("must be non-negative (got -1)", ex.Message); + } + + [Fact] + public void Test__Constructor__Length__TooLong() + { + string[] arr = { "abc", "def", "ghi" }; + ArgumentException ex = Assert.Throws(delegate + { + new ArrayEnumerator(arr, 1, 3); + }); + + Assert.Equal("length", ex.ParamName); + Assert.Contains("must fit within the string (array length 3, startIndex 1, length 3)", ex.Message); + } + [Fact] public void TestArrayEnumerator() { @@ -52,5 +115,33 @@ public void TestArrayEnumerator__Empty() IEnumerator enumerator = new ArrayEnumerator(arr); Assert.False(enumerator.MoveNext()); } + + [Fact] + public void TestArrayEnumerator__StartIndex() + { + string[] arr = { "abc", "def", "ghi" }; + + IEnumerator enumerator = new ArrayEnumerator(arr, 1); + + Assert.True(enumerator.MoveNext()); + Assert.Equal("def", enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal("ghi", enumerator.Current); + Assert.False(enumerator.MoveNext()); + } + + [Fact] + public void TestArrayEnumerator__StartIndex__Length() + { + string[] arr = { "abc", "def", "ghi", "jkl" }; + + IEnumerator enumerator = new ArrayEnumerator(arr, 1, 2); + + Assert.True(enumerator.MoveNext()); + Assert.Equal("def", enumerator.Current); + Assert.True(enumerator.MoveNext()); + Assert.Equal("ghi", enumerator.Current); + Assert.False(enumerator.MoveNext()); + } } } From 7d994ad71842dd85e4c48efcdeed075d8ed09dd3 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 3 Jul 2018 23:53:23 -0700 Subject: [PATCH 018/140] Tag, TagList, TagListParser A lot of things in MM are structured like :tag[value]trailer and this formalizes that structure --- ModuleManager/ModuleManager.csproj | 3 + ModuleManager/Tags/Tag.cs | 34 +++ ModuleManager/Tags/TagList.cs | 30 +++ ModuleManager/Tags/TagListParser.cs | 160 ++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 3 + ModuleManagerTests/Tags/TagListParserTest.cs | 253 +++++++++++++++++++ ModuleManagerTests/Tags/TagListTest.cs | 44 ++++ ModuleManagerTests/Tags/TagTest.cs | 145 +++++++++++ 8 files changed, 672 insertions(+) create mode 100644 ModuleManager/Tags/Tag.cs create mode 100644 ModuleManager/Tags/TagList.cs create mode 100644 ModuleManager/Tags/TagListParser.cs create mode 100644 ModuleManagerTests/Tags/TagListParserTest.cs create mode 100644 ModuleManagerTests/Tags/TagListTest.cs create mode 100644 ModuleManagerTests/Tags/TagTest.cs diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 0bb5a934..3468f389 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -75,6 +75,9 @@ + + + diff --git a/ModuleManager/Tags/Tag.cs b/ModuleManager/Tags/Tag.cs new file mode 100644 index 00000000..173058a9 --- /dev/null +++ b/ModuleManager/Tags/Tag.cs @@ -0,0 +1,34 @@ +using System; + +namespace ModuleManager.Tags +{ + public struct Tag + { + public readonly string key; + public readonly string value; + public readonly string trailer; + + public Tag(string key, string value, string trailer) + { + this.key = key ?? throw new ArgumentNullException(nameof(key)); + if (key == string.Empty) throw new ArgumentException("can't be empty", nameof(key)); + + if (value == null && trailer != null) + throw new ArgumentException("trailer must be null if value is null"); + + if (trailer == string.Empty) throw new ArgumentException("can't be empty (null allowed)", nameof(trailer)); + + this.value = value; + this.trailer = trailer; + } + + public override string ToString() + { + string s = "< '" + key + "' "; + if (value != null) s += "[ '" + value + "' ] "; + if (trailer != null) s += "'" + trailer + "' "; + s += ">"; + return s; + } + } +} diff --git a/ModuleManager/Tags/TagList.cs b/ModuleManager/Tags/TagList.cs new file mode 100644 index 00000000..0744c411 --- /dev/null +++ b/ModuleManager/Tags/TagList.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using ModuleManager.Collections; + +namespace ModuleManager.Tags +{ + public interface ITagList : IEnumerable + { + Tag PrimaryTag { get; } + } + + public class TagList : ITagList + { + private readonly Tag[] tags; + + public TagList(Tag primaryTag, IEnumerable tags) + { + PrimaryTag = primaryTag; + this.tags = tags?.ToArray() ?? throw new ArgumentNullException(nameof(tags)); + } + + public Tag PrimaryTag { get; private set; } + + public ArrayEnumerator GetEnumerator() => new ArrayEnumerator(tags); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/ModuleManager/Tags/TagListParser.cs b/ModuleManager/Tags/TagListParser.cs new file mode 100644 index 00000000..b714240e --- /dev/null +++ b/ModuleManager/Tags/TagListParser.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; + +namespace ModuleManager.Tags +{ + public interface ITagListParser + { + ITagList Parse(string ToParse); + } + + public class TagListParser : ITagListParser + { + public ITagList Parse(string toParse) + { + if (toParse == null) throw new ArgumentNullException(nameof(toParse)); + if (toParse.Length == 0) throw new FormatException("can't create tag list from empty string"); + if (toParse[0] == '[') throw new FormatException("can't create tag list beginning with ["); + if (toParse[0] == ':') throw new FormatException("can't create tag list beginning with :"); + if (toParse[toParse.Length - 1] == ':') throw new FormatException("tag list can't end with :"); + + List tags = new List(); + Tag primaryTag = ParsePrimaryTag(toParse, ref tags); + return new TagList(primaryTag, tags); + } + + private static Tag ParsePrimaryTag(string toParse, ref List tags) + { + for (int i = 1; i < toParse.Length; i++) + { + char c = toParse[i]; + + if (c == '[') + { + int j = ClosingBracketIndex(toParse, i + 1); + return ParsePrimaryTrailer(toParse, j + 1, ref tags, toParse.Substring(0, i), toParse.Substring(i + 1, j - i - 1)); + } + else if (c == ':') + { + ParseTag(toParse, i + 1, ref tags); + return new Tag(toParse.Substring(0, i), null, null); + } + else if (c == ']') + { + throw new FormatException("encountered closing bracket in primary key"); + } + } + + return new Tag(toParse, null, null); + } + + private static Tag ParsePrimaryTrailer(string toParse, int start, ref List tags, string primaryKey, string primaryValue) + { + for (int i = start; i < toParse.Length; i++) + { + char c = toParse[i]; + + if (c == ':') + { + string trailer = i == start ? null : toParse.Substring(start, i - start); + ParseTag(toParse, i + 1, ref tags); + return new Tag(primaryKey, primaryValue, trailer); + } + else if (c == '[') + { + throw new FormatException("encountered opening bracket in primary trailer"); + } + else if (c == ']') + { + throw new FormatException("encountered closing bracket in primary trailer"); + } + } + + string primaryTrailer = toParse.Length - start == 0 ? null : toParse.Substring(start); + return new Tag(primaryKey, primaryValue, primaryTrailer); + } + + private static void ParseTag(string toParse, int start, ref List tags) + { + for (int i = start; i < toParse.Length; i++) + { + char c = toParse[i]; + + if (c == '[') + { + if (i == start) + throw new FormatException("tag can't start with ["); + + int j = ClosingBracketIndex(toParse, i + 1); + ParseTrailer(toParse, j + 1, ref tags, toParse.Substring(start, i - start), toParse.Substring(i + 1, j - i - 1)); + return; + } + else if (c == ':') + { + if (i == start) + throw new FormatException("tag can't start with :"); + + tags.Add(new Tag(toParse.Substring(start, i - start), null, null)); + ParseTag(toParse, i + 1, ref tags); + return; + } + else if (c == ']') + { + throw new FormatException("encountered closing bracket in key"); + } + } + + tags.Add(new Tag(toParse.Substring(start), null, null)); + } + + private static void ParseTrailer(string toParse, int start, ref List tags, string key, string value) + { + for (int i = start; i < toParse.Length; i++) + { + char c = toParse[i]; + + if (c == ':') + { + string trailer = i == start ? null : toParse.Substring(start, i - start); + tags.Add(new Tag(key, value, trailer)); + ParseTag(toParse, i + 1, ref tags); + return; + } + else if (c == '[') + { + throw new FormatException("encountered opening bracket in trailer"); + } + else if (c == ']') + { + throw new FormatException("encountered closing bracket in trailer"); + } + } + + string finalTrailer = toParse.Length - start == 0 ? null : toParse.Substring(start); + tags.Add(new Tag(key, value, finalTrailer)); + } + + private static int ClosingBracketIndex(string toParse, int start) + { + int bracketLevel = 0; + + for (int i = start; i < toParse.Length; i++) + { + char c = toParse[i]; + + if (c == '[') + { + bracketLevel++; + } + else if (c == ']') + { + bracketLevel--; + } + + if (bracketLevel == -1) return i; + } + + throw new FormatException("reached end of the tag list without encountering a close bracket"); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 87d7ec27..7a2d4e8c 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -79,6 +79,9 @@ + + + diff --git a/ModuleManagerTests/Tags/TagListParserTest.cs b/ModuleManagerTests/Tags/TagListParserTest.cs new file mode 100644 index 00000000..b13277e8 --- /dev/null +++ b/ModuleManagerTests/Tags/TagListParserTest.cs @@ -0,0 +1,253 @@ +using System; +using Xunit; +using ModuleManager.Tags; + +namespace ModuleManagerTests.Tags +{ + public class TagListParserTest + { + private readonly TagListParser tagListParser = new TagListParser(); + + [Fact] + public void TestParse__OnlyPrimaryKey() + { + ITagList tagList = tagListParser.Parse("01"); + Assert.Equal(new Tag("01", null, null), tagList.PrimaryTag); + Assert.Empty(tagList); + } + + [Fact] + public void TestParse__OnlyPrimaryKeyAndValue() + { + ITagList tagList = tagListParser.Parse("01[02]"); + Assert.Equal(new Tag("01", "02", null), tagList.PrimaryTag); + Assert.Empty(tagList); + } + + [Fact] + public void TestParse__OnlyPrimaryKeyValueAndTrailer() + { + ITagList tagList = tagListParser.Parse("01[02]03"); + Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); + Assert.Empty(tagList); + } + + [Fact] + public void TestParse__OnlyPrimaryKeyValueAndTrailer__ValueHasSomeStuff() + { + ITagList tagList = tagListParser.Parse("01[02:[03:04[05]]]06"); + Assert.Equal(new Tag("01", "02:[03:04[05]]", "06"), tagList.PrimaryTag); + Assert.Empty(tagList); + } + + [Fact] + public void TestParse__TagWithOnlyKey() + { + ITagList tagList = tagListParser.Parse("01[02]03:04:05"); + Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); + Assert.Equal(new[] { + new Tag("04", null, null), + new Tag("05", null, null), + }, tagList); + } + + [Fact] + public void TestParse__TagWithOnlyKeyAndValue() + { + ITagList tagList = tagListParser.Parse("01[02]03:04[05]:06[07]"); + Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); + Assert.Equal(new[] { + new Tag("04", "05", null), + new Tag("06", "07", null), + }, tagList); + } + + [Fact] + public void TestParse__FullTags() + { + ITagList tagList = tagListParser.Parse("01[02]03:04[05]06:07[08]09"); + Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); + Assert.Equal(new[] { + new Tag("04", "05", "06"), + new Tag("07", "08", "09"), + }, tagList); + } + + [Fact] + public void TestParse__MixedTags() + { + ITagList tagList = tagListParser.Parse("01[02]03:04:05[06]:07[08]09"); + Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); + Assert.Equal(new[] { + new Tag("04", null, null), + new Tag("05", "06", null), + new Tag("07", "08", "09"), + }, tagList); + } + + [Fact] + public void TestParse__Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + tagListParser.Parse(null); + }); + + Assert.Equal("toParse", ex.ParamName); + } + + [Fact] + public void TestParse__Empty() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse(""); + }); + + Assert.Equal("can't create tag list from empty string", ex.Message); + } + + [Fact] + public void TestParse__StartsWithOpenBracket() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("[stuff]"); + }); + + Assert.Equal("can't create tag list beginning with [", ex.Message); + } + + [Fact] + public void TestParse__StartsWithColon() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse(":stuff"); + }); + + Assert.Equal("can't create tag list beginning with :", ex.Message); + } + + [Fact] + public void TestParse__EndsWithColon() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("stuff:blah:"); + }); + + Assert.Equal("tag list can't end with :", ex.Message); + } + + [Fact] + public void TestParse__ClosingBracketInPrimaryKey() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc]def"); + }); + + Assert.Equal("encountered closing bracket in primary key", ex.Message); + } + + [Fact] + public void TestParse__PrimaryValueHasNoClosingBracket() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc[def[ghi]jkl"); + }); + + Assert.Equal("reached end of the tag list without encountering a close bracket", ex.Message); + } + + [Fact] + public void TestParse__OpeningBracketInPrimaryTrailer() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc[def]ghi[jkl]"); + }); + + Assert.Equal("encountered opening bracket in primary trailer", ex.Message); + } + + [Fact] + public void TestParse__ClosingBracketInPrimaryTrailer() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc[def]ghi]jkl"); + }); + + Assert.Equal("encountered closing bracket in primary trailer", ex.Message); + } + + [Fact] + public void TestParse__TagStartsWithOpenBracket() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc:def:[ghi]"); + }); + + Assert.Equal("tag can't start with [", ex.Message); + } + + [Fact] + public void TestParse__TagStartsWithColon() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc:def::ghi"); + }); + + Assert.Equal("tag can't start with :", ex.Message); + } + + [Fact] + public void TestParse__ClosingBracketInKey() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc:def:ghi]jkl"); + }); + + Assert.Equal("encountered closing bracket in key", ex.Message); + } + + [Fact] + public void TestParse__ValueHasNoClosingBracket() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc:def:ghi[jkl[mno]pqr"); + }); + + Assert.Equal("reached end of the tag list without encountering a close bracket", ex.Message); + } + + [Fact] + public void TestParse__OpeningBracketInTrailer() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc:def:ghi[jkl]mno[pqr]"); + }); + + Assert.Equal("encountered opening bracket in trailer", ex.Message); + } + + [Fact] + public void TestParse__ClosingBracketInTrailer() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc:def:ghi[jkl]mno]pqr"); + }); + + Assert.Equal("encountered closing bracket in trailer", ex.Message); + } + } +} diff --git a/ModuleManagerTests/Tags/TagListTest.cs b/ModuleManagerTests/Tags/TagListTest.cs new file mode 100644 index 00000000..0d3d40dd --- /dev/null +++ b/ModuleManagerTests/Tags/TagListTest.cs @@ -0,0 +1,44 @@ +using System; +using Xunit; +using ModuleManager.Tags; + +namespace ModuleManagerTests.Tags +{ + public class TagListTest + { + [Fact] + public void TestPrimaryTag() + { + Tag primaryTag = new Tag("stuff", null, null); + TagList tagList = new TagList(primaryTag, new Tag[0]); + + Assert.Equal(primaryTag, tagList.PrimaryTag); + } + + [Fact] + public void TestEnumeration() + { + Tag primaryTag = new Tag("stuff", null, null); + Tag tag1 = new Tag("tag1", null, null); + Tag tag2 = new Tag("tag2", null, null); + + Tag[] tags = new Tag[] { tag1, tag2 }; + TagList tagList = new TagList(primaryTag, tags); + + tags[0] = new Tag("tag3", null, null); + + Assert.Equal(new[] { tag1, tag2 }, tagList); + } + + [Fact] + public void TestConstructor__TagsNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new TagList(new Tag("blah", null, null), null); + }); + + Assert.Equal("tags", ex.ParamName); + } + } +} diff --git a/ModuleManagerTests/Tags/TagTest.cs b/ModuleManagerTests/Tags/TagTest.cs new file mode 100644 index 00000000..fec92b91 --- /dev/null +++ b/ModuleManagerTests/Tags/TagTest.cs @@ -0,0 +1,145 @@ +using System; +using Xunit; +using ModuleManager.Tags; + +namespace ModuleManagerTests.Tags +{ + public class TagTest + { + [Fact] + public void Test__OnlyKey() + { + Tag tag = new Tag("key", null, null); + + Assert.Equal("key", tag.key); + Assert.Null(tag.value); + Assert.Null(tag.trailer); + } + + [Fact] + public void Test__KeyAndValue() + { + Tag tag = new Tag("key", "value", null); + + Assert.Equal("key", tag.key); + Assert.Equal("value", tag.value); + Assert.Null(tag.trailer); + } + + [Fact] + public void Test__KeyAndEmptyValue() + { + Tag tag = new Tag("key", "", null); + + Assert.Equal("key", tag.key); + Assert.Equal("", tag.value); + Assert.Null(tag.trailer); + } + + [Fact] + public void Test__KeyValueAndTrailer() + { + Tag tag = new Tag("key", "value", "trailer"); + + Assert.Equal("key", tag.key); + Assert.Equal("value", tag.value); + Assert.Equal("trailer", tag.trailer); + } + + [Fact] + public void Test__KeyEmptyValueAndTrailer() + { + Tag tag = new Tag("key", "", "trailer"); + + Assert.Equal("key", tag.key); + Assert.Equal("", tag.value); + Assert.Equal("trailer", tag.trailer); + } + + [Fact] + public void TestConstructor__KeyNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new Tag(null, "value", "trailer"); + }); + + Assert.Equal("key", ex.ParamName); + } + + [Fact] + public void TestConstructor__KeyEmpty() + { + ArgumentException ex = Assert.Throws(delegate + { + new Tag("", "value", "trailer"); + }); + + Assert.Equal("key", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); + } + + [Fact] + public void TestConstructor__ValueNullButTrailerNotNull() + { + ArgumentException ex = Assert.Throws(delegate + { + new Tag("key", null, "trailer"); + }); + + Assert.Contains("trailer must be null if value is null", ex.Message); + } + + [Fact] + public void TestConstructor__TrailerEmpty() + { + ArgumentException ex = Assert.Throws(delegate + { + new Tag("key", "value", ""); + }); + + Assert.Equal("trailer", ex.ParamName); + Assert.Contains("can't be empty (null allowed)", ex.Message); + } + + [Fact] + public void TestToString__Key() + { + Tag tag = new Tag("key", null, null); + + Assert.Equal("< 'key' >", tag.ToString()); + } + + [Fact] + public void TestToString__KeyAndValue() + { + Tag tag = new Tag("key", "value", null); + + Assert.Equal("< 'key' [ 'value' ] >", tag.ToString()); + } + + [Fact] + public void TestToString__KeyAndEmptyValue() + { + Tag tag = new Tag("key", "", null); + + Assert.Equal("< 'key' [ '' ] >", tag.ToString()); + } + + [Fact] + public void TestToString__KeyValueAndTrailer() + { + Tag tag = new Tag("key", "value", "trailer"); + + Assert.Equal("< 'key' [ 'value' ] 'trailer' >", tag.ToString()); + } + + [Fact] + public void TestToString__KeyEmptyValueAndTrailer() + { + Tag tag = new Tag("key", "", "trailer"); + + Assert.Equal("< 'key' [ '' ] 'trailer' >", tag.ToString()); + } + } +} From 9b79b392f325efd89949a41c920f4871946a2ea5 Mon Sep 17 00:00:00 2001 From: blowfish Date: Wed, 4 Jul 2018 00:25:42 -0700 Subject: [PATCH 019/140] Restructure patch building Lots of changes * Parsing/validating patch is now separate code * Less code in the patch extractor (may even be able to go away entirely with some simplifications) * Pass specifier is now an explicit concept * Needs checker is now an object and has a cleaner interface * Some things which were errors before are now just warnings * If there is more than one pass specifier it will take the first one and warn * Syntax for root patch names is much more formal now, this might break some unusual cases that are silently accepted now --- ModuleManager/MMPatchLoader.cs | 40 +- ModuleManager/ModuleManager.csproj | 10 + ModuleManager/NeedsChecker.cs | 227 ++- ModuleManager/NodeMatcher.cs | 46 +- ModuleManager/PatchApplier.cs | 36 +- ModuleManager/PatchExtractor.cs | 201 +-- ModuleManager/PatchList.cs | 100 +- ModuleManager/Patches/CopyPatch.cs | 5 +- ModuleManager/Patches/DeletePatch.cs | 5 +- ModuleManager/Patches/EditPatch.cs | 5 +- ModuleManager/Patches/IPatch.cs | 2 + .../PassSpecifiers/AfterPassSpecifier.cs | 29 + .../PassSpecifiers/BeforePassSpecifier.cs | 29 + .../PassSpecifiers/FinalPassSpecifier.cs | 17 + .../PassSpecifiers/FirstPassSpecifier.cs | 17 + .../PassSpecifiers/ForPassSpecifier.cs | 29 + .../Patches/PassSpecifiers/IPassSpecifier.cs | 11 + .../PassSpecifiers/InsertPassSpecifier.cs | 17 + .../PassSpecifiers/LegacyPassSpecifier.cs | 17 + ModuleManager/Patches/PatchCompiler.cs | 25 +- ModuleManager/Patches/ProtoPatch.cs | 27 + ModuleManager/Patches/ProtoPatchBuilder.cs | 224 +++ ModuleManager/Progress/IPatchProgress.cs | 4 +- ModuleManager/Progress/PatchProgress.cs | 8 +- ModuleManagerTests/ModuleManagerTests.csproj | 8 + ModuleManagerTests/NeedsCheckerTest.cs | 647 +++------ ModuleManagerTests/NodeMatcherTest.cs | 51 +- ModuleManagerTests/PatchApplierTest.cs | 120 +- ModuleManagerTests/PatchExtractorTest.cs | 830 ++++------- ModuleManagerTests/PatchListTest.cs | 328 ++--- ModuleManagerTests/Patches/CopyPatchTest.cs | 41 +- ModuleManagerTests/Patches/DeletePatchTest.cs | 37 +- ModuleManagerTests/Patches/EditPatchTest.cs | 39 +- .../PassSpecifiers/AfterPassSpecifierTest.cs | 105 ++ .../PassSpecifiers/BeforePassSpecifierTest.cs | 105 ++ .../PassSpecifiers/FinalPassSpecifierTest.cs | 52 + .../PassSpecifiers/FirstPassSpecifierTest.cs | 52 + .../PassSpecifiers/ForPassSpecifierTest.cs | 105 ++ .../PassSpecifiers/InsertPassSpecifierTest.cs | 52 + .../PassSpecifiers/LegacyPassSpecifierTest.cs | 52 + .../Patches/PatchCompilerTest.cs | 175 ++- .../Patches/ProtoPatchBuilderTest.cs | 1231 +++++++++++++++++ .../Progress/PatchProgressTest.cs | 20 +- 43 files changed, 3343 insertions(+), 1838 deletions(-) create mode 100644 ModuleManager/Patches/PassSpecifiers/AfterPassSpecifier.cs create mode 100644 ModuleManager/Patches/PassSpecifiers/BeforePassSpecifier.cs create mode 100644 ModuleManager/Patches/PassSpecifiers/FinalPassSpecifier.cs create mode 100644 ModuleManager/Patches/PassSpecifiers/FirstPassSpecifier.cs create mode 100644 ModuleManager/Patches/PassSpecifiers/ForPassSpecifier.cs create mode 100644 ModuleManager/Patches/PassSpecifiers/IPassSpecifier.cs create mode 100644 ModuleManager/Patches/PassSpecifiers/InsertPassSpecifier.cs create mode 100644 ModuleManager/Patches/PassSpecifiers/LegacyPassSpecifier.cs create mode 100644 ModuleManager/Patches/ProtoPatch.cs create mode 100644 ModuleManager/Patches/ProtoPatchBuilder.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/AfterPassSpecifierTest.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/BeforePassSpecifierTest.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/FinalPassSpecifierTest.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/FirstPassSpecifierTest.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/ForPassSpecifierTest.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/InsertPassSpecifierTest.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/LegacyPassSpecifierTest.cs create mode 100644 ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 7c17bd27..6af7d5d7 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -15,7 +15,9 @@ using ModuleManager.Logging; using ModuleManager.Extensions; using ModuleManager.Collections; +using ModuleManager.Tags; using ModuleManager.Threading; +using ModuleManager.Patches; using ModuleManager.Progress; using NodeStack = ModuleManager.Collections.ImmutableStack; @@ -156,35 +158,24 @@ private IEnumerator ProcessPatch() LoadPhysicsConfig(); - #region Check Needs - - - - // Do filtering with NEEDS - status = "Checking NEEDS."; - logger.Info(status); - yield return null; - NeedsChecker.CheckNeeds(GameDatabase.Instance.root, mods, progress, logger); - - #endregion Check Needs - #region Sorting Patches - status = "Sorting patches"; + status = "Extracting patches"; logger.Info(status); yield return null; - // PatchList patchList = PatchExtractor.SortAndExtractPatches(GameDatabase.Instance.root, mods, progress); - - PatchList patchList = new PatchList(mods); - PatchExtractor extractor = new PatchExtractor(patchList, progress, logger); + UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == ""); + INeedsChecker needsChecker = new NeedsChecker(mods, gameData, progress, logger); + ITagListParser tagListParser = new TagListParser(); + IProtoPatchBuilder protoPatchBuilder = new ProtoPatchBuilder(progress); + IPatchCompiler patchCompiler = new PatchCompiler(); + PatchExtractor extractor = new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); // Have to convert to an array because we will be removing patches - foreach (UrlDir.UrlConfig urlConfig in GameDatabase.Instance.root.AllConfigs.ToArray()) - { - extractor.ExtractPatch(urlConfig); - } + UrlDir.UrlConfig[] allConfigs = GameDatabase.Instance.root.AllConfigs.ToArray(); + IEnumerable extractedPatches = allConfigs.Select(urlConfig => extractor.ExtractPatch(urlConfig)); + PatchList patchList = new PatchList(mods, extractedPatches.Where(patch => patch != null), progress); #endregion @@ -198,11 +189,14 @@ private IEnumerator ProcessPatch() MessageQueue logQueue = new MessageQueue(); IBasicLogger patchLogger = new QueueLogger(logQueue); IPatchProgress threadPatchProgress = new PatchProgress(progress, patchLogger); - PatchApplier applier = new PatchApplier(patchList, GameDatabase.Instance.root, threadPatchProgress, patchLogger); + PatchApplier applier = new PatchApplier(threadPatchProgress, patchLogger); logger.Info("Starting patch thread"); - ITaskStatus patchThread = BackgroundTask.Start(applier.ApplyPatches); + ITaskStatus patchThread = BackgroundTask.Start(delegate + { + applier.ApplyPatches(GameDatabase.Instance.root.AllConfigFiles.ToArray(), patchList); + }); float nextYield = Time.realtimeSinceStartup + yieldInterval; diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 3468f389..57fc26ae 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -63,11 +63,21 @@ + + + + + + + + + + diff --git a/ModuleManager/NeedsChecker.cs b/ModuleManager/NeedsChecker.cs index 792766cf..7e452642 100644 --- a/ModuleManager/NeedsChecker.cs +++ b/ModuleManager/NeedsChecker.cs @@ -8,87 +8,122 @@ namespace ModuleManager { - public static class NeedsChecker + public interface INeedsChecker { - public static void CheckNeeds(UrlDir gameDatabaseRoot, IEnumerable mods, IPatchProgress progress, IBasicLogger logger) + bool CheckNeeds(string mod); + bool CheckNeedsExpression(string needsString); + void CheckNeedsRecursive(ConfigNode node, UrlDir.UrlConfig urlConfig); + } + + public class NeedsChecker : INeedsChecker + { + private readonly IEnumerable mods; + private readonly UrlDir gameData; + private readonly IPatchProgress progress; + private readonly IBasicLogger logger; + + public NeedsChecker(IEnumerable mods, UrlDir gameData, IPatchProgress progress, IBasicLogger logger) { - UrlDir gameData = gameDatabaseRoot.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == ""); + this.mods = mods ?? throw new ArgumentNullException(nameof(mods)); + this.gameData = gameData ?? throw new ArgumentNullException(nameof(gameData)); + this.progress = progress ?? throw new ArgumentNullException(nameof(progress)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } - foreach (UrlDir.UrlConfig mod in gameDatabaseRoot.AllConfigs.ToArray()) + public bool CheckNeeds(string mod) + { + if (mod == null) throw new ArgumentNullException(nameof(mod)); + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + return mods.Contains(mod, StringComparer.InvariantCultureIgnoreCase); + } + + public bool CheckNeedsExpression(string needsExpression) + { + if (needsExpression == null) throw new ArgumentNullException(nameof(needsExpression)); + if (needsExpression == string.Empty) throw new ArgumentException("can't be empty", nameof(needsExpression)); + + foreach (string andDependencies in needsExpression.Split(',', '&')) { - UrlDir.UrlConfig currentMod = mod; - try + bool orMatch = false; + foreach (string orDependency in andDependencies.Split('|')) { - if (mod.config.name == null) - { - progress.Error(currentMod, "Error - Node in file " + currentMod.parent.url + " subnode: " + currentMod.type + - " has config.name == null"); - } + if (orDependency.Length == 0) + continue; - UrlDir.UrlConfig newMod; + bool not = orDependency[0] == '!'; + string toFind = not ? orDependency.Substring(1) : orDependency; - if (currentMod.type.IndexOf(":NEEDS[", StringComparison.OrdinalIgnoreCase) >= 0) - { - string type = currentMod.type; - - if (CheckNeeds(ref type, mods, gameData)) - { - - ConfigNode copy = new ConfigNode(type); - copy.ShallowCopyFrom(currentMod.config); - int index = mod.parent.configs.IndexOf(currentMod); - newMod = new UrlDir.UrlConfig(currentMod.parent, copy); - mod.parent.configs[index] = newMod; - } - else - { - progress.NeedsUnsatisfiedRoot(currentMod); - mod.parent.configs.Remove(currentMod); - continue; - } - } - else - { - newMod = currentMod; - } + bool found = CheckNeedsWithDirectories(toFind); - // Recursively check the contents - PatchContext context = new PatchContext(newMod, gameDatabaseRoot, logger, progress); - CheckNeeds(new NodeStack(newMod.config), context, mods, gameData); - } - catch (Exception ex) - { - try - { - mod.parent.configs.Remove(currentMod); - } - catch(Exception ex2) + if (not == !found) { - logger.Exception("Exception while attempting to ensure config removed" ,ex2); + orMatch = true; + break; } + } + if (!orMatch) + return false; + } - try - { - progress.Exception(mod, "Exception while checking needs on root node :\n" + mod.PrettyPrint(), ex); - } - catch (Exception ex2) + return true; + } + + public void CheckNeedsRecursive(ConfigNode node, UrlDir.UrlConfig urlConfig) + { + if (node == null) throw new ArgumentNullException(nameof(node)); + if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); + CheckNeedsRecursive(new NodeStack(node), urlConfig); + } + + private bool CheckNeedsWithDirectories(string mod) + { + if (CheckNeeds(mod)) return true; + if (mod.Contains('/')) + { + string[] splits = mod.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + bool result = true; + UrlDir current = gameData; + for (int i = 0; i < splits.Length; i++) + { + current = current.children.FirstOrDefault(dir => dir.name == splits[i]); + if (current == null) { - progress.Exception("Exception while attempting to log an exception", ex2); + result = false; + break; } } + return result; } + return false; + } + + private bool CheckNeedsName(ref string name) + { + if (name == null) + return true; + + int idxStart = name.IndexOf(":NEEDS[", StringComparison.OrdinalIgnoreCase); + if (idxStart < 0) + return true; + int idxEnd = name.IndexOf(']', idxStart + 7); + string needsString = name.Substring(idxStart + 7, idxEnd - idxStart - 7); + + name = name.Substring(0, idxStart) + name.Substring(idxEnd + 1); + + return CheckNeedsExpression(needsString); } - private static void CheckNeeds(NodeStack stack, PatchContext context, IEnumerable mods, UrlDir gameData) + private void CheckNeedsRecursive(NodeStack nodeStack, UrlDir.UrlConfig urlConfig) { - ConfigNode original = stack.value; + ConfigNode original = nodeStack.value; for (int i = 0; i < original.values.Count; ++i) { ConfigNode.Value val = original.values[i]; string valname = val.name; try { - if (CheckNeeds(ref valname, mods, gameData)) + if (CheckNeedsName(ref valname)) { val.name = valname; } @@ -96,17 +131,17 @@ private static void CheckNeeds(NodeStack stack, PatchContext context, IEnumerabl { original.values.Remove(val); i--; - context.progress.NeedsUnsatisfiedValue(context.patchUrl, stack, val.name); + progress.NeedsUnsatisfiedValue(urlConfig, nodeStack.GetPath() + '/' + val.name); } } catch (ArgumentOutOfRangeException e) { - context.progress.Exception("ArgumentOutOfRangeException in CheckNeeds for value \"" + val.name + "\"", e); + progress.Exception("ArgumentOutOfRangeException in CheckNeeds for value \"" + val.name + "\"", e); throw; } catch (Exception e) { - context.progress.Exception("General Exception in CheckNeeds for value \"" + val.name + "\"", e); + progress.Exception("General Exception in CheckNeeds for value \"" + val.name + "\"", e); throw; } } @@ -118,94 +153,34 @@ private static void CheckNeeds(NodeStack stack, PatchContext context, IEnumerabl if (nodeName == null) { - context.progress.Error(context.patchUrl, "Error - Node in file " + context.patchUrl.SafeUrl() + " subnode: " + stack.GetPath() + - " has config.name == null"); + progress.Error(urlConfig, "Error - Node in file " + urlConfig.SafeUrl() + " subnode: " + nodeStack.GetPath() + " has config.name == null"); } try { - if (CheckNeeds(ref nodeName, mods, gameData)) + if (CheckNeedsName(ref nodeName)) { node.name = nodeName; - CheckNeeds(stack.Push(node), context, mods, gameData); + CheckNeedsRecursive(nodeStack.Push(node), urlConfig); } else { original.nodes.Remove(node); i--; - context.progress.NeedsUnsatisfiedNode(context.patchUrl, stack.Push(node)); + progress.NeedsUnsatisfiedNode(urlConfig, nodeStack.GetPath() + '/' + node.name); } } catch (ArgumentOutOfRangeException e) { - context.progress.Exception("ArgumentOutOfRangeException in CheckNeeds for node \"" + node.name + "\"", e); + progress.Exception("ArgumentOutOfRangeException in CheckNeeds for node \"" + node.name + "\"", e); throw; } catch (Exception e) { - context.progress.Exception("General Exception " + e.GetType().Name + " for node \"" + node.name + "\"", e); + progress.Exception("General Exception " + e.GetType().Name + " for node \"" + node.name + "\"", e); throw; } } } - - /// - /// Returns true if needs are satisfied. - /// - private static bool CheckNeeds(ref string name, IEnumerable mods, UrlDir gameData) - { - if (name == null) - return true; - - int idxStart = name.IndexOf(":NEEDS[", StringComparison.OrdinalIgnoreCase); - if (idxStart < 0) - return true; - int idxEnd = name.IndexOf(']', idxStart + 7); - string needsString = name.Substring(idxStart + 7, idxEnd - idxStart - 7); - - name = name.Substring(0, idxStart) + name.Substring(idxEnd + 1); - - // Check to see if all the needed dependencies are present. - foreach (string andDependencies in needsString.Split(',', '&')) - { - bool orMatch = false; - foreach (string orDependency in andDependencies.Split('|')) - { - if (orDependency.Length == 0) - continue; - - bool not = orDependency[0] == '!'; - string toFind = not ? orDependency.Substring(1) : orDependency; - - bool found = mods.Contains(toFind.ToUpper(), StringComparer.OrdinalIgnoreCase); - if (!found && toFind.Contains('/')) - { - string[] splits = toFind.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - - found = true; - UrlDir current = gameData; - for (int i = 0; i < splits.Length; i++) - { - current = current.children.FirstOrDefault(dir => dir.name == splits[i]); - if (current == null) - { - found = false; - break; - } - } - } - - if (not == !found) - { - orMatch = true; - break; - } - } - if (!orMatch) - return false; - } - - return true; - } } } diff --git a/ModuleManager/NodeMatcher.cs b/ModuleManager/NodeMatcher.cs index cef87865..5c3a59ca 100644 --- a/ModuleManager/NodeMatcher.cs +++ b/ModuleManager/NodeMatcher.cs @@ -16,47 +16,19 @@ public class NodeMatcher : INodeMatcher private string[] namePatterns = null; private string constraints = ""; - public NodeMatcher(string nodeName) + public NodeMatcher(string type, string name, string constraints) { - if (nodeName == null) throw new ArgumentNullException(nameof(nodeName)); - if (nodeName == "") throw new ArgumentException("can't be empty", nameof(nodeName)); - if (!nodeName.IsBracketBalanced()) throw new FormatException("node name is not bracket balanced: " + nodeName); - string name = nodeName; + if (type == string.Empty) throw new ArgumentException("can't be empty", nameof(type)); + this.type = type ?? throw new ArgumentNullException(nameof(type)); - int indexOfHas = name.IndexOf(":HAS[", StringComparison.InvariantCultureIgnoreCase); - - if (indexOfHas == 0) - { - throw new FormatException("node name cannot begin with :HAS : " + nodeName); - } - else if (indexOfHas > 0) - { - int closingBracketIndex = name.LastIndexOf(']', name.Length - 1, name.Length - indexOfHas - 1); - // Really shouldn't happen if we're bracket balanced but just in case - if (closingBracketIndex == -1) throw new FormatException("Malformed :HAS[] block detected: " + nodeName); - - constraints = name.Substring(indexOfHas + 5, closingBracketIndex - indexOfHas - 5); - name = name.Substring(0, indexOfHas); - } + if (name == string.Empty) throw new ArgumentException("can't be empty (null allowed)", nameof(name)); + if (constraints == string.Empty) throw new ArgumentException("can't be empty (null allowed)", nameof(constraints)); - int bracketIndex = name.IndexOf('['); - if (bracketIndex == 0) - { - throw new FormatException("node name cannot begin with a bracket: " + nodeName); - } - else if (bracketIndex > 0) - { - int closingBracketIndex = name.LastIndexOf(']', name.Length - 1, name.Length - bracketIndex - 1); - // Really shouldn't happen if we're bracket balanced but just in case - if (closingBracketIndex == -1) throw new FormatException("Malformed brackets detected: " + nodeName); - string patterns = name.Substring(bracketIndex + 1, closingBracketIndex - bracketIndex - 1); - namePatterns = patterns.Split(',', '|'); - type = name.Substring(0, bracketIndex); - } - else + if (name != null) namePatterns = name.Split(',', '|'); + if (constraints != null) { - type = name; - namePatterns = null; + if (!constraints.IsBracketBalanced()) throw new ArgumentException("is not bracket balanced: " + constraints, nameof(constraints)); + this.constraints = constraints; } } diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index f98b7c49..83440d70 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using ModuleManager.Logging; using ModuleManager.Extensions; @@ -11,31 +12,26 @@ public class PatchApplier { private readonly IBasicLogger logger; private readonly IPatchProgress progress; - - private readonly IPatchList patchList; - - private readonly UrlDir.UrlFile[] allConfigFiles; public string Activity { get; private set; } - public PatchApplier(IPatchList patchList, UrlDir databaseRoot, IPatchProgress progress, IBasicLogger logger) + public PatchApplier(IPatchProgress progress, IBasicLogger logger) { - this.patchList = patchList; - this.progress = progress; - this.logger = logger; - - allConfigFiles = databaseRoot.AllConfigFiles.ToArray(); + this.progress = progress ?? throw new ArgumentNullException(nameof(progress)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public void ApplyPatches() + public void ApplyPatches(IEnumerable configFiles, IEnumerable patches) { - foreach (IPass pass in patchList) + if (configFiles == null) throw new ArgumentNullException(nameof(configFiles)); + if (patches == null) throw new ArgumentNullException(nameof(patches)); + foreach (IPass pass in patches) { - ApplyPatches(pass); + ApplyPatches(configFiles, pass); } } - private void ApplyPatches(IPass pass) + private void ApplyPatches(IEnumerable configFiles, IPass pass) { logger.Info(pass.Name + " pass"); Activity = "ModuleManager " + pass.Name; @@ -44,7 +40,7 @@ private void ApplyPatches(IPass pass) { try { - foreach (UrlDir.UrlFile file in allConfigFiles) + foreach (UrlDir.UrlFile file in configFiles) { patch.Apply(file, progress, logger); } @@ -53,15 +49,7 @@ private void ApplyPatches(IPass pass) catch (Exception e) { progress.Exception(patch.UrlConfig, "Exception while processing node : " + patch.UrlConfig.SafeUrl(), e); - - try - { - logger.Error("Processed node was\n" + patch.UrlConfig.PrettyPrint()); - } - catch (Exception ex2) - { - logger.Exception("Exception while attempting to print a node", ex2); - } + logger.Error("Processed node was\n" + patch.UrlConfig.PrettyPrint()); } } } diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index 06de7182..6c604576 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -1,212 +1,129 @@ using System; -using System.Text.RegularExpressions; using ModuleManager.Extensions; using ModuleManager.Logging; using ModuleManager.Patches; using ModuleManager.Progress; +using ModuleManager.Tags; namespace ModuleManager { public class PatchExtractor { - private static readonly Regex firstRegex = new Regex(@":FIRST", RegexOptions.IgnoreCase); - private static readonly Regex finalRegex = new Regex(@":FINAL", RegexOptions.IgnoreCase); - private static readonly Regex beforeRegex = new Regex(@":BEFORE(?:\[([^\[\]]+)\])?", RegexOptions.IgnoreCase); - private static readonly Regex forRegex = new Regex(@":FOR(?:\[([^\[\]]+)\])?", RegexOptions.IgnoreCase); - private static readonly Regex afterRegex = new Regex(@":AFTER(?:\[([^\[\]]+)\])?", RegexOptions.IgnoreCase); - - private readonly IPatchList patchList; private readonly IPatchProgress progress; private readonly IBasicLogger logger; - - public PatchExtractor(IPatchList patchList, IPatchProgress progress, IBasicLogger logger) + private readonly INeedsChecker needsChecker; + private readonly ITagListParser tagListParser; + private readonly IProtoPatchBuilder protoPatchBuilder; + private readonly IPatchCompiler patchCompiler; + + public PatchExtractor( + IPatchProgress progress, + IBasicLogger logger, + INeedsChecker needsChecker, + ITagListParser tagListParser, + IProtoPatchBuilder protoPatchBuilder, + IPatchCompiler patchCompiler + ) { - this.patchList = patchList ?? throw new ArgumentNullException(nameof(patchList)); this.progress = progress ?? throw new ArgumentNullException(nameof(progress)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.needsChecker = needsChecker ?? throw new ArgumentNullException(nameof(needsChecker)); + this.tagListParser = tagListParser ?? throw new ArgumentNullException(nameof(tagListParser)); + this.protoPatchBuilder = protoPatchBuilder ?? throw new ArgumentNullException(nameof(protoPatchBuilder)); + this.patchCompiler = patchCompiler ?? throw new ArgumentNullException(nameof(patchCompiler)); } - public void ExtractPatch(UrlDir.UrlConfig urlConfig) + public IPatch ExtractPatch(UrlDir.UrlConfig urlConfig) { + if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); + try { + int index = urlConfig.parent.configs.IndexOf(urlConfig); + urlConfig.parent.configs.RemoveAt(index); + if (!urlConfig.type.IsBracketBalanced()) { progress.Error(urlConfig, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\n" + urlConfig.SafeUrl()); - urlConfig.parent.configs.Remove(urlConfig); - return; + return null; } Command command = CommandParser.Parse(urlConfig.type, out string name); - - Match firstMatch = firstRegex.Match(name); - Match finalMatch = finalRegex.Match(name); - Match beforeMatch = beforeRegex.Match(name); - Match forMatch = forRegex.Match(name); - Match afterMatch = afterRegex.Match(name); - - int matchCount = 0; - - if (firstMatch.Success) matchCount++; - if (finalMatch.Success) matchCount++; - if (beforeMatch.Success) matchCount++; - if (forMatch.Success) matchCount++; - if (afterMatch.Success) matchCount++; - - if (firstMatch.NextMatch().Success) matchCount++; - if (finalMatch.NextMatch().Success) matchCount++; - if (beforeMatch.NextMatch().Success) matchCount++; - if (forMatch.NextMatch().Success) matchCount++; - if (afterMatch.NextMatch().Success) matchCount++; - - bool error = false; - - if (command == Command.Insert && matchCount > 0) - { - progress.Error(urlConfig, $"Error - pass specifier detected on an insert node (not a patch): {urlConfig.SafeUrl()}"); - error = true; - } - else if (command == Command.Replace) + + if (command == Command.Replace) { progress.Error(urlConfig, $"Error - replace command (%) is not valid on a root node: {urlConfig.SafeUrl()}"); - error = true; + return null; } else if (command == Command.Create) { progress.Error(urlConfig, $"Error - create command (&) is not valid on a root node: {urlConfig.SafeUrl()}"); - error = true; + return null; } else if (command == Command.Rename) { progress.Error(urlConfig, $"Error - rename command (|) is not valid on a root node: {urlConfig.SafeUrl()}"); - error = true; + return null; } else if (command == Command.Paste) { progress.Error(urlConfig, $"Error - paste command (#) is not valid on a root node: {urlConfig.SafeUrl()}"); - error = true; + return null; } else if (command == Command.Special) { progress.Error(urlConfig, $"Error - special command (*) is not valid on a root node: {urlConfig.SafeUrl()}"); - error = true; + return null; } - if (matchCount > 1) - { - progress.Error(urlConfig, $"Error - more than one pass specifier on a node: {urlConfig.SafeUrl()}"); - error = true; - } - if (beforeMatch.Success && !beforeMatch.Groups[1].Success) - { - progress.Error(urlConfig, "Error - malformed :BEFORE patch specifier detected: " + urlConfig.SafeUrl()); - error = true; - } - if (forMatch.Success && !forMatch.Groups[1].Success) - { - progress.Error(urlConfig, "Error - malformed :FOR patch specifier detected: " + urlConfig.SafeUrl()); - error = true; - } - if (afterMatch.Success && !afterMatch.Groups[1].Success) + ITagList tagList; + try { - progress.Error(urlConfig, "Error - malformed :AFTER patch specifier detected: " + urlConfig.SafeUrl()); - error = true; + tagList = tagListParser.Parse(name); } - if (error) + catch (FormatException ex) { - urlConfig.parent.configs.Remove(urlConfig); - return; + progress.Error(urlConfig, $"Cannot parse node name as tag list: {ex.Message}\non: {urlConfig.SafeUrl()}"); + return null; } - if (command == Command.Insert) return; - - urlConfig.parent.configs.Remove(urlConfig); - - Match theMatch = null; - Action addPatch = null; + ProtoPatch protoPatch = protoPatchBuilder.Build(urlConfig, command, tagList); - if (firstMatch.Success) - { - theMatch = firstMatch; - addPatch = patchList.AddFirstPatch; - } - else if (finalMatch.Success) + if (protoPatch == null) { - theMatch = finalMatch; - addPatch = patchList.AddFinalPatch; + return null; } - else if (beforeMatch.Success) + + if (protoPatch.needs != null && !needsChecker.CheckNeedsExpression(protoPatch.needs)) { - if (CheckMod(beforeMatch, patchList, out string theMod)) - { - theMatch = beforeMatch; - addPatch = p => patchList.AddBeforePatch(theMod, p); - } - else - { - progress.NeedsUnsatisfiedBefore(urlConfig); - return; - } + progress.NeedsUnsatisfiedRoot(urlConfig); + return null; } - else if (forMatch.Success) + else if (!protoPatch.passSpecifier.CheckNeeds(needsChecker, progress)) { - if (CheckMod(forMatch, patchList, out string theMod)) - { - theMatch = forMatch; - addPatch = p => patchList.AddForPatch(theMod, p); - } - else - { - progress.NeedsUnsatisfiedFor(urlConfig); - return; - } + return null; } - else if (afterMatch.Success) + + if (command == Command.Insert) { - if (CheckMod(afterMatch, patchList, out string theMod)) - { - theMatch = afterMatch; - addPatch = p => patchList.AddAfterPatch(theMod, p); - } - else - { - progress.NeedsUnsatisfiedAfter(urlConfig); - return; - } + ConfigNode newNode = urlConfig.config.DeepCopy(); + newNode.name = protoPatch.nodeType; + newNode.id = urlConfig.config.id; + needsChecker.CheckNeedsRecursive(newNode, urlConfig); + urlConfig.parent.configs.Insert(index, new UrlDir.UrlConfig(urlConfig.parent, newNode)); + return null; } else { - addPatch = patchList.AddLegacyPatch; + needsChecker.CheckNeedsRecursive(urlConfig.config, urlConfig); + return patchCompiler.CompilePatch(protoPatch); } - - string newName; - if (theMatch == null) - newName = name; - else - newName = name.Remove(theMatch.Index, theMatch.Length); - - addPatch(PatchCompiler.CompilePatch(urlConfig, command, newName)); - progress.PatchAdded(); } catch(Exception e) { - progress.Exception(urlConfig, $"Exception while parsing pass for config: {urlConfig.SafeUrl()}", e); - - try - { - urlConfig.parent.configs.Remove(urlConfig); - } - catch (Exception ex) - { - logger.Exception("Exception while attempting to clean up bad config", ex); - } + progress.Exception(urlConfig, $"Exception while attempting to create patch from config: {urlConfig.SafeUrl()}", e); + return null; } } - - private static bool CheckMod(Match match, IPatchList patchList, out string theMod) - { - theMod = match.Groups[1].Value.Trim().ToLower(); - return patchList.HasMod(theMod); - } } } diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index e79f9570..6c44d0b3 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -4,21 +4,12 @@ using System.Linq; using ModuleManager.Collections; using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; namespace ModuleManager { - public interface IPatchList : IEnumerable - { - bool HasMod(string mod); - void AddFirstPatch(IPatch patch); - void AddLegacyPatch(IPatch patch); - void AddBeforePatch(string mod, IPatch patch); - void AddForPatch(string mod, IPatch patch); - void AddAfterPatch(string mod, IPatch patch); - void AddFinalPatch(IPatch patch); - } - - public class PatchList : IPatchList + public class PatchList : IEnumerable { private class ModPass { @@ -82,55 +73,54 @@ public ModPassCollection(IEnumerable modList) private readonly ModPassCollection modPasses; - public PatchList(IEnumerable modList) + public PatchList(IEnumerable modList, IEnumerable patches, IPatchProgress progress) { - modPasses = new ModPassCollection(modList); + modPasses = new ModPassCollection(modList ?? throw new ArgumentNullException(nameof(modList))); + if (patches == null) throw new ArgumentNullException(nameof(patches)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + + foreach (IPatch patch in patches) + { + if (patch.PassSpecifier is FirstPassSpecifier) + { + firstPatches.Add(patch); + } + else if (patch.PassSpecifier is LegacyPassSpecifier) + { + legacyPatches.Add(patch); + } + else if (patch.PassSpecifier is BeforePassSpecifier beforePassSpecifier) + { + EnsureMod(beforePassSpecifier.mod); + modPasses[beforePassSpecifier.mod].AddBeforePatch(patch); + } + else if (patch.PassSpecifier is ForPassSpecifier forPassSpecifier) + { + EnsureMod(forPassSpecifier.mod); + modPasses[forPassSpecifier.mod].AddForPatch(patch); + } + else if (patch.PassSpecifier is AfterPassSpecifier afterPassSpecifier) + { + EnsureMod(afterPassSpecifier.mod); + modPasses[afterPassSpecifier.mod].AddAfterPatch(patch); + } + else if (patch.PassSpecifier is FinalPassSpecifier) + { + finalPatches.Add(patch); + } + else + { + throw new NotImplementedException("Don't know what to do with pass specifier: " + patch.PassSpecifier.Descriptor); + } + + progress.PatchAdded(); + } } public ArrayEnumerator GetEnumerator() => new ArrayEnumerator(EnumeratePasses()); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool HasMod(string mod) - { - if (mod == null) throw new ArgumentNullException(nameof(mod)); - if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); - return modPasses.HasMod(mod); - } - - public void AddFirstPatch(IPatch patch) - { - firstPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); - } - - public void AddLegacyPatch(IPatch patch) - { - legacyPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); - } - - public void AddBeforePatch(string mod, IPatch patch) - { - EnsureMod(mod); - modPasses[mod].AddBeforePatch(patch ?? throw new ArgumentNullException(nameof(patch))); - } - - public void AddForPatch(string mod, IPatch patch) - { - EnsureMod(mod); - modPasses[mod].AddForPatch(patch ?? throw new ArgumentNullException(nameof(patch))); - } - - public void AddAfterPatch(string mod, IPatch patch) - { - EnsureMod(mod); - modPasses[mod].AddAfterPatch(patch ?? throw new ArgumentNullException(nameof(patch))); - } - - public void AddFinalPatch(IPatch patch) - { - finalPatches.Add(patch ?? throw new ArgumentNullException(nameof(patch))); - } - private IPass[] EnumeratePasses() { IPass[] result = new IPass[modPasses.Count * 3 + 3]; @@ -154,7 +144,7 @@ private void EnsureMod(string mod) { if (mod == null) throw new ArgumentNullException(nameof(mod)); if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); - if (!HasMod(mod)) throw new KeyNotFoundException($"Mod '{mod}' not found"); + if (!modPasses.HasMod(mod)) throw new KeyNotFoundException($"Mod '{mod}' not found"); } } } diff --git a/ModuleManager/Patches/CopyPatch.cs b/ModuleManager/Patches/CopyPatch.cs index e8fa295f..b94ee353 100644 --- a/ModuleManager/Patches/CopyPatch.cs +++ b/ModuleManager/Patches/CopyPatch.cs @@ -2,6 +2,7 @@ using NodeStack = ModuleManager.Collections.ImmutableStack; using ModuleManager.Extensions; using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManager.Patches @@ -10,11 +11,13 @@ public class CopyPatch : IPatch { public UrlDir.UrlConfig UrlConfig { get; } public INodeMatcher NodeMatcher { get; } + public IPassSpecifier PassSpecifier { get; } - public CopyPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher) + public CopyPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpecifier passSpecifier) { UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); } public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) diff --git a/ModuleManager/Patches/DeletePatch.cs b/ModuleManager/Patches/DeletePatch.cs index 2fbcd879..6d8762f9 100644 --- a/ModuleManager/Patches/DeletePatch.cs +++ b/ModuleManager/Patches/DeletePatch.cs @@ -1,6 +1,7 @@ using System; using ModuleManager.Extensions; using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManager.Patches @@ -9,11 +10,13 @@ public class DeletePatch : IPatch { public UrlDir.UrlConfig UrlConfig { get; } public INodeMatcher NodeMatcher { get; } + public IPassSpecifier PassSpecifier { get; } - public DeletePatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher) + public DeletePatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpecifier passSpecifier) { UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); } public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) diff --git a/ModuleManager/Patches/EditPatch.cs b/ModuleManager/Patches/EditPatch.cs index 4dfcc22f..b9358fdb 100644 --- a/ModuleManager/Patches/EditPatch.cs +++ b/ModuleManager/Patches/EditPatch.cs @@ -2,6 +2,7 @@ using NodeStack = ModuleManager.Collections.ImmutableStack; using ModuleManager.Extensions; using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManager.Patches @@ -12,11 +13,13 @@ public class EditPatch : IPatch public UrlDir.UrlConfig UrlConfig { get; } public INodeMatcher NodeMatcher { get; } + public IPassSpecifier PassSpecifier { get; } - public EditPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher) + public EditPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpecifier passSpecifier) { UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); loop = urlConfig.config.HasNode("MM_PATCH_LOOP"); } diff --git a/ModuleManager/Patches/IPatch.cs b/ModuleManager/Patches/IPatch.cs index 57ae2cfa..57920050 100644 --- a/ModuleManager/Patches/IPatch.cs +++ b/ModuleManager/Patches/IPatch.cs @@ -1,5 +1,6 @@ using System; using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManager.Patches @@ -8,6 +9,7 @@ public interface IPatch { UrlDir.UrlConfig UrlConfig { get; } INodeMatcher NodeMatcher { get; } + IPassSpecifier PassSpecifier { get; } void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger); } } diff --git a/ModuleManager/Patches/PassSpecifiers/AfterPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/AfterPassSpecifier.cs new file mode 100644 index 00000000..a50b99c5 --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/AfterPassSpecifier.cs @@ -0,0 +1,29 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class AfterPassSpecifier : IPassSpecifier + { + public readonly string mod; + public readonly UrlDir.UrlConfig urlConfig; + + public AfterPassSpecifier(string mod, UrlDir.UrlConfig urlConfig) + { + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); + this.urlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + } + + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) + { + if (needsChecker == null) throw new ArgumentNullException(nameof(needsChecker)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + bool result = needsChecker.CheckNeeds(mod); + if (!result) progress.NeedsUnsatisfiedAfter(urlConfig); + return result; + } + + public string Descriptor => $":AFTER[{mod.ToUpper()}]"; + } +} diff --git a/ModuleManager/Patches/PassSpecifiers/BeforePassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/BeforePassSpecifier.cs new file mode 100644 index 00000000..9f509f43 --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/BeforePassSpecifier.cs @@ -0,0 +1,29 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class BeforePassSpecifier : IPassSpecifier + { + public readonly string mod; + public readonly UrlDir.UrlConfig urlConfig; + + public BeforePassSpecifier(string mod, UrlDir.UrlConfig urlConfig) + { + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); + this.urlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + } + + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) + { + if (needsChecker == null) throw new ArgumentNullException(nameof(needsChecker)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + bool result = needsChecker.CheckNeeds(mod); + if (!result) progress.NeedsUnsatisfiedBefore(urlConfig); + return result; + } + + public string Descriptor => $":BEFORE[{mod.ToUpper()}]"; + } +} diff --git a/ModuleManager/Patches/PassSpecifiers/FinalPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/FinalPassSpecifier.cs new file mode 100644 index 00000000..7567f344 --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/FinalPassSpecifier.cs @@ -0,0 +1,17 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class FinalPassSpecifier : IPassSpecifier + { + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) + { + if (needsChecker == null) throw new ArgumentNullException(nameof(needsChecker)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + return true; + } + + public string Descriptor => ":FINAL"; + } +} diff --git a/ModuleManager/Patches/PassSpecifiers/FirstPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/FirstPassSpecifier.cs new file mode 100644 index 00000000..7d32bbbd --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/FirstPassSpecifier.cs @@ -0,0 +1,17 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class FirstPassSpecifier : IPassSpecifier + { + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) + { + if (needsChecker == null) throw new ArgumentNullException(nameof(needsChecker)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + return true; + } + + public string Descriptor => ":FIRST"; + } +} diff --git a/ModuleManager/Patches/PassSpecifiers/ForPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/ForPassSpecifier.cs new file mode 100644 index 00000000..3256a072 --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/ForPassSpecifier.cs @@ -0,0 +1,29 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class ForPassSpecifier : IPassSpecifier + { + public readonly string mod; + public readonly UrlDir.UrlConfig urlConfig; + + public ForPassSpecifier(string mod, UrlDir.UrlConfig urlConfig) + { + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); + this.urlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + } + + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) + { + if (needsChecker == null) throw new ArgumentNullException(nameof(needsChecker)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + bool result = needsChecker.CheckNeeds(mod); + if (!result) progress.NeedsUnsatisfiedFor(urlConfig); + return result; + } + + public string Descriptor => $":FOR[{mod.ToUpper()}]"; + } +} diff --git a/ModuleManager/Patches/PassSpecifiers/IPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/IPassSpecifier.cs new file mode 100644 index 00000000..384e52d5 --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/IPassSpecifier.cs @@ -0,0 +1,11 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public interface IPassSpecifier + { + bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress); + string Descriptor { get; } + } +} diff --git a/ModuleManager/Patches/PassSpecifiers/InsertPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/InsertPassSpecifier.cs new file mode 100644 index 00000000..244f9c5c --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/InsertPassSpecifier.cs @@ -0,0 +1,17 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class InsertPassSpecifier : IPassSpecifier + { + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) + { + if (needsChecker == null) throw new ArgumentNullException(nameof(needsChecker)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + return true; + } + + public string Descriptor => ":INSERT (initial)"; + } +} diff --git a/ModuleManager/Patches/PassSpecifiers/LegacyPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/LegacyPassSpecifier.cs new file mode 100644 index 00000000..ddbee929 --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/LegacyPassSpecifier.cs @@ -0,0 +1,17 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class LegacyPassSpecifier : IPassSpecifier + { + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) + { + if (needsChecker == null) throw new ArgumentNullException(nameof(needsChecker)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + return true; + } + + public string Descriptor => ":LEGACY (default)"; + } +} diff --git a/ModuleManager/Patches/PatchCompiler.cs b/ModuleManager/Patches/PatchCompiler.cs index 3e71a3ce..11ce994b 100644 --- a/ModuleManager/Patches/PatchCompiler.cs +++ b/ModuleManager/Patches/PatchCompiler.cs @@ -2,29 +2,32 @@ namespace ModuleManager.Patches { - public static class PatchCompiler + public interface IPatchCompiler { - public static IPatch CompilePatch(UrlDir.UrlConfig urlConfig, Command command, string name) + IPatch CompilePatch(ProtoPatch protoPatch); + } + + public class PatchCompiler : IPatchCompiler + { + public IPatch CompilePatch(ProtoPatch protoPatch) { - if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); - if (name == null) throw new ArgumentNullException(nameof(name)); - if (name == string.Empty) throw new ArgumentException("can't be empty", nameof(name)); + if (protoPatch == null) throw new ArgumentNullException(nameof(protoPatch)); - INodeMatcher nodeMatcher = new NodeMatcher(name); + INodeMatcher nodeMatcher = new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has); - switch (command) + switch (protoPatch.command) { case Command.Edit: - return new EditPatch(urlConfig, nodeMatcher); + return new EditPatch(protoPatch.urlConfig, nodeMatcher, protoPatch.passSpecifier); case Command.Copy: - return new CopyPatch(urlConfig, nodeMatcher); + return new CopyPatch(protoPatch.urlConfig, nodeMatcher, protoPatch.passSpecifier); case Command.Delete: - return new DeletePatch(urlConfig, nodeMatcher); + return new DeletePatch(protoPatch.urlConfig, nodeMatcher, protoPatch.passSpecifier); default: - throw new ArgumentException("invalid command for a root node: " + command, nameof(command)); + throw new ArgumentException("has an invalid command for a root node: " + protoPatch.command, nameof(protoPatch)); } } } diff --git a/ModuleManager/Patches/ProtoPatch.cs b/ModuleManager/Patches/ProtoPatch.cs new file mode 100644 index 00000000..c1ad8572 --- /dev/null +++ b/ModuleManager/Patches/ProtoPatch.cs @@ -0,0 +1,27 @@ +using System; +using ModuleManager.Patches.PassSpecifiers; + +namespace ModuleManager.Patches +{ + public class ProtoPatch + { + public readonly UrlDir.UrlConfig urlConfig; + public readonly Command command; + public readonly string nodeType; + public readonly string nodeName; + public readonly string needs = null; + public readonly string has = null; + public readonly IPassSpecifier passSpecifier; + + public ProtoPatch(UrlDir.UrlConfig urlConfig, Command command, string nodeType, string nodeName, string needs, string has, IPassSpecifier passSpecifier) + { + this.urlConfig = urlConfig; + this.command = command; + this.nodeType = nodeType; + this.nodeName = nodeName; + this.needs = needs; + this.has = has; + this.passSpecifier = passSpecifier; + } + } +} diff --git a/ModuleManager/Patches/ProtoPatchBuilder.cs b/ModuleManager/Patches/ProtoPatchBuilder.cs new file mode 100644 index 00000000..7979aac3 --- /dev/null +++ b/ModuleManager/Patches/ProtoPatchBuilder.cs @@ -0,0 +1,224 @@ +using System; +using ModuleManager.Extensions; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; +using ModuleManager.Tags; + +namespace ModuleManager.Patches +{ + public interface IProtoPatchBuilder + { + ProtoPatch Build(UrlDir.UrlConfig urlConfig, Command command, ITagList tagList); + } + + public class ProtoPatchBuilder : IProtoPatchBuilder + { + private readonly IPatchProgress progress; + + public ProtoPatchBuilder(IPatchProgress progress) + { + this.progress = progress ?? throw new ArgumentNullException(nameof(progress)); + } + + public ProtoPatch Build(UrlDir.UrlConfig urlConfig, Command command, ITagList tagList) + { + if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); + if (tagList == null) throw new ArgumentNullException(nameof(tagList)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + + bool error = false; + + string nodeType = tagList.PrimaryTag.key; + string nodeName = tagList.PrimaryTag.value; + + if (command == Command.Insert && nodeName != null) + { + progress.Error(urlConfig, "name specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + } + + if (nodeName == string.Empty) + { + progress.Warning(urlConfig, "empty brackets detected on patch name: " + urlConfig.SafeUrl()); + nodeName = null; + } + + if (tagList.PrimaryTag.trailer != null) + progress.Warning(urlConfig, "unrecognized trailer: '" + tagList.PrimaryTag.trailer + "' on: " + urlConfig.SafeUrl()); + + string needs = null; + string has = null; + IPassSpecifier passSpecifier = null; + + foreach (Tag tag in tagList) + { + if (tag.trailer != null) + progress.Warning(urlConfig, "unrecognized trailer: '" + tag.trailer + "' on: " + urlConfig.SafeUrl()); + + if (tag.key.Equals("NEEDS", StringComparison.CurrentCultureIgnoreCase)) + { + if (needs != null) + { + progress.Warning(urlConfig, "more than one :NEEDS tag detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + if (string.IsNullOrEmpty(tag.value)) + { + progress.Error(urlConfig, "empty :NEEDS tag detected: " + urlConfig.SafeUrl()); + error = true; + continue; + } + + needs = tag.value; + } + else if (tag.key.Equals("HAS", StringComparison.CurrentCultureIgnoreCase)) + { + if (command == Command.Insert) + { + progress.Error(urlConfig, ":HAS detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + continue; + } + if (has != null) + { + progress.Warning(urlConfig, "more than one :HAS tag detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + if (string.IsNullOrEmpty(tag.value)) + { + progress.Error(urlConfig, "empty :HAS tag detected: " + urlConfig.SafeUrl()); + error = true; + continue; + } + + has = tag.value; + } + else if (tag.key.Equals("FIRST", StringComparison.CurrentCultureIgnoreCase)) + { + if (tag.value != null) + { + progress.Warning(urlConfig, "value detected on :FIRST tag: " + urlConfig.SafeUrl()); + } + + if (command == Command.Insert) + { + progress.Error(urlConfig, "pass specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + continue; + } + if (passSpecifier != null) + { + progress.Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + + passSpecifier = new FirstPassSpecifier(); + } + else if (tag.key.Equals("BEFORE", StringComparison.CurrentCultureIgnoreCase)) + { + if (string.IsNullOrEmpty(tag.value)) + { + progress.Error(urlConfig, "empty :BEFORE tag detected: " + urlConfig.SafeUrl()); + error = true; + continue; + } + + if (command == Command.Insert) + { + progress.Error(urlConfig, "pass specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + continue; + } + if (passSpecifier != null) + { + progress.Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + + passSpecifier = new BeforePassSpecifier(tag.value, urlConfig); + } + else if (tag.key.Equals("FOR", StringComparison.CurrentCultureIgnoreCase)) + { + if (string.IsNullOrEmpty(tag.value)) + { + progress.Error(urlConfig, "empty :FOR tag detected: " + urlConfig.SafeUrl()); + error = true; + continue; + } + + if (command == Command.Insert) + { + progress.Error(urlConfig, "pass specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + continue; + } + if (passSpecifier != null) + { + progress.Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + + passSpecifier = new ForPassSpecifier(tag.value, urlConfig); + } + else if (tag.key.Equals("AFTER", StringComparison.CurrentCultureIgnoreCase)) + { + if (string.IsNullOrEmpty(tag.value)) + { + progress.Error(urlConfig, "empty :AFTER tag detected: " + urlConfig.SafeUrl()); + error = true; + continue; + } + + if (command == Command.Insert) + { + progress.Error(urlConfig, "pass specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + continue; + } + if (passSpecifier != null) + { + progress.Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + + passSpecifier = new AfterPassSpecifier(tag.value, urlConfig); + } + else if (tag.key.Equals("FINAL", StringComparison.CurrentCultureIgnoreCase)) + { + if (tag.value != null) + { + progress.Warning(urlConfig, "value detected on :FINAL tag: " + urlConfig.SafeUrl()); + } + + if (command == Command.Insert) + { + progress.Error(urlConfig, "pass specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + continue; + } + if (passSpecifier != null) + { + progress.Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + + passSpecifier = new FinalPassSpecifier(); + } + else + { + progress.Warning(urlConfig, "unrecognized tag: '" + tag.key + "' on: " + urlConfig.SafeUrl()); + } + } + + if (error) return null; + + if (passSpecifier == null) + { + if (command == Command.Insert) passSpecifier = new InsertPassSpecifier(); + else passSpecifier = new LegacyPassSpecifier(); + } + + return new ProtoPatch(urlConfig, command, nodeType, nodeName, needs, has, passSpecifier); + } + } +} diff --git a/ModuleManager/Progress/IPatchProgress.cs b/ModuleManager/Progress/IPatchProgress.cs index 3b6f13df..fe03995d 100644 --- a/ModuleManager/Progress/IPatchProgress.cs +++ b/ModuleManager/Progress/IPatchProgress.cs @@ -14,8 +14,8 @@ public interface IPatchProgress void Exception(string message, Exception exception); void Exception(UrlDir.UrlConfig url, string message, Exception exception); void NeedsUnsatisfiedRoot(UrlDir.UrlConfig url); - void NeedsUnsatisfiedNode(UrlDir.UrlConfig url, NodeStack path); - void NeedsUnsatisfiedValue(UrlDir.UrlConfig url, NodeStack path, string valName); + void NeedsUnsatisfiedNode(UrlDir.UrlConfig url, string path); + void NeedsUnsatisfiedValue(UrlDir.UrlConfig url, string path); void NeedsUnsatisfiedBefore(UrlDir.UrlConfig url); void NeedsUnsatisfiedFor(UrlDir.UrlConfig url); void NeedsUnsatisfiedAfter(UrlDir.UrlConfig url); diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index 9f1f6cfa..701fa4ef 100644 --- a/ModuleManager/Progress/PatchProgress.cs +++ b/ModuleManager/Progress/PatchProgress.cs @@ -67,14 +67,14 @@ public void NeedsUnsatisfiedRoot(UrlDir.UrlConfig url) Counter.needsUnsatisfied.Increment(); } - public void NeedsUnsatisfiedNode(UrlDir.UrlConfig url, NodeStack path) + public void NeedsUnsatisfiedNode(UrlDir.UrlConfig url, string path) { - logger.Info($"Deleting node in file {url.parent.url} subnode: {path.GetPath()} as it can't satisfy its NEEDS"); + logger.Info($"Deleting node in file {url.parent.url} subnode: {path} as it can't satisfy its NEEDS"); } - public void NeedsUnsatisfiedValue(UrlDir.UrlConfig url, NodeStack path, string valName) + public void NeedsUnsatisfiedValue(UrlDir.UrlConfig url, string path) { - logger.Info($"Deleting value in file {url.parent.url} subnode: {path.GetPath()} value: {valName} as it can't satisfy its NEEDS"); + logger.Info($"Deleting value in file {url.parent.url} value: {path} as it can't satisfy its NEEDS"); } public void NeedsUnsatisfiedBefore(UrlDir.UrlConfig url) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 7a2d4e8c..6e1a83bf 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -64,10 +64,18 @@ + + + + + + + + diff --git a/ModuleManagerTests/NeedsCheckerTest.cs b/ModuleManagerTests/NeedsCheckerTest.cs index 73c2918b..d0414a0b 100644 --- a/ModuleManagerTests/NeedsCheckerTest.cs +++ b/ModuleManagerTests/NeedsCheckerTest.cs @@ -1,233 +1,217 @@ using System; -using System.Linq; using Xunit; using NSubstitute; using TestUtils; using ModuleManager; using ModuleManager.Logging; using ModuleManager.Progress; -using ModuleManager.Extensions; using NodeStack = ModuleManager.Collections.ImmutableStack; namespace ModuleManagerTests { public class NeedsCheckerTest { - private readonly UrlDir root; private readonly UrlDir gameData; - private readonly UrlDir.UrlFile file; private readonly IPatchProgress progress; private readonly IBasicLogger logger; + private NeedsChecker needsChecker; public NeedsCheckerTest() { - root = UrlBuilder.CreateRoot(); - gameData = UrlBuilder.CreateGameData(root); - file = UrlBuilder.CreateFile("abc/def.cfg", gameData); + gameData = UrlBuilder.CreateGameData(); progress = Substitute.For(); logger = Substitute.For(); + + needsChecker = new NeedsChecker(new[] { "mod1", "mod2", "mod/2" }, gameData, progress, logger); } [Fact] - public void TestCheckNeeds__Root() + public void TestConstructor__ModsNull() { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1]"), file); - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:needs[mod1]"), file); - UrlDir.UrlConfig config4 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod2]:AFTER[mod3]"), file); - - UrlDir.UrlConfig config5 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3]"), file); - UrlDir.UrlConfig config6 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:needs[mod3]"), file); - UrlDir.UrlConfig config7 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3]:FOR[mod2]"), file); - - NeedsChecker.CheckNeeds(root, modList, progress, logger); - - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); + ArgumentNullException ex = Assert.Throws(delegate + { + new NeedsChecker(null, gameData, progress, logger); + }); - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(4, configs.Length); - - Assert.Same(config1, configs[0]); - AssertUrlCorrect("SOME_NODE", config2, configs[1]); - AssertUrlCorrect("SOME_NODE", config3, configs[2]); - AssertUrlCorrect("SOME_NODE:AFTER[mod3]", config4, configs[3]); - - progress.Received().NeedsUnsatisfiedRoot(config5); - progress.Received().NeedsUnsatisfiedRoot(config6); - progress.Received().NeedsUnsatisfiedRoot(config7); + Assert.Equal("mods", ex.ParamName); } [Fact] - public void TestCheckNeeds__Root__AndOr() + public void TestConstructor__GameDataNull() { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig noNeedsNode = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); - - UrlDir.UrlConfig[] needsSatisfiedConfigs = new[] { - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1&mod2]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1,mod2]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1|mod2]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1|mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1&mod2|mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1,mod2|mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1|mod3&mod1]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1|mod,mod1]"), file), - }; - - UrlDir.UrlConfig[] needsUnsatisfiedConfigs = new[] { - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1&mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1,mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1&mod2&mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1,mod2,mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3|mod4]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1|mod2&mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1|mod2,mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3&mod1|mod2]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3,mod1|mod2]"), file), - }; - - NeedsChecker.CheckNeeds(root, modList, progress, logger); + ArgumentNullException ex = Assert.Throws(delegate + { + new NeedsChecker(new string[0], null, progress, logger); + }); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); + Assert.Equal("gameData", ex.ParamName); + } - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(needsSatisfiedConfigs.Length + 1, configs.Length); + [Fact] + public void TestConstructor__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new NeedsChecker(new string[0], gameData, null, logger); + }); - Assert.Same(noNeedsNode, configs[0]); + Assert.Equal("progress", ex.ParamName); + } - for (int i = 0; i < needsSatisfiedConfigs.Length; i++) + [Fact] + public void TestConstructor__LoggerNull() + { + ArgumentNullException ex = Assert.Throws(delegate { - AssertUrlCorrect("SOME_NODE", needsSatisfiedConfigs[i], configs[i + 1]); - } + new NeedsChecker(new string[0], gameData, progress, null); + }); - foreach (UrlDir.UrlConfig config in needsUnsatisfiedConfigs) - { - progress.Received().NeedsUnsatisfiedRoot(config); - } + Assert.Equal("logger", ex.ParamName); } [Fact] - public void TestCheckNeeds__Root__Not() + public void TestCheckNeedsExpression() { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig noNeedsNode = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); + Assert.True(needsChecker.CheckNeedsExpression("mod1")); + Assert.True(needsChecker.CheckNeedsExpression("mod2")); + Assert.False(needsChecker.CheckNeedsExpression("mod3")); + } - UrlDir.UrlConfig[] needsSatisfiedConfigs = new[] { - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[!mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1,!mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[!mod1|!mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1|!mod2]"), file), - }; + [Fact] + public void TestCheckNeedsExpression__AndOr() + { + Assert.True(needsChecker.CheckNeedsExpression("mod1&mod2")); + Assert.True(needsChecker.CheckNeedsExpression("mod1,mod2")); + Assert.True(needsChecker.CheckNeedsExpression("mod1|mod2")); + Assert.True(needsChecker.CheckNeedsExpression("mod1|mod3")); + Assert.True(needsChecker.CheckNeedsExpression("mod1&mod2|mod3")); + Assert.True(needsChecker.CheckNeedsExpression("mod1,mod2|mod3")); + Assert.True(needsChecker.CheckNeedsExpression("mod1|mod3&mod2")); + Assert.True(needsChecker.CheckNeedsExpression("mod1|mod3,mod2")); + + Assert.False(needsChecker.CheckNeedsExpression("mod1&mod3")); + Assert.False(needsChecker.CheckNeedsExpression("mod1,mod3")); + Assert.False(needsChecker.CheckNeedsExpression("mod1&mod2&mod3")); + Assert.False(needsChecker.CheckNeedsExpression("mod1,mod2,mod3")); + Assert.False(needsChecker.CheckNeedsExpression("mod3|mod4")); + Assert.False(needsChecker.CheckNeedsExpression("mod1|mod2&mod3")); + Assert.False(needsChecker.CheckNeedsExpression("mod1|mod2,mod3")); + Assert.False(needsChecker.CheckNeedsExpression("mod3&mod1|mod2")); + Assert.False(needsChecker.CheckNeedsExpression("mod3,mod1|mod2")); + } - UrlDir.UrlConfig[] needsUnsatisfiedConfigs = new[] { - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[!mod1]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[!mod1,mod2]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[!mod1&!mod3]"), file), - }; + [Fact] + public void TestCheckNeedsExpression__Not() + { + Assert.True(needsChecker.CheckNeedsExpression("!mod3")); + Assert.True(needsChecker.CheckNeedsExpression("mod1,!mod3")); + Assert.True(needsChecker.CheckNeedsExpression("!mod1|!mod3")); + Assert.True(needsChecker.CheckNeedsExpression("mod1|!mod2")); + + Assert.False(needsChecker.CheckNeedsExpression("!mod1")); + Assert.False(needsChecker.CheckNeedsExpression("!mod1,mod2")); + Assert.False(needsChecker.CheckNeedsExpression("!mod1&!mod3")); + } - NeedsChecker.CheckNeeds(root, modList, progress, logger); + [Fact] + public void TestCheckNeedsExpression__Capitalization() + { + Assert.True(needsChecker.CheckNeedsExpression("mod1")); + Assert.True(needsChecker.CheckNeedsExpression("Mod1")); + Assert.True(needsChecker.CheckNeedsExpression("MOD1")); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); + Assert.False(needsChecker.CheckNeedsExpression("mod3")); + Assert.False(needsChecker.CheckNeedsExpression("Mod3")); + Assert.False(needsChecker.CheckNeedsExpression("MOD3")); + } - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(needsSatisfiedConfigs.Length + 1, configs.Length); + [Fact] + public void TestCheckNeedsExpression__Directory() + { + UrlBuilder.CreateDir("abc", gameData); + UrlBuilder.CreateDir("ghi/jkl", gameData); - Assert.Same(noNeedsNode, configs[0]); + Assert.True(needsChecker.CheckNeedsExpression("/abc")); + Assert.True(needsChecker.CheckNeedsExpression("abc/")); + Assert.True(needsChecker.CheckNeedsExpression("/abc/")); + Assert.True(needsChecker.CheckNeedsExpression("ghi/jkl")); + Assert.True(needsChecker.CheckNeedsExpression("/ghi/jkl")); + Assert.True(needsChecker.CheckNeedsExpression("ghi/jkl/")); + Assert.True(needsChecker.CheckNeedsExpression("mod1&ghi/jkl")); + Assert.True(needsChecker.CheckNeedsExpression("mod3|ghi/jkl")); + Assert.True(needsChecker.CheckNeedsExpression("abc/&ghi/jkl")); + Assert.True(needsChecker.CheckNeedsExpression("mod/2")); + + Assert.False(needsChecker.CheckNeedsExpression("abc")); + Assert.False(needsChecker.CheckNeedsExpression("mod3&ghi/jkl")); + Assert.False(needsChecker.CheckNeedsExpression("Ghi/jkl")); + Assert.False(needsChecker.CheckNeedsExpression("mno/pqr")); + } - for (int i = 0; i < needsSatisfiedConfigs.Length; i++) + [Fact] + public void TestCheckNeedsExpression__Null() + { + ArgumentNullException ex = Assert.Throws(delegate { - AssertUrlCorrect("SOME_NODE", needsSatisfiedConfigs[i], configs[i + 1]); - } + needsChecker.CheckNeedsExpression(null); + }); - foreach (UrlDir.UrlConfig config in needsUnsatisfiedConfigs) - { - progress.Received().NeedsUnsatisfiedRoot(config); - } + Assert.Equal("needsExpression", ex.ParamName); } [Fact] - public void TestCheckNeeds__Root__CaseInsensitive() + public void TestCheckNeedsExpression__Empty() { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig noNeedsNode = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); - - UrlDir.UrlConfig[] needsSatisfiedConfigs = new[] { - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod1]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[Mod1]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[MOD1]"), file), - }; - - UrlDir.UrlConfig[] needsUnsatisfiedConfigs = new[] { - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[Mod3]"), file), - UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[MOD3]"), file), - }; + ArgumentException ex = Assert.Throws(delegate + { + needsChecker.CheckNeedsExpression(""); + }); - NeedsChecker.CheckNeeds(root, modList, progress, logger); + Assert.Equal("needsExpression", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); + } - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); + [Fact] + public void TestCheckNeeds() + { + UrlBuilder.CreateDir("ghi/jkl", gameData); - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(needsSatisfiedConfigs.Length + 1, configs.Length); + Assert.True(needsChecker.CheckNeeds("mod1")); + Assert.True(needsChecker.CheckNeeds("MOD1")); + Assert.True(needsChecker.CheckNeeds("mod2")); - Assert.Same(noNeedsNode, configs[0]); + Assert.False(needsChecker.CheckNeeds("mod1&mod2")); + Assert.False(needsChecker.CheckNeeds("ghi/jkl")); + } - for (int i = 0; i < needsSatisfiedConfigs.Length; i++) + [Fact] + public void TestCheckNeeds__Null() + { + ArgumentNullException ex = Assert.Throws(delegate { - AssertUrlCorrect("SOME_NODE", needsSatisfiedConfigs[i], configs[i + 1]); - } + needsChecker.CheckNeeds(null); + }); - foreach (UrlDir.UrlConfig config in needsUnsatisfiedConfigs) - { - progress.Received().NeedsUnsatisfiedRoot(config); - } + Assert.Equal("mod", ex.ParamName); } [Fact] - public void TestCheckNeeds__Root__KeepsOrder() + public void TestCheckNeeds__Empty() { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new ConfigNode("NODE_1"), file); - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new ConfigNode("NODE_2:NEEDS[mod1]"), file); - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new ConfigNode("NODE_3:NEEDS[mod2]"), file); - UrlDir.UrlConfig config4 = UrlBuilder.CreateConfig(new ConfigNode("NODE_4"), file); - - NeedsChecker.CheckNeeds(root, modList, progress, logger); - - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(4, configs.Length); + ArgumentException ex = Assert.Throws(delegate + { + needsChecker.CheckNeeds(""); + }); - Assert.Same(config1, configs[0]); - AssertUrlCorrect("NODE_2", config2, configs[1]); - AssertUrlCorrect("NODE_3", config3, configs[2]); - Assert.Same(config4, configs[3]); + Assert.Equal("mod", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); } [Fact] - public void TestCheckNeeds__Nested() + public void TestCheckNeedsRecursive() { - string[] modList = { "mod1", "mod2" }; - ConfigNode node = new TestConfigNode("SOME_NODE") { { "aa", "00" }, @@ -284,324 +268,121 @@ public void TestCheckNeeds__Nested() }, }; - UrlDir.UrlConfig origUrl = UrlBuilder.CreateConfig(node, file); + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", node); - NeedsChecker.CheckNeeds(root, modList, progress, logger); + needsChecker.CheckNeedsRecursive(node, urlConfig); + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.DidNotReceiveWithAnyArgs().Error(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(1, configs.Length); - - UrlDir.UrlConfig url = configs[0]; - Assert.Equal("SOME_NODE", url.type); - ConfigNode newNode = url.config; - Assert.Equal("SOME_NODE", newNode.name); - - Assert.Equal(2, newNode.values.Count); - Assert.Equal(3, newNode.nodes.Count); + Received.InOrder(delegate + { + progress.NeedsUnsatisfiedValue(urlConfig, "SOME_NODE/cc:NEEDS[mod3]"); + progress.NeedsUnsatisfiedValue(urlConfig, "SOME_NODE/INNER_NODE_2/hh:NEEDS[mod3]"); + progress.NeedsUnsatisfiedNode(urlConfig, "SOME_NODE/INNER_NODE_2/INNER_INNER_NODE_12:NEEDS[mod3]"); + progress.NeedsUnsatisfiedValue(urlConfig, "SOME_NODE/INNER_NODE_3/nn:NEEDS[mod3]"); + progress.NeedsUnsatisfiedNode(urlConfig, "SOME_NODE/INNER_NODE_3/INNER_INNER_NODE_22:NEEDS[mod3]"); + progress.NeedsUnsatisfiedNode(urlConfig, "SOME_NODE/INNER_NODE_4:NEEDS[mod3]"); + }); + + Assert.Equal(2, node.values.Count); + Assert.Equal(3, node.nodes.Count); - Assert.Equal("aa", newNode.values[0].name); - Assert.Equal("00", newNode.values[0].value); + Assert.Equal("aa", node.values[0].name); + Assert.Equal("00", node.values[0].value); - Assert.Equal("bb", newNode.values[1].name); - Assert.Equal("01", newNode.values[1].value); + Assert.Equal("bb", node.values[1].name); + Assert.Equal("01", node.values[1].value); - Assert.Same(node.nodes[0], newNode.nodes[0]); - Assert.Equal("INNER_NODE_1", newNode.nodes[0].name); + Assert.Same(node.nodes[0], node.nodes[0]); + Assert.Equal("INNER_NODE_1", node.nodes[0].name); - Assert.Equal(2, newNode.nodes[0].values.Count); - Assert.Equal(1, newNode.nodes[0].nodes.Count); + Assert.Equal(2, node.nodes[0].values.Count); + Assert.Equal(1, node.nodes[0].nodes.Count); - Assert.Equal("dd", newNode.nodes[0].values[0].name); - Assert.Equal("03", newNode.nodes[0].values[0].value); + Assert.Equal("dd", node.nodes[0].values[0].name); + Assert.Equal("03", node.nodes[0].values[0].value); - Assert.Equal("ee", newNode.nodes[0].values[1].name); - Assert.Equal("04", newNode.nodes[0].values[1].value); + Assert.Equal("ee", node.nodes[0].values[1].name); + Assert.Equal("04", node.nodes[0].values[1].value); - Assert.Equal("INNER_INNER_NODE_1", newNode.nodes[0].nodes[0].name); + Assert.Equal("INNER_INNER_NODE_1", node.nodes[0].nodes[0].name); - Assert.Equal(1, newNode.nodes[0].nodes[0].values.Count); - Assert.Equal(0, newNode.nodes[0].nodes[0].nodes.Count); + Assert.Equal(1, node.nodes[0].nodes[0].values.Count); + Assert.Equal(0, node.nodes[0].nodes[0].nodes.Count); - Assert.Equal("ff", newNode.nodes[0].nodes[0].values[0].name); - Assert.Equal("05", newNode.nodes[0].nodes[0].values[0].value); + Assert.Equal("ff", node.nodes[0].nodes[0].values[0].name); + Assert.Equal("05", node.nodes[0].nodes[0].values[0].value); // Assert.NotSame(node.nodes[1], newNode.nodes[1]); - Assert.Equal("INNER_NODE_2", newNode.nodes[1].name); + Assert.Equal("INNER_NODE_2", node.nodes[1].name); - Assert.Equal(2, newNode.nodes[1].values.Count); - Assert.Equal(2, newNode.nodes[1].nodes.Count); + Assert.Equal(2, node.nodes[1].values.Count); + Assert.Equal(2, node.nodes[1].nodes.Count); - Assert.Equal("gg", newNode.nodes[1].values[0].name); - Assert.Equal("06", newNode.nodes[1].values[0].value); + Assert.Equal("gg", node.nodes[1].values[0].name); + Assert.Equal("06", node.nodes[1].values[0].value); - Assert.Equal("ii", newNode.nodes[1].values[1].name); - Assert.Equal("08", newNode.nodes[1].values[1].value); + Assert.Equal("ii", node.nodes[1].values[1].name); + Assert.Equal("08", node.nodes[1].values[1].value); - Assert.Equal("INNER_INNER_NODE_11", newNode.nodes[1].nodes[0].name); + Assert.Equal("INNER_INNER_NODE_11", node.nodes[1].nodes[0].name); - Assert.Equal("jj", newNode.nodes[1].nodes[0].values[0].name); - Assert.Equal("09", newNode.nodes[1].nodes[0].values[0].value); + Assert.Equal("jj", node.nodes[1].nodes[0].values[0].name); + Assert.Equal("09", node.nodes[1].nodes[0].values[0].value); - Assert.Equal("INNER_INNER_NODE_12", newNode.nodes[1].nodes[1].name); + Assert.Equal("INNER_INNER_NODE_12", node.nodes[1].nodes[1].name); - Assert.Equal("kk", newNode.nodes[1].nodes[1].values[0].name); - Assert.Equal("10", newNode.nodes[1].nodes[1].values[0].value); + Assert.Equal("kk", node.nodes[1].nodes[1].values[0].name); + Assert.Equal("10", node.nodes[1].nodes[1].values[0].value); // Assert.NotSame(node.nodes[1], newNode.nodes[1]); - Assert.Equal("INNER_NODE_3", newNode.nodes[2].name); + Assert.Equal("INNER_NODE_3", node.nodes[2].name); - Assert.Equal(2, newNode.nodes[2].values.Count); - Assert.Equal(2, newNode.nodes[2].nodes.Count); + Assert.Equal(2, node.nodes[2].values.Count); + Assert.Equal(2, node.nodes[2].nodes.Count); - Assert.Equal("mm", newNode.nodes[2].values[0].name); - Assert.Equal("12", newNode.nodes[2].values[0].value); + Assert.Equal("mm", node.nodes[2].values[0].name); + Assert.Equal("12", node.nodes[2].values[0].value); - Assert.Equal("oo", newNode.nodes[2].values[1].name); - Assert.Equal("14", newNode.nodes[2].values[1].value); + Assert.Equal("oo", node.nodes[2].values[1].name); + Assert.Equal("14", node.nodes[2].values[1].value); - Assert.Equal("INNER_INNER_NODE_21", newNode.nodes[2].nodes[0].name); + Assert.Equal("INNER_INNER_NODE_21", node.nodes[2].nodes[0].name); - Assert.Equal("pp", newNode.nodes[2].nodes[0].values[0].name); - Assert.Equal("15", newNode.nodes[2].nodes[0].values[0].value); + Assert.Equal("pp", node.nodes[2].nodes[0].values[0].name); + Assert.Equal("15", node.nodes[2].nodes[0].values[0].value); - Assert.Equal("INNER_INNER_NODE_22", newNode.nodes[2].nodes[1].name); + Assert.Equal("INNER_INNER_NODE_22", node.nodes[2].nodes[1].name); - Assert.Equal("qq", newNode.nodes[2].nodes[1].values[0].name); - Assert.Equal("16", newNode.nodes[2].nodes[1].values[0].value); + Assert.Equal("qq", node.nodes[2].nodes[1].values[0].name); + Assert.Equal("16", node.nodes[2].nodes[1].values[0].value); } - + [Fact] - public void TestCheckNeeds__RootAndNested() + public void TestCheckNeedsRecursive__NodeNull() { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("SOME_NODE:NEEDS[mod1]") + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + ArgumentNullException ex = Assert.Throws(delegate { - { "aa:NEEDS[mod2]", "00" }, - { "bb:NEEDS[mod3]", "01" }, - new TestConfigNode("INNER_NODE_1:NEEDS[mod2]") - { - { "cc", "02" }, - }, - new TestConfigNode("INNER_NODE_2:NEEDS[mod3]") - { - { "dd", "03" }, - }, - }, file); - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new ConfigNode("SOME_OTHER_NODE:NEEDS[mod3]"), file); - - NeedsChecker.CheckNeeds(root, modList, progress, logger); - - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(1, configs.Length); - - UrlDir.UrlConfig url = configs[0]; - Assert.Equal("SOME_NODE", url.type); - ConfigNode newNode = url.config; - Assert.Equal("SOME_NODE", newNode.name); - - Assert.Equal(1, newNode.values.Count); - Assert.Equal(1, newNode.nodes.Count); - - Assert.Equal("aa", newNode.values[0].name); - Assert.Equal("00", newNode.values[0].value); - - Assert.Equal("INNER_NODE_1", newNode.nodes[0].name); - - Assert.Equal("cc", newNode.nodes[0].values[0].name); - Assert.Equal("02", newNode.nodes[0].values[0].value); - - progress.Received().NeedsUnsatisfiedRoot(config2); - progress.Received().NeedsUnsatisfiedValue(url, Arg.Is(stack => stack.GetPath() == "SOME_NODE"), "bb:NEEDS[mod3]"); - progress.Received().NeedsUnsatisfiedNode(url, Arg.Is(stack => stack.GetPath() == "SOME_NODE/INNER_NODE_2:NEEDS[mod3]")); - } - - [Fact] - public void TestCheckNeeds__Exception() - { - string[] modList = { "mod1", "mod2" }; + needsChecker.CheckNeedsRecursive(null, urlConfig); + }); - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3]"), file); - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); - - Exception e = new Exception(); - progress.When(p => p.NeedsUnsatisfiedRoot(config2)).Throw(e); - - NeedsChecker.CheckNeeds(root, modList, progress, logger); - - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - - string expected = @" -Exception while checking needs on root node : -abc/def/SOME_NODE:NEEDS[mod3] - SOME_NODE:NEEDS[mod3] - { - } -".Replace("\r", null).TrimStart(); - - progress.Received().Exception(config2, expected, e); - - Assert.Equal(new[] { config1, config3 }, root.AllConfigs); + Assert.Equal("node", ex.ParamName); } [Fact] - public void TestCheckNeeds__Directory() + public void TestCheckNeedsRecursive__UrlConfigNull() { - string[] modList = { "mod1", "mod/2" }; - - UrlBuilder.CreateDir("ghi/jkl", gameData); - - UrlDir.UrlConfig config01 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE01:NEEDS[/abc]"), file); - UrlDir.UrlConfig config02 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE02:NEEDS[abc/]"), file); - UrlDir.UrlConfig config03 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE03:NEEDS[/abc/]"), file); - UrlDir.UrlConfig config04 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE04:NEEDS[ghi/jkl]"), file); - UrlDir.UrlConfig config05 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE05:NEEDS[/ghi/jkl]"), file); - UrlDir.UrlConfig config06 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE06:NEEDS[ghi/jkl/]"), file); - UrlDir.UrlConfig config07 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE07:NEEDS[mod1&ghi/jkl]"), file); - UrlDir.UrlConfig config08 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE08:NEEDS[mod3|ghi/jkl]"), file); - UrlDir.UrlConfig config09 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE09:NEEDS[abc/&ghi/jkl]"), file); - UrlDir.UrlConfig config10 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE10:NEEDS[mod/2]"), file); - - UrlDir.UrlConfig config11 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE11:NEEDS[abc]"), file); - UrlDir.UrlConfig config12 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE12:needs[mod3&ghi/jkl]"), file); - UrlDir.UrlConfig config13 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE13:NEEDS[Ghi/jkl]"), file); - UrlDir.UrlConfig config14 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE14:NEEDS[mno/pqr]"), file); - - NeedsChecker.CheckNeeds(root, modList, progress, logger); - - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - - UrlDir.UrlConfig[] configs = root.AllConfigs.ToArray(); - Assert.Equal(10, configs.Length); - - AssertUrlCorrect("SOME_NODE01", config01, configs[0]); - AssertUrlCorrect("SOME_NODE02", config02, configs[1]); - AssertUrlCorrect("SOME_NODE03", config03, configs[2]); - AssertUrlCorrect("SOME_NODE04", config04, configs[3]); - AssertUrlCorrect("SOME_NODE05", config05, configs[4]); - AssertUrlCorrect("SOME_NODE06", config06, configs[5]); - AssertUrlCorrect("SOME_NODE07", config07, configs[6]); - AssertUrlCorrect("SOME_NODE08", config08, configs[7]); - AssertUrlCorrect("SOME_NODE09", config09, configs[8]); - AssertUrlCorrect("SOME_NODE09", config09, configs[8]); - AssertUrlCorrect("SOME_NODE10", config10, configs[9]); - - progress.Received().NeedsUnsatisfiedRoot(config11); - progress.Received().NeedsUnsatisfiedRoot(config12); - progress.Received().NeedsUnsatisfiedRoot(config13); - progress.Received().NeedsUnsatisfiedRoot(config14); - } - - [Fact] - public void TestCheckNeeds__ExceptionWhileLoggingException() - { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE:NEEDS[mod3]"), file); - UrlDir.UrlConfig config3 = UrlBuilder.CreateConfig(new ConfigNode("SOME_NODE"), file); - - Exception e1 = new Exception(); - Exception e2 = new Exception(); - progress.When(p => p.NeedsUnsatisfiedRoot(config2)).Throw(e1); - progress.WhenForAnyArgs(p => p.Exception(null, null, null)).Throw(e2); - - NeedsChecker.CheckNeeds(root, modList, progress, logger); - - progress.ReceivedWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - - progress.Received().Exception("Exception while attempting to log an exception", e2); - - Assert.Equal(new[] { config1, config3 }, root.AllConfigs); - } - - [Fact] - public void TestCheckNeeds__AllNeedsSatisfied() - { - string[] modList = { "mod1", "mod2" }; - - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("SOME_NODE") + ArgumentNullException ex = Assert.Throws(delegate { - { "value", "1" }, - }, file); - UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig(new TestConfigNode("@SOME_NODE") - { - { "@value", "2" }, - { "@value:NEEDS[mod1] +", "4" }, - }, file); - - NeedsChecker.CheckNeeds(root, modList, progress, logger); + needsChecker.CheckNeedsRecursive(new ConfigNode(), null); + }); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - - ConfigNode node = root.AllConfigs.ToArray().Last().config; - Assert.Equal("@SOME_NODE", node.name); - Assert.Equal("@value", node.values[0].name); - Assert.Equal("2", node.values[0].value); - Assert.Equal("@value +", node.values[1].name); - Assert.Equal("4", node.values[1].value); - - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedValue(null, null, null); - - } - - private UrlDir.UrlConfig CreateConfig(string name) - { - ConfigNode node = new TestConfigNode(name) - { - { "name", "test" }, - { "foo", "bar" }, - new ConfigNode("INNER_NODE"), - }; - - node.id = "who_uses_this"; - - return UrlBuilder.CreateConfig(node, file); - } - - private void AssertUrlCorrect(string expectedNodeName, UrlDir.UrlConfig originalUrl, UrlDir.UrlConfig observedUrl) - { - // Assert.NotSame(originalUrl, observedUrl); - Assert.Equal(expectedNodeName, observedUrl.type); - - ConfigNode originalNode = originalUrl.config; - ConfigNode observedNode = observedUrl.config; - - Assert.Equal(expectedNodeName, observedNode.name); - - if (originalNode.HasValue("name")) Assert.Equal(originalNode.GetValue("name"), observedUrl.name); - - Assert.Same(originalUrl.parent, observedUrl.parent); - - Assert.Equal(originalNode.id, observedNode.id); - Assert.Equal(originalNode.values.Count, observedNode.values.Count); - Assert.Equal(originalNode.nodes.Count, observedNode.nodes.Count); - - for (int i = 0; i < originalNode.values.Count; i++) - { - Assert.Same(originalNode.values[i], observedNode.values[i]); - } - - for (int i = 0; i < originalNode.nodes.Count; i++) - { - Assert.Same(originalNode.nodes[i], observedNode.nodes[i]); - } + Assert.Equal("urlConfig", ex.ParamName); } } } diff --git a/ModuleManagerTests/NodeMatcherTest.cs b/ModuleManagerTests/NodeMatcherTest.cs index 036770cb..866c69fc 100644 --- a/ModuleManagerTests/NodeMatcherTest.cs +++ b/ModuleManagerTests/NodeMatcherTest.cs @@ -10,59 +10,62 @@ public class NodeMatcherTest #region Constructor [Fact] - public void TestConstructor__Null() + public void TestConstructor__TypeNull() { ArgumentNullException ex = Assert.Throws(delegate { - new NodeMatcher(null); + new NodeMatcher(null, null, null); }); - Assert.Equal("nodeName", ex.ParamName); + Assert.Equal("type", ex.ParamName); } [Fact] - public void TestConstructor__Blank() + public void TestConstructor__TypeBlank() { ArgumentException ex = Assert.Throws(delegate { - new NodeMatcher(""); + new NodeMatcher("", null, null); }); - Assert.Equal("nodeName", ex.ParamName); + Assert.Equal("type", ex.ParamName); Assert.Contains("can't be empty", ex.Message); } [Fact] - public void TestConstructor__NotBracketBalanced() + public void TestConstructor__NameBlank() { - FormatException ex = Assert.Throws(delegate + ArgumentException ex = Assert.Throws(delegate { - new NodeMatcher("NODE[stuff"); + new NodeMatcher("NODE", "", null); }); - Assert.Equal("node name is not bracket balanced: NODE[stuff", ex.Message); + Assert.Equal("name", ex.ParamName); + Assert.Contains("can't be empty (null allowed)", ex.Message); } [Fact] - public void TestConstructor__StartsWithHas() + public void TestConstructor__ConstraintsBlank() { - FormatException ex = Assert.Throws(delegate + ArgumentException ex = Assert.Throws(delegate { - new NodeMatcher(":HAS[#blah]"); + new NodeMatcher("NODE", null, ""); }); - Assert.Equal("node name cannot begin with :HAS : :HAS[#blah]", ex.Message); + Assert.Equal("constraints", ex.ParamName); + Assert.Contains("can't be empty (null allowed)", ex.Message); } [Fact] - public void TestConstructor__StartsWithBracket() + public void TestConstructor__ConstraintsNotBracketBalanced() { - FormatException ex = Assert.Throws(delegate + ArgumentException ex = Assert.Throws(delegate { - new NodeMatcher("[#blah]"); + new NodeMatcher("NODE", null, "stuff[blah"); }); - Assert.Equal("node name cannot begin with a bracket: [#blah]", ex.Message); + Assert.Equal("constraints", ex.ParamName); + Assert.Contains("is not bracket balanced: stuff[blah", ex.Message); } #endregion @@ -72,7 +75,7 @@ public void TestConstructor__StartsWithBracket() [Fact] public void TestIsMatch() { - NodeMatcher matcher = new NodeMatcher("NODE"); + NodeMatcher matcher = new NodeMatcher("NODE", null, null); Assert.True(matcher.IsMatch(new ConfigNode("NODE"))); Assert.False(matcher.IsMatch(new ConfigNode("PART"))); @@ -81,7 +84,7 @@ public void TestIsMatch() [Fact] public void TestIsMatch__Name() { - NodeMatcher matcher = new NodeMatcher("NODE[blah]"); + NodeMatcher matcher = new NodeMatcher("NODE", "blah", null); Assert.True(matcher.IsMatch(new TestConfigNode("NODE") { @@ -111,7 +114,7 @@ public void TestIsMatch__Name() [Fact] public void TestIsMatch__Name__Wildcard() { - NodeMatcher matcher = new NodeMatcher("NODE[bl*h]"); + NodeMatcher matcher = new NodeMatcher("NODE", "bl*h", null); Assert.True(matcher.IsMatch(new TestConfigNode("NODE") { @@ -151,7 +154,7 @@ public void TestIsMatch__Name__Wildcard() [Fact] public void TestIsMatch__Name__Multiple() { - NodeMatcher matcher = new NodeMatcher("NODE[blah|bleh|blih*]"); + NodeMatcher matcher = new NodeMatcher("NODE", "blah|bleh|blih*", null); Assert.True(matcher.IsMatch(new TestConfigNode("NODE") { @@ -206,7 +209,7 @@ public void TestIsMatch__Name__Multiple() [Fact] public void TestIsMatch__Constraints() { - NodeMatcher matcher = new NodeMatcher("NODE[blah]:HAS[@FOO[bar*],#something[else]]"); + NodeMatcher matcher = new NodeMatcher("NODE", "blah", "@FOO[bar*],#something[else]"); Assert.True(matcher.IsMatch(new TestConfigNode("NODE") { @@ -262,7 +265,7 @@ public void TestIsMatch__Constraints() [Fact] public void TestIsMatch__Constraints_Open() { - NodeMatcher matcher = new NodeMatcher("NODE[blah]:HAS[@FOO,#something]"); + NodeMatcher matcher = new NodeMatcher("NODE", "blah", "@FOO,#something"); Assert.True(matcher.IsMatch(new TestConfigNode("NODE") { diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index 8d07bd3c..fec4e148 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -13,58 +13,81 @@ namespace ModuleManagerTests { public class PatchApplierTest { - private readonly IBasicLogger logger; - private readonly IPatchProgress progress; - private readonly string[] modList = new[] { "mod1", "mod2" }; - private UrlDir databaseRoot; - private UrlDir.UrlFile file; - private readonly IPass pass1; - private readonly IPass pass2; - private readonly IPass pass3; - private readonly IPatchList patchList; - private readonly PatchApplier patchApplier; - - public PatchApplierTest() + [Fact] + public void TestConstructor__ProgressNull() { - logger = Substitute.For(); - progress = Substitute.For(); - databaseRoot = UrlBuilder.CreateRoot(); - file = UrlBuilder.CreateFile("abc/def.cfg", databaseRoot); - pass1 = Substitute.For(); - pass2 = Substitute.For(); - pass3 = Substitute.For(); - pass1.Name.Returns(":PASS1"); - pass2.Name.Returns(":PASS2"); - pass3.Name.Returns(":PASS3"); - patchList = Substitute.For(); - patchList.GetEnumerator().Returns(new ArrayEnumerator(pass1, pass2, pass3)); - patchApplier = new PatchApplier(patchList, databaseRoot, progress, logger); + ArgumentNullException ex = Assert.Throws(delegate + { + new PatchApplier(null, Substitute.For()); + }); + + Assert.Equal("progress", ex.ParamName); } [Fact] - public void TestApplyPatches() + public void TestConstructor__LoggerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new PatchApplier(Substitute.For(), null); + }); + + Assert.Equal("logger", ex.ParamName); + } + + [Fact] + public void TestApplyPatches__UrlFilesNull() + { + PatchApplier applier = new PatchApplier(Substitute.For(), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + applier.ApplyPatches(null, new IPass[0]); + }); + + Assert.Equal("configFiles", ex.ParamName); + } + + [Fact] + public void TestApplyPatches__PatchesNull() { - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("PART") + PatchApplier applier = new PatchApplier(Substitute.For(), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate { - { "name", "000" }, - { "aaa", "001" }, - }, file); + applier.ApplyPatches(new UrlDir.UrlFile[0], null); + }); + + Assert.Equal("patches", ex.ParamName); + } + + [Fact] + public void TestApplyPatches() + { + IBasicLogger logger = Substitute.For(); + IPatchProgress progress = Substitute.For(); + PatchApplier patchApplier = new PatchApplier(progress, logger); + UrlDir.UrlFile file1 = UrlBuilder.CreateFile("abc/def.cfg"); + UrlDir.UrlFile file2 = UrlBuilder.CreateFile("ghi/jkl.cfg"); + IPass pass1 = Substitute.For(); + IPass pass2 = Substitute.For(); + IPass pass3 = Substitute.For(); + pass1.Name.Returns(":PASS1"); + pass2.Name.Returns(":PASS2"); + pass3.Name.Returns(":PASS3"); UrlDir.UrlConfig[] patchUrlConfigs = new UrlDir.UrlConfig[9]; IPatch[] patches = new IPatch[9]; - - for (int i = 0; i < 9; i++) + for (int i = 0; i < patches.Length; i++) { - patchUrlConfigs[i] = UrlBuilder.CreateConfig(new ConfigNode(), file); patches[i] = Substitute.For(); - patches[i].UrlConfig.Returns(patchUrlConfigs[i]); } pass1.GetEnumerator().Returns(new ArrayEnumerator(patches[0], patches[1], patches[2])); pass2.GetEnumerator().Returns(new ArrayEnumerator(patches[3], patches[4], patches[5])); pass3.GetEnumerator().Returns(new ArrayEnumerator(patches[6], patches[7], patches[8])); - patchApplier.ApplyPatches(); + IPass[] patchList = new IPass[] { pass1, pass2, pass3 }; + + patchApplier.ApplyPatches(new[] { file1, file2 }, new[] { pass1, pass2, pass3 }); progress.DidNotReceiveWithAnyArgs().Error(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null); @@ -77,25 +100,34 @@ public void TestApplyPatches() Received.InOrder(delegate { logger.Log(LogType.Log, ":PASS1 pass"); - patches[0].Apply(file, progress, logger); + patches[0].Apply(file1, progress, logger); + patches[0].Apply(file2, progress, logger); progress.PatchApplied(); - patches[1].Apply(file, progress, logger); + patches[1].Apply(file1, progress, logger); + patches[1].Apply(file2, progress, logger); progress.PatchApplied(); - patches[2].Apply(file, progress, logger); + patches[2].Apply(file1, progress, logger); + patches[2].Apply(file2, progress, logger); progress.PatchApplied(); logger.Log(LogType.Log, ":PASS2 pass"); - patches[3].Apply(file, progress, logger); + patches[3].Apply(file1, progress, logger); + patches[3].Apply(file2, progress, logger); progress.PatchApplied(); - patches[4].Apply(file, progress, logger); + patches[4].Apply(file1, progress, logger); + patches[4].Apply(file2, progress, logger); progress.PatchApplied(); - patches[5].Apply(file, progress, logger); + patches[5].Apply(file1, progress, logger); + patches[5].Apply(file2, progress, logger); progress.PatchApplied(); logger.Log(LogType.Log, ":PASS3 pass"); - patches[6].Apply(file, progress, logger); + patches[6].Apply(file1, progress, logger); + patches[6].Apply(file2, progress, logger); progress.PatchApplied(); - patches[7].Apply(file, progress, logger); + patches[7].Apply(file1, progress, logger); + patches[7].Apply(file2, progress, logger); progress.PatchApplied(); - patches[8].Apply(file, progress, logger); + patches[8].Apply(file1, progress, logger); + patches[8].Apply(file2, progress, logger); progress.PatchApplied(); }); } diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 0adcbe90..7f82de56 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -1,719 +1,446 @@ using System; -using System.Collections.Generic; +using System.Linq; using Xunit; using NSubstitute; using TestUtils; using ModuleManager; using ModuleManager.Logging; using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; +using ModuleManager.Tags; namespace ModuleManagerTests { public class PatchExtractorTest { - private UrlDir root; - private UrlDir.UrlFile file; + private readonly UrlDir root; + private readonly UrlDir.UrlFile file; - private IPatchProgress progress; - private IPatchList patchList; - private IBasicLogger logger; - private PatchExtractor extractor; + private readonly IPatchProgress progress; + private readonly IBasicLogger logger; + private readonly INeedsChecker needsChecker; + private readonly ITagListParser tagListParser; + private readonly IProtoPatchBuilder protoPatchBuilder; + private readonly IPatchCompiler patchCompiler; + private readonly PatchExtractor patchExtractor; public PatchExtractorTest() { root = UrlBuilder.CreateRoot(); file = UrlBuilder.CreateFile("abc/def.cfg", root); - - patchList = Substitute.For(); + progress = Substitute.For(); logger = Substitute.For(); - extractor = new PatchExtractor(patchList, progress, logger); + needsChecker = Substitute.For(); + tagListParser = Substitute.For(); + protoPatchBuilder = Substitute.For(); + patchCompiler = Substitute.For(); + patchExtractor = new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); } [Fact] - public void TestConstructor__PatchListNull() + public void TestConstructor__ProgressNull() { ArgumentNullException ex = Assert.Throws(delegate { - new PatchExtractor(null, progress, logger); + new PatchExtractor(null, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); }); - Assert.Equal("patchList", ex.ParamName); + Assert.Equal("progress", ex.ParamName); } [Fact] - public void TestConstructor__ProgressNull() + public void TestConstructor__LoggerNull() { ArgumentNullException ex = Assert.Throws(delegate { - new PatchExtractor(patchList, null, logger); + new PatchExtractor(progress, null, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); }); - Assert.Equal("progress", ex.ParamName); + Assert.Equal("logger", ex.ParamName); } [Fact] - public void TestConstructor__LoggerNull() + public void TestConstructor__NeedsCheckerNull() { ArgumentNullException ex = Assert.Throws(delegate { - new PatchExtractor(patchList, progress, null); + new PatchExtractor(progress, logger, null, tagListParser, protoPatchBuilder, patchCompiler); }); - Assert.Equal("logger", ex.ParamName); + Assert.Equal("needsChecker", ex.ParamName); } [Fact] - public void TestExtractPatch__Insert() + public void TestConstructor__TagListParserNull() { - UrlDir.UrlConfig patchConfig = CreateConfig("NODE"); - - extractor.ExtractPatch(patchConfig); - - AssertNoErrors(); - - Assert.Equal(new[] { patchConfig }, root.AllConfigs); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - - progress.DidNotReceive().PatchAdded(); + ArgumentNullException ex = Assert.Throws(delegate + { + new PatchExtractor(progress, logger, needsChecker, null, protoPatchBuilder, patchCompiler); + }); - EnsureNeedsSatisfied(); + Assert.Equal("tagListParser", ex.ParamName); } [Fact] - public void TestExtractPatch__First() + public void TestConstructor__ProtoPatchBuilderNull() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FIRST"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FIRST"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:First"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:first"); - - List patches = new List(); - patchList.AddFirstPatch(Arg.Do(patch => patches.Add(patch))); - - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); - - AssertNoErrors(); - - Assert.Empty(root.AllConfigs); - - Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); - AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); - - Received.InOrder(delegate + ArgumentNullException ex = Assert.Throws(delegate { - patchList.Received().AddFirstPatch(patches[0]); - progress.Received().PatchAdded(); - patchList.Received().AddFirstPatch(patches[1]); - progress.Received().PatchAdded(); - patchList.Received().AddFirstPatch(patches[2]); - progress.Received().PatchAdded(); - patchList.Received().AddFirstPatch(patches[3]); - progress.Received().PatchAdded(); + new PatchExtractor(progress, logger, needsChecker, tagListParser, null, patchCompiler); }); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - - EnsureNeedsSatisfied(); + Assert.Equal("protoPatchBuilder", ex.ParamName); } [Fact] - public void TestExtractPatch__Legacy() + public void TestConstructor__PatchCompilerNull() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE"); - - List patches = new List(); - patchList.AddLegacyPatch(Arg.Do(patch => patches.Add(patch))); - - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - - AssertNoErrors(); - - Assert.Empty(root.AllConfigs); - - Assert.Equal(2, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); - AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); - - Received.InOrder(delegate + ArgumentNullException ex = Assert.Throws(delegate { - patchList.Received().AddLegacyPatch(patches[0]); - progress.Received().PatchAdded(); - patchList.Received().AddLegacyPatch(patches[1]); - progress.Received().PatchAdded(); + new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, null); }); - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - - EnsureNeedsSatisfied(); + Assert.Equal("patchCompiler", ex.ParamName); } [Fact] - public void TestExtractPatch__BeforeMod() + public void TestExtractPatch__ProtoPatchNull() { - patchList.HasMod("mod1").Returns(true); + UrlDir.UrlConfig patchConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE"), root); - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod1]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:BEFORE[mod1]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Before[mod1]"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:before[MOD1]"); + protoPatchBuilder.Build(patchConfig, Command.Insert, Arg.Any()).Returns(null, new ProtoPatch[0]); - List patches = new List(); - patchList.AddBeforePatch("mod1", Arg.Do(patch => patches.Add(patch))); + Assert.Null(patchExtractor.ExtractPatch(patchConfig)); - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsExpression(null); AssertNoErrors(); Assert.Empty(root.AllConfigs); - Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); - AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); - - Received.InOrder(delegate - { - patchList.Received().AddBeforePatch("mod1", patches[0]); - progress.Received().PatchAdded(); - patchList.Received().AddBeforePatch("mod1", patches[1]); - progress.Received().PatchAdded(); - patchList.Received().AddBeforePatch("mod1", patches[2]); - progress.Received().PatchAdded(); - patchList.Received().AddBeforePatch("mod1", patches[3]); - progress.Received().PatchAdded(); - }); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - - EnsureNeedsSatisfied(); + progress.DidNotReceive().PatchAdded(); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestExtractPatch__BeforeMod__ModDoesNotExist() + public void TestExtractPatch() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod3]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:BEFORE[mod3]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Before[mod3]"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:before[MOD3]"); + UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE").Returns(tagList); - AssertNoErrors(); + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Edit, + "NODE_TYPE", + "nodeName", + null, + "has", + passSpecifier + ); - Assert.Empty(root.AllConfigs); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + passSpecifier.CheckNeeds(needsChecker, progress).Returns(true); - Received.InOrder(delegate - { - progress.Received().NeedsUnsatisfiedBefore(patchConfig1); - progress.Received().NeedsUnsatisfiedBefore(patchConfig2); - progress.Received().NeedsUnsatisfiedBefore(patchConfig3); - progress.Received().NeedsUnsatisfiedBefore(patchConfig4); - }); + IPatch patch = Substitute.For(); + patchCompiler.CompilePatch(protoPatch).Returns(patch); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + Assert.Same(patch, patchExtractor.ExtractPatch(urlConfig)); - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + AssertNoErrors(); - progress.DidNotReceive().PatchAdded(); + Assert.Empty(root.AllConfigs); + + needsChecker.Received().CheckNeedsRecursive(urlConfig.config, urlConfig); + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsExpression(null); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestExtractPatch__ForMod() + public void TestExtractPatch__Needs() { - patchList.HasMod("mod1").Returns(true); - - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod1]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FOR[mod1]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:For[mod1]"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:for[MOD1]"); + UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); - List patches = new List(); - patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE").Returns(tagList); - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Edit, + "NODE_TYPE", + "nodeName", + "needs", + "has", + passSpecifier + ); - AssertNoErrors(); - - Assert.Empty(root.AllConfigs); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + needsChecker.CheckNeedsExpression("needs").Returns(true); + passSpecifier.CheckNeeds(needsChecker, progress).Returns(true); - Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); - AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); + IPatch patch = Substitute.For(); + patchCompiler.CompilePatch(protoPatch).Returns(patch); + Assert.Same(patch, patchExtractor.ExtractPatch(urlConfig)); - Received.InOrder(delegate - { - patchList.Received().AddForPatch("mod1", patches[0]); - progress.Received().PatchAdded(); - patchList.Received().AddForPatch("mod1", patches[1]); - progress.Received().PatchAdded(); - patchList.Received().AddForPatch("mod1", patches[2]); - progress.Received().PatchAdded(); - patchList.Received().AddForPatch("mod1", patches[3]); - progress.Received().PatchAdded(); - }); + AssertNoErrors(); - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + Assert.Empty(root.AllConfigs); - EnsureNeedsSatisfied(); + needsChecker.Received().CheckNeedsRecursive(urlConfig.config, urlConfig); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestExtractPatch__ForMod__ModDoesNotExist() + public void TestExtractPatch__NeedsUnsatisfied() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod3]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FOR[mod3]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:For[mod3]"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:for[MOD3]"); + UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE").Returns(tagList); - AssertNoErrors(); + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Edit, + "NODE_TYPE", + "nodeName", + "needs", + "has", + passSpecifier + ); - Assert.Empty(root.AllConfigs); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + needsChecker.CheckNeedsExpression("needs").Returns(false); - Received.InOrder(delegate - { - progress.Received().NeedsUnsatisfiedFor(patchConfig1); - progress.Received().NeedsUnsatisfiedFor(patchConfig2); - progress.Received().NeedsUnsatisfiedFor(patchConfig3); - progress.Received().NeedsUnsatisfiedFor(patchConfig4); - }); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + AssertNoErrors(); - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + Assert.Empty(root.AllConfigs); - progress.DidNotReceive().PatchAdded(); + passSpecifier.DidNotReceiveWithAnyArgs().CheckNeeds(null, null); + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsRecursive(null, null); + patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); + + progress.Received().NeedsUnsatisfiedRoot(urlConfig); } [Fact] - public void TestExtractPatch__AfterMod() + public void TestExtractPatch__NeedsUnsatisfiedPassSpecifier() { - patchList.HasMod("mod1").Returns(true); + UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod1]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:AFTER[mod1]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:After[mod1]"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:after[MOD1]"); + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE").Returns(tagList); - List patches = new List(); - patchList.AddAfterPatch("mod1", Arg.Do(patch => patches.Add(patch))); + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Edit, + "NODE_TYPE", + "nodeName", + "needs", + "has", + passSpecifier + ); - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + needsChecker.CheckNeedsExpression("needs").Returns(true); + passSpecifier.CheckNeeds(needsChecker, progress).Returns(false); - AssertNoErrors(); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); - AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); + AssertNoErrors(); Assert.Empty(root.AllConfigs); - Received.InOrder(delegate - { - patchList.Received().AddAfterPatch("mod1", patches[0]); - progress.Received().PatchAdded(); - patchList.Received().AddAfterPatch("mod1", patches[1]); - progress.Received().PatchAdded(); - patchList.Received().AddAfterPatch("mod1", patches[2]); - progress.Received().PatchAdded(); - patchList.Received().AddAfterPatch("mod1", patches[3]); - progress.Received().PatchAdded(); - }); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsRecursive(null, null); + patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); - EnsureNeedsSatisfied(); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestExtractPatch__AfterMod__ModDoesNotExist() + public void TestExtractPatch__Insert() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod3]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:AFTER[mod3]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:After[mod3]"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:after[MOD3]"); - - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); - + UrlDir.UrlConfig urlConfig = CreateConfig("NODE_TYPE"); + + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE").Returns(tagList); + + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Insert, + "NODE_TYPE", + null, + "needs", + null, + passSpecifier + ); + + protoPatchBuilder.Build(urlConfig, Command.Insert, tagList).Returns(protoPatch); + needsChecker.CheckNeedsExpression("needs").Returns(true); + passSpecifier.CheckNeeds(needsChecker, progress).Returns(true); + + ConfigNode needsCheckedNode = null; + needsChecker.CheckNeedsRecursive(Arg.Do(node => needsCheckedNode = node), urlConfig); + + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + AssertNoErrors(); - Assert.Empty(root.AllConfigs); - Received.InOrder(delegate { - progress.Received().NeedsUnsatisfiedAfter(patchConfig1); - progress.Received().NeedsUnsatisfiedAfter(patchConfig2); - progress.Received().NeedsUnsatisfiedAfter(patchConfig3); - progress.Received().NeedsUnsatisfiedAfter(patchConfig4); + tagListParser.Parse("NODE_TYPE"); + protoPatchBuilder.Build(urlConfig, Command.Insert, tagList); + needsChecker.CheckNeedsExpression("needs"); + passSpecifier.CheckNeeds(needsChecker, progress); + needsChecker.CheckNeedsRecursive(needsCheckedNode, urlConfig); }); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); + + Assert.Equal(1, root.AllConfigs.Count()); + UrlDir.UrlConfig newUrlConfig = root.AllConfigs.First(); + Assert.NotSame(urlConfig, newUrlConfig); + Assert.NotSame(urlConfig.config, newUrlConfig.config); + AssertConfigNodesEqual(urlConfig.config, newUrlConfig.config); + Assert.Same(needsCheckedNode, newUrlConfig.config); - progress.DidNotReceive().PatchAdded(); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestExtractPatch__Final() + public void TestExtractPatch__Null() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE[foo]:HAS[#bar]:FINAL"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FINAL"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:Final"); - UrlDir.UrlConfig patchConfig4 = CreateConfig("@NODE:final"); - - List patches = new List(); - patchList.AddFinalPatch(Arg.Do(patch => patches.Add(patch))); - - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); - extractor.ExtractPatch(patchConfig4); - - AssertNoErrors(); - - Assert.Empty(root.AllConfigs); - - Assert.Equal(4, patches.Count); - AssertPatchCorrect(patches[0], patchConfig1, AssertNodeMatcher__Name__Has); - AssertPatchCorrect(patches[1], patchConfig2, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[2], patchConfig3, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[3], patchConfig4, AssertNodeMatcher__Bare); - - Received.InOrder(delegate + ArgumentNullException ex = Assert.Throws(delegate { - patchList.Received().AddFinalPatch(patches[0]); - progress.Received().PatchAdded(); - patchList.Received().AddFinalPatch(patches[1]); - progress.Received().PatchAdded(); - patchList.Received().AddFinalPatch(patches[2]); - progress.Received().PatchAdded(); - patchList.Received().AddFinalPatch(patches[3]); - progress.Received().PatchAdded(); + patchExtractor.ExtractPatch(null); }); - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - - EnsureNeedsSatisfied(); + Assert.Equal("urlConfig", ex.ParamName); } [Fact] - public void TestExtractPatch__InsertWithPass() + public void TestExtractPatch__NotBracketBalanced() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("NODE:FOR[mod1]"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("NODE:FOR[mod2]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("NODE:FINAL"); - - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); + UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR["); + UrlDir.UrlConfig config2 = CreateConfig("NODE:HAS[#foo[]"); + patchExtractor.ExtractPatch(config1); + patchExtractor.ExtractPatch(config2); + Assert.Empty(root.AllConfigs); - + progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - + Received.InOrder(delegate { - progress.Received().Error(patchConfig1, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FOR[mod1]"); - progress.Received().Error(patchConfig2, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FOR[mod2]"); - progress.Received().Error(patchConfig3, "Error - pass specifier detected on an insert node (not a patch): abc/def/NODE:FINAL"); + progress.Received().Error(config1, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/@NODE:FOR["); + progress.Received().Error(config2, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/NODE:HAS[#foo[]"); }); - EnsureNeedsSatisfied(); - - progress.DidNotReceive().PatchAdded(); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestExtractPatch__MoreThanOnePass() + public void TestExtractPatch__InvalidCommand__Replace() { - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE:FIRST:FIRST"); - UrlDir.UrlConfig patchConfig2 = CreateConfig("@NODE:FIRST:FOR[mod1]"); - UrlDir.UrlConfig patchConfig3 = CreateConfig("@NODE:FOR[mod1]:AFTER[mod2]"); - - extractor.ExtractPatch(patchConfig1); - extractor.ExtractPatch(patchConfig2); - extractor.ExtractPatch(patchConfig3); + UrlDir.UrlConfig urlConfig = CreateConfig("%NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); Assert.Empty(root.AllConfigs); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - - Received.InOrder(delegate - { - progress.Received().Error(patchConfig1, "Error - more than one pass specifier on a node: abc/def/@NODE:FIRST:FIRST"); - progress.Received().Error(patchConfig2, "Error - more than one pass specifier on a node: abc/def/@NODE:FIRST:FOR[mod1]"); - progress.Received().Error(patchConfig3, "Error - more than one pass specifier on a node: abc/def/@NODE:FOR[mod1]:AFTER[mod2]"); - }); - - EnsureNeedsSatisfied(); - - progress.DidNotReceive().PatchAdded(); + progress.Received().Error(urlConfig, "Error - replace command (%) is not valid on a root node: abc/def/%NODE"); } [Fact] - public void TestExtractPatch__Exception() + public void TestExtractPatch__InvalidCommand__Create() { - Exception e = new Exception("an exception was thrown"); - progress.WhenForAnyArgs(p => p.Error(null, null)).Throw(e); - - UrlDir.UrlConfig patchConfig1 = CreateConfig("@NODE:FIRST:FIRST"); - - extractor.ExtractPatch(patchConfig1); + UrlDir.UrlConfig urlConfig = CreateConfig("&NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); Assert.Empty(root.AllConfigs); - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); - - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - - progress.Received().Exception(patchConfig1, "Exception while parsing pass for config: abc/def/@NODE:FIRST:FIRST", e); - - EnsureNeedsSatisfied(); - - progress.DidNotReceive().PatchAdded(); + progress.Received().Error(urlConfig, "Error - create command (&) is not valid on a root node: abc/def/&NODE"); } [Fact] - public void TestExtractPatch__NotBracketBalanced() + public void TestExtractPatch__InvalidCommand__Rename() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR["); - UrlDir.UrlConfig config2 = CreateConfig("NODE:HAS[#foo[]"); - - extractor.ExtractPatch(config1); - extractor.ExtractPatch(config2); + UrlDir.UrlConfig urlConfig = CreateConfig("|NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); Assert.Empty(root.AllConfigs); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + progress.Received().Error(urlConfig, "Error - rename command (|) is not valid on a root node: abc/def/|NODE"); + } - Received.InOrder(delegate - { - progress.Received().Error(config1, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/@NODE:FOR["); - progress.Received().Error(config2, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\nabc/def/NODE:HAS[#foo[]"); - }); + [Fact] + public void TestExtractPatch__InvalidCommand__Paste() + { + UrlDir.UrlConfig urlConfig = CreateConfig("#NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - EnsureNeedsSatisfied(); + Assert.Empty(root.AllConfigs); - progress.DidNotReceive().PatchAdded(); + progress.Received().Error(urlConfig, "Error - paste command (#) is not valid on a root node: abc/def/#NODE"); } [Fact] - public void TestExtractPatch__BadlyFormed() + public void TestExtractPatch__InvalidCommand__Special() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE[foo]:HAS[#bar]:FOR[]"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:BEFORE"); - UrlDir.UrlConfig config3 = CreateConfig("@NODE:AFTER"); - - extractor.ExtractPatch(config1); - extractor.ExtractPatch(config2); - extractor.ExtractPatch(config3); + UrlDir.UrlConfig urlConfig = CreateConfig("*NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); Assert.Empty(root.AllConfigs); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddForPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + progress.Received().Error(urlConfig, "Error - special command (*) is not valid on a root node: abc/def/*NODE"); + } - Received.InOrder(delegate - { - progress.Received().Error(config1, "Error - malformed :FOR patch specifier detected: abc/def/@NODE[foo]:HAS[#bar]:FOR[]"); - progress.Received().Error(config2, "Error - malformed :BEFORE patch specifier detected: abc/def/@NODE:BEFORE"); - progress.Received().Error(config3, "Error - malformed :AFTER patch specifier detected: abc/def/@NODE:AFTER"); - }); + [Fact] + public void TestExtractPatch__TagListBadlyFormatted() + { + UrlDir.UrlConfig urlConfig = CreateConfig("badSomehow"); + tagListParser.When(t => t.Parse("badSomehow")).Throw(new FormatException("badly formatted")); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - EnsureNeedsSatisfied(); + Assert.Empty(root.AllConfigs); - progress.DidNotReceive().PatchAdded(); + progress.Received().Error(urlConfig, "Cannot parse node name as tag list: badly formatted\non: abc/def/badSomehow"); } [Fact] - public void TestExtractPatch__Command() + public void TestExtractPatch__ProtoPatchFailed() { - patchList.HasMod("mod1").Returns(true); - - UrlDir.UrlConfig config01 = CreateConfig("@NODE:FOR[mod1]"); - UrlDir.UrlConfig config02 = CreateConfig("+NODE:FOR[mod1]"); - UrlDir.UrlConfig config03 = CreateConfig("$NODE:FOR[mod1]"); - UrlDir.UrlConfig config04 = CreateConfig("!NODE:FOR[mod1]"); - UrlDir.UrlConfig config05 = CreateConfig("-NODE:FOR[mod1]"); - UrlDir.UrlConfig config06 = CreateConfig("%NODE:FOR[mod1]"); - UrlDir.UrlConfig config07 = CreateConfig("&NODE:FOR[mod1]"); - UrlDir.UrlConfig config08 = CreateConfig("|NODE:FOR[mod1]"); - UrlDir.UrlConfig config09 = CreateConfig("#NODE:FOR[mod1]"); - UrlDir.UrlConfig config10 = CreateConfig("*NODE:FOR[mod1]"); - - List patches = new List(); - patchList.AddForPatch("mod1", Arg.Do(patch => patches.Add(patch))); - - extractor.ExtractPatch(config01); - extractor.ExtractPatch(config02); - extractor.ExtractPatch(config03); - extractor.ExtractPatch(config04); - extractor.ExtractPatch(config05); - extractor.ExtractPatch(config06); - extractor.ExtractPatch(config07); - extractor.ExtractPatch(config08); - extractor.ExtractPatch(config09); - extractor.ExtractPatch(config10); + UrlDir.UrlConfig urlConfig = CreateConfig("NODE"); + protoPatchBuilder.Build(urlConfig, Command.Insert, Arg.Any()).Returns((ProtoPatch)null); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + AssertNoErrors(); Assert.Empty(root.AllConfigs); + } - Assert.Equal(5, patches.Count); - AssertPatchCorrect(patches[0], config01, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[1], config02, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[2], config03, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[3], config04, AssertNodeMatcher__Bare); - AssertPatchCorrect(patches[4], config05, AssertNodeMatcher__Bare); - progress.Received().PatchAdded(); - - Received.InOrder(delegate - { - patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[0]); - progress.Received().PatchAdded(); - patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[1]); - progress.Received().PatchAdded(); - patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[2]); - progress.Received().PatchAdded(); - patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[3]); - progress.Received().PatchAdded(); - patchList.DidNotReceiveWithAnyArgs().AddForPatch("mod1", patches[4]); - progress.Received().PatchAdded(); - progress.Received().Error(config06, "Error - replace command (%) is not valid on a root node: abc/def/%NODE:FOR[mod1]"); - progress.Received().Error(config07, "Error - create command (&) is not valid on a root node: abc/def/&NODE:FOR[mod1]"); - progress.Received().Error(config08, "Error - rename command (|) is not valid on a root node: abc/def/|NODE:FOR[mod1]"); - progress.Received().Error(config09, "Error - paste command (#) is not valid on a root node: abc/def/#NODE:FOR[mod1]"); - progress.Received().Error(config10, "Error - special command (*) is not valid on a root node: abc/def/*NODE:FOR[mod1]"); - }); + [Fact] + public void TestExtractPatch__Exception() + { + UrlDir.UrlConfig urlConfig = CreateConfig("NODE"); + Exception ex = new Exception(); + tagListParser.When(t => t.Parse("NODE")).Throw(ex); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - EnsureNeedsSatisfied(); + Assert.Empty(root.AllConfigs); - patchList.DidNotReceiveWithAnyArgs().AddFirstPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddLegacyPatch(null); - patchList.DidNotReceiveWithAnyArgs().AddBeforePatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddAfterPatch(null, null); - patchList.DidNotReceiveWithAnyArgs().AddFinalPatch(null); + progress.Received().Exception(urlConfig, "Exception while attempting to create patch from config: abc/def/NODE", ex); } private UrlDir.UrlConfig CreateConfig(string name) @@ -726,20 +453,12 @@ private UrlDir.UrlConfig CreateConfig(string name) new ConfigNode("wine"), new ConfigNode("fruit"), }; - + node.id = "hungry?"; - + return UrlBuilder.CreateConfig(node, file); } - private void AssertPatchCorrect(IPatch patch, UrlDir.UrlConfig originalUrl, Action assertNodeMatcher) where T : IPatch - { - Assert.IsType(patch); - Assert.Same(originalUrl, patch.UrlConfig); - - assertNodeMatcher(patch.NodeMatcher); - } - private void AssertNoErrors() { progress.DidNotReceiveWithAnyArgs().Error(null, null); @@ -747,58 +466,9 @@ private void AssertNoErrors() progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); } - private void EnsureNeedsSatisfied() - { - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); - } - - private void AssertNodeMatcher__Bare(INodeMatcher matcher) - { - Assert.True(matcher.IsMatch(new ConfigNode("NODE"))); - - Assert.True(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "boo" }, - { "bar", "baz" }, - })); - - Assert.False(matcher.IsMatch(new ConfigNode("NADE"))); - } - - private void AssertNodeMatcher__Name__Has(INodeMatcher matcher) + private void AssertConfigNodesEqual(ConfigNode expected, ConfigNode observed) { - Assert.True(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "foo" }, - { "bar", "baz" }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "foo" }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "boo" }, - { "bar", "baz" }, - })); - - Assert.False(matcher.IsMatch(new ConfigNode("NODE"))); - - Assert.False(matcher.IsMatch(new TestConfigNode("NADE") - { - { "name", "foo" }, - { "bar", "baz" }, - })); - - Assert.False(matcher.IsMatch(new TestConfigNode("NODE") - { - { "name", "boo" }, - { "bar", "baz" }, - })); + Assert.Equal(expected.ToString(), observed.ToString()); } } } diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index 0de0a2ea..d28821a6 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -6,296 +6,164 @@ using TestUtils; using ModuleManager; using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; namespace ModuleManagerTests { public class PatchListTest { - private UrlDir databaseRoot; - private UrlDir.UrlFile file; - private PatchList patchList; - - public PatchListTest() - { - databaseRoot = UrlBuilder.CreateRoot(); - file = UrlBuilder.CreateFile("abc/def.cfg", databaseRoot); - - patchList = new PatchList(new[] { "mod1", "mod2" }); - } - - [Fact] - public void Test__Lifecycle() - { - IPatch patch01 = Substitute.For(); - IPatch patch02 = Substitute.For(); - IPatch patch03 = Substitute.For(); - IPatch patch04 = Substitute.For(); - IPatch patch05 = Substitute.For(); - IPatch patch06 = Substitute.For(); - IPatch patch07 = Substitute.For(); - IPatch patch08 = Substitute.For(); - IPatch patch09 = Substitute.For(); - IPatch patch10 = Substitute.For(); - IPatch patch11 = Substitute.For(); - IPatch patch12 = Substitute.For(); - IPatch patch13 = Substitute.For(); - IPatch patch14 = Substitute.For(); - IPatch patch15 = Substitute.For(); - IPatch patch16 = Substitute.For(); - IPatch patch17 = Substitute.For(); - IPatch patch18 = Substitute.For(); - - patchList.AddFirstPatch(patch01); - patchList.AddFirstPatch(patch02); - patchList.AddLegacyPatch(patch03); - patchList.AddLegacyPatch(patch04); - patchList.AddBeforePatch("mod1", patch05); - patchList.AddBeforePatch("MOD1", patch06); - patchList.AddForPatch("mod1", patch07); - patchList.AddForPatch("MOD1", patch08); - patchList.AddAfterPatch("mod1", patch09); - patchList.AddAfterPatch("MOD1", patch10); - patchList.AddBeforePatch("mod2", patch11); - patchList.AddBeforePatch("MOD2", patch12); - patchList.AddForPatch("mod2", patch13); - patchList.AddForPatch("MOD2", patch14); - patchList.AddAfterPatch("mod2", patch15); - patchList.AddAfterPatch("MOD2", patch16); - patchList.AddFinalPatch(patch17); - patchList.AddFinalPatch(patch18); - - IPass[] passes = patchList.ToArray(); - - Assert.Equal(":FIRST", passes[0].Name); - Assert.Equal(new[] { patch01, patch02 }, passes[0]); - - Assert.Equal(":LEGACY (default)", passes[1].Name); - Assert.Equal(new[] { patch03, patch04 }, passes[1]); - - Assert.Equal(":BEFORE[MOD1]", passes[2].Name); - Assert.Equal(new[] { patch05, patch06 }, passes[2]); - - Assert.Equal(":FOR[MOD1]", passes[3].Name); - Assert.Equal(new[] { patch07, patch08 }, passes[3]); - - Assert.Equal(":AFTER[MOD1]", passes[4].Name); - Assert.Equal(new[] { patch09, patch10 }, passes[4]); - - Assert.Equal(":BEFORE[MOD2]", passes[5].Name); - Assert.Equal(new[] { patch11, patch12 }, passes[5]); - - Assert.Equal(":FOR[MOD2]", passes[6].Name); - Assert.Equal(new[] { patch13, patch14 }, passes[6]); - - Assert.Equal(":AFTER[MOD2]", passes[7].Name); - Assert.Equal(new[] { patch15, patch16 }, passes[7]); - - Assert.Equal(":FINAL", passes[8].Name); - Assert.Equal(new[] { patch17, patch18 }, passes[8]); - } - [Fact] - public void TestHasMod__True() - { - patchList = new PatchList(new[] { "mod1", "Mod2", "MOD3" }); - - Assert.True(patchList.HasMod("mod1")); - Assert.True(patchList.HasMod("Mod1")); - Assert.True(patchList.HasMod("MOD1")); - Assert.True(patchList.HasMod("mod2")); - Assert.True(patchList.HasMod("Mod2")); - Assert.True(patchList.HasMod("MOD2")); - Assert.True(patchList.HasMod("mod3")); - Assert.True(patchList.HasMod("Mod3")); - Assert.True(patchList.HasMod("MOD3")); - } - - [Fact] - public void TestHasMod__False() - { - Assert.False(patchList.HasMod("mod3")); - Assert.False(patchList.HasMod("Mod3")); - Assert.False(patchList.HasMod("MOD3")); - } - - [Fact] - public void TestHasMod__Null() + public void TestConstructor__ModListNull() { ArgumentNullException ex = Assert.Throws(delegate { - Assert.True(patchList.HasMod(null)); - }); - - Assert.Equal("mod", ex.ParamName); - } - - [Fact] - public void TestHasMod__Blank() - { - ArgumentException ex = Assert.Throws(delegate - { - Assert.True(patchList.HasMod("")); + new PatchList(null, new IPatch[0], Substitute.For()); }); - Assert.Contains("can't be empty", ex.Message); - Assert.Equal("mod", ex.ParamName); + Assert.Equal("modList", ex.ParamName); } [Fact] - public void TestAddLegacyPatch__Null() + public void TestConstructor__PatchesNull() { ArgumentNullException ex = Assert.Throws(delegate { - patchList.AddLegacyPatch(null); + new PatchList(new string[0], null, Substitute.For()); }); - Assert.Equal("patch", ex.ParamName); + Assert.Equal("patches", ex.ParamName); } [Fact] - public void TestAddBeforePatch__ModNull() + public void TestConstructor__ProgressNull() { ArgumentNullException ex = Assert.Throws(delegate { - patchList.AddBeforePatch(null, Substitute.For()); + new PatchList(new string[0], new IPatch[0], null); }); - Assert.Equal("mod", ex.ParamName); + Assert.Equal("progress", ex.ParamName); } [Fact] - public void TestAddBeforePatch__ModBlank() + public void TestConstructor__UnknownMod() { - ArgumentException ex = Assert.Throws(delegate - { - patchList.AddBeforePatch("", Substitute.For()); - }); - - Assert.Contains("can't be empty", ex.Message); - Assert.Equal("mod", ex.ParamName); - } + IPatch patch = Substitute.For(); + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + patch.PassSpecifier.Returns(new BeforePassSpecifier("mod3", urlConfig)); + IPatchProgress progress = Substitute.For(); - [Fact] - public void TestAddBeforePatch__PatchNull() - { - ArgumentNullException ex = Assert.Throws(delegate - { - patchList.AddBeforePatch("mod1", null); - }); - - Assert.Equal("patch", ex.ParamName); - } - - [Fact] - public void TestAddBeforePatch__ModDoesNotExist() - { KeyNotFoundException ex = Assert.Throws(delegate { - patchList.AddBeforePatch("mod3", Substitute.For()); + new PatchList(new[] { "mod1", "mod2" }, new[] { patch }, progress); }); Assert.Equal("Mod 'mod3' not found", ex.Message); - } - [Fact] - public void TestAddForPatch__ModNull() - { - ArgumentNullException ex = Assert.Throws(delegate - { - patchList.AddForPatch(null, Substitute.For()); - }); - - Assert.Equal("mod", ex.ParamName); + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestAddForPatch__ModBlank() + public void TestConstructor__UnknownPassSpecifier() { - ArgumentException ex = Assert.Throws(delegate - { - patchList.AddForPatch("", Substitute.For()); - }); + IPatch patch = Substitute.For(); + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + IPassSpecifier passSpecifier = Substitute.For(); + passSpecifier.Descriptor.Returns(":SOMEPASS"); + patch.PassSpecifier.Returns(passSpecifier); + IPatchProgress progress = Substitute.For(); - Assert.Contains("can't be empty", ex.Message); - Assert.Equal("mod", ex.ParamName); - } - - [Fact] - public void TestAddForPatch__PatchNull() - { - ArgumentNullException ex = Assert.Throws(delegate + NotImplementedException ex = Assert.Throws(delegate { - patchList.AddForPatch("mod1", null); + new PatchList(new string[0], new[] { patch }, progress); }); - Assert.Equal("patch", ex.ParamName); - } + Assert.Equal("Don't know what to do with pass specifier: :SOMEPASS", ex.Message); - [Fact] - public void TestAddForPatch__ModDoesNotExist() - { - KeyNotFoundException ex = Assert.Throws(delegate - { - patchList.AddForPatch("mod3", Substitute.For()); - }); - - Assert.Equal("Mod 'mod3' not found", ex.Message); + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestAddAfterPatch__ModNull() + public void Test__Lifecycle() { - ArgumentNullException ex = Assert.Throws(delegate - { - patchList.AddAfterPatch(null, Substitute.For()); - }); + IPatch[] patches = new IPatch[] + { + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + }; + + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + + patches[00].PassSpecifier.Returns(new FirstPassSpecifier()); + patches[01].PassSpecifier.Returns(new FirstPassSpecifier()); + patches[02].PassSpecifier.Returns(new LegacyPassSpecifier()); + patches[03].PassSpecifier.Returns(new LegacyPassSpecifier()); + patches[04].PassSpecifier.Returns(new BeforePassSpecifier("mod1", urlConfig)); + patches[05].PassSpecifier.Returns(new BeforePassSpecifier("MOD1", urlConfig)); + patches[06].PassSpecifier.Returns(new ForPassSpecifier("mod1", urlConfig)); + patches[07].PassSpecifier.Returns(new ForPassSpecifier("MOD1", urlConfig)); + patches[08].PassSpecifier.Returns(new AfterPassSpecifier("mod1", urlConfig)); + patches[09].PassSpecifier.Returns(new AfterPassSpecifier("MOD1", urlConfig)); + patches[10].PassSpecifier.Returns(new BeforePassSpecifier("mod2", urlConfig)); + patches[11].PassSpecifier.Returns(new BeforePassSpecifier("MOD2", urlConfig)); + patches[12].PassSpecifier.Returns(new ForPassSpecifier("mod2", urlConfig)); + patches[13].PassSpecifier.Returns(new ForPassSpecifier("MOD2", urlConfig)); + patches[14].PassSpecifier.Returns(new AfterPassSpecifier("mod2", urlConfig)); + patches[15].PassSpecifier.Returns(new AfterPassSpecifier("MOD2", urlConfig)); + patches[16].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[17].PassSpecifier.Returns(new FinalPassSpecifier()); + + IPatchProgress progress = Substitute.For(); + + PatchList patchList = new PatchList(new[] { "mod1", "mod2" }, patches, progress); - Assert.Equal("mod", ex.ParamName); - } + IPass[] passes = patchList.ToArray(); + + Assert.Equal(":FIRST", passes[0].Name); + Assert.Equal(new[] { patches[0], patches[1] }, passes[0]); - [Fact] - public void TestAddAfterPatch__ModBlank() - { - ArgumentException ex = Assert.Throws(delegate - { - patchList.AddAfterPatch("", Substitute.For()); - }); + Assert.Equal(":LEGACY (default)", passes[1].Name); + Assert.Equal(new[] { patches[2], patches[3] }, passes[1]); - Assert.Contains("can't be empty", ex.Message); - Assert.Equal("mod", ex.ParamName); - } + Assert.Equal(":BEFORE[MOD1]", passes[2].Name); + Assert.Equal(new[] { patches[4], patches[5] }, passes[2]); - [Fact] - public void TestAddAfterPatch__PatchNull() - { - ArgumentNullException ex = Assert.Throws(delegate - { - patchList.AddAfterPatch("mod1", null); - }); + Assert.Equal(":FOR[MOD1]", passes[3].Name); + Assert.Equal(new[] { patches[6], patches[7] }, passes[3]); - Assert.Equal("patch", ex.ParamName); - } + Assert.Equal(":AFTER[MOD1]", passes[4].Name); + Assert.Equal(new[] { patches[8], patches[9] }, passes[4]); - [Fact] - public void TestAddAddafterPatch__ModDoesNotExist() - { - KeyNotFoundException ex = Assert.Throws(delegate - { - patchList.AddAfterPatch("mod3", Substitute.For()); - }); + Assert.Equal(":BEFORE[MOD2]", passes[5].Name); + Assert.Equal(new[] { patches[10], patches[11] }, passes[5]); - Assert.Equal("Mod 'mod3' not found", ex.Message); - } + Assert.Equal(":FOR[MOD2]", passes[6].Name); + Assert.Equal(new[] { patches[12], patches[13] }, passes[6]); - [Fact] - public void TestAddFinalPatch__Null() - { - ArgumentNullException ex = Assert.Throws(delegate - { - patchList.AddFinalPatch(null); - }); + Assert.Equal(":AFTER[MOD2]", passes[7].Name); + Assert.Equal(new[] { patches[14], patches[15] }, passes[7]); + + Assert.Equal(":FINAL", passes[8].Name); + Assert.Equal(new[] { patches[16], patches[17] }, passes[8]); - Assert.Equal("patch", ex.ParamName); + progress.Received(patches.Length).PatchAdded(); } } } diff --git a/ModuleManagerTests/Patches/CopyPatchTest.cs b/ModuleManagerTests/Patches/CopyPatchTest.cs index 3b5e1b1e..70eddd6e 100644 --- a/ModuleManagerTests/Patches/CopyPatchTest.cs +++ b/ModuleManagerTests/Patches/CopyPatchTest.cs @@ -5,6 +5,7 @@ using ModuleManager; using ModuleManager.Logging; using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManagerTests.Patches @@ -16,7 +17,7 @@ public void TestConstructor__urlConfigNull() { ArgumentNullException ex = Assert.Throws(delegate { - new CopyPatch(null, Substitute.For()); + new CopyPatch(null, Substitute.For(), Substitute.For()); }); Assert.Equal("urlConfig", ex.ParamName); @@ -27,17 +28,28 @@ public void TestConstructor__nodeMatcherNull() { ArgumentNullException ex = Assert.Throws(delegate { - new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null); + new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null, Substitute.For()); }); Assert.Equal("nodeMatcher", ex.ParamName); } + [Fact] + public void TestConstructor__passSpecifierNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), null); + }); + + Assert.Equal("passSpecifier", ex.ParamName); + } + [Fact] public void TestUrlConfig() { UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode()); - CopyPatch patch = new CopyPatch(urlConfig, Substitute.For()); + CopyPatch patch = new CopyPatch(urlConfig, Substitute.For(), Substitute.For()); Assert.Same(urlConfig, patch.UrlConfig); } @@ -46,11 +58,20 @@ public void TestUrlConfig() public void TestNodeMatcher() { INodeMatcher nodeMatcher = Substitute.For(); - CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher); + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher, Substitute.For()); Assert.Same(nodeMatcher, patch.NodeMatcher); } + [Fact] + public void TestPassSpecifier() + { + IPassSpecifier passSpecifier = Substitute.For(); + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), passSpecifier); + + Assert.Same(passSpecifier, patch.PassSpecifier); + } + [Fact] public void TestApply() { @@ -80,7 +101,7 @@ public void TestApply() { { "@foo", "baz" }, { "pqr", "stw" }, - }), nodeMatcher); + }), nodeMatcher, Substitute.For()); IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); @@ -152,7 +173,7 @@ public void TestApply__NameChanged() { "@name", "001" }, { "@foo", "baz" }, { "pqr", "stw" }, - }), nodeMatcher); + }), nodeMatcher, Substitute.For()); IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); @@ -204,7 +225,7 @@ public void TestApply__NameNotChanged() { { "@foo", "baz" }, { "pqr", "stw" }, - }), nodeMatcher); + }), nodeMatcher, Substitute.For()); IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); @@ -233,7 +254,7 @@ public void TestApply__NameNotChanged() [Fact] public void TestApply__FileNull() { - CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(null, Substitute.For(), Substitute.For()); @@ -245,7 +266,7 @@ public void TestApply__FileNull() [Fact] public void TestApply__ProgressNull() { - CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); @@ -257,7 +278,7 @@ public void TestApply__ProgressNull() [Fact] public void TestApply__LoggerNull() { - CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); diff --git a/ModuleManagerTests/Patches/DeletePatchTest.cs b/ModuleManagerTests/Patches/DeletePatchTest.cs index b75576b7..743b7bb2 100644 --- a/ModuleManagerTests/Patches/DeletePatchTest.cs +++ b/ModuleManagerTests/Patches/DeletePatchTest.cs @@ -5,6 +5,7 @@ using ModuleManager; using ModuleManager.Logging; using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManagerTests.Patches @@ -16,7 +17,7 @@ public void TestConstructor__urlConfigNull() { ArgumentNullException ex = Assert.Throws(delegate { - new DeletePatch(null, Substitute.For()); + new DeletePatch(null, Substitute.For(), Substitute.For()); }); Assert.Equal("urlConfig", ex.ParamName); @@ -27,17 +28,28 @@ public void TestConstructor__nodeMatcherNull() { ArgumentNullException ex = Assert.Throws(delegate { - new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null); + new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null, Substitute.For()); }); Assert.Equal("nodeMatcher", ex.ParamName); } + [Fact] + public void TestConstructor__passSpecifierNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), null); + }); + + Assert.Equal("passSpecifier", ex.ParamName); + } + [Fact] public void TestUrlConfig() { UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode()); - DeletePatch patch = new DeletePatch(urlConfig, Substitute.For()); + DeletePatch patch = new DeletePatch(urlConfig, Substitute.For(), Substitute.For()); Assert.Same(urlConfig, patch.UrlConfig); } @@ -46,11 +58,20 @@ public void TestUrlConfig() public void TestNodeMatcher() { INodeMatcher nodeMatcher = Substitute.For(); - DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher); + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher, Substitute.For()); Assert.Same(nodeMatcher, patch.NodeMatcher); } + [Fact] + public void TestPassSpecifier() + { + IPassSpecifier passSpecifier = Substitute.For(); + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), passSpecifier); + + Assert.Same(passSpecifier, patch.PassSpecifier); + } + [Fact] public void TestApply() { @@ -68,7 +89,7 @@ public void TestApply() nodeMatcher.IsMatch(urlConfig3.config).Returns(false); nodeMatcher.IsMatch(urlConfig4.config).Returns(true); - DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("!NODE")), nodeMatcher); + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("!NODE")), nodeMatcher, Substitute.For()); IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); @@ -94,7 +115,7 @@ public void TestApply() [Fact] public void TestApply__FileNull() { - DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(null, Substitute.For(), Substitute.For()); @@ -106,7 +127,7 @@ public void TestApply__FileNull() [Fact] public void TestApply__ProgressNull() { - DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); @@ -118,7 +139,7 @@ public void TestApply__ProgressNull() [Fact] public void TestApply__LoggerNull() { - DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); diff --git a/ModuleManagerTests/Patches/EditPatchTest.cs b/ModuleManagerTests/Patches/EditPatchTest.cs index 7db368e3..d0afbc24 100644 --- a/ModuleManagerTests/Patches/EditPatchTest.cs +++ b/ModuleManagerTests/Patches/EditPatchTest.cs @@ -7,6 +7,7 @@ using ModuleManager; using ModuleManager.Logging; using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManagerTests.Patches @@ -18,7 +19,7 @@ public void TestConstructor__urlConfigNull() { ArgumentNullException ex = Assert.Throws(delegate { - new EditPatch(null, Substitute.For()); + new EditPatch(null, Substitute.For(), Substitute.For()); }); Assert.Equal("urlConfig", ex.ParamName); @@ -29,17 +30,28 @@ public void TestConstructor__nodeMatcherNull() { ArgumentNullException ex = Assert.Throws(delegate { - new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null); + new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null, Substitute.For()); }); Assert.Equal("nodeMatcher", ex.ParamName); } + [Fact] + public void TestConstructor__passSpecifierNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), null); + }); + + Assert.Equal("passSpecifier", ex.ParamName); + } + [Fact] public void TestUrlConfig() { UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode()); - EditPatch patch = new EditPatch(urlConfig, Substitute.For()); + EditPatch patch = new EditPatch(urlConfig, Substitute.For(), Substitute.For()); Assert.Same(urlConfig, patch.UrlConfig); } @@ -48,11 +60,20 @@ public void TestUrlConfig() public void TestNodeMatcher() { INodeMatcher nodeMatcher = Substitute.For(); - EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher); + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), nodeMatcher, Substitute.For()); Assert.Same(nodeMatcher, patch.NodeMatcher); } + [Fact] + public void TestPassSpecifier() + { + IPassSpecifier passSpecifier = Substitute.For(); + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), passSpecifier); + + Assert.Same(passSpecifier, patch.PassSpecifier); + } + [Fact] public void TestApply() { @@ -82,7 +103,7 @@ public void TestApply() { { "@foo", "baz" }, { "pqr", "stw" }, - }), nodeMatcher); + }), nodeMatcher, Substitute.For()); IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); @@ -147,7 +168,7 @@ public void TestApply__Loop() { "@aaa *", "2" }, { "bbb", "002" }, new ConfigNode("MM_PATCH_LOOP"), - }), nodeMatcher); + }), nodeMatcher, Substitute.For()); IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); @@ -194,7 +215,7 @@ public void TestApply__Loop() [Fact] public void TestApply__FileNull() { - EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(null, Substitute.For(), Substitute.For()); @@ -206,7 +227,7 @@ public void TestApply__FileNull() [Fact] public void TestApply__ProgressNull() { - EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); @@ -218,7 +239,7 @@ public void TestApply__ProgressNull() [Fact] public void TestApply__LoggerNull() { - EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For()); + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); diff --git a/ModuleManagerTests/Patches/PassSpecifiers/AfterPassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/AfterPassSpecifierTest.cs new file mode 100644 index 00000000..6e086b57 --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/AfterPassSpecifierTest.cs @@ -0,0 +1,105 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class AfterPassSpecifierTest + { + public readonly UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + public readonly AfterPassSpecifier passSpecifier; + + public AfterPassSpecifierTest() + { + passSpecifier = new AfterPassSpecifier("mod1", urlConfig); + } + + [Fact] + public void TestConstructor__ModNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new AfterPassSpecifier(null, urlConfig); + }); + + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestConstructor__ModEmpty() + { + ArgumentException ex = Assert.Throws(delegate + { + new AfterPassSpecifier("", urlConfig); + }); + + Assert.Equal("mod", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); + } + + [Fact] + public void TestConstructor__UrlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new AfterPassSpecifier("mod1", null); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestCheckNeeds__False() + { + needsChecker.CheckNeeds("mod1").Returns(false); + Assert.False(passSpecifier.CheckNeeds(needsChecker, progress)); + + progress.Received().NeedsUnsatisfiedAfter(urlConfig); + } + + [Fact] + public void TestCheckNeeds__True() + { + needsChecker.CheckNeeds("mod1").Returns(true); + Assert.True(passSpecifier.CheckNeeds(needsChecker, progress)); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + } + + [Fact] + public void TestCheckNeeds__NeedsCheckerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(null, progress); + }); + + Assert.Equal("needsChecker", ex.ParamName); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + } + + [Fact] + public void TestCheckNeeds__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(needsChecker, null); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":AFTER[MOD1]", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/PassSpecifiers/BeforePassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/BeforePassSpecifierTest.cs new file mode 100644 index 00000000..937da7c2 --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/BeforePassSpecifierTest.cs @@ -0,0 +1,105 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches.PassSpecifiers +{ + public class BeforePassSpecifierTest + { + public readonly UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + public readonly BeforePassSpecifier passSpecifier; + + public BeforePassSpecifierTest() + { + passSpecifier = new BeforePassSpecifier("mod1", urlConfig); + } + + [Fact] + public void TestConstructor__ModNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new BeforePassSpecifier(null, urlConfig); + }); + + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestConstructor__ModEmpty() + { + ArgumentException ex = Assert.Throws(delegate + { + new BeforePassSpecifier("", urlConfig); + }); + + Assert.Equal("mod", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); + } + + [Fact] + public void TestConstructor__UrlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new BeforePassSpecifier("mod1", null); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestCheckNeeds__False() + { + needsChecker.CheckNeeds("mod1").Returns(false); + Assert.False(passSpecifier.CheckNeeds(needsChecker, progress)); + + progress.Received().NeedsUnsatisfiedBefore(urlConfig); + } + + [Fact] + public void TestCheckNeeds__True() + { + needsChecker.CheckNeeds("mod1").Returns(true); + Assert.True(passSpecifier.CheckNeeds(needsChecker, progress)); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); + } + + [Fact] + public void TestCheckNeeds__NeedsCheckerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(null, progress); + }); + + Assert.Equal("needsChecker", ex.ParamName); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedBefore(null); + } + + [Fact] + public void TestCheckNeeds__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(needsChecker, null); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":BEFORE[MOD1]", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/PassSpecifiers/FinalPassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/FinalPassSpecifierTest.cs new file mode 100644 index 00000000..74b90123 --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/FinalPassSpecifierTest.cs @@ -0,0 +1,52 @@ +using System; +using Xunit; +using NSubstitute; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class FinalPassSpecifierrTest + { + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + private readonly FinalPassSpecifier passSpecifier = new FinalPassSpecifier(); + + [Fact] + public void TestCheckNeeds() + { + Assert.True(passSpecifier.CheckNeeds(needsChecker, progress)); + } + + [Fact] + public void TestCheckNeeds__NeedsCheckerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(null, progress); + }); + + Assert.Equal("needsChecker", ex.ParamName); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + } + + [Fact] + public void TestCheckNeeds__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(needsChecker, null); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":FINAL", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/PassSpecifiers/FirstPassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/FirstPassSpecifierTest.cs new file mode 100644 index 00000000..e8d22151 --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/FirstPassSpecifierTest.cs @@ -0,0 +1,52 @@ +using System; +using Xunit; +using NSubstitute; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class FirstPassSpecifierTest + { + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + private readonly FirstPassSpecifier passSpecifier = new FirstPassSpecifier(); + + [Fact] + public void TestCheckNeeds() + { + Assert.True(passSpecifier.CheckNeeds(needsChecker, progress)); + } + + [Fact] + public void TestCheckNeeds__NeedsCheckerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(null, progress); + }); + + Assert.Equal("needsChecker", ex.ParamName); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + } + + [Fact] + public void TestCheckNeeds__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(needsChecker, null); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":FIRST", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/PassSpecifiers/ForPassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/ForPassSpecifierTest.cs new file mode 100644 index 00000000..cec25628 --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/ForPassSpecifierTest.cs @@ -0,0 +1,105 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class ForPassSpecifierTest + { + public readonly UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + public readonly ForPassSpecifier passSpecifier; + + public ForPassSpecifierTest() + { + passSpecifier = new ForPassSpecifier("mod1", urlConfig); + } + + [Fact] + public void TestConstructor__ModNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new ForPassSpecifier(null, urlConfig); + }); + + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestConstructor__ModEmpty() + { + ArgumentException ex = Assert.Throws(delegate + { + new ForPassSpecifier("", urlConfig); + }); + + Assert.Equal("mod", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); + } + + [Fact] + public void TestConstructor__UrlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new ForPassSpecifier("mod1", null); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestCheckNeeds__False() + { + needsChecker.CheckNeeds("mod1").Returns(false); + Assert.False(passSpecifier.CheckNeeds(needsChecker, progress)); + + progress.Received().NeedsUnsatisfiedFor(urlConfig); + } + + [Fact] + public void TestCheckNeeds__True() + { + needsChecker.CheckNeeds("mod1").Returns(true); + Assert.True(passSpecifier.CheckNeeds(needsChecker, progress)); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); + } + + [Fact] + public void TestCheckNeeds__NeedsCheckerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(null, progress); + }); + + Assert.Equal("needsChecker", ex.ParamName); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedFor(null); + } + + [Fact] + public void TestCheckNeeds__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(needsChecker, null); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":FOR[MOD1]", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/PassSpecifiers/InsertPassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/InsertPassSpecifierTest.cs new file mode 100644 index 00000000..9c6cccf1 --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/InsertPassSpecifierTest.cs @@ -0,0 +1,52 @@ +using System; +using Xunit; +using NSubstitute; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class InsertPassSpecifierTest + { + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + private readonly InsertPassSpecifier passSpecifier = new InsertPassSpecifier(); + + [Fact] + public void TestCheckNeeds() + { + Assert.True(passSpecifier.CheckNeeds(needsChecker, progress)); + } + + [Fact] + public void TestCheckNeeds__NeedsCheckerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(null, progress); + }); + + Assert.Equal("needsChecker", ex.ParamName); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + } + + [Fact] + public void TestCheckNeeds__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(needsChecker, null); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":INSERT (initial)", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/PassSpecifiers/LegacyPassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/LegacyPassSpecifierTest.cs new file mode 100644 index 00000000..e565655c --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/LegacyPassSpecifierTest.cs @@ -0,0 +1,52 @@ +using System; +using Xunit; +using NSubstitute; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class LegacyPassSpecifierTest + { + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + private readonly LegacyPassSpecifier passSpecifier = new LegacyPassSpecifier(); + + [Fact] + public void TestCheckNeeds() + { + Assert.True(passSpecifier.CheckNeeds(needsChecker, progress)); + } + + [Fact] + public void TestCheckNeeds__NeedsCheckerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(null, progress); + }); + + Assert.Equal("needsChecker", ex.ParamName); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedAfter(null); + } + + [Fact] + public void TestCheckNeeds__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + passSpecifier.CheckNeeds(needsChecker, null); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":LEGACY (default)", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/PatchCompilerTest.cs b/ModuleManagerTests/Patches/PatchCompilerTest.cs index 0b401ef3..4484e61b 100644 --- a/ModuleManagerTests/Patches/PatchCompilerTest.cs +++ b/ModuleManagerTests/Patches/PatchCompilerTest.cs @@ -5,6 +5,7 @@ using ModuleManager; using ModuleManager.Logging; using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; namespace ModuleManagerTests.Patches @@ -14,18 +15,27 @@ public class PatchCompilerTest private readonly IPatchProgress progress = Substitute.For(); private readonly IBasicLogger logger = Substitute.For(); private readonly UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + private readonly PatchCompiler patchCompiler = new PatchCompiler(); [Fact] public void TestCompilePatch__Edit() { - UrlDir.UrlConfig patchUrlConfig = UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") - { - { "@bar", "bleh" }, - }); - - EditPatch patch = Assert.IsType(PatchCompiler.CompilePatch(patchUrlConfig, Command.Edit, "NODE[foo]:HAS[#bar]")); - - Assert.Same(patchUrlConfig, patch.UrlConfig); + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") + { + { "@bar", "bleh" }, + }), + Command.Edit, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + + EditPatch patch = Assert.IsType(patchCompiler.CompilePatch(protoPatch)); + + Assert.Same(protoPatch.urlConfig, patch.UrlConfig); AssertNodeMatcher(patch.NodeMatcher); UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") @@ -38,7 +48,7 @@ public void TestCompilePatch__Edit() AssertNoErrors(); - progress.Received().ApplyingUpdate(urlConfig, patchUrlConfig); + progress.Received().ApplyingUpdate(urlConfig, protoPatch.urlConfig); Assert.Equal(1, file.configs.Count); Assert.NotSame(urlConfig, file.configs[0]); @@ -52,15 +62,23 @@ public void TestCompilePatch__Edit() [Fact] public void TestCompilePatch__Copy() { - UrlDir.UrlConfig patchUrlConfig = UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("+NODE") - { - { "@name", "boo" }, - { "@bar", "bleh" }, - }); - - CopyPatch patch = Assert.IsType(PatchCompiler.CompilePatch(patchUrlConfig, Command.Copy, "NODE[foo]:HAS[#bar]")); - - Assert.Same(patchUrlConfig, patch.UrlConfig); + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("+NODE") + { + { "@name", "boo" }, + { "@bar", "bleh" }, + }), + Command.Copy, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + + CopyPatch patch = Assert.IsType(patchCompiler.CompilePatch(protoPatch)); + + Assert.Same(protoPatch.urlConfig, patch.UrlConfig); AssertNodeMatcher(patch.NodeMatcher); UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") @@ -73,7 +91,7 @@ public void TestCompilePatch__Copy() AssertNoErrors(); - progress.Received().ApplyingCopy(urlConfig, patchUrlConfig); + progress.Received().ApplyingCopy(urlConfig, protoPatch.urlConfig); Assert.Equal(2, file.configs.Count); Assert.Same(urlConfig, file.configs[0]); @@ -92,11 +110,19 @@ public void TestCompilePatch__Copy() [Fact] public void TestCompilePatch__Delete() { - UrlDir.UrlConfig patchUrlConfig = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("-NODE")); - - DeletePatch patch = Assert.IsType(PatchCompiler.CompilePatch(patchUrlConfig, Command.Delete, "NODE[foo]:HAS[#bar]")); - - Assert.Same(patchUrlConfig, patch.UrlConfig); + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("-NODE")), + Command.Delete, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + + DeletePatch patch = Assert.IsType(patchCompiler.CompilePatch(protoPatch)); + + Assert.Same(protoPatch.urlConfig, patch.UrlConfig); AssertNodeMatcher(patch.NodeMatcher); UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") @@ -109,102 +135,129 @@ public void TestCompilePatch__Delete() AssertNoErrors(); - progress.Received().ApplyingDelete(urlConfig, patchUrlConfig); + progress.Received().ApplyingDelete(urlConfig, protoPatch.urlConfig); Assert.Equal(0, file.configs.Count); } [Fact] - public void TestCompilePatch__NullUrlConfig() + public void TestCompilePatch__NullProtoPatch() { ArgumentNullException ex = Assert.Throws(delegate { - PatchCompiler.CompilePatch(null, Command.Edit, "NODE[foo]:HAS[#bar]"); + patchCompiler.CompilePatch(null); }); - Assert.Equal("urlConfig", ex.ParamName); - } - - [Fact] - public void TestCompilePatch__NullName() - { - ArgumentNullException ex = Assert.Throws(delegate - { - PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Edit, null); - }); - - Assert.Equal("name", ex.ParamName); - } - - [Fact] - public void TestCompilePatch__BlankName() - { - ArgumentException ex = Assert.Throws(delegate - { - PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Edit, ""); - }); - - Assert.Equal("name", ex.ParamName); - Assert.Contains("can't be empty", ex.Message); + Assert.Equal("protoPatch", ex.ParamName); } [Fact] public void TestCompilePatch__InvalidCommand__Replace() { + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode()), + Command.Replace, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + ArgumentException ex = Assert.Throws(delegate { - PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Replace, "NODE[foo]:HAS[#bar]"); + patchCompiler.CompilePatch(protoPatch); }); - Assert.Equal("command", ex.ParamName); + Assert.Equal("protoPatch", ex.ParamName); Assert.Contains("invalid command for a root node: Replace", ex.Message); } [Fact] public void TestCompilePatch__InvalidCommand__Create() { + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode()), + Command.Create, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + ArgumentException ex = Assert.Throws(delegate { - PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Create, "NODE[foo]:HAS[#bar]"); + patchCompiler.CompilePatch(protoPatch); }); - Assert.Equal("command", ex.ParamName); + Assert.Equal("protoPatch", ex.ParamName); Assert.Contains("invalid command for a root node: Create", ex.Message); } [Fact] public void TestCompilePatch__InvalidCommand__Rename() { + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode()), + Command.Rename, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + ArgumentException ex = Assert.Throws(delegate { - PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Rename, "NODE[foo]:HAS[#bar]"); + patchCompiler.CompilePatch(protoPatch); }); - Assert.Equal("command", ex.ParamName); + Assert.Equal("protoPatch", ex.ParamName); Assert.Contains("invalid command for a root node: Rename", ex.Message); } [Fact] public void TestCompilePatch__InvalidCommand__Paste() { + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode()), + Command.Paste, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + ArgumentException ex = Assert.Throws(delegate { - PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Paste, "NODE[foo]:HAS[#bar]"); + patchCompiler.CompilePatch(protoPatch); }); - Assert.Equal("command", ex.ParamName); + Assert.Equal("protoPatch", ex.ParamName); Assert.Contains("invalid command for a root node: Paste", ex.Message); } [Fact] public void TestCompilePatch__InvalidCommand__Special() { + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode()), + Command.Special, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + ArgumentException ex = Assert.Throws(delegate { - PatchCompiler.CompilePatch(UrlBuilder.CreateConfig(new ConfigNode(), file), Command.Special, "NODE[foo]:HAS[#bar]"); + patchCompiler.CompilePatch(protoPatch); }); - Assert.Equal("command", ex.ParamName); + Assert.Equal("protoPatch", ex.ParamName); Assert.Contains("invalid command for a root node: Special", ex.Message); } diff --git a/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs b/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs new file mode 100644 index 00000000..07477bb4 --- /dev/null +++ b/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs @@ -0,0 +1,1231 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Collections; +using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; +using ModuleManager.Tags; + +namespace ModuleManagerTests.Patches +{ + public class ProtoPatchBuilderTest + { + private readonly IPatchProgress progress; + private readonly ProtoPatchBuilder builder; + private readonly UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); + + public ProtoPatchBuilderTest() + { + progress = Substitute.For(); + builder = new ProtoPatchBuilder(progress); + } + + [Fact] + public void TestBuild__PrimaryValueNull() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__PrimaryValue() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", "stuff", null)); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Equal("stuff", protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Needs() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("NEEDS", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Equal("stuff", protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Needs__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("Needs", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Equal("stuff", protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Needs__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("needs", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Equal("stuff", protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Has() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("HAS", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Equal("stuff", protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Has__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("Has", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Equal("stuff", protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Has__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("has", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Equal("stuff", protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__First() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__First__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("First", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__First__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("first", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Before() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("BEFORE", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + BeforePassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__Before__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("Before", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + BeforePassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__Before__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("before", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + BeforePassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__For() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FOR", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + ForPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__For__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("For", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + ForPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__For__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("for", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + ForPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__After() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("AFTER", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + AfterPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__After__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("After", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + AfterPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__After__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("after", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + AfterPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + Assert.Same(urlConfig, passSpecifier.urlConfig); + } + + [Fact] + public void TestBuild__Final() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FINAL", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Final__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("Final", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Final__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("final", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__Insert__InsertPass() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("NEEDS", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Insert, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Insert, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Equal("stuff", protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__UrlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + builder.Build(null, Command.Edit, Substitute.For()); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestBuild__TagListNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + builder.Build(urlConfig, Command.Edit, null); + }); + + Assert.Equal("tagList", ex.ParamName); + } + + [Fact] + public void TestBuild__PrimaryValueEmpty() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", "", null)); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "empty brackets detected on patch name: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__NodeNameOnInsert() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", "blah", null)); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "name specifier detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__TrailerOnPrimaryTag() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", "stuff", "otherStuff")); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "unrecognized trailer: 'otherStuff' on: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Equal("stuff", protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__TrailerOnSomeTag() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("NEEDS", "stuff", "morestuff") + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "unrecognized trailer: 'morestuff' on: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Equal("stuff", protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__MoreThanOneNeeds() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("NEEDS", "stuff", null), + new Tag("NEEDS", "otherStuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one :NEEDS tag detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Equal("stuff", protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__NullNeeds() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("NEEDS", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :NEEDS tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__EmptyNeeds() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("NEEDS", "", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :NEEDS tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__MoreThanOneHas() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("HAS", "stuff", null), + new Tag("HAS", "otherStuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one :HAS tag detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Equal("stuff", protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__NullHas() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("HAS", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :HAS tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__EmptyHas() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("HAS", "", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :HAS tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__HasOnInsert() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("HAS", "", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, ":HAS detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__BracketsOnFirst() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", "", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "value detected on :FIRST tag: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__ValueOnFirst() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "value detected on :FIRST tag: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__MoreThanOnePass__First() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FINAL", null, null), + new Tag("FIRST", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__PassSpecifierOnInsert__First() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "pass specifier detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__NullBefore() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("BEFORE", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :BEFORE tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__EmptyBefore() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("BEFORE", "", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :BEFORE tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__MoreThanOnePass__Before() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", null, null), + new Tag("BEFORE", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__PassSpecifierOnInsert__Before() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("BEFORE", "mod1", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "pass specifier detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__NullFor() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FOR", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :FOR tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__EmptyFor() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FOR", "", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :FOR tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__MoreThanOnePass__For() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", null, null), + new Tag("FOR", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__PassSpecifierOnInsert__For() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FOR", "mod1", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "pass specifier detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__NullAfter() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("AFTER", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :AFTER tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__EmptyAfter() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("AFTER", "", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :AFTER tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__MoreThanOnePass__After() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", null, null), + new Tag("AFTER", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__PassSpecifierOnInsert__After() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("AFTER", "mod1", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "pass specifier detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__BracketsOnFinal() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FINAL", "", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "value detected on :FINAL tag: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__ValueOnFinal() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FINAL", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "value detected on :FINAL tag: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__MoreThanOnePass__Final() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", null, null), + new Tag("FINAL", null, null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__PassSpecifierOnInsert__Final() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FINAL", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "pass specifier detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__UnrecognizedTag() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("SOMESTUFF", "blah", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "unrecognized tag: 'SOMESTUFF' on: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + private void EnsureNoErrors() + { + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + } + + private void EnsureNoExceptions() + { + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + } +} diff --git a/ModuleManagerTests/Progress/PatchProgressTest.cs b/ModuleManagerTests/Progress/PatchProgressTest.cs index cf650285..caaced90 100644 --- a/ModuleManagerTests/Progress/PatchProgressTest.cs +++ b/ModuleManagerTests/Progress/PatchProgressTest.cs @@ -134,38 +134,34 @@ public void TestNeedsUnsatisfiedRoot() public void TestNeedsUnsatisfiedNode() { UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); - NodeStack stack1 = new NodeStack(config1.config).Push(new ConfigNode("SOME_CHILD_NODE")); UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("SOME_OTHER_NODE")); - NodeStack stack2 = new NodeStack(config2.config).Push(new ConfigNode("SOME_OTHER_CHILD_NODE")); Assert.Equal(0, progress.Counter.needsUnsatisfied); - progress.NeedsUnsatisfiedNode(config1, stack1); + progress.NeedsUnsatisfiedNode(config1, "SOME/NODE/PATH/SOME_CHILD_NODE"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting node in file abc/def subnode: SOME_NODE/SOME_CHILD_NODE as it can't satisfy its NEEDS"); + logger.Received().Log(LogType.Log, "Deleting node in file abc/def subnode: SOME/NODE/PATH/SOME_CHILD_NODE as it can't satisfy its NEEDS"); - progress.NeedsUnsatisfiedNode(config2, stack2); + progress.NeedsUnsatisfiedNode(config2, "SOME/NODE/PATH/SOME_OTHER_CHILD_NODE"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting node in file ghi/jkl subnode: SOME_OTHER_NODE/SOME_OTHER_CHILD_NODE as it can't satisfy its NEEDS"); + logger.Received().Log(LogType.Log, "Deleting node in file ghi/jkl subnode: SOME/NODE/PATH/SOME_OTHER_CHILD_NODE as it can't satisfy its NEEDS"); } [Fact] public void TestNeedsUnsatisfiedValue() { UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); - NodeStack stack1 = new NodeStack(config1.config).Push(new ConfigNode("SOME_CHILD_NODE")); UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("SOME_OTHER_NODE")); - NodeStack stack2 = new NodeStack(config2.config).Push(new ConfigNode("SOME_OTHER_CHILD_NODE")); Assert.Equal(0, progress.Counter.needsUnsatisfied); - progress.NeedsUnsatisfiedValue(config1, stack1, "some_value"); + progress.NeedsUnsatisfiedValue(config1, "SOME/NODE/PATH/some_value"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting value in file abc/def subnode: SOME_NODE/SOME_CHILD_NODE value: some_value as it can't satisfy its NEEDS"); + logger.Received().Log(LogType.Log, "Deleting value in file abc/def value: SOME/NODE/PATH/some_value as it can't satisfy its NEEDS"); - progress.NeedsUnsatisfiedValue(config2, stack2, "some_other_value"); + progress.NeedsUnsatisfiedValue(config2, "SOME/NODE/PATH/some_other_value"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting value in file ghi/jkl subnode: SOME_OTHER_NODE/SOME_OTHER_CHILD_NODE value: some_other_value as it can't satisfy its NEEDS"); + logger.Received().Log(LogType.Log, "Deleting value in file ghi/jkl value: SOME/NODE/PATH/some_other_value as it can't satisfy its NEEDS"); } [Fact] From 845fd212cf7121a33ff68a2d5706126f09391aa8 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 8 Jul 2018 21:39:10 -0700 Subject: [PATCH 020/140] Make extra colons a warning rather than an error Been seeing a lot of these and the correct path is determinate --- ModuleManager/MMPatchLoader.cs | 2 +- ModuleManager/PatchExtractor.cs | 2 +- ModuleManager/Tags/TagListParser.cs | 47 ++++++--- ModuleManagerTests/PatchExtractorTest.cs | 16 +-- ModuleManagerTests/Tags/TagListParserTest.cs | 103 +++++++++++++------ 5 files changed, 111 insertions(+), 59 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 6af7d5d7..882f8e0c 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -167,7 +167,7 @@ private IEnumerator ProcessPatch() UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == ""); INeedsChecker needsChecker = new NeedsChecker(mods, gameData, progress, logger); - ITagListParser tagListParser = new TagListParser(); + ITagListParser tagListParser = new TagListParser(progress); IProtoPatchBuilder protoPatchBuilder = new ProtoPatchBuilder(progress); IPatchCompiler patchCompiler = new PatchCompiler(); PatchExtractor extractor = new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index 6c604576..3306014d 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -79,7 +79,7 @@ public IPatch ExtractPatch(UrlDir.UrlConfig urlConfig) ITagList tagList; try { - tagList = tagListParser.Parse(name); + tagList = tagListParser.Parse(name, urlConfig); } catch (FormatException ex) { diff --git a/ModuleManager/Tags/TagListParser.cs b/ModuleManager/Tags/TagListParser.cs index b714240e..c9ff1eed 100644 --- a/ModuleManager/Tags/TagListParser.cs +++ b/ModuleManager/Tags/TagListParser.cs @@ -1,29 +1,43 @@ using System; using System.Collections.Generic; +using ModuleManager.Progress; namespace ModuleManager.Tags { public interface ITagListParser { - ITagList Parse(string ToParse); + ITagList Parse(string ToParse, UrlDir.UrlConfig urlConfig); } public class TagListParser : ITagListParser { - public ITagList Parse(string toParse) + private readonly IPatchProgress progress; + + public TagListParser(IPatchProgress progress) + { + this.progress = progress ?? throw new ArgumentNullException(nameof(progress)); + } + + public ITagList Parse(string toParse, UrlDir.UrlConfig urlConfig) { if (toParse == null) throw new ArgumentNullException(nameof(toParse)); + if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); if (toParse.Length == 0) throw new FormatException("can't create tag list from empty string"); if (toParse[0] == '[') throw new FormatException("can't create tag list beginning with ["); if (toParse[0] == ':') throw new FormatException("can't create tag list beginning with :"); - if (toParse[toParse.Length - 1] == ':') throw new FormatException("tag list can't end with :"); + + if (toParse[toParse.Length - 1] == ':') + { + progress.Warning(urlConfig, "trailing : detected"); + toParse = toParse.TrimEnd(':'); + } List tags = new List(); - Tag primaryTag = ParsePrimaryTag(toParse, ref tags); + Tag primaryTag = ParsePrimaryTag(toParse, ref tags, urlConfig); return new TagList(primaryTag, tags); } - private static Tag ParsePrimaryTag(string toParse, ref List tags) + private Tag ParsePrimaryTag(string toParse, ref List tags, UrlDir.UrlConfig urlConfig) { for (int i = 1; i < toParse.Length; i++) { @@ -32,11 +46,11 @@ private static Tag ParsePrimaryTag(string toParse, ref List tags) if (c == '[') { int j = ClosingBracketIndex(toParse, i + 1); - return ParsePrimaryTrailer(toParse, j + 1, ref tags, toParse.Substring(0, i), toParse.Substring(i + 1, j - i - 1)); + return ParsePrimaryTrailer(toParse, j + 1, ref tags, toParse.Substring(0, i), toParse.Substring(i + 1, j - i - 1), urlConfig); } else if (c == ':') { - ParseTag(toParse, i + 1, ref tags); + ParseTag(toParse, i + 1, ref tags, urlConfig); return new Tag(toParse.Substring(0, i), null, null); } else if (c == ']') @@ -48,7 +62,7 @@ private static Tag ParsePrimaryTag(string toParse, ref List tags) return new Tag(toParse, null, null); } - private static Tag ParsePrimaryTrailer(string toParse, int start, ref List tags, string primaryKey, string primaryValue) + private Tag ParsePrimaryTrailer(string toParse, int start, ref List tags, string primaryKey, string primaryValue, UrlDir.UrlConfig urlConfig) { for (int i = start; i < toParse.Length; i++) { @@ -57,7 +71,7 @@ private static Tag ParsePrimaryTrailer(string toParse, int start, ref List if (c == ':') { string trailer = i == start ? null : toParse.Substring(start, i - start); - ParseTag(toParse, i + 1, ref tags); + ParseTag(toParse, i + 1, ref tags, urlConfig); return new Tag(primaryKey, primaryValue, trailer); } else if (c == '[') @@ -74,7 +88,7 @@ private static Tag ParsePrimaryTrailer(string toParse, int start, ref List return new Tag(primaryKey, primaryValue, primaryTrailer); } - private static void ParseTag(string toParse, int start, ref List tags) + private void ParseTag(string toParse, int start, ref List tags, UrlDir.UrlConfig urlConfig) { for (int i = start; i < toParse.Length; i++) { @@ -86,16 +100,17 @@ private static void ParseTag(string toParse, int start, ref List tags) throw new FormatException("tag can't start with ["); int j = ClosingBracketIndex(toParse, i + 1); - ParseTrailer(toParse, j + 1, ref tags, toParse.Substring(start, i - start), toParse.Substring(i + 1, j - i - 1)); + ParseTrailer(toParse, j + 1, ref tags, toParse.Substring(start, i - start), toParse.Substring(i + 1, j - i - 1), urlConfig); return; } else if (c == ':') { if (i == start) - throw new FormatException("tag can't start with :"); + progress.Warning(urlConfig, "extra : detected"); + else + tags.Add(new Tag(toParse.Substring(start, i - start), null, null)); - tags.Add(new Tag(toParse.Substring(start, i - start), null, null)); - ParseTag(toParse, i + 1, ref tags); + ParseTag(toParse, i + 1, ref tags, urlConfig); return; } else if (c == ']') @@ -107,7 +122,7 @@ private static void ParseTag(string toParse, int start, ref List tags) tags.Add(new Tag(toParse.Substring(start), null, null)); } - private static void ParseTrailer(string toParse, int start, ref List tags, string key, string value) + private void ParseTrailer(string toParse, int start, ref List tags, string key, string value, UrlDir.UrlConfig urlConfig) { for (int i = start; i < toParse.Length; i++) { @@ -117,7 +132,7 @@ private static void ParseTrailer(string toParse, int start, ref List tags, { string trailer = i == start ? null : toParse.Substring(start, i - start); tags.Add(new Tag(key, value, trailer)); - ParseTag(toParse, i + 1, ref tags); + ParseTag(toParse, i + 1, ref tags, urlConfig); return; } else if (c == '[') diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 7f82de56..e2fab5c0 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -130,7 +130,7 @@ public void TestExtractPatch() UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); ITagList tagList = Substitute.For(); - tagListParser.Parse("NODE_TYPE").Returns(tagList); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); IPassSpecifier passSpecifier = Substitute.For(); ProtoPatch protoPatch = new ProtoPatch( @@ -166,7 +166,7 @@ public void TestExtractPatch__Needs() UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); ITagList tagList = Substitute.For(); - tagListParser.Parse("NODE_TYPE").Returns(tagList); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); IPassSpecifier passSpecifier = Substitute.For(); ProtoPatch protoPatch = new ProtoPatch( @@ -202,7 +202,7 @@ public void TestExtractPatch__NeedsUnsatisfied() UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); ITagList tagList = Substitute.For(); - tagListParser.Parse("NODE_TYPE").Returns(tagList); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); IPassSpecifier passSpecifier = Substitute.For(); ProtoPatch protoPatch = new ProtoPatch( @@ -237,7 +237,7 @@ public void TestExtractPatch__NeedsUnsatisfiedPassSpecifier() UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); ITagList tagList = Substitute.For(); - tagListParser.Parse("NODE_TYPE").Returns(tagList); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); IPassSpecifier passSpecifier = Substitute.For(); ProtoPatch protoPatch = new ProtoPatch( @@ -272,7 +272,7 @@ public void TestExtractPatch__Insert() UrlDir.UrlConfig urlConfig = CreateConfig("NODE_TYPE"); ITagList tagList = Substitute.For(); - tagListParser.Parse("NODE_TYPE").Returns(tagList); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); IPassSpecifier passSpecifier = Substitute.For(); ProtoPatch protoPatch = new ProtoPatch( @@ -298,7 +298,7 @@ public void TestExtractPatch__Insert() Received.InOrder(delegate { - tagListParser.Parse("NODE_TYPE"); + tagListParser.Parse("NODE_TYPE", urlConfig); protoPatchBuilder.Build(urlConfig, Command.Insert, tagList); needsChecker.CheckNeedsExpression("needs"); passSpecifier.CheckNeeds(needsChecker, progress); @@ -410,7 +410,7 @@ public void TestExtractPatch__InvalidCommand__Special() public void TestExtractPatch__TagListBadlyFormatted() { UrlDir.UrlConfig urlConfig = CreateConfig("badSomehow"); - tagListParser.When(t => t.Parse("badSomehow")).Throw(new FormatException("badly formatted")); + tagListParser.When(t => t.Parse("badSomehow", urlConfig)).Throw(new FormatException("badly formatted")); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); Assert.Empty(root.AllConfigs); @@ -435,7 +435,7 @@ public void TestExtractPatch__Exception() { UrlDir.UrlConfig urlConfig = CreateConfig("NODE"); Exception ex = new Exception(); - tagListParser.When(t => t.Parse("NODE")).Throw(ex); + tagListParser.When(t => t.Parse("NODE", urlConfig)).Throw(ex); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); Assert.Empty(root.AllConfigs); diff --git a/ModuleManagerTests/Tags/TagListParserTest.cs b/ModuleManagerTests/Tags/TagListParserTest.cs index b13277e8..9ee395d0 100644 --- a/ModuleManagerTests/Tags/TagListParserTest.cs +++ b/ModuleManagerTests/Tags/TagListParserTest.cs @@ -1,17 +1,38 @@ using System; using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager.Progress; using ModuleManager.Tags; namespace ModuleManagerTests.Tags { public class TagListParserTest { - private readonly TagListParser tagListParser = new TagListParser(); + private readonly IPatchProgress progress = Substitute.For(); + private readonly TagListParser tagListParser; + private readonly UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def.cfg", new ConfigNode("BLAH")); + + public TagListParserTest() + { + tagListParser = new TagListParser(progress); + } + + [Fact] + public void TestConstructor__ProgressNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new TagListParser(null); + }); + + Assert.Equal("progress", ex.ParamName); + } [Fact] public void TestParse__OnlyPrimaryKey() { - ITagList tagList = tagListParser.Parse("01"); + ITagList tagList = tagListParser.Parse("01", urlConfig); Assert.Equal(new Tag("01", null, null), tagList.PrimaryTag); Assert.Empty(tagList); } @@ -19,7 +40,7 @@ public void TestParse__OnlyPrimaryKey() [Fact] public void TestParse__OnlyPrimaryKeyAndValue() { - ITagList tagList = tagListParser.Parse("01[02]"); + ITagList tagList = tagListParser.Parse("01[02]", urlConfig); Assert.Equal(new Tag("01", "02", null), tagList.PrimaryTag); Assert.Empty(tagList); } @@ -27,7 +48,7 @@ public void TestParse__OnlyPrimaryKeyAndValue() [Fact] public void TestParse__OnlyPrimaryKeyValueAndTrailer() { - ITagList tagList = tagListParser.Parse("01[02]03"); + ITagList tagList = tagListParser.Parse("01[02]03", urlConfig); Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); Assert.Empty(tagList); } @@ -35,7 +56,7 @@ public void TestParse__OnlyPrimaryKeyValueAndTrailer() [Fact] public void TestParse__OnlyPrimaryKeyValueAndTrailer__ValueHasSomeStuff() { - ITagList tagList = tagListParser.Parse("01[02:[03:04[05]]]06"); + ITagList tagList = tagListParser.Parse("01[02:[03:04[05]]]06", urlConfig); Assert.Equal(new Tag("01", "02:[03:04[05]]", "06"), tagList.PrimaryTag); Assert.Empty(tagList); } @@ -43,7 +64,7 @@ public void TestParse__OnlyPrimaryKeyValueAndTrailer__ValueHasSomeStuff() [Fact] public void TestParse__TagWithOnlyKey() { - ITagList tagList = tagListParser.Parse("01[02]03:04:05"); + ITagList tagList = tagListParser.Parse("01[02]03:04:05", urlConfig); Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); Assert.Equal(new[] { new Tag("04", null, null), @@ -54,7 +75,7 @@ public void TestParse__TagWithOnlyKey() [Fact] public void TestParse__TagWithOnlyKeyAndValue() { - ITagList tagList = tagListParser.Parse("01[02]03:04[05]:06[07]"); + ITagList tagList = tagListParser.Parse("01[02]03:04[05]:06[07]", urlConfig); Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); Assert.Equal(new[] { new Tag("04", "05", null), @@ -65,7 +86,7 @@ public void TestParse__TagWithOnlyKeyAndValue() [Fact] public void TestParse__FullTags() { - ITagList tagList = tagListParser.Parse("01[02]03:04[05]06:07[08]09"); + ITagList tagList = tagListParser.Parse("01[02]03:04[05]06:07[08]09", urlConfig); Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); Assert.Equal(new[] { new Tag("04", "05", "06"), @@ -76,7 +97,7 @@ public void TestParse__FullTags() [Fact] public void TestParse__MixedTags() { - ITagList tagList = tagListParser.Parse("01[02]03:04:05[06]:07[08]09"); + ITagList tagList = tagListParser.Parse("01[02]03:04:05[06]:07[08]09", urlConfig); Assert.Equal(new Tag("01", "02", "03"), tagList.PrimaryTag); Assert.Equal(new[] { new Tag("04", null, null), @@ -86,22 +107,33 @@ public void TestParse__MixedTags() } [Fact] - public void TestParse__Null() + public void TestParse__StringNull() { ArgumentNullException ex = Assert.Throws(delegate { - tagListParser.Parse(null); + tagListParser.Parse(null, urlConfig); }); Assert.Equal("toParse", ex.ParamName); } + [Fact] + public void TestParse__UrlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + tagListParser.Parse("BLAH", null); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + [Fact] public void TestParse__Empty() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse(""); + tagListParser.Parse("", urlConfig); }); Assert.Equal("can't create tag list from empty string", ex.Message); @@ -112,7 +144,7 @@ public void TestParse__StartsWithOpenBracket() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("[stuff]"); + tagListParser.Parse("[stuff]", urlConfig); }); Assert.Equal("can't create tag list beginning with [", ex.Message); @@ -123,7 +155,7 @@ public void TestParse__StartsWithColon() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse(":stuff"); + tagListParser.Parse(":stuff", urlConfig); }); Assert.Equal("can't create tag list beginning with :", ex.Message); @@ -132,12 +164,14 @@ public void TestParse__StartsWithColon() [Fact] public void TestParse__EndsWithColon() { - FormatException ex = Assert.Throws(delegate - { - tagListParser.Parse("stuff:blah:"); - }); + ITagList tagList = tagListParser.Parse("stuff:blah::", urlConfig); - Assert.Equal("tag list can't end with :", ex.Message); + progress.Received().Warning(urlConfig, "trailing : detected"); + + Assert.Equal(new Tag("stuff", null, null), tagList.PrimaryTag); + Assert.Equal(new[] { + new Tag("blah", null, null), + }, tagList); } [Fact] @@ -145,7 +179,7 @@ public void TestParse__ClosingBracketInPrimaryKey() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc]def"); + tagListParser.Parse("abc]def", urlConfig); }); Assert.Equal("encountered closing bracket in primary key", ex.Message); @@ -156,7 +190,7 @@ public void TestParse__PrimaryValueHasNoClosingBracket() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc[def[ghi]jkl"); + tagListParser.Parse("abc[def[ghi]jkl", urlConfig); }); Assert.Equal("reached end of the tag list without encountering a close bracket", ex.Message); @@ -167,7 +201,7 @@ public void TestParse__OpeningBracketInPrimaryTrailer() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc[def]ghi[jkl]"); + tagListParser.Parse("abc[def]ghi[jkl]", urlConfig); }); Assert.Equal("encountered opening bracket in primary trailer", ex.Message); @@ -178,7 +212,7 @@ public void TestParse__ClosingBracketInPrimaryTrailer() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc[def]ghi]jkl"); + tagListParser.Parse("abc[def]ghi]jkl", urlConfig); }); Assert.Equal("encountered closing bracket in primary trailer", ex.Message); @@ -189,7 +223,7 @@ public void TestParse__TagStartsWithOpenBracket() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc:def:[ghi]"); + tagListParser.Parse("abc:def:[ghi]", urlConfig); }); Assert.Equal("tag can't start with [", ex.Message); @@ -198,12 +232,15 @@ public void TestParse__TagStartsWithOpenBracket() [Fact] public void TestParse__TagStartsWithColon() { - FormatException ex = Assert.Throws(delegate - { - tagListParser.Parse("abc:def::ghi"); - }); + ITagList tagList = tagListParser.Parse("abc:def::ghi", urlConfig); - Assert.Equal("tag can't start with :", ex.Message); + progress.Received().Warning(urlConfig, "extra : detected"); + + Assert.Equal(new Tag("abc", null, null), tagList.PrimaryTag); + Assert.Equal(new[] { + new Tag("def", null, null), + new Tag("ghi", null, null), + }, tagList); } [Fact] @@ -211,7 +248,7 @@ public void TestParse__ClosingBracketInKey() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc:def:ghi]jkl"); + tagListParser.Parse("abc:def:ghi]jkl", urlConfig); }); Assert.Equal("encountered closing bracket in key", ex.Message); @@ -222,7 +259,7 @@ public void TestParse__ValueHasNoClosingBracket() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc:def:ghi[jkl[mno]pqr"); + tagListParser.Parse("abc:def:ghi[jkl[mno]pqr", urlConfig); }); Assert.Equal("reached end of the tag list without encountering a close bracket", ex.Message); @@ -233,7 +270,7 @@ public void TestParse__OpeningBracketInTrailer() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc:def:ghi[jkl]mno[pqr]"); + tagListParser.Parse("abc:def:ghi[jkl]mno[pqr]", urlConfig); }); Assert.Equal("encountered opening bracket in trailer", ex.Message); @@ -244,7 +281,7 @@ public void TestParse__ClosingBracketInTrailer() { FormatException ex = Assert.Throws(delegate { - tagListParser.Parse("abc:def:ghi[jkl]mno]pqr"); + tagListParser.Parse("abc:def:ghi[jkl]mno]pqr", urlConfig); }); Assert.Equal("encountered closing bracket in trailer", ex.Message); From f00933615ce403b866dd0bf5c0a0f2d62d544073 Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Sun, 11 Nov 2018 12:30:32 -0800 Subject: [PATCH 021/140] Implement :LAST[mod] pass --- ModuleManager/ModuleManager.csproj | 1 + ModuleManager/PatchList.cs | 17 ++- .../PassSpecifiers/LastPassSpecifier.cs | 19 +++ ModuleManager/Patches/ProtoPatchBuilder.cs | 31 +++- ModuleManagerTests/ModuleManagerTests.csproj | 1 + ModuleManagerTests/PatchListTest.cs | 44 ++++-- .../PassSpecifiers/LastPassSpecifierTest.cs | 58 +++++++ .../Patches/ProtoPatchBuilderTest.cs | 142 ++++++++++++++++++ 8 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 ModuleManager/Patches/PassSpecifiers/LastPassSpecifier.cs create mode 100644 ModuleManagerTests/Patches/PassSpecifiers/LastPassSpecifierTest.cs diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 57fc26ae..1e5efa63 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -71,6 +71,7 @@ + diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index 6c44d0b3..e600a7c0 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -17,7 +17,8 @@ private class ModPass public readonly Pass beforePass; public readonly Pass forPass; public readonly Pass afterPass; - + public readonly Pass lastPass; + public ModPass(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -27,11 +28,13 @@ public ModPass(string name) beforePass = new Pass($":BEFORE[{this.name}]"); forPass = new Pass($":FOR[{this.name}]"); afterPass = new Pass($":AFTER[{this.name}]"); + lastPass = new Pass($":LAST[{this.name}]"); } public void AddBeforePatch(IPatch patch) => beforePass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); public void AddForPatch(IPatch patch) => forPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); public void AddAfterPatch(IPatch patch) => afterPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); + public void AddLastPatch(IPatch patch) => lastPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); } private class ModPassCollection : IEnumerable @@ -104,6 +107,11 @@ public PatchList(IEnumerable modList, IEnumerable patches, IPatc EnsureMod(afterPassSpecifier.mod); modPasses[afterPassSpecifier.mod].AddAfterPatch(patch); } + else if (patch.PassSpecifier is LastPassSpecifier lastPassSpecifier) + { + EnsureMod(lastPassSpecifier.mod); + modPasses[lastPassSpecifier.mod].AddLastPatch(patch); + } else if (patch.PassSpecifier is FinalPassSpecifier) { finalPatches.Add(patch); @@ -123,7 +131,7 @@ public PatchList(IEnumerable modList, IEnumerable patches, IPatc private IPass[] EnumeratePasses() { - IPass[] result = new IPass[modPasses.Count * 3 + 3]; + IPass[] result = new IPass[modPasses.Count * 4 + 3]; result[0] = firstPatches; result[1] = legacyPatches; @@ -135,6 +143,11 @@ private IPass[] EnumeratePasses() result[i * 3 + 4] = modPasses[i].afterPass; } + for (int i = 0; i < modPasses.Count; i++) + { + result[2 + (modPasses.Count * 3) + i] = modPasses[i].lastPass; + } + result[result.Length - 1] = finalPatches; return result; diff --git a/ModuleManager/Patches/PassSpecifiers/LastPassSpecifier.cs b/ModuleManager/Patches/PassSpecifiers/LastPassSpecifier.cs new file mode 100644 index 00000000..e0d4fcc4 --- /dev/null +++ b/ModuleManager/Patches/PassSpecifiers/LastPassSpecifier.cs @@ -0,0 +1,19 @@ +using System; +using ModuleManager.Progress; + +namespace ModuleManager.Patches.PassSpecifiers +{ + public class LastPassSpecifier : IPassSpecifier + { + public readonly string mod; + + public LastPassSpecifier(string mod) + { + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); + } + + public bool CheckNeeds(INeedsChecker needsChecker, IPatchProgress progress) => true; + public string Descriptor => $":LAST[{mod.ToUpper()}]"; + } +} diff --git a/ModuleManager/Patches/ProtoPatchBuilder.cs b/ModuleManager/Patches/ProtoPatchBuilder.cs index 7979aac3..377855fa 100644 --- a/ModuleManager/Patches/ProtoPatchBuilder.cs +++ b/ModuleManager/Patches/ProtoPatchBuilder.cs @@ -25,7 +25,7 @@ public ProtoPatch Build(UrlDir.UrlConfig urlConfig, Command command, ITagList ta if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); if (tagList == null) throw new ArgumentNullException(nameof(tagList)); if (progress == null) throw new ArgumentNullException(nameof(progress)); - + bool error = false; string nodeType = tagList.PrimaryTag.key; @@ -42,7 +42,7 @@ public ProtoPatch Build(UrlDir.UrlConfig urlConfig, Command command, ITagList ta progress.Warning(urlConfig, "empty brackets detected on patch name: " + urlConfig.SafeUrl()); nodeName = null; } - + if (tagList.PrimaryTag.trailer != null) progress.Warning(urlConfig, "unrecognized trailer: '" + tagList.PrimaryTag.trailer + "' on: " + urlConfig.SafeUrl()); @@ -52,7 +52,7 @@ public ProtoPatch Build(UrlDir.UrlConfig urlConfig, Command command, ITagList ta foreach (Tag tag in tagList) { - if (tag.trailer != null) + if (tag.trailer != null) progress.Warning(urlConfig, "unrecognized trailer: '" + tag.trailer + "' on: " + urlConfig.SafeUrl()); if (tag.key.Equals("NEEDS", StringComparison.CurrentCultureIgnoreCase)) @@ -122,7 +122,7 @@ public ProtoPatch Build(UrlDir.UrlConfig urlConfig, Command command, ITagList ta error = true; continue; } - + if (command == Command.Insert) { progress.Error(urlConfig, "pass specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); @@ -183,6 +183,29 @@ public ProtoPatch Build(UrlDir.UrlConfig urlConfig, Command command, ITagList ta passSpecifier = new AfterPassSpecifier(tag.value, urlConfig); } + else if (tag.key.Equals("LAST", StringComparison.CurrentCultureIgnoreCase)) + { + if (string.IsNullOrEmpty(tag.value)) + { + progress.Error(urlConfig, "empty :LAST tag detected: " + urlConfig.SafeUrl()); + error = true; + continue; + } + + if (command == Command.Insert) + { + progress.Error(urlConfig, "pass specifier detected on insert node (not a patch): " + urlConfig.SafeUrl()); + error = true; + continue; + } + if (passSpecifier != null) + { + progress.Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: " + urlConfig.SafeUrl()); + continue; + } + + passSpecifier = new LastPassSpecifier(tag.value); + } else if (tag.key.Equals("FINAL", StringComparison.CurrentCultureIgnoreCase)) { if (tag.value != null) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 6e1a83bf..654b7839 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -74,6 +74,7 @@ + diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index d28821a6..d21c8c3f 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -107,6 +107,10 @@ public void Test__Lifecycle() Substitute.For(), Substitute.For(), Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), }; UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); @@ -121,21 +125,27 @@ public void Test__Lifecycle() patches[07].PassSpecifier.Returns(new ForPassSpecifier("MOD1", urlConfig)); patches[08].PassSpecifier.Returns(new AfterPassSpecifier("mod1", urlConfig)); patches[09].PassSpecifier.Returns(new AfterPassSpecifier("MOD1", urlConfig)); - patches[10].PassSpecifier.Returns(new BeforePassSpecifier("mod2", urlConfig)); - patches[11].PassSpecifier.Returns(new BeforePassSpecifier("MOD2", urlConfig)); - patches[12].PassSpecifier.Returns(new ForPassSpecifier("mod2", urlConfig)); - patches[13].PassSpecifier.Returns(new ForPassSpecifier("MOD2", urlConfig)); - patches[14].PassSpecifier.Returns(new AfterPassSpecifier("mod2", urlConfig)); - patches[15].PassSpecifier.Returns(new AfterPassSpecifier("MOD2", urlConfig)); - patches[16].PassSpecifier.Returns(new FinalPassSpecifier()); - patches[17].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[10].PassSpecifier.Returns(new LastPassSpecifier("mod1")); + patches[11].PassSpecifier.Returns(new LastPassSpecifier("MOD1")); + patches[12].PassSpecifier.Returns(new BeforePassSpecifier("mod2", urlConfig)); + patches[13].PassSpecifier.Returns(new BeforePassSpecifier("MOD2", urlConfig)); + patches[14].PassSpecifier.Returns(new ForPassSpecifier("mod2", urlConfig)); + patches[15].PassSpecifier.Returns(new ForPassSpecifier("MOD2", urlConfig)); + patches[16].PassSpecifier.Returns(new AfterPassSpecifier("mod2", urlConfig)); + patches[17].PassSpecifier.Returns(new AfterPassSpecifier("MOD2", urlConfig)); + patches[18].PassSpecifier.Returns(new LastPassSpecifier("mod2")); + patches[19].PassSpecifier.Returns(new LastPassSpecifier("MOD2")); + patches[20].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[21].PassSpecifier.Returns(new FinalPassSpecifier()); IPatchProgress progress = Substitute.For(); PatchList patchList = new PatchList(new[] { "mod1", "mod2" }, patches, progress); IPass[] passes = patchList.ToArray(); - + + Assert.Equal(11, passes.Length); + Assert.Equal(":FIRST", passes[0].Name); Assert.Equal(new[] { patches[0], patches[1] }, passes[0]); @@ -152,16 +162,22 @@ public void Test__Lifecycle() Assert.Equal(new[] { patches[8], patches[9] }, passes[4]); Assert.Equal(":BEFORE[MOD2]", passes[5].Name); - Assert.Equal(new[] { patches[10], patches[11] }, passes[5]); + Assert.Equal(new[] { patches[12], patches[13] }, passes[5]); Assert.Equal(":FOR[MOD2]", passes[6].Name); - Assert.Equal(new[] { patches[12], patches[13] }, passes[6]); + Assert.Equal(new[] { patches[14], patches[15] }, passes[6]); Assert.Equal(":AFTER[MOD2]", passes[7].Name); - Assert.Equal(new[] { patches[14], patches[15] }, passes[7]); + Assert.Equal(new[] { patches[16], patches[17] }, passes[7]); + + Assert.Equal(":LAST[MOD1]", passes[8].Name); + Assert.Equal(new[] { patches[10], patches[11] }, passes[8]); + + Assert.Equal(":LAST[MOD2]", passes[9].Name); + Assert.Equal(new[] { patches[18], patches[19] }, passes[9]); - Assert.Equal(":FINAL", passes[8].Name); - Assert.Equal(new[] { patches[16], patches[17] }, passes[8]); + Assert.Equal(":FINAL", passes[10].Name); + Assert.Equal(new[] { patches[20], patches[21] }, passes[10]); progress.Received(patches.Length).PatchAdded(); } diff --git a/ModuleManagerTests/Patches/PassSpecifiers/LastPassSpecifierTest.cs b/ModuleManagerTests/Patches/PassSpecifiers/LastPassSpecifierTest.cs new file mode 100644 index 00000000..bdf6bee5 --- /dev/null +++ b/ModuleManagerTests/Patches/PassSpecifiers/LastPassSpecifierTest.cs @@ -0,0 +1,58 @@ +using System; +using Xunit; +using NSubstitute; +using ModuleManager; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class LastPassSpecifierTest + { + public readonly INeedsChecker needsChecker = Substitute.For(); + public readonly IPatchProgress progress = Substitute.For(); + public readonly LastPassSpecifier passSpecifier; + + public LastPassSpecifierTest() + { + passSpecifier = new LastPassSpecifier("mod1"); + } + + [Fact] + public void TestConstructor__ModNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new LastPassSpecifier(null); + }); + + Assert.Equal("mod", ex.ParamName); + } + + [Fact] + public void TestConstructor__ModEmpty() + { + ArgumentException ex = Assert.Throws(delegate + { + new LastPassSpecifier(""); + }); + + Assert.Equal("mod", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); + } + + [Fact] + public void TestCheckNeeds() + { + passSpecifier.CheckNeeds(needsChecker, progress); + + needsChecker.DidNotReceiveWithAnyArgs().CheckNeeds(null); + } + + [Fact] + public void TestDescriptor() + { + Assert.Equal(":LAST[MOD1]", passSpecifier.Descriptor); + } + } +} diff --git a/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs b/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs index 07477bb4..fd2ff4ab 100644 --- a/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs +++ b/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs @@ -475,6 +475,75 @@ public void TestBuild__After__Case2() Assert.Same(urlConfig, passSpecifier.urlConfig); } + [Fact] + public void TestBuild__Last() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("LAST", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + LastPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + } + + [Fact] + public void TestBuild__Last__Case1() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("Last", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + LastPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + } + + [Fact] + public void TestBuild__Last__Case2() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("last", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + EnsureNoErrors(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + LastPassSpecifier passSpecifier = Assert.IsType(protoPatch.passSpecifier); + Assert.Equal("stuff", passSpecifier.mod); + } + [Fact] public void TestBuild__Final() { @@ -1102,6 +1171,79 @@ public void TestBuild__PassSpecifierOnInsert__After() EnsureNoExceptions(); } + [Fact] + public void TestBuild__NullLast() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("LAST", null, null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :LAST tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__EmptyLast() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("LAST", "", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Copy, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "empty :LAST tag detected: abc/def/NODE"); + EnsureNoExceptions(); + } + + [Fact] + public void TestBuild__MoreThanOnePass__Last() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("FIRST", null, null), + new Tag("LAST", "stuff", null) + )); + + ProtoPatch protoPatch = builder.Build(urlConfig, Command.Copy, tagList); + + progress.Received().Warning(urlConfig, "more than one pass specifier detected, ignoring all but the first: abc/def/NODE"); + progress.DidNotReceiveWithAnyArgs().Error(null, null); + EnsureNoExceptions(); + + Assert.Same(urlConfig, protoPatch.urlConfig); + Assert.Equal(Command.Copy, protoPatch.command); + Assert.Equal("NODE", protoPatch.nodeType); + Assert.Null(protoPatch.nodeName); + Assert.Null(protoPatch.needs); + Assert.Null(protoPatch.has); + Assert.IsType(protoPatch.passSpecifier); + } + + [Fact] + public void TestBuild__PassSpecifierOnInsert__Last() + { + ITagList tagList = Substitute.For(); + tagList.PrimaryTag.Returns(new Tag("NODE", null, null)); + tagList.GetEnumerator().Returns(new ArrayEnumerator( + new Tag("LAST", "mod1", null) + )); + + Assert.Null(builder.Build(urlConfig, Command.Insert, tagList)); + + progress.DidNotReceiveWithAnyArgs().Warning(null, null); + progress.Received().Error(urlConfig, "pass specifier detected on insert node (not a patch): abc/def/NODE"); + EnsureNoExceptions(); + } + [Fact] public void TestBuild__BracketsOnFinal() { From ea89e1a6321aabd8b193969f8a538952b732802a Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Sun, 11 Nov 2018 12:38:00 -0800 Subject: [PATCH 022/140] return iterator rather than array This is only iterated over once, I don't know why it was implemented as an array originally --- ModuleManager/PatchList.cs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index e600a7c0..69c58537 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -125,34 +125,28 @@ public PatchList(IEnumerable modList, IEnumerable patches, IPatc } } - public ArrayEnumerator GetEnumerator() => new ArrayEnumerator(EnumeratePasses()); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private IPass[] EnumeratePasses() + public IEnumerator GetEnumerator() { - IPass[] result = new IPass[modPasses.Count * 4 + 3]; - - result[0] = firstPatches; - result[1] = legacyPatches; + yield return firstPatches; + yield return legacyPatches; - for (int i = 0; i < modPasses.Count; i++) + foreach (ModPass modPass in modPasses) { - result[i * 3 + 2] = modPasses[i].beforePass; - result[i * 3 + 3] = modPasses[i].forPass; - result[i * 3 + 4] = modPasses[i].afterPass; + yield return modPass.beforePass; + yield return modPass.forPass; + yield return modPass.afterPass; } - for (int i = 0; i < modPasses.Count; i++) + foreach (ModPass modPass in modPasses) { - result[2 + (modPasses.Count * 3) + i] = modPasses[i].lastPass; + yield return modPass.lastPass; } - result[result.Length - 1] = finalPatches; - - return result; + yield return finalPatches; } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + private void EnsureMod(string mod) { if (mod == null) throw new ArgumentNullException(nameof(mod)); From 00216d33dcf53034d6c4e593effd84d67e341c49 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Mon, 12 Nov 2018 19:36:54 +0100 Subject: [PATCH 023/140] v3.1.1 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index a27f4b16..a03f38e4 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("3.1.0")] +[assembly: AssemblyVersion("3.1.1")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 6e75d17c8f245e13b8dd7fb5ca271da1e05c9fd1 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 12 Nov 2018 23:37:47 -0800 Subject: [PATCH 024/140] Fix color tag --- ModuleManager/MMPatchLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 882f8e0c..48af7e51 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -693,7 +693,7 @@ private void StatusUpdate(IPatchProgress progress) status = "ModuleManager: " + progress.Counter.patchedNodes + " patch" + (progress.Counter.patchedNodes != 1 ? "es" : "") + " applied"; if (progress.Counter.warnings > 0) - status += ", found " + progress.Counter.warnings + " warning" + (progress.Counter.warnings != 1 ? "s" : "") + ""; + status += ", found " + progress.Counter.warnings + " warning" + (progress.Counter.warnings != 1 ? "s" : "") + ""; if (progress.Counter.errors > 0) status += ", found " + progress.Counter.errors + " error" + (progress.Counter.errors != 1 ? "s" : "") + ""; From 458b4862eeef6f1d97c5683122e799fbe4e64311 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 24 Nov 2018 23:38:58 -0800 Subject: [PATCH 025/140] Clear filesSha before generating sha Otherwise this ends up getting reused for database reloads then generates a bunch of errors due to duplicate keys and doesn't actually detect changes --- ModuleManager/MMPatchLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 882f8e0c..f07755b5 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -436,6 +436,9 @@ private bool IsCacheUpToDate() System.Security.Cryptography.SHA256 sha = System.Security.Cryptography.SHA256.Create(); System.Security.Cryptography.SHA256 filesha = System.Security.Cryptography.SHA256.Create(); UrlDir.UrlFile[] files = GameDatabase.Instance.root.AllConfigFiles.ToArray(); + + filesSha.Clear(); + for (int i = 0; i < files.Length; i++) { // Hash the file path so the checksum change if files are moved From dd851a136149c97acb1630bc74fb5d712985f7e5 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 24 Nov 2018 23:42:55 -0800 Subject: [PATCH 026/140] Don't run setup outside of loading screen KSP tries to re-instantiate all addons ona a database reload. It NREs because the loading screen doesn't exist anymore. None of this stuff needs to be run again. --- ModuleManager/ModuleManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 96a40656..454082d6 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -60,6 +60,12 @@ public static void Log(String s) internal void Awake() { + if (LoadingScreen.Instance == null) + { + Destroy(gameObject); + return; + } + totalTime.Start(); // Allow loading the background in the laoding screen From edc09b68454fc1b4c9f1f378fcfc208fb89d8954 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 00:08:10 -0800 Subject: [PATCH 027/140] Move election check up Really none of this stuff should run if it's not the right instance --- ModuleManager/ModuleManager.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 454082d6..c5cbd28e 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -66,6 +66,16 @@ internal void Awake() return; } + // Ensure that only one copy of the service is run per scene change. + if (loadedInScene || !ElectionAndCheck()) + { + Assembly currentAssembly = Assembly.GetExecutingAssembly(); + Log("Multiple copies of current version. Using the first copy. Version: " + + currentAssembly.GetName().Version); + Destroy(gameObject); + return; + } + totalTime.Start(); // Allow loading the background in the laoding screen @@ -87,16 +97,6 @@ internal void Awake() { textPos = Mathf.Min(textPos, text.rectTransform.localPosition.y); } - - // Ensure that only one copy of the service is run per scene change. - if (loadedInScene || !ElectionAndCheck()) - { - Assembly currentAssembly = Assembly.GetExecutingAssembly(); - Log("Multiple copies of current version. Using the first copy. Version: " + - currentAssembly.GetName().Version); - Destroy(gameObject); - return; - } DontDestroyOnLoad(gameObject); Version v = Assembly.GetExecutingAssembly().GetName().Version; From 9f5aa027d834ff26f01fcdce7c6fd3ce00c8b5a8 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 00:08:44 -0800 Subject: [PATCH 028/140] Make MM a once addon Really shouldn't trigger anyway but just to be sure a duplicate doesn't get created --- ModuleManager/ModuleManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index c5cbd28e..869a3079 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -12,7 +12,7 @@ namespace ModuleManager { - [KSPAddon(KSPAddon.Startup.Instantly, false)] + [KSPAddon(KSPAddon.Startup.Instantly, true)] public class ModuleManager : MonoBehaviour { #region state From 7cb640ac6626f126079572e2a9d1a23b710161aa Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 00:09:23 -0800 Subject: [PATCH 029/140] Get rid of unnecessary using --- ModuleManager/ModuleManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 869a3079..9a2b93a2 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -7,7 +7,6 @@ using System.Reflection; using TMPro; using UnityEngine; -using Debug = UnityEngine.Debug; using ModuleManager.Cats; namespace ModuleManager From 4e2af6775cfc1e0ec7b91007863ad3c8c971b6dc Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 11:08:49 -0800 Subject: [PATCH 030/140] v3.1.2 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index a03f38e4..f14fb3ec 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("3.1.1")] +[assembly: AssemblyVersion("3.1.2")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 357259df4184473489420e91c32bfa5b06bb74be Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 8 Jan 2019 20:00:23 +0100 Subject: [PATCH 031/140] Workaround for 1.6.0 PartDatabase rebuild --- ModuleManager/Fix16.cs | 81 ++++++++++++++++++++++++++++++ ModuleManager/ModuleManager.cs | 7 +++ ModuleManager/ModuleManager.csproj | 1 + 3 files changed, 89 insertions(+) create mode 100644 ModuleManager/Fix16.cs diff --git a/ModuleManager/Fix16.cs b/ModuleManager/Fix16.cs new file mode 100644 index 00000000..ab20e6ae --- /dev/null +++ b/ModuleManager/Fix16.cs @@ -0,0 +1,81 @@ +using System.Collections; + +namespace ModuleManager +{ + class Fix16 : LoadingSystem + { + private void Awake() + { + if (Instance != null) + { + DestroyImmediate(this); + return; + } + + Instance = this; + DontDestroyOnLoad(gameObject); + } + + private static Fix16 Instance { get; set; } + + private bool ready; + + private int count; + + private int current; + + private const int yieldStep = 20; + + public override bool IsReady() + { + return ready; + } + + public override string ProgressTitle() + { + return "Fix 1.6.0 " + current + "/" + count; + } + + public override float ProgressFraction() + { + return (float) current / count; + } + + public override void StartLoad() + { + ready = false; + + count = PartLoader.LoadedPartsList.Count; + + StartCoroutine(DoFix()); + } + + private IEnumerator DoFix() + { + int yieldCounter = 0; + for (current = 0; current < count; current++) + { + AvailablePart avp = PartLoader.LoadedPartsList[current]; + if (avp.partPrefab.dragModel == Part.DragModel.CUBE && !avp.partPrefab.DragCubes.Procedural && + !avp.partPrefab.DragCubes.None && avp.partPrefab.DragCubes.Cubes.Count == 0) + { + DragCubeSystem.Instance.LoadDragCubes(avp.partPrefab); + } + + if (yieldCounter++ >= yieldStep) + { + yieldCounter = 0; + yield return null; + } + } + + ready = true; + yield return null; + } + + public override float LoadWeight() + { + return 0.1f; + } + } +} diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 9a2b93a2..2509855c 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -128,6 +128,13 @@ internal void Awake() int gameDatabaseIndex = list.FindIndex(s => s is GameDatabase); list.Insert(gameDatabaseIndex + 1, loader); + + // Workaround for 1.6.0 Editor bug after a PartDatabase rebuild. + if (Versioning.version_major == 1 && Versioning.version_minor == 6 && Versioning.Revision == 0) + { + Fix16 fix16 = aGameObject.AddComponent(); + list.Add(fix16); + } } bool foolsDay = (DateTime.Now.Month == 4 && DateTime.Now.Day == 1); diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 1e5efa63..6aef2e69 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -31,6 +31,7 @@ False + From 716cbd1e414c7f75b23d8add94de9c21cd11bc94 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 8 Jan 2019 20:03:07 +0100 Subject: [PATCH 032/140] v3.1.3 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index f14fb3ec..d3094e3a 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("3.1.2")] +[assembly: AssemblyVersion("3.1.3")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From b605a7345aed428c1f1d64856329fcdf722c2391 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 23:38:28 -0800 Subject: [PATCH 033/140] Update NuGet packages where available Others require .NET 4 --- ModuleManagerTests/ModuleManagerTests.csproj | 8 ++++---- ModuleManagerTests/packages.config | 4 ++-- TestUtilsTests/TestUtilsTests.csproj | 8 ++++---- TestUtilsTests/packages.config | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 654b7839..234e4441 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -1,7 +1,7 @@  - - + + Debug @@ -116,7 +116,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + \ No newline at end of file diff --git a/ModuleManagerTests/packages.config b/ModuleManagerTests/packages.config index dc40245d..1db300e6 100644 --- a/ModuleManagerTests/packages.config +++ b/ModuleManagerTests/packages.config @@ -2,6 +2,6 @@ - - + + \ No newline at end of file diff --git a/TestUtilsTests/TestUtilsTests.csproj b/TestUtilsTests/TestUtilsTests.csproj index 1e7d65dd..e59fb6d5 100644 --- a/TestUtilsTests/TestUtilsTests.csproj +++ b/TestUtilsTests/TestUtilsTests.csproj @@ -1,7 +1,7 @@  - - + + Debug @@ -69,7 +69,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + \ No newline at end of file diff --git a/TestUtilsTests/packages.config b/TestUtilsTests/packages.config index cf3669ce..a0f40e78 100644 --- a/TestUtilsTests/packages.config +++ b/TestUtilsTests/packages.config @@ -1,6 +1,6 @@  - - + + \ No newline at end of file From 29932505aafa752c4a3bb7eb1cd51491118c9fd1 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 23:46:53 -0800 Subject: [PATCH 034/140] Accessory projects on .NET 4.7.1 Visual Studio apparently requires .NET 4 to run tests now. Main project stays on 3.5 since KSP requires that. --- ModuleManagerTests/ModuleManagerTests.csproj | 5 ++++- ModuleManagerTests/packages.config | 4 ++-- TestUtils/TestUtils.csproj | 5 ++++- TestUtilsTests/TestUtilsTests.csproj | 5 ++++- TestUtilsTests/packages.config | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 234e4441..2ca88a71 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -11,10 +11,11 @@ Properties ModuleManagerTests ModuleManagerTests - v3.5 + v4.7.1 512 + true @@ -24,6 +25,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -32,6 +34,7 @@ TRACE prompt 4 + false diff --git a/ModuleManagerTests/packages.config b/ModuleManagerTests/packages.config index 1db300e6..9edbb289 100644 --- a/ModuleManagerTests/packages.config +++ b/ModuleManagerTests/packages.config @@ -1,7 +1,7 @@  - + - + \ No newline at end of file diff --git a/TestUtils/TestUtils.csproj b/TestUtils/TestUtils.csproj index 06ab8be0..10073bd0 100644 --- a/TestUtils/TestUtils.csproj +++ b/TestUtils/TestUtils.csproj @@ -9,8 +9,9 @@ Properties TestUtils TestUtils - v3.5 + v4.7.1 512 + true @@ -20,6 +21,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -28,6 +30,7 @@ TRACE prompt 4 + false diff --git a/TestUtilsTests/TestUtilsTests.csproj b/TestUtilsTests/TestUtilsTests.csproj index e59fb6d5..8422b948 100644 --- a/TestUtilsTests/TestUtilsTests.csproj +++ b/TestUtilsTests/TestUtilsTests.csproj @@ -11,10 +11,11 @@ Properties TestUtilsTests TestUtilsTests - v3.5 + v4.7.1 512 + true @@ -24,6 +25,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -32,6 +34,7 @@ TRACE prompt 4 + false diff --git a/TestUtilsTests/packages.config b/TestUtilsTests/packages.config index a0f40e78..95d3d31b 100644 --- a/TestUtilsTests/packages.config +++ b/TestUtilsTests/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file From 9a9bd677dadd99e360490bb2b110522b3ca3b1bd Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 23:50:06 -0800 Subject: [PATCH 035/140] Update NSubstitute to latest --- ModuleManagerTests/ModuleManagerTests.csproj | 16 ++++++++++++++-- ModuleManagerTests/app.config | 15 +++++++++++++++ ModuleManagerTests/packages.config | 5 ++++- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 ModuleManagerTests/app.config diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 2ca88a71..722bcfe7 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -38,11 +38,22 @@ - - ..\packages\NSubstitute.2.0.3\lib\net35\NSubstitute.dll + + ..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll + + + + ..\packages\NSubstitute.3.1.0\lib\net46\NSubstitute.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + @@ -99,6 +110,7 @@ + diff --git a/ModuleManagerTests/app.config b/ModuleManagerTests/app.config new file mode 100644 index 00000000..1d152980 --- /dev/null +++ b/ModuleManagerTests/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ModuleManagerTests/packages.config b/ModuleManagerTests/packages.config index 9edbb289..2c63a510 100644 --- a/ModuleManagerTests/packages.config +++ b/ModuleManagerTests/packages.config @@ -1,6 +1,9 @@  - + + + + From bb858604b6cdd5f1369ce0b7cad8819e355b133c Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 25 Nov 2018 23:58:27 -0800 Subject: [PATCH 036/140] Update Xunit --- ModuleManagerTests/ModuleManagerTests.csproj | 21 ++++++++++++++++++-- ModuleManagerTests/packages.config | 8 +++++++- TestUtilsTests/TestUtilsTests.csproj | 21 ++++++++++++++++++-- TestUtilsTests/packages.config | 8 +++++++- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 722bcfe7..1e179f0c 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -1,5 +1,6 @@  + @@ -59,8 +60,18 @@ - - ..\packages\xunit.1.9.2\lib\net20\xunit.dll + + ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll @@ -126,6 +137,9 @@ TestUtils + + + @@ -133,5 +147,8 @@ + + + \ No newline at end of file diff --git a/ModuleManagerTests/packages.config b/ModuleManagerTests/packages.config index 2c63a510..dc5635be 100644 --- a/ModuleManagerTests/packages.config +++ b/ModuleManagerTests/packages.config @@ -4,7 +4,13 @@ - + + + + + + + \ No newline at end of file diff --git a/TestUtilsTests/TestUtilsTests.csproj b/TestUtilsTests/TestUtilsTests.csproj index 8422b948..619e6bb4 100644 --- a/TestUtilsTests/TestUtilsTests.csproj +++ b/TestUtilsTests/TestUtilsTests.csproj @@ -1,5 +1,6 @@  + @@ -45,8 +46,18 @@ - - ..\packages\xunit.1.9.2\lib\net20\xunit.dll + + ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll @@ -67,6 +78,9 @@ + + + @@ -74,5 +88,8 @@ + + + \ No newline at end of file diff --git a/TestUtilsTests/packages.config b/TestUtilsTests/packages.config index 95d3d31b..bbb4cb82 100644 --- a/TestUtilsTests/packages.config +++ b/TestUtilsTests/packages.config @@ -1,6 +1,12 @@  - + + + + + + + \ No newline at end of file From 0968853237bbf2341f14a9456a2417a982efc1fb Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 26 Nov 2018 00:01:25 -0800 Subject: [PATCH 037/140] Fix Xunit warnings --- ModuleManagerTests/DummyTest.cs | 2 +- ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs | 1 + ModuleManagerTests/PatchExtractorTest.cs | 2 +- ModuleManagerTests/Patches/CopyPatchTest.cs | 2 +- ModuleManagerTests/Patches/EditPatchTest.cs | 2 +- ModuleManagerTests/Patches/PatchCompilerTest.cs | 4 ++-- TestUtilsTests/DummyTest.cs | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ModuleManagerTests/DummyTest.cs b/ModuleManagerTests/DummyTest.cs index d9fc0624..50783efd 100644 --- a/ModuleManagerTests/DummyTest.cs +++ b/ModuleManagerTests/DummyTest.cs @@ -8,7 +8,7 @@ public class DummyTest [Fact] public void PassingTest() { - Assert.Equal(true, true); + Assert.True(true); } } } diff --git a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs index e82971af..7e4030ad 100644 --- a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs @@ -8,6 +8,7 @@ namespace ModuleManagerTests.Extensions { public class ConfigNodeExtensionsTest { + [Fact] public void TestShallowCopyFrom() { ConfigNode fromNode = new TestConfigNode("SOME_NODE") diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index e2fab5c0..a7e2f0a0 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -307,7 +307,7 @@ public void TestExtractPatch__Insert() patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); - Assert.Equal(1, root.AllConfigs.Count()); + Assert.Single(root.AllConfigs); UrlDir.UrlConfig newUrlConfig = root.AllConfigs.First(); Assert.NotSame(urlConfig, newUrlConfig); Assert.NotSame(urlConfig.config, newUrlConfig.config); diff --git a/ModuleManagerTests/Patches/CopyPatchTest.cs b/ModuleManagerTests/Patches/CopyPatchTest.cs index 70eddd6e..635e5e22 100644 --- a/ModuleManagerTests/Patches/CopyPatchTest.cs +++ b/ModuleManagerTests/Patches/CopyPatchTest.cs @@ -232,7 +232,7 @@ public void TestApply__NameNotChanged() patch.Apply(file, progress, logger); - Assert.Equal(1, file.configs.Count); + Assert.Single(file.configs); Assert.Same(urlConfig, file.configs[0]); AssertNodesEqual(new TestConfigNode("NODE") diff --git a/ModuleManagerTests/Patches/EditPatchTest.cs b/ModuleManagerTests/Patches/EditPatchTest.cs index d0afbc24..e72963f1 100644 --- a/ModuleManagerTests/Patches/EditPatchTest.cs +++ b/ModuleManagerTests/Patches/EditPatchTest.cs @@ -178,7 +178,7 @@ public void TestApply__Loop() patch.Apply(file, progress, logger); - Assert.Equal(1, file.configs.Count); + Assert.Single(file.configs); Assert.NotSame(urlConfig, file.configs[0]); AssertNodesEqual(new TestConfigNode("NODE") { diff --git a/ModuleManagerTests/Patches/PatchCompilerTest.cs b/ModuleManagerTests/Patches/PatchCompilerTest.cs index 4484e61b..5e660876 100644 --- a/ModuleManagerTests/Patches/PatchCompilerTest.cs +++ b/ModuleManagerTests/Patches/PatchCompilerTest.cs @@ -50,7 +50,7 @@ public void TestCompilePatch__Edit() progress.Received().ApplyingUpdate(urlConfig, protoPatch.urlConfig); - Assert.Equal(1, file.configs.Count); + Assert.Single(file.configs); Assert.NotSame(urlConfig, file.configs[0]); AssertNodesEqual(new TestConfigNode("NODE") { @@ -137,7 +137,7 @@ public void TestCompilePatch__Delete() progress.Received().ApplyingDelete(urlConfig, protoPatch.urlConfig); - Assert.Equal(0, file.configs.Count); + Assert.Empty(file.configs); } [Fact] diff --git a/TestUtilsTests/DummyTest.cs b/TestUtilsTests/DummyTest.cs index 28c10410..815d8fb6 100644 --- a/TestUtilsTests/DummyTest.cs +++ b/TestUtilsTests/DummyTest.cs @@ -8,7 +8,7 @@ public class DummyTest [Fact] public void PassingTest() { - Assert.Equal(true, true); + Assert.True(true); } } } From e03db37b1605fe8496b3456f0af5a7a752439442 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 24 Mar 2018 12:09:39 -0700 Subject: [PATCH 038/140] implement stream logger log to a stream --- ModuleManager/Logging/StreamLogger.cs | 69 ++++++ ModuleManager/ModuleManager.csproj | 1 + .../Logging/StreamLoggerTest.cs | 225 ++++++++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 4 files changed, 296 insertions(+) create mode 100644 ModuleManager/Logging/StreamLogger.cs create mode 100644 ModuleManagerTests/Logging/StreamLoggerTest.cs diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs new file mode 100644 index 00000000..4da737e1 --- /dev/null +++ b/ModuleManager/Logging/StreamLogger.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using UnityEngine; + +namespace ModuleManager.Logging +{ + public class StreamLogger : IBasicLogger, IDisposable + { + private readonly Stream stream; + private readonly StreamWriter streamWriter; + private readonly IBasicLogger exceptionLogger; + private bool disposed = false; + + public StreamLogger(Stream stream, IBasicLogger exceptionLogger) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + if (!stream.CanWrite) throw new ArgumentException("must be writable", nameof(stream)); + streamWriter = new StreamWriter(stream); + this.exceptionLogger = exceptionLogger ?? throw new ArgumentNullException(nameof(exceptionLogger)); + } + + public void Log(LogType logType, string message) + { + if (disposed) throw new InvalidOperationException("Object has already been disposed"); + try + { + string prefix; + if (logType == LogType.Log) + prefix = "LOG"; + else if (logType == LogType.Warning) + prefix = "WRN"; + else if (logType == LogType.Error) + prefix = "ERR"; + else if (logType == LogType.Assert) + prefix = "AST"; + else if (logType == LogType.Exception) + prefix = "EXC"; + else + prefix = "UNK"; + + streamWriter.WriteLine("[{0} {1}] {2}", prefix, DateTime.Now.ToString(), message); + } + catch (Exception e) + { + exceptionLogger.Exception("Exception while attempting to log to stream", e); + } + } + + public void Exception(string message, Exception exception) + { + Log(LogType.Exception, exception?.ToString() ?? ""); + } + + public void Dispose() + { + try + { + // Flushes and closes the StreamWriter and the underlying stream + streamWriter.Close(); + } + catch(Exception e) + { + exceptionLogger.Exception("Exception while attempting to close stream writer", e); + } + + disposed = true; + } + } +} diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 6aef2e69..52efc4e8 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -47,6 +47,7 @@ + diff --git a/ModuleManagerTests/Logging/StreamLoggerTest.cs b/ModuleManagerTests/Logging/StreamLoggerTest.cs new file mode 100644 index 00000000..2b9f8733 --- /dev/null +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -0,0 +1,225 @@ +using System; +using System.IO; +using UnityEngine; +using Xunit; +using NSubstitute; +using ModuleManager.Logging; + +namespace ModuleManagerTests.Logging +{ + public class StreamLoggerTest + { + [Fact] + public void TestConstructor__StreamNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new StreamLogger(null, Substitute.For()); + }); + + Assert.Equal("stream", ex.ParamName); + } + + [Fact] + public void TestConstructor__CantWrite() + { + using (MemoryStream stream = new MemoryStream(new byte[0], false)) + { + ArgumentException ex = Assert.Throws(delegate + { + new StreamLogger(stream, Substitute.For()); + }); + + Assert.Equal("stream", ex.ParamName); + Assert.Contains("must be writable", ex.Message); + } + } + + [Fact] + public void TestConstructor__ExceptionLoggerNull() + { + using (MemoryStream stream = new MemoryStream(new byte[0], true)) + { + ArgumentNullException ex = Assert.Throws(delegate + { + new StreamLogger(stream, null); + }); + + Assert.Equal("exceptionLogger", ex.ParamName); + } + } + + [Fact] + public void TestLog__AlreadyDisposed() + { + using (MemoryStream stream = new MemoryStream(new byte[0], true)) + { + StreamLogger streamLogger = new StreamLogger(stream, Substitute.For()); + streamLogger.Dispose(); + + InvalidOperationException ex = Assert.Throws(delegate + { + streamLogger.Log(LogType.Log, "a message"); + }); + + Assert.Contains("Object has already been disposed", ex.Message); + } + } + + [Fact] + public void TestLog__Log() + { + IBasicLogger exceptionLogger = Substitute.For(); + byte[] bytes = new byte[50]; + using (MemoryStream stream = new MemoryStream(bytes, true)) + { + using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + { + streamLogger.Log(LogType.Log, "a message"); + } + } + + exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + Assert.Contains("[LOG ", result); + Assert.Contains("] a message", result); + } + } + } + + [Fact] + public void TestLog__Assert() + { + IBasicLogger exceptionLogger = Substitute.For(); + byte[] bytes = new byte[50]; + using (MemoryStream stream = new MemoryStream(bytes, true)) + { + using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + { + streamLogger.Log(LogType.Assert, "a message"); + } + } + + exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + Assert.Contains("[AST ", result); + Assert.Contains("] a message", result); + } + } + } + + [Fact] + public void TestLog__Warning() + { + IBasicLogger exceptionLogger = Substitute.For(); + byte[] bytes = new byte[50]; + using (MemoryStream stream = new MemoryStream(bytes, true)) + { + using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + { + streamLogger.Log(LogType.Warning, "a message"); + } + } + + exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + Assert.Contains("[WRN ", result); + Assert.Contains("] a message", result); + } + } + } + + [Fact] + public void TestLog__Error() + { + IBasicLogger exceptionLogger = Substitute.For(); + byte[] bytes = new byte[50]; + using (MemoryStream stream = new MemoryStream(bytes, true)) + { + using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + { + streamLogger.Log(LogType.Error, "a message"); + } + } + + exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + Assert.Contains("[ERR ", result); + Assert.Contains("] a message", result); + } + } + } + + [Fact] + public void TestLog__Exception() + { + IBasicLogger exceptionLogger = Substitute.For(); + byte[] bytes = new byte[50]; + using (MemoryStream stream = new MemoryStream(bytes, true)) + { + using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + { + streamLogger.Log(LogType.Exception, "a message"); + } + } + + exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + Assert.Contains("[EXC ", result); + Assert.Contains("] a message", result); + } + } + } + + [Fact] + public void TestLog__Unknown() + { + IBasicLogger exceptionLogger = Substitute.For(); + byte[] bytes = new byte[50]; + using (MemoryStream stream = new MemoryStream(bytes, true)) + { + using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + { + streamLogger.Log((LogType)1000, "a message"); + } + } + + exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + Assert.Contains("[UNK ", result); + Assert.Contains("] a message", result); + } + } + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 1e179f0c..6272e879 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -78,6 +78,7 @@ + From 16d88d209d5f1cc5f22061d2e9e065122d1f00a9 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 21 Oct 2018 22:50:28 -0700 Subject: [PATCH 039/140] Unnecessary using --- ModuleManager/Progress/IPatchProgress.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ModuleManager/Progress/IPatchProgress.cs b/ModuleManager/Progress/IPatchProgress.cs index fe03995d..4edd07c2 100644 --- a/ModuleManager/Progress/IPatchProgress.cs +++ b/ModuleManager/Progress/IPatchProgress.cs @@ -1,5 +1,4 @@ using System; -using NodeStack = ModuleManager.Collections.ImmutableStack; namespace ModuleManager.Progress { From 376b71fe5cdee0ea1410bb4d63763ed00bc251c9 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 22 Oct 2018 00:42:28 -0700 Subject: [PATCH 040/140] These are already run by MMPatchLoader No need to explicitly run them again on database reload --- ModuleManager/ModuleManager.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 2509855c..1d20a586 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -334,9 +334,6 @@ private IEnumerator DataBaseReloadWithMM(bool dump = false) while (!MMPatchLoader.Instance.IsReady()) yield return null; - PartResourceLibrary.Instance.LoadDefinitions(); - - PartUpgradeManager.Handler.FillUpgrades(); if (dump) OutputAllConfigs(); From e1a2be8b7b7dfab2c1c9f1acc0a96cc6d3696b34 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 22 Oct 2018 22:02:49 -0700 Subject: [PATCH 041/140] Extract post patch Now its own loading system Extract test runner and add tests for it (meta!) --- ModuleManager/MMPatchLoader.cs | 153 +-------------- ModuleManager/ModuleManager.cs | 15 +- ModuleManager/ModuleManager.csproj | 2 + ModuleManager/ModuleManagerPostPatch.cs | 154 +++++++++++++++ ModuleManager/ModuleManagerTestRunner.cs | 75 ++++++++ ModuleManagerTests/InGameTestRunnerTest.cs | 188 +++++++++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 7 files changed, 432 insertions(+), 156 deletions(-) create mode 100644 ModuleManager/ModuleManagerPostPatch.cs create mode 100644 ModuleManager/ModuleManagerTestRunner.cs create mode 100644 ModuleManagerTests/InGameTestRunnerTest.cs diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index ca782e0c..4df7667e 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -23,8 +23,6 @@ namespace ModuleManager { - public delegate void ModuleManagerPostPatchCallback(); - [SuppressMessage("ReSharper", "StringLastIndexOfIsCultureSpecific.1")] [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] public class MMPatchLoader : LoadingSystem @@ -57,8 +55,6 @@ public class MMPatchLoader : LoadingSystem private string configSha; private Dictionary filesSha = new Dictionary(); - private static readonly List postPatchCallbacks = new List(); - private const float yieldInterval = 1f/30f; // Patch at ~30fps private IBasicLogger logger; @@ -75,7 +71,6 @@ private void Awake() return; } Instance = this; - DontDestroyOnLoad(gameObject); cachePath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigCache"); techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree"); @@ -115,8 +110,7 @@ public override void StartLoad() public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) { - if (!postPatchCallbacks.Contains(callback)) - postPatchCallbacks.Add(callback); + PostPatchLoader.AddPostPatchCallback(callback); } private IEnumerator ProcessPatch() @@ -303,91 +297,6 @@ float updateTimeRemaining() logger.Info(status + "\n" + errors); -#if DEBUG - RunTestCases(); -#endif - - // TODO : Remove if we ever get a way to load sooner - logger.Info("Reloading resources definitions"); - PartResourceLibrary.Instance.LoadDefinitions(); - - logger.Info("Reloading Trait configs"); - GameDatabase.Instance.ExperienceConfigs.LoadTraitConfigs(); - - logger.Info("Reloading Part Upgrades"); - PartUpgradeManager.Handler.FillUpgrades(); - - foreach (ModuleManagerPostPatchCallback callback in postPatchCallbacks) - { - try - { - callback(); - } - catch (Exception e) - { - logger.Exception("Exception while running a post patch callback", e); - } - yield return null; - } - yield return null; - - // Call all "public static void ModuleManagerPostLoad()" on all class - foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies()) - { - try - { - foreach (Type type in ass.GetTypes()) - { - MethodInfo method = type.GetMethod("ModuleManagerPostLoad", BindingFlags.Public | BindingFlags.Static); - - if (method != null && method.GetParameters().Length == 0) - { - try - { - logger.Info("Calling " + ass.GetName().Name + "." + type.Name + "." + method.Name + "()"); - method.Invoke(null, null); - } - catch (Exception e) - { - logger.Exception("Exception while calling " + ass.GetName().Name + "." + type.Name + "." + method.Name + "()", e); - } - } - } - } - catch (Exception e) - { - logger.Exception("Post run call threw an exception in loading " + ass.FullName, e); - } - } - - yield return null; - - // Call "public void ModuleManagerPostLoad()" on all active MonoBehaviour instance - foreach (MonoBehaviour obj in FindObjectsOfType()) - { - MethodInfo method = obj.GetType().GetMethod("ModuleManagerPostLoad", BindingFlags.Public | BindingFlags.Instance); - - if (method != null && method.GetParameters().Length == 0) - { - try - { - logger.Info("Calling " + obj.GetType().Name + "." + method.Name + "()"); - method.Invoke(obj, null); - } - catch (Exception e) - { - logger.Exception("Exception while calling " + obj.GetType().Name + "." + method.Name + "() :\n", e); - } - } - } - - yield return null; - - if (ModuleManager.dumpPostPatch) - ModuleManager.OutputAllConfigs(); - - yield return null; - patchSw.Stop(); logger.Info("Ran in " + ((float)patchSw.ElapsedMilliseconds / 1000).ToString("F3") + "s"); @@ -1935,66 +1844,6 @@ private static ConfigNode.Value FindValueIn(ConfigNode newNode, string valName, return v; } - private static bool CompareRecursive(ConfigNode expectNode, ConfigNode gotNode) - { - if (expectNode.values.Count != gotNode.values.Count || expectNode.nodes.Count != gotNode.nodes.Count) - return false; - for (int i = 0; i < expectNode.values.Count; ++i) - { - ConfigNode.Value eVal = expectNode.values[i]; - ConfigNode.Value gVal = gotNode.values[i]; - if (eVal.name != gVal.name || eVal.value != gVal.value) - return false; - } - for (int i = 0; i < expectNode.nodes.Count; ++i) - { - ConfigNode eNode = expectNode.nodes[i]; - ConfigNode gNode = gotNode.nodes[i]; - if (!CompareRecursive(eNode, gNode)) - return false; - } - return true; - } - #endregion Config Node Utilities - - #region Tests - - private void RunTestCases() - { - logger.Info("Running tests..."); - - // Do MM testcases - foreach (UrlDir.UrlConfig expect in GameDatabase.Instance.GetConfigs("MMTEST_EXPECT")) - { - // So for each of the expects, we expect all the configs before that node to match exactly. - UrlDir.UrlFile parent = expect.parent; - if (parent.configs.Count != expect.config.CountNodes + 1) - { - logger.Error("Test " + parent.name + " failed as expected number of nodes differs expected:" + - expect.config.CountNodes + " found: " + parent.configs.Count); - for (int i = 0; i < parent.configs.Count; ++i) - logger.Info(parent.configs[i].config.ToString()); - continue; - } - for (int i = 0; i < expect.config.CountNodes; ++i) - { - ConfigNode gotNode = parent.configs[i].config; - ConfigNode expectNode = expect.config.nodes[i]; - if (!CompareRecursive(expectNode, gotNode)) - { - logger.Error("Test " + parent.name + "[" + i + - "] failed as expected output and actual output differ.\nexpected:\n" + expectNode + - "\nActually got:\n" + gotNode); - } - } - - // Purge the tests - parent.configs.Clear(); - } - logger.Info("tests complete."); - } - - #endregion Tests } } diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 1d20a586..8ed242a4 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -121,13 +121,16 @@ internal void Awake() // We could insert ModuleManager after GameDatabase to get it to run there // and SaveGameFixer after PartLoader. + int gameDatabaseIndex = list.FindIndex(s => s is GameDatabase); + GameObject aGameObject = new GameObject("ModuleManager"); - MMPatchLoader loader = aGameObject.AddComponent(); + DontDestroyOnLoad(aGameObject); - Log(string.Format("Adding ModuleManager to the loading screen {0}", list.Count)); + Log(string.Format("Adding patch loader to the loading screen {0}", list.Count)); + list.Insert(gameDatabaseIndex + 1, aGameObject.AddComponent()); - int gameDatabaseIndex = list.FindIndex(s => s is GameDatabase); - list.Insert(gameDatabaseIndex + 1, loader); + Log(string.Format("Adding post patch to the loading screen {0}", list.Count)); + list.Insert(gameDatabaseIndex + 2, aGameObject.AddComponent()); // Workaround for 1.6.0 Editor bug after a PartDatabase rebuild. if (Versioning.version_major == 1 && Versioning.version_minor == 6 && Versioning.Revision == 0) @@ -334,6 +337,10 @@ private IEnumerator DataBaseReloadWithMM(bool dump = false) while (!MMPatchLoader.Instance.IsReady()) yield return null; + PostPatchLoader.Instance.StartLoad(); + + while (!PostPatchLoader.Instance.IsReady()) + yield return null; if (dump) OutputAllConfigs(); diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 52efc4e8..de9a3e6b 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -58,6 +58,8 @@ + + diff --git a/ModuleManager/ModuleManagerPostPatch.cs b/ModuleManager/ModuleManagerPostPatch.cs new file mode 100644 index 00000000..1f4da965 --- /dev/null +++ b/ModuleManager/ModuleManagerPostPatch.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using UnityEngine; +using ModuleManager.Extensions; +using ModuleManager.Logging; + +namespace ModuleManager +{ + public delegate void ModuleManagerPostPatchCallback(); + + public class PostPatchLoader : LoadingSystem + { + public static PostPatchLoader Instance { get; private set; } + + private static readonly List postPatchCallbacks = new List(); + + private readonly IBasicLogger logger = new ModLogger("ModuleManager", new UnityLogger(UnityEngine.Debug.unityLogger)); + + private bool ready = false; + + public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) + { + if (!postPatchCallbacks.Contains(callback)) + postPatchCallbacks.Add(callback); + } + + private void Awake() + { + if (Instance != null) + { + Destroy(this); + return; + } + Instance = this; + } + + public override bool IsReady() => ready; + + public override float LoadWeight() => 0; + + public override float ProgressFraction() => 1; + + public override string ProgressTitle() => "ModuleManager : post patch"; + + public override void StartLoad() + { + ready = false; + StartCoroutine(Run()); + } + + private IEnumerator Run() + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + +#if DEBUG + InGameTestRunner testRunner = new InGameTestRunner(logger); + testRunner.RunTestCases(GameDatabase.Instance.root); +#endif + + yield return null; + + logger.Info("Reloading resources definitions"); + PartResourceLibrary.Instance.LoadDefinitions(); + + logger.Info("Reloading Trait configs"); + GameDatabase.Instance.ExperienceConfigs.LoadTraitConfigs(); + + logger.Info("Reloading Part Upgrades"); + PartUpgradeManager.Handler.FillUpgrades(); + + yield return null; + + logger.Info("Running post patch callbacks"); + + foreach (ModuleManagerPostPatchCallback callback in postPatchCallbacks) + { + try + { + callback(); + } + catch (Exception e) + { + logger.Exception("Exception while running a post patch callback", e); + } + yield return null; + } + yield return null; + + // Call all "public static void ModuleManagerPostLoad()" on all class + foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + foreach (Type type in ass.GetTypes()) + { + MethodInfo method = type.GetMethod("ModuleManagerPostLoad", BindingFlags.Public | BindingFlags.Static); + + if (method != null && method.GetParameters().Length == 0) + { + try + { + logger.Info("Calling " + ass.GetName().Name + "." + type.Name + "." + method.Name + "()"); + method.Invoke(null, null); + } + catch (Exception e) + { + logger.Exception("Exception while calling " + ass.GetName().Name + "." + type.Name + "." + method.Name + "()", e); + } + } + } + } + catch (Exception e) + { + logger.Exception("Post run call threw an exception in loading " + ass.FullName, e); + } + } + + yield return null; + + // Call "public void ModuleManagerPostLoad()" on all active MonoBehaviour instance + foreach (MonoBehaviour obj in FindObjectsOfType()) + { + MethodInfo method = obj.GetType().GetMethod("ModuleManagerPostLoad", BindingFlags.Public | BindingFlags.Instance); + + if (method != null && method.GetParameters().Length == 0) + { + try + { + logger.Info("Calling " + obj.GetType().Name + "." + method.Name + "()"); + method.Invoke(obj, null); + } + catch (Exception e) + { + logger.Exception("Exception while calling " + obj.GetType().Name + "." + method.Name + "() :\n", e); + } + } + } + + yield return null; + + if (ModuleManager.dumpPostPatch) + ModuleManager.OutputAllConfigs(); + + stopwatch.Stop(); + logger.Info("Post patch ran in " + ((float)stopwatch.ElapsedMilliseconds / 1000).ToString("F3") + "s"); + + ready = true; + } + } +} diff --git a/ModuleManager/ModuleManagerTestRunner.cs b/ModuleManager/ModuleManagerTestRunner.cs new file mode 100644 index 00000000..2f90a453 --- /dev/null +++ b/ModuleManager/ModuleManagerTestRunner.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ModuleManager.Extensions; +using ModuleManager.Logging; + +namespace ModuleManager +{ + public class InGameTestRunner + { + private readonly IBasicLogger logger; + + public InGameTestRunner(IBasicLogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void RunTestCases(UrlDir gameDatabaseRoot) + { + if (gameDatabaseRoot == null) throw new ArgumentNullException(nameof(gameDatabaseRoot)); + logger.Info("Running tests..."); + + foreach (UrlDir.UrlConfig expect in gameDatabaseRoot.GetConfigs("MMTEST_EXPECT")) + { + // So for each of the expects, we expect all the configs before that node to match exactly. + UrlDir.UrlFile parent = expect.parent; + if (parent.configs.Count != expect.config.CountNodes + 1) + { + logger.Error("Test " + parent.name + " failed as expected number of nodes differs expected: " + + expect.config.CountNodes + " found: " + (parent.configs.Count - 1)); + for (int i = 0; i < parent.configs.Count; ++i) + logger.Info(parent.configs[i].config.ToString()); + continue; + } + for (int i = 0; i < expect.config.CountNodes; ++i) + { + ConfigNode gotNode = parent.configs[i].config; + ConfigNode expectNode = expect.config.nodes[i]; + if (!CompareRecursive(expectNode, gotNode)) + { + logger.Error("Test " + parent.name + "[" + i + + "] failed as expected output and actual output differ.\nexpected:\n" + expectNode + + "\nActually got:\n" + gotNode); + } + } + + // Purge the tests + parent.configs.Clear(); + } + logger.Info("tests complete."); + } + + private static bool CompareRecursive(ConfigNode expectNode, ConfigNode gotNode) + { + if (expectNode.values.Count != gotNode.values.Count || expectNode.nodes.Count != gotNode.nodes.Count) + return false; + for (int i = 0; i < expectNode.values.Count; ++i) + { + ConfigNode.Value eVal = expectNode.values[i]; + ConfigNode.Value gVal = gotNode.values[i]; + if (eVal.name != gVal.name || eVal.value != gVal.value) + return false; + } + for (int i = 0; i < expectNode.nodes.Count; ++i) + { + ConfigNode eNode = expectNode.nodes[i]; + ConfigNode gNode = gotNode.nodes[i]; + if (!CompareRecursive(eNode, gNode)) + return false; + } + return true; + } + } +} diff --git a/ModuleManagerTests/InGameTestRunnerTest.cs b/ModuleManagerTests/InGameTestRunnerTest.cs new file mode 100644 index 00000000..cd860bc2 --- /dev/null +++ b/ModuleManagerTests/InGameTestRunnerTest.cs @@ -0,0 +1,188 @@ +using System; +using System.Linq; +using Xunit; +using NSubstitute; +using UnityEngine; +using TestUtils; +using ModuleManager; +using ModuleManager.Logging; + +namespace ModuleManagerTests +{ + public class InGameTestRunnerTest + { + private readonly IBasicLogger logger; + private readonly UrlDir databaseRoot; + private readonly InGameTestRunner testRunner; + + public InGameTestRunnerTest() + { + logger = Substitute.For(); + databaseRoot = UrlBuilder.CreateRoot(); + testRunner = new InGameTestRunner(logger); + } + + [Fact] + public void TestConstructor__LoggerNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new InGameTestRunner(null); + }); + + Assert.Equal("logger", ex.ParamName); + } + + [Fact] + public void TestRunTestCases__DatabaseRootNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + testRunner.RunTestCases(null); + }); + + Assert.Equal("gameDatabaseRoot", ex.ParamName); + } + + [Fact] + public void TestRunTestCases__WrongNumberOfNodes() + { + UrlDir.UrlFile file1 = UrlBuilder.CreateFile("abc/blah1.cfg", databaseRoot); + + // Call CreateCopy otherwise XUnit sees that it's an IEnumerable and attempts to compare by enumeration + ConfigNode testNode1 = new TestConfigNode("NODE1") + { + { "key1", "value1" }, + }.CreateCopy(); + + ConfigNode testNode2 = new ConfigNode("NODE2"); + + ConfigNode expectNode = new TestConfigNode("MMTEST_EXPECT") + { + new TestConfigNode("NODE1") + { + { "key1", "value1" }, + }, + }.CreateCopy(); + + UrlBuilder.CreateConfig(testNode1, file1); + UrlBuilder.CreateConfig(testNode2, file1); + UrlBuilder.CreateConfig(expectNode, file1); + + testRunner.RunTestCases(databaseRoot); + + Received.InOrder(delegate + { + logger.Log(LogType.Log, "Running tests..."); + logger.Log(LogType.Error, $"Test blah1 failed as expected number of nodes differs expected: 1 found: 2"); + logger.Log(LogType.Log, testNode1.ToString()); + logger.Log(LogType.Log, testNode2.ToString()); + logger.Log(LogType.Log, expectNode.ToString()); + logger.Log(LogType.Log, "tests complete."); + }); + + Assert.Equal(3, file1.configs.Count); + Assert.Equal(testNode1, file1.configs[0].config); + Assert.Equal(testNode2, file1.configs[1].config); + Assert.Equal(expectNode, file1.configs[2].config); + } + + [Fact] + public void TestRunTestCases__AllPassing() + { + UrlDir.UrlFile file1 = UrlBuilder.CreateFile("abc/blah1.cfg", databaseRoot); + UrlDir.UrlFile file2 = UrlBuilder.CreateFile("abc/blah2.cfg", databaseRoot); + + ConfigNode testNode1 = new TestConfigNode("NODE1") + { + { "key1", "value1" }, + { "key2", "value2" }, + new TestConfigNode("NODE2") + { + { "key3", "value3" }, + }, + }; + + ConfigNode testNode2 = new TestConfigNode("NODE3") + { + { "key4", "value4" }, + }; + + ConfigNode testNode3 = new TestConfigNode("NODE4") + { + { "key5", "value5" }, + }; + + UrlBuilder.CreateConfig(testNode1, file1); + UrlBuilder.CreateConfig(testNode2, file1); + UrlBuilder.CreateConfig(new TestConfigNode("MMTEST_EXPECT") + { + testNode1.CreateCopy(), + testNode2.CreateCopy(), + }, file1); + + UrlBuilder.CreateConfig(testNode3, file2); + UrlBuilder.CreateConfig(new TestConfigNode("MMTEST_EXPECT") + { + testNode3.CreateCopy(), + }, file2); + + testRunner.RunTestCases(databaseRoot); + + Received.InOrder(delegate + { + logger.Log(LogType.Log, "Running tests..."); + logger.Log(LogType.Log, "tests complete."); + }); + + logger.DidNotReceive().Log(LogType.Error, Arg.Any()); + + Assert.Empty(file1.configs); + Assert.Empty(file2.configs); + } + + [Fact] + public void TestRunTestCases__Failure() + { + UrlDir.UrlFile file1 = UrlBuilder.CreateFile("abc/blah1.cfg", databaseRoot); + + ConfigNode testNode1 = new TestConfigNode("NODE1") + { + { "key1", "value1" }, + { "key2", "value2" }, + new TestConfigNode("NODE2") + { + { "key3", "value3" }, + }, + }; + + ConfigNode expectNode1 = new TestConfigNode("NODE1") + { + { "key1", "value1" }, + { "key2", "value2" }, + new TestConfigNode("NODE2") + { + { "key4", "value3" }, + }, + }; + + UrlBuilder.CreateConfig(testNode1, file1); + UrlBuilder.CreateConfig(new TestConfigNode("MMTEST_EXPECT") + { + expectNode1, + }, file1); + + testRunner.RunTestCases(databaseRoot); + + Received.InOrder(delegate + { + logger.Log(LogType.Log, "Running tests..."); + logger.Log(LogType.Error, $"Test blah1[0] failed as expected output and actual output differ.\nexpected:\n{expectNode1}\nActually got:\n{testNode1}"); + logger.Log(LogType.Log, "tests complete."); + }); + + + Assert.Empty(file1.configs); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 6272e879..59998d7b 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -78,6 +78,7 @@ + From 48df502d6d38721788489652d57cd83d1d16a8dd Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 29 Oct 2018 22:44:31 -0700 Subject: [PATCH 042/140] Operate on a copy of the game database then apply * Insert nodes are now patches. They don't support MM syntax yet (just applied directly) but that could be added * ProtoUrlConfig identifies a UrlFile and node without the expectation that the UrlFile knows about the node (turned into a real UrlConfig at the end) * Intermedate state of the game database is now a linked list of nodes --- ModuleManager/MMPatchLoader.cs | 90 ++++----- ModuleManager/ModuleManager.csproj | 2 + ModuleManager/PatchApplier.cs | 18 +- ModuleManager/PatchContext.cs | 7 +- ModuleManager/PatchExtractor.cs | 21 +-- ModuleManager/PatchList.cs | 8 +- ModuleManager/Patches/CopyPatch.cs | 27 ++- ModuleManager/Patches/DeletePatch.cs | 25 ++- ModuleManager/Patches/EditPatch.cs | 25 +-- ModuleManager/Patches/IPatch.cs | 4 +- ModuleManager/Patches/InsertPatch.cs | 34 ++++ ModuleManager/Patches/PatchCompiler.cs | 11 +- ModuleManager/Progress/IPatchProgress.cs | 7 +- ModuleManager/Progress/PatchProgress.cs | 18 +- ModuleManager/ProtoUrlConfig.cs | 34 ++++ ModuleManagerTests/MMPatchLoaderTest.cs | 10 +- ModuleManagerTests/ModuleManagerTests.csproj | 1 + ModuleManagerTests/PatchApplierTest.cs | 47 ++--- ModuleManagerTests/PatchExtractorTest.cs | 79 -------- ModuleManagerTests/PatchListTest.cs | 81 ++++---- ModuleManagerTests/Patches/CopyPatchTest.cs | 142 +++++++++----- ModuleManagerTests/Patches/DeletePatchTest.cs | 47 +++-- ModuleManagerTests/Patches/EditPatchTest.cs | 100 ++++++---- ModuleManagerTests/Patches/InsertPatchTest.cs | 173 ++++++++++++++++++ .../Patches/PatchCompilerTest.cs | 93 ++++++++-- .../Progress/PatchProgressTest.cs | 40 ++-- 26 files changed, 732 insertions(+), 412 deletions(-) create mode 100644 ModuleManager/Patches/InsertPatch.cs create mode 100644 ModuleManager/ProtoUrlConfig.cs create mode 100644 ModuleManagerTests/Patches/InsertPatchTest.cs diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 4df7667e..fee3b2bd 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -167,9 +167,9 @@ private IEnumerator ProcessPatch() PatchExtractor extractor = new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); // Have to convert to an array because we will be removing patches - UrlDir.UrlConfig[] allConfigs = GameDatabase.Instance.root.AllConfigs.ToArray(); - IEnumerable extractedPatches = allConfigs.Select(urlConfig => extractor.ExtractPatch(urlConfig)); - PatchList patchList = new PatchList(mods, extractedPatches.Where(patch => patch != null), progress); + IEnumerable extractedPatches = + GameDatabase.Instance.root.AllConfigs.Select(urlConfig => extractor.ExtractPatch(urlConfig)).Where(patch => patch != null); + PatchList patchList = new PatchList(mods, extractedPatches, progress); #endregion @@ -187,9 +187,11 @@ private IEnumerator ProcessPatch() logger.Info("Starting patch thread"); + IEnumerable databaseConfigs = null; + ITaskStatus patchThread = BackgroundTask.Start(delegate { - applier.ApplyPatches(GameDatabase.Instance.root.AllConfigFiles.ToArray(), patchList); + databaseConfigs = applier.ApplyPatches(patchList); }); float nextYield = Time.realtimeSinceStartup + yieldInterval; @@ -236,6 +238,22 @@ float updateTimeRemaining() FatalErrorHandler.HandleFatalError("The patch runner threw an exception"); yield break; } + if (databaseConfigs == null) + { + progress.Error("The patcher returned a null collection of configs"); + FatalErrorHandler.HandleFatalError("The patcher returned a null collection of configs"); + yield break; + } + + foreach (UrlDir.UrlFile file in GameDatabase.Instance.root.AllConfigFiles) + { + file.configs.Clear(); + } + + foreach (IProtoUrlConfig protoConfig in databaseConfigs) + { + protoConfig.UrlFile.AddConfig(protoConfig.Node); + } logger.Info("Done patching"); yield return null; @@ -1219,28 +1237,24 @@ private static ConfigNode RecurseNodeSearch(string path, NodeStack nodeStack, Pa // @XXXXX if (root) { - IEnumerable urlConfigs = context.databaseRoot.GetConfigs(nodeType); - if (!urlConfigs.Any()) + bool foundNodeType = false; + foreach (IProtoUrlConfig urlConfig in context.databaseConfigs) { - context.logger.Warning("Can't find nodeType:" + nodeType); - return null; - } + ConfigNode node = urlConfig.Node; - if (nodeName == null) - { - nodeStack = new NodeStack(urlConfigs.First().config); - } - else - { - foreach (UrlDir.UrlConfig url in urlConfigs) + if (node.name != nodeType) continue; + + foundNodeType = true; + + if (nodeName == null || (node.GetValue("name") is string testNodeName && WildcardMatch(testNodeName, nodeName))) { - if (url.config.HasValue("name") && WildcardMatch(url.config.GetValue("name"), nodeName)) - { - nodeStack = new NodeStack(url.config); - break; - } + nodeStack = new NodeStack(node); + break; } } + + if (!foundNodeType) context.logger.Warning("Can't find nodeType:" + nodeType); + if (nodeStack == null) return null; } else { @@ -1305,7 +1319,6 @@ private static ConfigNode.Value RecurseVariableSearch(string path, NodeStack nod string subName = path.Substring(1, nextSep - 1); string nodeType, nodeName; - UrlDir.UrlConfig target = null; if (subName.Contains("[")) { @@ -1317,32 +1330,27 @@ private static ConfigNode.Value RecurseVariableSearch(string path, NodeStack nod { // @NODETYPE/ nodeType = subName; - nodeName = string.Empty; + nodeName = null; } - IEnumerable urlConfigs = context.databaseRoot.GetConfigs(nodeType); - if (!urlConfigs.Any()) + bool foundNodeType = false; + foreach (IProtoUrlConfig urlConfig in context.databaseConfigs) { - context.logger.Warning("Can't find nodeType:" + nodeType); - return null; - } + ConfigNode node = urlConfig.Node; - if (nodeName == string.Empty) - { - target = urlConfigs.First(); - } - else - { - foreach (UrlDir.UrlConfig url in urlConfigs) + if (node.name != nodeType) continue; + + foundNodeType = true; + + if (nodeName == null || (node.GetValue("name") is string testNodeName && WildcardMatch(testNodeName, nodeName))) { - if (url.config.HasValue("name") && WildcardMatch(url.config.GetValue("name"), nodeName)) - { - target = url; - break; - } + return RecurseVariableSearch(path.Substring(nextSep + 1), new NodeStack(node), context); } } - return target != null ? RecurseVariableSearch(path.Substring(nextSep + 1), new NodeStack(target.config), context) : null; + + if (!foundNodeType) context.logger.Warning("Can't find nodeType:" + nodeType); + + return null; } if (path.StartsWith("../")) { diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index de9a3e6b..6de340a4 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -67,6 +67,7 @@ + @@ -90,6 +91,7 @@ + diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index 83440d70..72e4fbd0 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using ModuleManager.Logging; using ModuleManager.Extensions; using ModuleManager.Patches; @@ -21,17 +20,21 @@ public PatchApplier(IPatchProgress progress, IBasicLogger logger) this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public void ApplyPatches(IEnumerable configFiles, IEnumerable patches) + public IEnumerable ApplyPatches(IEnumerable patches) { - if (configFiles == null) throw new ArgumentNullException(nameof(configFiles)); if (patches == null) throw new ArgumentNullException(nameof(patches)); + + LinkedList databaseConfigs = new LinkedList(); + foreach (IPass pass in patches) { - ApplyPatches(configFiles, pass); + ApplyPatches(databaseConfigs, pass); } + + return databaseConfigs; } - private void ApplyPatches(IEnumerable configFiles, IPass pass) + private void ApplyPatches(LinkedList databaseConfigs, IPass pass) { logger.Info(pass.Name + " pass"); Activity = "ModuleManager " + pass.Name; @@ -40,10 +43,7 @@ private void ApplyPatches(IEnumerable configFiles, IPass pass) { try { - foreach (UrlDir.UrlFile file in configFiles) - { - patch.Apply(file, progress, logger); - } + patch.Apply(databaseConfigs, progress, logger); progress.PatchApplied(); } catch (Exception e) diff --git a/ModuleManager/PatchContext.cs b/ModuleManager/PatchContext.cs index ac3b7200..09e2f441 100644 --- a/ModuleManager/PatchContext.cs +++ b/ModuleManager/PatchContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using ModuleManager.Logging; using ModuleManager.Progress; @@ -7,14 +8,14 @@ namespace ModuleManager public struct PatchContext { public readonly UrlDir.UrlConfig patchUrl; - public readonly UrlDir databaseRoot; + public readonly IEnumerable databaseConfigs; public readonly IBasicLogger logger; public readonly IPatchProgress progress; - public PatchContext(UrlDir.UrlConfig patchUrl, UrlDir databaseRoot, IBasicLogger logger, IPatchProgress progress) + public PatchContext(UrlDir.UrlConfig patchUrl, IEnumerable databaseConfigs, IBasicLogger logger, IPatchProgress progress) { this.patchUrl = patchUrl; - this.databaseRoot = databaseRoot; + this.databaseConfigs = databaseConfigs; this.logger = logger; this.progress = progress; } diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index 3306014d..cf476a64 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -39,9 +39,6 @@ public IPatch ExtractPatch(UrlDir.UrlConfig urlConfig) try { - int index = urlConfig.parent.configs.IndexOf(urlConfig); - urlConfig.parent.configs.RemoveAt(index); - if (!urlConfig.type.IsBracketBalanced()) { progress.Error(urlConfig, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\n" + urlConfig.SafeUrl()); @@ -103,21 +100,9 @@ public IPatch ExtractPatch(UrlDir.UrlConfig urlConfig) { return null; } - - if (command == Command.Insert) - { - ConfigNode newNode = urlConfig.config.DeepCopy(); - newNode.name = protoPatch.nodeType; - newNode.id = urlConfig.config.id; - needsChecker.CheckNeedsRecursive(newNode, urlConfig); - urlConfig.parent.configs.Insert(index, new UrlDir.UrlConfig(urlConfig.parent, newNode)); - return null; - } - else - { - needsChecker.CheckNeedsRecursive(urlConfig.config, urlConfig); - return patchCompiler.CompilePatch(protoPatch); - } + + needsChecker.CheckNeedsRecursive(urlConfig.config, urlConfig); + return patchCompiler.CompilePatch(protoPatch); } catch(Exception e) { diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index 69c58537..892b6843 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -70,6 +70,7 @@ public ModPassCollection(IEnumerable modList) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + private readonly Pass insertPatches = new Pass(":INSERT (initial)"); private readonly Pass firstPatches = new Pass(":FIRST"); private readonly Pass legacyPatches = new Pass(":LEGACY (default)"); private readonly Pass finalPatches = new Pass(":FINAL"); @@ -84,7 +85,11 @@ public PatchList(IEnumerable modList, IEnumerable patches, IPatc foreach (IPatch patch in patches) { - if (patch.PassSpecifier is FirstPassSpecifier) + if (patch.PassSpecifier is InsertPassSpecifier) + { + insertPatches.Add(patch); + } + else if (patch.PassSpecifier is FirstPassSpecifier) { firstPatches.Add(patch); } @@ -127,6 +132,7 @@ public PatchList(IEnumerable modList, IEnumerable patches, IPatc public IEnumerator GetEnumerator() { + yield return insertPatches; yield return firstPatches; yield return legacyPatches; diff --git a/ModuleManager/Patches/CopyPatch.cs b/ModuleManager/Patches/CopyPatch.cs index b94ee353..05241973 100644 --- a/ModuleManager/Patches/CopyPatch.cs +++ b/ModuleManager/Patches/CopyPatch.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NodeStack = ModuleManager.Collections.ImmutableStack; using ModuleManager.Extensions; using ModuleManager.Logging; @@ -20,37 +21,35 @@ public CopyPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpec PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); } - public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) + public void Apply(LinkedList databaseConfigs, IPatchProgress progress, IBasicLogger logger) { - if (file == null) throw new ArgumentNullException(nameof(file)); + if (databaseConfigs == null) throw new ArgumentNullException(nameof(databaseConfigs)); if (progress == null) throw new ArgumentNullException(nameof(progress)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - PatchContext context = new PatchContext(UrlConfig, file.root, logger, progress); + PatchContext context = new PatchContext(UrlConfig, databaseConfigs, logger, progress); - // Avoid checking the new configs we are creating - int count = file.configs.Count; - for (int i = 0; i < count; i++) + for (LinkedListNode listNode = databaseConfigs.First; listNode != null; listNode = listNode.Next) { - UrlDir.UrlConfig url = file.configs[i]; + IProtoUrlConfig protoConfig = listNode.Value; try { - if (!NodeMatcher.IsMatch(url.config)) continue; + if (!NodeMatcher.IsMatch(protoConfig.Node)) continue; - ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(url.config), UrlConfig.config, context); - if (url.config.HasValue("name") && url.config.GetValue("name") == clone.GetValue("name")) + ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(protoConfig.Node), UrlConfig.config, context); + if (protoConfig.Node.GetValue("name") is string name && name == clone.GetValue("name")) { - progress.Error(UrlConfig, $"Error - when applying copy {UrlConfig.SafeUrl()} to {url.SafeUrl()} - the copy needs to have a different name than the parent (use @name = xxx)"); + progress.Error(UrlConfig, $"Error - when applying copy {UrlConfig.SafeUrl()} to {protoConfig.FullUrl} - the copy needs to have a different name than the parent (use @name = xxx)"); } else { - progress.ApplyingCopy(url, UrlConfig); - file.AddConfig(clone); + progress.ApplyingCopy(protoConfig, UrlConfig); + listNode = databaseConfigs.AddAfter(listNode, new ProtoUrlConfig(protoConfig.UrlFile, clone)); } } catch (Exception ex) { - progress.Exception(UrlConfig, $"Exception while applying copy {UrlConfig.SafeUrl()} to {url.SafeUrl()}", ex); + progress.Exception(UrlConfig, $"Exception while applying copy {UrlConfig.SafeUrl()} to {protoConfig.FullUrl}", ex); } } } diff --git a/ModuleManager/Patches/DeletePatch.cs b/ModuleManager/Patches/DeletePatch.cs index 6d8762f9..fd465c9a 100644 --- a/ModuleManager/Patches/DeletePatch.cs +++ b/ModuleManager/Patches/DeletePatch.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using ModuleManager.Extensions; using ModuleManager.Logging; using ModuleManager.Patches.PassSpecifiers; @@ -19,31 +20,29 @@ public DeletePatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSp PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); } - public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) + public void Apply(LinkedList databaseConfigs, IPatchProgress progress, IBasicLogger logger) { - if (file == null) throw new ArgumentNullException(nameof(file)); + if (databaseConfigs == null) throw new ArgumentNullException(nameof(databaseConfigs)); if (progress == null) throw new ArgumentNullException(nameof(progress)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - int i = 0; - while (i < file.configs.Count) + LinkedListNode currentNode = databaseConfigs.First; + while (currentNode != null) { - UrlDir.UrlConfig url = file.configs[i]; + IProtoUrlConfig protoConfig = currentNode.Value; try { - if (NodeMatcher.IsMatch(url.config)) + LinkedListNode nextNode = currentNode.Next; + if (NodeMatcher.IsMatch(protoConfig.Node)) { - progress.ApplyingDelete(url, UrlConfig); - file.configs.RemoveAt(i); - } - else - { - i++; + progress.ApplyingDelete(protoConfig, UrlConfig); + databaseConfigs.Remove(currentNode); } + currentNode = nextNode; } catch (Exception ex) { - progress.Exception(UrlConfig, $"Exception while applying delete {UrlConfig.SafeUrl()} to {url.SafeUrl()}", ex); + progress.Exception(UrlConfig, $"Exception while applying delete {UrlConfig.SafeUrl()} to {protoConfig.FullUrl}", ex); } } } diff --git a/ModuleManager/Patches/EditPatch.cs b/ModuleManager/Patches/EditPatch.cs index b9358fdb..501144e9 100644 --- a/ModuleManager/Patches/EditPatch.cs +++ b/ModuleManager/Patches/EditPatch.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NodeStack = ModuleManager.Collections.ImmutableStack; using ModuleManager.Extensions; using ModuleManager.Logging; @@ -24,32 +25,32 @@ public EditPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpec loop = urlConfig.config.HasNode("MM_PATCH_LOOP"); } - public void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger) + public void Apply(LinkedList databaseConfigs, IPatchProgress progress, IBasicLogger logger) { - if (file == null) throw new ArgumentNullException(nameof(file)); + if (databaseConfigs == null) throw new ArgumentNullException(nameof(databaseConfigs)); if (progress == null) throw new ArgumentNullException(nameof(progress)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - PatchContext context = new PatchContext(UrlConfig, file.root, logger, progress); - for (int i = 0; i < file.configs.Count; i++) + PatchContext context = new PatchContext(UrlConfig, databaseConfigs, logger, progress); + for (LinkedListNode listNode = databaseConfigs.First; listNode != null; listNode = listNode.Next) { - UrlDir.UrlConfig urlConfig = file.configs[i]; + IProtoUrlConfig protoConfig = listNode.Value; try { - if (!NodeMatcher.IsMatch(urlConfig.config)) continue; - if (loop) logger.Info($"Looping on {UrlConfig.SafeUrl()} to {urlConfig.SafeUrl()}"); + if (!NodeMatcher.IsMatch(protoConfig.Node)) continue; + if (loop) logger.Info($"Looping on {UrlConfig.SafeUrl()} to {protoConfig.FullUrl}"); do { - progress.ApplyingUpdate(urlConfig, UrlConfig); - file.configs[i] = urlConfig = new UrlDir.UrlConfig(file, MMPatchLoader.ModifyNode(new NodeStack(urlConfig.config), UrlConfig.config, context)); - } while (loop && NodeMatcher.IsMatch(urlConfig.config)); + progress.ApplyingUpdate(protoConfig, UrlConfig); + listNode.Value = protoConfig = new ProtoUrlConfig(protoConfig.UrlFile, MMPatchLoader.ModifyNode(new NodeStack(protoConfig.Node), UrlConfig.config, context)); + } while (loop && NodeMatcher.IsMatch(protoConfig.Node)); - if (loop) file.configs[i].config.RemoveNodes("MM_PATCH_LOOP"); + if (loop) protoConfig.Node.RemoveNodes("MM_PATCH_LOOP"); } catch (Exception ex) { - progress.Exception(UrlConfig, $"Exception while applying update {UrlConfig.SafeUrl()} to {urlConfig.SafeUrl()}", ex); + progress.Exception(UrlConfig, $"Exception while applying update {UrlConfig.SafeUrl()} to {protoConfig.FullUrl}", ex); } } } diff --git a/ModuleManager/Patches/IPatch.cs b/ModuleManager/Patches/IPatch.cs index 57920050..032ce6e1 100644 --- a/ModuleManager/Patches/IPatch.cs +++ b/ModuleManager/Patches/IPatch.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using ModuleManager.Logging; using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; @@ -8,8 +9,7 @@ namespace ModuleManager.Patches public interface IPatch { UrlDir.UrlConfig UrlConfig { get; } - INodeMatcher NodeMatcher { get; } IPassSpecifier PassSpecifier { get; } - void Apply(UrlDir.UrlFile file, IPatchProgress progress, IBasicLogger logger); + void Apply(LinkedList configs, IPatchProgress progress, IBasicLogger logger); } } diff --git a/ModuleManager/Patches/InsertPatch.cs b/ModuleManager/Patches/InsertPatch.cs new file mode 100644 index 00000000..9f3c8a50 --- /dev/null +++ b/ModuleManager/Patches/InsertPatch.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + public class InsertPatch : IPatch + { + public UrlDir.UrlConfig UrlConfig { get; } + public string NodeType { get; } + public IPassSpecifier PassSpecifier { get; } + + public InsertPatch(UrlDir.UrlConfig urlConfig, string nodeType, IPassSpecifier passSpecifier) + { + UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + NodeType = nodeType ?? throw new ArgumentNullException(nameof(nodeType)); + PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); + } + + public void Apply(LinkedList configs, IPatchProgress progress, IBasicLogger logger) + { + if (configs == null) throw new ArgumentNullException(nameof(configs)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + ConfigNode node = UrlConfig.config.DeepCopy(); + node.name = NodeType; + configs.AddLast(new ProtoUrlConfig(UrlConfig.parent, node)); + } + } +} diff --git a/ModuleManager/Patches/PatchCompiler.cs b/ModuleManager/Patches/PatchCompiler.cs index 11ce994b..67387372 100644 --- a/ModuleManager/Patches/PatchCompiler.cs +++ b/ModuleManager/Patches/PatchCompiler.cs @@ -13,18 +13,19 @@ public IPatch CompilePatch(ProtoPatch protoPatch) { if (protoPatch == null) throw new ArgumentNullException(nameof(protoPatch)); - INodeMatcher nodeMatcher = new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has); - switch (protoPatch.command) { + case Command.Insert: + return new InsertPatch(protoPatch.urlConfig, protoPatch.nodeType, protoPatch.passSpecifier); + case Command.Edit: - return new EditPatch(protoPatch.urlConfig, nodeMatcher, protoPatch.passSpecifier); + return new EditPatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier); case Command.Copy: - return new CopyPatch(protoPatch.urlConfig, nodeMatcher, protoPatch.passSpecifier); + return new CopyPatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier); case Command.Delete: - return new DeletePatch(protoPatch.urlConfig, nodeMatcher, protoPatch.passSpecifier); + return new DeletePatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier); default: throw new ArgumentException("has an invalid command for a root node: " + protoPatch.command, nameof(protoPatch)); diff --git a/ModuleManager/Progress/IPatchProgress.cs b/ModuleManager/Progress/IPatchProgress.cs index 4edd07c2..d1b39cae 100644 --- a/ModuleManager/Progress/IPatchProgress.cs +++ b/ModuleManager/Progress/IPatchProgress.cs @@ -10,6 +10,7 @@ public interface IPatchProgress void Warning(UrlDir.UrlConfig url, string message); void Error(UrlDir.UrlConfig url, string message); + void Error(string message); void Exception(string message, Exception exception); void Exception(UrlDir.UrlConfig url, string message, Exception exception); void NeedsUnsatisfiedRoot(UrlDir.UrlConfig url); @@ -18,9 +19,9 @@ public interface IPatchProgress void NeedsUnsatisfiedBefore(UrlDir.UrlConfig url); void NeedsUnsatisfiedFor(UrlDir.UrlConfig url); void NeedsUnsatisfiedAfter(UrlDir.UrlConfig url); - void ApplyingCopy(UrlDir.UrlConfig original, UrlDir.UrlConfig patch); - void ApplyingDelete(UrlDir.UrlConfig original, UrlDir.UrlConfig patch); - void ApplyingUpdate(UrlDir.UrlConfig original, UrlDir.UrlConfig patch); + void ApplyingCopy(IUrlConfigIdentifier original, UrlDir.UrlConfig patch); + void ApplyingDelete(IUrlConfigIdentifier original, UrlDir.UrlConfig patch); + void ApplyingUpdate(IUrlConfigIdentifier original, UrlDir.UrlConfig patch); void PatchAdded(); void PatchApplied(); } diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index 701fa4ef..d61c35eb 100644 --- a/ModuleManager/Progress/PatchProgress.cs +++ b/ModuleManager/Progress/PatchProgress.cs @@ -38,21 +38,21 @@ public void PatchAdded() Counter.totalPatches.Increment(); } - public void ApplyingUpdate(UrlDir.UrlConfig original, UrlDir.UrlConfig patch) + public void ApplyingUpdate(IUrlConfigIdentifier original, UrlDir.UrlConfig patch) { - logger.Info($"Applying update {patch.SafeUrl()} to {original.SafeUrl()}"); + logger.Info($"Applying update {patch.SafeUrl()} to {original.FullUrl}"); Counter.patchedNodes.Increment(); } - public void ApplyingCopy(UrlDir.UrlConfig original, UrlDir.UrlConfig patch) + public void ApplyingCopy(IUrlConfigIdentifier original, UrlDir.UrlConfig patch) { - logger.Info($"Applying copy {patch.SafeUrl()} to {original.SafeUrl()}"); + logger.Info($"Applying copy {patch.SafeUrl()} to {original.FullUrl}"); Counter.patchedNodes.Increment(); } - public void ApplyingDelete(UrlDir.UrlConfig original, UrlDir.UrlConfig patch) + public void ApplyingDelete(IUrlConfigIdentifier original, UrlDir.UrlConfig patch) { - logger.Info($"Applying delete {patch.SafeUrl()} to {original.SafeUrl()}"); + logger.Info($"Applying delete {patch.SafeUrl()} to {original.FullUrl}"); Counter.patchedNodes.Increment(); } @@ -109,6 +109,12 @@ public void Error(UrlDir.UrlConfig url, string message) RecordErrorFile(url); } + public void Error(string message) + { + Counter.errors.Increment(); + logger.Error(message); + } + public void Exception(string message, Exception exception) { Counter.exceptions.Increment(); diff --git a/ModuleManager/ProtoUrlConfig.cs b/ModuleManager/ProtoUrlConfig.cs new file mode 100644 index 00000000..74533948 --- /dev/null +++ b/ModuleManager/ProtoUrlConfig.cs @@ -0,0 +1,34 @@ +using System; + +namespace ModuleManager +{ + public interface IUrlConfigIdentifier + { + string FileUrl { get; } + string NodeType { get; } + string FullUrl { get; } + } + + public interface IProtoUrlConfig : IUrlConfigIdentifier + { + UrlDir.UrlFile UrlFile { get; } + ConfigNode Node { get; } + } + + public class ProtoUrlConfig : IProtoUrlConfig + { + public UrlDir.UrlFile UrlFile { get; } + public ConfigNode Node { get; } + public string FileUrl { get; } + public string NodeType => Node.name; + public string FullUrl { get; } + + public ProtoUrlConfig(UrlDir.UrlFile urlFile, ConfigNode node) + { + UrlFile = urlFile ?? throw new ArgumentNullException(nameof(urlFile)); + Node = node ?? throw new ArgumentNullException(nameof(node)); + FileUrl = UrlFile.url + '.' + urlFile.fileExtension; + FullUrl = FileUrl + '/' + Node.name; + } + } +} diff --git a/ModuleManagerTests/MMPatchLoaderTest.cs b/ModuleManagerTests/MMPatchLoaderTest.cs index efeb39a1..ac687909 100644 --- a/ModuleManagerTests/MMPatchLoaderTest.cs +++ b/ModuleManagerTests/MMPatchLoaderTest.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Xunit; using NSubstitute; using UnityEngine; @@ -15,7 +16,6 @@ public class MMPatchLoaderTest { private readonly IBasicLogger logger = Substitute.For(); private readonly IPatchProgress progress = Substitute.For(); - private readonly UrlDir root = UrlBuilder.CreateRoot(); [Fact] public void TestModifyNode__IndexAllWithAssign() @@ -29,9 +29,9 @@ public void TestModifyNode__IndexAllWithAssign() UrlDir.UrlConfig c2u = UrlBuilder.CreateConfig("abc/def", new TestConfigNode("@NODE") { { "@foo,*", "bar3" }, - }, root); + }); - PatchContext context = new PatchContext(c2u, root, logger, progress); + PatchContext context = new PatchContext(c2u, Enumerable.Empty(), logger, progress); ConfigNode c3 = MMPatchLoader.ModifyNode(new NodeStack(c1), c2u.config, context); @@ -56,9 +56,9 @@ public void TestModifyNode__MultiplyValue() UrlDir.UrlConfig c2u = UrlBuilder.CreateConfig("abc/def", new TestConfigNode("@NODE") { { "@foo *", "2" }, - }, root); + }); - PatchContext context = new PatchContext(c2u, root, logger, progress); + PatchContext context = new PatchContext(c2u, Enumerable.Empty(), logger, progress); ConfigNode c3 = MMPatchLoader.ModifyNode(new NodeStack(c1), c2u.config, context); diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 59998d7b..f07b5c07 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -91,6 +91,7 @@ + diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index fec4e148..81011b97 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using Xunit; using NSubstitute; using UnityEngine; -using TestUtils; using ModuleManager; using ModuleManager.Collections; using ModuleManager.Logging; @@ -35,25 +35,13 @@ public void TestConstructor__LoggerNull() Assert.Equal("logger", ex.ParamName); } - [Fact] - public void TestApplyPatches__UrlFilesNull() - { - PatchApplier applier = new PatchApplier(Substitute.For(), Substitute.For()); - ArgumentNullException ex = Assert.Throws(delegate - { - applier.ApplyPatches(null, new IPass[0]); - }); - - Assert.Equal("configFiles", ex.ParamName); - } - [Fact] public void TestApplyPatches__PatchesNull() { PatchApplier applier = new PatchApplier(Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { - applier.ApplyPatches(new UrlDir.UrlFile[0], null); + applier.ApplyPatches(null); }); Assert.Equal("patches", ex.ParamName); @@ -65,8 +53,6 @@ public void TestApplyPatches() IBasicLogger logger = Substitute.For(); IPatchProgress progress = Substitute.For(); PatchApplier patchApplier = new PatchApplier(progress, logger); - UrlDir.UrlFile file1 = UrlBuilder.CreateFile("abc/def.cfg"); - UrlDir.UrlFile file2 = UrlBuilder.CreateFile("ghi/jkl.cfg"); IPass pass1 = Substitute.For(); IPass pass2 = Substitute.For(); IPass pass3 = Substitute.For(); @@ -87,7 +73,7 @@ public void TestApplyPatches() IPass[] patchList = new IPass[] { pass1, pass2, pass3 }; - patchApplier.ApplyPatches(new[] { file1, file2 }, new[] { pass1, pass2, pass3 }); + LinkedList databaseConfigs = Assert.IsType>(patchApplier.ApplyPatches(new[] { pass1, pass2, pass3 })); progress.DidNotReceiveWithAnyArgs().Error(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null); @@ -100,34 +86,25 @@ public void TestApplyPatches() Received.InOrder(delegate { logger.Log(LogType.Log, ":PASS1 pass"); - patches[0].Apply(file1, progress, logger); - patches[0].Apply(file2, progress, logger); + patches[0].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - patches[1].Apply(file1, progress, logger); - patches[1].Apply(file2, progress, logger); + patches[1].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - patches[2].Apply(file1, progress, logger); - patches[2].Apply(file2, progress, logger); + patches[2].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); logger.Log(LogType.Log, ":PASS2 pass"); - patches[3].Apply(file1, progress, logger); - patches[3].Apply(file2, progress, logger); + patches[3].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - patches[4].Apply(file1, progress, logger); - patches[4].Apply(file2, progress, logger); + patches[4].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - patches[5].Apply(file1, progress, logger); - patches[5].Apply(file2, progress, logger); + patches[5].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); logger.Log(LogType.Log, ":PASS3 pass"); - patches[6].Apply(file1, progress, logger); - patches[6].Apply(file2, progress, logger); + patches[6].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - patches[7].Apply(file1, progress, logger); - patches[7].Apply(file2, progress, logger); + patches[7].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - patches[8].Apply(file1, progress, logger); - patches[8].Apply(file2, progress, logger); + patches[8].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); }); } diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index a7e2f0a0..4106486e 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -118,8 +118,6 @@ public void TestExtractPatch__ProtoPatchNull() AssertNoErrors(); - Assert.Empty(root.AllConfigs); - progress.DidNotReceive().PatchAdded(); progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } @@ -153,8 +151,6 @@ public void TestExtractPatch() AssertNoErrors(); - Assert.Empty(root.AllConfigs); - needsChecker.Received().CheckNeedsRecursive(urlConfig.config, urlConfig); needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsExpression(null); progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); @@ -190,8 +186,6 @@ public void TestExtractPatch__Needs() AssertNoErrors(); - Assert.Empty(root.AllConfigs); - needsChecker.Received().CheckNeedsRecursive(urlConfig.config, urlConfig); progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } @@ -222,8 +216,6 @@ public void TestExtractPatch__NeedsUnsatisfied() AssertNoErrors(); - Assert.Empty(root.AllConfigs); - passSpecifier.DidNotReceiveWithAnyArgs().CheckNeeds(null, null); needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsRecursive(null, null); patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); @@ -258,65 +250,12 @@ public void TestExtractPatch__NeedsUnsatisfiedPassSpecifier() AssertNoErrors(); - Assert.Empty(root.AllConfigs); - needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsRecursive(null, null); patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } - [Fact] - public void TestExtractPatch__Insert() - { - UrlDir.UrlConfig urlConfig = CreateConfig("NODE_TYPE"); - - ITagList tagList = Substitute.For(); - tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); - - IPassSpecifier passSpecifier = Substitute.For(); - ProtoPatch protoPatch = new ProtoPatch( - urlConfig, - Command.Insert, - "NODE_TYPE", - null, - "needs", - null, - passSpecifier - ); - - protoPatchBuilder.Build(urlConfig, Command.Insert, tagList).Returns(protoPatch); - needsChecker.CheckNeedsExpression("needs").Returns(true); - passSpecifier.CheckNeeds(needsChecker, progress).Returns(true); - - ConfigNode needsCheckedNode = null; - needsChecker.CheckNeedsRecursive(Arg.Do(node => needsCheckedNode = node), urlConfig); - - Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - - AssertNoErrors(); - - Received.InOrder(delegate - { - tagListParser.Parse("NODE_TYPE", urlConfig); - protoPatchBuilder.Build(urlConfig, Command.Insert, tagList); - needsChecker.CheckNeedsExpression("needs"); - passSpecifier.CheckNeeds(needsChecker, progress); - needsChecker.CheckNeedsRecursive(needsCheckedNode, urlConfig); - }); - - patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); - - Assert.Single(root.AllConfigs); - UrlDir.UrlConfig newUrlConfig = root.AllConfigs.First(); - Assert.NotSame(urlConfig, newUrlConfig); - Assert.NotSame(urlConfig.config, newUrlConfig.config); - AssertConfigNodesEqual(urlConfig.config, newUrlConfig.config); - Assert.Same(needsCheckedNode, newUrlConfig.config); - - progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); - } - [Fact] public void TestExtractPatch__Null() { @@ -337,8 +276,6 @@ public void TestExtractPatch__NotBracketBalanced() patchExtractor.ExtractPatch(config1); patchExtractor.ExtractPatch(config2); - Assert.Empty(root.AllConfigs); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); @@ -357,8 +294,6 @@ public void TestExtractPatch__InvalidCommand__Replace() UrlDir.UrlConfig urlConfig = CreateConfig("%NODE"); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Empty(root.AllConfigs); - progress.Received().Error(urlConfig, "Error - replace command (%) is not valid on a root node: abc/def/%NODE"); } @@ -368,8 +303,6 @@ public void TestExtractPatch__InvalidCommand__Create() UrlDir.UrlConfig urlConfig = CreateConfig("&NODE"); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Empty(root.AllConfigs); - progress.Received().Error(urlConfig, "Error - create command (&) is not valid on a root node: abc/def/&NODE"); } @@ -379,8 +312,6 @@ public void TestExtractPatch__InvalidCommand__Rename() UrlDir.UrlConfig urlConfig = CreateConfig("|NODE"); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Empty(root.AllConfigs); - progress.Received().Error(urlConfig, "Error - rename command (|) is not valid on a root node: abc/def/|NODE"); } @@ -390,8 +321,6 @@ public void TestExtractPatch__InvalidCommand__Paste() UrlDir.UrlConfig urlConfig = CreateConfig("#NODE"); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Empty(root.AllConfigs); - progress.Received().Error(urlConfig, "Error - paste command (#) is not valid on a root node: abc/def/#NODE"); } @@ -401,8 +330,6 @@ public void TestExtractPatch__InvalidCommand__Special() UrlDir.UrlConfig urlConfig = CreateConfig("*NODE"); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Empty(root.AllConfigs); - progress.Received().Error(urlConfig, "Error - special command (*) is not valid on a root node: abc/def/*NODE"); } @@ -413,8 +340,6 @@ public void TestExtractPatch__TagListBadlyFormatted() tagListParser.When(t => t.Parse("badSomehow", urlConfig)).Throw(new FormatException("badly formatted")); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Empty(root.AllConfigs); - progress.Received().Error(urlConfig, "Cannot parse node name as tag list: badly formatted\non: abc/def/badSomehow"); } @@ -426,8 +351,6 @@ public void TestExtractPatch__ProtoPatchFailed() Assert.Null(patchExtractor.ExtractPatch(urlConfig)); AssertNoErrors(); - - Assert.Empty(root.AllConfigs); } [Fact] @@ -438,8 +361,6 @@ public void TestExtractPatch__Exception() tagListParser.When(t => t.Parse("NODE", urlConfig)).Throw(ex); Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - Assert.Empty(root.AllConfigs); - progress.Received().Exception(urlConfig, "Exception while attempting to create patch from config: abc/def/NODE", ex); } diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index d21c8c3f..8e66f0b3 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -111,32 +111,36 @@ public void Test__Lifecycle() Substitute.For(), Substitute.For(), Substitute.For(), + Substitute.For(), + Substitute.For(), }; UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); - patches[00].PassSpecifier.Returns(new FirstPassSpecifier()); - patches[01].PassSpecifier.Returns(new FirstPassSpecifier()); - patches[02].PassSpecifier.Returns(new LegacyPassSpecifier()); - patches[03].PassSpecifier.Returns(new LegacyPassSpecifier()); - patches[04].PassSpecifier.Returns(new BeforePassSpecifier("mod1", urlConfig)); - patches[05].PassSpecifier.Returns(new BeforePassSpecifier("MOD1", urlConfig)); - patches[06].PassSpecifier.Returns(new ForPassSpecifier("mod1", urlConfig)); - patches[07].PassSpecifier.Returns(new ForPassSpecifier("MOD1", urlConfig)); - patches[08].PassSpecifier.Returns(new AfterPassSpecifier("mod1", urlConfig)); - patches[09].PassSpecifier.Returns(new AfterPassSpecifier("MOD1", urlConfig)); - patches[10].PassSpecifier.Returns(new LastPassSpecifier("mod1")); - patches[11].PassSpecifier.Returns(new LastPassSpecifier("MOD1")); - patches[12].PassSpecifier.Returns(new BeforePassSpecifier("mod2", urlConfig)); - patches[13].PassSpecifier.Returns(new BeforePassSpecifier("MOD2", urlConfig)); - patches[14].PassSpecifier.Returns(new ForPassSpecifier("mod2", urlConfig)); - patches[15].PassSpecifier.Returns(new ForPassSpecifier("MOD2", urlConfig)); - patches[16].PassSpecifier.Returns(new AfterPassSpecifier("mod2", urlConfig)); - patches[17].PassSpecifier.Returns(new AfterPassSpecifier("MOD2", urlConfig)); - patches[18].PassSpecifier.Returns(new LastPassSpecifier("mod2")); - patches[19].PassSpecifier.Returns(new LastPassSpecifier("MOD2")); - patches[20].PassSpecifier.Returns(new FinalPassSpecifier()); - patches[21].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[00].PassSpecifier.Returns(new InsertPassSpecifier()); + patches[01].PassSpecifier.Returns(new InsertPassSpecifier()); + patches[02].PassSpecifier.Returns(new FirstPassSpecifier()); + patches[03].PassSpecifier.Returns(new FirstPassSpecifier()); + patches[04].PassSpecifier.Returns(new LegacyPassSpecifier()); + patches[05].PassSpecifier.Returns(new LegacyPassSpecifier()); + patches[06].PassSpecifier.Returns(new BeforePassSpecifier("mod1", urlConfig)); + patches[07].PassSpecifier.Returns(new BeforePassSpecifier("MOD1", urlConfig)); + patches[08].PassSpecifier.Returns(new ForPassSpecifier("mod1", urlConfig)); + patches[09].PassSpecifier.Returns(new ForPassSpecifier("MOD1", urlConfig)); + patches[10].PassSpecifier.Returns(new AfterPassSpecifier("mod1", urlConfig)); + patches[11].PassSpecifier.Returns(new AfterPassSpecifier("MOD1", urlConfig)); + patches[12].PassSpecifier.Returns(new LastPassSpecifier("mod1")); + patches[13].PassSpecifier.Returns(new LastPassSpecifier("MOD1")); + patches[14].PassSpecifier.Returns(new BeforePassSpecifier("mod2", urlConfig)); + patches[15].PassSpecifier.Returns(new BeforePassSpecifier("MOD2", urlConfig)); + patches[16].PassSpecifier.Returns(new ForPassSpecifier("mod2", urlConfig)); + patches[17].PassSpecifier.Returns(new ForPassSpecifier("MOD2", urlConfig)); + patches[18].PassSpecifier.Returns(new AfterPassSpecifier("mod2", urlConfig)); + patches[19].PassSpecifier.Returns(new AfterPassSpecifier("MOD2", urlConfig)); + patches[20].PassSpecifier.Returns(new LastPassSpecifier("mod2")); + patches[21].PassSpecifier.Returns(new LastPassSpecifier("MOD2")); + patches[22].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[23].PassSpecifier.Returns(new FinalPassSpecifier()); IPatchProgress progress = Substitute.For(); @@ -144,41 +148,44 @@ public void Test__Lifecycle() IPass[] passes = patchList.ToArray(); - Assert.Equal(11, passes.Length); + Assert.Equal(12, passes.Length); - Assert.Equal(":FIRST", passes[0].Name); + Assert.Equal(":INSERT (initial)", passes[0].Name); Assert.Equal(new[] { patches[0], patches[1] }, passes[0]); - Assert.Equal(":LEGACY (default)", passes[1].Name); + Assert.Equal(":FIRST", passes[1].Name); Assert.Equal(new[] { patches[2], patches[3] }, passes[1]); - Assert.Equal(":BEFORE[MOD1]", passes[2].Name); + Assert.Equal(":LEGACY (default)", passes[2].Name); Assert.Equal(new[] { patches[4], patches[5] }, passes[2]); - Assert.Equal(":FOR[MOD1]", passes[3].Name); + Assert.Equal(":BEFORE[MOD1]", passes[3].Name); Assert.Equal(new[] { patches[6], patches[7] }, passes[3]); - Assert.Equal(":AFTER[MOD1]", passes[4].Name); + Assert.Equal(":FOR[MOD1]", passes[4].Name); Assert.Equal(new[] { patches[8], patches[9] }, passes[4]); - Assert.Equal(":BEFORE[MOD2]", passes[5].Name); - Assert.Equal(new[] { patches[12], patches[13] }, passes[5]); + Assert.Equal(":AFTER[MOD1]", passes[5].Name); + Assert.Equal(new[] { patches[10], patches[11] }, passes[5]); - Assert.Equal(":FOR[MOD2]", passes[6].Name); + Assert.Equal(":BEFORE[MOD2]", passes[6].Name); Assert.Equal(new[] { patches[14], patches[15] }, passes[6]); - Assert.Equal(":AFTER[MOD2]", passes[7].Name); + Assert.Equal(":FOR[MOD2]", passes[7].Name); Assert.Equal(new[] { patches[16], patches[17] }, passes[7]); - Assert.Equal(":LAST[MOD1]", passes[8].Name); - Assert.Equal(new[] { patches[10], patches[11] }, passes[8]); + Assert.Equal(":AFTER[MOD2]", passes[8].Name); + Assert.Equal(new[] { patches[18], patches[19] }, passes[8]); - Assert.Equal(":LAST[MOD2]", passes[9].Name); - Assert.Equal(new[] { patches[18], patches[19] }, passes[9]); + Assert.Equal(":LAST[MOD1]", passes[9].Name); + Assert.Equal(new[] { patches[12], patches[13] }, passes[9]); - Assert.Equal(":FINAL", passes[10].Name); + Assert.Equal(":LAST[MOD2]", passes[10].Name); Assert.Equal(new[] { patches[20], patches[21] }, passes[10]); + Assert.Equal(":FINAL", passes[11].Name); + Assert.Equal(new[] { patches[22], patches[23] }, passes[11]); + progress.Received(patches.Length).PatchAdded(); } } diff --git a/ModuleManagerTests/Patches/CopyPatchTest.cs b/ModuleManagerTests/Patches/CopyPatchTest.cs index 635e5e22..c0e8e14a 100644 --- a/ModuleManagerTests/Patches/CopyPatchTest.cs +++ b/ModuleManagerTests/Patches/CopyPatchTest.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Xunit; using NSubstitute; using TestUtils; @@ -77,72 +79,97 @@ public void TestApply() { UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); - UrlDir.UrlConfig urlConfig1 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config1 = new TestConfigNode("NODE") { { "foo", "bar" }, - }, file); + }; - UrlDir.UrlConfig urlConfig2 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config2 = new TestConfigNode("NODE") { { "foo", "bar" }, - }, file); + }; - UrlDir.UrlConfig urlConfig3 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); - UrlDir.UrlConfig urlConfig4 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + ConfigNode config3 = new ConfigNode("NODE"); + ConfigNode config4 = new ConfigNode("NODE"); INodeMatcher nodeMatcher = Substitute.For(); - nodeMatcher.IsMatch(urlConfig1.config).Returns(false); - nodeMatcher.IsMatch(urlConfig2.config).Returns(true); - nodeMatcher.IsMatch(urlConfig3.config).Returns(false); - nodeMatcher.IsMatch(urlConfig4.config).Returns(true); + nodeMatcher.IsMatch(config1).Returns(false); + nodeMatcher.IsMatch(config2).Returns(true); + nodeMatcher.IsMatch(config3).Returns(false); + nodeMatcher.IsMatch(config4).Returns(true); CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") { { "@foo", "baz" }, { "pqr", "stw" }, }), nodeMatcher, Substitute.For()); - + + IProtoUrlConfig protoUrlConfig1 = Substitute.For(); + IProtoUrlConfig protoUrlConfig2 = Substitute.For(); + IProtoUrlConfig protoUrlConfig3 = Substitute.For(); + IProtoUrlConfig protoUrlConfig4 = Substitute.For(); + + protoUrlConfig1.Node.Returns(config1); + protoUrlConfig2.Node.Returns(config2); + protoUrlConfig3.Node.Returns(config3); + protoUrlConfig4.Node.Returns(config4); + + protoUrlConfig1.UrlFile.Returns(file); + protoUrlConfig2.UrlFile.Returns(file); + protoUrlConfig3.UrlFile.Returns(file); + protoUrlConfig4.UrlFile.Returns(file); + + LinkedList configs = new LinkedList(); + configs.AddLast(protoUrlConfig1); + configs.AddLast(protoUrlConfig2); + configs.AddLast(protoUrlConfig3); + configs.AddLast(protoUrlConfig4); + IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); + + IProtoUrlConfig[] newConfigs = configs.ToArray(); - Assert.Equal(6, file.configs.Count); + Assert.Equal(6, newConfigs.Length); - Assert.Same(urlConfig1, file.configs[0]); + Assert.Same(protoUrlConfig1, newConfigs[0]); AssertNodesEqual(new TestConfigNode("NODE") { { "foo", "bar" }, - }, file.configs[0].config); + }, newConfigs[0].Node); - Assert.Same(urlConfig2, file.configs[1]); + Assert.Same(protoUrlConfig2, newConfigs[1]); AssertNodesEqual(new TestConfigNode("NODE") { { "foo", "bar" }, - }, file.configs[1].config); + }, newConfigs[1].Node); - Assert.Same(urlConfig3, file.configs[2]); - AssertNodesEqual(new ConfigNode("NODE"), file.configs[2].config); - - Assert.Same(urlConfig4, file.configs[3]); - AssertNodesEqual(new ConfigNode("NODE"), file.configs[3].config); - AssertNodesEqual(new TestConfigNode("NODE") { { "foo", "baz" }, { "pqr", "stw" }, - }, file.configs[4].config); + }, newConfigs[2].Node); + Assert.Same(file, newConfigs[2].UrlFile); + + Assert.Same(protoUrlConfig3, newConfigs[3]); + AssertNodesEqual(new ConfigNode("NODE"), newConfigs[3].Node); + + Assert.Same(protoUrlConfig4, newConfigs[4]); + AssertNodesEqual(new ConfigNode("NODE"), newConfigs[4].Node); AssertNodesEqual(new TestConfigNode("NODE") { { "pqr", "stw" }, - }, file.configs[5].config); + }, newConfigs[5].Node); + Assert.Same(file, newConfigs[5].UrlFile); Received.InOrder(delegate { - progress.ApplyingCopy(urlConfig2, patch.UrlConfig); - progress.ApplyingCopy(urlConfig4, patch.UrlConfig); + progress.ApplyingCopy(protoUrlConfig2, patch.UrlConfig); + progress.ApplyingCopy(protoUrlConfig4, patch.UrlConfig); }); progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); @@ -158,15 +185,15 @@ public void TestApply__NameChanged() { UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config = new TestConfigNode("NODE") { { "name", "000" }, { "foo", "bar" }, - }, file); + }; INodeMatcher nodeMatcher = Substitute.For(); - nodeMatcher.IsMatch(urlConfig.config).Returns(true); + nodeMatcher.IsMatch(config).Returns(true); CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") { @@ -175,28 +202,38 @@ public void TestApply__NameChanged() { "pqr", "stw" }, }), nodeMatcher, Substitute.For()); + IProtoUrlConfig protoConfig = Substitute.For(); + protoConfig.Node.Returns(config); + protoConfig.UrlFile.Returns(file); + + LinkedList configs = new LinkedList(); + configs.AddLast(protoConfig); + IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); + + IProtoUrlConfig[] newConfigs = configs.ToArray(); - Assert.Equal(2, file.configs.Count); + Assert.Equal(2, newConfigs.Length); - Assert.Same(urlConfig, file.configs[0]); + Assert.Same(protoConfig, newConfigs[0]); AssertNodesEqual(new TestConfigNode("NODE") { { "name", "000" }, { "foo", "bar" }, - }, file.configs[0].config); + }, newConfigs[0].Node); AssertNodesEqual(new TestConfigNode("NODE") { { "name", "001" }, { "foo", "baz" }, { "pqr", "stw" }, - }, file.configs[1].config); + }, newConfigs[1].Node); + Assert.Same(file, newConfigs[1].UrlFile); - progress.Received().ApplyingCopy(urlConfig, patch.UrlConfig); + progress.Received().ApplyingCopy(protoConfig, patch.UrlConfig); progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); @@ -209,17 +246,15 @@ public void TestApply__NameChanged() [Fact] public void TestApply__NameNotChanged() { - UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); - - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config = new TestConfigNode("NODE") { { "name", "000" }, { "foo", "bar" }, - }, file); + }; INodeMatcher nodeMatcher = Substitute.For(); - nodeMatcher.IsMatch(urlConfig.config).Returns(true); + nodeMatcher.IsMatch(config).Returns(true); CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("+NODE") { @@ -227,21 +262,28 @@ public void TestApply__NameNotChanged() { "pqr", "stw" }, }), nodeMatcher, Substitute.For()); + IProtoUrlConfig protoConfig = Substitute.For(); + protoConfig.Node.Returns(config); + protoConfig.FullUrl.Returns("abc/def.cfg/NODE"); + + LinkedList configs = new LinkedList(); + configs.AddLast(protoConfig); + IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); - Assert.Single(file.configs); + Assert.Single(configs); - Assert.Same(urlConfig, file.configs[0]); + Assert.Same(protoConfig, configs.First.Value); AssertNodesEqual(new TestConfigNode("NODE") { { "name", "000" }, { "foo", "bar" }, - }, file.configs[0].config); + }, configs.First.Value.Node); - progress.Received().Error(patch.UrlConfig, "Error - when applying copy ghi/jkl/+NODE to abc/def/NODE - the copy needs to have a different name than the parent (use @name = xxx)"); + progress.Received().Error(patch.UrlConfig, "Error - when applying copy ghi/jkl/+NODE to abc/def.cfg/NODE - the copy needs to have a different name than the parent (use @name = xxx)"); progress.DidNotReceiveWithAnyArgs().ApplyingUpdate(null, null); progress.DidNotReceiveWithAnyArgs().ApplyingCopy(null, null); @@ -252,7 +294,7 @@ public void TestApply__NameNotChanged() } [Fact] - public void TestApply__FileNull() + public void TestApply__DatabaseConfigsNullNull() { CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate @@ -260,7 +302,7 @@ public void TestApply__FileNull() patch.Apply(null, Substitute.For(), Substitute.For()); }); - Assert.Equal("file", ex.ParamName); + Assert.Equal("databaseConfigs", ex.ParamName); } [Fact] @@ -269,7 +311,7 @@ public void TestApply__ProgressNull() CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { - patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); + patch.Apply(new LinkedList(), null, Substitute.For()); }); Assert.Equal("progress", ex.ParamName); @@ -281,7 +323,7 @@ public void TestApply__LoggerNull() CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { - patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); + patch.Apply(new LinkedList(), Substitute.For(), null); }); Assert.Equal("logger", ex.ParamName); diff --git a/ModuleManagerTests/Patches/DeletePatchTest.cs b/ModuleManagerTests/Patches/DeletePatchTest.cs index 743b7bb2..1cfa950d 100644 --- a/ModuleManagerTests/Patches/DeletePatchTest.cs +++ b/ModuleManagerTests/Patches/DeletePatchTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Xunit; using NSubstitute; using TestUtils; @@ -75,28 +76,42 @@ public void TestPassSpecifier() [Fact] public void TestApply() { - UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); - - UrlDir.UrlConfig urlConfig1 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); - UrlDir.UrlConfig urlConfig2 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); - UrlDir.UrlConfig urlConfig3 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); - UrlDir.UrlConfig urlConfig4 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + ConfigNode config1 = new ConfigNode("NODE"); + ConfigNode config2 = new ConfigNode("NODE"); + ConfigNode config3 = new ConfigNode("NODE"); + ConfigNode config4 = new ConfigNode("NODE"); INodeMatcher nodeMatcher = Substitute.For(); - nodeMatcher.IsMatch(urlConfig1.config).Returns(false); - nodeMatcher.IsMatch(urlConfig2.config).Returns(true); - nodeMatcher.IsMatch(urlConfig3.config).Returns(false); - nodeMatcher.IsMatch(urlConfig4.config).Returns(true); + nodeMatcher.IsMatch(config1).Returns(false); + nodeMatcher.IsMatch(config2).Returns(true); + nodeMatcher.IsMatch(config3).Returns(false); + nodeMatcher.IsMatch(config4).Returns(true); DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("!NODE")), nodeMatcher, Substitute.For()); + IProtoUrlConfig urlConfig1 = Substitute.For(); + IProtoUrlConfig urlConfig2 = Substitute.For(); + IProtoUrlConfig urlConfig3 = Substitute.For(); + IProtoUrlConfig urlConfig4 = Substitute.For(); + + urlConfig1.Node.Returns(config1); + urlConfig2.Node.Returns(config2); + urlConfig3.Node.Returns(config3); + urlConfig4.Node.Returns(config4); + + LinkedList configs = new LinkedList(); + configs.AddLast(urlConfig1); + configs.AddLast(urlConfig2); + configs.AddLast(urlConfig3); + configs.AddLast(urlConfig4); + IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); - Assert.Equal(new[] { urlConfig1, urlConfig3 }, file.configs); + Assert.Equal(new[] { urlConfig1, urlConfig3 }, configs); Received.InOrder(delegate { @@ -113,7 +128,7 @@ public void TestApply() } [Fact] - public void TestApply__FileNull() + public void TestApply__DatabaseConfigsNull() { DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate @@ -121,7 +136,7 @@ public void TestApply__FileNull() patch.Apply(null, Substitute.For(), Substitute.For()); }); - Assert.Equal("file", ex.ParamName); + Assert.Equal("databaseConfigs", ex.ParamName); } [Fact] @@ -130,7 +145,7 @@ public void TestApply__ProgressNull() DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { - patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); + patch.Apply(new LinkedList(), null, Substitute.For()); }); Assert.Equal("progress", ex.ParamName); @@ -142,7 +157,7 @@ public void TestApply__LoggerNull() DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { - patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); + patch.Apply(new LinkedList(), Substitute.For(), null); }); Assert.Equal("logger", ex.ParamName); diff --git a/ModuleManagerTests/Patches/EditPatchTest.cs b/ModuleManagerTests/Patches/EditPatchTest.cs index e72963f1..7ce5f6a9 100644 --- a/ModuleManagerTests/Patches/EditPatchTest.cs +++ b/ModuleManagerTests/Patches/EditPatchTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Xunit; using NSubstitute; using UnityEngine; @@ -79,60 +80,83 @@ public void TestApply() { UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); - UrlDir.UrlConfig urlConfig1 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config1 = new TestConfigNode("NODE") { { "foo", "bar" }, - }, file); + }; - UrlDir.UrlConfig urlConfig2 = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config2 = new TestConfigNode("NODE") { { "foo", "bar" }, - }, file); + }; - UrlDir.UrlConfig urlConfig3 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); - UrlDir.UrlConfig urlConfig4 = UrlBuilder.CreateConfig(new ConfigNode("NODE"), file); + ConfigNode config3 = new ConfigNode("NODE"); + ConfigNode config4 = new ConfigNode("NODE"); INodeMatcher nodeMatcher = Substitute.For(); - nodeMatcher.IsMatch(urlConfig1.config).Returns(false); - nodeMatcher.IsMatch(urlConfig2.config).Returns(true); - nodeMatcher.IsMatch(urlConfig3.config).Returns(false); - nodeMatcher.IsMatch(urlConfig4.config).Returns(true); + nodeMatcher.IsMatch(config1).Returns(false); + nodeMatcher.IsMatch(config2).Returns(true); + nodeMatcher.IsMatch(config3).Returns(false); + nodeMatcher.IsMatch(config4).Returns(true); EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") { { "@foo", "baz" }, { "pqr", "stw" }, }), nodeMatcher, Substitute.For()); - + + IProtoUrlConfig urlConfig1 = Substitute.For(); + IProtoUrlConfig urlConfig2 = Substitute.For(); + IProtoUrlConfig urlConfig3 = Substitute.For(); + IProtoUrlConfig urlConfig4 = Substitute.For(); + + urlConfig1.Node.Returns(config1); + urlConfig2.Node.Returns(config2); + urlConfig3.Node.Returns(config3); + urlConfig4.Node.Returns(config4); + + urlConfig1.UrlFile.Returns(file); + urlConfig2.UrlFile.Returns(file); + urlConfig3.UrlFile.Returns(file); + urlConfig4.UrlFile.Returns(file); + + LinkedList configs = new LinkedList(); + configs.AddLast(urlConfig1); + configs.AddLast(urlConfig2); + configs.AddLast(urlConfig3); + configs.AddLast(urlConfig4); + IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); + + IProtoUrlConfig[] newConfigs = configs.ToArray(); - Assert.Equal(4, file.configs.Count); + Assert.Equal(4, newConfigs.Length); - Assert.Same(urlConfig1, file.configs[0]); + Assert.Same(urlConfig1, newConfigs[0]); AssertNodesEqual(new TestConfigNode("NODE") { { "foo", "bar" }, - }, file.configs[0].config); + }, newConfigs[0].Node); - Assert.NotSame(urlConfig2, file.configs[1]); AssertNodesEqual(new TestConfigNode("NODE") { { "foo", "baz" }, { "pqr", "stw" }, - }, file.configs[1].config); + }, newConfigs[1].Node); + Assert.Same(file, newConfigs[1].UrlFile); - Assert.Same(urlConfig3, file.configs[2]); - AssertNodesEqual(new ConfigNode("NODE"), file.configs[2].config); + Assert.Same(urlConfig3, newConfigs[2]); + AssertNodesEqual(new ConfigNode("NODE"), newConfigs[2].Node); - Assert.NotSame(urlConfig4, file.configs[3]); AssertNodesEqual(new TestConfigNode("NODE") { { "pqr", "stw" }, - }, file.configs[3].config); + }, newConfigs[3].Node); + Assert.Same(file, newConfigs[3].UrlFile); Received.InOrder(delegate { @@ -153,11 +177,11 @@ public void TestApply__Loop() { UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config = new TestConfigNode("NODE") { { "name", "000" }, { "aaa", "1" }, - }, file); + }; INodeMatcher nodeMatcher = Substitute.For(); @@ -170,16 +194,23 @@ public void TestApply__Loop() new ConfigNode("MM_PATCH_LOOP"), }), nodeMatcher, Substitute.For()); + IProtoUrlConfig urlConfig = Substitute.For(); + urlConfig.Node.Returns(config); + urlConfig.UrlFile.Returns(file); + urlConfig.FullUrl.Returns("abc/def.cfg/NODE"); + + LinkedList configs = new LinkedList(); + configs.AddLast(urlConfig); + IPatchProgress progress = Substitute.For(); IBasicLogger logger = Substitute.For(); - List modifiedUrlConfigs = new List(); - progress.ApplyingUpdate(Arg.Do(url => modifiedUrlConfigs.Add(url)), patch.UrlConfig); + List modifiedUrlConfigs = new List(); + progress.ApplyingUpdate(Arg.Do(url => modifiedUrlConfigs.Add(url)), patch.UrlConfig); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); - Assert.Single(file.configs); - Assert.NotSame(urlConfig, file.configs[0]); + Assert.Single(configs); AssertNodesEqual(new TestConfigNode("NODE") { { "name", "000" }, @@ -188,7 +219,8 @@ public void TestApply__Loop() { "bbb", "002" }, { "bbb", "002" }, { "bbb", "002" }, - }, file.configs[0].config); + }, configs.First.Value.Node); + Assert.Same(file, configs.First.Value.UrlFile); Assert.Same(urlConfig, modifiedUrlConfigs[0]); Assert.NotSame(urlConfig, modifiedUrlConfigs[1]); @@ -197,7 +229,7 @@ public void TestApply__Loop() Received.InOrder(delegate { - logger.Log(LogType.Log, "Looping on ghi/jkl/@NODE to abc/def/NODE"); + logger.Log(LogType.Log, "Looping on ghi/jkl/@NODE to abc/def.cfg/NODE"); progress.ApplyingUpdate(urlConfig, patch.UrlConfig); progress.ApplyingUpdate(modifiedUrlConfigs[1], patch.UrlConfig); progress.ApplyingUpdate(modifiedUrlConfigs[2], patch.UrlConfig); @@ -213,7 +245,7 @@ public void TestApply__Loop() } [Fact] - public void TestApply__FileNull() + public void TestApply__DatabaseConfigsNull() { EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate @@ -221,7 +253,7 @@ public void TestApply__FileNull() patch.Apply(null, Substitute.For(), Substitute.For()); }); - Assert.Equal("file", ex.ParamName); + Assert.Equal("databaseConfigs", ex.ParamName); } [Fact] @@ -230,7 +262,7 @@ public void TestApply__ProgressNull() EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { - patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), null, Substitute.For()); + patch.Apply(new LinkedList(), null, Substitute.For()); }); Assert.Equal("progress", ex.ParamName); @@ -242,7 +274,7 @@ public void TestApply__LoggerNull() EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); ArgumentNullException ex = Assert.Throws(delegate { - patch.Apply(UrlBuilder.CreateFile("abc/def.cfg"), Substitute.For(), null); + patch.Apply(new LinkedList(), Substitute.For(), null); }); Assert.Equal("logger", ex.ParamName); diff --git a/ModuleManagerTests/Patches/InsertPatchTest.cs b/ModuleManagerTests/Patches/InsertPatchTest.cs new file mode 100644 index 00000000..b9944a9d --- /dev/null +++ b/ModuleManagerTests/Patches/InsertPatchTest.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager; +using ModuleManager.Logging; +using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + + +namespace ModuleManagerTests.Patches +{ + public class InsertPatchTest + { + [Fact] + public void TestConstructor__UrlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new InsertPatch(null, "A_NODE", Substitute.For()); + }); + + Assert.Equal("urlConfig", ex.ParamName); + } + + [Fact] + public void TestConstructor__NodeTypeNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), null, Substitute.For()); + }); + + Assert.Equal("nodeType", ex.ParamName); + } + + [Fact] + public void TestConstructor__PassSpecifierNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", null); + }); + + Assert.Equal("passSpecifier", ex.ParamName); + } + + [Fact] + public void TestUrlConfig() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode()); + InsertPatch patch = new InsertPatch(urlConfig, "A_NODE", Substitute.For()); + + Assert.Same(urlConfig, patch.UrlConfig); + } + + [Fact] + public void TestNodeType() + { + InsertPatch patch = new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", Substitute.For()); + + Assert.Equal("A_NODE", patch.NodeType); + } + + [Fact] + public void TestPassSpecifier() + { + IPassSpecifier passSpecifier = Substitute.For(); + InsertPatch patch = new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", passSpecifier); + + Assert.Same(passSpecifier, patch.PassSpecifier); + } + + [Fact] + public void TestApply() + { + UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new TestConfigNode("A_NODE:NEEDS[someMod]:FOR[somePass]") + { + { "key1", "value1" }, + { "key2", "value2" }, + new TestConfigNode("NODE_1") + { + { "key3", "value3" }, + }, + new TestConfigNode("NODE_2") + { + { "key4", "value4" }, + }, + }); + + InsertPatch patch = new InsertPatch(urlConfig, "A_NODE", Substitute.For()); + + LinkedList databaseConfigs = new LinkedList(); + + IProtoUrlConfig config1 = Substitute.For(); + IProtoUrlConfig config2 = Substitute.For(); + + databaseConfigs.AddLast(config1); + databaseConfigs.AddLast(config2); + + patch.Apply(databaseConfigs, Substitute.For(), Substitute.For()); + + IProtoUrlConfig[] databaseConfigsArray = databaseConfigs.ToArray(); + Assert.Equal(3, databaseConfigsArray.Length); + Assert.Same(config1, databaseConfigsArray[0]); + Assert.Same(config2, databaseConfigsArray[1]); + + Assert.Same(urlConfig.parent, databaseConfigsArray[2].UrlFile); + Assert.Equal("abc/def.cfg", databaseConfigsArray[2].FileUrl); + Assert.Equal("A_NODE", databaseConfigsArray[2].NodeType); + Assert.Equal("abc/def.cfg/A_NODE", databaseConfigsArray[2].FullUrl); + + Assert.NotSame(urlConfig.config, databaseConfigsArray[2].Node); + Assert.Equal("A_NODE", databaseConfigsArray[2].Node.name); + Assert.Equal("A_NODE:NEEDS[someMod]:FOR[somePass]", urlConfig.config.name); // make sure this hasn't been changed + Assert.Equal(2, databaseConfigsArray[2].Node.values.Count); + Assert.Equal("key1", databaseConfigsArray[2].Node.values[0].name); + Assert.Equal("value1", databaseConfigsArray[2].Node.values[0].value); + Assert.Equal("key2", databaseConfigsArray[2].Node.values[1].name); + Assert.Equal("value2", databaseConfigsArray[2].Node.values[1].value); + Assert.Equal(2, databaseConfigsArray[2].Node.nodes.Count); + Assert.Equal("NODE_1", databaseConfigsArray[2].Node.nodes[0].name); + Assert.Equal(1, databaseConfigsArray[2].Node.nodes[0].values.Count); + Assert.Equal("key3", databaseConfigsArray[2].Node.nodes[0].values[0].name); + Assert.Equal("value3", databaseConfigsArray[2].Node.nodes[0].values[0].value); + Assert.Equal(0, databaseConfigsArray[2].Node.nodes[0].nodes.Count); + Assert.Equal("NODE_2", databaseConfigsArray[2].Node.nodes[1].name); + Assert.Equal(1, databaseConfigsArray[2].Node.nodes[1].values.Count); + Assert.Equal("key4", databaseConfigsArray[2].Node.nodes[1].values[0].name); + Assert.Equal("value4", databaseConfigsArray[2].Node.nodes[1].values[0].value); + Assert.Equal(0, databaseConfigsArray[2].Node.nodes[1].nodes.Count); + } + + [Fact] + public void TestApply__DatabaseConfigsNull() + { + InsertPatch patch = new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(null, Substitute.For(), Substitute.For()); + }); + + Assert.Equal("configs", ex.ParamName); + } + + [Fact] + public void TestApply__ProgressNull() + { + InsertPatch patch = new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(new LinkedList(), null, Substitute.For()); + }); + + Assert.Equal("progress", ex.ParamName); + } + + [Fact] + public void TestApply__LoggerNull() + { + InsertPatch patch = new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(new LinkedList(), Substitute.For(), null); + }); + + Assert.Equal("logger", ex.ParamName); + } + } +} diff --git a/ModuleManagerTests/Patches/PatchCompilerTest.cs b/ModuleManagerTests/Patches/PatchCompilerTest.cs index 5e660876..5ad3c794 100644 --- a/ModuleManagerTests/Patches/PatchCompilerTest.cs +++ b/ModuleManagerTests/Patches/PatchCompilerTest.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Xunit; using NSubstitute; using TestUtils; @@ -17,6 +19,41 @@ public class PatchCompilerTest private readonly UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); private readonly PatchCompiler patchCompiler = new PatchCompiler(); + [Fact] + public void TestCompilePatch__Insert() + { + ProtoPatch protoPatch = new ProtoPatch( + UrlBuilder.CreateConfig(new TestConfigNode("NODEE") + { + { "name", "foo" }, + { "bar", "bleh" }, + }, file), + Command.Insert, + "NODE", + "foo", + null, + "#bar", + Substitute.For() + ); + + InsertPatch patch = Assert.IsType(patchCompiler.CompilePatch(protoPatch)); + + Assert.Same(protoPatch.urlConfig, patch.UrlConfig); + + LinkedList configs = new LinkedList(); + + patch.Apply(configs, progress, logger); + + Assert.Equal(1, configs.Count); + Assert.NotSame(protoPatch.urlConfig.config, configs.First.Value.Node); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "bleh" }, + }, configs.First.Value.Node); + Assert.Same(file, configs.First.Value.UrlFile); + } + [Fact] public void TestCompilePatch__Edit() { @@ -38,25 +75,33 @@ public void TestCompilePatch__Edit() Assert.Same(protoPatch.urlConfig, patch.UrlConfig); AssertNodeMatcher(patch.NodeMatcher); - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config = new TestConfigNode("NODE") { { "name", "foo" }, { "bar", "baz" }, - }, file); + }; - patch.Apply(file, progress, logger); + IProtoUrlConfig urlConfig = Substitute.For(); + urlConfig.Node.Returns(config); + urlConfig.UrlFile.Returns(file); + + LinkedList configs = new LinkedList(); + configs.AddLast(urlConfig); + + patch.Apply(configs, progress, logger); AssertNoErrors(); progress.Received().ApplyingUpdate(urlConfig, protoPatch.urlConfig); - Assert.Single(file.configs); - Assert.NotSame(urlConfig, file.configs[0]); + Assert.Single(configs); + Assert.NotSame(config, configs.First.Value.Node); AssertNodesEqual(new TestConfigNode("NODE") { { "name", "foo" }, { "bar", "bleh" }, - }, file.configs[0].config); + }, configs.First.Value.Node); + Assert.Same(file, configs.First.Value.UrlFile); } [Fact] @@ -81,30 +126,40 @@ public void TestCompilePatch__Copy() Assert.Same(protoPatch.urlConfig, patch.UrlConfig); AssertNodeMatcher(patch.NodeMatcher); - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + ConfigNode config = new TestConfigNode("NODE") { { "name", "foo" }, { "bar", "baz" }, - }, file); + }; + + IProtoUrlConfig urlConfig = Substitute.For(); + urlConfig.Node.Returns(config); + urlConfig.UrlFile.Returns(file); + + LinkedList configs = new LinkedList(); + configs.AddLast(urlConfig); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); AssertNoErrors(); progress.Received().ApplyingCopy(urlConfig, protoPatch.urlConfig); - Assert.Equal(2, file.configs.Count); - Assert.Same(urlConfig, file.configs[0]); + IProtoUrlConfig[] newConfigs = configs.ToArray(); + + Assert.Equal(2, newConfigs.Length); + Assert.Same(config, newConfigs[0].Node); AssertNodesEqual(new TestConfigNode("NODE") { { "name", "foo" }, { "bar", "baz" }, - }, file.configs[0].config); + }, newConfigs[0].Node); AssertNodesEqual(new TestConfigNode("NODE") { { "name", "boo" }, { "bar", "bleh" }, - }, file.configs[1].config); + }, newConfigs[1].Node); + Assert.Same(file, newConfigs[1].UrlFile); } [Fact] @@ -125,19 +180,23 @@ public void TestCompilePatch__Delete() Assert.Same(protoPatch.urlConfig, patch.UrlConfig); AssertNodeMatcher(patch.NodeMatcher); - UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig(new TestConfigNode("NODE") + IProtoUrlConfig urlConfig = Substitute.For(); + urlConfig.Node.Returns(new TestConfigNode("NODE") { { "name", "foo" }, { "bar", "baz" }, - }, file); + }); + + LinkedList configs = new LinkedList(); + configs.AddLast(urlConfig); - patch.Apply(file, progress, logger); + patch.Apply(configs, progress, logger); AssertNoErrors(); progress.Received().ApplyingDelete(urlConfig, protoPatch.urlConfig); - Assert.Empty(file.configs); + Assert.Empty(configs); } [Fact] diff --git a/ModuleManagerTests/Progress/PatchProgressTest.cs b/ModuleManagerTests/Progress/PatchProgressTest.cs index caaced90..c7f448e1 100644 --- a/ModuleManagerTests/Progress/PatchProgressTest.cs +++ b/ModuleManagerTests/Progress/PatchProgressTest.cs @@ -3,9 +3,9 @@ using NSubstitute; using UnityEngine; using TestUtils; +using ModuleManager; using ModuleManager.Logging; using ModuleManager.Progress; -using NodeStack = ModuleManager.Collections.ImmutableStack; namespace ModuleManagerTests { @@ -30,13 +30,14 @@ public void Test__Constructor__Nested() Assert.Equal(0, progress.Counter.patchedNodes); - UrlDir.UrlConfig original = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); + IProtoUrlConfig original = Substitute.For(); + original.FullUrl.Returns("abc/def.cfg/SOME_NODE"); UrlDir.UrlConfig patch1 = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("@SOME_NODE")); progress2.ApplyingUpdate(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); logger.DidNotReceiveWithAnyArgs().Log(LogType.Log, null); - logger2.Received().Log(LogType.Log, "Applying update ghi/jkl/@SOME_NODE to abc/def/SOME_NODE"); + logger2.Received().Log(LogType.Log, "Applying update ghi/jkl/@SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] @@ -52,7 +53,8 @@ public void TestPatchAdded() [Fact] public void TestApplyingUpdate() { - UrlDir.UrlConfig original = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); + IProtoUrlConfig original = Substitute.For(); + original.FullUrl.Returns("abc/def.cfg/SOME_NODE"); UrlDir.UrlConfig patch1 = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("@SOME_NODE")); UrlDir.UrlConfig patch2 = UrlBuilder.CreateConfig("pqr/stu", new ConfigNode("@SOME_NODE")); @@ -60,17 +62,18 @@ public void TestApplyingUpdate() progress.ApplyingUpdate(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying update ghi/jkl/@SOME_NODE to abc/def/SOME_NODE"); + logger.Received().Log(LogType.Log, "Applying update ghi/jkl/@SOME_NODE to abc/def.cfg/SOME_NODE"); progress.ApplyingUpdate(original, patch2); Assert.Equal(2, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying update pqr/stu/@SOME_NODE to abc/def/SOME_NODE"); + logger.Received().Log(LogType.Log, "Applying update pqr/stu/@SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] public void TesApplyingCopy() { - UrlDir.UrlConfig original = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); + IProtoUrlConfig original = Substitute.For(); + original.FullUrl.Returns("abc/def.cfg/SOME_NODE"); UrlDir.UrlConfig patch1 = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("+SOME_NODE")); UrlDir.UrlConfig patch2 = UrlBuilder.CreateConfig("pqr/stu", new ConfigNode("+SOME_NODE")); @@ -78,17 +81,18 @@ public void TesApplyingCopy() progress.ApplyingCopy(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying copy ghi/jkl/+SOME_NODE to abc/def/SOME_NODE"); + logger.Received().Log(LogType.Log, "Applying copy ghi/jkl/+SOME_NODE to abc/def.cfg/SOME_NODE"); progress.ApplyingCopy(original, patch2); Assert.Equal(2, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying copy pqr/stu/+SOME_NODE to abc/def/SOME_NODE"); + logger.Received().Log(LogType.Log, "Applying copy pqr/stu/+SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] public void TesApplyingDelete() { - UrlDir.UrlConfig original = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); + IProtoUrlConfig original = Substitute.For(); + original.FullUrl.Returns("abc/def.cfg/SOME_NODE"); UrlDir.UrlConfig patch1 = UrlBuilder.CreateConfig("ghi/jkl", new ConfigNode("!SOME_NODE")); UrlDir.UrlConfig patch2 = UrlBuilder.CreateConfig("pqr/stu", new ConfigNode("!SOME_NODE")); @@ -96,11 +100,11 @@ public void TesApplyingDelete() progress.ApplyingDelete(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying delete ghi/jkl/!SOME_NODE to abc/def/SOME_NODE"); + logger.Received().Log(LogType.Log, "Applying delete ghi/jkl/!SOME_NODE to abc/def.cfg/SOME_NODE"); progress.ApplyingDelete(original, patch2); Assert.Equal(2, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying delete pqr/stu/!SOME_NODE to abc/def/SOME_NODE"); + logger.Received().Log(LogType.Log, "Applying delete pqr/stu/!SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] @@ -236,6 +240,18 @@ public void TestWarning() [Fact] public void TestError() + { + Assert.Equal(0, progress.Counter.errors); + + progress.Error("An error message no one is going to read"); + Assert.Equal(1, progress.Counter.errors); + + progress.Error("Maybe someone will read this one"); + Assert.Equal(2, progress.Counter.errors); + } + + [Fact] + public void TestError__Config() { UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_NODE")); UrlDir.UrlConfig config2 = UrlBuilder.CreateConfig("abc/def", new ConfigNode("SOME_OTHER_NODE")); From 72f37f1b58158c6dbf5fed3aef2cd06d3ffc9fc5 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 29 Oct 2018 23:21:26 -0700 Subject: [PATCH 043/140] Move path initialization to static initializer By the time the plugin is even loaded this should all exist --- ModuleManager/MMPatchLoader.cs | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index fee3b2bd..749c170e 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -37,18 +37,18 @@ public class MMPatchLoader : LoadingSystem private static readonly Dictionary regexCache = new Dictionary(); - private static string cachePath; + private static readonly string cachePath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigCache"); - internal static string techTreeFile; - internal static string techTreePath; + internal static readonly string techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree"); + internal static readonly string techTreePath = Path.Combine(KSPUtil.ApplicationRootPath, techTreeFile); - internal static string physicsFile; - internal static string physicsPath; - private static string defaultPhysicsPath; + internal static readonly string physicsFile = Path.Combine("GameData", "ModuleManager.Physics"); + internal static readonly string physicsPath = Path.Combine(KSPUtil.ApplicationRootPath, physicsFile); + private static readonly string defaultPhysicsPath = Path.Combine(KSPUtil.ApplicationRootPath, "Physics.cfg"); - internal static string partDatabasePath; + internal static readonly string partDatabasePath = Path.Combine(KSPUtil.ApplicationRootPath, "PartDatabase.cfg"); - private static string shaPath; + private static readonly string shaPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigSHA"); private UrlDir.UrlFile physicsUrlFile; @@ -72,15 +72,6 @@ private void Awake() } Instance = this; - cachePath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigCache"); - techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree"); - techTreePath = Path.Combine(KSPUtil.ApplicationRootPath, techTreeFile); - physicsFile = Path.Combine("GameData", "ModuleManager.Physics"); - physicsPath = Path.Combine(KSPUtil.ApplicationRootPath, physicsFile); - defaultPhysicsPath = Path.Combine(KSPUtil.ApplicationRootPath, "Physics.cfg"); - partDatabasePath = Path.Combine(KSPUtil.ApplicationRootPath, "PartDatabase.cfg"); - shaPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigSHA"); - logger = new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger)); } From 122d4bfbfc8d4c32c84cc63a7da43430c0be1c32 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Oct 2018 01:04:50 -0700 Subject: [PATCH 044/140] Make sure cache also uses copy of game db Configs can be applied after in either case --- ModuleManager/MMPatchLoader.cs | 55 +++++++++++++++------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 749c170e..8c50fc7f 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -128,6 +128,8 @@ private IEnumerator ProcessPatch() #endif yield return null; + IEnumerable databaseConfigs = null; + if (!useCache) { IPatchProgress progress = new PatchProgress(logger); @@ -178,8 +180,6 @@ private IEnumerator ProcessPatch() logger.Info("Starting patch thread"); - IEnumerable databaseConfigs = null; - ITaskStatus patchThread = BackgroundTask.Start(delegate { databaseConfigs = applier.ApplyPatches(patchList); @@ -236,16 +236,6 @@ float updateTimeRemaining() yield break; } - foreach (UrlDir.UrlFile file in GameDatabase.Instance.root.AllConfigFiles) - { - file.configs.Clear(); - } - - foreach (IProtoUrlConfig protoConfig in databaseConfigs) - { - protoConfig.UrlFile.AddConfig(protoConfig.Node); - } - logger.Info("Done patching"); yield return null; @@ -286,7 +276,7 @@ float updateTimeRemaining() status = "Saving Cache"; logger.Info(status); yield return null; - CreateCache(progress.Counter.patchedNodes); + CreateCache(databaseConfigs, progress.Counter.patchedNodes); } StatusUpdate(progress); @@ -301,7 +291,17 @@ float updateTimeRemaining() status = "Loading from Cache"; logger.Info(status); yield return null; - LoadCache(); + databaseConfigs = LoadCache(); + } + + foreach (UrlDir.UrlFile file in GameDatabase.Instance.root.AllConfigFiles) + { + file.configs.Clear(); + } + + foreach (IProtoUrlConfig protoConfig in databaseConfigs) + { + protoConfig.UrlFile.AddConfig(protoConfig.Node); } logger.Info(status + "\n" + errors); @@ -476,7 +476,7 @@ private ConfigNode GetFileNode(ConfigNode shaConfigNode, string filename) } - private void CreateCache(int patchedNodeCount) + private void CreateCache(IEnumerable databaseConfigs, int patchedNodeCount) { ConfigNode shaConfigNode = new ConfigNode(); shaConfigNode.AddValue("SHA", configSha); @@ -488,13 +488,11 @@ private void CreateCache(int patchedNodeCount) cache.AddValue("patchedNodeCount", patchedNodeCount.ToString()); - foreach (UrlDir.UrlConfig config in GameDatabase.Instance.root.AllConfigs) + foreach (IProtoUrlConfig urlConfig in databaseConfigs) { ConfigNode node = cache.AddNode("UrlConfig"); - node.AddValue("name", config.name); - node.AddValue("type", config.type); - node.AddValue("parentUrl", config.parent.url); - node.AddNode(config.config); + node.AddValue("parentUrl", urlConfig.UrlFile.url); + node.AddNode(urlConfig.Node); } foreach (var file in GameDatabase.Instance.root.AllConfigFiles) @@ -567,15 +565,8 @@ private void SaveModdedTechTree() techNode.Save(techTreePath); } - private void LoadCache() + private IEnumerable LoadCache() { - // Clear the config DB - foreach (UrlDir.UrlFile files in GameDatabase.Instance.root.AllConfigFiles) - { - files.configs.Clear(); - } - - // And then load all the cached configs ConfigNode cache = ConfigNode.Load(cachePath); if (cache.HasValue("patchedNodeCount") && int.TryParse(cache.GetValue("patchedNodeCount"), out int patchedNodeCount)) @@ -587,16 +578,16 @@ private void LoadCache() physicsUrlFile = new UrlDir.UrlFile(gameDataDir, new FileInfo(defaultPhysicsPath)); gameDataDir.files.Add(physicsUrlFile); + List databaseConfigs = new List(cache.nodes.Count); + foreach (ConfigNode node in cache.nodes) { - string name = node.GetValue("name"); - string type = node.GetValue("type"); string parentUrl = node.GetValue("parentUrl"); UrlDir.UrlFile parent = GameDatabase.Instance.root.AllConfigFiles.FirstOrDefault(f => f.url == parentUrl); if (parent != null) { - parent.AddConfig(node.nodes[0]); + databaseConfigs.Add(new ProtoUrlConfig(parent, node.nodes[0])); } else { @@ -605,6 +596,8 @@ private void LoadCache() } progressFraction = 1; logger.Info("Cache Loaded"); + + return databaseConfigs; } private void StatusUpdate(IPatchProgress progress) From ea7b05a6358295f43f20d02aba4095e76e58fef3 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Oct 2018 22:48:55 -0700 Subject: [PATCH 045/140] Extract paths to their own class Forgot that the static initalizers would prevent tests from running --- ModuleManager/CustomConfigsManager.cs | 10 ++++++---- ModuleManager/FilePathRepository.cs | 21 +++++++++++++++++++++ ModuleManager/MMPatchLoader.cs | 15 ++------------- ModuleManager/ModuleManager.csproj | 1 + 4 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 ModuleManager/FilePathRepository.cs diff --git a/ModuleManager/CustomConfigsManager.cs b/ModuleManager/CustomConfigsManager.cs index d88f4ea9..b749ba2f 100644 --- a/ModuleManager/CustomConfigsManager.cs +++ b/ModuleManager/CustomConfigsManager.cs @@ -2,6 +2,8 @@ using System.IO; using UnityEngine; +using static ModuleManager.FilePathRepository; + namespace ModuleManager { [KSPAddon(KSPAddon.Startup.SpaceCentre, false)] @@ -9,17 +11,17 @@ public class CustomConfigsManager : MonoBehaviour { internal void Start() { - if (HighLogic.CurrentGame.Parameters.Career.TechTreeUrl != MMPatchLoader.techTreeFile && File.Exists(MMPatchLoader.techTreePath)) + if (HighLogic.CurrentGame.Parameters.Career.TechTreeUrl != techTreeFile && File.Exists(techTreePath)) { Log("Setting modded tech tree as the active one"); - HighLogic.CurrentGame.Parameters.Career.TechTreeUrl = MMPatchLoader.techTreeFile; + HighLogic.CurrentGame.Parameters.Career.TechTreeUrl = techTreeFile; } - if (PhysicsGlobals.PhysicsDatabaseFilename != MMPatchLoader.physicsFile && File.Exists(MMPatchLoader.physicsPath)) + if (PhysicsGlobals.PhysicsDatabaseFilename != physicsFile && File.Exists(physicsPath)) { Log("Setting modded physics as the active one"); - PhysicsGlobals.PhysicsDatabaseFilename = MMPatchLoader.physicsFile; + PhysicsGlobals.PhysicsDatabaseFilename = physicsFile; if (!PhysicsGlobals.Instance.LoadDatabase()) Log("Something went wrong while setting the active physics config."); diff --git a/ModuleManager/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs new file mode 100644 index 00000000..17ccee3d --- /dev/null +++ b/ModuleManager/FilePathRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; + +namespace ModuleManager +{ + internal static class FilePathRepository + { + internal static readonly string cachePath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigCache"); + + internal static readonly string techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree"); + internal static readonly string techTreePath = Path.Combine(KSPUtil.ApplicationRootPath, techTreeFile); + + internal static readonly string physicsFile = Path.Combine("GameData", "ModuleManager.Physics"); + internal static readonly string physicsPath = Path.Combine(KSPUtil.ApplicationRootPath, physicsFile); + internal static readonly string defaultPhysicsPath = Path.Combine(KSPUtil.ApplicationRootPath, "Physics.cfg"); + + internal static readonly string partDatabasePath = Path.Combine(KSPUtil.ApplicationRootPath, "PartDatabase.cfg"); + + internal static readonly string shaPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigSHA"); + } +} diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 8c50fc7f..02c56207 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -21,6 +21,8 @@ using ModuleManager.Progress; using NodeStack = ModuleManager.Collections.ImmutableStack; +using static ModuleManager.FilePathRepository; + namespace ModuleManager { [SuppressMessage("ReSharper", "StringLastIndexOfIsCultureSpecific.1")] @@ -37,19 +39,6 @@ public class MMPatchLoader : LoadingSystem private static readonly Dictionary regexCache = new Dictionary(); - private static readonly string cachePath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigCache"); - - internal static readonly string techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree"); - internal static readonly string techTreePath = Path.Combine(KSPUtil.ApplicationRootPath, techTreeFile); - - internal static readonly string physicsFile = Path.Combine("GameData", "ModuleManager.Physics"); - internal static readonly string physicsPath = Path.Combine(KSPUtil.ApplicationRootPath, physicsFile); - private static readonly string defaultPhysicsPath = Path.Combine(KSPUtil.ApplicationRootPath, "Physics.cfg"); - - internal static readonly string partDatabasePath = Path.Combine(KSPUtil.ApplicationRootPath, "PartDatabase.cfg"); - - private static readonly string shaPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigSHA"); - private UrlDir.UrlFile physicsUrlFile; private string configSha; diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 6de340a4..9996e052 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -58,6 +58,7 @@ + From d50fc9e2403b3860c842267b4ff3ef04d32c53e6 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Oct 2018 00:08:13 -0700 Subject: [PATCH 046/140] patch in background Patches are now applied on a separate thread to an isolated copy of the game database, then copied into the actual game database by post-patch runner. Post patch runner will wait for patched database if it isn't done yet. One consequence is that logging during patching can no longer be directed to the main log (it'll get mixed up with other messages). Now directed to /Logs/ModuleManager.log --- ModuleManager/MMPatchLoader.cs | 149 +++--------------- ModuleManager/MMPatchRunner.cs | 125 +++++++++++++++ ModuleManager/ModuleManager.cs | 64 ++++---- ModuleManager/ModuleManager.csproj | 1 + ModuleManager/ModuleManagerPostPatch.cs | 32 +++- ModuleManager/PatchApplier.cs | 5 +- ModuleManager/Progress/IPatchProgress.cs | 4 + ModuleManager/Progress/PatchProgress.cs | 11 ++ ModuleManagerTests/PatchApplierTest.cs | 6 +- .../Progress/PatchProgressTest.cs | 35 ++++ 10 files changed, 268 insertions(+), 164 deletions(-) create mode 100644 ModuleManager/MMPatchRunner.cs diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 02c56207..3ffd20e8 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -14,9 +13,7 @@ using ModuleManager.Logging; using ModuleManager.Extensions; -using ModuleManager.Collections; using ModuleManager.Tags; -using ModuleManager.Threading; using ModuleManager.Patches; using ModuleManager.Progress; using NodeStack = ModuleManager.Collections.ImmutableStack; @@ -27,7 +24,7 @@ namespace ModuleManager { [SuppressMessage("ReSharper", "StringLastIndexOfIsCultureSpecific.1")] [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - public class MMPatchLoader : LoadingSystem + public class MMPatchLoader { public string status = ""; @@ -35,8 +32,6 @@ public class MMPatchLoader : LoadingSystem public static bool keepPartDB = false; - private string activity = "Module Manager"; - private static readonly Dictionary regexCache = new Dictionary(); private UrlDir.UrlFile physicsUrlFile; @@ -48,59 +43,23 @@ public class MMPatchLoader : LoadingSystem private IBasicLogger logger; - private float progressFraction = 0; - - public static MMPatchLoader Instance { get; private set; } - - private void Awake() - { - if (Instance != null) - { - DestroyImmediate(this); - return; - } - Instance = this; - - logger = new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger)); - } - - private bool ready; - - public override bool IsReady() - { - return ready; - } - - public override float ProgressFraction() => progressFraction; - - public override string ProgressTitle() - { - return activity; - } - - public override void StartLoad() + public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) { - ready = false; - - // DB check used to track the now fixed TextureReplacer corruption - //checkValues(); - - StartCoroutine(ProcessPatch()); + PostPatchLoader.AddPostPatchCallback(callback); } - public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) + public MMPatchLoader(IBasicLogger logger) { - PostPatchLoader.AddPostPatchCallback(callback); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - private IEnumerator ProcessPatch() + public IEnumerable Run() { Stopwatch patchSw = new Stopwatch(); patchSw.Start(); status = "Checking Cache"; logger.Info(status); - yield return null; bool useCache = false; try @@ -115,7 +74,6 @@ private IEnumerator ProcessPatch() #if DEBUG //useCache = false; #endif - yield return null; IEnumerable databaseConfigs = null; @@ -126,8 +84,6 @@ private IEnumerator ProcessPatch() logger.Info(status); IEnumerable mods = ModListGenerator.GenerateModList(progress, logger); - yield return null; - // If we don't use the cache then it is best to clean the PartDatabase.cfg if (!keepPartDB && File.Exists(partDatabasePath)) File.Delete(partDatabasePath); @@ -139,8 +95,6 @@ private IEnumerator ProcessPatch() status = "Extracting patches"; logger.Info(status); - yield return null; - UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == ""); INeedsChecker needsChecker = new NeedsChecker(mods, gameData, progress, logger); ITagListParser tagListParser = new TagListParser(progress); @@ -160,73 +114,30 @@ private IEnumerator ProcessPatch() status = "Applying patches"; logger.Info(status); - yield return null; - - MessageQueue logQueue = new MessageQueue(); - IBasicLogger patchLogger = new QueueLogger(logQueue); - IPatchProgress threadPatchProgress = new PatchProgress(progress, patchLogger); - PatchApplier applier = new PatchApplier(threadPatchProgress, patchLogger); + IPass currentPass = null; + float nextUpdate = Time.realtimeSinceStartup + yieldInterval; - logger.Info("Starting patch thread"); - - ITaskStatus patchThread = BackgroundTask.Start(delegate + progress.OnPassStarted.Add(delegate (IPass pass) { - databaseConfigs = applier.ApplyPatches(patchList); + currentPass = pass; + StatusUpdate(progress, currentPass.Name); }); - float nextYield = Time.realtimeSinceStartup + yieldInterval; - - float updateTimeRemaining() - { - float timeRemaining = nextYield - Time.realtimeSinceStartup; - if (timeRemaining < 0) - { - nextYield = Time.realtimeSinceStartup + yieldInterval; - StatusUpdate(progress); - activity = applier.Activity; - } - return timeRemaining; - } - - while (patchThread.IsRunning) + progress.OnPatchApplied.Add(delegate { - foreach (ILogMessage message in logQueue.TakeAll()) + if (Time.realtimeSinceStartup > nextUpdate) { - message.LogTo(logger); - - if (updateTimeRemaining() < 0) yield return null; + StatusUpdate(progress, currentPass.Name); + nextUpdate = Time.realtimeSinceStartup + yieldInterval; } + }); - float timeRemaining = updateTimeRemaining(); - if (timeRemaining > 0) System.Threading.Thread.Sleep((int)(timeRemaining * 1000)); - yield return null; - } + PatchApplier applier = new PatchApplier(progress, logger); + databaseConfigs = applier.ApplyPatches(patchList); StatusUpdate(progress); - activity = "ModuleManager - finishing up"; - yield return null; - - // Clear any log messages that might still be in the queue - foreach (ILogMessage message in logQueue.TakeAll()) - { - message.LogTo(logger); - } - - if (patchThread.IsExitedWithError) - { - progress.Exception("The patch runner threw an exception", patchThread.Exception); - FatalErrorHandler.HandleFatalError("The patch runner threw an exception"); - yield break; - } - if (databaseConfigs == null) - { - progress.Error("The patcher returned a null collection of configs"); - FatalErrorHandler.HandleFatalError("The patcher returned a null collection of configs"); - yield break; - } logger.Info("Done patching"); - yield return null; PurgeUnused(); @@ -264,7 +175,6 @@ float updateTimeRemaining() { status = "Saving Cache"; logger.Info(status); - yield return null; CreateCache(databaseConfigs, progress.Counter.patchedNodes); } @@ -279,26 +189,15 @@ float updateTimeRemaining() { status = "Loading from Cache"; logger.Info(status); - yield return null; databaseConfigs = LoadCache(); } - foreach (UrlDir.UrlFile file in GameDatabase.Instance.root.AllConfigFiles) - { - file.configs.Clear(); - } - - foreach (IProtoUrlConfig protoConfig in databaseConfigs) - { - protoConfig.UrlFile.AddConfig(protoConfig.Node); - } - logger.Info(status + "\n" + errors); patchSw.Stop(); logger.Info("Ran in " + ((float)patchSw.ElapsedMilliseconds / 1000).ToString("F3") + "s"); - ready = true; + return databaseConfigs; } private void LoadPhysicsConfig() @@ -583,17 +482,19 @@ private IEnumerable LoadCache() logger.Warning("Parent null for " + parentUrl); } } - progressFraction = 1; logger.Info("Cache Loaded"); return databaseConfigs; } - private void StatusUpdate(IPatchProgress progress) + private void StatusUpdate(IPatchProgress progress, string activity = null) { - progressFraction = progress.ProgressFraction; - status = "ModuleManager: " + progress.Counter.patchedNodes + " patch" + (progress.Counter.patchedNodes != 1 ? "es" : "") + " applied"; + if (progress.ProgressFraction < 1f - float.Epsilon) + status += " (" + progress.ProgressFraction * 100 + "%)"; + + if (activity != null) + status += "\n" + activity; if (progress.Counter.warnings > 0) status += ", found " + progress.Counter.warnings + " warning" + (progress.Counter.warnings != 1 ? "s" : "") + ""; diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs new file mode 100644 index 00000000..473053ad --- /dev/null +++ b/ModuleManager/MMPatchRunner.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using ModuleManager.Collections; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Threading; + +namespace ModuleManager +{ + public class MMPatchRunner + { + private const float TIME_TO_WAIT_FOR_LOGS = 0.05f; + + private readonly IBasicLogger kspLogger; + + public string Status { get; private set; } = ""; + public string Errors { get; private set; } = ""; + + public MMPatchRunner(IBasicLogger kspLogger) + { + this.kspLogger = kspLogger ?? throw new ArgumentNullException(nameof(kspLogger)); + } + + public IEnumerator Run() + { + PostPatchLoader.Instance.databaseConfigs = null; + + string logsDirPath = Path.Combine(KSPUtil.ApplicationRootPath, "Logs"); + if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath); + string logPath = Path.Combine(logsDirPath, "ModuleManager.log"); + + kspLogger.Info("Patching started on a new thread, all output will be directed to " + logPath); + + MessageQueue kspLogQueue = new MessageQueue(); + MessageQueue mmLogQueue = new MessageQueue(); + bool logThreadExitFlag = false; + ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate + { + QueueLogger kspLogger = new QueueLogger(kspLogQueue); + using (StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create), kspLogger)) + { + while (!logThreadExitFlag) + { + float waitTargetTime = Time.realtimeSinceStartup + TIME_TO_WAIT_FOR_LOGS; + + foreach (ILogMessage message in mmLogQueue.TakeAll()) + { + message.LogTo(streamLogger); + } + + float timeRemaining = waitTargetTime - Time.realtimeSinceStartup; + if (timeRemaining > 0) + System.Threading.Thread.Sleep((int)(timeRemaining * 1000)); + } + + foreach (ILogMessage message in mmLogQueue.TakeAll()) + { + message.LogTo(streamLogger); + } + + streamLogger.Info("Done!"); + } + }); + + // Wait for game database to be initialized for the 2nd time + yield return null; + + IEnumerable databaseConfigs = null; + + MMPatchLoader patchLoader = new MMPatchLoader(new QueueLogger(mmLogQueue)); + + ITaskStatus patchingThreadStatus = BackgroundTask.Start(delegate + { + databaseConfigs = patchLoader.Run(); + }); + + while(true) + { + yield return null; + + if (!patchingThreadStatus.IsRunning) + logThreadExitFlag = true; + + Status = patchLoader.status; + Errors = patchLoader.errors; + + foreach (ILogMessage message in kspLogQueue.TakeAll()) + { + message.LogTo(kspLogger); + } + + if (!patchingThreadStatus.IsRunning && !loggingThreadStatus.IsRunning) break; + } + + foreach (ILogMessage message in kspLogQueue.TakeAll()) + { + message.LogTo(kspLogger); + } + + if (patchingThreadStatus.IsExitedWithError) + { + kspLogger.Exception("The patching thread threw an exception", patchingThreadStatus.Exception); + FatalErrorHandler.HandleFatalError("The patching thread threw an exception"); + } + + if (loggingThreadStatus.IsExitedWithError) + { + kspLogger.Exception("The logging thread threw an exception", loggingThreadStatus.Exception); + FatalErrorHandler.HandleFatalError("The logging thread threw an exception"); + } + + if (databaseConfigs == null) + { + kspLogger.Error("The patcher returned a null collection of configs"); + FatalErrorHandler.HandleFatalError("The patcher returned a null collection of configs"); + yield break; + } + + PostPatchLoader.Instance.databaseConfigs = databaseConfigs; + } + } +} diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 8ed242a4..1d62fb75 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -7,7 +7,9 @@ using System.Reflection; using TMPro; using UnityEngine; +using Debug = UnityEngine.Debug; using ModuleManager.Cats; +using ModuleManager.Logging; namespace ModuleManager { @@ -36,6 +38,8 @@ public class ModuleManager : MonoBehaviour private PopupDialog menu; + private MMPatchRunner patchRunner; + #endregion state private static bool loadedInScene; @@ -126,11 +130,11 @@ internal void Awake() GameObject aGameObject = new GameObject("ModuleManager"); DontDestroyOnLoad(aGameObject); - Log(string.Format("Adding patch loader to the loading screen {0}", list.Count)); - list.Insert(gameDatabaseIndex + 1, aGameObject.AddComponent()); - Log(string.Format("Adding post patch to the loading screen {0}", list.Count)); - list.Insert(gameDatabaseIndex + 2, aGameObject.AddComponent()); + list.Insert(gameDatabaseIndex + 1, aGameObject.AddComponent()); + + patchRunner = new MMPatchRunner(new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); + StartCoroutine(patchRunner.Run()); // Workaround for 1.6.0 Editor bug after a PartDatabase rebuild. if (Versioning.version_major == 1 && Versioning.version_minor == 6 && Versioning.Revision == 0) @@ -266,27 +270,31 @@ internal void Update() float offsetY = textPos; float h; - if (warning) - { - h = warning.text.Length > 0 ? warning.textBounds.size.y : 0; - offsetY = offsetY + h; - warning.rectTransform.localPosition = new Vector3(0, offsetY); - } - if (status) + if (patchRunner != null) { - status.text = MMPatchLoader.Instance.status; - h = status.text.Length > 0 ? status.textBounds.size.y: 0; - offsetY = offsetY + h; - status.transform.localPosition = new Vector3(0, offsetY); - } + if (warning) + { + h = warning.text.Length > 0 ? warning.textBounds.size.y : 0; + offsetY = offsetY + h; + warning.rectTransform.localPosition = new Vector3(0, offsetY); + } - if (errors) - { - errors.text = MMPatchLoader.Instance.errors; - h = errors.text.Length > 0 ? errors.textBounds.size.y: 0; - offsetY = offsetY + h; - errors.transform.localPosition = new Vector3(0, offsetY); + if (status) + { + status.text = patchRunner.Status; + h = status.text.Length > 0 ? status.textBounds.size.y : 0; + offsetY = offsetY + h; + status.transform.localPosition = new Vector3(0, offsetY); + } + + if (errors) + { + errors.text = patchRunner.Errors; + h = errors.text.Length > 0 ? errors.textBounds.size.y : 0; + offsetY = offsetY + h; + errors.transform.localPosition = new Vector3(0, offsetY); + } } if (reloading) @@ -294,8 +302,6 @@ internal void Update() float percent = 0; if (!GameDatabase.Instance.IsReady()) percent = GameDatabase.Instance.ProgressFraction(); - else if (!MMPatchLoader.Instance.IsReady()) - percent = 1f + MMPatchLoader.Instance.ProgressFraction(); else if (!PartLoader.Instance.IsReady()) percent = 2f + PartLoader.Instance.ProgressFraction(); @@ -328,13 +334,13 @@ private IEnumerator DataBaseReloadWithMM(bool dump = false) GameDatabase.Instance.Recompile = true; GameDatabase.Instance.StartLoad(); - // wait for it to finish - while (!GameDatabase.Instance.IsReady()) - yield return null; + yield return null; - MMPatchLoader.Instance.StartLoad(); + patchRunner = new MMPatchRunner(new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); + StartCoroutine(patchRunner.Run()); - while (!MMPatchLoader.Instance.IsReady()) + // wait for it to finish + while (!GameDatabase.Instance.IsReady()) yield return null; PostPatchLoader.Instance.StartLoad(); diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 9996e052..d6e367b0 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -56,6 +56,7 @@ + diff --git a/ModuleManager/ModuleManagerPostPatch.cs b/ModuleManager/ModuleManagerPostPatch.cs index 1f4da965..00c0f74c 100644 --- a/ModuleManager/ModuleManagerPostPatch.cs +++ b/ModuleManager/ModuleManagerPostPatch.cs @@ -15,6 +15,8 @@ public class PostPatchLoader : LoadingSystem { public static PostPatchLoader Instance { get; private set; } + public IEnumerable databaseConfigs = null; + private static readonly List postPatchCallbacks = new List(); private readonly IBasicLogger logger = new ModLogger("ModuleManager", new UnityLogger(UnityEngine.Debug.unityLogger)); @@ -53,8 +55,30 @@ public override void StartLoad() private IEnumerator Run() { - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); + Stopwatch waitTimer = new Stopwatch(); + waitTimer.Start(); + + while (databaseConfigs == null) yield return null; + + waitTimer.Stop(); + logger.Info("Waited " + ((float)waitTimer.ElapsedMilliseconds / 1000).ToString("F3") + "s for patching to finish"); + + Stopwatch postPatchTimer = new Stopwatch(); + postPatchTimer.Start(); + + logger.Info("Applying patched game database"); + + foreach (UrlDir.UrlFile file in GameDatabase.Instance.root.AllConfigFiles) + { + file.configs.Clear(); + } + + foreach (IProtoUrlConfig protoConfig in databaseConfigs) + { + protoConfig.UrlFile.AddConfig(protoConfig.Node); + } + + databaseConfigs = null; #if DEBUG InGameTestRunner testRunner = new InGameTestRunner(logger); @@ -145,8 +169,8 @@ private IEnumerator Run() if (ModuleManager.dumpPostPatch) ModuleManager.OutputAllConfigs(); - stopwatch.Stop(); - logger.Info("Post patch ran in " + ((float)stopwatch.ElapsedMilliseconds / 1000).ToString("F3") + "s"); + postPatchTimer.Stop(); + logger.Info("Post patch ran in " + ((float)postPatchTimer.ElapsedMilliseconds / 1000).ToString("F3") + "s"); ready = true; } diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index 72e4fbd0..94d16e03 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -12,8 +12,6 @@ public class PatchApplier private readonly IBasicLogger logger; private readonly IPatchProgress progress; - public string Activity { get; private set; } - public PatchApplier(IPatchProgress progress, IBasicLogger logger) { this.progress = progress ?? throw new ArgumentNullException(nameof(progress)); @@ -36,8 +34,7 @@ public IEnumerable ApplyPatches(IEnumerable patches) private void ApplyPatches(LinkedList databaseConfigs, IPass pass) { - logger.Info(pass.Name + " pass"); - Activity = "ModuleManager " + pass.Name; + progress.PassStarted(pass); foreach (IPatch patch in pass) { diff --git a/ModuleManager/Progress/IPatchProgress.cs b/ModuleManager/Progress/IPatchProgress.cs index d1b39cae..46bcb2ad 100644 --- a/ModuleManager/Progress/IPatchProgress.cs +++ b/ModuleManager/Progress/IPatchProgress.cs @@ -8,6 +8,9 @@ public interface IPatchProgress float ProgressFraction { get; } + EventVoid OnPatchApplied { get; } + EventData OnPassStarted { get; } + void Warning(UrlDir.UrlConfig url, string message); void Error(UrlDir.UrlConfig url, string message); void Error(string message); @@ -24,5 +27,6 @@ public interface IPatchProgress void ApplyingUpdate(IUrlConfigIdentifier original, UrlDir.UrlConfig patch); void PatchAdded(); void PatchApplied(); + void PassStarted(IPass pass); } } diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index d61c35eb..b1d92721 100644 --- a/ModuleManager/Progress/PatchProgress.cs +++ b/ModuleManager/Progress/PatchProgress.cs @@ -21,6 +21,9 @@ public float ProgressFraction } } + public EventVoid OnPatchApplied { get; } = new EventVoid("OnPatchApplied"); + public EventData OnPassStarted { get; } = new EventData("OnPassStarted"); + public PatchProgress(IBasicLogger logger) { this.logger = logger; @@ -38,6 +41,13 @@ public void PatchAdded() Counter.totalPatches.Increment(); } + public void PassStarted(IPass pass) + { + if (pass == null) throw new ArgumentNullException(nameof(pass)); + logger.Info(pass.Name + " pass"); + OnPassStarted.Fire(pass); + } + public void ApplyingUpdate(IUrlConfigIdentifier original, UrlDir.UrlConfig patch) { logger.Info($"Applying update {patch.SafeUrl()} to {original.FullUrl}"); @@ -59,6 +69,7 @@ public void ApplyingDelete(IUrlConfigIdentifier original, UrlDir.UrlConfig patch public void PatchApplied() { Counter.appliedPatches.Increment(); + OnPatchApplied.Fire(); } public void NeedsUnsatisfiedRoot(UrlDir.UrlConfig url) diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index 81011b97..7cd8f7c9 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -85,21 +85,21 @@ public void TestApplyPatches() Received.InOrder(delegate { - logger.Log(LogType.Log, ":PASS1 pass"); + progress.PassStarted(pass1); patches[0].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); patches[1].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); patches[2].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - logger.Log(LogType.Log, ":PASS2 pass"); + progress.PassStarted(pass2); patches[3].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); patches[4].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); patches[5].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); - logger.Log(LogType.Log, ":PASS3 pass"); + progress.PassStarted(pass3); patches[6].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); patches[7].Apply(databaseConfigs, progress, logger); diff --git a/ModuleManagerTests/Progress/PatchProgressTest.cs b/ModuleManagerTests/Progress/PatchProgressTest.cs index c7f448e1..ee61fd46 100644 --- a/ModuleManagerTests/Progress/PatchProgressTest.cs +++ b/ModuleManagerTests/Progress/PatchProgressTest.cs @@ -110,11 +110,15 @@ public void TesApplyingDelete() [Fact] public void TestPatchApplied() { + int eventCounter = 0; + progress.OnPatchApplied.Add(() => eventCounter++); Assert.Equal(0, progress.Counter.appliedPatches); progress.PatchApplied(); Assert.Equal(1, progress.Counter.appliedPatches); + Assert.Equal(1, eventCounter); progress.PatchApplied(); Assert.Equal(2, progress.Counter.appliedPatches); + Assert.Equal(2, eventCounter); } [Fact] @@ -219,6 +223,37 @@ public void TestNeedsUnsatisfiedAfter() logger.Received().Log(LogType.Log, "Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its AFTER"); } + [Fact] + public void TestStartingPass() + { + EventData.OnEvent onEvent = Substitute.For.OnEvent>(); + progress.OnPassStarted.Add(onEvent); + IPass pass1 = Substitute.For(); + pass1.Name.Returns(":SOME_PASS"); + + progress.PassStarted(pass1); + + logger.Received().Log(LogType.Log, ":SOME_PASS pass"); + onEvent.Received()(pass1); + } + + [Fact] + public void TestStartingPass__NullArgument() + { + EventData.OnEvent onEvent = Substitute.For.OnEvent>(); + progress.OnPassStarted.Add(onEvent); + + ArgumentNullException ex = Assert.Throws(delegate + { + progress.PassStarted(null); + }); + + Assert.Equal("pass", ex.ParamName); + + logger.DidNotReceiveWithAnyArgs().Log(LogType.Log, null); + onEvent.DidNotReceiveWithAnyArgs()(null); + } + [Fact] public void TestWarning() { From 09b1eff0444f3ecdb1e6b312b1f10906e1ef2b73 Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 1 Nov 2018 23:02:19 -0700 Subject: [PATCH 047/140] Remove PurgeUnused Hasn't been necessary for a while --- ModuleManager/MMPatchLoader.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 3ffd20e8..eb9a7f42 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -139,8 +139,6 @@ public IEnumerable Run() logger.Info("Done patching"); - PurgeUnused(); - #endregion Applying patches #region Saving Cache @@ -506,17 +504,6 @@ private void StatusUpdate(IPatchProgress progress, string activity = null) status += ", encountered " + progress.Counter.exceptions + " exception" + (progress.Counter.exceptions != 1 ? "s" : "") + ""; } - private static void PurgeUnused() - { - foreach (UrlDir.UrlConfig mod in GameDatabase.Instance.root.AllConfigs.ToArray()) - { - string name = mod.type.RemoveWS(); - - if (CommandParser.Parse(name, out name) != Command.Insert) - mod.parent.configs.Remove(mod); - } - } - #region Applying Patches // Name is group 1, index is group 2, vector related filed is group 3, vector separator is group 4, operator is group 5 From 39e4157f86bcce19eafb2d2adbc0fc5b64f520ff Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 1 Nov 2018 23:07:51 -0700 Subject: [PATCH 048/140] Put log paths in path repository --- ModuleManager/FilePathRepository.cs | 3 +++ ModuleManager/MMPatchRunner.cs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ModuleManager/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs index 17ccee3d..24cac366 100644 --- a/ModuleManager/FilePathRepository.cs +++ b/ModuleManager/FilePathRepository.cs @@ -17,5 +17,8 @@ internal static class FilePathRepository internal static readonly string partDatabasePath = Path.Combine(KSPUtil.ApplicationRootPath, "PartDatabase.cfg"); internal static readonly string shaPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigSHA"); + + internal static readonly string logsDirPath = Path.Combine(KSPUtil.ApplicationRootPath, "Logs"); + internal static readonly string logPath = Path.Combine(logsDirPath, "ModuleManager.log"); } } diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index 473053ad..47b41371 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -8,6 +8,8 @@ using ModuleManager.Logging; using ModuleManager.Threading; +using static ModuleManager.FilePathRepository; + namespace ModuleManager { public class MMPatchRunner @@ -28,9 +30,7 @@ public IEnumerator Run() { PostPatchLoader.Instance.databaseConfigs = null; - string logsDirPath = Path.Combine(KSPUtil.ApplicationRootPath, "Logs"); if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath); - string logPath = Path.Combine(logsDirPath, "ModuleManager.log"); kspLogger.Info("Patching started on a new thread, all output will be directed to " + logPath); From de6b621e8e8bfcdbd9ec917f0fcd9404d1967579 Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 1 Nov 2018 23:09:17 -0700 Subject: [PATCH 049/140] Rename file to reflect class name --- ModuleManager/ModuleManager.csproj | 2 +- ModuleManager/{ModuleManagerPostPatch.cs => PostPatchLoader.cs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename ModuleManager/{ModuleManagerPostPatch.cs => PostPatchLoader.cs} (100%) diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index d6e367b0..c22076f6 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -60,7 +60,7 @@ - + diff --git a/ModuleManager/ModuleManagerPostPatch.cs b/ModuleManager/PostPatchLoader.cs similarity index 100% rename from ModuleManager/ModuleManagerPostPatch.cs rename to ModuleManager/PostPatchLoader.cs From 6ed6702fcf105168da1552667e2d6cf5a8e5dc2a Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 1 Nov 2018 23:35:54 -0700 Subject: [PATCH 050/140] Dump ModuleManager log to main log after patching Makes it easier to debug people's issues --- ModuleManager/PostPatchLoader.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index 00c0f74c..630b3818 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -7,6 +8,8 @@ using ModuleManager.Extensions; using ModuleManager.Logging; +using static ModuleManager.FilePathRepository; + namespace ModuleManager { public delegate void ModuleManagerPostPatchCallback(); @@ -80,6 +83,20 @@ private IEnumerator Run() databaseConfigs = null; + yield return null; + + if (File.Exists(logPath)) + { + logger.Info("Dumping ModuleManager log to main log"); + logger.Info("\n#### BEGIN MODULEMANAGER LOG ####\n\n\n" + File.ReadAllText(logPath) + "\n\n\n#### END MODULEMANAGER LOG ####"); + } + else + { + logger.Error("ModuleManager log does not exist: " + logPath); + } + + yield return null; + #if DEBUG InGameTestRunner testRunner = new InGameTestRunner(logger); testRunner.RunTestCases(GameDatabase.Instance.root); From 1589e0708914af3889854411f44e89ee86c78b02 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 26 Nov 2018 23:16:01 -0800 Subject: [PATCH 051/140] Don't count insert nodes as patches As far as progress is concerned, these take much less time to apply than patches, and are often less numerous. This can lead to weird completion percentages. --- ModuleManager/PatchApplier.cs | 2 +- ModuleManager/PatchList.cs | 2 +- ModuleManager/Patches/CopyPatch.cs | 1 + ModuleManager/Patches/DeletePatch.cs | 1 + ModuleManager/Patches/EditPatch.cs | 1 + ModuleManager/Patches/IPatch.cs | 1 + ModuleManager/Patches/InsertPatch.cs | 1 + ModuleManagerTests/PatchApplierTest.cs | 13 ++++++--- ModuleManagerTests/PatchListTest.cs | 27 ++++++++++++++++++- ModuleManagerTests/Patches/CopyPatchTest.cs | 7 +++++ ModuleManagerTests/Patches/DeletePatchTest.cs | 7 +++++ ModuleManagerTests/Patches/EditPatchTest.cs | 7 +++++ ModuleManagerTests/Patches/InsertPatchTest.cs | 7 +++++ 13 files changed, 71 insertions(+), 6 deletions(-) diff --git a/ModuleManager/PatchApplier.cs b/ModuleManager/PatchApplier.cs index 94d16e03..adaade88 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -41,7 +41,7 @@ private void ApplyPatches(LinkedList databaseConfigs, IPass pas try { patch.Apply(databaseConfigs, progress, logger); - progress.PatchApplied(); + if (patch.CountsAsPatch) progress.PatchApplied(); } catch (Exception e) { diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index 892b6843..9716de22 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -126,7 +126,7 @@ public PatchList(IEnumerable modList, IEnumerable patches, IPatc throw new NotImplementedException("Don't know what to do with pass specifier: " + patch.PassSpecifier.Descriptor); } - progress.PatchAdded(); + if (patch.CountsAsPatch) progress.PatchAdded(); } } diff --git a/ModuleManager/Patches/CopyPatch.cs b/ModuleManager/Patches/CopyPatch.cs index 05241973..45ee09fe 100644 --- a/ModuleManager/Patches/CopyPatch.cs +++ b/ModuleManager/Patches/CopyPatch.cs @@ -13,6 +13,7 @@ public class CopyPatch : IPatch public UrlDir.UrlConfig UrlConfig { get; } public INodeMatcher NodeMatcher { get; } public IPassSpecifier PassSpecifier { get; } + public bool CountsAsPatch => true; public CopyPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpecifier passSpecifier) { diff --git a/ModuleManager/Patches/DeletePatch.cs b/ModuleManager/Patches/DeletePatch.cs index fd465c9a..46d46a94 100644 --- a/ModuleManager/Patches/DeletePatch.cs +++ b/ModuleManager/Patches/DeletePatch.cs @@ -12,6 +12,7 @@ public class DeletePatch : IPatch public UrlDir.UrlConfig UrlConfig { get; } public INodeMatcher NodeMatcher { get; } public IPassSpecifier PassSpecifier { get; } + public bool CountsAsPatch => true; public DeletePatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpecifier passSpecifier) { diff --git a/ModuleManager/Patches/EditPatch.cs b/ModuleManager/Patches/EditPatch.cs index 501144e9..91613879 100644 --- a/ModuleManager/Patches/EditPatch.cs +++ b/ModuleManager/Patches/EditPatch.cs @@ -15,6 +15,7 @@ public class EditPatch : IPatch public UrlDir.UrlConfig UrlConfig { get; } public INodeMatcher NodeMatcher { get; } public IPassSpecifier PassSpecifier { get; } + public bool CountsAsPatch => true; public EditPatch(UrlDir.UrlConfig urlConfig, INodeMatcher nodeMatcher, IPassSpecifier passSpecifier) { diff --git a/ModuleManager/Patches/IPatch.cs b/ModuleManager/Patches/IPatch.cs index 032ce6e1..a877ea80 100644 --- a/ModuleManager/Patches/IPatch.cs +++ b/ModuleManager/Patches/IPatch.cs @@ -10,6 +10,7 @@ public interface IPatch { UrlDir.UrlConfig UrlConfig { get; } IPassSpecifier PassSpecifier { get; } + bool CountsAsPatch { get; } void Apply(LinkedList configs, IPatchProgress progress, IBasicLogger logger); } } diff --git a/ModuleManager/Patches/InsertPatch.cs b/ModuleManager/Patches/InsertPatch.cs index 9f3c8a50..c5f2c17f 100644 --- a/ModuleManager/Patches/InsertPatch.cs +++ b/ModuleManager/Patches/InsertPatch.cs @@ -12,6 +12,7 @@ public class InsertPatch : IPatch public UrlDir.UrlConfig UrlConfig { get; } public string NodeType { get; } public IPassSpecifier PassSpecifier { get; } + public bool CountsAsPatch => false; public InsertPatch(UrlDir.UrlConfig urlConfig, string nodeType, IPassSpecifier passSpecifier) { diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index 7cd8f7c9..d7fb0eb1 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -67,6 +67,16 @@ public void TestApplyPatches() patches[i] = Substitute.For(); } + patches[0].CountsAsPatch.Returns(false); + patches[1].CountsAsPatch.Returns(false); + patches[2].CountsAsPatch.Returns(false); + patches[3].CountsAsPatch.Returns(true); + patches[4].CountsAsPatch.Returns(true); + patches[5].CountsAsPatch.Returns(true); + patches[6].CountsAsPatch.Returns(true); + patches[7].CountsAsPatch.Returns(true); + patches[8].CountsAsPatch.Returns(true); + pass1.GetEnumerator().Returns(new ArrayEnumerator(patches[0], patches[1], patches[2])); pass2.GetEnumerator().Returns(new ArrayEnumerator(patches[3], patches[4], patches[5])); pass3.GetEnumerator().Returns(new ArrayEnumerator(patches[6], patches[7], patches[8])); @@ -87,11 +97,8 @@ public void TestApplyPatches() { progress.PassStarted(pass1); patches[0].Apply(databaseConfigs, progress, logger); - progress.PatchApplied(); patches[1].Apply(databaseConfigs, progress, logger); - progress.PatchApplied(); patches[2].Apply(databaseConfigs, progress, logger); - progress.PatchApplied(); progress.PassStarted(pass2); patches[3].Apply(databaseConfigs, progress, logger); progress.PatchApplied(); diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index 8e66f0b3..09b1c1dc 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -142,6 +142,31 @@ public void Test__Lifecycle() patches[22].PassSpecifier.Returns(new FinalPassSpecifier()); patches[23].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[00].CountsAsPatch.Returns(false); + patches[01].CountsAsPatch.Returns(false); + patches[02].CountsAsPatch.Returns(true); + patches[03].CountsAsPatch.Returns(true); + patches[04].CountsAsPatch.Returns(true); + patches[05].CountsAsPatch.Returns(true); + patches[06].CountsAsPatch.Returns(true); + patches[07].CountsAsPatch.Returns(true); + patches[08].CountsAsPatch.Returns(true); + patches[09].CountsAsPatch.Returns(true); + patches[10].CountsAsPatch.Returns(true); + patches[11].CountsAsPatch.Returns(true); + patches[12].CountsAsPatch.Returns(true); + patches[13].CountsAsPatch.Returns(true); + patches[14].CountsAsPatch.Returns(true); + patches[15].CountsAsPatch.Returns(true); + patches[16].CountsAsPatch.Returns(true); + patches[17].CountsAsPatch.Returns(true); + patches[18].CountsAsPatch.Returns(true); + patches[19].CountsAsPatch.Returns(true); + patches[20].CountsAsPatch.Returns(true); + patches[21].CountsAsPatch.Returns(true); + patches[22].CountsAsPatch.Returns(true); + patches[23].CountsAsPatch.Returns(true); + IPatchProgress progress = Substitute.For(); PatchList patchList = new PatchList(new[] { "mod1", "mod2" }, patches, progress); @@ -186,7 +211,7 @@ public void Test__Lifecycle() Assert.Equal(":FINAL", passes[11].Name); Assert.Equal(new[] { patches[22], patches[23] }, passes[11]); - progress.Received(patches.Length).PatchAdded(); + progress.Received(22).PatchAdded(); } } } diff --git a/ModuleManagerTests/Patches/CopyPatchTest.cs b/ModuleManagerTests/Patches/CopyPatchTest.cs index c0e8e14a..ee6443dd 100644 --- a/ModuleManagerTests/Patches/CopyPatchTest.cs +++ b/ModuleManagerTests/Patches/CopyPatchTest.cs @@ -74,6 +74,13 @@ public void TestPassSpecifier() Assert.Same(passSpecifier, patch.PassSpecifier); } + [Fact] + public void TestCountsAsPatch() + { + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); + Assert.True(patch.CountsAsPatch); + } + [Fact] public void TestApply() { diff --git a/ModuleManagerTests/Patches/DeletePatchTest.cs b/ModuleManagerTests/Patches/DeletePatchTest.cs index 1cfa950d..f45916fa 100644 --- a/ModuleManagerTests/Patches/DeletePatchTest.cs +++ b/ModuleManagerTests/Patches/DeletePatchTest.cs @@ -73,6 +73,13 @@ public void TestPassSpecifier() Assert.Same(passSpecifier, patch.PassSpecifier); } + [Fact] + public void TestCountsAsPatch() + { + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); + Assert.True(patch.CountsAsPatch); + } + [Fact] public void TestApply() { diff --git a/ModuleManagerTests/Patches/EditPatchTest.cs b/ModuleManagerTests/Patches/EditPatchTest.cs index 7ce5f6a9..50efd3b1 100644 --- a/ModuleManagerTests/Patches/EditPatchTest.cs +++ b/ModuleManagerTests/Patches/EditPatchTest.cs @@ -75,6 +75,13 @@ public void TestPassSpecifier() Assert.Same(passSpecifier, patch.PassSpecifier); } + [Fact] + public void TestCountsAsPatch() + { + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); + Assert.True(patch.CountsAsPatch); + } + [Fact] public void TestApply() { diff --git a/ModuleManagerTests/Patches/InsertPatchTest.cs b/ModuleManagerTests/Patches/InsertPatchTest.cs index b9944a9d..f1e134a4 100644 --- a/ModuleManagerTests/Patches/InsertPatchTest.cs +++ b/ModuleManagerTests/Patches/InsertPatchTest.cs @@ -73,6 +73,13 @@ public void TestPassSpecifier() Assert.Same(passSpecifier, patch.PassSpecifier); } + + [Fact] + public void TestCountsAsPatch() + { + InsertPatch patch = new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", Substitute.For()); + Assert.False(patch.CountsAsPatch); + } [Fact] public void TestApply() From c2424cb2254dbaa3bdd45e2defcfb20c40116e26 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 26 Nov 2018 23:25:00 -0800 Subject: [PATCH 052/140] Only display whole percentages --- ModuleManager/MMPatchLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index eb9a7f42..6acc6740 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -489,7 +489,7 @@ private void StatusUpdate(IPatchProgress progress, string activity = null) { status = "ModuleManager: " + progress.Counter.patchedNodes + " patch" + (progress.Counter.patchedNodes != 1 ? "es" : "") + " applied"; if (progress.ProgressFraction < 1f - float.Epsilon) - status += " (" + progress.ProgressFraction * 100 + "%)"; + status += " (" + progress.ProgressFraction.ToString("P0") + ")"; if (activity != null) status += "\n" + activity; From aeca3350c3960f3b87489f73e120c86c8c3f21de Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 26 Nov 2018 23:30:54 -0800 Subject: [PATCH 053/140] Add subdir under Logs Makes it consistent with Kopernicus --- ModuleManager/FilePathRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs index 24cac366..52f94573 100644 --- a/ModuleManager/FilePathRepository.cs +++ b/ModuleManager/FilePathRepository.cs @@ -18,7 +18,7 @@ internal static class FilePathRepository internal static readonly string shaPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigSHA"); - internal static readonly string logsDirPath = Path.Combine(KSPUtil.ApplicationRootPath, "Logs"); + internal static readonly string logsDirPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "Logs"), "ModuleManager"); internal static readonly string logPath = Path.Combine(logsDirPath, "ModuleManager.log"); } } From ed78083e1fd679a0c23ffe272b5fa1863aefd893 Mon Sep 17 00:00:00 2001 From: blowfish Date: Wed, 19 Dec 2018 21:44:12 -0800 Subject: [PATCH 054/140] Replace reloading screen messages with dialog --- ModuleManager/ModuleManager.cs | 92 ++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 22 deletions(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 1d62fb75..ac92cddc 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -20,8 +20,6 @@ public class ModuleManager : MonoBehaviour private bool inRnDCenter; - private bool reloading; - public bool showUI = false; private Rect windowPos = new Rect(80f, 60f, 240f, 40f); @@ -296,19 +294,6 @@ internal void Update() errors.transform.localPosition = new Vector3(0, offsetY); } } - - if (reloading) - { - float percent = 0; - if (!GameDatabase.Instance.IsReady()) - percent = GameDatabase.Instance.ProgressFraction(); - else if (!PartLoader.Instance.IsReady()) - percent = 2f + PartLoader.Instance.ProgressFraction(); - - int intPercent = Mathf.CeilToInt(percent * 100f / 3f); - ScreenMessages.PostScreenMessage("Database reloading " + intPercent + "%", Time.deltaTime, - ScreenMessageStyle.UPPER_CENTER); - } } #region GUI stuff. @@ -323,20 +308,83 @@ public static bool IsABadIdea() private IEnumerator DataBaseReloadWithMM(bool dump = false) { - reloading = true; - QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; - ScreenMessages.PostScreenMessage("Database reloading started", 1, ScreenMessageStyle.UPPER_CENTER); + patchRunner = new MMPatchRunner(new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); + + float totalLoadWeight = GameDatabase.Instance.LoadWeight() + PartLoader.Instance.LoadWeight(); + bool startedReload = false; + + UISkinDef skinDef = HighLogic.UISkin; + UIStyle centeredTextStyle = new UIStyle(skinDef.label) + { + alignment = TextAnchor.UpperCenter + }; + + PopupDialog reloadingDialog = PopupDialog.SpawnPopupDialog(new Vector2(0.5f, 0.5f), + new Vector2(0.5f, 0.5f), + new MultiOptionDialog( + "ModuleManagerReloading", + "", + "ModuleManager - Reloading Database", + skinDef, + new Rect(0.5f, 0.5f, 600f, 60f), + new DialogGUIFlexibleSpace(), + new DialogGUIVerticalLayout( + new DialogGUIFlexibleSpace(), + new DialogGUILabel(delegate () + { + float progressFraction; + if (!startedReload) + { + progressFraction = 0f; + } + else if (!GameDatabase.Instance.IsReady()) + { + progressFraction = GameDatabase.Instance.ProgressFraction() * GameDatabase.Instance.LoadWeight(); + progressFraction /= totalLoadWeight; + } + else if (!PartLoader.Instance.IsReady()) + { + progressFraction = GameDatabase.Instance.LoadWeight() + (PartLoader.Instance.ProgressFraction() * GameDatabase.Instance.LoadWeight()); + progressFraction /= totalLoadWeight; + } + else + { + progressFraction = 1f; + } + + return $"Overall progress: {progressFraction:P0}"; + }, centeredTextStyle, expandW: true), + new DialogGUILabel(delegate () + { + if (!startedReload) + return "Starting"; + else if (!GameDatabase.Instance.IsReady()) + return GameDatabase.Instance.ProgressTitle(); + else if (!PostPatchLoader.Instance.IsReady()) + return PostPatchLoader.Instance.ProgressTitle(); + else if (!PartLoader.Instance.IsReady()) + return PartLoader.Instance.ProgressTitle(); + else + return ""; + }), + new DialogGUISpace(5f), + new DialogGUILabel(() => patchRunner.Status) + ) + ), + false, + skinDef); + yield return null; GameDatabase.Instance.Recompile = true; GameDatabase.Instance.StartLoad(); - yield return null; + startedReload = true; - patchRunner = new MMPatchRunner(new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); + yield return null; StartCoroutine(patchRunner.Run()); // wait for it to finish @@ -372,8 +420,8 @@ private IEnumerator DataBaseReloadWithMM(bool dump = false) QualitySettings.vSyncCount = GameSettings.SYNC_VBL; Application.targetFrameRate = GameSettings.FRAMERATE_LIMIT; - reloading = false; - ScreenMessages.PostScreenMessage("Database reloading finished", 1, ScreenMessageStyle.UPPER_CENTER); + + reloadingDialog.Dismiss(); } public static void OutputAllConfigs() From 52d8b18040d915575e643cf6d8b5cc039f779fb0 Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Mon, 24 Dec 2018 22:28:54 -0800 Subject: [PATCH 055/140] add LogSplitter directs logs to two other loggers --- ModuleManager/Logging/LogSplitter.cs | 29 ++++++++++ ModuleManager/ModuleManager.csproj | 1 + ModuleManagerTests/Logging/LogSplitterTest.cs | 56 +++++++++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 4 files changed, 87 insertions(+) create mode 100644 ModuleManager/Logging/LogSplitter.cs create mode 100644 ModuleManagerTests/Logging/LogSplitterTest.cs diff --git a/ModuleManager/Logging/LogSplitter.cs b/ModuleManager/Logging/LogSplitter.cs new file mode 100644 index 00000000..53712f20 --- /dev/null +++ b/ModuleManager/Logging/LogSplitter.cs @@ -0,0 +1,29 @@ +using System; +using UnityEngine; + +namespace ModuleManager.Logging +{ + public class LogSplitter : IBasicLogger + { + private readonly IBasicLogger logger1; + private readonly IBasicLogger logger2; + + public LogSplitter(IBasicLogger logger1, IBasicLogger logger2) + { + this.logger1 = logger1 ?? throw new ArgumentNullException(nameof(logger1)); + this.logger2 = logger2 ?? throw new ArgumentNullException(nameof(logger2)); + } + + public void Log(LogType logType, string message) + { + logger1.Log(logType, message); + logger2.Log(logType, message); + } + + public void Exception(string message, Exception exception) + { + logger1.Exception(message, exception); + logger2.Exception(message, exception); + } + } +} diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index c22076f6..51550b90 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -50,6 +50,7 @@ + diff --git a/ModuleManagerTests/Logging/LogSplitterTest.cs b/ModuleManagerTests/Logging/LogSplitterTest.cs new file mode 100644 index 00000000..2375a3a3 --- /dev/null +++ b/ModuleManagerTests/Logging/LogSplitterTest.cs @@ -0,0 +1,56 @@ +using System; +using Xunit; +using NSubstitute; +using UnityEngine; +using ModuleManager.Logging; + +namespace ModuleManagerTests.Logging +{ + public class LogSplitterTest + { + [Fact] + public void TestConstructor__Logger1Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new LogSplitter(null, Substitute.For()); + }); + + Assert.Equal("logger1", ex.ParamName); + } + + [Fact] + public void TestConstructor__Logger2Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new LogSplitter(Substitute.For(), null); + }); + + Assert.Equal("logger2", ex.ParamName); + } + + [Fact] + public void TestLog() + { + IBasicLogger logger1 = Substitute.For(); + IBasicLogger logger2 = Substitute.For(); + LogSplitter logSplitter = new LogSplitter(logger1, logger2); + logSplitter.Log(LogType.Log, "some stuff"); + logger1.Received().Log(LogType.Log, "some stuff"); + logger2.Received().Log(LogType.Log, "some stuff"); + } + + [Fact] + public void TestException() + { + IBasicLogger logger1 = Substitute.For(); + IBasicLogger logger2 = Substitute.For(); + LogSplitter logSplitter = new LogSplitter(logger1, logger2); + Exception ex = new Exception(); + logSplitter.Exception("some stuff", ex); + logger1.Received().Exception("some stuff", ex); + logger2.Received().Exception("some stuff", ex); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index f07b5c07..f5d9a47e 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -85,6 +85,7 @@ + From 714500341d17e5a0fccba3baa61b2ac6105e62f9 Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Mon, 24 Dec 2018 22:31:58 -0800 Subject: [PATCH 056/140] Save patch log and dump when loading from cache This ensures that modders have access to a full history of what ModuleManager did even when the log was taken from a run where it loaded from cache --- ModuleManager/FilePathRepository.cs | 1 + ModuleManager/MMPatchLoader.cs | 96 +++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/ModuleManager/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs index 52f94573..4ba34321 100644 --- a/ModuleManager/FilePathRepository.cs +++ b/ModuleManager/FilePathRepository.cs @@ -20,5 +20,6 @@ internal static class FilePathRepository internal static readonly string logsDirPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "Logs"), "ModuleManager"); internal static readonly string logPath = Path.Combine(logsDirPath, "ModuleManager.log"); + internal static readonly string patchLogPath = Path.Combine(logsDirPath, "MMPatch.log"); } } diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 6acc6740..49d6e326 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -11,8 +11,10 @@ using UnityEngine; using Debug = UnityEngine.Debug; +using ModuleManager.Collections; using ModuleManager.Logging; using ModuleManager.Extensions; +using ModuleManager.Threading; using ModuleManager.Tags; using ModuleManager.Patches; using ModuleManager.Progress; @@ -41,6 +43,8 @@ public class MMPatchLoader private const float yieldInterval = 1f/30f; // Patch at ~30fps + private const float TIME_TO_WAIT_FOR_LOGS = 0.05f; + private IBasicLogger logger; public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) @@ -79,10 +83,53 @@ public IEnumerable Run() if (!useCache) { - IPatchProgress progress = new PatchProgress(logger); + if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath); + MessageQueue externalLogQueue = new MessageQueue(); + MessageQueue patchLogQueue = new MessageQueue(); + bool logThreadExitFlag = false; + ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate + { + QueueLogger externalLogger = new QueueLogger(externalLogQueue); + using (StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create), externalLogger)) + { + while (!logThreadExitFlag) + { + float waitTargetTime = Time.realtimeSinceStartup + TIME_TO_WAIT_FOR_LOGS; + + foreach (ILogMessage message in patchLogQueue.TakeAll()) + { + message.LogTo(streamLogger); + } + + foreach (ILogMessage message in externalLogQueue.TakeAll()) + { + message.LogTo(logger); + } + + float timeRemaining = waitTargetTime - Time.realtimeSinceStartup; + if (timeRemaining > 0) + System.Threading.Thread.Sleep((int)(timeRemaining * 1000)); + } + + foreach (ILogMessage message in patchLogQueue.TakeAll()) + { + message.LogTo(streamLogger); + } + + foreach (ILogMessage message in externalLogQueue.TakeAll()) + { + message.LogTo(logger); + } + + streamLogger.Info("Done!"); + } + }); + IBasicLogger patchLogger = new LogSplitter(logger, new QueueLogger(patchLogQueue)); + + IPatchProgress progress = new PatchProgress(patchLogger); status = "Pre patch init"; - logger.Info(status); - IEnumerable mods = ModListGenerator.GenerateModList(progress, logger); + patchLogger.Info(status); + IEnumerable mods = ModListGenerator.GenerateModList(progress, patchLogger); // If we don't use the cache then it is best to clean the PartDatabase.cfg if (!keepPartDB && File.Exists(partDatabasePath)) @@ -93,14 +140,14 @@ public IEnumerable Run() #region Sorting Patches status = "Extracting patches"; - logger.Info(status); + patchLogger.Info(status); UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == ""); - INeedsChecker needsChecker = new NeedsChecker(mods, gameData, progress, logger); + INeedsChecker needsChecker = new NeedsChecker(mods, gameData, progress, patchLogger); ITagListParser tagListParser = new TagListParser(progress); IProtoPatchBuilder protoPatchBuilder = new ProtoPatchBuilder(progress); IPatchCompiler patchCompiler = new PatchCompiler(); - PatchExtractor extractor = new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); + PatchExtractor extractor = new PatchExtractor(progress, patchLogger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); // Have to convert to an array because we will be removing patches IEnumerable extractedPatches = @@ -112,7 +159,7 @@ public IEnumerable Run() #region Applying patches status = "Applying patches"; - logger.Info(status); + patchLogger.Info(status); IPass currentPass = null; float nextUpdate = Time.realtimeSinceStartup + yieldInterval; @@ -132,12 +179,12 @@ public IEnumerable Run() } }); - PatchApplier applier = new PatchApplier(progress, logger); + PatchApplier applier = new PatchApplier(progress, patchLogger); databaseConfigs = applier.ApplyPatches(patchList); StatusUpdate(progress); - logger.Info("Done patching"); + patchLogger.Info("Done patching"); #endregion Applying patches @@ -145,7 +192,7 @@ public IEnumerable Run() foreach (KeyValuePair item in progress.Counter.warningFiles) { - logger.Warning(item.Value + " warning" + (item.Value > 1 ? "s" : "") + " related to GameData/" + item.Key); + patchLogger.Warning(item.Value + " warning" + (item.Value > 1 ? "s" : "") + " related to GameData/" + item.Key); } if (progress.Counter.errors > 0 || progress.Counter.exceptions > 0) @@ -156,7 +203,7 @@ public IEnumerable Run() + "\n"; } - logger.Warning("Errors in patch prevents the creation of the cache"); + patchLogger.Warning("Errors in patch prevents the creation of the cache"); try { if (File.Exists(cachePath)) @@ -166,13 +213,13 @@ public IEnumerable Run() } catch (Exception e) { - logger.Exception("Exception while deleting stale cache ", e); + patchLogger.Exception("Exception while deleting stale cache ", e); } } else { status = "Saving Cache"; - logger.Info(status); + patchLogger.Info(status); CreateCache(databaseConfigs, progress.Counter.patchedNodes); } @@ -182,12 +229,35 @@ public IEnumerable Run() SaveModdedTechTree(); SaveModdedPhysics(); + + logThreadExitFlag = true; + + while (loggingThreadStatus.IsRunning) + { + System.Threading.Thread.Sleep(100); + } + + if (loggingThreadStatus.IsExitedWithError) + { + logger.Error("The patching thread threw an exception"); + throw loggingThreadStatus.Exception; + } } else { status = "Loading from Cache"; logger.Info(status); databaseConfigs = LoadCache(); + + if (File.Exists(patchLogPath)) + { + logger.Info("Dumping patch log"); + logger.Info("\n#### BEGIN PATCH LOG ####\n\n\n" + File.ReadAllText(patchLogPath) + "\n\n\n#### END PATCH LOG ####"); + } + else + { + logger.Error("Patch log does not exist: " + patchLogPath); + } } logger.Info(status + "\n" + errors); From ee9073b8a94c4ea4e8fbbf01c2e48704cb1088cb Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Mon, 24 Dec 2018 22:52:30 -0800 Subject: [PATCH 057/140] Get rid of stream logger's exception logger In theory it should be monitored, and not being able to log is a pretty serious condition we'd want to watch for --- ModuleManager/Logging/StreamLogger.cs | 51 +++++++------------ ModuleManager/MMPatchLoader.cs | 14 +---- ModuleManager/MMPatchRunner.cs | 14 +---- .../Logging/StreamLoggerTest.cs | 50 ++++-------------- 4 files changed, 29 insertions(+), 100 deletions(-) diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs index 4da737e1..52ebd6bf 100644 --- a/ModuleManager/Logging/StreamLogger.cs +++ b/ModuleManager/Logging/StreamLogger.cs @@ -8,42 +8,34 @@ public class StreamLogger : IBasicLogger, IDisposable { private readonly Stream stream; private readonly StreamWriter streamWriter; - private readonly IBasicLogger exceptionLogger; private bool disposed = false; - public StreamLogger(Stream stream, IBasicLogger exceptionLogger) + public StreamLogger(Stream stream) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); if (!stream.CanWrite) throw new ArgumentException("must be writable", nameof(stream)); streamWriter = new StreamWriter(stream); - this.exceptionLogger = exceptionLogger ?? throw new ArgumentNullException(nameof(exceptionLogger)); } public void Log(LogType logType, string message) { if (disposed) throw new InvalidOperationException("Object has already been disposed"); - try - { - string prefix; - if (logType == LogType.Log) - prefix = "LOG"; - else if (logType == LogType.Warning) - prefix = "WRN"; - else if (logType == LogType.Error) - prefix = "ERR"; - else if (logType == LogType.Assert) - prefix = "AST"; - else if (logType == LogType.Exception) - prefix = "EXC"; - else - prefix = "UNK"; - streamWriter.WriteLine("[{0} {1}] {2}", prefix, DateTime.Now.ToString(), message); - } - catch (Exception e) - { - exceptionLogger.Exception("Exception while attempting to log to stream", e); - } + string prefix; + if (logType == LogType.Log) + prefix = "LOG"; + else if (logType == LogType.Warning) + prefix = "WRN"; + else if (logType == LogType.Error) + prefix = "ERR"; + else if (logType == LogType.Assert) + prefix = "AST"; + else if (logType == LogType.Exception) + prefix = "EXC"; + else + prefix = "UNK"; + + streamWriter.WriteLine("[{0} {1}] {2}", prefix, DateTime.Now.ToString(), message); } public void Exception(string message, Exception exception) @@ -53,15 +45,8 @@ public void Exception(string message, Exception exception) public void Dispose() { - try - { - // Flushes and closes the StreamWriter and the underlying stream - streamWriter.Close(); - } - catch(Exception e) - { - exceptionLogger.Exception("Exception while attempting to close stream writer", e); - } + // Flushes and closes the StreamWriter and the underlying stream + streamWriter.Close(); disposed = true; } diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 49d6e326..f723f5b1 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -84,13 +84,11 @@ public IEnumerable Run() if (!useCache) { if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath); - MessageQueue externalLogQueue = new MessageQueue(); MessageQueue patchLogQueue = new MessageQueue(); bool logThreadExitFlag = false; ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate { - QueueLogger externalLogger = new QueueLogger(externalLogQueue); - using (StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create), externalLogger)) + using (StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create))) { while (!logThreadExitFlag) { @@ -101,11 +99,6 @@ public IEnumerable Run() message.LogTo(streamLogger); } - foreach (ILogMessage message in externalLogQueue.TakeAll()) - { - message.LogTo(logger); - } - float timeRemaining = waitTargetTime - Time.realtimeSinceStartup; if (timeRemaining > 0) System.Threading.Thread.Sleep((int)(timeRemaining * 1000)); @@ -116,11 +109,6 @@ public IEnumerable Run() message.LogTo(streamLogger); } - foreach (ILogMessage message in externalLogQueue.TakeAll()) - { - message.LogTo(logger); - } - streamLogger.Info("Done!"); } }); diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index 47b41371..01ab1aae 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -34,13 +34,11 @@ public IEnumerator Run() kspLogger.Info("Patching started on a new thread, all output will be directed to " + logPath); - MessageQueue kspLogQueue = new MessageQueue(); MessageQueue mmLogQueue = new MessageQueue(); bool logThreadExitFlag = false; ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate { - QueueLogger kspLogger = new QueueLogger(kspLogQueue); - using (StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create), kspLogger)) + using (StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create))) { while (!logThreadExitFlag) { @@ -87,19 +85,9 @@ public IEnumerator Run() Status = patchLoader.status; Errors = patchLoader.errors; - foreach (ILogMessage message in kspLogQueue.TakeAll()) - { - message.LogTo(kspLogger); - } - if (!patchingThreadStatus.IsRunning && !loggingThreadStatus.IsRunning) break; } - foreach (ILogMessage message in kspLogQueue.TakeAll()) - { - message.LogTo(kspLogger); - } - if (patchingThreadStatus.IsExitedWithError) { kspLogger.Exception("The patching thread threw an exception", patchingThreadStatus.Exception); diff --git a/ModuleManagerTests/Logging/StreamLoggerTest.cs b/ModuleManagerTests/Logging/StreamLoggerTest.cs index 2b9f8733..fdcb832c 100644 --- a/ModuleManagerTests/Logging/StreamLoggerTest.cs +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -14,7 +14,7 @@ public void TestConstructor__StreamNull() { ArgumentNullException ex = Assert.Throws(delegate { - new StreamLogger(null, Substitute.For()); + new StreamLogger(null); }); Assert.Equal("stream", ex.ParamName); @@ -27,7 +27,7 @@ public void TestConstructor__CantWrite() { ArgumentException ex = Assert.Throws(delegate { - new StreamLogger(stream, Substitute.For()); + new StreamLogger(stream); }); Assert.Equal("stream", ex.ParamName); @@ -35,26 +35,12 @@ public void TestConstructor__CantWrite() } } - [Fact] - public void TestConstructor__ExceptionLoggerNull() - { - using (MemoryStream stream = new MemoryStream(new byte[0], true)) - { - ArgumentNullException ex = Assert.Throws(delegate - { - new StreamLogger(stream, null); - }); - - Assert.Equal("exceptionLogger", ex.ParamName); - } - } - [Fact] public void TestLog__AlreadyDisposed() { using (MemoryStream stream = new MemoryStream(new byte[0], true)) { - StreamLogger streamLogger = new StreamLogger(stream, Substitute.For()); + StreamLogger streamLogger = new StreamLogger(stream); streamLogger.Dispose(); InvalidOperationException ex = Assert.Throws(delegate @@ -69,18 +55,15 @@ public void TestLog__AlreadyDisposed() [Fact] public void TestLog__Log() { - IBasicLogger exceptionLogger = Substitute.For(); byte[] bytes = new byte[50]; using (MemoryStream stream = new MemoryStream(bytes, true)) { - using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + using (StreamLogger streamLogger = new StreamLogger(stream)) { streamLogger.Log(LogType.Log, "a message"); } } - exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); - using (MemoryStream stream = new MemoryStream(bytes, false)) { using (StreamReader reader = new StreamReader(stream)) @@ -95,18 +78,15 @@ public void TestLog__Log() [Fact] public void TestLog__Assert() { - IBasicLogger exceptionLogger = Substitute.For(); byte[] bytes = new byte[50]; using (MemoryStream stream = new MemoryStream(bytes, true)) { - using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + using (StreamLogger streamLogger = new StreamLogger(stream)) { streamLogger.Log(LogType.Assert, "a message"); } } - exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); - using (MemoryStream stream = new MemoryStream(bytes, false)) { using (StreamReader reader = new StreamReader(stream)) @@ -121,18 +101,15 @@ public void TestLog__Assert() [Fact] public void TestLog__Warning() { - IBasicLogger exceptionLogger = Substitute.For(); byte[] bytes = new byte[50]; using (MemoryStream stream = new MemoryStream(bytes, true)) { - using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + using (StreamLogger streamLogger = new StreamLogger(stream)) { streamLogger.Log(LogType.Warning, "a message"); } } - exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); - using (MemoryStream stream = new MemoryStream(bytes, false)) { using (StreamReader reader = new StreamReader(stream)) @@ -147,18 +124,15 @@ public void TestLog__Warning() [Fact] public void TestLog__Error() { - IBasicLogger exceptionLogger = Substitute.For(); byte[] bytes = new byte[50]; using (MemoryStream stream = new MemoryStream(bytes, true)) { - using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + using (StreamLogger streamLogger = new StreamLogger(stream)) { streamLogger.Log(LogType.Error, "a message"); } } - exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); - using (MemoryStream stream = new MemoryStream(bytes, false)) { using (StreamReader reader = new StreamReader(stream)) @@ -173,18 +147,15 @@ public void TestLog__Error() [Fact] public void TestLog__Exception() { - IBasicLogger exceptionLogger = Substitute.For(); byte[] bytes = new byte[50]; using (MemoryStream stream = new MemoryStream(bytes, true)) { - using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + using (StreamLogger streamLogger = new StreamLogger(stream)) { streamLogger.Log(LogType.Exception, "a message"); } } - exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); - using (MemoryStream stream = new MemoryStream(bytes, false)) { using (StreamReader reader = new StreamReader(stream)) @@ -199,18 +170,15 @@ public void TestLog__Exception() [Fact] public void TestLog__Unknown() { - IBasicLogger exceptionLogger = Substitute.For(); byte[] bytes = new byte[50]; using (MemoryStream stream = new MemoryStream(bytes, true)) { - using (StreamLogger streamLogger = new StreamLogger(stream, exceptionLogger)) + using (StreamLogger streamLogger = new StreamLogger(stream)) { streamLogger.Log((LogType)1000, "a message"); } } - exceptionLogger.DidNotReceiveWithAnyArgs().Exception(null, null); - using (MemoryStream stream = new MemoryStream(bytes, false)) { using (StreamReader reader = new StreamReader(stream)) From e0c1400300d9aafc649cce033ed99d85df50f24c Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Mon, 24 Dec 2018 22:53:15 -0800 Subject: [PATCH 058/140] Get rid of trailing whitespace Thanks VisualStudio --- ModuleManager/MMPatchLoader.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index f723f5b1..9685de23 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -306,11 +306,11 @@ private bool IsCacheUpToDate() // Hash the file path so the checksum change if files are moved byte[] pathBytes = Encoding.UTF8.GetBytes(files[i].url); sha.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0); - + // hash the file content byte[] contentBytes = File.ReadAllBytes(files[i].fullPath); sha.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0); - + filesha.ComputeHash(contentBytes); if (!filesSha.ContainsKey(files[i].url)) { @@ -372,7 +372,7 @@ private bool CheckFilesChange(UrlDir.UrlFile[] files, ConfigNode shaConfigNode) StringBuilder changes = new StringBuilder(); changes.Append("Changes :\n"); - + for (int i = 0; i < files.Length; i++) { ConfigNode fileNode = GetFileNode(shaConfigNode, files[i].url); @@ -418,7 +418,7 @@ private ConfigNode GetFileNode(ConfigNode shaConfigNode, string filename) } return null; } - + private void CreateCache(IEnumerable databaseConfigs, int patchedNodeCount) { @@ -512,7 +512,7 @@ private void SaveModdedTechTree() private IEnumerable LoadCache() { ConfigNode cache = ConfigNode.Load(cachePath); - + if (cache.HasValue("patchedNodeCount") && int.TryParse(cache.GetValue("patchedNodeCount"), out int patchedNodeCount)) status = "ModuleManager: " + patchedNodeCount + " patch" + (patchedNodeCount != 1 ? "es" : "") + " loaded from cache"; @@ -586,7 +586,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon #endif Command cmd = CommandParser.Parse(modVal.name, out string valName); - + Operator op; if (valName.Length > 2 && valName[valName.Length - 2] == ',') op = Operator.Assign; @@ -602,10 +602,10 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon context.progress.Error(context.patchUrl, "Error - Cannot find value assigning command: " + valName); continue; } - + if (op != Operator.Assign) { - if (double.TryParse(modVal.value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double s) + if (double.TryParse(modVal.value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double s) && double.TryParse(val.value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double os)) { switch (op) @@ -1000,7 +1000,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon if (n != null) subNodes.Add(n); } - + if (command == Command.Replace) { // if the original exists modify it @@ -1362,7 +1362,7 @@ private static ConfigNode.Value RecurseVariableSearch(string path, NodeStack nod context.logger.Warning("Cannot find key " + valName + " in " + nodeStack.value.name); return null; } - + if (match.Groups[3].Success) { ConfigNode.Value newVal = new ConfigNode.Value(cVal.name, cVal.value); @@ -1471,7 +1471,7 @@ private static string FindAndReplaceValue( return null; } } - else if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double s) + else if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double s) && double.TryParse(oValue, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double os)) { switch (op) @@ -1543,7 +1543,7 @@ public static List SplitConstraints(string condition) public static bool CheckConstraints(ConfigNode node, string constraints) { constraints = constraints.RemoveWS(); - + if (constraints.Length == 0) return true; From 326119bc1ec3d1c2cf8b13f22f6d426e47b7e96a Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Mon, 24 Dec 2018 23:13:38 -0800 Subject: [PATCH 059/140] use international date format --- ModuleManager/Logging/StreamLogger.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs index 52ebd6bf..c439149c 100644 --- a/ModuleManager/Logging/StreamLogger.cs +++ b/ModuleManager/Logging/StreamLogger.cs @@ -6,6 +6,8 @@ namespace ModuleManager.Logging { public class StreamLogger : IBasicLogger, IDisposable { + private const string DATETIME_FORMAT_STRING = "yyyy-mm-dd HH:mm:ss.fff"; + private readonly Stream stream; private readonly StreamWriter streamWriter; private bool disposed = false; @@ -35,7 +37,7 @@ public void Log(LogType logType, string message) else prefix = "UNK"; - streamWriter.WriteLine("[{0} {1}] {2}", prefix, DateTime.Now.ToString(), message); + streamWriter.WriteLine("[{0} {1}] {2}", prefix, DateTime.Now.ToString(DATETIME_FORMAT_STRING), message); } public void Exception(string message, Exception exception) From 072b0d002a2e3facd4850a6a9ec9efdd7f6c99c5 Mon Sep 17 00:00:00 2001 From: Joseph Wong Date: Thu, 27 Dec 2018 18:18:45 -0800 Subject: [PATCH 060/140] extract common parts of reading logs from queue to its own class --- ModuleManager/Collections/MessageQueue.cs | 7 +- ModuleManager/Logging/QueueLogRunner.cs | 68 ++++++++ ModuleManager/MMPatchLoader.cs | 24 +-- ModuleManager/MMPatchRunner.cs | 24 +-- ModuleManager/ModuleManager.csproj | 1 + .../Collections/MessageQueueTest.cs | 2 +- .../Logging/QueueLogRunnerTest.cs | 152 ++++++++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 8 files changed, 233 insertions(+), 46 deletions(-) create mode 100644 ModuleManager/Logging/QueueLogRunner.cs create mode 100644 ModuleManagerTests/Logging/QueueLogRunnerTest.cs diff --git a/ModuleManager/Collections/MessageQueue.cs b/ModuleManager/Collections/MessageQueue.cs index 9f1bc864..321a3503 100644 --- a/ModuleManager/Collections/MessageQueue.cs +++ b/ModuleManager/Collections/MessageQueue.cs @@ -4,9 +4,10 @@ namespace ModuleManager.Collections { - public interface IMessageQueue + public interface IMessageQueue : IEnumerable { void Add(T value); + IMessageQueue TakeAll(); } public class MessageQueue : IMessageQueue, IEnumerable @@ -56,7 +57,7 @@ public Node(T value) private readonly object lockObject = new object(); private Node head; private Node tail; - + public void Add(T value) { Node node = new Node(value); @@ -75,7 +76,7 @@ public void Add(T value) } } - public MessageQueue TakeAll() + public IMessageQueue TakeAll() { MessageQueue queue = new MessageQueue(); lock(lockObject) diff --git a/ModuleManager/Logging/QueueLogRunner.cs b/ModuleManager/Logging/QueueLogRunner.cs new file mode 100644 index 00000000..2ed5c300 --- /dev/null +++ b/ModuleManager/Logging/QueueLogRunner.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; + +using ModuleManager.Collections; + +namespace ModuleManager.Logging +{ + public class QueueLogRunner + { + private enum State + { + Initialized, + Running, + StopRequested, + Stopped, + } + + private State state = State.Initialized; + private readonly IMessageQueue logQueue; + private readonly long timeToWaitForLogsMs; + + public QueueLogRunner(IMessageQueue logQueue, long timeToWaitForLogsMs = 50) + { + this.logQueue = logQueue ?? throw new ArgumentNullException(nameof(logQueue)); + if (timeToWaitForLogsMs < 0) throw new ArgumentException("must be non-negative", nameof(timeToWaitForLogsMs)); + this.timeToWaitForLogsMs = timeToWaitForLogsMs; + } + + public void RequestStop() + { + if (state == State.StopRequested || state == State.Stopped) return; + if (state != State.Running) throw new InvalidOperationException($"Cannot request stop from {state} state"); + state = State.StopRequested; + } + + public void Run(IBasicLogger logger) + { + if (state != State.Initialized) throw new InvalidOperationException($"Cannot run from {state} state"); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + state = State.Running; + + System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); + + while (state == State.Running) + { + stopwatch.Start(); + + foreach (ILogMessage message in logQueue.TakeAll()) + { + message.LogTo(logger); + } + + long timeRemaining = timeToWaitForLogsMs - stopwatch.ElapsedMilliseconds; + if (timeRemaining > 0) + System.Threading.Thread.Sleep((int)timeRemaining); + + stopwatch.Reset(); + } + + foreach (ILogMessage message in logQueue.TakeAll()) + { + message.LogTo(logger); + } + + state = State.Stopped; + } + } +} diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 9685de23..0e94bca8 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -85,30 +85,12 @@ public IEnumerable Run() { if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath); MessageQueue patchLogQueue = new MessageQueue(); - bool logThreadExitFlag = false; + QueueLogRunner logRunner = new QueueLogRunner(patchLogQueue); ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate { using (StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create))) { - while (!logThreadExitFlag) - { - float waitTargetTime = Time.realtimeSinceStartup + TIME_TO_WAIT_FOR_LOGS; - - foreach (ILogMessage message in patchLogQueue.TakeAll()) - { - message.LogTo(streamLogger); - } - - float timeRemaining = waitTargetTime - Time.realtimeSinceStartup; - if (timeRemaining > 0) - System.Threading.Thread.Sleep((int)(timeRemaining * 1000)); - } - - foreach (ILogMessage message in patchLogQueue.TakeAll()) - { - message.LogTo(streamLogger); - } - + logRunner.Run(streamLogger); streamLogger.Info("Done!"); } }); @@ -218,7 +200,7 @@ public IEnumerable Run() SaveModdedTechTree(); SaveModdedPhysics(); - logThreadExitFlag = true; + logRunner.RequestStop(); while (loggingThreadStatus.IsRunning) { diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index 01ab1aae..6852464c 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -35,30 +35,12 @@ public IEnumerator Run() kspLogger.Info("Patching started on a new thread, all output will be directed to " + logPath); MessageQueue mmLogQueue = new MessageQueue(); - bool logThreadExitFlag = false; + QueueLogRunner logRunner = new QueueLogRunner(mmLogQueue); ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate { using (StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create))) { - while (!logThreadExitFlag) - { - float waitTargetTime = Time.realtimeSinceStartup + TIME_TO_WAIT_FOR_LOGS; - - foreach (ILogMessage message in mmLogQueue.TakeAll()) - { - message.LogTo(streamLogger); - } - - float timeRemaining = waitTargetTime - Time.realtimeSinceStartup; - if (timeRemaining > 0) - System.Threading.Thread.Sleep((int)(timeRemaining * 1000)); - } - - foreach (ILogMessage message in mmLogQueue.TakeAll()) - { - message.LogTo(streamLogger); - } - + logRunner.Run(streamLogger); streamLogger.Info("Done!"); } }); @@ -80,7 +62,7 @@ public IEnumerator Run() yield return null; if (!patchingThreadStatus.IsRunning) - logThreadExitFlag = true; + logRunner.RequestStop(); Status = patchLoader.status; Errors = patchLoader.errors; diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 51550b90..92ce283a 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -54,6 +54,7 @@ + diff --git a/ModuleManagerTests/Collections/MessageQueueTest.cs b/ModuleManagerTests/Collections/MessageQueueTest.cs index 9ee116ff..83bf1217 100644 --- a/ModuleManagerTests/Collections/MessageQueueTest.cs +++ b/ModuleManagerTests/Collections/MessageQueueTest.cs @@ -42,7 +42,7 @@ public void TestTakeAll() queue.Add(o2); queue.Add(o3); - MessageQueue queue2 = queue.TakeAll(); + MessageQueue queue2 = Assert.IsType>(queue.TakeAll()); queue.Add(o4); diff --git a/ModuleManagerTests/Logging/QueueLogRunnerTest.cs b/ModuleManagerTests/Logging/QueueLogRunnerTest.cs new file mode 100644 index 00000000..a52e7add --- /dev/null +++ b/ModuleManagerTests/Logging/QueueLogRunnerTest.cs @@ -0,0 +1,152 @@ +using System; +using Xunit; +using NSubstitute; + +using ModuleManager.Collections; +using ModuleManager.Logging; + +namespace ModuleManagerTests.Logging +{ + public class QueueLogRunnerTest + { + [Fact] + public void TestConstructor__LogQueueNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new QueueLogRunner(null); + }); + + Assert.Equal("logQueue", ex.ParamName); + } + + [Fact] + public void TestConstructor__TimeToWaitForLogsMsNegative() + { + ArgumentException ex = Assert.Throws(delegate + { + new QueueLogRunner(Substitute.For>(), -1); + }); + + Assert.Contains("must be non-negative", ex.Message); + Assert.Equal("timeToWaitForLogsMs", ex.ParamName); + } + + [Fact] + public void TestRun() + { + ILogMessage message1 = Substitute.For(); + ILogMessage message2 = Substitute.For(); + ILogMessage message3 = Substitute.For(); + ILogMessage message4 = Substitute.For(); + ILogMessage message5 = Substitute.For(); + ILogMessage message6 = Substitute.For(); + IMessageQueue messageQueue = Substitute.For>(); + QueueLogRunner logRunner = new QueueLogRunner(messageQueue, 0); + int counter = 0; + messageQueue.TakeAll().Returns(delegate + { + IMessageQueue messageQueue2 = Substitute.For>(); + if (counter == 0) + { + messageQueue2.GetEnumerator().Returns(new ArrayEnumerator(message1, message2)); + } + else if (counter == 1) + { + logRunner.RequestStop(); // Called from Running state + messageQueue2.GetEnumerator().Returns(new ArrayEnumerator(message3, message4)); + } + else + { + logRunner.RequestStop(); // Called from StopRequested state + messageQueue2.GetEnumerator().Returns(new ArrayEnumerator(message5, message6)); + } + counter++; + return messageQueue2; + }); + + IBasicLogger logger = Substitute.For(); + + logRunner.Run(logger); + + logRunner.RequestStop(); // Called from Stopped state + + Received.InOrder(delegate + { + message1.LogTo(logger); + message2.LogTo(logger); + message3.LogTo(logger); + message4.LogTo(logger); + message5.LogTo(logger); + message6.LogTo(logger); + }); + } + + [Fact] + public void TestRun__AlreadyStarted() + { + IMessageQueue messageQueue = Substitute.For>(); + QueueLogRunner logRunner = new QueueLogRunner(messageQueue, 0); + int counter = 0; + messageQueue.TakeAll().Returns(delegate + { + IMessageQueue messageQueue2 = Substitute.For>(); + if (counter == 0) + { + InvalidOperationException ex = Assert.Throws(delegate + { + logRunner.Run(Substitute.For()); + }); + Assert.Equal("Cannot run from Running state", ex.Message); + logRunner.RequestStop(); + messageQueue2.GetEnumerator().Returns(new ArrayEnumerator()); + } + else + { + InvalidOperationException ex = Assert.Throws(delegate + { + logRunner.Run(Substitute.For()); + }); + Assert.Equal("Cannot run from StopRequested state", ex.Message); + logRunner.RequestStop(); + messageQueue2.GetEnumerator().Returns(new ArrayEnumerator()); + } + counter++; + return messageQueue2; + }); + + IBasicLogger logger = Substitute.For(); + logRunner.Run(logger); + + InvalidOperationException ex2 = Assert.Throws(delegate + { + logRunner.Run(Substitute.For()); + }); + Assert.Equal("Cannot run from Stopped state", ex2.Message); + } + + [Fact] + public void TestRun__LoggerNull() + { + QueueLogRunner logRunner = new QueueLogRunner(Substitute.For>()); + + ArgumentNullException ex = Assert.Throws(delegate + { + logRunner.Run(null); + }); + Assert.Equal("logger", ex.ParamName); + } + + [Fact] + public void TestRequestStop__NotStarted() + { + QueueLogRunner logRunner = new QueueLogRunner(Substitute.For>()); + + InvalidOperationException ex = Assert.Throws(delegate + { + logRunner.RequestStop(); + }); + Assert.Equal("Cannot request stop from Initialized state", ex.Message); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index f5d9a47e..7e1bd948 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -88,6 +88,7 @@ + From 166b72c9a2a087c3b58e4e3e4ea949171c48c9c7 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Thu, 24 Jan 2019 20:15:25 +0100 Subject: [PATCH 061/140] v4.0.0 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index d3094e3a..56e080a3 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("3.1.3")] +[assembly: AssemblyVersion("4.0.0")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 05b2e9aa869bad0acb15880c8ba2729f7b2a076f Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 24 Jan 2019 23:03:56 -0800 Subject: [PATCH 062/140] Delete unnecessary space --- ModuleManager/CustomConfigsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/CustomConfigsManager.cs b/ModuleManager/CustomConfigsManager.cs index b749ba2f..e2b8689c 100644 --- a/ModuleManager/CustomConfigsManager.cs +++ b/ModuleManager/CustomConfigsManager.cs @@ -11,7 +11,7 @@ public class CustomConfigsManager : MonoBehaviour { internal void Start() { - if (HighLogic.CurrentGame.Parameters.Career.TechTreeUrl != techTreeFile && File.Exists(techTreePath)) + if (HighLogic.CurrentGame.Parameters.Career.TechTreeUrl != techTreeFile && File.Exists(techTreePath)) { Log("Setting modded tech tree as the active one"); HighLogic.CurrentGame.Parameters.Career.TechTreeUrl = techTreeFile; From 22a05651acde051c1d601d5dae4ef224d4f1d8a2 Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 24 Jan 2019 23:46:38 -0800 Subject: [PATCH 063/140] Fix modded physics and tech tree * Both should be saved from proto configs, not real game database since at that point patched nodes are not copied to real database * Don't need to keep track of physics url file --- ModuleManager/MMPatchLoader.cs | 37 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 0e94bca8..fc1dc42c 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -36,8 +36,6 @@ public class MMPatchLoader private static readonly Dictionary regexCache = new Dictionary(); - private UrlDir.UrlFile physicsUrlFile; - private string configSha; private Dictionary filesSha = new Dictionary(); @@ -197,8 +195,8 @@ public IEnumerable Run() #endregion Saving Cache - SaveModdedTechTree(); - SaveModdedPhysics(); + SaveModdedTechTree(databaseConfigs); + SaveModdedPhysics(databaseConfigs); logRunner.RequestStop(); @@ -243,7 +241,7 @@ private void LoadPhysicsConfig() logger.Info("Loading Physics.cfg"); UrlDir gameDataDir = GameDatabase.Instance.root.AllDirectories.First(d => d.path.EndsWith("GameData") && d.name == "" && d.url == ""); // need to use a file with a cfg extension to get the right fileType or you can't AddConfig on it - physicsUrlFile = new UrlDir.UrlFile(gameDataDir, new FileInfo(defaultPhysicsPath)); + UrlDir.UrlFile physicsUrlFile = new UrlDir.UrlFile(gameDataDir, new FileInfo(defaultPhysicsPath)); // Since it loaded the default config badly (sub node only) we clear it first physicsUrlFile.configs.Clear(); // And reload it properly @@ -253,23 +251,23 @@ private void LoadPhysicsConfig() gameDataDir.files.Add(physicsUrlFile); } - - private void SaveModdedPhysics() + private void SaveModdedPhysics(IEnumerable databaseConfigs) { - List configs = physicsUrlFile.configs; + IEnumerable configs = databaseConfigs.Where(config => config.NodeType == "PHYSICSGLOBALS"); + int count = configs.Count(); - if (configs.Count == 0) + if (count == 0) { logger.Info("No PHYSICSGLOBALS node found. No custom Physics config will be saved"); return; } - if (configs.Count > 1) + if (count > 1) { - logger.Info(configs.Count + " PHYSICSGLOBALS node found. A patch may be wrong. Using the first one"); + logger.Info($"{count} PHYSICSGLOBALS node found. A patch may be wrong. Using the first one"); } - configs[0].config.Save(physicsPath); + configs.First().Node.Save(physicsPath); } private bool IsCacheUpToDate() @@ -471,23 +469,24 @@ private void CreateCache(IEnumerable databaseConfigs, int patch } } - private void SaveModdedTechTree() + private void SaveModdedTechTree(IEnumerable databaseConfigs) { - UrlDir.UrlConfig[] configs = GameDatabase.Instance.GetConfigs("TechTree"); + IEnumerable configs = databaseConfigs.Where(config => config.NodeType == "TechTree"); + int count = configs.Count(); - if (configs.Length == 0) + if (count == 0) { logger.Info("No TechTree node found. No custom TechTree will be saved"); return; } - if (configs.Length > 1) + if (count > 1) { - logger.Info(configs.Length + " TechTree node found. A patch may be wrong. Using the first one"); + logger.Info($"{count} TechTree node found. A patch may be wrong. Using the first one"); } ConfigNode techNode = new ConfigNode("TechTree"); - techNode.AddNode(configs[0].config); + techNode.AddNode(configs.First().Node); techNode.Save(techTreePath); } @@ -501,7 +500,7 @@ private IEnumerable LoadCache() // Create the fake file where we load the physic config cache UrlDir gameDataDir = GameDatabase.Instance.root.AllDirectories.First(d => d.path.EndsWith("GameData") && d.name == "" && d.url == ""); // need to use a file with a cfg extension to get the right fileType or you can't AddConfig on it - physicsUrlFile = new UrlDir.UrlFile(gameDataDir, new FileInfo(defaultPhysicsPath)); + UrlDir.UrlFile physicsUrlFile = new UrlDir.UrlFile(gameDataDir, new FileInfo(defaultPhysicsPath)); gameDataDir.files.Add(physicsUrlFile); List databaseConfigs = new List(cache.nodes.Count); From 65f0a36beb784c910726287981870b3a5579bc2d Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 24 Jan 2019 23:50:24 -0800 Subject: [PATCH 064/140] Make physics and tech tree node names constant --- ModuleManager/MMPatchLoader.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index fc1dc42c..4130cf4e 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -28,6 +28,9 @@ namespace ModuleManager [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] public class MMPatchLoader { + private const string PHYSICS_NODE_NAME = "PHYSICSGLOBALS"; + private const string TECH_TREE_NODE_NAME = "TechTree"; + public string status = ""; public string errors = ""; @@ -246,25 +249,25 @@ private void LoadPhysicsConfig() physicsUrlFile.configs.Clear(); // And reload it properly ConfigNode physicsContent = ConfigNode.Load(defaultPhysicsPath); - physicsContent.name = "PHYSICSGLOBALS"; + physicsContent.name = PHYSICS_NODE_NAME; physicsUrlFile.AddConfig(physicsContent); gameDataDir.files.Add(physicsUrlFile); } private void SaveModdedPhysics(IEnumerable databaseConfigs) { - IEnumerable configs = databaseConfigs.Where(config => config.NodeType == "PHYSICSGLOBALS"); + IEnumerable configs = databaseConfigs.Where(config => config.NodeType == PHYSICS_NODE_NAME); int count = configs.Count(); if (count == 0) { - logger.Info("No PHYSICSGLOBALS node found. No custom Physics config will be saved"); + logger.Info($"No {PHYSICS_NODE_NAME} node found. No custom Physics config will be saved"); return; } if (count > 1) { - logger.Info($"{count} PHYSICSGLOBALS node found. A patch may be wrong. Using the first one"); + logger.Info($"{count} {PHYSICS_NODE_NAME} nodes found. A patch may be wrong. Using the first one"); } configs.First().Node.Save(physicsPath); @@ -471,21 +474,21 @@ private void CreateCache(IEnumerable databaseConfigs, int patch private void SaveModdedTechTree(IEnumerable databaseConfigs) { - IEnumerable configs = databaseConfigs.Where(config => config.NodeType == "TechTree"); + IEnumerable configs = databaseConfigs.Where(config => config.NodeType == TECH_TREE_NODE_NAME); int count = configs.Count(); if (count == 0) { - logger.Info("No TechTree node found. No custom TechTree will be saved"); + logger.Info($"No {TECH_TREE_NODE_NAME} node found. No custom {TECH_TREE_NODE_NAME} will be saved"); return; } if (count > 1) { - logger.Info($"{count} TechTree node found. A patch may be wrong. Using the first one"); + logger.Info($"{count} {TECH_TREE_NODE_NAME} nodes found. A patch may be wrong. Using the first one"); } - ConfigNode techNode = new ConfigNode("TechTree"); + ConfigNode techNode = new ConfigNode(TECH_TREE_NODE_NAME); techNode.AddNode(configs.First().Node); techNode.Save(techTreePath); } From 89a807960ba09dee297cdecdd51d231640e7215a Mon Sep 17 00:00:00 2001 From: sarbian Date: Fri, 25 Jan 2019 10:36:36 +0100 Subject: [PATCH 065/140] v4.0.1 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index 56e080a3..5ad144f3 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.0.0")] +[assembly: AssemblyVersion("4.0.1")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 4ebd40a5b6ebd770c0cd0ea5d3c280fbc22b706b Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 2 Feb 2019 00:27:26 -0800 Subject: [PATCH 066/140] Fix datetime format Don't want minute as month --- ModuleManager/Logging/StreamLogger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs index c439149c..8f4b131a 100644 --- a/ModuleManager/Logging/StreamLogger.cs +++ b/ModuleManager/Logging/StreamLogger.cs @@ -6,7 +6,7 @@ namespace ModuleManager.Logging { public class StreamLogger : IBasicLogger, IDisposable { - private const string DATETIME_FORMAT_STRING = "yyyy-mm-dd HH:mm:ss.fff"; + private const string DATETIME_FORMAT_STRING = "yyyy-MM-dd HH:mm:ss.fff"; private readonly Stream stream; private readonly StreamWriter streamWriter; From 2b356b0db6578ce2c723acbb566a5756ae75775b Mon Sep 17 00:00:00 2001 From: blowfish Date: Fri, 1 Feb 2019 21:17:58 -0800 Subject: [PATCH 067/140] Use KSP's directory listing rather than file system Avoids asking for Unity's application root path on a thread, which apparently breaks badly in debug mode --- ModuleManager/ModListGenerator.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ModuleManager/ModListGenerator.cs b/ModuleManager/ModListGenerator.cs index cca98805..bd0e8453 100644 --- a/ModuleManager/ModListGenerator.cs +++ b/ModuleManager/ModListGenerator.cs @@ -117,11 +117,10 @@ public static IEnumerable GenerateModList(IPatchProgress progress, IBasi } } modListInfo.Append("Mods by directory (sub directories of GameData):\n"); - string gameData = Path.Combine(Path.GetFullPath(KSPUtil.ApplicationRootPath), "GameData"); - foreach (string subdir in Directory.GetDirectories(gameData)) + UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData); + foreach (UrlDir subDir in gameData.children) { - string name = Path.GetFileName(subdir); - string cleanName = name.RemoveWS(); + string cleanName = subDir.name.RemoveWS(); if (!mods.Contains(cleanName, StringComparer.OrdinalIgnoreCase)) { mods.Add(cleanName); From 9d9808790ce4a9ab345a69c9ba0ccfe2a992c865 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 2 Feb 2019 00:43:22 -0800 Subject: [PATCH 068/140] Better status updates in post patch Each step can take a while, so explicitly say what's going on --- ModuleManager/PostPatchLoader.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index 630b3818..9da8ce4b 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -26,6 +26,8 @@ public class PostPatchLoader : LoadingSystem private bool ready = false; + private string progressTitle = "ModuleManager: Post patch"; + public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) { if (!postPatchCallbacks.Contains(callback)) @@ -48,7 +50,7 @@ private void Awake() public override float ProgressFraction() => 1; - public override string ProgressTitle() => "ModuleManager : post patch"; + public override string ProgressTitle() => progressTitle; public override void StartLoad() { @@ -61,6 +63,8 @@ private IEnumerator Run() Stopwatch waitTimer = new Stopwatch(); waitTimer.Start(); + progressTitle = "ModuleManager: Waiting for patching to finish"; + while (databaseConfigs == null) yield return null; waitTimer.Stop(); @@ -69,6 +73,7 @@ private IEnumerator Run() Stopwatch postPatchTimer = new Stopwatch(); postPatchTimer.Start(); + progressTitle = "ModuleManager: Applying patched game database"; logger.Info("Applying patched game database"); foreach (UrlDir.UrlFile file in GameDatabase.Instance.root.AllConfigFiles) @@ -87,6 +92,7 @@ private IEnumerator Run() if (File.Exists(logPath)) { + progressTitle = "ModuleManager: Dumping log to KSP log"; logger.Info("Dumping ModuleManager log to main log"); logger.Info("\n#### BEGIN MODULEMANAGER LOG ####\n\n\n" + File.ReadAllText(logPath) + "\n\n\n#### END MODULEMANAGER LOG ####"); } @@ -104,6 +110,8 @@ private IEnumerator Run() yield return null; + progressTitle = "ModuleManager: Reloading things"; + logger.Info("Reloading resources definitions"); PartResourceLibrary.Instance.LoadDefinitions(); @@ -115,6 +123,7 @@ private IEnumerator Run() yield return null; + progressTitle = "ModuleManager: Running post patch callbacks"; logger.Info("Running post patch callbacks"); foreach (ModuleManagerPostPatchCallback callback in postPatchCallbacks) From 20d339ad0922fe38d70acd517452909d7c27ab61 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 2 Feb 2019 00:54:43 -0800 Subject: [PATCH 069/140] Add -mm-dont-copy-logs command line flag If set, don't copy MM log to main log. Intended for advanced users with very larger installs where this takes a significant amount of time --- ModuleManager/ModuleManager.cs | 3 +++ ModuleManager/PostPatchLoader.cs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index ac92cddc..4d7e76de 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -33,6 +33,7 @@ public class ModuleManager : MonoBehaviour private bool nyan = false; private bool nCats = false; public static bool dumpPostPatch = false; + public static bool DontCopyLogs { get; private set; } = false; private PopupDialog menu; @@ -152,6 +153,8 @@ internal void Awake() dumpPostPatch = Environment.GetCommandLineArgs().Contains("-mm-dump"); + DontCopyLogs = Environment.GetCommandLineArgs().Contains("-mm-dont-copy-logs"); + loadedInScene = true; } diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index 9da8ce4b..758ab0ae 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -92,9 +92,16 @@ private IEnumerator Run() if (File.Exists(logPath)) { - progressTitle = "ModuleManager: Dumping log to KSP log"; - logger.Info("Dumping ModuleManager log to main log"); - logger.Info("\n#### BEGIN MODULEMANAGER LOG ####\n\n\n" + File.ReadAllText(logPath) + "\n\n\n#### END MODULEMANAGER LOG ####"); + if (ModuleManager.DontCopyLogs) + { + logger.Info("Not dumping log because -mm-dont-copy-logs was set"); + } + else + { + progressTitle = "ModuleManager: Dumping log to KSP log"; + logger.Info("Dumping ModuleManager log to main log"); + logger.Info("\n#### BEGIN MODULEMANAGER LOG ####\n\n\n" + File.ReadAllText(logPath) + "\n\n\n#### END MODULEMANAGER LOG ####"); + } } else { From c43268f5d4ece8ef2f5c5c55537cd12db081c6cf Mon Sep 17 00:00:00 2001 From: blowfish Date: Sat, 2 Feb 2019 00:19:23 -0800 Subject: [PATCH 070/140] Allow assemblies to add to mod list Allow ModuleManagerAddToModList callback to be defined as a static method or on MonoBehaviour instances which returns an IEnumerable to be added to the mod list It will always be called on the main thread Result will also be added to the config sha to ensure proper cache invalidation --- ModuleManager/MMPatchLoader.cs | 14 +++- ModuleManager/MMPatchRunner.cs | 6 +- ModuleManager/ModListGenerator.cs | 107 +++++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 5 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 4130cf4e..93447f74 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -46,15 +46,17 @@ public class MMPatchLoader private const float TIME_TO_WAIT_FOR_LOGS = 0.05f; - private IBasicLogger logger; + private readonly IEnumerable modsAddedByAssemblies; + private readonly IBasicLogger logger; public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) { PostPatchLoader.AddPostPatchCallback(callback); } - public MMPatchLoader(IBasicLogger logger) + public MMPatchLoader(IEnumerable modsAddedByAssemblies, IBasicLogger logger) { + this.modsAddedByAssemblies = modsAddedByAssemblies ?? throw new ArgumentNullException(nameof(modsAddedByAssemblies)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -100,7 +102,7 @@ public IEnumerable Run() IPatchProgress progress = new PatchProgress(patchLogger); status = "Pre patch init"; patchLogger.Info(status); - IEnumerable mods = ModListGenerator.GenerateModList(progress, patchLogger); + IEnumerable mods = ModListGenerator.GenerateModList(modsAddedByAssemblies, progress, patchLogger); // If we don't use the cache then it is best to clean the PartDatabase.cfg if (!keepPartDB && File.Exists(partDatabasePath)) @@ -313,6 +315,12 @@ private bool IsCacheUpToDate() sha.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0); } + foreach (ModListGenerator.ModAddedByAssembly mod in modsAddedByAssemblies) + { + byte[] modBytes = Encoding.UTF8.GetBytes(mod.modName); + sha.TransformBlock(modBytes, 0, modBytes.Length, modBytes, 0); + } + byte[] godsFinalMessageToHisCreation = Encoding.UTF8.GetBytes("We apologize for the inconvenience."); sha.TransformFinalBlock(godsFinalMessageToHisCreation, 0, godsFinalMessageToHisCreation.Length); diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index 6852464c..e6138254 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -48,9 +48,13 @@ public IEnumerator Run() // Wait for game database to be initialized for the 2nd time yield return null; + IBasicLogger mmLogger = new QueueLogger(mmLogQueue); + + IEnumerable modsAddedByAssemblies = ModListGenerator.GetAdditionalModsFromStaticMethods(mmLogger); + IEnumerable databaseConfigs = null; - MMPatchLoader patchLoader = new MMPatchLoader(new QueueLogger(mmLogQueue)); + MMPatchLoader patchLoader = new MMPatchLoader(modsAddedByAssemblies, mmLogger); ITaskStatus patchingThreadStatus = BackgroundTask.Start(delegate { diff --git a/ModuleManager/ModListGenerator.cs b/ModuleManager/ModListGenerator.cs index bd0e8453..97292ba0 100644 --- a/ModuleManager/ModListGenerator.cs +++ b/ModuleManager/ModListGenerator.cs @@ -5,6 +5,7 @@ using System.Text; using System.Diagnostics; using System.Reflection; +using UnityEngine; using ModuleManager.Extensions; using ModuleManager.Logging; using ModuleManager.Utils; @@ -14,7 +15,7 @@ namespace ModuleManager { public static class ModListGenerator { - public static IEnumerable GenerateModList(IPatchProgress progress, IBasicLogger logger) + public static IEnumerable GenerateModList(IEnumerable modsAddedByAssemblies, IPatchProgress progress, IBasicLogger logger) { #region List of mods @@ -127,6 +128,17 @@ public static IEnumerable GenerateModList(IPatchProgress progress, IBasi modListInfo.AppendFormat(" {0}\n", cleanName); } } + + modListInfo.Append("Mods added by assemblies:\n"); + foreach (ModAddedByAssembly mod in modsAddedByAssemblies) + { + if (!mods.Contains(mod.modName, StringComparer.OrdinalIgnoreCase)) + { + mods.Add(mod.modName); + modListInfo.AppendFormat(" {0}\n", mod); + } + } + logger.Info(modListInfo.ToString()); mods.Sort(); @@ -135,5 +147,98 @@ public static IEnumerable GenerateModList(IPatchProgress progress, IBasi return mods; } + + public class ModAddedByAssembly + { + public readonly string modName; + public readonly string assemblyName; + + public ModAddedByAssembly(string modName, string assemblyName) + { + this.modName = modName ?? throw new ArgumentNullException(nameof(modName)); + this.assemblyName = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName)); + } + + public override string ToString() + { + return $"{modName} (added by {assemblyName})"; + } + } + + public static IEnumerable GetAdditionalModsFromStaticMethods(IBasicLogger logger) + { + List result = new List(); + foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + foreach (Type type in ass.GetTypes()) + { + MethodInfo method = type.GetMethod("ModuleManagerAddToModList", BindingFlags.Public | BindingFlags.Static); + + if (method != null && method.GetParameters().Length == 0 && typeof(IEnumerable).IsAssignableFrom(method.ReturnType)) + { + string methodName = $"{ass.GetName().Name}.{type.Name}.{method.Name}()"; + try + { + logger.Info("Calling " + methodName); + IEnumerable modsToAdd = (IEnumerable)method.Invoke(null, null); + + if (modsToAdd == null) + { + logger.Error("ModuleManagerAddToModList returned null: " + methodName); + continue; + } + + foreach (string mod in modsToAdd) + { + result.Add(new ModAddedByAssembly(mod, ass.GetName().Name)); + } + } + catch (Exception e) + { + logger.Exception("Exception while calling " + methodName, e); + } + } + } + } + catch (Exception e) + { + logger.Exception("Add to mod list threw an exception in loading " + ass.FullName, e); + } + } + + foreach (MonoBehaviour obj in UnityEngine.Object.FindObjectsOfType()) + { + MethodInfo method = obj.GetType().GetMethod("ModuleManagerAddToModList", BindingFlags.Public | BindingFlags.Instance); + + if (method != null && method.GetParameters().Length == 0 && typeof(IEnumerable).IsAssignableFrom(method.ReturnType)) + { + string methodName = $"{obj.GetType().Name}.{method.Name}()"; + try + { + logger.Info("Calling " + methodName); + IEnumerable modsToAdd = (IEnumerable)method.Invoke(obj, null); + + if (modsToAdd == null) + { + logger.Error("ModuleManagerAddToModList returned null: " + methodName); + continue; + } + + foreach (string mod in modsToAdd) + { + result.Add(new ModAddedByAssembly(mod, obj.GetType().Assembly.GetName().Name)); + } + } + catch (Exception e) + { + logger.Exception("Exception while calling " + methodName, e); + } + } + } + + return result; + } } } From 811fe8afa127b2bbad03ff5d1189a0c0c54f273b Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Sat, 2 Feb 2019 15:44:25 -0800 Subject: [PATCH 071/140] wait another frame for plugins to finish initializing --- ModuleManager/MMPatchRunner.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index e6138254..802b76fb 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -45,7 +45,8 @@ public IEnumerator Run() } }); - // Wait for game database to be initialized for the 2nd time + // Wait for game database to be initialized for the 2nd time and wait for any plugins to initialize + yield return null; yield return null; IBasicLogger mmLogger = new QueueLogger(mmLogQueue); From 922114970bbbff001d70cdd3a833419407faaa38 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Sat, 2 Feb 2019 21:33:40 -0800 Subject: [PATCH 072/140] Don't use Unity's time since startup in thread We only care about delta time here anyway so stopwatch should be fine --- ModuleManager/MMPatchLoader.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 93447f74..f3e0ab91 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -42,9 +42,7 @@ public class MMPatchLoader private string configSha; private Dictionary filesSha = new Dictionary(); - private const float yieldInterval = 1f/30f; // Patch at ~30fps - - private const float TIME_TO_WAIT_FOR_LOGS = 0.05f; + private const int STATUS_UPDATE_INVERVAL_MS = 33; private readonly IEnumerable modsAddedByAssemblies; private readonly IBasicLogger logger; @@ -135,7 +133,6 @@ public IEnumerable Run() patchLogger.Info(status); IPass currentPass = null; - float nextUpdate = Time.realtimeSinceStartup + yieldInterval; progress.OnPassStarted.Add(delegate (IPass pass) { @@ -143,18 +140,24 @@ public IEnumerable Run() StatusUpdate(progress, currentPass.Name); }); + System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); + stopwatch.Start(); + progress.OnPatchApplied.Add(delegate { - if (Time.realtimeSinceStartup > nextUpdate) + long timeRemaining = STATUS_UPDATE_INVERVAL_MS - stopwatch.ElapsedMilliseconds; + if (timeRemaining < 0) { StatusUpdate(progress, currentPass.Name); - nextUpdate = Time.realtimeSinceStartup + yieldInterval; + stopwatch.Reset(); + stopwatch.Start(); } }); PatchApplier applier = new PatchApplier(progress, patchLogger); databaseConfigs = applier.ApplyPatches(patchList); + stopwatch.Stop(); StatusUpdate(progress); patchLogger.Info("Done patching"); From 6a2ee3ddad4a8c6088886968f45dbf2da8a5c6eb Mon Sep 17 00:00:00 2001 From: Sarbian Date: Sun, 3 Feb 2019 19:39:39 +0100 Subject: [PATCH 073/140] v4.0.2 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index 5ad144f3..43b2dc37 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.0.1")] +[assembly: AssemblyVersion("4.0.2")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 918b0a24de7f604b31a058395e4e2709c7dc8ede Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 19 Apr 2019 17:15:36 -0700 Subject: [PATCH 074/140] add thread-safe KeyValueCache --- ModuleManager/Collections/KeyValueCache.cs | 29 ++++++++++ ModuleManager/ModuleManager.csproj | 1 + .../Collections/KeyValueCacheTest.cs | 53 +++++++++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 4 files changed, 84 insertions(+) create mode 100644 ModuleManager/Collections/KeyValueCache.cs create mode 100644 ModuleManagerTests/Collections/KeyValueCacheTest.cs diff --git a/ModuleManager/Collections/KeyValueCache.cs b/ModuleManager/Collections/KeyValueCache.cs new file mode 100644 index 00000000..0aaee61b --- /dev/null +++ b/ModuleManager/Collections/KeyValueCache.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace ModuleManager.Collections +{ + public class KeyValueCache + { + private readonly Dictionary dict = new Dictionary(); + private readonly object lockObject = new object(); + + public TValue Fetch(TKey key, Func createValue) + { + if (createValue == null) throw new ArgumentNullException(nameof(createValue)); + lock(lockObject) + { + if (dict.TryGetValue(key, out TValue value)) + { + return value; + } + else + { + TValue newValue = createValue(); + dict.Add(key, newValue); + return newValue; + } + } + } + } +} diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 92ce283a..c4913ef7 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -38,6 +38,7 @@ + diff --git a/ModuleManagerTests/Collections/KeyValueCacheTest.cs b/ModuleManagerTests/Collections/KeyValueCacheTest.cs new file mode 100644 index 00000000..4fc5a9e6 --- /dev/null +++ b/ModuleManagerTests/Collections/KeyValueCacheTest.cs @@ -0,0 +1,53 @@ +using System; +using Xunit; +using ModuleManager.Collections; + +namespace ModuleManagerTests.Collections +{ + public class KeyValueCacheTest + { + [Fact] + public void TestFetch__CreateValueNull() + { + KeyValueCache cache = new KeyValueCache(); + ArgumentNullException ex = Assert.Throws(delegate + { + cache.Fetch(new object(), null); + }); + + Assert.Equal("createValue", ex.ParamName); + } + + [Fact] + public void TestFetch__KeyNotPresent() + { + object key = new object(); + object value = new object(); + KeyValueCache cache = new KeyValueCache(); + + object fetchedValue = cache.Fetch(key, () => value); + + Assert.Same(value, fetchedValue); + } + + [Fact] + public void TestFetch__KeyPresent() + { + object key = new object(); + object value = new object(); + KeyValueCache cache = new KeyValueCache(); + + cache.Fetch(key, () => value); + + bool called2ndTime = false; + object fetchedValue = cache.Fetch(key, delegate + { + called2ndTime = true; + return null; + }); + + Assert.Same(value, fetchedValue); + Assert.False(called2ndTime); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 7e1bd948..d9aabe53 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -110,6 +110,7 @@ + From 6e6a465c4a04578772eb989b9f2132b3dd4b4d56 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 19 Apr 2019 17:34:03 -0700 Subject: [PATCH 075/140] use new KeyValueCache class as regex cache Makes it thread safe - doesn't matter in production but might matter in tests which can be run in parallel --- ModuleManager/MMPatchLoader.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index f3e0ab91..4ab1827a 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -37,7 +37,7 @@ public class MMPatchLoader public static bool keepPartDB = false; - private static readonly Dictionary regexCache = new Dictionary(); + private static readonly KeyValueCache regexCache = new KeyValueCache(); private string configSha; private Dictionary filesSha = new Dictionary(); @@ -1447,14 +1447,10 @@ private static string FindAndReplaceValue( { string[] split = value.Split(value[0]); - Regex replace; - if (regexCache.ContainsKey(split[1])) - replace = regexCache[split[1]]; - else + Regex replace = regexCache.Fetch(split[1], delegate { - replace = new Regex(split[1], RegexOptions.None); - regexCache.Add(split[1], replace); - } + return new Regex(split[1]); + }); value = replace.Replace(oValue, split[2]); } @@ -1659,11 +1655,10 @@ public static bool WildcardMatch(string s, string wildcard) return true; string pattern = "^" + Regex.Escape(wildcard).Replace(@"\*", ".*").Replace(@"\?", ".") + "$"; - if (!regexCache.TryGetValue(pattern, out Regex regex)) + Regex regex = regexCache.Fetch(pattern, delegate { - regex = new Regex(pattern); - regexCache.Add(pattern, regex); - } + return new Regex(pattern); + }); return regex.IsMatch(s); } From a0c1dfc2c7342c8ee1010bee9a3019a3d4eb2b31 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 19 Apr 2019 16:33:05 -0700 Subject: [PATCH 076/140] add test for ProtoUrlConfig --- ModuleManagerTests/ModuleManagerTests.csproj | 1 + ModuleManagerTests/ProtoUrlConfigTest.cs | 74 ++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 ModuleManagerTests/ProtoUrlConfigTest.cs diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index d9aabe53..a550a59a 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -108,6 +108,7 @@ + diff --git a/ModuleManagerTests/ProtoUrlConfigTest.cs b/ModuleManagerTests/ProtoUrlConfigTest.cs new file mode 100644 index 00000000..9a1cfcdb --- /dev/null +++ b/ModuleManagerTests/ProtoUrlConfigTest.cs @@ -0,0 +1,74 @@ +using System; +using Xunit; +using TestUtils; +using ModuleManager; + +namespace ModuleManagerTests +{ + public class ProtoUrlConfigTest + { + [Fact] + public void TestContructor__UrlFileNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new ProtoUrlConfig(null, new ConfigNode()); + }); + + Assert.Equal("urlFile", ex.ParamName); + } + + [Fact] + public void TestContructor__NodeNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new ProtoUrlConfig(UrlBuilder.CreateFile("foo/bar"), null); + }); + + Assert.Equal("node", ex.ParamName); + } + + [Fact] + public void TestUrlFile() + { + UrlDir.UrlFile urlFile = UrlBuilder.CreateFile("abc/def.cfg"); + ProtoUrlConfig protoUrlConfig = new ProtoUrlConfig(urlFile, new ConfigNode()); + + Assert.Same(urlFile, protoUrlConfig.UrlFile); + } + + [Fact] + public void TestNode() + { + ConfigNode node = new ConfigNode("NODE"); + ProtoUrlConfig protoUrlConfig = new ProtoUrlConfig(UrlBuilder.CreateFile("foo/bar"), node); + + Assert.Same(node, protoUrlConfig.Node); + } + + [Fact] + public void TestFileUrl() + { + ProtoUrlConfig protoUrlConfig = new ProtoUrlConfig(UrlBuilder.CreateFile("abc/def.cfg"), new ConfigNode()); + + Assert.Equal("abc/def.cfg", protoUrlConfig.FileUrl); + } + + [Fact] + public void TestNodeType() + { + ProtoUrlConfig protoUrlConfig = new ProtoUrlConfig(UrlBuilder.CreateFile("abc/def"), new ConfigNode("SOME_NODE")); + + Assert.Equal("SOME_NODE", protoUrlConfig.NodeType); + } + + [Fact] + public void TestFullUrl() + { + ProtoUrlConfig protoUrlConfig = new ProtoUrlConfig(UrlBuilder.CreateFile("abc/def.cfg"), new ConfigNode("SOME_NODE")); + + Assert.Equal("abc/def.cfg/SOME_NODE", protoUrlConfig.FullUrl); + } + } +} From 72717253e9d96fa05a9ca19603b8dbbe3964c444 Mon Sep 17 00:00:00 2001 From: blowfish Date: Fri, 19 Apr 2019 20:15:10 -0700 Subject: [PATCH 077/140] Add name value to applying patch messages Makes debugging patches easier, since multiple root nodes can save the same url Resolves #143 --- ModuleManager/ProtoUrlConfig.cs | 3 +++ ModuleManagerTests/ProtoUrlConfigTest.cs | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/ModuleManager/ProtoUrlConfig.cs b/ModuleManager/ProtoUrlConfig.cs index 74533948..71f39df3 100644 --- a/ModuleManager/ProtoUrlConfig.cs +++ b/ModuleManager/ProtoUrlConfig.cs @@ -29,6 +29,9 @@ public ProtoUrlConfig(UrlDir.UrlFile urlFile, ConfigNode node) Node = node ?? throw new ArgumentNullException(nameof(node)); FileUrl = UrlFile.url + '.' + urlFile.fileExtension; FullUrl = FileUrl + '/' + Node.name; + + if (node.GetValue("name") is string nameValue) + FullUrl += '[' + nameValue + ']'; } } } diff --git a/ModuleManagerTests/ProtoUrlConfigTest.cs b/ModuleManagerTests/ProtoUrlConfigTest.cs index 9a1cfcdb..f16cd1bd 100644 --- a/ModuleManagerTests/ProtoUrlConfigTest.cs +++ b/ModuleManagerTests/ProtoUrlConfigTest.cs @@ -70,5 +70,18 @@ public void TestFullUrl() Assert.Equal("abc/def.cfg/SOME_NODE", protoUrlConfig.FullUrl); } + + [Fact] + public void TestFullUrl__NameValue() + { + ConfigNode node = new TestConfigNode("SOME_NODE") + { + { "name", "some_value" }, + }; + + ProtoUrlConfig protoUrlConfig = new ProtoUrlConfig(UrlBuilder.CreateFile("abc/def.cfg"), node); + + Assert.Equal("abc/def.cfg/SOME_NODE[some_value]", protoUrlConfig.FullUrl); + } } } From cfaac6aa5031db26d8d84216f5e6885314a10306 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 29 Apr 2019 21:25:15 -0700 Subject: [PATCH 078/140] Fix StreamLogger ignoring messages Resolves #145 --- ModuleManager/Logging/StreamLogger.cs | 4 ++- .../Logging/StreamLoggerTest.cs | 31 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs index 8f4b131a..6e157698 100644 --- a/ModuleManager/Logging/StreamLogger.cs +++ b/ModuleManager/Logging/StreamLogger.cs @@ -42,7 +42,9 @@ public void Log(LogType logType, string message) public void Exception(string message, Exception exception) { - Log(LogType.Exception, exception?.ToString() ?? ""); + if (!string.IsNullOrEmpty(message)) message += ": "; + message += exception?.ToString() ?? ""; + Log(LogType.Exception, message); } public void Dispose() diff --git a/ModuleManagerTests/Logging/StreamLoggerTest.cs b/ModuleManagerTests/Logging/StreamLoggerTest.cs index fdcb832c..f577adae 100644 --- a/ModuleManagerTests/Logging/StreamLoggerTest.cs +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -147,12 +147,13 @@ public void TestLog__Error() [Fact] public void TestLog__Exception() { - byte[] bytes = new byte[50]; + Exception ex = new Exception("something went wrong"); + byte[] bytes = new byte[100]; using (MemoryStream stream = new MemoryStream(bytes, true)) { using (StreamLogger streamLogger = new StreamLogger(stream)) { - streamLogger.Log(LogType.Exception, "a message"); + streamLogger.Exception("a message", ex); } } @@ -162,7 +163,31 @@ public void TestLog__Exception() { string result = reader.ReadToEnd(); Assert.Contains("[EXC ", result); - Assert.Contains("] a message", result); + Assert.Contains("] a message: " + ex.ToString(), result); + } + } + } + + [Fact] + public void TestLog__Exception__NullMessage() + { + Exception ex = new Exception("something went wrong"); + byte[] bytes = new byte[100]; + using (MemoryStream stream = new MemoryStream(bytes, true)) + { + using (StreamLogger streamLogger = new StreamLogger(stream)) + { + streamLogger.Exception(null, ex); + } + } + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd(); + Assert.Contains("[EXC ", result); + Assert.Contains("] " + ex.ToString(), result); } } } From d4d0fb78126ad4979c7df55f47dd4e9fc1e55b9b Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Apr 2019 00:24:38 -0700 Subject: [PATCH 079/140] Redo logging interface * Convert exceptions to strings rather than keeping a separate interface for exceptions everywhere * Log messages know how to convert themselves to log strings * Simplified interface (including assertions) provided by extension methods --- .../Extensions/IBasicLoggerExtensions.cs | 19 ++- ModuleManager/Logging/ExceptionMessage.cs | 21 --- ModuleManager/Logging/IBasicLogger.cs | 4 +- ModuleManager/Logging/ILogMessage.cs | 5 +- ModuleManager/Logging/LogMessage.cs | 43 +++++ ModuleManager/Logging/LogSplitter.cs | 14 +- ModuleManager/Logging/ModLogger.cs | 14 +- ModuleManager/Logging/NormalMessage.cs | 22 --- ModuleManager/Logging/QueueLogRunner.cs | 4 +- ModuleManager/Logging/QueueLogger.cs | 10 +- ModuleManager/Logging/StreamLogger.cs | 29 +--- ModuleManager/Logging/UnityLogger.cs | 9 +- ModuleManager/ModuleManager.csproj | 3 +- .../Extensions/IBasicLoggerExtensionsTest.cs | 55 ++++++- ModuleManagerTests/InGameTestRunnerTest.cs | 24 +-- .../Logging/ExceptionMessageTest.cs | 22 --- ModuleManagerTests/Logging/LogMessageTest.cs | 70 ++++++++ ModuleManagerTests/Logging/LogSplitterTest.cs | 23 +-- ModuleManagerTests/Logging/ModLoggerTest.cs | 30 ++-- .../Logging/NormalMessageTest.cs | 37 ----- .../Logging/QueueLogRunnerTest.cs | 12 +- ModuleManagerTests/Logging/QueueLoggerTest.cs | 36 ++--- .../Logging/StreamLoggerTest.cs | 153 ++---------------- ModuleManagerTests/Logging/UnityLoggerTest.cs | 24 ++- ModuleManagerTests/LoggingAssertionHelpers.cs | 70 ++++++++ ModuleManagerTests/MMPatchLoaderTest.cs | 6 +- ModuleManagerTests/ModuleManagerTests.csproj | 4 +- ModuleManagerTests/PatchApplierTest.cs | 6 +- ModuleManagerTests/Patches/EditPatchTest.cs | 2 +- .../Progress/PatchProgressTest.cs | 60 +++---- 30 files changed, 406 insertions(+), 425 deletions(-) delete mode 100644 ModuleManager/Logging/ExceptionMessage.cs create mode 100644 ModuleManager/Logging/LogMessage.cs delete mode 100644 ModuleManager/Logging/NormalMessage.cs delete mode 100644 ModuleManagerTests/Logging/ExceptionMessageTest.cs create mode 100644 ModuleManagerTests/Logging/LogMessageTest.cs delete mode 100644 ModuleManagerTests/Logging/NormalMessageTest.cs create mode 100644 ModuleManagerTests/LoggingAssertionHelpers.cs diff --git a/ModuleManager/Extensions/IBasicLoggerExtensions.cs b/ModuleManager/Extensions/IBasicLoggerExtensions.cs index 46c4c5a4..9fd29be3 100644 --- a/ModuleManager/Extensions/IBasicLoggerExtensions.cs +++ b/ModuleManager/Extensions/IBasicLoggerExtensions.cs @@ -6,8 +6,21 @@ namespace ModuleManager.Extensions { public static class IBasicLoggerExtensions { - public static void Info(this IBasicLogger logger, string message) => logger.Log(LogType.Log, message); - public static void Warning(this IBasicLogger logger, string message) => logger.Log(LogType.Warning, message); - public static void Error(this IBasicLogger logger, string message) => logger.Log(LogType.Error, message); + public static void Info(this IBasicLogger logger, string message) => logger.Log(new LogMessage(LogType.Log, message)); + public static void Warning(this IBasicLogger logger, string message) => logger.Log(new LogMessage(LogType.Warning, message)); + public static void Error(this IBasicLogger logger, string message) => logger.Log(new LogMessage(LogType.Error, message)); + + public static void Exception(this IBasicLogger logger, Exception exception) + { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + logger.Log(new LogMessage(LogType.Exception, exception.ToString())); + } + + public static void Exception(this IBasicLogger logger, string message, Exception exception) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + if (exception == null) throw new ArgumentNullException(nameof(exception)); + logger.Log(new LogMessage(LogType.Exception, message + ": " + exception.ToString())); + } } } diff --git a/ModuleManager/Logging/ExceptionMessage.cs b/ModuleManager/Logging/ExceptionMessage.cs deleted file mode 100644 index fceb166d..00000000 --- a/ModuleManager/Logging/ExceptionMessage.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace ModuleManager.Logging -{ - public class ExceptionMessage : ILogMessage - { - public readonly string message; - public readonly Exception exception; - - public ExceptionMessage(string message, Exception exception) - { - this.message = message; - this.exception = exception; - } - - public void LogTo(IBasicLogger logger) - { - logger.Exception(message, exception); - } - } -} diff --git a/ModuleManager/Logging/IBasicLogger.cs b/ModuleManager/Logging/IBasicLogger.cs index c2fdd85e..25cd9367 100644 --- a/ModuleManager/Logging/IBasicLogger.cs +++ b/ModuleManager/Logging/IBasicLogger.cs @@ -1,12 +1,10 @@ using System; -using UnityEngine; namespace ModuleManager.Logging { // Stripped down version of UnityEngine.ILogger public interface IBasicLogger { - void Log(LogType logType, string message); - void Exception(string message, Exception exception); + void Log(ILogMessage message); } } diff --git a/ModuleManager/Logging/ILogMessage.cs b/ModuleManager/Logging/ILogMessage.cs index 3193f973..55137cc7 100644 --- a/ModuleManager/Logging/ILogMessage.cs +++ b/ModuleManager/Logging/ILogMessage.cs @@ -1,9 +1,12 @@ using System; +using UnityEngine; namespace ModuleManager.Logging { public interface ILogMessage { - void LogTo(IBasicLogger logger); + LogType LogType { get; } + string Message { get; } + string ToLogString(); } } diff --git a/ModuleManager/Logging/LogMessage.cs b/ModuleManager/Logging/LogMessage.cs new file mode 100644 index 00000000..53f3c0f8 --- /dev/null +++ b/ModuleManager/Logging/LogMessage.cs @@ -0,0 +1,43 @@ +using System; +using UnityEngine; + +namespace ModuleManager.Logging +{ + public class LogMessage : ILogMessage + { + private const string DATETIME_FORMAT_STRING = "yyyy-MM-dd HH:mm:ss.fff"; + + public LogType LogType { get; } + public string Message { get; } + + public LogMessage(LogType logType, string message) + { + LogType = logType; + Message = message ?? throw new ArgumentNullException(nameof(message)); + } + + public string ToLogString() + { + string prefix; + if (LogType == LogType.Log) + prefix = "LOG"; + else if (LogType == LogType.Warning) + prefix = "WRN"; + else if (LogType == LogType.Error) + prefix = "ERR"; + else if (LogType == LogType.Assert) + prefix = "AST"; + else if (LogType == LogType.Exception) + prefix = "EXC"; + else + prefix = "???"; + + return $"[{prefix} {DateTime.Now.ToString(DATETIME_FORMAT_STRING)}] {Message}"; + } + + public override string ToString() + { + return $"[{GetType().FullName} LogType={LogType} Message={Message}]"; + } + } +} diff --git a/ModuleManager/Logging/LogSplitter.cs b/ModuleManager/Logging/LogSplitter.cs index 53712f20..b1d2c89f 100644 --- a/ModuleManager/Logging/LogSplitter.cs +++ b/ModuleManager/Logging/LogSplitter.cs @@ -1,5 +1,4 @@ using System; -using UnityEngine; namespace ModuleManager.Logging { @@ -14,16 +13,11 @@ public LogSplitter(IBasicLogger logger1, IBasicLogger logger2) this.logger2 = logger2 ?? throw new ArgumentNullException(nameof(logger2)); } - public void Log(LogType logType, string message) + public void Log(ILogMessage message) { - logger1.Log(logType, message); - logger2.Log(logType, message); - } - - public void Exception(string message, Exception exception) - { - logger1.Exception(message, exception); - logger2.Exception(message, exception); + if (message == null) throw new ArgumentNullException(nameof(message)); + logger1.Log(message); + logger2.Log(message); } } } diff --git a/ModuleManager/Logging/ModLogger.cs b/ModuleManager/Logging/ModLogger.cs index eb2d3699..89a05066 100644 --- a/ModuleManager/Logging/ModLogger.cs +++ b/ModuleManager/Logging/ModLogger.cs @@ -1,21 +1,23 @@ using System; -using UnityEngine; namespace ModuleManager.Logging { public class ModLogger : IBasicLogger { - private string prefix; - private IBasicLogger logger; + private readonly string prefix; + private readonly IBasicLogger logger; public ModLogger(string prefix, IBasicLogger logger) { if (string.IsNullOrEmpty(prefix)) throw new ArgumentNullException(nameof(prefix)); - this.prefix = "[" + prefix + "] "; + this.prefix = $"[{prefix}] "; this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public void Log(LogType logType, string message) => logger.Log(logType, prefix + message); - public void Exception(string message, Exception exception) => logger.Exception(prefix + message, exception); + public void Log(ILogMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + logger.Log(new LogMessage(message.LogType, prefix + message.Message)); + } } } diff --git a/ModuleManager/Logging/NormalMessage.cs b/ModuleManager/Logging/NormalMessage.cs deleted file mode 100644 index 549f9aa8..00000000 --- a/ModuleManager/Logging/NormalMessage.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using UnityEngine; - -namespace ModuleManager.Logging -{ - public class NormalMessage : ILogMessage - { - public readonly LogType logType; - public readonly string message; - - public NormalMessage(LogType logType, string message) - { - this.logType = logType; - this.message = message; - } - - public void LogTo(IBasicLogger logger) - { - logger.Log(logType, message); - } - } -} diff --git a/ModuleManager/Logging/QueueLogRunner.cs b/ModuleManager/Logging/QueueLogRunner.cs index 2ed5c300..f78cf611 100644 --- a/ModuleManager/Logging/QueueLogRunner.cs +++ b/ModuleManager/Logging/QueueLogRunner.cs @@ -47,7 +47,7 @@ public void Run(IBasicLogger logger) foreach (ILogMessage message in logQueue.TakeAll()) { - message.LogTo(logger); + logger.Log(message); } long timeRemaining = timeToWaitForLogsMs - stopwatch.ElapsedMilliseconds; @@ -59,7 +59,7 @@ public void Run(IBasicLogger logger) foreach (ILogMessage message in logQueue.TakeAll()) { - message.LogTo(logger); + logger.Log(message); } state = State.Stopped; diff --git a/ModuleManager/Logging/QueueLogger.cs b/ModuleManager/Logging/QueueLogger.cs index 82d0bb8e..b7cd288a 100644 --- a/ModuleManager/Logging/QueueLogger.cs +++ b/ModuleManager/Logging/QueueLogger.cs @@ -1,5 +1,4 @@ using System; -using UnityEngine; using ModuleManager.Collections; namespace ModuleManager.Logging @@ -10,10 +9,13 @@ public class QueueLogger : IBasicLogger public QueueLogger(IMessageQueue queue) { - this.queue = queue; + this.queue = queue ?? throw new ArgumentNullException(nameof(queue)); } - public void Log(LogType logType, string message) => queue.Add(new NormalMessage(logType, message)); - public void Exception(string message, Exception exception) => queue.Add(new ExceptionMessage(message, exception)); + public void Log(ILogMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + queue.Add(message); + } } } diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs index 6e157698..bfbb1053 100644 --- a/ModuleManager/Logging/StreamLogger.cs +++ b/ModuleManager/Logging/StreamLogger.cs @@ -1,13 +1,10 @@ using System; using System.IO; -using UnityEngine; namespace ModuleManager.Logging { public class StreamLogger : IBasicLogger, IDisposable { - private const string DATETIME_FORMAT_STRING = "yyyy-MM-dd HH:mm:ss.fff"; - private readonly Stream stream; private readonly StreamWriter streamWriter; private bool disposed = false; @@ -19,32 +16,12 @@ public StreamLogger(Stream stream) streamWriter = new StreamWriter(stream); } - public void Log(LogType logType, string message) + public void Log(ILogMessage message) { if (disposed) throw new InvalidOperationException("Object has already been disposed"); + if (message == null) throw new ArgumentNullException(nameof(message)); - string prefix; - if (logType == LogType.Log) - prefix = "LOG"; - else if (logType == LogType.Warning) - prefix = "WRN"; - else if (logType == LogType.Error) - prefix = "ERR"; - else if (logType == LogType.Assert) - prefix = "AST"; - else if (logType == LogType.Exception) - prefix = "EXC"; - else - prefix = "UNK"; - - streamWriter.WriteLine("[{0} {1}] {2}", prefix, DateTime.Now.ToString(DATETIME_FORMAT_STRING), message); - } - - public void Exception(string message, Exception exception) - { - if (!string.IsNullOrEmpty(message)) message += ": "; - message += exception?.ToString() ?? ""; - Log(LogType.Exception, message); + streamWriter.WriteLine(message.ToLogString()); } public void Dispose() diff --git a/ModuleManager/Logging/UnityLogger.cs b/ModuleManager/Logging/UnityLogger.cs index 0655b807..b31c1003 100644 --- a/ModuleManager/Logging/UnityLogger.cs +++ b/ModuleManager/Logging/UnityLogger.cs @@ -1,6 +1,5 @@ using System; using UnityEngine; -using ModuleManager.Extensions; namespace ModuleManager.Logging { @@ -13,12 +12,10 @@ public UnityLogger(ILogger logger) this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public void Log(LogType logType, string message) => logger.Log(logType, message); - - public void Exception(string message, Exception exception) + public void Log(ILogMessage message) { - this.Error(message); - logger.LogException(exception); + if (message == null) throw new ArgumentNullException(nameof(message)); + logger.Log(message.LogType, message.Message); } } } diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index c4913ef7..1a2d77f5 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -47,13 +47,12 @@ - - + diff --git a/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs b/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs index 781cccb6..f6c1fad7 100644 --- a/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs @@ -20,21 +20,70 @@ public IBasicLoggerExtensionsTest() public void TestInfo() { logger.Info("well hi there"); - logger.Received().Log(LogType.Log, "well hi there"); + logger.AssertInfo("well hi there"); } [Fact] public void TestWarning() { logger.Warning("I'm warning you"); - logger.Received().Log(LogType.Warning, "I'm warning you"); + logger.AssertWarning("I'm warning you"); } [Fact] public void TestError() { logger.Error("You have made a grave mistake"); - logger.Received().Log(LogType.Error, "You have made a grave mistake"); + logger.AssertError("You have made a grave mistake"); + } + + [Fact] + public void TestException() + { + Exception ex = new Exception(); + logger.Exception(ex); + logger.AssertException(ex); + } + + [Fact] + public void TestException__Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + logger.Exception(null); + }); + + Assert.Equal("exception", ex.ParamName); + } + + [Fact] + public void TestException__Message() + { + Exception ex = new Exception(); + logger.Exception("a message", ex); + logger.AssertException("a message", ex); + } + + [Fact] + public void TestException__Message__MessageNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + logger.Exception(null, new Exception()); + }); + + Assert.Equal("message", ex.ParamName); + } + + [Fact] + public void TestException__Message__ExceptionNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + logger.Exception("a message", null); + }); + + Assert.Equal("exception", ex.ParamName); } } } diff --git a/ModuleManagerTests/InGameTestRunnerTest.cs b/ModuleManagerTests/InGameTestRunnerTest.cs index cd860bc2..a75897f4 100644 --- a/ModuleManagerTests/InGameTestRunnerTest.cs +++ b/ModuleManagerTests/InGameTestRunnerTest.cs @@ -73,12 +73,12 @@ public void TestRunTestCases__WrongNumberOfNodes() Received.InOrder(delegate { - logger.Log(LogType.Log, "Running tests..."); - logger.Log(LogType.Error, $"Test blah1 failed as expected number of nodes differs expected: 1 found: 2"); - logger.Log(LogType.Log, testNode1.ToString()); - logger.Log(LogType.Log, testNode2.ToString()); - logger.Log(LogType.Log, expectNode.ToString()); - logger.Log(LogType.Log, "tests complete."); + logger.AssertInfo("Running tests..."); + logger.AssertError($"Test blah1 failed as expected number of nodes differs expected: 1 found: 2"); + logger.AssertInfo(testNode1.ToString()); + logger.AssertInfo(testNode2.ToString()); + logger.AssertInfo(expectNode.ToString()); + logger.AssertInfo("tests complete."); }); Assert.Equal(3, file1.configs.Count); @@ -131,11 +131,11 @@ public void TestRunTestCases__AllPassing() Received.InOrder(delegate { - logger.Log(LogType.Log, "Running tests..."); - logger.Log(LogType.Log, "tests complete."); + logger.AssertInfo("Running tests..."); + logger.AssertInfo("tests complete."); }); - logger.DidNotReceive().Log(LogType.Error, Arg.Any()); + logger.AssertNoError(); Assert.Empty(file1.configs); Assert.Empty(file2.configs); @@ -176,9 +176,9 @@ public void TestRunTestCases__Failure() Received.InOrder(delegate { - logger.Log(LogType.Log, "Running tests..."); - logger.Log(LogType.Error, $"Test blah1[0] failed as expected output and actual output differ.\nexpected:\n{expectNode1}\nActually got:\n{testNode1}"); - logger.Log(LogType.Log, "tests complete."); + logger.AssertInfo("Running tests..."); + logger.AssertError($"Test blah1[0] failed as expected output and actual output differ.\nexpected:\n{expectNode1}\nActually got:\n{testNode1}"); + logger.AssertInfo("tests complete."); }); diff --git a/ModuleManagerTests/Logging/ExceptionMessageTest.cs b/ModuleManagerTests/Logging/ExceptionMessageTest.cs deleted file mode 100644 index b072ce89..00000000 --- a/ModuleManagerTests/Logging/ExceptionMessageTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Xunit; -using NSubstitute; -using ModuleManager.Logging; - -namespace ModuleManagerTests.Logging -{ - public class ExceptionMessageTest - { - [Fact] - public void TestLogTo() - { - IBasicLogger logger = Substitute.For(); - - Exception e = new Exception(); - ExceptionMessage message = new ExceptionMessage("An exception was thrown", e); - message.LogTo(logger); - - logger.Received().Exception("An exception was thrown", e); - } - } -} diff --git a/ModuleManagerTests/Logging/LogMessageTest.cs b/ModuleManagerTests/Logging/LogMessageTest.cs new file mode 100644 index 00000000..0a0d42c3 --- /dev/null +++ b/ModuleManagerTests/Logging/LogMessageTest.cs @@ -0,0 +1,70 @@ +using System; +using Xunit; +using UnityEngine; +using ModuleManager.Logging; + +namespace ModuleManagerTests.Logging +{ + public class LogMessageTest + { + [Fact] + public void TestConstructor__NullMessage() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new LogMessage(LogType.Log, null); + }); + + Assert.Equal("message", ex.ParamName); + } + + [Fact] + public void TestToLogMessage__Info() + { + LogMessage message = new LogMessage(LogType.Log, "everything is ok"); + Assert.Matches(@"^\[LOG \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] everything is ok$", message.ToLogString()); + } + + [Fact] + public void TestToLogMessage__Warning() + { + LogMessage message = new LogMessage(LogType.Warning, "I'm warning you"); + Assert.Matches(@"^\[WRN \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] I'm warning you$", message.ToLogString()); + } + + [Fact] + public void TestToLogMessage__Error() + { + LogMessage message = new LogMessage(LogType.Error, "You went too far"); + Assert.Matches(@"^\[ERR \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + } + + [Fact] + public void TestToLogMessage__Exception() + { + LogMessage message = new LogMessage(LogType.Exception, "You went too far"); + Assert.Matches(@"^\[EXC \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + } + + [Fact] + public void TestToLogMessage__Assert() + { + LogMessage message = new LogMessage(LogType.Assert, "You went too far"); + Assert.Matches(@"^\[AST \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + } + + [Fact] + public void TestToLogMessage__Unknown() + { + LogMessage message = new LogMessage((LogType)9999, "You went too far"); + Assert.Matches(@"^\[\?\?\? \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + } + + [Fact] + public void TestToString() + { + LogMessage message = new LogMessage(LogType.Log, "everything is ok"); + Assert.Equal("[ModuleManager.Logging.LogMessage LogType=Log Message=everything is ok]", message.ToString()); + } + } +} diff --git a/ModuleManagerTests/Logging/LogSplitterTest.cs b/ModuleManagerTests/Logging/LogSplitterTest.cs index 2375a3a3..4135f92d 100644 --- a/ModuleManagerTests/Logging/LogSplitterTest.cs +++ b/ModuleManagerTests/Logging/LogSplitterTest.cs @@ -36,21 +36,22 @@ public void TestLog() IBasicLogger logger1 = Substitute.For(); IBasicLogger logger2 = Substitute.For(); LogSplitter logSplitter = new LogSplitter(logger1, logger2); - logSplitter.Log(LogType.Log, "some stuff"); - logger1.Received().Log(LogType.Log, "some stuff"); - logger2.Received().Log(LogType.Log, "some stuff"); + ILogMessage message = Substitute.For(); + logSplitter.Log(message); + logger1.Received().Log(message); + logger2.Received().Log(message); } [Fact] - public void TestException() + public void TestLog__MessageNull() { - IBasicLogger logger1 = Substitute.For(); - IBasicLogger logger2 = Substitute.For(); - LogSplitter logSplitter = new LogSplitter(logger1, logger2); - Exception ex = new Exception(); - logSplitter.Exception("some stuff", ex); - logger1.Received().Exception("some stuff", ex); - logger2.Received().Exception("some stuff", ex); + LogSplitter logSplitter = new LogSplitter(Substitute.For(), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + logSplitter.Log(null); + }); + + Assert.Equal("message", ex.ParamName); } } } diff --git a/ModuleManagerTests/Logging/ModLoggerTest.cs b/ModuleManagerTests/Logging/ModLoggerTest.cs index 7c4bf427..0ca45679 100644 --- a/ModuleManagerTests/Logging/ModLoggerTest.cs +++ b/ModuleManagerTests/Logging/ModLoggerTest.cs @@ -1,7 +1,7 @@ using System; using Xunit; using NSubstitute; -using UnityEngine; +using ModuleManager.Extensions; using ModuleManager.Logging; namespace ModuleManagerTests.Logging @@ -51,36 +51,30 @@ public void TestConstructor__LoggerNull() } [Fact] - public void TestLog__Info() + public void TestLog() { - logger.Log(LogType.Log, "well hi there"); + logger.Info("well hi there"); - innerLogger.Received().Log(LogType.Log, "[MyMod] well hi there"); + innerLogger.AssertInfo("[MyMod] well hi there"); } [Fact] public void TestLog__Warning() { - logger.Log(LogType.Warning, "I'm warning you"); + logger.Warning("I'm warning you"); - innerLogger.Received().Log(LogType.Warning, "[MyMod] I'm warning you"); + innerLogger.AssertWarning("[MyMod] I'm warning you"); } [Fact] - public void TestLog__Error() + public void TestLog__Null() { - logger.Log(LogType.Error, "You have made a grave mistake"); - - innerLogger.Received().Log(LogType.Error, "[MyMod] You have made a grave mistake"); - } + ArgumentNullException ex = Assert.Throws(delegate + { + logger.Log(null); + }); - [Fact] - public void TestException() - { - Exception e = new Exception(); - logger.Exception("An exception was thrown", e); - - innerLogger.Received().Exception("[MyMod] An exception was thrown", e); + Assert.Equal("message", ex.ParamName); } } } diff --git a/ModuleManagerTests/Logging/NormalMessageTest.cs b/ModuleManagerTests/Logging/NormalMessageTest.cs deleted file mode 100644 index c63dd985..00000000 --- a/ModuleManagerTests/Logging/NormalMessageTest.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Xunit; -using NSubstitute; -using UnityEngine; -using ModuleManager.Logging; - -namespace ModuleManagerTests.Logging -{ - public class NormalMessageTest - { - private IBasicLogger logger = Substitute.For(); - - [Fact] - public void TestLogTo__Info() - { - NormalMessage message = new NormalMessage(LogType.Log, "everything is ok"); - message.LogTo(logger); - logger.Received().Log(LogType.Log, "everything is ok"); - } - - [Fact] - public void TestLogTo__Warning() - { - NormalMessage message = new NormalMessage(LogType.Warning, "I'm warning you"); - message.LogTo(logger); - logger.Received().Log(LogType.Warning, "I'm warning you"); - } - - [Fact] - public void TestLogTo__Error() - { - NormalMessage message = new NormalMessage(LogType.Error, "You went too far"); - message.LogTo(logger); - logger.Received().Log(LogType.Error, "You went too far"); - } - } -} diff --git a/ModuleManagerTests/Logging/QueueLogRunnerTest.cs b/ModuleManagerTests/Logging/QueueLogRunnerTest.cs index a52e7add..6a975bd2 100644 --- a/ModuleManagerTests/Logging/QueueLogRunnerTest.cs +++ b/ModuleManagerTests/Logging/QueueLogRunnerTest.cs @@ -73,12 +73,12 @@ public void TestRun() Received.InOrder(delegate { - message1.LogTo(logger); - message2.LogTo(logger); - message3.LogTo(logger); - message4.LogTo(logger); - message5.LogTo(logger); - message6.LogTo(logger); + logger.Log(message1); + logger.Log(message2); + logger.Log(message3); + logger.Log(message4); + logger.Log(message5); + logger.Log(message6); }); } diff --git a/ModuleManagerTests/Logging/QueueLoggerTest.cs b/ModuleManagerTests/Logging/QueueLoggerTest.cs index b984f8c2..dc8a1dfd 100644 --- a/ModuleManagerTests/Logging/QueueLoggerTest.cs +++ b/ModuleManagerTests/Logging/QueueLoggerTest.cs @@ -1,7 +1,6 @@ using System; using Xunit; using NSubstitute; -using UnityEngine; using ModuleManager.Collections; using ModuleManager.Logging; @@ -19,33 +18,34 @@ public QueueLoggerTest() } [Fact] - public void TestLog__Info() + public void TestConstructor__QueueNull() { - logger.Log(LogType.Log, "useful information"); - queue.Received().Add(Arg.Is(m => m.logType == LogType.Log && m.message == "useful information")); - } + ArgumentNullException ex = Assert.Throws(delegate + { + new QueueLogger(null); + }); - [Fact] - public void TestLog__Warning() - { - logger.Log(LogType.Warning, "not to alarm you, but something might be wrong"); - queue.Received().Add(Arg.Is(m => m.logType == LogType.Warning && m.message == "not to alarm you, but something might be wrong")); + Assert.Equal("queue", ex.ParamName); } [Fact] - public void TestLog__Error() + public void TestLog() { - logger.Log(LogType.Error, "you broke everything"); - queue.Received().Add(Arg.Is(m => m.logType == LogType.Error && m.message == "you broke everything")); + ILogMessage message = Substitute.For(); + logger.Log(message); + queue.Received().Add(message); } - [Fact] - public void TestException() + public void TestLog__MessageNull() { - Exception e = new Exception(); - logger.Exception("An exception was thrown", e); - queue.Received().Add(Arg.Is(m => m.message == "An exception was thrown" && m.exception == e)); + ArgumentNullException ex = Assert.Throws(delegate + { + logger.Log(null); + }); + + Assert.Equal("message", ex.ParamName); } + } } diff --git a/ModuleManagerTests/Logging/StreamLoggerTest.cs b/ModuleManagerTests/Logging/StreamLoggerTest.cs index f577adae..61535387 100644 --- a/ModuleManagerTests/Logging/StreamLoggerTest.cs +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -45,7 +45,7 @@ public void TestLog__AlreadyDisposed() InvalidOperationException ex = Assert.Throws(delegate { - streamLogger.Log(LogType.Log, "a message"); + streamLogger.Log(Substitute.For()); }); Assert.Contains("Object has already been disposed", ex.Message); @@ -53,14 +53,16 @@ public void TestLog__AlreadyDisposed() } [Fact] - public void TestLog__Log() + public void TestLog() { - byte[] bytes = new byte[50]; + ILogMessage message = Substitute.For(); + message.ToLogString().Returns("[OMG wtf] bbq"); + byte[] bytes = new byte[15]; using (MemoryStream stream = new MemoryStream(bytes, true)) { using (StreamLogger streamLogger = new StreamLogger(stream)) { - streamLogger.Log(LogType.Log, "a message"); + streamLogger.Log(message); } } @@ -68,150 +70,25 @@ public void TestLog__Log() { using (StreamReader reader = new StreamReader(stream)) { - string result = reader.ReadToEnd(); - Assert.Contains("[LOG ", result); - Assert.Contains("] a message", result); + string result = reader.ReadToEnd().Trim(); + Assert.Equal("[OMG wtf] bbq", result); } } } [Fact] - public void TestLog__Assert() + public void TestLog__MessageNull() { - byte[] bytes = new byte[50]; - using (MemoryStream stream = new MemoryStream(bytes, true)) - { - using (StreamLogger streamLogger = new StreamLogger(stream)) - { - streamLogger.Log(LogType.Assert, "a message"); - } - } - - using (MemoryStream stream = new MemoryStream(bytes, false)) - { - using (StreamReader reader = new StreamReader(stream)) - { - string result = reader.ReadToEnd(); - Assert.Contains("[AST ", result); - Assert.Contains("] a message", result); - } - } - } - - [Fact] - public void TestLog__Warning() - { - byte[] bytes = new byte[50]; - using (MemoryStream stream = new MemoryStream(bytes, true)) - { - using (StreamLogger streamLogger = new StreamLogger(stream)) - { - streamLogger.Log(LogType.Warning, "a message"); - } - } - - using (MemoryStream stream = new MemoryStream(bytes, false)) - { - using (StreamReader reader = new StreamReader(stream)) - { - string result = reader.ReadToEnd(); - Assert.Contains("[WRN ", result); - Assert.Contains("] a message", result); - } - } - } - - [Fact] - public void TestLog__Error() - { - byte[] bytes = new byte[50]; - using (MemoryStream stream = new MemoryStream(bytes, true)) - { - using (StreamLogger streamLogger = new StreamLogger(stream)) - { - streamLogger.Log(LogType.Error, "a message"); - } - } - - using (MemoryStream stream = new MemoryStream(bytes, false)) - { - using (StreamReader reader = new StreamReader(stream)) - { - string result = reader.ReadToEnd(); - Assert.Contains("[ERR ", result); - Assert.Contains("] a message", result); - } - } - } - - [Fact] - public void TestLog__Exception() - { - Exception ex = new Exception("something went wrong"); - byte[] bytes = new byte[100]; - using (MemoryStream stream = new MemoryStream(bytes, true)) - { - using (StreamLogger streamLogger = new StreamLogger(stream)) - { - streamLogger.Exception("a message", ex); - } - } - - using (MemoryStream stream = new MemoryStream(bytes, false)) - { - using (StreamReader reader = new StreamReader(stream)) - { - string result = reader.ReadToEnd(); - Assert.Contains("[EXC ", result); - Assert.Contains("] a message: " + ex.ToString(), result); - } - } - } - - [Fact] - public void TestLog__Exception__NullMessage() - { - Exception ex = new Exception("something went wrong"); - byte[] bytes = new byte[100]; - using (MemoryStream stream = new MemoryStream(bytes, true)) - { - using (StreamLogger streamLogger = new StreamLogger(stream)) - { - streamLogger.Exception(null, ex); - } - } - - using (MemoryStream stream = new MemoryStream(bytes, false)) + using (MemoryStream stream = new MemoryStream(new byte[0], true)) { - using (StreamReader reader = new StreamReader(stream)) - { - string result = reader.ReadToEnd(); - Assert.Contains("[EXC ", result); - Assert.Contains("] " + ex.ToString(), result); - } - } - } + StreamLogger streamLogger = new StreamLogger(stream); - [Fact] - public void TestLog__Unknown() - { - byte[] bytes = new byte[50]; - using (MemoryStream stream = new MemoryStream(bytes, true)) - { - using (StreamLogger streamLogger = new StreamLogger(stream)) + ArgumentNullException ex = Assert.Throws(delegate { - streamLogger.Log((LogType)1000, "a message"); - } - } + streamLogger.Log(null); + }); - using (MemoryStream stream = new MemoryStream(bytes, false)) - { - using (StreamReader reader = new StreamReader(stream)) - { - string result = reader.ReadToEnd(); - Assert.Contains("[UNK ", result); - Assert.Contains("] a message", result); - } + Assert.Equal("message", ex.ParamName); } } } diff --git a/ModuleManagerTests/Logging/UnityLoggerTest.cs b/ModuleManagerTests/Logging/UnityLoggerTest.cs index b2e22d4e..5d820c25 100644 --- a/ModuleManagerTests/Logging/UnityLoggerTest.cs +++ b/ModuleManagerTests/Logging/UnityLoggerTest.cs @@ -2,6 +2,7 @@ using Xunit; using NSubstitute; using UnityEngine; +using ModuleManager.Extensions; using ModuleManager.Logging; namespace ModuleManagerTests.Logging @@ -31,7 +32,7 @@ public void TestConstructor__LoggerNull() [Fact] public void TestLog__Info() { - logger.Log(LogType.Log, "well hi there"); + logger.Info("well hi there"); innerLogger.Received().Log(LogType.Log, "well hi there"); } @@ -39,27 +40,20 @@ public void TestLog__Info() [Fact] public void TestLog__Warning() { - logger.Log(LogType.Warning, "I'm warning you"); + logger.Warning("I'm warning you"); innerLogger.Received().Log(LogType.Warning, "I'm warning you"); } [Fact] - public void TestLog__Error() + public void TestLog__MessageNull() { - logger.Log(LogType.Error, "You have made a grave mistake"); - - innerLogger.Received().Log(LogType.Error, "You have made a grave mistake"); - } - - [Fact] - public void TestException() - { - Exception e = new Exception(); - logger.Exception("An exception was thrown", e); + ArgumentNullException e = Assert.Throws(delegate + { + logger.Log(null); + }); - innerLogger.Received().Log(LogType.Error, "An exception was thrown"); - innerLogger.Received().LogException(e); + Assert.Equal("message", e.ParamName); } } } diff --git a/ModuleManagerTests/LoggingAssertionHelpers.cs b/ModuleManagerTests/LoggingAssertionHelpers.cs new file mode 100644 index 00000000..74ae5cc4 --- /dev/null +++ b/ModuleManagerTests/LoggingAssertionHelpers.cs @@ -0,0 +1,70 @@ +using System; +using UnityEngine; +using NSubstitute; +using ModuleManager.Logging; + +namespace ModuleManagerTests +{ + public static class LoggingAssertionHelpers + { + public static void AssertInfo(this IBasicLogger logger, string message) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.Received().Log(Arg.Is(msg => msg.LogType == LogType.Log && msg.Message == message)); + } + + public static void AssertNoInfo(this IBasicLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.DidNotReceive().Log(Arg.Is(msg => msg.LogType == LogType.Log)); + } + + public static void AssertWarning(this IBasicLogger logger, string message) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.Received().Log(Arg.Is(msg => msg.LogType == LogType.Warning && msg.Message == message)); + } + + public static void AssertNoWarning(this IBasicLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.DidNotReceive().Log(Arg.Is(msg => msg.LogType == LogType.Warning)); + } + + public static void AssertError(this IBasicLogger logger, string message) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.Received().Log(Arg.Is(msg => msg.LogType == LogType.Error && msg.Message == message)); + } + + public static void AssertNoError(this IBasicLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.DidNotReceive().Log(Arg.Is(msg => msg.LogType == LogType.Error)); + } + + public static void AssertException(this IBasicLogger logger, string message, Exception exception) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.Received().Log(Arg.Is(msg => msg.LogType == LogType.Exception && msg.Message == message + ": " + exception.ToString())); + } + + public static void AssertException(this IBasicLogger logger, Exception exception) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.Received().Log(Arg.Is(msg => msg.LogType == LogType.Exception && msg.Message == exception.ToString())); + } + + public static void AssertNoException(this IBasicLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.DidNotReceive().Log(Arg.Is(msg => msg.LogType == LogType.Exception)); + } + + public static void AssertNoLog(this IBasicLogger logger) + { + if (logger == null) throw new ArgumentNullException(nameof(logger)); + logger.DidNotReceiveWithAnyArgs().Log(null); + } + } +} diff --git a/ModuleManagerTests/MMPatchLoaderTest.cs b/ModuleManagerTests/MMPatchLoaderTest.cs index ac687909..29682448 100644 --- a/ModuleManagerTests/MMPatchLoaderTest.cs +++ b/ModuleManagerTests/MMPatchLoaderTest.cs @@ -82,9 +82,9 @@ private void EnsureNoErrors() progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - logger.DidNotReceive().Log(LogType.Warning, Arg.Any()); - logger.DidNotReceive().Log(LogType.Error, Arg.Any()); - logger.DidNotReceive().Log(LogType.Exception, Arg.Any()); + logger.AssertNoWarning(); + logger.AssertNoError(); + logger.AssertNoException(); } } } diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index a550a59a..12379fd6 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -79,14 +79,14 @@ + - - + diff --git a/ModuleManagerTests/PatchApplierTest.cs b/ModuleManagerTests/PatchApplierTest.cs index d7fb0eb1..d54ac286 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -89,9 +89,9 @@ public void TestApplyPatches() progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); - logger.DidNotReceive().Log(LogType.Warning, Arg.Any()); - logger.DidNotReceive().Log(LogType.Error, Arg.Any()); - logger.DidNotReceiveWithAnyArgs().Exception(null, null); + logger.AssertNoWarning(); + logger.AssertNoError(); + logger.AssertNoException(); Received.InOrder(delegate { diff --git a/ModuleManagerTests/Patches/EditPatchTest.cs b/ModuleManagerTests/Patches/EditPatchTest.cs index 50efd3b1..7c76cf48 100644 --- a/ModuleManagerTests/Patches/EditPatchTest.cs +++ b/ModuleManagerTests/Patches/EditPatchTest.cs @@ -236,7 +236,7 @@ public void TestApply__Loop() Received.InOrder(delegate { - logger.Log(LogType.Log, "Looping on ghi/jkl/@NODE to abc/def.cfg/NODE"); + logger.AssertInfo("Looping on ghi/jkl/@NODE to abc/def.cfg/NODE"); progress.ApplyingUpdate(urlConfig, patch.UrlConfig); progress.ApplyingUpdate(modifiedUrlConfigs[1], patch.UrlConfig); progress.ApplyingUpdate(modifiedUrlConfigs[2], patch.UrlConfig); diff --git a/ModuleManagerTests/Progress/PatchProgressTest.cs b/ModuleManagerTests/Progress/PatchProgressTest.cs index ee61fd46..8cde86c2 100644 --- a/ModuleManagerTests/Progress/PatchProgressTest.cs +++ b/ModuleManagerTests/Progress/PatchProgressTest.cs @@ -36,8 +36,8 @@ public void Test__Constructor__Nested() progress2.ApplyingUpdate(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); - logger.DidNotReceiveWithAnyArgs().Log(LogType.Log, null); - logger2.Received().Log(LogType.Log, "Applying update ghi/jkl/@SOME_NODE to abc/def.cfg/SOME_NODE"); + logger.AssertNoLog(); + logger2.AssertInfo("Applying update ghi/jkl/@SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] @@ -62,11 +62,11 @@ public void TestApplyingUpdate() progress.ApplyingUpdate(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying update ghi/jkl/@SOME_NODE to abc/def.cfg/SOME_NODE"); + logger.AssertInfo("Applying update ghi/jkl/@SOME_NODE to abc/def.cfg/SOME_NODE"); progress.ApplyingUpdate(original, patch2); Assert.Equal(2, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying update pqr/stu/@SOME_NODE to abc/def.cfg/SOME_NODE"); + logger.AssertInfo("Applying update pqr/stu/@SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] @@ -81,11 +81,11 @@ public void TesApplyingCopy() progress.ApplyingCopy(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying copy ghi/jkl/+SOME_NODE to abc/def.cfg/SOME_NODE"); + logger.AssertInfo("Applying copy ghi/jkl/+SOME_NODE to abc/def.cfg/SOME_NODE"); progress.ApplyingCopy(original, patch2); Assert.Equal(2, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying copy pqr/stu/+SOME_NODE to abc/def.cfg/SOME_NODE"); + logger.AssertInfo("Applying copy pqr/stu/+SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] @@ -100,11 +100,11 @@ public void TesApplyingDelete() progress.ApplyingDelete(original, patch1); Assert.Equal(1, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying delete ghi/jkl/!SOME_NODE to abc/def.cfg/SOME_NODE"); + logger.AssertInfo("Applying delete ghi/jkl/!SOME_NODE to abc/def.cfg/SOME_NODE"); progress.ApplyingDelete(original, patch2); Assert.Equal(2, progress.Counter.patchedNodes); - logger.Received().Log(LogType.Log, "Applying delete pqr/stu/!SOME_NODE to abc/def.cfg/SOME_NODE"); + logger.AssertInfo("Applying delete pqr/stu/!SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] @@ -131,11 +131,11 @@ public void TestNeedsUnsatisfiedRoot() progress.NeedsUnsatisfiedRoot(config1); Assert.Equal(1, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its NEEDS"); + logger.AssertInfo("Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its NEEDS"); progress.NeedsUnsatisfiedRoot(config2); Assert.Equal(2, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its NEEDS"); + logger.AssertInfo("Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its NEEDS"); } [Fact] @@ -148,11 +148,11 @@ public void TestNeedsUnsatisfiedNode() progress.NeedsUnsatisfiedNode(config1, "SOME/NODE/PATH/SOME_CHILD_NODE"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting node in file abc/def subnode: SOME/NODE/PATH/SOME_CHILD_NODE as it can't satisfy its NEEDS"); + logger.AssertInfo("Deleting node in file abc/def subnode: SOME/NODE/PATH/SOME_CHILD_NODE as it can't satisfy its NEEDS"); progress.NeedsUnsatisfiedNode(config2, "SOME/NODE/PATH/SOME_OTHER_CHILD_NODE"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting node in file ghi/jkl subnode: SOME/NODE/PATH/SOME_OTHER_CHILD_NODE as it can't satisfy its NEEDS"); + logger.AssertInfo("Deleting node in file ghi/jkl subnode: SOME/NODE/PATH/SOME_OTHER_CHILD_NODE as it can't satisfy its NEEDS"); } [Fact] @@ -165,11 +165,11 @@ public void TestNeedsUnsatisfiedValue() progress.NeedsUnsatisfiedValue(config1, "SOME/NODE/PATH/some_value"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting value in file abc/def value: SOME/NODE/PATH/some_value as it can't satisfy its NEEDS"); + logger.AssertInfo("Deleting value in file abc/def value: SOME/NODE/PATH/some_value as it can't satisfy its NEEDS"); progress.NeedsUnsatisfiedValue(config2, "SOME/NODE/PATH/some_other_value"); Assert.Equal(0, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting value in file ghi/jkl value: SOME/NODE/PATH/some_other_value as it can't satisfy its NEEDS"); + logger.AssertInfo("Deleting value in file ghi/jkl value: SOME/NODE/PATH/some_other_value as it can't satisfy its NEEDS"); } [Fact] @@ -182,11 +182,11 @@ public void TestNeedsUnsatisfiedBefore() progress.NeedsUnsatisfiedBefore(config1); Assert.Equal(1, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its BEFORE"); + logger.AssertInfo("Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its BEFORE"); progress.NeedsUnsatisfiedBefore(config2); Assert.Equal(2, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its BEFORE"); + logger.AssertInfo("Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its BEFORE"); } [Fact] @@ -199,11 +199,11 @@ public void TestNeedsUnsatisfiedFor() progress.NeedsUnsatisfiedFor(config1); Assert.Equal(1, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Warning, "Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its FOR (this shouldn't happen)"); + logger.AssertWarning("Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its FOR (this shouldn't happen)"); progress.NeedsUnsatisfiedFor(config2); Assert.Equal(2, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Warning, "Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its FOR (this shouldn't happen)"); + logger.AssertWarning("Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its FOR (this shouldn't happen)"); } [Fact] @@ -216,11 +216,11 @@ public void TestNeedsUnsatisfiedAfter() progress.NeedsUnsatisfiedAfter(config1); Assert.Equal(1, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its AFTER"); + logger.AssertInfo("Deleting root node in file abc/def node: SOME_NODE as it can't satisfy its AFTER"); progress.NeedsUnsatisfiedAfter(config2); Assert.Equal(2, progress.Counter.needsUnsatisfied); - logger.Received().Log(LogType.Log, "Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its AFTER"); + logger.AssertInfo("Deleting root node in file ghi/jkl node: SOME_OTHER_NODE as it can't satisfy its AFTER"); } [Fact] @@ -233,7 +233,7 @@ public void TestStartingPass() progress.PassStarted(pass1); - logger.Received().Log(LogType.Log, ":SOME_PASS pass"); + logger.AssertInfo(":SOME_PASS pass"); onEvent.Received()(pass1); } @@ -250,7 +250,7 @@ public void TestStartingPass__NullArgument() Assert.Equal("pass", ex.ParamName); - logger.DidNotReceiveWithAnyArgs().Log(LogType.Log, null); + logger.AssertNoLog(); onEvent.DidNotReceiveWithAnyArgs()(null); } @@ -265,12 +265,12 @@ public void TestWarning() progress.Warning(config1, "I'm warning you"); Assert.Equal(1, progress.Counter.warnings); Assert.Equal(1, progress.Counter.warningFiles["abc/def.cfg"]); - logger.Received().Log(LogType.Warning, "I'm warning you"); + logger.AssertWarning("I'm warning you"); progress.Warning(config2, "You should probably pay attention to this"); Assert.Equal(2, progress.Counter.warnings); Assert.Equal(2, progress.Counter.warningFiles["abc/def.cfg"]); - logger.Received().Log(LogType.Warning, "You should probably pay attention to this"); + logger.AssertWarning("You should probably pay attention to this"); } [Fact] @@ -297,12 +297,12 @@ public void TestError__Config() progress.Error(config1, "An error message no one is going to read"); Assert.Equal(1, progress.Counter.errors); Assert.Equal(1, progress.Counter.errorFiles["abc/def.cfg"]); - logger.Received().Log(LogType.Error, "An error message no one is going to read"); + logger.AssertError("An error message no one is going to read"); progress.Error(config2, "Maybe someone will read this one"); Assert.Equal(2, progress.Counter.errors); Assert.Equal(2, progress.Counter.errorFiles["abc/def.cfg"]); - logger.Received().Log(LogType.Error, "Maybe someone will read this one"); + logger.AssertError("Maybe someone will read this one"); } [Fact] @@ -315,11 +315,11 @@ public void TestException() progress.Exception("An exception was thrown", e1); Assert.Equal(1, progress.Counter.exceptions); - logger.Received().Exception("An exception was thrown", e1); + logger.AssertException("An exception was thrown", e1); progress.Exception("An exception was tossed", e2); Assert.Equal(2, progress.Counter.exceptions); - logger.Received().Exception("An exception was tossed", e2); + logger.AssertException("An exception was tossed", e2); } [Fact] @@ -336,12 +336,12 @@ public void TestException__Url() progress.Exception(config1, "An exception was thrown", e1); Assert.Equal(1, progress.Counter.exceptions); Assert.Equal(1, progress.Counter.errorFiles["abc/def.cfg"]); - logger.Received().Exception("An exception was thrown", e1); + logger.AssertException("An exception was thrown", e1); progress.Exception(config2, "An exception was tossed", e2); Assert.Equal(2, progress.Counter.exceptions); Assert.Equal(2, progress.Counter.errorFiles["abc/def.cfg"]); - logger.Received().Exception("An exception was tossed", e2); + logger.AssertException("An exception was tossed", e2); } [Fact] From fc74f1fcda46031eb9b3139c7a56830b6728d01d Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Apr 2019 00:32:39 -0700 Subject: [PATCH 080/140] Initialize LogMessage from old LogMessage with new message only uses LogType from old message for now but more coming --- ModuleManager/Logging/LogMessage.cs | 7 ++++ ModuleManager/Logging/ModLogger.cs | 2 +- ModuleManagerTests/Logging/LogMessageTest.cs | 34 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/ModuleManager/Logging/LogMessage.cs b/ModuleManager/Logging/LogMessage.cs index 53f3c0f8..82c5a0fe 100644 --- a/ModuleManager/Logging/LogMessage.cs +++ b/ModuleManager/Logging/LogMessage.cs @@ -16,6 +16,13 @@ public LogMessage(LogType logType, string message) Message = message ?? throw new ArgumentNullException(nameof(message)); } + public LogMessage(ILogMessage logMessage, string newMessage) + { + if (logMessage == null) throw new ArgumentNullException(nameof(logMessage)); + LogType = logMessage.LogType; + Message = newMessage ?? throw new ArgumentNullException(nameof(newMessage)); + } + public string ToLogString() { string prefix; diff --git a/ModuleManager/Logging/ModLogger.cs b/ModuleManager/Logging/ModLogger.cs index 89a05066..6fead909 100644 --- a/ModuleManager/Logging/ModLogger.cs +++ b/ModuleManager/Logging/ModLogger.cs @@ -17,7 +17,7 @@ public ModLogger(string prefix, IBasicLogger logger) public void Log(ILogMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); - logger.Log(new LogMessage(message.LogType, prefix + message.Message)); + logger.Log(new LogMessage(message, prefix + message.Message)); } } } diff --git a/ModuleManagerTests/Logging/LogMessageTest.cs b/ModuleManagerTests/Logging/LogMessageTest.cs index 0a0d42c3..f44c8276 100644 --- a/ModuleManagerTests/Logging/LogMessageTest.cs +++ b/ModuleManagerTests/Logging/LogMessageTest.cs @@ -1,5 +1,6 @@ using System; using Xunit; +using NSubstitute; using UnityEngine; using ModuleManager.Logging; @@ -18,6 +19,39 @@ public void TestConstructor__NullMessage() Assert.Equal("message", ex.ParamName); } + [Fact] + public void TestConstructor__FromOtherMessage() + { + ILogMessage logMessage = Substitute.For(); + logMessage.LogType.Returns(LogType.Log); + logMessage.Message.Returns("the old message"); + LogMessage newLogMessage = new LogMessage(logMessage, "a new message"); + Assert.Equal(LogType.Log, newLogMessage.LogType); + Assert.Equal("a new message", newLogMessage.Message); + } + + [Fact] + public void TestConstructor__FromOtherMessage__LogMessageNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new LogMessage(null, "a new message"); + }); + + Assert.Equal("logMessage", ex.ParamName); + } + + [Fact] + public void TestConstructor__FromOtherMessage__NewMessageNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new LogMessage(Substitute.For(), null); + }); + + Assert.Equal("newMessage", ex.ParamName); + } + [Fact] public void TestToLogMessage__Info() { From 07afe29d79f4260bbc6a3e6f517535ebc6afbba9 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Apr 2019 00:53:04 -0700 Subject: [PATCH 081/140] Don't put date on every log message Instead put it once at the top of the log --- ModuleManager/Logging/LogMessage.cs | 1 + ModuleManager/MMPatchLoader.cs | 1 + ModuleManager/MMPatchRunner.cs | 1 + ModuleManagerTests/Logging/LogMessageTest.cs | 12 ++++++------ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ModuleManager/Logging/LogMessage.cs b/ModuleManager/Logging/LogMessage.cs index 82c5a0fe..86348df3 100644 --- a/ModuleManager/Logging/LogMessage.cs +++ b/ModuleManager/Logging/LogMessage.cs @@ -6,6 +6,7 @@ namespace ModuleManager.Logging public class LogMessage : ILogMessage { private const string DATETIME_FORMAT_STRING = "yyyy-MM-dd HH:mm:ss.fff"; + private const string DATETIME_FORMAT_STRING = "HH:mm:ss.fff"; public LogType LogType { get; } public string Message { get; } diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 4ab1827a..56909ece 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -91,6 +91,7 @@ public IEnumerable Run() { using (StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create))) { + streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); logRunner.Run(streamLogger); streamLogger.Info("Done!"); } diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index 802b76fb..739ecd82 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -40,6 +40,7 @@ public IEnumerator Run() { using (StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create))) { + streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); logRunner.Run(streamLogger); streamLogger.Info("Done!"); } diff --git a/ModuleManagerTests/Logging/LogMessageTest.cs b/ModuleManagerTests/Logging/LogMessageTest.cs index f44c8276..6e7d78ff 100644 --- a/ModuleManagerTests/Logging/LogMessageTest.cs +++ b/ModuleManagerTests/Logging/LogMessageTest.cs @@ -56,42 +56,42 @@ public void TestConstructor__FromOtherMessage__NewMessageNull() public void TestToLogMessage__Info() { LogMessage message = new LogMessage(LogType.Log, "everything is ok"); - Assert.Matches(@"^\[LOG \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] everything is ok$", message.ToLogString()); + Assert.Matches(@"^\[LOG \d\d:\d\d:\d\d.\d\d\d\] everything is ok$", message.ToLogString()); } [Fact] public void TestToLogMessage__Warning() { LogMessage message = new LogMessage(LogType.Warning, "I'm warning you"); - Assert.Matches(@"^\[WRN \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] I'm warning you$", message.ToLogString()); + Assert.Matches(@"^\[WRN \d\d:\d\d:\d\d.\d\d\d\] I'm warning you$", message.ToLogString()); } [Fact] public void TestToLogMessage__Error() { LogMessage message = new LogMessage(LogType.Error, "You went too far"); - Assert.Matches(@"^\[ERR \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + Assert.Matches(@"^\[ERR \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); } [Fact] public void TestToLogMessage__Exception() { LogMessage message = new LogMessage(LogType.Exception, "You went too far"); - Assert.Matches(@"^\[EXC \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + Assert.Matches(@"^\[EXC \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); } [Fact] public void TestToLogMessage__Assert() { LogMessage message = new LogMessage(LogType.Assert, "You went too far"); - Assert.Matches(@"^\[AST \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + Assert.Matches(@"^\[AST \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); } [Fact] public void TestToLogMessage__Unknown() { LogMessage message = new LogMessage((LogType)9999, "You went too far"); - Assert.Matches(@"^\[\?\?\? \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); + Assert.Matches(@"^\[\?\?\? \d\d:\d\d:\d\d.\d\d\d\] You went too far$", message.ToLogString()); } [Fact] From 0a2c842ae3b141416b4a176f0fdb5fc1df438760 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Apr 2019 01:03:16 -0700 Subject: [PATCH 082/140] Initialize timestamp with log message So if it takes some time to actually get to the log it will display the originating timestamp rather than the timestamp it was logged at --- ModuleManager/Logging/ILogMessage.cs | 1 + ModuleManager/Logging/LogMessage.cs | 6 ++++-- ModuleManagerTests/Logging/LogMessageTest.cs | 22 ++++++++++++++++++++ ModuleManagerTests/Logging/ModLoggerTest.cs | 21 ++++++++++--------- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/ModuleManager/Logging/ILogMessage.cs b/ModuleManager/Logging/ILogMessage.cs index 55137cc7..bf85f488 100644 --- a/ModuleManager/Logging/ILogMessage.cs +++ b/ModuleManager/Logging/ILogMessage.cs @@ -6,6 +6,7 @@ namespace ModuleManager.Logging public interface ILogMessage { LogType LogType { get; } + DateTime Timestamp { get; } string Message { get; } string ToLogString(); } diff --git a/ModuleManager/Logging/LogMessage.cs b/ModuleManager/Logging/LogMessage.cs index 86348df3..c6554aaf 100644 --- a/ModuleManager/Logging/LogMessage.cs +++ b/ModuleManager/Logging/LogMessage.cs @@ -5,15 +5,16 @@ namespace ModuleManager.Logging { public class LogMessage : ILogMessage { - private const string DATETIME_FORMAT_STRING = "yyyy-MM-dd HH:mm:ss.fff"; private const string DATETIME_FORMAT_STRING = "HH:mm:ss.fff"; public LogType LogType { get; } + public DateTime Timestamp { get; } public string Message { get; } public LogMessage(LogType logType, string message) { LogType = logType; + Timestamp = DateTime.Now; Message = message ?? throw new ArgumentNullException(nameof(message)); } @@ -21,6 +22,7 @@ public LogMessage(ILogMessage logMessage, string newMessage) { if (logMessage == null) throw new ArgumentNullException(nameof(logMessage)); LogType = logMessage.LogType; + Timestamp = logMessage.Timestamp; Message = newMessage ?? throw new ArgumentNullException(nameof(newMessage)); } @@ -40,7 +42,7 @@ public string ToLogString() else prefix = "???"; - return $"[{prefix} {DateTime.Now.ToString(DATETIME_FORMAT_STRING)}] {Message}"; + return $"[{prefix} {Timestamp.ToString(DATETIME_FORMAT_STRING)}] {Message}"; } public override string ToString() diff --git a/ModuleManagerTests/Logging/LogMessageTest.cs b/ModuleManagerTests/Logging/LogMessageTest.cs index 6e7d78ff..d70fe8db 100644 --- a/ModuleManagerTests/Logging/LogMessageTest.cs +++ b/ModuleManagerTests/Logging/LogMessageTest.cs @@ -8,6 +8,16 @@ namespace ModuleManagerTests.Logging { public class LogMessageTest { + [Fact] + public void TestConstructor() + { + LogMessage logMessage = new LogMessage(LogType.Log, "a message"); + Assert.Equal(LogType.Log, logMessage.LogType); + Assert.True(logMessage.Timestamp <= DateTime.Now); + Assert.True(logMessage.Timestamp > DateTime.Now - new TimeSpan(0, 0, 5)); + Assert.Equal("a message", logMessage.Message); + } + [Fact] public void TestConstructor__NullMessage() { @@ -25,8 +35,10 @@ public void TestConstructor__FromOtherMessage() ILogMessage logMessage = Substitute.For(); logMessage.LogType.Returns(LogType.Log); logMessage.Message.Returns("the old message"); + logMessage.Timestamp.Returns(new DateTime(2000, 1, 1, 12, 34, 45, 678)); LogMessage newLogMessage = new LogMessage(logMessage, "a new message"); Assert.Equal(LogType.Log, newLogMessage.LogType); + Assert.Equal(logMessage.Timestamp, newLogMessage.Timestamp); Assert.Equal("a new message", newLogMessage.Message); } @@ -100,5 +112,15 @@ public void TestToString() LogMessage message = new LogMessage(LogType.Log, "everything is ok"); Assert.Equal("[ModuleManager.Logging.LogMessage LogType=Log Message=everything is ok]", message.ToString()); } + + [Fact] + public void TestToLogMessage__Timestamp() + { + ILogMessage logMessage = Substitute.For(); + logMessage.LogType.Returns(LogType.Log); + logMessage.Timestamp.Returns(new DateTime(2000, 1, 1, 12, 34, 56, 789)); + LogMessage message = new LogMessage(logMessage, "everything is ok"); + Assert.Equal("[LOG 12:34:56.789] everything is ok", message.ToLogString()); + } } } diff --git a/ModuleManagerTests/Logging/ModLoggerTest.cs b/ModuleManagerTests/Logging/ModLoggerTest.cs index 0ca45679..fa15d6be 100644 --- a/ModuleManagerTests/Logging/ModLoggerTest.cs +++ b/ModuleManagerTests/Logging/ModLoggerTest.cs @@ -1,7 +1,7 @@ using System; using Xunit; using NSubstitute; -using ModuleManager.Extensions; +using UnityEngine; using ModuleManager.Logging; namespace ModuleManagerTests.Logging @@ -53,17 +53,18 @@ public void TestConstructor__LoggerNull() [Fact] public void TestLog() { - logger.Info("well hi there"); + ILogMessage logMessage = Substitute.For(); + logMessage.LogType.Returns(LogType.Log); + logMessage.Message.Returns("well hi there"); + logMessage.Timestamp.Returns(new DateTime(2000, 1, 1, 12, 34, 45, 678)); - innerLogger.AssertInfo("[MyMod] well hi there"); - } - - [Fact] - public void TestLog__Warning() - { - logger.Warning("I'm warning you"); + logger.Log(logMessage); - innerLogger.AssertWarning("[MyMod] I'm warning you"); + innerLogger.Received().Log(Arg.Is(msg => + msg.LogType == LogType.Log && + msg.Timestamp == logMessage.Timestamp && + msg.Message == "[MyMod] well hi there" + )); } [Fact] From a8bcad2adbb10413dd8b3175a0b61ef44390533c Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 30 Apr 2019 22:00:25 -0700 Subject: [PATCH 083/140] ModLogger -> PrefixLogger Explains what it does better --- .../Logging/{ModLogger.cs => PrefixLogger.cs} | 4 ++-- ModuleManager/ModuleManager.cs | 4 ++-- ModuleManager/ModuleManager.csproj | 2 +- ModuleManager/PostPatchLoader.cs | 2 +- .../{ModLoggerTest.cs => PrefixLoggerTest.cs} | 14 +++++++------- ModuleManagerTests/ModuleManagerTests.csproj | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) rename ModuleManager/Logging/{ModLogger.cs => PrefixLogger.cs} (85%) rename ModuleManagerTests/Logging/{ModLoggerTest.cs => PrefixLoggerTest.cs} (85%) diff --git a/ModuleManager/Logging/ModLogger.cs b/ModuleManager/Logging/PrefixLogger.cs similarity index 85% rename from ModuleManager/Logging/ModLogger.cs rename to ModuleManager/Logging/PrefixLogger.cs index 6fead909..410473fa 100644 --- a/ModuleManager/Logging/ModLogger.cs +++ b/ModuleManager/Logging/PrefixLogger.cs @@ -2,12 +2,12 @@ namespace ModuleManager.Logging { - public class ModLogger : IBasicLogger + public class PrefixLogger : IBasicLogger { private readonly string prefix; private readonly IBasicLogger logger; - public ModLogger(string prefix, IBasicLogger logger) + public PrefixLogger(string prefix, IBasicLogger logger) { if (string.IsNullOrEmpty(prefix)) throw new ArgumentNullException(nameof(prefix)); this.prefix = $"[{prefix}] "; diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 4d7e76de..50072d91 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -132,7 +132,7 @@ internal void Awake() Log(string.Format("Adding post patch to the loading screen {0}", list.Count)); list.Insert(gameDatabaseIndex + 1, aGameObject.AddComponent()); - patchRunner = new MMPatchRunner(new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); + patchRunner = new MMPatchRunner(new PrefixLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); StartCoroutine(patchRunner.Run()); // Workaround for 1.6.0 Editor bug after a PartDatabase rebuild. @@ -314,7 +314,7 @@ private IEnumerator DataBaseReloadWithMM(bool dump = false) QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; - patchRunner = new MMPatchRunner(new ModLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); + patchRunner = new MMPatchRunner(new PrefixLogger("ModuleManager", new UnityLogger(Debug.unityLogger))); float totalLoadWeight = GameDatabase.Instance.LoadWeight() + PartLoader.Instance.LoadWeight(); bool startedReload = false; diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 1a2d77f5..6739e50d 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -51,7 +51,7 @@ - + diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index 758ab0ae..f92f1fad 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -22,7 +22,7 @@ public class PostPatchLoader : LoadingSystem private static readonly List postPatchCallbacks = new List(); - private readonly IBasicLogger logger = new ModLogger("ModuleManager", new UnityLogger(UnityEngine.Debug.unityLogger)); + private readonly IBasicLogger logger = new PrefixLogger("ModuleManager", new UnityLogger(UnityEngine.Debug.unityLogger)); private bool ready = false; diff --git a/ModuleManagerTests/Logging/ModLoggerTest.cs b/ModuleManagerTests/Logging/PrefixLoggerTest.cs similarity index 85% rename from ModuleManagerTests/Logging/ModLoggerTest.cs rename to ModuleManagerTests/Logging/PrefixLoggerTest.cs index fa15d6be..28a82edc 100644 --- a/ModuleManagerTests/Logging/ModLoggerTest.cs +++ b/ModuleManagerTests/Logging/PrefixLoggerTest.cs @@ -6,15 +6,15 @@ namespace ModuleManagerTests.Logging { - public class ModLoggerTest + public class PrefixLoggerTest { private IBasicLogger innerLogger; - private ModLogger logger; + private PrefixLogger logger; - public ModLoggerTest() + public PrefixLoggerTest() { innerLogger = Substitute.For(); - logger = new ModLogger("MyMod", innerLogger); + logger = new PrefixLogger("MyMod", innerLogger); } [Fact] @@ -22,7 +22,7 @@ public void TestConstructor__PrefixNull() { ArgumentNullException e = Assert.Throws(delegate { - new ModLogger(null, innerLogger); + new PrefixLogger(null, innerLogger); }); Assert.Equal("prefix", e.ParamName); @@ -33,7 +33,7 @@ public void TestConstructor__PrefixBlank() { ArgumentNullException e = Assert.Throws(delegate { - new ModLogger("", innerLogger); + new PrefixLogger("", innerLogger); }); Assert.Equal("prefix", e.ParamName); @@ -44,7 +44,7 @@ public void TestConstructor__LoggerNull() { ArgumentNullException e = Assert.Throws(delegate { - new ModLogger("blah", null); + new PrefixLogger("blah", null); }); Assert.Equal("logger", e.ParamName); diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 12379fd6..17d1b48d 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -116,7 +116,7 @@ - + From 5711994c101b86891f9403a5c08c47492665810d Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 5 May 2019 13:55:42 -0700 Subject: [PATCH 084/140] Unnecessary using --- ModuleManager/Progress/PatchProgress.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index b1d92721..a1d53ed3 100644 --- a/ModuleManager/Progress/PatchProgress.cs +++ b/ModuleManager/Progress/PatchProgress.cs @@ -1,7 +1,6 @@ using System; using ModuleManager.Extensions; using ModuleManager.Logging; -using NodeStack = ModuleManager.Collections.ImmutableStack; namespace ModuleManager.Progress { From 3650d6927de303a7be7b27fdf4d0a144ee496737 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 5 May 2019 13:58:08 -0700 Subject: [PATCH 085/140] Use better assertion --- ModuleManagerTests/Patches/PatchCompilerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManagerTests/Patches/PatchCompilerTest.cs b/ModuleManagerTests/Patches/PatchCompilerTest.cs index 5ad3c794..bb786dc5 100644 --- a/ModuleManagerTests/Patches/PatchCompilerTest.cs +++ b/ModuleManagerTests/Patches/PatchCompilerTest.cs @@ -44,7 +44,7 @@ public void TestCompilePatch__Insert() patch.Apply(configs, progress, logger); - Assert.Equal(1, configs.Count); + Assert.Single(configs); Assert.NotSame(protoPatch.urlConfig.config, configs.First.Value.Node); AssertNodesEqual(new TestConfigNode("NODE") { From 5f7071d88369e69beb0e2fc5a74d1a8d3305c159 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 5 May 2019 14:06:28 -0700 Subject: [PATCH 086/140] Mark disposables as sealed Microsoft seems to recommend this --- ModuleManager/Collections/MessageQueue.cs | 2 +- ModuleManager/Logging/StreamLogger.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ModuleManager/Collections/MessageQueue.cs b/ModuleManager/Collections/MessageQueue.cs index 321a3503..b6038a77 100644 --- a/ModuleManager/Collections/MessageQueue.cs +++ b/ModuleManager/Collections/MessageQueue.cs @@ -12,7 +12,7 @@ public interface IMessageQueue : IEnumerable public class MessageQueue : IMessageQueue, IEnumerable { - public class Enumerator : IEnumerator + public sealed class Enumerator : IEnumerator { private readonly MessageQueue queue; private Node current; diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs index bfbb1053..74115d3e 100644 --- a/ModuleManager/Logging/StreamLogger.cs +++ b/ModuleManager/Logging/StreamLogger.cs @@ -3,7 +3,7 @@ namespace ModuleManager.Logging { - public class StreamLogger : IBasicLogger, IDisposable + public sealed class StreamLogger : IBasicLogger, IDisposable { private readonly Stream stream; private readonly StreamWriter streamWriter; From 28bd925fbe75663c34b67d0bad6c012e01135602 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 5 May 2019 14:06:37 -0700 Subject: [PATCH 087/140] Another unnecessary using --- ModuleManager/ModListGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ModuleManager/ModListGenerator.cs b/ModuleManager/ModListGenerator.cs index 97292ba0..5d017232 100644 --- a/ModuleManager/ModListGenerator.cs +++ b/ModuleManager/ModListGenerator.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Collections.Generic; using System.Linq; using System.Text; From 5e5314e2d73e4ec237684793ec8975c2a2bc84ca Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 5 May 2019 22:29:35 -0700 Subject: [PATCH 088/140] Use custom code to lookup file by url Should be less gc-happy and potentially faster, though this step rarely takes any signicant amount of time. Allows extensions to be specified optionally. Doesn't support /Parts or /Internals but probably a bunch of assumptions in ModuleManager about that too. --- ModuleManager/Extensions/UrlDirExtensions.cs | 43 +++++++++ ModuleManager/MMPatchLoader.cs | 2 +- ModuleManager/ModuleManager.csproj | 1 + .../Extensions/UrlDirExtensionsTest.cs | 88 +++++++++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 5 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 ModuleManager/Extensions/UrlDirExtensions.cs create mode 100644 ModuleManagerTests/Extensions/UrlDirExtensionsTest.cs diff --git a/ModuleManager/Extensions/UrlDirExtensions.cs b/ModuleManager/Extensions/UrlDirExtensions.cs new file mode 100644 index 00000000..3a57bf6a --- /dev/null +++ b/ModuleManager/Extensions/UrlDirExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; + +namespace ModuleManager.Extensions +{ + public static class UrlDirExtensions + { + public static UrlDir.UrlFile Find(this UrlDir urlDir, string url) + { + if (urlDir == null) throw new ArgumentNullException(nameof(urlDir)); + if (url == null) throw new ArgumentNullException(nameof(url)); + string[] splits = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + UrlDir currentDir = urlDir; + + for (int i = 0; i < splits.Length - 1; i++) + { + currentDir = currentDir.children.FirstOrDefault(subDir => subDir.name == splits[i]); + if (currentDir == null) return null; + } + + string fileName = splits[splits.Length - 1]; + string fileExtension = null; + + int idx = fileName.LastIndexOf('.'); + + if (idx > -1) + { + fileExtension = fileName.Substring(idx + 1); + fileName = fileName.Substring(0, idx); + } + + foreach (UrlDir.UrlFile file in currentDir.files) + { + if (file.name != fileName) continue; + if (fileExtension != null && fileExtension != file.fileExtension) continue; + return file; + } + + return null; + } + } +} diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 56909ece..fffd5e3a 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -524,7 +524,7 @@ private IEnumerable LoadCache() { string parentUrl = node.GetValue("parentUrl"); - UrlDir.UrlFile parent = GameDatabase.Instance.root.AllConfigFiles.FirstOrDefault(f => f.url == parentUrl); + UrlDir.UrlFile parent = gameDataDir.Find(parentUrl); if (parent != null) { databaseConfigs.Add(new ProtoUrlConfig(parent, node.nodes[0])); diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 6739e50d..c69f36b6 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -31,6 +31,7 @@ False + diff --git a/ModuleManagerTests/Extensions/UrlDirExtensionsTest.cs b/ModuleManagerTests/Extensions/UrlDirExtensionsTest.cs new file mode 100644 index 00000000..d7ff2316 --- /dev/null +++ b/ModuleManagerTests/Extensions/UrlDirExtensionsTest.cs @@ -0,0 +1,88 @@ +using System; +using Xunit; +using TestUtils; +using ModuleManager.Extensions; + +namespace ModuleManagerTests.Extensions +{ + public class UrlDirExtensionsTest + { + + [Fact] + public void TestFind__IndirectChild() + { + UrlDir urlDir = UrlBuilder.CreateDir("abc"); + UrlDir.UrlFile urlFile = UrlBuilder.CreateFile("def/ghi.cfg", urlDir); + + Assert.Equal(urlFile, urlDir.Find("def/ghi")); + } + + [Fact] + public void TestFind__DirectChild() + { + UrlDir urlDir = UrlBuilder.CreateDir("abc"); + UrlDir.UrlFile urlFile = UrlBuilder.CreateFile("def.cfg", urlDir); + + Assert.Equal(urlFile, urlDir.Find("def")); + } + + [Fact] + public void TestFind__Extension() + { + UrlDir urlDir = UrlBuilder.CreateDir("abc"); + UrlBuilder.CreateFile("def/ghi.yyy", urlDir); + UrlDir.UrlFile urlFile = UrlBuilder.CreateFile("def/ghi.cfg", urlDir); + UrlBuilder.CreateFile("def/ghi.zzz", urlDir); + + Assert.Equal(urlFile, urlDir.Find("def/ghi.cfg")); + } + + [Fact] + public void TestFind__NotFound() + { + UrlDir urlDir = UrlBuilder.CreateDir("abc"); + UrlBuilder.CreateDir("def", urlDir); + + Assert.Null(urlDir.Find("def/ghi")); + } + + [Fact] + public void TestFind__Extension__NotFound() + { + UrlDir urlDir = UrlBuilder.CreateDir("abc"); + UrlBuilder.CreateFile("def/ghi.yyy", urlDir); + UrlBuilder.CreateFile("def/ghi.zzz", urlDir); + + Assert.Null(urlDir.Find("def/ghi.cfg")); + } + + [Fact] + public void TestFind__IntermediateDirectoryNotFound() + { + UrlDir urlDir = UrlBuilder.CreateDir("abc"); + Assert.Null(urlDir.Find("def/ghi")); + } + + [Fact] + public void TestFind__UrlDirNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + UrlDirExtensions.Find(null, "abc"); + }); + + Assert.Equal("urlDir", ex.ParamName); + } + + [Fact] + public void TestFind__UrlNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + UrlDirExtensions.Find(UrlBuilder.CreateDir("abc"), null); + }); + + Assert.Equal("url", ex.ParamName); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 17d1b48d..7e97430e 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -78,6 +78,7 @@ + From ae0d7a7c50df234e141909eedb2ceb6c5de6f3d2 Mon Sep 17 00:00:00 2001 From: blowfish Date: Sun, 5 May 2019 22:48:22 -0700 Subject: [PATCH 089/140] Put extension on cache URLs Makes trailing spaces not break things Resolves #121 --- ModuleManager/Extensions/UrlFileExtensions.cs | 12 +++++++ ModuleManager/MMPatchLoader.cs | 32 +++++++++++-------- ModuleManager/ModuleManager.cs | 5 +-- ModuleManager/ModuleManager.csproj | 1 + ModuleManager/Progress/PatchProgress.cs | 4 +-- .../Extensions/UrlFileExtensionsTest.cs | 17 ++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 7 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 ModuleManager/Extensions/UrlFileExtensions.cs create mode 100644 ModuleManagerTests/Extensions/UrlFileExtensionsTest.cs diff --git a/ModuleManager/Extensions/UrlFileExtensions.cs b/ModuleManager/Extensions/UrlFileExtensions.cs new file mode 100644 index 00000000..6fe1cbe9 --- /dev/null +++ b/ModuleManager/Extensions/UrlFileExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace ModuleManager.Extensions +{ + public static class UrlFileExtensions + { + public static string GetUrlWithExtension(this UrlDir.UrlFile urlFile) + { + return $"{urlFile.url}.{urlFile.fileExtension}"; + } + } +} diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index fffd5e3a..b7e28179 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -292,8 +292,9 @@ private bool IsCacheUpToDate() for (int i = 0; i < files.Length; i++) { + string url = files[i].GetUrlWithExtension(); // Hash the file path so the checksum change if files are moved - byte[] pathBytes = Encoding.UTF8.GetBytes(files[i].url); + byte[] pathBytes = Encoding.UTF8.GetBytes(url); sha.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0); // hash the file content @@ -301,13 +302,13 @@ private bool IsCacheUpToDate() sha.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0); filesha.ComputeHash(contentBytes); - if (!filesSha.ContainsKey(files[i].url)) + if (!filesSha.ContainsKey(url)) { - filesSha.Add(files[i].url, BitConverter.ToString(filesha.Hash)); + filesSha.Add(url, BitConverter.ToString(filesha.Hash)); } else { - logger.Warning("Duplicate fileSha key. This should not append. The key is " + files[i].url); + logger.Warning("Duplicate fileSha key. This should not append. The key is " + url); } } @@ -370,13 +371,14 @@ private bool CheckFilesChange(UrlDir.UrlFile[] files, ConfigNode shaConfigNode) for (int i = 0; i < files.Length; i++) { - ConfigNode fileNode = GetFileNode(shaConfigNode, files[i].url); + string url = files[i].GetUrlWithExtension(); + ConfigNode fileNode = GetFileNode(shaConfigNode, url); string fileSha = fileNode?.GetValue("SHA"); if (fileNode == null) continue; - if (fileSha == null || filesSha[files[i].url] != fileSha) + if (fileSha == null || filesSha[url] != fileSha) { changes.Append("Changed : " + fileNode.GetValue("filename") + ".cfg\n"); noChange = false; @@ -384,18 +386,19 @@ private bool CheckFilesChange(UrlDir.UrlFile[] files, ConfigNode shaConfigNode) } for (int i = 0; i < files.Length; i++) { - ConfigNode fileNode = GetFileNode(shaConfigNode, files[i].url); + string url = files[i].GetUrlWithExtension(); + ConfigNode fileNode = GetFileNode(shaConfigNode, url); if (fileNode == null) { - changes.Append("Added : " + files[i].url + ".cfg\n"); + changes.Append("Added : " + url + "\n"); noChange = false; } shaConfigNode.RemoveNode(fileNode); } foreach (ConfigNode fileNode in shaConfigNode.GetNodes()) { - changes.Append("Deleted : " + fileNode.GetValue("filename") + ".cfg\n"); + changes.Append("Deleted : " + fileNode.GetValue("filename") + "\n"); noChange = false; } if (!noChange) @@ -430,19 +433,20 @@ private void CreateCache(IEnumerable databaseConfigs, int patch foreach (IProtoUrlConfig urlConfig in databaseConfigs) { ConfigNode node = cache.AddNode("UrlConfig"); - node.AddValue("parentUrl", urlConfig.UrlFile.url); + node.AddValue("parentUrl", urlConfig.UrlFile.GetUrlWithExtension()); node.AddNode(urlConfig.Node); } foreach (var file in GameDatabase.Instance.root.AllConfigFiles) { + string url = file.GetUrlWithExtension(); // "/Physics" is the node we created manually to loads the PHYSIC config - if (file.url != "/Physics" && filesSha.ContainsKey(file.url)) + if (file.url != "/Physics" && filesSha.ContainsKey(url)) { ConfigNode shaNode = filesSHANode.AddNode("FILE"); - shaNode.AddValue("filename", file.url); - shaNode.AddValue("SHA", filesSha[file.url]); - filesSha.Remove(file.url); + shaNode.AddValue("filename", url); + shaNode.AddValue("SHA", filesSha[url]); + filesSha.Remove(url); } } diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 50072d91..75b4f6ec 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -9,6 +9,7 @@ using UnityEngine; using Debug = UnityEngine.Debug; using ModuleManager.Cats; +using ModuleManager.Extensions; using ModuleManager.Logging; namespace ModuleManager @@ -472,8 +473,8 @@ public static void OutputAllConfigs() Directory.CreateDirectory(dirPath); } - Log("Exporting " + currentPath + urlFile.name + "." + urlFile.fileExtension); - string filePath = dirPath + urlFile.name + "." + urlFile.fileExtension; + Log("Exporting " + currentPath + urlFile.GetUrlWithExtension()); + string filePath = dirPath + urlFile.GetUrlWithExtension(); foreach (UrlDir.UrlConfig urlConfig in urlFile.configs) { try diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index c69f36b6..3c4b0366 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -32,6 +32,7 @@ + diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index a1d53ed3..3cebf994 100644 --- a/ModuleManager/Progress/PatchProgress.cs +++ b/ModuleManager/Progress/PatchProgress.cs @@ -139,7 +139,7 @@ public void Exception(UrlDir.UrlConfig url, string message, Exception exception) private void RecordWarningFile(UrlDir.UrlConfig url) { - string key = url.parent.url + "." + url.parent.fileExtension; + string key = url.parent.GetUrlWithExtension(); if (key[0] == '/') key = key.Substring(1); @@ -151,7 +151,7 @@ private void RecordWarningFile(UrlDir.UrlConfig url) private void RecordErrorFile(UrlDir.UrlConfig url) { - string key = url.parent.url + "." + url.parent.fileExtension; + string key = url.parent.GetUrlWithExtension(); if (key[0] == '/') key = key.Substring(1); diff --git a/ModuleManagerTests/Extensions/UrlFileExtensionsTest.cs b/ModuleManagerTests/Extensions/UrlFileExtensionsTest.cs new file mode 100644 index 00000000..147e603e --- /dev/null +++ b/ModuleManagerTests/Extensions/UrlFileExtensionsTest.cs @@ -0,0 +1,17 @@ +using System; +using Xunit; +using TestUtils; +using ModuleManager.Extensions; + +namespace ModuleManagerTests.Extensions +{ + public static class UrlFileExtensionsTest + { + [Fact] + public static void TestGetUrlWithExtension() + { + UrlDir.UrlFile urlFile = UrlBuilder.CreateFile("abc/def/ghi.cfg"); + Assert.Equal("abc/def/ghi.cfg", urlFile.GetUrlWithExtension()); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 7e97430e..37a2edaf 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -79,6 +79,7 @@ + From 69ab085da68e4ed3c80f444f0a04bfa65e6171f5 Mon Sep 17 00:00:00 2001 From: blowfish Date: Mon, 27 May 2019 14:35:04 -0700 Subject: [PATCH 090/140] Alphabetize .csproj files --- ModuleManager/ModuleManager.csproj | 22 +++++++-------- ModuleManagerTests/ModuleManagerTests.csproj | 28 ++++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 3c4b0366..ac7665b4 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -31,9 +31,6 @@ False - - - @@ -41,6 +38,7 @@ + @@ -48,22 +46,24 @@ + + - + + + - - + - @@ -73,7 +73,11 @@ + + + + @@ -83,10 +87,6 @@ - - - - diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 37a2edaf..49e52d41 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -75,7 +75,15 @@ + + + + + + + + @@ -84,25 +92,25 @@ - - - + + + + + + - - - @@ -111,14 +119,6 @@ - - - - - - - - From e1bed9419ca20a15e04ba24e8bfe7e69563259e4 Mon Sep 17 00:00:00 2001 From: blowfish Date: Tue, 28 May 2019 21:58:27 -0700 Subject: [PATCH 091/140] improve file sha generator * Actually dispose the sha when we're done * Use a better way of converting bytes to hex strings (hopefully less gc) * Test byte array to hex string conversion --- .../Extensions/ByteArrayExtensions.cs | 29 +++++++++++++++++++ ModuleManager/ModuleManager.csproj | 1 + ModuleManager/Utils/FileUtils.cs | 19 +++++------- .../Extensions/ByteArrayExtensionsTest.cs | 28 ++++++++++++++++++ ModuleManagerTests/ModuleManagerTests.csproj | 1 + 5 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 ModuleManager/Extensions/ByteArrayExtensions.cs create mode 100644 ModuleManagerTests/Extensions/ByteArrayExtensionsTest.cs diff --git a/ModuleManager/Extensions/ByteArrayExtensions.cs b/ModuleManager/Extensions/ByteArrayExtensions.cs new file mode 100644 index 00000000..520393df --- /dev/null +++ b/ModuleManager/Extensions/ByteArrayExtensions.cs @@ -0,0 +1,29 @@ +using System; + +namespace ModuleManager.Extensions +{ + public static class ByteArrayExtensions + { + public static string ToHex(this byte[] data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + char[] result = new char[data.Length * 2]; + + for (int i = 0; i < data.Length; i++) + { + result[i * 2] = GetHexValue(data[i] / 16); + result[i * 2 + 1] = GetHexValue(data[i] % 16); + } + + return new string(result); + } + + private static char GetHexValue(int i) + { + if (i < 10) + return (char)(i + '0'); + else + return (char)(i - 10 + 'a'); + } + } +} diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index ac7665b4..04a043ae 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -41,6 +41,7 @@ + diff --git a/ModuleManager/Utils/FileUtils.cs b/ModuleManager/Utils/FileUtils.cs index 73cd8dba..e069e14c 100644 --- a/ModuleManager/Utils/FileUtils.cs +++ b/ModuleManager/Utils/FileUtils.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Security.Cryptography; +using ModuleManager.Extensions; namespace ModuleManager.Utils { @@ -9,22 +11,17 @@ public static string FileSHA(string filename) { if (!File.Exists(filename)) throw new FileNotFoundException("File does not exist", filename); - System.Security.Cryptography.SHA256 sha = System.Security.Cryptography.SHA256.Create(); - byte[] data = null; - using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read)) - { - data = sha.ComputeHash(fs); - } - - string hashedValue = string.Empty; - foreach (byte b in data) + using (SHA256 sha = SHA256.Create()) { - hashedValue += String.Format("{0,2:x2}", b); + using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read)) + { + data = sha.ComputeHash(fs); + } } - return hashedValue; + return data.ToHex(); } } } diff --git a/ModuleManagerTests/Extensions/ByteArrayExtensionsTest.cs b/ModuleManagerTests/Extensions/ByteArrayExtensionsTest.cs new file mode 100644 index 00000000..ee53dcc8 --- /dev/null +++ b/ModuleManagerTests/Extensions/ByteArrayExtensionsTest.cs @@ -0,0 +1,28 @@ +using System; +using Xunit; +using ModuleManager.Extensions; + +namespace ModuleManagerTests.Extensions +{ + public class ByteArrayExtensionsTest + { + [Fact] + public void TestToHex() + { + byte[] data = { 0x00, 0xff, 0x01, 0xfe, 0x02, 0xfd, 0x9a }; + + Assert.Equal("00ff01fe02fd9a", data.ToHex()); + } + + [Fact] + public void TestToHex__NullData() + { + ArgumentNullException ex = Assert.Throws(delegate + { + ByteArrayExtensions.ToHex(null); + }); + + Assert.Equal("data", ex.ParamName); + } + } +} diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 49e52d41..9824ace1 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -81,6 +81,7 @@ + From a27cb3df58b5b2218cfff2b0027959ce7332864f Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 30 May 2019 22:18:20 -0700 Subject: [PATCH 092/140] Convert UrlBuilder cfg test to theory allows more inline data to be added --- TestUtils/UrlBuilder.cs | 1 + TestUtilsTests/UrlBuilderTest.cs | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/TestUtils/UrlBuilder.cs b/TestUtils/UrlBuilder.cs index 2fff4a47..9e7f1393 100644 --- a/TestUtils/UrlBuilder.cs +++ b/TestUtils/UrlBuilder.cs @@ -118,6 +118,7 @@ public static UrlDir.UrlFile CreateFile(string path, UrlDir parent = null) bool cfg = false; string newName = name; + // KSP tries to load .cfg files so need to have special handling if (extension == "cfg") { cfg = true; diff --git a/TestUtilsTests/UrlBuilderTest.cs b/TestUtilsTests/UrlBuilderTest.cs index 956350dd..8a685779 100644 --- a/TestUtilsTests/UrlBuilderTest.cs +++ b/TestUtilsTests/UrlBuilderTest.cs @@ -209,23 +209,22 @@ public void TestCreateFile__Parent() Assert.Contains(file, root.AllFiles); } - // KSP tries to load .cfg files so need to have special handling - [Fact] - public void TestCreateFile__cfg() + [InlineData("cfg", UrlDir.FileType.Config)] + [Theory] + public void TestCreateFile__Extension(string extension, UrlDir.FileType fileType) { - UrlDir root = UrlBuilder.CreateRoot(); - UrlDir dir = UrlBuilder.CreateDir("someDir", root); - UrlDir.UrlFile file = UrlBuilder.CreateFile("someFile.cfg", dir); + UrlDir.UrlFile file = UrlBuilder.CreateFile("someFile." + extension); Assert.Equal("someFile", file.name); Assert.Equal("cfg", file.fileExtension); - Assert.Equal(UrlDir.FileType.Config, file.fileType); - Assert.Same(dir, file.parent); - Assert.Same(root, file.root); + Assert.Equal(fileType, file.fileType); - Assert.Equal("someDir/someFile", file.url); - Assert.Contains(file, dir.files); - Assert.Contains(file, root.AllConfigFiles); + UrlDir root = file.parent; + Assert.NotNull(root); + Assert.Equal("root", root.name); + Assert.Null(root.parent); + Assert.Contains(file, root.files); + Assert.Same(root, file.root); } [Fact] From 3d8594dd855febd0024a0ac876ad7e8ce03d2848 Mon Sep 17 00:00:00 2001 From: blowfish Date: Thu, 30 May 2019 22:37:47 -0700 Subject: [PATCH 093/140] Set FileType based on common types Makes checking against them easier --- TestUtils/UrlBuilder.cs | 35 +++++++++++++++++++++++++++++++- TestUtilsTests/UrlBuilderTest.cs | 17 +++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/TestUtils/UrlBuilder.cs b/TestUtils/UrlBuilder.cs index 9e7f1393..e8e60bd5 100644 --- a/TestUtils/UrlBuilder.cs +++ b/TestUtils/UrlBuilder.cs @@ -118,6 +118,38 @@ public static UrlDir.UrlFile CreateFile(string path, UrlDir parent = null) bool cfg = false; string newName = name; + UrlDir.FileType fileType = UrlDir.FileType.Unknown; + + switch (extension) + { + case "dll": + fileType = UrlDir.FileType.Assembly; + break; + case "ksp": + fileType = UrlDir.FileType.AssetBundle; + break; + case "wav": + case "ogg": + fileType = UrlDir.FileType.Audio; + break; + case "cfg": + fileType = UrlDir.FileType.Config; + break; + case "dae": + case "mu": + fileType = UrlDir.FileType.Model; + break; + case "dds": + case "jpg": + case "jpeg": + case "mbm": + case "png": + case "tga": + case "truecolor": + fileType = UrlDir.FileType.Texture; + break; + } + // KSP tries to load .cfg files so need to have special handling if (extension == "cfg") { @@ -127,11 +159,12 @@ public static UrlDir.UrlFile CreateFile(string path, UrlDir parent = null) UrlDir.UrlFile file = new UrlDir.UrlFile(parent, new FileInfo(newName)); + UrlFile__field__fileType.SetValue(file, fileType); + if (cfg) { UrlFile__field__name.SetValue(file, nameWithoutExtension); UrlFile__field__fileExtension.SetValue(file, "cfg"); - UrlFile__field__fileType.SetValue(file, UrlDir.FileType.Config); } parent.files.Add(file); diff --git a/TestUtilsTests/UrlBuilderTest.cs b/TestUtilsTests/UrlBuilderTest.cs index 8a685779..38a788f0 100644 --- a/TestUtilsTests/UrlBuilderTest.cs +++ b/TestUtilsTests/UrlBuilderTest.cs @@ -209,14 +209,29 @@ public void TestCreateFile__Parent() Assert.Contains(file, root.AllFiles); } + [InlineData("dll", UrlDir.FileType.Assembly)] + [InlineData("ksp", UrlDir.FileType.AssetBundle)] + [InlineData("wav", UrlDir.FileType.Audio)] + [InlineData("ogg", UrlDir.FileType.Audio)] [InlineData("cfg", UrlDir.FileType.Config)] + [InlineData("dae", UrlDir.FileType.Model)] + [InlineData("mu", UrlDir.FileType.Model)] + [InlineData("dds", UrlDir.FileType.Texture)] + [InlineData("jpg", UrlDir.FileType.Texture)] + [InlineData("jpeg", UrlDir.FileType.Texture)] + [InlineData("mbm", UrlDir.FileType.Texture)] + [InlineData("png", UrlDir.FileType.Texture)] + [InlineData("tga", UrlDir.FileType.Texture)] + [InlineData("truecolor", UrlDir.FileType.Texture)] + [InlineData("txt", UrlDir.FileType.Unknown)] + [InlineData("xml", UrlDir.FileType.Unknown)] [Theory] public void TestCreateFile__Extension(string extension, UrlDir.FileType fileType) { UrlDir.UrlFile file = UrlBuilder.CreateFile("someFile." + extension); Assert.Equal("someFile", file.name); - Assert.Equal("cfg", file.fileExtension); + Assert.Equal(extension, file.fileExtension); Assert.Equal(fileType, file.fileType); UrlDir root = file.parent; From eb3db605b24f56af6e4da5a17a8dbf3b9795292e Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 31 May 2019 00:33:33 -0700 Subject: [PATCH 094/140] unnecessary using --- ModuleManagerTests/Logging/StreamLoggerTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ModuleManagerTests/Logging/StreamLoggerTest.cs b/ModuleManagerTests/Logging/StreamLoggerTest.cs index 61535387..734a3521 100644 --- a/ModuleManagerTests/Logging/StreamLoggerTest.cs +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using UnityEngine; using Xunit; using NSubstitute; using ModuleManager.Logging; From 9795e3656b9e73a7ed1622d2b832002ad9b70d38 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 31 May 2019 00:40:53 -0700 Subject: [PATCH 095/140] make StreamLoggerTest.TestLog work on \n platforms line breaks are shorter which causes an extra null character to exist at the end of the string, meaning that the default Trim() wasn't removing that and the newline --- ModuleManagerTests/Logging/StreamLoggerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManagerTests/Logging/StreamLoggerTest.cs b/ModuleManagerTests/Logging/StreamLoggerTest.cs index 734a3521..bc613038 100644 --- a/ModuleManagerTests/Logging/StreamLoggerTest.cs +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -69,7 +69,7 @@ public void TestLog() { using (StreamReader reader = new StreamReader(stream)) { - string result = reader.ReadToEnd().Trim(); + string result = reader.ReadToEnd().Trim('\r', '\n', '\0'); Assert.Equal("[OMG wtf] bbq", result); } } From 9311cfb45507323f3f56f315dbda61f5034a31b0 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Wed, 26 Jun 2019 22:55:57 -0700 Subject: [PATCH 096/140] Improve TestConfigNodeTest Strictly assert the structure of the node --- TestUtilsTests/TestConfigNodeTest.cs | 45 +++++++++++++++++----------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/TestUtilsTests/TestConfigNodeTest.cs b/TestUtilsTests/TestConfigNodeTest.cs index 6b00b4de..0858ece6 100644 --- a/TestUtilsTests/TestConfigNodeTest.cs +++ b/TestUtilsTests/TestConfigNodeTest.cs @@ -34,31 +34,40 @@ public void TestTestConfigNode() }, }; - Assert.Equal("something", node.GetValue("value1")); - Assert.Equal("something else", node.GetValue("value2")); - Assert.Equal(new[] { "first", "second" }, node.GetValues("multiple")); - Assert.Equal("bar", node.GetValue("foo")); + Assert.Equal(5, node.values.Count); + AssertValue("value1", "something", node.values[0]); + AssertValue("value2", "something else", node.values[1]); + AssertValue("multiple", "first", node.values[2]); + AssertValue("multiple", "second", node.values[3]); + AssertValue("foo", "bar", node.values[4]); + Assert.Equal(3, node.nodes.Count); ConfigNode innerNode1 = node.GetNode("NODE_1"); Assert.NotNull(innerNode1); - Assert.Equal("NODE_1", innerNode1.name); - Assert.Equal("something", innerNode1.GetValue("name")); - Assert.Equal("something else", innerNode1.GetValue("stuff")); + Assert.Equal("NODE_1", node.nodes[0].name); + Assert.Equal(2, node.nodes[0].values.Count); + AssertValue("name", "something", node.nodes[0].values[0]); + AssertValue("stuff", "something else", node.nodes[0].values[1]); + Assert.Empty(node.nodes[0].nodes); - ConfigNode[] innerNodes2 = node.GetNodes("MULTIPLE"); - Assert.NotNull(innerNodes2); - Assert.Equal(2, innerNodes2.Length); + Assert.Equal("MULTIPLE", node.nodes[1].name); + Assert.Equal(2, node.nodes[1].values.Count); + AssertValue("value3", "blah", node.nodes[1].values[0]); + AssertValue("value4", "bleh", node.nodes[1].values[1]); + Assert.Empty(node.nodes[1].nodes); - ConfigNode innerNode2a = innerNodes2[0]; - Assert.NotNull(innerNode2a); - Assert.Equal("blah", innerNode2a.GetValue("value3")); - Assert.Equal("bleh", innerNode2a.GetValue("value4")); + Assert.Equal("MULTIPLE", node.nodes[2].name); + Assert.Equal(2, node.nodes[2].values.Count); + AssertValue("value3", "blih", node.nodes[2].values[0]); + AssertValue("value4", "bloh", node.nodes[2].values[1]); + Assert.Empty(node.nodes[2].nodes); + } - ConfigNode innerNode2b = innerNodes2[1]; - Assert.NotNull(innerNode2b); - Assert.Equal("blih", innerNode2b.GetValue("value3")); - Assert.Equal("bloh", innerNode2b.GetValue("value4")); + private void AssertValue(string name, string value, ConfigNode.Value nodeValue) + { + Assert.Equal(name, nodeValue.name); + Assert.Equal(value, nodeValue.value); } } } From ee5995f6196cfe58f2f646b1f8f8f847b32b9f0d Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Wed, 26 Jun 2019 22:58:26 -0700 Subject: [PATCH 097/140] Remove redundant parent class --- TestUtils/TestConfigNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TestUtils/TestConfigNode.cs b/TestUtils/TestConfigNode.cs index a954c107..6b71a3ae 100644 --- a/TestUtils/TestConfigNode.cs +++ b/TestUtils/TestConfigNode.cs @@ -9,7 +9,7 @@ public TestConfigNode() : base() { } public TestConfigNode(string name) : base(name) { } public void Add(string name, string value) => AddValue(name, value); - public void Add(ConfigNode.Value value) => values.Add(value); + public void Add(Value value) => values.Add(value); public void Add(string name, ConfigNode node) => AddNode(name, node); public void Add(ConfigNode node) => AddNode(node); From 3e4a9f998ecd40118e07bce7c655da6fb15a8442 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Wed, 26 Jun 2019 23:03:27 -0700 Subject: [PATCH 098/140] Fix TestConfigNode's handling of escaped chars KSP removes these automatically, so we want to avoid that by constructing the Value ourselves --- TestUtils/TestConfigNode.cs | 2 +- TestUtilsTests/TestConfigNodeTest.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/TestUtils/TestConfigNode.cs b/TestUtils/TestConfigNode.cs index 6b71a3ae..694fc55e 100644 --- a/TestUtils/TestConfigNode.cs +++ b/TestUtils/TestConfigNode.cs @@ -8,7 +8,7 @@ public class TestConfigNode : ConfigNode, IEnumerable public TestConfigNode() : base() { } public TestConfigNode(string name) : base(name) { } - public void Add(string name, string value) => AddValue(name, value); + public void Add(string name, string value) => Add(new Value(name, value)); public void Add(Value value) => values.Add(value); public void Add(string name, ConfigNode node) => AddNode(name, node); public void Add(ConfigNode node) => AddNode(node); diff --git a/TestUtilsTests/TestConfigNodeTest.cs b/TestUtilsTests/TestConfigNodeTest.cs index 0858ece6..e544da78 100644 --- a/TestUtilsTests/TestConfigNodeTest.cs +++ b/TestUtilsTests/TestConfigNodeTest.cs @@ -16,6 +16,7 @@ public void TestTestConfigNode() { "multiple", "first" }, { "multiple", "second" }, new ConfigNode.Value("foo", "bar"), + { "weird_values", "some\r\n\tstuff" }, { "NODE_1", new TestConfigNode { { "name", "something" }, @@ -34,12 +35,13 @@ public void TestTestConfigNode() }, }; - Assert.Equal(5, node.values.Count); + Assert.Equal(6, node.values.Count); AssertValue("value1", "something", node.values[0]); AssertValue("value2", "something else", node.values[1]); AssertValue("multiple", "first", node.values[2]); AssertValue("multiple", "second", node.values[3]); AssertValue("foo", "bar", node.values[4]); + AssertValue("weird_values", "some\r\n\tstuff", node.values[5]); Assert.Equal(3, node.nodes.Count); ConfigNode innerNode1 = node.GetNode("NODE_1"); From 8e5c29ad0983157c88116085d4745e125941a416 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Wed, 26 Jun 2019 23:18:01 -0700 Subject: [PATCH 099/140] Clean up test a bit Use AssertValue method Use AssertEmpty rather than asserting count zero --- .../Extensions/ConfigNodeExtensionsTest.cs | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs index 7e4030ad..fcf9533e 100644 --- a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs @@ -49,13 +49,11 @@ public void TestShallowCopyFrom() Assert.Same(value1, fromNode.values[0]); Assert.Same(value1, toNode.values[0]); - Assert.Equal("abc", value1.name); - Assert.Equal("def", value1.value); + AssertValue("abc", "def", value1); Assert.Same(value2, fromNode.values[1]); Assert.Same(value2, toNode.values[1]); - Assert.Equal("ghi", value2.name); - Assert.Equal("jkl", value2.value); + AssertValue("ghi", "jkl", value2); Assert.Equal(2, fromNode.nodes.Count); Assert.Equal(2, toNode.nodes.Count); @@ -64,23 +62,21 @@ public void TestShallowCopyFrom() Assert.Same(innerNode1, toNode.nodes[0]); Assert.Equal("INNER_NODE_1", innerNode1.name); Assert.Equal(1, innerNode1.values.Count); - Assert.Equal("mno", innerNode1.values[0].name); - Assert.Equal("pqr", innerNode1.values[0].value); + AssertValue("mno", "pqr", innerNode1.values[0]); Assert.Equal(1, innerNode1.nodes.Count); Assert.Equal("INNER_INNER_NODE_1", innerNode1.nodes[0].name); - Assert.Equal(0, innerNode1.nodes[0].values.Count); - Assert.Equal(0, innerNode1.nodes[0].nodes.Count); + Assert.Empty(innerNode1.nodes[0].values); + Assert.Empty(innerNode1.nodes[0].nodes); Assert.Same(innerNode2, fromNode.nodes[1]); Assert.Same(innerNode2, toNode.nodes[1]); Assert.Equal("INNER_NODE_2", innerNode2.name); Assert.Equal(1, innerNode2.values.Count); - Assert.Equal("stu", innerNode2.values[0].name); - Assert.Equal("vwx", innerNode2.values[0].value); + AssertValue("stu", "vwx", innerNode2.values[0]); Assert.Equal(1, innerNode2.nodes.Count); Assert.Equal("INNER_INNER_NODE_2", innerNode2.nodes[0].name); - Assert.Equal(0, innerNode2.nodes[0].values.Count); - Assert.Equal(0, innerNode2.nodes[0].nodes.Count); + Assert.Empty(innerNode2.nodes[0].values); + Assert.Empty(innerNode2.nodes[0].nodes); } [Fact] @@ -109,13 +105,11 @@ public void TestDeepCopy() Assert.Equal(2, toNode.values.Count); Assert.NotSame(fromNode.values[0], toNode.values[0]); - Assert.Equal("abc", toNode.values[0].name); - Assert.Equal("def", toNode.values[0].value); - + AssertValue("abc", "def", toNode.values[0]); + Assert.NotSame(fromNode.values[1], toNode.values[1]); - Assert.Equal("ghi", toNode.values[1].name); - Assert.Equal("jkl", toNode.values[1].value); - + AssertValue("ghi", "jkl", toNode.values[1]); + Assert.Equal(2, toNode.nodes.Count); ConfigNode innerNode1 = toNode.nodes[0]; @@ -123,26 +117,24 @@ public void TestDeepCopy() Assert.Equal("INNER_NODE_1", innerNode1.name); Assert.Equal(1, innerNode1.values.Count); Assert.NotSame(fromNode.nodes[0].values[0], innerNode1.values[0]); - Assert.Equal("mno", innerNode1.values[0].name); - Assert.Equal("pqr", innerNode1.values[0].value); + AssertValue("mno", "pqr", innerNode1.values[0]); Assert.Equal(1, toNode.nodes[0].nodes.Count); Assert.NotSame(fromNode.nodes[0].nodes[0], innerNode1.nodes[0]); Assert.Equal("INNER_INNER_NODE_1", innerNode1.nodes[0].name); - Assert.Equal(0, innerNode1.nodes[0].values.Count); - Assert.Equal(0, innerNode1.nodes[0].nodes.Count); + Assert.Empty(innerNode1.nodes[0].values); + Assert.Empty(innerNode1.nodes[0].nodes); ConfigNode innerNode2 = toNode.nodes[1]; Assert.NotSame(fromNode.nodes[1], innerNode2); Assert.Equal("INNER_NODE_2", innerNode2.name); Assert.Equal(1, innerNode2.values.Count); Assert.NotSame(fromNode.nodes[1].values[0], innerNode2.values[0]); - Assert.Equal("stu", innerNode2.values[0].name); - Assert.Equal("vwx", innerNode2.values[0].value); + AssertValue("stu", "vwx", innerNode2.values[0]); Assert.Equal(1, innerNode2.nodes.Count); Assert.NotSame(fromNode.nodes[1].nodes[0], innerNode2.nodes[0]); Assert.Equal("INNER_INNER_NODE_2", innerNode2.nodes[0].name); - Assert.Equal(0, innerNode2.nodes[0].values.Count); - Assert.Equal(0, innerNode2.nodes[0].nodes.Count); + Assert.Empty(innerNode2.nodes[0].values); + Assert.Empty(innerNode2.nodes[0].nodes); } [Fact] @@ -273,5 +265,11 @@ XX INNER_NODE node.PrettyPrint(ref sb, "XX"); Assert.Equal(expected, sb.ToString()); } + + private void AssertValue(string name, string value, ConfigNode.Value nodeValue) + { + Assert.Equal(name, nodeValue.name); + Assert.Equal(value, nodeValue.value); + } } } From 0feef88557d938ee16b889ba5685db307be2c798 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Wed, 26 Jun 2019 23:26:15 -0700 Subject: [PATCH 100/140] Fix DeepCopy handling of escaped values KSP removes these in AddValue, we can circumvent that by constructing the Value ourselves --- ModuleManager/Extensions/ConfigNodeExtensions.cs | 2 +- ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ModuleManager/Extensions/ConfigNodeExtensions.cs b/ModuleManager/Extensions/ConfigNodeExtensions.cs index 6940048a..79c192b5 100644 --- a/ModuleManager/Extensions/ConfigNodeExtensions.cs +++ b/ModuleManager/Extensions/ConfigNodeExtensions.cs @@ -19,7 +19,7 @@ public static ConfigNode DeepCopy(this ConfigNode from) { ConfigNode to = new ConfigNode(from.name); foreach (ConfigNode.Value value in from.values) - to.AddValue(value.name, value.value); + to.values.Add(new ConfigNode.Value(value.name, value.value)); foreach (ConfigNode node in from.nodes) { ConfigNode newNode = DeepCopy(node); diff --git a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs index fcf9533e..d771bf0d 100644 --- a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs @@ -89,6 +89,7 @@ public void TestDeepCopy() new TestConfigNode("INNER_NODE_1") { { "mno", "pqr" }, + { "weird_values", "some\r\n\tstuff" }, new TestConfigNode("INNER_INNER_NODE_1"), }, new TestConfigNode("INNER_NODE_2") @@ -115,9 +116,11 @@ public void TestDeepCopy() ConfigNode innerNode1 = toNode.nodes[0]; Assert.NotSame(fromNode.nodes[0], innerNode1); Assert.Equal("INNER_NODE_1", innerNode1.name); - Assert.Equal(1, innerNode1.values.Count); + Assert.Equal(2, innerNode1.values.Count); Assert.NotSame(fromNode.nodes[0].values[0], innerNode1.values[0]); AssertValue("mno", "pqr", innerNode1.values[0]); + Assert.NotSame(fromNode.nodes[0].values[1], innerNode1.values[1]); + AssertValue("weird_values", "some\r\n\tstuff", innerNode1.values[1]); Assert.Equal(1, toNode.nodes[0].nodes.Count); Assert.NotSame(fromNode.nodes[0].nodes[0], innerNode1.nodes[0]); Assert.Equal("INNER_INNER_NODE_1", innerNode1.nodes[0].name); From 96201a72d6c1037c95dc7f98da8177c25ee867f0 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Thu, 27 Jun 2019 00:23:57 -0700 Subject: [PATCH 101/140] Fix ModifyNode handling of escaped characters new AddValueSafe method to handle this use safe ShallowCopyFrom rather than ConfigNode's CopyTo as recursion is not necessary and it handles escaped characters correctly --- .../Extensions/ConfigNodeExtensions.cs | 7 +- ModuleManager/MMPatchLoader.cs | 26 +++---- .../Extensions/ConfigNodeExtensionsTest.cs | 15 ++++ ModuleManagerTests/MMPatchLoaderTest.cs | 78 +++++++++++++++++++ 4 files changed, 112 insertions(+), 14 deletions(-) diff --git a/ModuleManager/Extensions/ConfigNodeExtensions.cs b/ModuleManager/Extensions/ConfigNodeExtensions.cs index 79c192b5..a54100ba 100644 --- a/ModuleManager/Extensions/ConfigNodeExtensions.cs +++ b/ModuleManager/Extensions/ConfigNodeExtensions.cs @@ -19,7 +19,7 @@ public static ConfigNode DeepCopy(this ConfigNode from) { ConfigNode to = new ConfigNode(from.name); foreach (ConfigNode.Value value in from.values) - to.values.Add(new ConfigNode.Value(value.name, value.value)); + to.AddValueSafe(value.name, value.value); foreach (ConfigNode node in from.nodes) { ConfigNode newNode = DeepCopy(node); @@ -68,5 +68,10 @@ public static void PrettyPrint(this ConfigNode node, ref StringBuilder sb, strin sb.AppendFormat("{0}}}\n", indent); } + + public static void AddValueSafe(this ConfigNode node, string name, string value) + { + node.values.Add(new ConfigNode.Value(name, value)); + } } } diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index b7e28179..367805db 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -725,7 +725,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon if (varValue != null) { newNode.RemoveValues(valName); - newNode.AddValue(valName, varValue); + newNode.AddValueSafe(valName, varValue); } else { @@ -771,7 +771,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon if (cmd != Command.Copy) origVal.value = value; else - newNode.AddValue(valName, value); + newNode.AddValueSafe(valName, value); } } else @@ -847,7 +847,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon if (varValue != null) { if (!newNode.HasValue(valName)) - newNode.AddValue(valName, varValue); + newNode.AddValueSafe(valName, varValue); } else { @@ -1010,8 +1010,8 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon msg += " Applying subnode " + subMod.name + "\n"; #endif ConfigNode newSubNode = ModifyNode(nodeStack.Push(subNodes[0]), subMod, context); - subNodes[0].ClearData(); - newSubNode.CopyTo(subNodes[0], newSubNode.name); + subNodes[0].ShallowCopyFrom(newSubNode); + subNodes[0].name = newSubNode.name; } else { @@ -1023,7 +1023,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon ConfigNode copy = new ConfigNode(nodeType); if (nodeName != null) - copy.AddValue("name", nodeName); + copy.AddValueSafe("name", nodeName); ConfigNode newSubNode = ModifyNode(nodeStack.Push(copy), subMod, context); newNode.nodes.Add(newSubNode); @@ -1040,7 +1040,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon ConfigNode copy = new ConfigNode(nodeType); if (nodeName != null) - copy.AddValue("name", nodeName); + copy.AddValueSafe("name", nodeName); ConfigNode newSubNode = ModifyNode(nodeStack.Push(copy), subMod, context); newNode.nodes.Add(newSubNode); @@ -1066,8 +1066,8 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon // Edit in place newSubNode = ModifyNode(nodeStack.Push(subNode), subMod, context); - subNode.ClearData(); - newSubNode.CopyTo(subNode, newSubNode.name); + subNode.ShallowCopyFrom(newSubNode); + subNode.name = newSubNode.name; break; case Command.Delete: @@ -1698,13 +1698,13 @@ private static void InsertValue(ConfigNode newNode, int index, string name, stri newNode.RemoveValues(name); int i = 0; for (; i < index; ++i) - newNode.AddValue(name, oldValues[i]); - newNode.AddValue(name, value); + newNode.AddValueSafe(name, oldValues[i]); + newNode.AddValueSafe(name, value); for (; i < oldValues.Length; ++i) - newNode.AddValue(name, oldValues[i]); + newNode.AddValueSafe(name, oldValues[i]); return; } - newNode.AddValue(name, value); + newNode.AddValueSafe(name, value); } //FindConfigNodeIn finds and returns a ConfigNode in src of type nodeType. diff --git a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs index d771bf0d..cb0c5e1f 100644 --- a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs @@ -269,6 +269,21 @@ XX INNER_NODE Assert.Equal(expected, sb.ToString()); } + [Fact] + public void TestAddValueSafe() + { + ConfigNode node = new TestConfigNode + { + { "key1", "value1" }, + }; + + node.AddValueSafe("weird_values", "some\r\n\tstuff"); + + Assert.Equal(2, node.values.Count); + AssertValue("key1", "value1", node.values[0]); + AssertValue("weird_values", "some\r\n\tstuff", node.values[1]); + } + private void AssertValue(string name, string value, ConfigNode.Value nodeValue) { Assert.Equal(name, nodeValue.name); diff --git a/ModuleManagerTests/MMPatchLoaderTest.cs b/ModuleManagerTests/MMPatchLoaderTest.cs index 29682448..b0617fe8 100644 --- a/ModuleManagerTests/MMPatchLoaderTest.cs +++ b/ModuleManagerTests/MMPatchLoaderTest.cs @@ -71,6 +71,84 @@ public void TestModifyNode__MultiplyValue() }, c3); } + [Fact] + public void TestModifyNode__EditNode__SpecialCharacters() + { + ConfigNode c1 = new TestConfigNode("NODE") + { + new TestConfigNode("INNER_NODE") + { + { "weird_values", "some\r\n\tstuff" }, + }, + }; + + UrlDir.UrlConfig c2u = UrlBuilder.CreateConfig("abc/def", new TestConfigNode("@NODE") + { + new TestConfigNode("@INNER_NODE") + { + { "another_weird_value", "some\r\nmore\tstuff" }, + }, + }); + + PatchContext context = new PatchContext(c2u, Enumerable.Empty(), logger, progress); + + ConfigNode c3 = MMPatchLoader.ModifyNode(new NodeStack(c1), c2u.config, context); + + EnsureNoErrors(); + + AssertConfigNodesEqual(new TestConfigNode("NODE") + { + new TestConfigNode("INNER_NODE") + { + { "weird_values", "some\r\n\tstuff" }, + { "another_weird_value", "some\r\nmore\tstuff" }, + }, + }, c3); + } + + [Fact] + public void TestModifyNode__ReplaceNode__SpecialCharacters() + { + ConfigNode c1 = new TestConfigNode("NODE") + { + new TestConfigNode("INNER_NODE") + { + { "weird_values", "some\r\n\tstuff" }, + }, + }; + + UrlDir.UrlConfig c2u = UrlBuilder.CreateConfig("abc/def", new TestConfigNode("@NODE") + { + new TestConfigNode("%INNER_NODE") + { + { "another_weird_value", "some\r\nmore\tstuff" }, + }, + new TestConfigNode("%OTHER_INNER_NODE") + { + { "another_weirder_value", "even\r\nmore\tstuff" }, + }, + }); + + PatchContext context = new PatchContext(c2u, Enumerable.Empty(), logger, progress); + + ConfigNode c3 = MMPatchLoader.ModifyNode(new NodeStack(c1), c2u.config, context); + + EnsureNoErrors(); + + AssertConfigNodesEqual(new TestConfigNode("NODE") + { + new TestConfigNode("INNER_NODE") + { + { "weird_values", "some\r\n\tstuff" }, + { "another_weird_value", "some\r\nmore\tstuff" }, + }, + new TestConfigNode("OTHER_INNER_NODE") + { + { "another_weirder_value", "even\r\nmore\tstuff" }, + }, + }, c3); + } + private void AssertConfigNodesEqual(ConfigNode expected, ConfigNode observed) { Assert.Equal(expected.ToString(), observed.ToString()); From f2fba86ce98bcba5ab13b7d3ce6c6fd7e51e74ea Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Thu, 27 Jun 2019 00:40:17 -0700 Subject: [PATCH 102/140] Ensure tabs and newlines don't break cache Explicitly escape \n and \t which the localizer unescapes when game database is initially loaded --- ModuleManager/MMPatchLoader.cs | 37 +++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 367805db..243aa0bb 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -430,11 +430,30 @@ private void CreateCache(IEnumerable databaseConfigs, int patch cache.AddValue("patchedNodeCount", patchedNodeCount.ToString()); + // Undoes escaping done by the localizer + void FixValuesRecursive(ConfigNode theNode) + { + foreach (ConfigNode subNode in theNode.nodes) + { + FixValuesRecursive(subNode); + } + + foreach (ConfigNode.Value value in theNode.values) + { + value.value = value.value.Replace("\n", "\\n"); + value.value = value.value.Replace("\t", "\\t"); + } + } + foreach (IProtoUrlConfig urlConfig in databaseConfigs) { ConfigNode node = cache.AddNode("UrlConfig"); node.AddValue("parentUrl", urlConfig.UrlFile.GetUrlWithExtension()); - node.AddNode(urlConfig.Node); + + ConfigNode urlNode = urlConfig.Node.DeepCopy(); + FixValuesRecursive(urlNode); + + node.AddNode(urlNode); } foreach (var file in GameDatabase.Instance.root.AllConfigFiles) @@ -524,6 +543,21 @@ private IEnumerable LoadCache() List databaseConfigs = new List(cache.nodes.Count); + // Evaluate escape sequences + void FixValuesRecursive(ConfigNode theNode) + { + foreach (ConfigNode subNode in theNode.nodes) + { + FixValuesRecursive(subNode); + } + + foreach (ConfigNode.Value value in theNode.values) + { + value.value = value.value.Replace("\\n", "\n"); + value.value = value.value.Replace("\\t", "\t"); + } + } + foreach (ConfigNode node in cache.nodes) { string parentUrl = node.GetValue("parentUrl"); @@ -531,6 +565,7 @@ private IEnumerable LoadCache() UrlDir.UrlFile parent = gameDataDir.Find(parentUrl); if (parent != null) { + FixValuesRecursive(node.nodes[0]); databaseConfigs.Add(new ProtoUrlConfig(parent, node.nodes[0])); } else From 3d85ddca5fcf62e81e21904fcb04364039081bbf Mon Sep 17 00:00:00 2001 From: Sarbian Date: Wed, 7 Aug 2019 01:03:23 +0200 Subject: [PATCH 103/140] v4.0.3 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index 43b2dc37..26449b74 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.0.2")] +[assembly: AssemblyVersion("4.0.3")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From aa6ff2e35e6dc06c7b5ea3f2941d2d621fd90791 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Mon, 12 Aug 2019 21:08:53 -0700 Subject: [PATCH 104/140] Extract escaping/unescaping of node values Allows it to be tested --- .../Extensions/ConfigNodeExtensions.cs | 30 ++++++++++++ ModuleManager/MMPatchLoader.cs | 34 +------------- .../Extensions/ConfigNodeExtensionsTest.cs | 46 +++++++++++++++++++ 3 files changed, 78 insertions(+), 32 deletions(-) diff --git a/ModuleManager/Extensions/ConfigNodeExtensions.cs b/ModuleManager/Extensions/ConfigNodeExtensions.cs index a54100ba..9f552566 100644 --- a/ModuleManager/Extensions/ConfigNodeExtensions.cs +++ b/ModuleManager/Extensions/ConfigNodeExtensions.cs @@ -73,5 +73,35 @@ public static void AddValueSafe(this ConfigNode node, string name, string value) { node.values.Add(new ConfigNode.Value(name, value)); } + + public static void EscapeValuesRecursive(this ConfigNode theNode) + { + foreach (ConfigNode subNode in theNode.nodes) + { + subNode.EscapeValuesRecursive(); + } + + foreach (ConfigNode.Value value in theNode.values) + { + value.value = value.value.Replace("\n", "\\n"); + value.value = value.value.Replace("\r", "\\r"); + value.value = value.value.Replace("\t", "\\t"); + } + } + + public static void UnescapeValuesRecursive(this ConfigNode theNode) + { + foreach (ConfigNode subNode in theNode.nodes) + { + subNode.UnescapeValuesRecursive(); + } + + foreach (ConfigNode.Value value in theNode.values) + { + value.value = value.value.Replace("\\n", "\n"); + value.value = value.value.Replace("\\r", "\r"); + value.value = value.value.Replace("\\t", "\t"); + } + } } } diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 243aa0bb..41e28bd3 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -430,28 +430,13 @@ private void CreateCache(IEnumerable databaseConfigs, int patch cache.AddValue("patchedNodeCount", patchedNodeCount.ToString()); - // Undoes escaping done by the localizer - void FixValuesRecursive(ConfigNode theNode) - { - foreach (ConfigNode subNode in theNode.nodes) - { - FixValuesRecursive(subNode); - } - - foreach (ConfigNode.Value value in theNode.values) - { - value.value = value.value.Replace("\n", "\\n"); - value.value = value.value.Replace("\t", "\\t"); - } - } - foreach (IProtoUrlConfig urlConfig in databaseConfigs) { ConfigNode node = cache.AddNode("UrlConfig"); node.AddValue("parentUrl", urlConfig.UrlFile.GetUrlWithExtension()); ConfigNode urlNode = urlConfig.Node.DeepCopy(); - FixValuesRecursive(urlNode); + urlNode.EscapeValuesRecursive(); node.AddNode(urlNode); } @@ -543,21 +528,6 @@ private IEnumerable LoadCache() List databaseConfigs = new List(cache.nodes.Count); - // Evaluate escape sequences - void FixValuesRecursive(ConfigNode theNode) - { - foreach (ConfigNode subNode in theNode.nodes) - { - FixValuesRecursive(subNode); - } - - foreach (ConfigNode.Value value in theNode.values) - { - value.value = value.value.Replace("\\n", "\n"); - value.value = value.value.Replace("\\t", "\t"); - } - } - foreach (ConfigNode node in cache.nodes) { string parentUrl = node.GetValue("parentUrl"); @@ -565,7 +535,7 @@ void FixValuesRecursive(ConfigNode theNode) UrlDir.UrlFile parent = gameDataDir.Find(parentUrl); if (parent != null) { - FixValuesRecursive(node.nodes[0]); + node.nodes[0].UnescapeValuesRecursive(); databaseConfigs.Add(new ProtoUrlConfig(parent, node.nodes[0])); } else diff --git a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs index cb0c5e1f..471be9cb 100644 --- a/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs @@ -284,6 +284,52 @@ public void TestAddValueSafe() AssertValue("weird_values", "some\r\n\tstuff", node.values[1]); } + [Fact] + public void TestEscapeValuesRecursive() + { + ConfigNode node = new TestConfigNode + { + { "key1", "value1" }, + { "key2", "value\nwith\rescped\tchars" }, + new TestConfigNode("SUBNODE") + { + { "key3", "value\nwith\rescped\tchars2" }, + }, + }; + + node.EscapeValuesRecursive(); + + Assert.Equal(2, node.values.Count); + AssertValue("key1", "value1", node.values[0]); + AssertValue("key2", "value\\nwith\\rescped\\tchars", node.values[1]); + Assert.Equal(1, node.nodes.Count); + Assert.Equal(1, node.nodes[0].values.Count); + AssertValue("key3", "value\\nwith\\rescped\\tchars2", node.nodes[0].values[0]); + } + + [Fact] + public void TestUnescapeValuesRecursive() + { + ConfigNode node = new TestConfigNode + { + { "key1", "value1" }, + { "key2", "value\\nwith\\rescped\\tchars" }, + new TestConfigNode("SUBNODE") + { + { "key3", "value\\nwith\\rescped\\tchars2" }, + }, + }; + + node.UnescapeValuesRecursive(); + + Assert.Equal(2, node.values.Count); + AssertValue("key1", "value1", node.values[0]); + AssertValue("key2", "value\nwith\rescped\tchars", node.values[1]); + Assert.Equal(1, node.nodes.Count); + Assert.Equal(1, node.nodes[0].values.Count); + AssertValue("key3", "value\nwith\rescped\tchars2", node.nodes[0].values[0]); + } + private void AssertValue(string name, string value, ConfigNode.Value nodeValue) { Assert.Equal(name, nodeValue.name); From 4201d5fd486810d249327e76482a617181a03cc7 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Mon, 12 Aug 2019 21:09:47 -0700 Subject: [PATCH 105/140] Fix dump database to files * Fix paths getting duplicated resulting in files not being written * Escape node values before writing --- ModuleManager/Extensions/UrlFileExtensions.cs | 4 ++ ModuleManager/ModuleManager.cs | 65 ++++++++++--------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/ModuleManager/Extensions/UrlFileExtensions.cs b/ModuleManager/Extensions/UrlFileExtensions.cs index 6fe1cbe9..3d203b24 100644 --- a/ModuleManager/Extensions/UrlFileExtensions.cs +++ b/ModuleManager/Extensions/UrlFileExtensions.cs @@ -8,5 +8,9 @@ public static string GetUrlWithExtension(this UrlDir.UrlFile urlFile) { return $"{urlFile.url}.{urlFile.fileExtension}"; } + public static string GetNameWithExtension(this UrlDir.UrlFile urlFile) + { + return $"{urlFile.name}.{urlFile.fileExtension}"; + } } } diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 75b4f6ec..18425f3f 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -430,7 +430,7 @@ private IEnumerator DataBaseReloadWithMM(bool dump = false) public static void OutputAllConfigs() { - string path = KSPUtil.ApplicationRootPath + "/_MMCfgOutput/"; + string path = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "_MMCfgOutput")); try { Directory.CreateDirectory(path); @@ -451,51 +451,52 @@ public static void OutputAllConfigs() { Log("Exception while cleaning the export dir\n" + unauthorizedAccessException); } - Stack dirs = new Stack(); - dirs.Push(GameDatabase.Instance.root); - Stack paths = new Stack(); - paths.Push(""); - try + void WriteDirectoryRecursive(UrlDir currentDir, string dirPath) { - while (dirs.Count > 0) + if (currentDir.files.Count > 0) Directory.CreateDirectory(dirPath); + + foreach (UrlDir.UrlFile urlFile in currentDir.files) { - var currentDir = dirs.Pop(); - string currentPath = paths.Pop(); - - foreach (UrlDir.UrlFile urlFile in currentDir.files) + if (urlFile.fileType != UrlDir.FileType.Config) continue; + + Log("Exporting " + urlFile.GetUrlWithExtension()); + string filePath = Path.Combine(dirPath, urlFile.GetNameWithExtension()); + + bool first = true; + + using (FileStream stream = new FileStream(filePath, FileMode.Create)) + using (StreamWriter writer = new StreamWriter(stream)) { - if (urlFile.fileType == UrlDir.FileType.Config) + foreach (UrlDir.UrlConfig urlConfig in urlFile.configs) { - string dirPath = path + currentPath; - if (!Directory.Exists(dirPath)) + try { - Directory.CreateDirectory(dirPath); - } + if (first) first = false; + else writer.Write("\n"); - Log("Exporting " + currentPath + urlFile.GetUrlWithExtension()); - string filePath = dirPath + urlFile.GetUrlWithExtension(); - foreach (UrlDir.UrlConfig urlConfig in urlFile.configs) + ConfigNode copy = urlConfig.config.DeepCopy(); + copy.EscapeValuesRecursive(); + writer.Write(copy.ToString()); + } + catch (Exception e) { - try - { - File.AppendAllText(filePath, urlConfig.config.ToString()); - } - catch (Exception e) - { - Log("Exception while trying to write the file " + filePath + "\n" + e); - } + Log("Exception while trying to write the file " + filePath + "\n" + e); } } } + } - foreach (UrlDir urlDir in currentDir.children) - { - dirs.Push(urlDir); - paths.Push(currentPath + urlDir.name + "/"); - } + foreach (UrlDir urlDir in currentDir.children) + { + WriteDirectoryRecursive(urlDir, Path.Combine(dirPath, urlDir.name)); } } + + try + { + WriteDirectoryRecursive(GameDatabase.Instance.root, path); + } catch (DirectoryNotFoundException directoryNotFoundException) { Log("Exception while exporting the cfg\n" + directoryNotFoundException); From cc11776ad6576ae756ae1c03f1903734a191c735 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Mon, 12 Aug 2019 21:31:23 -0700 Subject: [PATCH 106/140] Fix reload dialog showing 100% when it shouldn't PartLoader reports 100% done before it has started loading --- ModuleManager/ModuleManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 18425f3f..b10c977c 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -344,7 +344,7 @@ private IEnumerator DataBaseReloadWithMM(bool dump = false) { progressFraction = 0f; } - else if (!GameDatabase.Instance.IsReady()) + else if (!GameDatabase.Instance.IsReady() || !PostPatchLoader.Instance.IsReady()) { progressFraction = GameDatabase.Instance.ProgressFraction() * GameDatabase.Instance.LoadWeight(); progressFraction /= totalLoadWeight; From a79618e0d1c040ef2ec572e06acc1709a3284716 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Wed, 16 Oct 2019 22:37:47 +0200 Subject: [PATCH 107/140] KSP 1.8 ! With less cat bugs --- ModuleManager/Cats/CatManager.cs | 2 +- ModuleManager/Cats/CatMover.cs | 12 ++++++++++- ModuleManager/ModuleManager.cs | 2 +- ModuleManager/ModuleManager.csproj | 34 ++++++++++++++++++++++++------ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/ModuleManager/Cats/CatManager.cs b/ModuleManager/Cats/CatManager.cs index 0567de54..744a0ca8 100644 --- a/ModuleManager/Cats/CatManager.cs +++ b/ModuleManager/Cats/CatManager.cs @@ -94,7 +94,7 @@ private static GameObject LaunchCat(int scale) sr.sprite = catFrames[0]; - trail.material = new Material(Shader.Find("Particles/Alpha Blended")); + trail.material = new Material(Shader.Find("Legacy Shaders/Particles/Alpha Blended")); Debug.Log("material = " + trail.material); trail.material.mainTexture = rainbow; diff --git a/ModuleManager/Cats/CatMover.cs b/ModuleManager/Cats/CatMover.cs index f5dd7678..2e37ac9a 100644 --- a/ModuleManager/Cats/CatMover.cs +++ b/ModuleManager/Cats/CatMover.cs @@ -21,6 +21,8 @@ public class CatMover : MonoBehaviour private const float time = 5; private const float trailTime = time / 4; + private bool clearTrail = false; + // Use this for initialization void Start() { @@ -36,6 +38,7 @@ void Start() totalLenth = (int) (Screen.width / time * trail.time) + 150; trail.time = trailTime; trail.widthCurve = new AnimationCurve(new Keyframe(0, trail.startWidth ), new Keyframe(0.7f, trail.startWidth), new Keyframe(1, trail.startWidth * 0.9f)); + clearTrail = true; } void Update() @@ -50,7 +53,7 @@ void Update() if (activePos > (Screen.width + totalLenth)) { activePos = -spriteRenderer.sprite.rect.width; - trail.time = 0; + clearTrail = true; } float f = 2f * Mathf.PI * (activePos) / (Screen.width * 0.5f); @@ -62,6 +65,13 @@ void Update() transform.position = KSP.UI.UIMainCamera.Camera.ScreenToWorldPoint(spos); transform.rotation = Quaternion.Euler(0, 0, Mathf.Cos(f) * 0.25f * Mathf.PI * Mathf.Rad2Deg); + + if (clearTrail) + { + trail.Clear(); + clearTrail = false; + } + } diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index b10c977c..cfc88979 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -81,7 +81,7 @@ internal void Awake() totalTime.Start(); - // Allow loading the background in the laoding screen + // Allow loading the background in the loading screen Application.runInBackground = true; QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 04a043ae..8110ed4f 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -9,7 +9,8 @@ Library ModuleManager ModuleManager - v3.5 + v4.7 + True @@ -21,6 +22,7 @@ 4 False default + false none @@ -29,6 +31,7 @@ prompt 4 False + false @@ -115,16 +118,35 @@ False False + + False + - + + False - False + + + False + + + False + + + False + + + False + + + False False - C:\Games\KSPSteamController\KSPSteamCtrlr\KSPUnity-Steam-Symlinks\UnityEngine.UI.dll - False + + + False From 5aa82999b419e5c05ab7b8076b3034cb8e861b6d Mon Sep 17 00:00:00 2001 From: Sarbian Date: Wed, 16 Oct 2019 22:38:00 +0200 Subject: [PATCH 108/140] v4.1.0 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index 26449b74..c9549df2 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.0.3")] +[assembly: AssemblyVersion("4.1.0")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From ff637237458fbd372ca64a8da4d105ae0c0d8719 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Sun, 27 Oct 2019 15:36:21 +0100 Subject: [PATCH 109/140] v4.1.1 - Outdated Firespitter warning --- ModuleManager/ModuleManager.cs | 12 ++++++++++++ ModuleManager/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index cfc88979..30db5f59 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -184,6 +184,18 @@ private void Start() // //if (GUI.Button(new Rect(Screen.width / 2f - 100, offsetY, 200, 20), "Click to open the Forum thread")) // // Application.OpenURL("http://forum.kerbalspaceprogram.com/index.php?/topic/124998-silent-patch-for-ksp-105-published/"); //} + + if (Versioning.version_major == 1 && Versioning.version_minor >= 8) + { + foreach (AssemblyLoader.LoadedAssembly assembly in AssemblyLoader.loadedAssemblies) + { + AssemblyName assemblyName = assembly.assembly.GetName(); + if (assemblyName.Name == "Firespitter" && assemblyName.Version <= Version.Parse("7.3.7175.38653")) + { + warning.text = "You are using a version of Firespitter that does not run properly on KSP 1.8+\nThis version may prevent the game from loading properly and may create problems for other mods"; + } + } + } } private TextMeshProUGUI CreateTextObject(Canvas canvas, string name) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index c9549df2..b9e12e75 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.1.0")] +[assembly: AssemblyVersion("4.1.1")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From d172fc66ff56ffdc665ea47a3a143df8f17f441c Mon Sep 17 00:00:00 2001 From: Sarbian Date: Fri, 15 Nov 2019 22:32:36 +0100 Subject: [PATCH 110/140] Add an Exception interceptor to catch ReflectionTypeLoadException and properly blame broken DLLs --- .../ExceptionIntercept/InterceptLogHandler.cs | 56 +++++++++++++++++++ ModuleManager/ModuleManager.cs | 5 ++ ModuleManager/ModuleManager.csproj | 2 + 3 files changed, 63 insertions(+) create mode 100644 ModuleManager/ExceptionIntercept/InterceptLogHandler.cs diff --git a/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs new file mode 100644 index 00000000..a1af8d5c --- /dev/null +++ b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace ModuleManager.UnityLogHandle +{ + class InterceptLogHandler : ILogHandler + { + private readonly ILogHandler baseLogHandler; + private readonly List brokenAssemblies = new List(); + private readonly int gamePathLength; + + public static string Warnings { get; private set; } = ""; + + public InterceptLogHandler() + { + baseLogHandler = Debug.unityLogger.logHandler; + Debug.unityLogger.logHandler = this; + gamePathLength = Path.GetFullPath(KSPUtil.ApplicationRootPath).Length; + } + + public void LogFormat(LogType logType, Object context, string format, params object[] args) + { + baseLogHandler.LogFormat(logType, context, format, args); + } + + public void LogException(Exception exception, Object context) + { + baseLogHandler.LogException(exception, context); + + if (exception is ReflectionTypeLoadException ex) + { + ModuleManager.Log("Intercepted a ReflectionTypeLoadException. List of broken DLLs:"); + var assemblies = ex.Types.Where(x => x != null).Select(x => x.Assembly).Distinct(); + foreach (Assembly assembly in assemblies) + { + if (Warnings == "") + { + Warnings = "ModuleManager mod(s) DLL that are not compatible with this version of KSP\n"; + } + if (!brokenAssemblies.Contains(assembly)) + { + brokenAssemblies.Add(assembly); + Warnings += assembly.GetName().Name + " " + assembly.GetName().Version + " " + assembly.Location.Remove(0, gamePathLength) + "\n"; + } + ModuleManager.Log(assembly.GetName().Name + " " + assembly.GetName().Version + " " + assembly.Location.Remove(0, gamePathLength)); + } + } + baseLogHandler.LogException(exception, context); + } + } +} diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 30db5f59..4b8689ba 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -11,6 +11,7 @@ using ModuleManager.Cats; using ModuleManager.Extensions; using ModuleManager.Logging; +using ModuleManager.UnityLogHandle; namespace ModuleManager { @@ -40,6 +41,8 @@ public class ModuleManager : MonoBehaviour private MMPatchRunner patchRunner; + private InterceptLogHandler interceptLogHandler; + #endregion state private static bool loadedInScene; @@ -196,6 +199,7 @@ private void Start() } } } + interceptLogHandler = new InterceptLogHandler(); } private TextMeshProUGUI CreateTextObject(Canvas canvas, string name) @@ -289,6 +293,7 @@ internal void Update() { if (warning) { + warning.text = InterceptLogHandler.Warnings; h = warning.text.Length > 0 ? warning.textBounds.size.y : 0; offsetY = offsetY + h; warning.rectTransform.localPosition = new Vector3(0, offsetY); diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 8110ed4f..d256b235 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -108,6 +108,7 @@ + @@ -194,6 +195,7 @@ + sh -c "TARGET_PATH='$(TargetPath)' TARGET_DIR='$(TargetDir)' TARGET_NAME='$(TargetName)' sh '$(ProjectDir)/copy_build.sh'" From 1adea49112048cec35c3a971042ee5517ae0d416 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Fri, 15 Nov 2019 22:32:50 +0100 Subject: [PATCH 111/140] v4.1.2 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index b9e12e75..58db010d 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.1.1")] +[assembly: AssemblyVersion("4.1.2")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From ae4d27ace9ef61603a813d368609c9d7eba4d378 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Sat, 30 Nov 2019 15:46:59 +0100 Subject: [PATCH 112/140] Cleanup the InterceptLogHandler, remove double logging and avoid any risk of throwing more --- .../ExceptionIntercept/InterceptLogHandler.cs | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs index a1af8d5c..6cd3dcd0 100644 --- a/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs +++ b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs @@ -34,23 +34,32 @@ public void LogException(Exception exception, Object context) if (exception is ReflectionTypeLoadException ex) { - ModuleManager.Log("Intercepted a ReflectionTypeLoadException. List of broken DLLs:"); - var assemblies = ex.Types.Where(x => x != null).Select(x => x.Assembly).Distinct(); - foreach (Assembly assembly in assemblies) + string message = "Intercepted a ReflectionTypeLoadException. List of broken DLLs:\n"; + try { - if (Warnings == "") + var assemblies = ex.Types.Where(x => x != null).Select(x => x.Assembly).Distinct(); + foreach (Assembly assembly in assemblies) { - Warnings = "ModuleManager mod(s) DLL that are not compatible with this version of KSP\n"; + if (Warnings == "") + { + Warnings = "Mod(s) DLL that are not compatible with this version of KSP\n"; + } + string modInfo = assembly.GetName().Name + " " + assembly.GetName().Version + " " + + assembly.Location.Remove(0, gamePathLength) + "\n"; + if (!brokenAssemblies.Contains(assembly)) + { + brokenAssemblies.Add(assembly); + Warnings += modInfo; + } + message += modInfo; } - if (!brokenAssemblies.Contains(assembly)) - { - brokenAssemblies.Add(assembly); - Warnings += assembly.GetName().Name + " " + assembly.GetName().Version + " " + assembly.Location.Remove(0, gamePathLength) + "\n"; - } - ModuleManager.Log(assembly.GetName().Name + " " + assembly.GetName().Version + " " + assembly.Location.Remove(0, gamePathLength)); } + catch (Exception e) + { + message += "Exception " + e.GetType().Name + " while handling the exception..."; + } + ModuleManager.Log(message); } - baseLogHandler.LogException(exception, context); } } } From 51cdc23933193823a45e711174bd4a9591593fd3 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Sat, 30 Nov 2019 15:47:17 +0100 Subject: [PATCH 113/140] v4.1.3 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index 58db010d..cf5d1a0c 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.1.2")] +[assembly: AssemblyVersion("4.1.3")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 4b7319c9da475e6d7598723151264b9de7112e06 Mon Sep 17 00:00:00 2001 From: Sarbian Date: Sat, 30 Nov 2019 15:54:06 +0100 Subject: [PATCH 114/140] Start the Handler earlier --- ModuleManager/ModuleManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 4b8689ba..ac452b97 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -83,6 +83,8 @@ internal void Awake() } totalTime.Start(); + + interceptLogHandler = new InterceptLogHandler(); // Allow loading the background in the loading screen Application.runInBackground = true; @@ -199,7 +201,6 @@ private void Start() } } } - interceptLogHandler = new InterceptLogHandler(); } private TextMeshProUGUI CreateTextObject(Canvas canvas, string name) From 16ca42a586661ba24be3086dfa73e75386752081 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 8 Nov 2019 22:20:00 -0800 Subject: [PATCH 115/140] Put both projects on .NET 4.7.1 --- ModuleManager/ModuleManager.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index d256b235..d58d8d14 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -9,7 +9,7 @@ Library ModuleManager ModuleManager - v4.7 + v4.7.1 From 8be22e017db270080393ad6e4cc7e0846b5816d1 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 8 Nov 2019 22:20:11 -0800 Subject: [PATCH 116/140] Add UnityEngine.CoreModule to test project --- ModuleManagerTests/ModuleManagerTests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 9824ace1..b4fafe96 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -60,6 +60,7 @@ + ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll True From 35b02dff99eea2d83c6315c264bc46534fa9b9fc Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 8 Nov 2019 22:21:45 -0800 Subject: [PATCH 117/140] Use multi-argument Path.Combine now available in .NET 4 --- ModuleManager/FilePathRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ModuleManager/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs index 4ba34321..db15419e 100644 --- a/ModuleManager/FilePathRepository.cs +++ b/ModuleManager/FilePathRepository.cs @@ -5,7 +5,7 @@ namespace ModuleManager { internal static class FilePathRepository { - internal static readonly string cachePath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigCache"); + internal static readonly string cachePath = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "ModuleManager.ConfigCache"); internal static readonly string techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree"); internal static readonly string techTreePath = Path.Combine(KSPUtil.ApplicationRootPath, techTreeFile); @@ -16,9 +16,9 @@ internal static class FilePathRepository internal static readonly string partDatabasePath = Path.Combine(KSPUtil.ApplicationRootPath, "PartDatabase.cfg"); - internal static readonly string shaPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "GameData"), "ModuleManager.ConfigSHA"); + internal static readonly string shaPath = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "ModuleManager.ConfigSHA"); - internal static readonly string logsDirPath = Path.Combine(Path.Combine(KSPUtil.ApplicationRootPath, "Logs"), "ModuleManager"); + internal static readonly string logsDirPath = Path.Combine(KSPUtil.ApplicationRootPath, "Logs", "ModuleManager"); internal static readonly string logPath = Path.Combine(logsDirPath, "ModuleManager.log"); internal static readonly string patchLogPath = Path.Combine(logsDirPath, "MMPatch.log"); } From 51290e18ab97ab990bffbe3f3886310d500530d8 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Sat, 9 Nov 2019 21:12:39 -0800 Subject: [PATCH 118/140] Adress a bunch of messages Delete unused stuff, make stuff readonly, simplify stuff, add suppressions to legitimate things --- ModuleManager/MMPatchLoader.cs | 20 +++++++------- ModuleManager/ModuleManager.cs | 48 ++++++++++++++-------------------- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 41e28bd3..1cffe055 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -40,7 +40,7 @@ public class MMPatchLoader private static readonly KeyValueCache regexCache = new KeyValueCache(); private string configSha; - private Dictionary filesSha = new Dictionary(); + private readonly Dictionary filesSha = new Dictionary(); private const int STATUS_UPDATE_INVERVAL_MS = 33; @@ -89,12 +89,10 @@ public IEnumerable Run() QueueLogRunner logRunner = new QueueLogRunner(patchLogQueue); ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate { - using (StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create))) - { - streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); - logRunner.Run(streamLogger); - streamLogger.Info("Done!"); - } + using StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create)); + streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); + logRunner.Run(streamLogger); + streamLogger.Info("Done!"); }); IBasicLogger patchLogger = new LogSplitter(logger, new QueueLogger(patchLogQueue)); @@ -284,8 +282,8 @@ private bool IsCacheUpToDate() Stopwatch sw = new Stopwatch(); sw.Start(); - System.Security.Cryptography.SHA256 sha = System.Security.Cryptography.SHA256.Create(); - System.Security.Cryptography.SHA256 filesha = System.Security.Cryptography.SHA256.Create(); + using System.Security.Cryptography.SHA256 sha = System.Security.Cryptography.SHA256.Create(); + using System.Security.Cryptography.SHA256 filesha = System.Security.Cryptography.SHA256.Create(); UrlDir.UrlFile[] files = GameDatabase.Instance.root.AllConfigFiles.ToArray(); filesSha.Clear(); @@ -570,7 +568,7 @@ private void StatusUpdate(IPatchProgress progress, string activity = null) #region Applying Patches // Name is group 1, index is group 2, vector related filed is group 3, vector separator is group 4, operator is group 5 - private static Regex parseValue = new Regex(@"([\w\&\-\.\?\*+/^!\(\) ]+(?:,[^*\d][\w\&\-\.\?\*\(\) ]*)*)(?:,(-?[0-9\*]+))?(?:\[((?:[0-9\*]+)+)(?:,(.))?\])?"); + private static readonly Regex parseValue = new Regex(@"([\w\&\-\.\?\*+/^!\(\) ]+(?:,[^*\d][\w\&\-\.\?\*\(\) ]*)*)(?:,(-?[0-9\*]+))?(?:\[((?:[0-9\*]+)+)(?:,(.))?\])?"); // ModifyNode applies the ConfigNode mod as a 'patch' to ConfigNode original, then returns the patched ConfigNode. // it uses FindConfigNodeIn(src, nodeType, nodeName, nodeTag) to recurse. @@ -1401,7 +1399,7 @@ private static string ProcessVariableSearch(string value, NodeStack nodeStack, P StringBuilder builder = new StringBuilder(); builder.Append(split[0].Substring(1)); - for (int i = 1; i < split.Length - 1; i = i + 2) + for (int i = 1; i < split.Length - 1; i += 2) { ConfigNode.Value result = RecurseVariableSearch(split[i], nodeStack, context); if (result == null || result.value == null) diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index ac452b97..361cb540 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -23,12 +24,8 @@ public class ModuleManager : MonoBehaviour private bool inRnDCenter; public bool showUI = false; - - private Rect windowPos = new Rect(80f, 60f, 240f, 40f); private float textPos = 0; - private string version = ""; - //private Texture2D tex; //private Texture2D tex2; @@ -62,7 +59,7 @@ public static void Log(String s) print("[ModuleManager] " + s); } - private Stopwatch totalTime = new Stopwatch(); + private readonly Stopwatch totalTime = new Stopwatch(); internal void Awake() { @@ -107,9 +104,6 @@ internal void Awake() } DontDestroyOnLoad(gameObject); - Version v = Assembly.GetExecutingAssembly().GetName().Version; - version = v.Major + "." + v.Minor + "." + v.Build; - // Subscribe to the RnD center spawn/deSpawn events GameEvents.onGUIRnDComplexSpawn.Add(OnRnDCenterSpawn); GameEvents.onGUIRnDComplexDespawn.Add(OnRnDCenterDeSpawn); @@ -168,7 +162,7 @@ internal void Awake() private TextMeshProUGUI errors; private TextMeshProUGUI warning; - + [SuppressMessage("Code Quality", "IDE0051", Justification = "Called by Unity")] private void Start() { if (nCats) @@ -296,7 +290,7 @@ internal void Update() { warning.text = InterceptLogHandler.Warnings; h = warning.text.Length > 0 ? warning.textBounds.size.y : 0; - offsetY = offsetY + h; + offsetY += h; warning.rectTransform.localPosition = new Vector3(0, offsetY); } @@ -304,7 +298,7 @@ internal void Update() { status.text = patchRunner.Status; h = status.text.Length > 0 ? status.textBounds.size.y : 0; - offsetY = offsetY + h; + offsetY += h; status.transform.localPosition = new Vector3(0, offsetY); } @@ -312,7 +306,7 @@ internal void Update() { errors.text = patchRunner.Errors; h = errors.text.Length > 0 ? errors.textBounds.size.y : 0; - offsetY = offsetY + h; + offsetY += h; errors.transform.localPosition = new Vector3(0, offsetY); } } @@ -470,7 +464,7 @@ public static void OutputAllConfigs() Log("Exception while cleaning the export dir\n" + unauthorizedAccessException); } - void WriteDirectoryRecursive(UrlDir currentDir, string dirPath) + static void WriteDirectoryRecursive(UrlDir currentDir, string dirPath) { if (currentDir.files.Count > 0) Directory.CreateDirectory(dirPath); @@ -483,24 +477,22 @@ void WriteDirectoryRecursive(UrlDir currentDir, string dirPath) bool first = true; - using (FileStream stream = new FileStream(filePath, FileMode.Create)) - using (StreamWriter writer = new StreamWriter(stream)) + using FileStream stream = new FileStream(filePath, FileMode.Create); + using StreamWriter writer = new StreamWriter(stream); + foreach (UrlDir.UrlConfig urlConfig in urlFile.configs) { - foreach (UrlDir.UrlConfig urlConfig in urlFile.configs) + try { - try - { - if (first) first = false; - else writer.Write("\n"); + if (first) first = false; + else writer.Write("\n"); - ConfigNode copy = urlConfig.config.DeepCopy(); - copy.EscapeValuesRecursive(); - writer.Write(copy.ToString()); - } - catch (Exception e) - { - Log("Exception while trying to write the file " + filePath + "\n" + e); - } + ConfigNode copy = urlConfig.config.DeepCopy(); + copy.EscapeValuesRecursive(); + writer.Write(copy.ToString()); + } + catch (Exception e) + { + Log("Exception while trying to write the file " + filePath + "\n" + e); } } } From a2fdef4c38b6aeb066b7f23c212e357eedeacb3d Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Sun, 5 Jul 2020 23:22:43 -0700 Subject: [PATCH 119/140] Don't overwrite other fatal messages --- ModuleManager/MMPatchRunner.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index 739ecd82..b812ceed 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -80,12 +80,14 @@ public IEnumerator Run() { kspLogger.Exception("The patching thread threw an exception", patchingThreadStatus.Exception); FatalErrorHandler.HandleFatalError("The patching thread threw an exception"); + yield break; } if (loggingThreadStatus.IsExitedWithError) { kspLogger.Exception("The logging thread threw an exception", loggingThreadStatus.Exception); FatalErrorHandler.HandleFatalError("The logging thread threw an exception"); + yield break; } if (databaseConfigs == null) From c4ad2c3dd5defcdd37154831be4fa7040d3d1616 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Sun, 5 Jul 2020 23:23:45 -0700 Subject: [PATCH 120/140] Fix :LAST when mod doesn't exist Per the original feature design (#96) it should still run --- ModuleManager/PatchList.cs | 13 +++++++++---- ModuleManagerTests/PatchListTest.cs | 16 +++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index 9716de22..e9193f74 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -76,6 +76,7 @@ public ModPassCollection(IEnumerable modList) private readonly Pass finalPatches = new Pass(":FINAL"); private readonly ModPassCollection modPasses; + private readonly SortedDictionary lastPasses = new SortedDictionary(StringComparer.InvariantCultureIgnoreCase); public PatchList(IEnumerable modList, IEnumerable patches, IPatchProgress progress) { @@ -114,8 +115,12 @@ public PatchList(IEnumerable modList, IEnumerable patches, IPatc } else if (patch.PassSpecifier is LastPassSpecifier lastPassSpecifier) { - EnsureMod(lastPassSpecifier.mod); - modPasses[lastPassSpecifier.mod].AddLastPatch(patch); + if (!lastPasses.TryGetValue(lastPassSpecifier.mod, out Pass thisPass)) + { + thisPass = new Pass($":LAST[{lastPassSpecifier.mod.ToUpperInvariant()}]"); + lastPasses.Add(lastPassSpecifier.mod.ToLowerInvariant(), thisPass); + } + thisPass.Add(patch); } else if (patch.PassSpecifier is FinalPassSpecifier) { @@ -143,9 +148,9 @@ public IEnumerator GetEnumerator() yield return modPass.afterPass; } - foreach (ModPass modPass in modPasses) + foreach (Pass lastPass in lastPasses.Values) { - yield return modPass.lastPass; + yield return lastPass; } yield return finalPatches; diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index 09b1c1dc..4dc5c751 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -113,6 +113,7 @@ public void Test__Lifecycle() Substitute.For(), Substitute.For(), Substitute.For(), + Substitute.For(), }; UrlDir.UrlConfig urlConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE")); @@ -139,8 +140,9 @@ public void Test__Lifecycle() patches[19].PassSpecifier.Returns(new AfterPassSpecifier("MOD2", urlConfig)); patches[20].PassSpecifier.Returns(new LastPassSpecifier("mod2")); patches[21].PassSpecifier.Returns(new LastPassSpecifier("MOD2")); - patches[22].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[22].PassSpecifier.Returns(new LastPassSpecifier("mod3")); patches[23].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[24].PassSpecifier.Returns(new FinalPassSpecifier()); patches[00].CountsAsPatch.Returns(false); patches[01].CountsAsPatch.Returns(false); @@ -166,6 +168,7 @@ public void Test__Lifecycle() patches[21].CountsAsPatch.Returns(true); patches[22].CountsAsPatch.Returns(true); patches[23].CountsAsPatch.Returns(true); + patches[24].CountsAsPatch.Returns(true); IPatchProgress progress = Substitute.For(); @@ -173,7 +176,7 @@ public void Test__Lifecycle() IPass[] passes = patchList.ToArray(); - Assert.Equal(12, passes.Length); + Assert.Equal(13, passes.Length); Assert.Equal(":INSERT (initial)", passes[0].Name); Assert.Equal(new[] { patches[0], patches[1] }, passes[0]); @@ -208,10 +211,13 @@ public void Test__Lifecycle() Assert.Equal(":LAST[MOD2]", passes[10].Name); Assert.Equal(new[] { patches[20], patches[21] }, passes[10]); - Assert.Equal(":FINAL", passes[11].Name); - Assert.Equal(new[] { patches[22], patches[23] }, passes[11]); + Assert.Equal(":LAST[MOD3]", passes[11].Name); + Assert.Equal(new[] { patches[22] }, passes[11]); + + Assert.Equal(":FINAL", passes[12].Name); + Assert.Equal(new[] { patches[23], patches[24] }, passes[12]); - progress.Received(22).PatchAdded(); + progress.Received(23).PatchAdded(); } } } From 8a95d3709f0dd651deb1fc8294007b847e562045 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Mon, 6 Jul 2020 22:55:39 -0700 Subject: [PATCH 121/140] PatchList handles sorting consistently eliminates private class that mostly just passed methods through --- ModuleManager/PatchList.cs | 46 ++++++++------------------------------ 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index e9193f74..f97c8b49 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -37,53 +37,25 @@ public ModPass(string name) public void AddLastPatch(IPatch patch) => lastPass.Add(patch ?? throw new ArgumentNullException(nameof(patch))); } - private class ModPassCollection : IEnumerable - { - private readonly ModPass[] passesArray; - private readonly Dictionary passesDict; - - public ModPassCollection(IEnumerable modList) - { - int count = modList.Count(); - passesArray = new ModPass[count]; - passesDict = new Dictionary(count); - - int i = 0; - foreach (string mod in modList) - { - ModPass pass = new ModPass(mod); - passesArray[i] = pass; - passesDict.Add(mod.ToLowerInvariant(), pass); - i++; - } - } - - public ModPass this[string name] => passesDict[name.ToLowerInvariant()]; - public ModPass this[int index] => passesArray[index]; - - public bool HasMod(string name) => passesDict.ContainsKey(name.ToLowerInvariant()); - - public int Count => passesArray.Length; - - public ArrayEnumerator GetEnumerator() => new ArrayEnumerator(passesArray); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - private readonly Pass insertPatches = new Pass(":INSERT (initial)"); private readonly Pass firstPatches = new Pass(":FIRST"); private readonly Pass legacyPatches = new Pass(":LEGACY (default)"); private readonly Pass finalPatches = new Pass(":FINAL"); - private readonly ModPassCollection modPasses; + private readonly SortedDictionary modPasses = new SortedDictionary(StringComparer.InvariantCultureIgnoreCase); private readonly SortedDictionary lastPasses = new SortedDictionary(StringComparer.InvariantCultureIgnoreCase); public PatchList(IEnumerable modList, IEnumerable patches, IPatchProgress progress) { - modPasses = new ModPassCollection(modList ?? throw new ArgumentNullException(nameof(modList))); + if (modList == null) throw new ArgumentNullException(nameof(modList)); if (patches == null) throw new ArgumentNullException(nameof(patches)); if (progress == null) throw new ArgumentNullException(nameof(progress)); + foreach (string mod in modList) + { + modPasses.Add(mod, new ModPass(mod)); + } + foreach (IPatch patch in patches) { if (patch.PassSpecifier is InsertPassSpecifier) @@ -141,7 +113,7 @@ public IEnumerator GetEnumerator() yield return firstPatches; yield return legacyPatches; - foreach (ModPass modPass in modPasses) + foreach (ModPass modPass in modPasses.Values) { yield return modPass.beforePass; yield return modPass.forPass; @@ -162,7 +134,7 @@ private void EnsureMod(string mod) { if (mod == null) throw new ArgumentNullException(nameof(mod)); if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); - if (!modPasses.HasMod(mod)) throw new KeyNotFoundException($"Mod '{mod}' not found"); + if (!modPasses.ContainsKey(mod)) throw new KeyNotFoundException($"Mod '{mod}' not found"); } } } From 5168b5c743371838de869745081fda8849c86eb0 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Mon, 6 Jul 2020 23:52:39 -0700 Subject: [PATCH 122/140] Address some messages Do some dependency in jection on InterceptLogHandler, no need to hold onto a reference to it as Unity will Remove paramter checks in StreamLogger as StreamWriter does the same checks Ibut keep basic tests for them) --- ModuleManager/Cats/CatAnimator.cs | 4 +++- ModuleManager/Cats/CatManager.cs | 2 +- ModuleManager/Cats/CatMover.cs | 8 ++++--- ModuleManager/Cats/CatOrbiter.cs | 8 ++++--- ModuleManager/Collections/ImmutableStack.cs | 2 +- .../ExceptionIntercept/InterceptLogHandler.cs | 5 ++-- ModuleManager/Extensions/StringExtensions.cs | 2 +- ModuleManager/Fix16.cs | 2 ++ ModuleManager/Logging/StreamLogger.cs | 3 --- ModuleManager/Logging/UnityLogger.cs | 2 +- ModuleManager/MMPatchLoader.cs | 2 -- ModuleManager/MMPatchRunner.cs | 13 ++++------- ModuleManager/ModuleManager.cs | 6 ++--- ModuleManager/NeedsChecker.cs | 2 ++ ModuleManager/NodeMatcher.cs | 8 +++---- ModuleManager/PatchExtractor.cs | 2 ++ ModuleManager/PatchList.cs | 2 -- ModuleManager/Patches/PatchCompiler.cs | 23 ++++++------------- ModuleManager/PostPatchLoader.cs | 2 ++ ModuleManager/Progress/PatchProgress.cs | 2 +- ModuleManager/Threading/TaskStatus.cs | 22 ++++++++---------- ModuleManager/Threading/TaskStatusWrapper.cs | 2 +- ModuleManager/Utils/FileUtils.cs | 12 +++------- .../Collections/MessageQueueTest.cs | 2 +- .../Extensions/IBasicLoggerExtensionsTest.cs | 8 +------ .../Logging/PrefixLoggerTest.cs | 5 ++-- ModuleManagerTests/Logging/QueueLoggerTest.cs | 5 ++-- .../Logging/StreamLoggerTest.cs | 9 ++------ ModuleManagerTests/Logging/UnityLoggerTest.cs | 5 ++-- ModuleManagerTests/NeedsCheckerTest.cs | 11 +++------ ModuleManagerTests/PassTest.cs | 8 ------- ModuleManagerTests/PatchExtractorTest.cs | 6 ----- .../Progress/PatchProgressTest.cs | 6 ++--- 33 files changed, 73 insertions(+), 128 deletions(-) diff --git a/ModuleManager/Cats/CatAnimator.cs b/ModuleManager/Cats/CatAnimator.cs index ed5b2c70..7806d4aa 100644 --- a/ModuleManager/Cats/CatAnimator.cs +++ b/ModuleManager/Cats/CatAnimator.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using UnityEngine; namespace ModuleManager.Cats @@ -12,9 +13,10 @@ class CatAnimator : MonoBehaviour private SpriteRenderer spriteRenderer; private int spriteIdx; + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] void Start() { - spriteRenderer = this.GetComponent(); + spriteRenderer = GetComponent(); spriteRenderer.sortingOrder = 3; StartCoroutine(Animate()); } diff --git a/ModuleManager/Cats/CatManager.cs b/ModuleManager/Cats/CatManager.cs index 744a0ca8..05f671d6 100644 --- a/ModuleManager/Cats/CatManager.cs +++ b/ModuleManager/Cats/CatManager.cs @@ -14,7 +14,7 @@ public static void LaunchCat() InitCats(); GameObject cat = LaunchCat(scale); - CatMover catMover = cat.AddComponent(); + cat.AddComponent(); } public static void LaunchCats() diff --git a/ModuleManager/Cats/CatMover.cs b/ModuleManager/Cats/CatMover.cs index 2e37ac9a..d97a3f7c 100644 --- a/ModuleManager/Cats/CatMover.cs +++ b/ModuleManager/Cats/CatMover.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Diagnostics.CodeAnalysis; using UnityEngine; namespace ModuleManager.Cats @@ -24,12 +24,13 @@ public class CatMover : MonoBehaviour private bool clearTrail = false; // Use this for initialization + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] void Start() { - trail = this.GetComponent(); + trail = GetComponent(); trail.sortingOrder = 2; - spriteRenderer = this.GetComponent(); + spriteRenderer = GetComponent(); offsetY = Mathf.FloorToInt(0.2f * Screen.height); @@ -41,6 +42,7 @@ void Start() clearTrail = true; } + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] void Update() { if (trail.time <= 0f) diff --git a/ModuleManager/Cats/CatOrbiter.cs b/ModuleManager/Cats/CatOrbiter.cs index 17621c6f..34b1f6e8 100644 --- a/ModuleManager/Cats/CatOrbiter.cs +++ b/ModuleManager/Cats/CatOrbiter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using KSP.UI; using UnityEngine; using Random = UnityEngine.Random; @@ -8,7 +9,7 @@ namespace ModuleManager.Cats { class CatOrbiter : MonoBehaviour { - private static List orbiters = new List(); + private static readonly List orbiters = new List(); private static CatOrbiter sun; @@ -20,7 +21,7 @@ class CatOrbiter : MonoBehaviour private Vector2d force; private float scale = 1; - private double G = 6.67408E-11; + private const double G = 6.67408E-11; public double Mass { @@ -113,13 +114,14 @@ private void DoForces() } } - + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] void OnDestroy() { orbiters.Remove(this); TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Earlyish, DoForces); } + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] void FixedUpdate() { //if (this == sun) diff --git a/ModuleManager/Collections/ImmutableStack.cs b/ModuleManager/Collections/ImmutableStack.cs index 5d181567..d710c423 100644 --- a/ModuleManager/Collections/ImmutableStack.cs +++ b/ModuleManager/Collections/ImmutableStack.cs @@ -8,7 +8,7 @@ public class ImmutableStack : IEnumerable { public struct Enumerator : IEnumerator { - private ImmutableStack head; + private readonly ImmutableStack head; private ImmutableStack currentStack; public Enumerator(ImmutableStack stack) diff --git a/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs index 6cd3dcd0..471b1fa8 100644 --- a/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs +++ b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs @@ -16,10 +16,9 @@ class InterceptLogHandler : ILogHandler public static string Warnings { get; private set; } = ""; - public InterceptLogHandler() + public InterceptLogHandler(ILogHandler baseLogHandler) { - baseLogHandler = Debug.unityLogger.logHandler; - Debug.unityLogger.logHandler = this; + this.baseLogHandler = baseLogHandler ?? throw new ArgumentNullException(nameof(baseLogHandler)); gamePathLength = Path.GetFullPath(KSPUtil.ApplicationRootPath).Length; } diff --git a/ModuleManager/Extensions/StringExtensions.cs b/ModuleManager/Extensions/StringExtensions.cs index c4190045..b9136e30 100644 --- a/ModuleManager/Extensions/StringExtensions.cs +++ b/ModuleManager/Extensions/StringExtensions.cs @@ -18,7 +18,7 @@ public static bool IsBracketBalanced(this string s) return level == 0; } - private static Regex whitespaceRegex = new Regex(@"\s+"); + private static readonly Regex whitespaceRegex = new Regex(@"\s+"); public static string RemoveWS(this string withWhite) { diff --git a/ModuleManager/Fix16.cs b/ModuleManager/Fix16.cs index ab20e6ae..b567eb56 100644 --- a/ModuleManager/Fix16.cs +++ b/ModuleManager/Fix16.cs @@ -1,9 +1,11 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; namespace ModuleManager { class Fix16 : LoadingSystem { + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] private void Awake() { if (Instance != null) diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs index 74115d3e..d020f64c 100644 --- a/ModuleManager/Logging/StreamLogger.cs +++ b/ModuleManager/Logging/StreamLogger.cs @@ -5,14 +5,11 @@ namespace ModuleManager.Logging { public sealed class StreamLogger : IBasicLogger, IDisposable { - private readonly Stream stream; private readonly StreamWriter streamWriter; private bool disposed = false; public StreamLogger(Stream stream) { - this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); - if (!stream.CanWrite) throw new ArgumentException("must be writable", nameof(stream)); streamWriter = new StreamWriter(stream); } diff --git a/ModuleManager/Logging/UnityLogger.cs b/ModuleManager/Logging/UnityLogger.cs index b31c1003..d51063c4 100644 --- a/ModuleManager/Logging/UnityLogger.cs +++ b/ModuleManager/Logging/UnityLogger.cs @@ -5,7 +5,7 @@ namespace ModuleManager.Logging { public class UnityLogger : IBasicLogger { - private ILogger logger; + private readonly ILogger logger; public UnityLogger(ILogger logger) { diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 1cffe055..d90e29a1 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -8,8 +8,6 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using UnityEngine; -using Debug = UnityEngine.Debug; using ModuleManager.Collections; using ModuleManager.Logging; diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs index b812ceed..6c3d63a0 100644 --- a/ModuleManager/MMPatchRunner.cs +++ b/ModuleManager/MMPatchRunner.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using UnityEngine; using ModuleManager.Collections; using ModuleManager.Extensions; using ModuleManager.Logging; @@ -14,8 +13,6 @@ namespace ModuleManager { public class MMPatchRunner { - private const float TIME_TO_WAIT_FOR_LOGS = 0.05f; - private readonly IBasicLogger kspLogger; public string Status { get; private set; } = ""; @@ -38,12 +35,10 @@ public IEnumerator Run() QueueLogRunner logRunner = new QueueLogRunner(mmLogQueue); ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate { - using (StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create))) - { - streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); - logRunner.Run(streamLogger); - streamLogger.Info("Done!"); - } + using StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create)); + streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); + logRunner.Run(streamLogger); + streamLogger.Info("Done!"); }); // Wait for game database to be initialized for the 2nd time and wait for any plugins to initialize diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 361cb540..dd5e04e5 100644 --- a/ModuleManager/ModuleManager.cs +++ b/ModuleManager/ModuleManager.cs @@ -38,8 +38,6 @@ public class ModuleManager : MonoBehaviour private MMPatchRunner patchRunner; - private InterceptLogHandler interceptLogHandler; - #endregion state private static bool loadedInScene; @@ -80,8 +78,8 @@ internal void Awake() } totalTime.Start(); - - interceptLogHandler = new InterceptLogHandler(); + + Debug.unityLogger.logHandler = new InterceptLogHandler(Debug.unityLogger.logHandler); // Allow loading the background in the loading screen Application.runInBackground = true; diff --git a/ModuleManager/NeedsChecker.cs b/ModuleManager/NeedsChecker.cs index 7e452642..94f2ad29 100644 --- a/ModuleManager/NeedsChecker.cs +++ b/ModuleManager/NeedsChecker.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using ModuleManager.Extensions; using ModuleManager.Logging; @@ -20,6 +21,7 @@ public class NeedsChecker : INeedsChecker private readonly IEnumerable mods; private readonly UrlDir gameData; private readonly IPatchProgress progress; + [SuppressMessage("CodeQuality", "IDE0052", Justification = "Reserved for future use")] private readonly IBasicLogger logger; public NeedsChecker(IEnumerable mods, UrlDir gameData, IPatchProgress progress, IBasicLogger logger) diff --git a/ModuleManager/NodeMatcher.cs b/ModuleManager/NodeMatcher.cs index 5c3a59ca..8777de97 100644 --- a/ModuleManager/NodeMatcher.cs +++ b/ModuleManager/NodeMatcher.cs @@ -10,11 +10,9 @@ public interface INodeMatcher public class NodeMatcher : INodeMatcher { - private static readonly char[] sep = { '[', ']' }; - - private string type; - private string[] namePatterns = null; - private string constraints = ""; + private readonly string type; + private readonly string[] namePatterns = null; + private readonly string constraints = ""; public NodeMatcher(string type, string name, string constraints) { diff --git a/ModuleManager/PatchExtractor.cs b/ModuleManager/PatchExtractor.cs index cf476a64..6c8765f4 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using ModuleManager.Extensions; using ModuleManager.Logging; using ModuleManager.Patches; @@ -10,6 +11,7 @@ namespace ModuleManager public class PatchExtractor { private readonly IPatchProgress progress; + [SuppressMessage("CodeQuality", "IDE0052", Justification = "Reserved for future use")] private readonly IBasicLogger logger; private readonly INeedsChecker needsChecker; private readonly ITagListParser tagListParser; diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index f97c8b49..d3990540 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -1,8 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using ModuleManager.Collections; using ModuleManager.Patches; using ModuleManager.Patches.PassSpecifiers; using ModuleManager.Progress; diff --git a/ModuleManager/Patches/PatchCompiler.cs b/ModuleManager/Patches/PatchCompiler.cs index 67387372..6c7e7f30 100644 --- a/ModuleManager/Patches/PatchCompiler.cs +++ b/ModuleManager/Patches/PatchCompiler.cs @@ -13,23 +13,14 @@ public IPatch CompilePatch(ProtoPatch protoPatch) { if (protoPatch == null) throw new ArgumentNullException(nameof(protoPatch)); - switch (protoPatch.command) + return protoPatch.command switch { - case Command.Insert: - return new InsertPatch(protoPatch.urlConfig, protoPatch.nodeType, protoPatch.passSpecifier); - - case Command.Edit: - return new EditPatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier); - - case Command.Copy: - return new CopyPatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier); - - case Command.Delete: - return new DeletePatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier); - - default: - throw new ArgumentException("has an invalid command for a root node: " + protoPatch.command, nameof(protoPatch)); - } + Command.Insert => new InsertPatch(protoPatch.urlConfig, protoPatch.nodeType, protoPatch.passSpecifier), + Command.Edit => new EditPatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier), + Command.Copy => new CopyPatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier), + Command.Delete => new DeletePatch(protoPatch.urlConfig, new NodeMatcher(protoPatch.nodeType, protoPatch.nodeName, protoPatch.has), protoPatch.passSpecifier), + _ => throw new ArgumentException("has an invalid command for a root node: " + protoPatch.command, nameof(protoPatch)), + }; } } } diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index f92f1fad..619f09d0 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -9,6 +9,7 @@ using ModuleManager.Logging; using static ModuleManager.FilePathRepository; +using System.Diagnostics.CodeAnalysis; namespace ModuleManager { @@ -34,6 +35,7 @@ public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) postPatchCallbacks.Add(callback); } + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] private void Awake() { if (Instance != null) diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index 3cebf994..61fd0d33 100644 --- a/ModuleManager/Progress/PatchProgress.cs +++ b/ModuleManager/Progress/PatchProgress.cs @@ -8,7 +8,7 @@ public class PatchProgress : IPatchProgress { public ProgressCounter Counter { get; private set; } - private IBasicLogger logger; + private readonly IBasicLogger logger; public float ProgressFraction { diff --git a/ModuleManager/Threading/TaskStatus.cs b/ModuleManager/Threading/TaskStatus.cs index 92f8c237..1d2c6b67 100644 --- a/ModuleManager/Threading/TaskStatus.cs +++ b/ModuleManager/Threading/TaskStatus.cs @@ -4,12 +4,10 @@ namespace ModuleManager.Threading { public class TaskStatus : ITaskStatus { - private bool isRunning = true; - private Exception exception = null; - private object lockObject = new object(); + private readonly object lockObject = new object(); - public bool IsRunning => isRunning; - public Exception Exception => exception; + public bool IsRunning { get; private set; } = true; + public Exception Exception { get; private set; } = null; public bool IsFinished { @@ -17,7 +15,7 @@ public bool IsFinished { lock (lockObject) { - return !isRunning && exception == null; + return !IsRunning && Exception == null; } } } @@ -28,7 +26,7 @@ public bool IsExitedWithError { lock (lockObject) { - return !isRunning && exception != null; + return !IsRunning && Exception != null; } } } @@ -37,8 +35,8 @@ public void Finished() { lock (lockObject) { - if (!isRunning) throw new InvalidOperationException("Task is not running"); - isRunning = false; + if (!IsRunning) throw new InvalidOperationException("Task is not running"); + IsRunning = false; } } @@ -46,9 +44,9 @@ public void Error(Exception exception) { lock(lockObject) { - if (!isRunning) throw new InvalidOperationException("Task is not running"); - this.exception = exception ?? throw new ArgumentNullException(nameof(exception)); - isRunning = false; + if (!IsRunning) throw new InvalidOperationException("Task is not running"); + this.Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + IsRunning = false; } } } diff --git a/ModuleManager/Threading/TaskStatusWrapper.cs b/ModuleManager/Threading/TaskStatusWrapper.cs index eff24f78..1750633c 100644 --- a/ModuleManager/Threading/TaskStatusWrapper.cs +++ b/ModuleManager/Threading/TaskStatusWrapper.cs @@ -4,7 +4,7 @@ namespace ModuleManager.Threading { public class TaskStatusWrapper : ITaskStatus { - private ITaskStatus inner; + private readonly ITaskStatus inner; public TaskStatusWrapper(ITaskStatus inner) { diff --git a/ModuleManager/Utils/FileUtils.cs b/ModuleManager/Utils/FileUtils.cs index e069e14c..818d3cc6 100644 --- a/ModuleManager/Utils/FileUtils.cs +++ b/ModuleManager/Utils/FileUtils.cs @@ -11,15 +11,9 @@ public static string FileSHA(string filename) { if (!File.Exists(filename)) throw new FileNotFoundException("File does not exist", filename); - byte[] data = null; - - using (SHA256 sha = SHA256.Create()) - { - using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read)) - { - data = sha.ComputeHash(fs); - } - } + using SHA256 sha = SHA256.Create(); + using FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read); + byte[] data = sha.ComputeHash(fs); return data.ToHex(); } diff --git a/ModuleManagerTests/Collections/MessageQueueTest.cs b/ModuleManagerTests/Collections/MessageQueueTest.cs index 83bf1217..baf35d20 100644 --- a/ModuleManagerTests/Collections/MessageQueueTest.cs +++ b/ModuleManagerTests/Collections/MessageQueueTest.cs @@ -8,7 +8,7 @@ public class MessageQueueTest { private class TestClass { } - private MessageQueue queue = new MessageQueue(); + private readonly MessageQueue queue = new MessageQueue(); [Fact] public void Test__Empty() diff --git a/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs b/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs index f6c1fad7..74e82d88 100644 --- a/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs @@ -1,7 +1,6 @@ using System; using Xunit; using NSubstitute; -using UnityEngine; using ModuleManager.Logging; using ModuleManager.Extensions; @@ -9,12 +8,7 @@ namespace ModuleManagerTests.Extensions { public class IBasicLoggerExtensionsTest { - private IBasicLogger logger; - - public IBasicLoggerExtensionsTest() - { - logger = Substitute.For(); - } + private readonly IBasicLogger logger = Substitute.For(); [Fact] public void TestInfo() diff --git a/ModuleManagerTests/Logging/PrefixLoggerTest.cs b/ModuleManagerTests/Logging/PrefixLoggerTest.cs index 28a82edc..5aaf0113 100644 --- a/ModuleManagerTests/Logging/PrefixLoggerTest.cs +++ b/ModuleManagerTests/Logging/PrefixLoggerTest.cs @@ -8,12 +8,11 @@ namespace ModuleManagerTests.Logging { public class PrefixLoggerTest { - private IBasicLogger innerLogger; - private PrefixLogger logger; + private readonly IBasicLogger innerLogger = Substitute.For(); + private readonly PrefixLogger logger; public PrefixLoggerTest() { - innerLogger = Substitute.For(); logger = new PrefixLogger("MyMod", innerLogger); } diff --git a/ModuleManagerTests/Logging/QueueLoggerTest.cs b/ModuleManagerTests/Logging/QueueLoggerTest.cs index dc8a1dfd..25e1abbe 100644 --- a/ModuleManagerTests/Logging/QueueLoggerTest.cs +++ b/ModuleManagerTests/Logging/QueueLoggerTest.cs @@ -8,12 +8,11 @@ namespace ModuleManagerTests.Logging { public class QueueLoggerTest { - private IMessageQueue queue; - private QueueLogger logger; + private readonly IMessageQueue queue = Substitute.For>(); + private readonly QueueLogger logger; public QueueLoggerTest() { - queue = Substitute.For>(); logger = new QueueLogger(queue); } diff --git a/ModuleManagerTests/Logging/StreamLoggerTest.cs b/ModuleManagerTests/Logging/StreamLoggerTest.cs index bc613038..296eeac2 100644 --- a/ModuleManagerTests/Logging/StreamLoggerTest.cs +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -11,12 +11,10 @@ public class StreamLoggerTest [Fact] public void TestConstructor__StreamNull() { - ArgumentNullException ex = Assert.Throws(delegate + Assert.Throws(delegate { new StreamLogger(null); }); - - Assert.Equal("stream", ex.ParamName); } [Fact] @@ -24,13 +22,10 @@ public void TestConstructor__CantWrite() { using (MemoryStream stream = new MemoryStream(new byte[0], false)) { - ArgumentException ex = Assert.Throws(delegate + Assert.Throws(delegate { new StreamLogger(stream); }); - - Assert.Equal("stream", ex.ParamName); - Assert.Contains("must be writable", ex.Message); } } diff --git a/ModuleManagerTests/Logging/UnityLoggerTest.cs b/ModuleManagerTests/Logging/UnityLoggerTest.cs index 5d820c25..7c4dc4a1 100644 --- a/ModuleManagerTests/Logging/UnityLoggerTest.cs +++ b/ModuleManagerTests/Logging/UnityLoggerTest.cs @@ -9,12 +9,11 @@ namespace ModuleManagerTests.Logging { public class UnityLoggerTest { - private ILogger innerLogger; - private UnityLogger logger; + private readonly ILogger innerLogger = Substitute.For(); + private readonly UnityLogger logger; public UnityLoggerTest() { - innerLogger = Substitute.For(); logger = new UnityLogger(innerLogger); } diff --git a/ModuleManagerTests/NeedsCheckerTest.cs b/ModuleManagerTests/NeedsCheckerTest.cs index d0414a0b..4d3540b9 100644 --- a/ModuleManagerTests/NeedsCheckerTest.cs +++ b/ModuleManagerTests/NeedsCheckerTest.cs @@ -5,7 +5,6 @@ using ModuleManager; using ModuleManager.Logging; using ModuleManager.Progress; -using NodeStack = ModuleManager.Collections.ImmutableStack; namespace ModuleManagerTests { @@ -13,17 +12,13 @@ public class NeedsCheckerTest { private readonly UrlDir gameData; - private readonly IPatchProgress progress; - private readonly IBasicLogger logger; - private NeedsChecker needsChecker; + private readonly IPatchProgress progress = Substitute.For(); + private readonly IBasicLogger logger = Substitute.For(); + private readonly NeedsChecker needsChecker; public NeedsCheckerTest() { gameData = UrlBuilder.CreateGameData(); - - progress = Substitute.For(); - logger = Substitute.For(); - needsChecker = new NeedsChecker(new[] { "mod1", "mod2", "mod/2" }, gameData, progress, logger); } diff --git a/ModuleManagerTests/PassTest.cs b/ModuleManagerTests/PassTest.cs index d2cce882..bc829816 100644 --- a/ModuleManagerTests/PassTest.cs +++ b/ModuleManagerTests/PassTest.cs @@ -2,7 +2,6 @@ using System.Linq; using Xunit; using NSubstitute; -using TestUtils; using ModuleManager; using ModuleManager.Patches; @@ -10,13 +9,6 @@ namespace ModuleManagerTests { public class PassTest { - private UrlDir.UrlFile file; - - public PassTest() - { - file = UrlBuilder.CreateFile("abc/def.cfg"); - } - [Fact] public void TestConstructor__NameNull() { diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 4106486e..49a12583 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Xunit; using NSubstitute; using TestUtils; @@ -386,10 +385,5 @@ private void AssertNoErrors() progress.DidNotReceiveWithAnyArgs().Exception(null, null); progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); } - - private void AssertConfigNodesEqual(ConfigNode expected, ConfigNode observed) - { - Assert.Equal(expected.ToString(), observed.ToString()); - } } } diff --git a/ModuleManagerTests/Progress/PatchProgressTest.cs b/ModuleManagerTests/Progress/PatchProgressTest.cs index 8cde86c2..c78964e3 100644 --- a/ModuleManagerTests/Progress/PatchProgressTest.cs +++ b/ModuleManagerTests/Progress/PatchProgressTest.cs @@ -1,7 +1,6 @@ using System; using Xunit; using NSubstitute; -using UnityEngine; using TestUtils; using ModuleManager; using ModuleManager.Logging; @@ -11,12 +10,11 @@ namespace ModuleManagerTests { public class PatchProgressTest { - private IBasicLogger logger; - private PatchProgress progress; + private readonly IBasicLogger logger = Substitute.For(); + private readonly PatchProgress progress; public PatchProgressTest() { - logger = Substitute.For(); progress = new PatchProgress(logger); } From e0da6ba13bca4dea7181cd009b49691d9fd47df0 Mon Sep 17 00:00:00 2001 From: sarbian Date: Tue, 7 Jul 2020 11:42:00 +0200 Subject: [PATCH 123/140] v4.1.4 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index cf5d1a0c..fba8c4bf 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.1.3")] +[assembly: AssemblyVersion("4.1.4")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 0261dc6dac6b3340357a02c8322d21770be9c09c Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Wed, 30 Sep 2020 22:15:15 -0700 Subject: [PATCH 124/140] Normalize KSP root path KSP makes it weird Addresses confusion in #164 --- ModuleManager/FilePathRepository.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ModuleManager/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs index db15419e..0d1448d7 100644 --- a/ModuleManager/FilePathRepository.cs +++ b/ModuleManager/FilePathRepository.cs @@ -5,20 +5,21 @@ namespace ModuleManager { internal static class FilePathRepository { - internal static readonly string cachePath = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "ModuleManager.ConfigCache"); + internal static readonly string normalizedRootPath = Path.GetFullPath(KSPUtil.ApplicationRootPath); + internal static readonly string cachePath = Path.Combine(normalizedRootPath, "GameData", "ModuleManager.ConfigCache"); internal static readonly string techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree"); - internal static readonly string techTreePath = Path.Combine(KSPUtil.ApplicationRootPath, techTreeFile); + internal static readonly string techTreePath = Path.Combine(normalizedRootPath, techTreeFile); internal static readonly string physicsFile = Path.Combine("GameData", "ModuleManager.Physics"); - internal static readonly string physicsPath = Path.Combine(KSPUtil.ApplicationRootPath, physicsFile); - internal static readonly string defaultPhysicsPath = Path.Combine(KSPUtil.ApplicationRootPath, "Physics.cfg"); + internal static readonly string physicsPath = Path.Combine(normalizedRootPath, physicsFile); + internal static readonly string defaultPhysicsPath = Path.Combine(normalizedRootPath, "Physics.cfg"); - internal static readonly string partDatabasePath = Path.Combine(KSPUtil.ApplicationRootPath, "PartDatabase.cfg"); + internal static readonly string partDatabasePath = Path.Combine(normalizedRootPath, "PartDatabase.cfg"); - internal static readonly string shaPath = Path.Combine(KSPUtil.ApplicationRootPath, "GameData", "ModuleManager.ConfigSHA"); + internal static readonly string shaPath = Path.Combine(normalizedRootPath, "GameData", "ModuleManager.ConfigSHA"); - internal static readonly string logsDirPath = Path.Combine(KSPUtil.ApplicationRootPath, "Logs", "ModuleManager"); + internal static readonly string logsDirPath = Path.Combine(normalizedRootPath, "Logs", "ModuleManager"); internal static readonly string logPath = Path.Combine(logsDirPath, "ModuleManager.log"); internal static readonly string patchLogPath = Path.Combine(logsDirPath, "MMPatch.log"); } From f6669bf7b7a9a2f0cb818ee757b3d2ed870898bb Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Thu, 14 Jan 2021 23:41:11 -0800 Subject: [PATCH 125/140] Set modded physics and reload earlier Do it in post patch, this allows the part loader to pick up changes (e.g. rigidbody min mass) --- ModuleManager/CustomConfigsManager.cs | 10 ---------- ModuleManager/PostPatchLoader.cs | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/ModuleManager/CustomConfigsManager.cs b/ModuleManager/CustomConfigsManager.cs index e2b8689c..d7444363 100644 --- a/ModuleManager/CustomConfigsManager.cs +++ b/ModuleManager/CustomConfigsManager.cs @@ -16,16 +16,6 @@ internal void Start() Log("Setting modded tech tree as the active one"); HighLogic.CurrentGame.Parameters.Career.TechTreeUrl = techTreeFile; } - - if (PhysicsGlobals.PhysicsDatabaseFilename != physicsFile && File.Exists(physicsPath)) - { - Log("Setting modded physics as the active one"); - - PhysicsGlobals.PhysicsDatabaseFilename = physicsFile; - - if (!PhysicsGlobals.Instance.LoadDatabase()) - Log("Something went wrong while setting the active physics config."); - } } public static void Log(String s) diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index 619f09d0..fa1b9df6 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -130,6 +130,8 @@ private IEnumerator Run() logger.Info("Reloading Part Upgrades"); PartUpgradeManager.Handler.FillUpgrades(); + LoadModdedPhysics(); + yield return null; progressTitle = "ModuleManager: Running post patch callbacks"; @@ -209,5 +211,23 @@ private IEnumerator Run() ready = true; } + + private void LoadModdedPhysics() + { + if (PhysicsGlobals.PhysicsDatabaseFilename == physicsFile) return; + + if (!File.Exists(physicsPath)) + { + logger.Error("Physics file not found"); + return; + } + + logger.Info("Setting modded physics as the active one"); + + PhysicsGlobals.PhysicsDatabaseFilename = physicsFile; + + if (!PhysicsGlobals.Instance.LoadDatabase()) + logger.Error("Something went wrong while setting the active physics config."); + } } } From 983b330b259547f05d1ff2ff10bc5828153c7883 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Thu, 14 Jan 2021 23:52:56 -0800 Subject: [PATCH 126/140] mark dependencies as copy local false prevents them from showing up in the target directory --- ModuleManager/ModuleManager.csproj | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index d58d8d14..4556194b 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -121,33 +121,42 @@ False + False False + False False + False False + False False + False False + False False + False False + False False + False From b3d5ad47526c43b6d175c775cec77b967055cae7 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Thu, 14 Jan 2021 23:56:16 -0800 Subject: [PATCH 127/140] fix remaining .NET 3.5 nuget packages upgrade visual studio runner --- ModuleManagerTests/ModuleManagerTests.csproj | 8 ++++---- ModuleManagerTests/packages.config | 4 ++-- TestUtilsTests/TestUtilsTests.csproj | 8 ++++---- TestUtilsTests/packages.config | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index b4fafe96..7e311b3b 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -1,8 +1,8 @@  - + - + Debug @@ -156,10 +156,10 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + \ No newline at end of file diff --git a/ModuleManagerTests/packages.config b/ModuleManagerTests/packages.config index dc5635be..63061ffe 100644 --- a/ModuleManagerTests/packages.config +++ b/ModuleManagerTests/packages.config @@ -11,6 +11,6 @@ - - + + \ No newline at end of file diff --git a/TestUtilsTests/TestUtilsTests.csproj b/TestUtilsTests/TestUtilsTests.csproj index 619e6bb4..1d3ff25e 100644 --- a/TestUtilsTests/TestUtilsTests.csproj +++ b/TestUtilsTests/TestUtilsTests.csproj @@ -1,8 +1,8 @@  - + - + Debug @@ -86,10 +86,10 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + \ No newline at end of file diff --git a/TestUtilsTests/packages.config b/TestUtilsTests/packages.config index bbb4cb82..8cf69e4f 100644 --- a/TestUtilsTests/packages.config +++ b/TestUtilsTests/packages.config @@ -7,6 +7,6 @@ - - + + \ No newline at end of file From 6a32ebb8213c2d7f8bca1d3532b15dcced12a598 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Thu, 14 Jan 2021 23:57:18 -0800 Subject: [PATCH 128/140] Update remaining NuGet packages --- ModuleManagerTests/ModuleManagerTests.csproj | 14 +++++++------- ModuleManagerTests/app.config | 4 ++-- ModuleManagerTests/packages.config | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 7e311b3b..1a82db26 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -40,20 +40,20 @@ - ..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll + ..\packages\Castle.Core.4.4.1\lib\net45\Castle.Core.dll - - ..\packages\NSubstitute.3.1.0\lib\net46\NSubstitute.dll + + ..\packages\NSubstitute.4.2.2\lib\net46\NSubstitute.dll - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll - - ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll diff --git a/ModuleManagerTests/app.config b/ModuleManagerTests/app.config index 1d152980..fe205877 100644 --- a/ModuleManagerTests/app.config +++ b/ModuleManagerTests/app.config @@ -4,11 +4,11 @@ - + - + diff --git a/ModuleManagerTests/packages.config b/ModuleManagerTests/packages.config index 63061ffe..b159434d 100644 --- a/ModuleManagerTests/packages.config +++ b/ModuleManagerTests/packages.config @@ -1,9 +1,9 @@  - - - - + + + + From da38958f48b34bff2052169e5f84885ec2b7daa1 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 15 Jan 2021 00:04:06 -0800 Subject: [PATCH 129/140] Add specific language markers to all project files seems to get confused otherwise --- ModuleManager/ModuleManager.csproj | 3 +++ ModuleManagerTests/ModuleManagerTests.csproj | 3 +++ TestUtils/TestUtils.csproj | 3 +++ TestUtilsTests/TestUtilsTests.csproj | 3 +++ 4 files changed, 12 insertions(+) diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 4556194b..20fe0da1 100644 --- a/ModuleManager/ModuleManager.csproj +++ b/ModuleManager/ModuleManager.csproj @@ -33,6 +33,9 @@ False false + + 8.0 + diff --git a/ModuleManagerTests/ModuleManagerTests.csproj b/ModuleManagerTests/ModuleManagerTests.csproj index 1a82db26..f3afbbf5 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -37,6 +37,9 @@ 4 false + + 8.0 + diff --git a/TestUtils/TestUtils.csproj b/TestUtils/TestUtils.csproj index 10073bd0..5c53b27c 100644 --- a/TestUtils/TestUtils.csproj +++ b/TestUtils/TestUtils.csproj @@ -32,6 +32,9 @@ 4 false + + 8.0 + diff --git a/TestUtilsTests/TestUtilsTests.csproj b/TestUtilsTests/TestUtilsTests.csproj index 1d3ff25e..cfe71bf2 100644 --- a/TestUtilsTests/TestUtilsTests.csproj +++ b/TestUtilsTests/TestUtilsTests.csproj @@ -37,6 +37,9 @@ 4 false + + 8.0 + From 838677db5f2d2c17c1ce653489a3caa974110009 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 22 Jan 2021 22:34:18 -0800 Subject: [PATCH 130/140] Ensure string comparison is culture invariant And get rid of message suppresions related to it --- ModuleManager/Extensions/StringExtensions.cs | 9 +++++ ModuleManager/MMPatchLoader.cs | 35 ++++++++----------- .../Extensions/StringExtensionsTest.cs | 30 ++++++++++++++++ 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/ModuleManager/Extensions/StringExtensions.cs b/ModuleManager/Extensions/StringExtensions.cs index b9136e30..634fab7a 100644 --- a/ModuleManager/Extensions/StringExtensions.cs +++ b/ModuleManager/Extensions/StringExtensions.cs @@ -24,5 +24,14 @@ public static string RemoveWS(this string withWhite) { return whitespaceRegex.Replace(withWhite, ""); } + + public static bool Contains(this string str, string value, out int index) + { + if (str == null) throw new ArgumentNullException(nameof(str)); + if (value == null) throw new ArgumentNullException(nameof(value)); + + index = str.IndexOf(value, StringComparison.CurrentCultureIgnoreCase); + return index != -1; + } } } diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index d90e29a1..61d993a3 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -22,8 +22,6 @@ namespace ModuleManager { - [SuppressMessage("ReSharper", "StringLastIndexOfIsCultureSpecific.1")] - [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] public class MMPatchLoader { private const string PHYSICS_NODE_NAME = "PHYSICSGLOBALS"; @@ -922,7 +920,7 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon ConfigNode newSubMod = new ConfigNode(toPaste.name); newSubMod = ModifyNode(nodeStack.Push(newSubMod), toPaste, context); - if (subName.LastIndexOf(",") > 0 && int.TryParse(subName.Substring(subName.LastIndexOf(",") + 1), out int index)) + if (subName.LastIndexOf(',') > 0 && int.TryParse(subName.Substring(subName.LastIndexOf(',') + 1), out int index)) { // In this case insert the node at position index InsertNode(newNode, newSubMod, index); @@ -945,11 +943,10 @@ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchCon // NODE,n will match the nth node (NODE is the same as NODE,0) // NODE,* will match ALL nodes // NODE:HAS[condition] will match ALL nodes with condition - if (subName.Contains(":HAS[")) + if (subName.Contains(":HAS[", out int hasStart)) { - int start = subName.IndexOf(":HAS["); - constraints = subName.Substring(start + 5, subName.LastIndexOf(']') - start - 5); - subName = subName.Substring(0, start); + constraints = subName.Substring(hasStart + 5, subName.LastIndexOf(']') - hasStart - 5); + subName = subName.Substring(0, hasStart); } if (subName.Contains(",")) @@ -1117,11 +1114,10 @@ private static ConfigNode RecurseNodeSearch(string path, NodeStack nodeStack, Pa string constraint = ""; int index = 0; - if (subName.Contains(":HAS[")) + if (subName.Contains(":HAS[", out int hasStart)) { - int start = subName.IndexOf(":HAS["); - constraint = subName.Substring(start + 5, subName.LastIndexOf(']') - start - 5); - subName = subName.Substring(0, start); + constraint = subName.Substring(hasStart + 5, subName.LastIndexOf(']') - hasStart - 5); + subName = subName.Substring(0, hasStart); } else if (subName.Contains(",")) { @@ -1290,13 +1286,12 @@ private static ConfigNode.Value RecurseVariableSearch(string path, NodeStack nod string constraint = ""; string nodeType, nodeName; int index = 0; - if (subName.Contains(":HAS[")) + if (subName.Contains(":HAS[", out int hasStart)) { - int start = subName.IndexOf(":HAS["); - constraint = subName.Substring(start + 5, subName.LastIndexOf(']') - start - 5); - subName = subName.Substring(0, start); + constraint = subName.Substring(hasStart + 5, subName.LastIndexOf(']') - hasStart - 5); + subName = subName.Substring(0, hasStart); } - else if (subName.Contains(",")) + else if (subName.Contains(',')) { string tag = subName.Split(',')[1]; subName = subName.Split(',')[0]; @@ -1551,11 +1546,11 @@ public static bool CheckConstraints(ConfigNode node, string constraints) constraints = constraintList[0]; string remainingConstraints = ""; - if (constraints.Contains("HAS[")) + if (constraints.Contains(":HAS[", out int hasStart)) { - int start = constraints.IndexOf("HAS[") + 4; - remainingConstraints = constraints.Substring(start, constraintList[0].LastIndexOf(']') - start); - constraints = constraints.Substring(0, start - 5); + hasStart += 4; + remainingConstraints = constraints.Substring(hasStart, constraintList[0].LastIndexOf(']') - hasStart); + constraints = constraints.Substring(0, hasStart - 5); } string[] splits = constraints.Split(contraintSeparators, 3); diff --git a/ModuleManagerTests/Extensions/StringExtensionsTest.cs b/ModuleManagerTests/Extensions/StringExtensionsTest.cs index fdf3b33f..acf9274e 100644 --- a/ModuleManagerTests/Extensions/StringExtensionsTest.cs +++ b/ModuleManagerTests/Extensions/StringExtensionsTest.cs @@ -39,5 +39,35 @@ public void TestRemoveWS() { Assert.Equal("abcdef", " abc \tdef\r\n\t ".RemoveWS()); } + + + [InlineData("abc", "b", true, 1)] + [InlineData("abc", "x", false, -1)] + [Theory] + public void TestContains(string str, string test, bool expectedResult, int expectedIndex) + { + bool result = str.Contains(test, out int index); + Assert.Equal(expectedResult, result); + Assert.Equal(expectedIndex, index); + } + + [Fact] + public void TestContains__NullStr() + { + string s = null; + Assert.Throws(delegate + { + s.Contains("x", out int _x); + }); + } + + [Fact] + public void TestContains__NullValue() + { + Assert.Throws(delegate + { + "abc".Contains(null, out int _x); + }); + } } } From c6163a2e495628e59822d2074da027e70aa85136 Mon Sep 17 00:00:00 2001 From: blowfishpro Date: Fri, 22 Jan 2021 22:36:24 -0800 Subject: [PATCH 131/140] Always replace physics On a database reload this will already be true but we still want physics reloading to happen --- ModuleManager/PostPatchLoader.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index fa1b9df6..d850e4e1 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -214,8 +214,6 @@ private IEnumerator Run() private void LoadModdedPhysics() { - if (PhysicsGlobals.PhysicsDatabaseFilename == physicsFile) return; - if (!File.Exists(physicsPath)) { logger.Error("Physics file not found"); From 3894ce10ac129611594f2524bf87fef27aafbd85 Mon Sep 17 00:00:00 2001 From: sarbian Date: Sun, 1 Aug 2021 10:42:05 +0200 Subject: [PATCH 132/140] v4.2.0 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index fba8c4bf..0cd2c63f 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.1.4")] +[assembly: AssemblyVersion("4.2.0")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From d9e9264fb2f04bb3e4f0026133bf5b110bd4296d Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 1 Aug 2021 11:51:32 -0400 Subject: [PATCH 133/140] Fix off-by-one string indexing in constraint checking Also change string comparison type to `StringComparison.Ordinal`, which should be the correct type according to https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings. --- ModuleManager/Extensions/StringExtensions.cs | 2 +- ModuleManager/MMPatchLoader.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ModuleManager/Extensions/StringExtensions.cs b/ModuleManager/Extensions/StringExtensions.cs index 634fab7a..55720367 100644 --- a/ModuleManager/Extensions/StringExtensions.cs +++ b/ModuleManager/Extensions/StringExtensions.cs @@ -30,7 +30,7 @@ public static bool Contains(this string str, string value, out int index) if (str == null) throw new ArgumentNullException(nameof(str)); if (value == null) throw new ArgumentNullException(nameof(value)); - index = str.IndexOf(value, StringComparison.CurrentCultureIgnoreCase); + index = str.IndexOf(value, StringComparison.Ordinal); return index != -1; } } diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index 61d993a3..cf3418c3 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -1548,7 +1548,7 @@ public static bool CheckConstraints(ConfigNode node, string constraints) string remainingConstraints = ""; if (constraints.Contains(":HAS[", out int hasStart)) { - hasStart += 4; + hasStart += 5; remainingConstraints = constraints.Substring(hasStart, constraintList[0].LastIndexOf(']') - hasStart); constraints = constraints.Substring(0, hasStart - 5); } From c60e3c537a352de35b02d5fa79a1a6c712a83e9a Mon Sep 17 00:00:00 2001 From: Alvin Meng Date: Sun, 1 Aug 2021 15:08:13 -0400 Subject: [PATCH 134/140] Undo string comparison change. --- ModuleManager/Extensions/StringExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Extensions/StringExtensions.cs b/ModuleManager/Extensions/StringExtensions.cs index 55720367..634fab7a 100644 --- a/ModuleManager/Extensions/StringExtensions.cs +++ b/ModuleManager/Extensions/StringExtensions.cs @@ -30,7 +30,7 @@ public static bool Contains(this string str, string value, out int index) if (str == null) throw new ArgumentNullException(nameof(str)); if (value == null) throw new ArgumentNullException(nameof(value)); - index = str.IndexOf(value, StringComparison.Ordinal); + index = str.IndexOf(value, StringComparison.CurrentCultureIgnoreCase); return index != -1; } } From 86e60c36036c9fa6cd24d9abdc679f452059a8b2 Mon Sep 17 00:00:00 2001 From: sarbian Date: Sun, 1 Aug 2021 21:52:17 +0200 Subject: [PATCH 135/140] v4.2.1 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index 0cd2c63f..ef0b7100 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.2.0")] +[assembly: AssemblyVersion("4.2.1")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From ebe2895e3a55384fc8c39c9e0f964a5e39d3ede9 Mon Sep 17 00:00:00 2001 From: NathanKell Date: Thu, 16 Jun 2022 21:35:54 -0700 Subject: [PATCH 136/140] Support patching Localization tokens. * Support wildcards in nodetype matching so you can do @*,* {} * Support # in value names since loc names start with # * Tell Localizer to reload the language after MM finishes --- ModuleManager/MMPatchLoader.cs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index cf3418c3..5278758b 100644 --- a/ModuleManager/MMPatchLoader.cs +++ b/ModuleManager/MMPatchLoader.cs @@ -231,6 +231,9 @@ public IEnumerable Run() } } + if (KSP.Localization.Localizer.Instance != null) + KSP.Localization.Localizer.SwitchToLanguage(KSP.Localization.Localizer.CurrentLanguage); + logger.Info(status + "\n" + errors); patchSw.Stop(); @@ -564,7 +567,7 @@ private void StatusUpdate(IPatchProgress progress, string activity = null) #region Applying Patches // Name is group 1, index is group 2, vector related filed is group 3, vector separator is group 4, operator is group 5 - private static readonly Regex parseValue = new Regex(@"([\w\&\-\.\?\*+/^!\(\) ]+(?:,[^*\d][\w\&\-\.\?\*\(\) ]*)*)(?:,(-?[0-9\*]+))?(?:\[((?:[0-9\*]+)+)(?:,(.))?\])?"); + private static readonly Regex parseValue = new Regex(@"([\w\&\-\.\?\*\#+/^!\(\) ]+(?:,[^*\d][\w\&\-\.\?\*\(\) ]*)*)(?:,(-?[0-9\*]+))?(?:\[((?:[0-9\*]+)+)(?:,(.))?\])?"); // ModifyNode applies the ConfigNode mod as a 'patch' to ConfigNode original, then returns the patched ConfigNode. // it uses FindConfigNodeIn(src, nodeType, nodeName, nodeTag) to recurse. @@ -1712,19 +1715,26 @@ public static ConfigNode FindConfigNodeIn( string nodeName = null, int index = 0) { - ConfigNode[] nodes = src.GetNodes(nodeType); - if (nodes.Length == 0) + List nodes = new List(); + int c = src.nodes.Count; + for(int i = 0; i < c; ++i) + { + if (WildcardMatch(src.nodes[i].name, nodeType)) + nodes.Add(src.nodes[i]); + } + int nodeCount = nodes.Count; + if (nodeCount == 0) return null; if (nodeName == null) { if (index >= 0) - return nodes[Math.Min(index, nodes.Length - 1)]; - return nodes[Math.Max(0, nodes.Length + index)]; + return nodes[Math.Min(index, nodeCount - 1)]; + return nodes[Math.Max(0, nodeCount + index)]; } ConfigNode last = null; if (index >= 0) { - for (int i = 0; i < nodes.Length; ++i) + for (int i = 0; i < nodeCount; ++i) { if (nodes[i].HasValue("name") && WildcardMatch(nodes[i].GetValue("name"), nodeName)) { @@ -1735,7 +1745,7 @@ public static ConfigNode FindConfigNodeIn( } return last; } - for (int i = nodes.Length - 1; i >= 0; --i) + for (int i = nodeCount - 1; i >= 0; --i) { if (nodes[i].HasValue("name") && WildcardMatch(nodes[i].GetValue("name"), nodeName)) { From a303e0a7118f3506cc1546b70b03acc04263a6b7 Mon Sep 17 00:00:00 2001 From: sarbian Date: Sat, 18 Jun 2022 20:22:56 +0200 Subject: [PATCH 137/140] v4.2.2 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index ef0b7100..e4e038f0 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.2.1")] +[assembly: AssemblyVersion("4.2.2")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, From 2ba651e41aebba1758e57b679f01bb8592b2d536 Mon Sep 17 00:00:00 2001 From: Jonathan Bayer Date: Sun, 14 May 2023 18:19:50 -0400 Subject: [PATCH 138/140] Added , FileShare.ReadWrite to the File.Open, to allow MM to read files which are already opened by KSP --- ModuleManager/Utils/FileUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Utils/FileUtils.cs b/ModuleManager/Utils/FileUtils.cs index 818d3cc6..fcf04482 100644 --- a/ModuleManager/Utils/FileUtils.cs +++ b/ModuleManager/Utils/FileUtils.cs @@ -12,7 +12,7 @@ public static string FileSHA(string filename) if (!File.Exists(filename)) throw new FileNotFoundException("File does not exist", filename); using SHA256 sha = SHA256.Create(); - using FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read); + using FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); byte[] data = sha.ComputeHash(fs); return data.ToHex(); From b22136a8302b592626533f864413cb7b37a32126 Mon Sep 17 00:00:00 2001 From: siimav Date: Fri, 19 May 2023 23:51:58 +0300 Subject: [PATCH 139/140] Fix invalid modded physics cfg path being fed to KSP when using the faulty PDLauncher workaround --- ModuleManager/PostPatchLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs index d850e4e1..1bccce78 100644 --- a/ModuleManager/PostPatchLoader.cs +++ b/ModuleManager/PostPatchLoader.cs @@ -222,7 +222,7 @@ private void LoadModdedPhysics() logger.Info("Setting modded physics as the active one"); - PhysicsGlobals.PhysicsDatabaseFilename = physicsFile; + PhysicsGlobals.PhysicsDatabaseFilename = physicsPath; if (!PhysicsGlobals.Instance.LoadDatabase()) logger.Error("Something went wrong while setting the active physics config."); From c4561925f983e7ae81d9dfd4d11356a35cb6b9b6 Mon Sep 17 00:00:00 2001 From: sarbian Date: Mon, 3 Jul 2023 20:01:38 +0200 Subject: [PATCH 140/140] v4.2.3 --- ModuleManager/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index e4e038f0..12a660e1 100644 --- a/ModuleManager/Properties/AssemblyInfo.cs +++ b/ModuleManager/Properties/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("4.2.2")] +[assembly: AssemblyVersion("4.2.3")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly,