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 0567de54..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() @@ -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..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 @@ -21,13 +21,16 @@ public class CatMover : MonoBehaviour private const float time = 5; private const float trailTime = time / 4; + 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); @@ -36,8 +39,10 @@ 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; } + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] void Update() { if (trail.time <= 0f) @@ -50,7 +55,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 +67,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/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/ArrayEnumerator.cs b/ModuleManager/Collections/ArrayEnumerator.cs index d002b2ad..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(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/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/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/Collections/MessageQueue.cs b/ModuleManager/Collections/MessageQueue.cs index 9f1bc864..b6038a77 100644 --- a/ModuleManager/Collections/MessageQueue.cs +++ b/ModuleManager/Collections/MessageQueue.cs @@ -4,14 +4,15 @@ namespace ModuleManager.Collections { - public interface IMessageQueue + public interface IMessageQueue : IEnumerable { void Add(T value); + IMessageQueue TakeAll(); } public class MessageQueue : IMessageQueue, IEnumerable { - public class Enumerator : IEnumerator + public sealed class Enumerator : IEnumerator { private readonly MessageQueue queue; private Node current; @@ -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/CustomConfigsManager.cs b/ModuleManager/CustomConfigsManager.cs index d88f4ea9..d7444363 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,20 +11,10 @@ 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; - } - - if (PhysicsGlobals.PhysicsDatabaseFilename != MMPatchLoader.physicsFile && File.Exists(MMPatchLoader.physicsPath)) - { - Log("Setting modded physics as the active one"); - - PhysicsGlobals.PhysicsDatabaseFilename = MMPatchLoader.physicsFile; - - if (!PhysicsGlobals.Instance.LoadDatabase()) - Log("Something went wrong while setting the active physics config."); + HighLogic.CurrentGame.Parameters.Career.TechTreeUrl = techTreeFile; } } diff --git a/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs new file mode 100644 index 00000000..471b1fa8 --- /dev/null +++ b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs @@ -0,0 +1,64 @@ +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(ILogHandler baseLogHandler) + { + this.baseLogHandler = baseLogHandler ?? throw new ArgumentNullException(nameof(baseLogHandler)); + 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) + { + string message = "Intercepted a ReflectionTypeLoadException. List of broken DLLs:\n"; + try + { + var assemblies = ex.Types.Where(x => x != null).Select(x => x.Assembly).Distinct(); + foreach (Assembly assembly in assemblies) + { + 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; + } + } + catch (Exception e) + { + message += "Exception " + e.GetType().Name + " while handling the exception..."; + } + ModuleManager.Log(message); + } + } + } +} 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/Extensions/ConfigNodeExtensions.cs b/ModuleManager/Extensions/ConfigNodeExtensions.cs index 6940048a..9f552566 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.AddValueSafe(value.name, value.value); foreach (ConfigNode node in from.nodes) { ConfigNode newNode = DeepCopy(node); @@ -68,5 +68,40 @@ 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)); + } + + 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/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/Extensions/StringExtensions.cs b/ModuleManager/Extensions/StringExtensions.cs index c4190045..634fab7a 100644 --- a/ModuleManager/Extensions/StringExtensions.cs +++ b/ModuleManager/Extensions/StringExtensions.cs @@ -18,11 +18,20 @@ 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) { 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/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/Extensions/UrlFileExtensions.cs b/ModuleManager/Extensions/UrlFileExtensions.cs new file mode 100644 index 00000000..3d203b24 --- /dev/null +++ b/ModuleManager/Extensions/UrlFileExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace ModuleManager.Extensions +{ + public static class UrlFileExtensions + { + 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/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs new file mode 100644 index 00000000..0d1448d7 --- /dev/null +++ b/ModuleManager/FilePathRepository.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; + +namespace ModuleManager +{ + internal static class FilePathRepository + { + 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(normalizedRootPath, techTreeFile); + + internal static readonly string physicsFile = Path.Combine("GameData", "ModuleManager.Physics"); + 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(normalizedRootPath, "PartDatabase.cfg"); + + internal static readonly string shaPath = Path.Combine(normalizedRootPath, "GameData", "ModuleManager.ConfigSHA"); + + 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"); + } +} diff --git a/ModuleManager/Fix16.cs b/ModuleManager/Fix16.cs new file mode 100644 index 00000000..b567eb56 --- /dev/null +++ b/ModuleManager/Fix16.cs @@ -0,0 +1,83 @@ +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) + { + 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/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..bf85f488 100644 --- a/ModuleManager/Logging/ILogMessage.cs +++ b/ModuleManager/Logging/ILogMessage.cs @@ -1,9 +1,13 @@ using System; +using UnityEngine; namespace ModuleManager.Logging { public interface ILogMessage { - void LogTo(IBasicLogger logger); + LogType LogType { get; } + DateTime Timestamp { get; } + string Message { get; } + string ToLogString(); } } diff --git a/ModuleManager/Logging/LogMessage.cs b/ModuleManager/Logging/LogMessage.cs new file mode 100644 index 00000000..c6554aaf --- /dev/null +++ b/ModuleManager/Logging/LogMessage.cs @@ -0,0 +1,53 @@ +using System; +using UnityEngine; + +namespace ModuleManager.Logging +{ + public class LogMessage : ILogMessage + { + 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)); + } + + 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)); + } + + 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} {Timestamp.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 new file mode 100644 index 00000000..b1d2c89f --- /dev/null +++ b/ModuleManager/Logging/LogSplitter.cs @@ -0,0 +1,23 @@ +using System; + +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(ILogMessage message) + { + 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 deleted file mode 100644 index eb2d3699..00000000 --- a/ModuleManager/Logging/ModLogger.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using UnityEngine; - -namespace ModuleManager.Logging -{ - public class ModLogger : IBasicLogger - { - private string prefix; - private IBasicLogger logger; - - public ModLogger(string prefix, IBasicLogger logger) - { - if (string.IsNullOrEmpty(prefix)) throw new ArgumentNullException(nameof(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); - } -} 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/PrefixLogger.cs b/ModuleManager/Logging/PrefixLogger.cs new file mode 100644 index 00000000..410473fa --- /dev/null +++ b/ModuleManager/Logging/PrefixLogger.cs @@ -0,0 +1,23 @@ +using System; + +namespace ModuleManager.Logging +{ + public class PrefixLogger : IBasicLogger + { + private readonly string prefix; + private readonly IBasicLogger logger; + + public PrefixLogger(string prefix, IBasicLogger logger) + { + if (string.IsNullOrEmpty(prefix)) throw new ArgumentNullException(nameof(prefix)); + this.prefix = $"[{prefix}] "; + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void Log(ILogMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + logger.Log(new LogMessage(message, prefix + message.Message)); + } + } +} diff --git a/ModuleManager/Logging/QueueLogRunner.cs b/ModuleManager/Logging/QueueLogRunner.cs new file mode 100644 index 00000000..f78cf611 --- /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()) + { + logger.Log(message); + } + + long timeRemaining = timeToWaitForLogsMs - stopwatch.ElapsedMilliseconds; + if (timeRemaining > 0) + System.Threading.Thread.Sleep((int)timeRemaining); + + stopwatch.Reset(); + } + + foreach (ILogMessage message in logQueue.TakeAll()) + { + 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 new file mode 100644 index 00000000..d020f64c --- /dev/null +++ b/ModuleManager/Logging/StreamLogger.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; + +namespace ModuleManager.Logging +{ + public sealed class StreamLogger : IBasicLogger, IDisposable + { + private readonly StreamWriter streamWriter; + private bool disposed = false; + + public StreamLogger(Stream stream) + { + streamWriter = new StreamWriter(stream); + } + + public void Log(ILogMessage message) + { + if (disposed) throw new InvalidOperationException("Object has already been disposed"); + if (message == null) throw new ArgumentNullException(nameof(message)); + + streamWriter.WriteLine(message.ToLogString()); + } + + public void Dispose() + { + // Flushes and closes the StreamWriter and the underlying stream + streamWriter.Close(); + + disposed = true; + } + } +} diff --git a/ModuleManager/Logging/UnityLogger.cs b/ModuleManager/Logging/UnityLogger.cs index 0655b807..d51063c4 100644 --- a/ModuleManager/Logging/UnityLogger.cs +++ b/ModuleManager/Logging/UnityLogger.cs @@ -1,24 +1,21 @@ using System; using UnityEngine; -using ModuleManager.Extensions; namespace ModuleManager.Logging { public class UnityLogger : IBasicLogger { - private ILogger logger; + private readonly ILogger logger; 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/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs index adcf1999..5278758b 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; @@ -9,156 +8,94 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; -using UnityEngine; -using Debug = UnityEngine.Debug; +using ModuleManager.Collections; using ModuleManager.Logging; using ModuleManager.Extensions; -using ModuleManager.Collections; using ModuleManager.Threading; +using ModuleManager.Tags; +using ModuleManager.Patches; using ModuleManager.Progress; using NodeStack = ModuleManager.Collections.ImmutableStack; +using static ModuleManager.FilePathRepository; + namespace ModuleManager { - public delegate void ModuleManagerPostPatchCallback(); - - [SuppressMessage("ReSharper", "StringLastIndexOfIsCultureSpecific.1")] - [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - public class MMPatchLoader : LoadingSystem + public class MMPatchLoader { + private const string PHYSICS_NODE_NAME = "PHYSICSGLOBALS"; + private const string TECH_TREE_NODE_NAME = "TechTree"; + public string status = ""; public string errors = ""; public static bool keepPartDB = false; - private string activity = "Module Manager"; - - private static readonly Dictionary regexCache = new Dictionary(); - - private static string cachePath; - - internal static string techTreeFile; - internal static string techTreePath; - - internal static string physicsFile; - internal static string physicsPath; - private static string defaultPhysicsPath; - - internal static string partDatabasePath; - - private static string shaPath; - - private UrlDir.UrlFile physicsUrlFile; + private static readonly KeyValueCache regexCache = new KeyValueCache(); private string configSha; - private Dictionary filesSha = new Dictionary(); - - private bool useCache = false; + private readonly Dictionary filesSha = new Dictionary(); - private readonly Stopwatch patchSw = new Stopwatch(); + private const int STATUS_UPDATE_INVERVAL_MS = 33; - private static readonly List postPatchCallbacks = new List(); + private readonly IEnumerable modsAddedByAssemblies; + private readonly IBasicLogger logger; - private const float yieldInterval = 1f/30f; // Patch at ~30fps - - private IBasicLogger logger; - - private float progressFraction = 0; - - public static MMPatchLoader Instance { get; private set; } - - private void Awake() + public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) { - if (Instance != null) - { - DestroyImmediate(this); - return; - } - Instance = this; - DontDestroyOnLoad(gameObject); - - 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)); + PostPatchLoader.AddPostPatchCallback(callback); } - private bool ready; - - public override bool IsReady() + public MMPatchLoader(IEnumerable modsAddedByAssemblies, IBasicLogger logger) { - //return false; - if (ready) - { - patchSw.Stop(); - logger.Info("Ran in " + ((float)patchSw.ElapsedMilliseconds / 1000).ToString("F3") + "s"); - } - return ready; + this.modsAddedByAssemblies = modsAddedByAssemblies ?? throw new ArgumentNullException(nameof(modsAddedByAssemblies)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public override float ProgressFraction() => progressFraction; - - public override string ProgressTitle() + public IEnumerable Run() { - return activity; - } - - public override void StartLoad() - { - patchSw.Reset(); + Stopwatch patchSw = new Stopwatch(); patchSw.Start(); - ready = false; - - // DB check used to track the now fixed TextureReplacer corruption - //checkValues(); - - StartCoroutine(ProcessPatch()); - } - - public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) - { - if (!postPatchCallbacks.Contains(callback)) - postPatchCallbacks.Add(callback); - } - - 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 //useCache = false; #endif - yield return null; + + IEnumerable databaseConfigs = null; if (!useCache) { - IPatchProgress progress = new PatchProgress(logger); + if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath); + MessageQueue patchLogQueue = new MessageQueue(); + 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!"); + }); + 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); - - yield return null; + patchLogger.Info(status); + 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)) @@ -166,99 +103,69 @@ 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"; - logger.Info(status); + status = "Extracting patches"; + patchLogger.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, patchLogger); + ITagListParser tagListParser = new TagListParser(progress); + IProtoPatchBuilder protoPatchBuilder = new ProtoPatchBuilder(progress); + IPatchCompiler patchCompiler = new PatchCompiler(); + PatchExtractor extractor = new PatchExtractor(progress, patchLogger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); - PatchList patchList = PatchExtractor.SortAndExtractPatches(GameDatabase.Instance.root, mods, progress); + // Have to convert to an array because we will be removing patches + IEnumerable extractedPatches = + GameDatabase.Instance.root.AllConfigs.Select(urlConfig => extractor.ExtractPatch(urlConfig)).Where(patch => patch != null); + PatchList patchList = new PatchList(mods, extractedPatches, progress); #endregion #region Applying patches status = "Applying patches"; - logger.Info(status); - - yield return null; + patchLogger.Info(status); - 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); + IPass currentPass = null; - logger.Info("Starting patch thread"); - - ITaskStatus patchThread = BackgroundTask.Start(applier.ApplyPatches); + progress.OnPassStarted.Add(delegate (IPass pass) + { + currentPass = pass; + StatusUpdate(progress, currentPass.Name); + }); - float nextYield = Time.realtimeSinceStartup + yieldInterval; + System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); + stopwatch.Start(); - float updateTimeRemaining() + progress.OnPatchApplied.Add(delegate { - float timeRemaining = nextYield - Time.realtimeSinceStartup; + long timeRemaining = STATUS_UPDATE_INVERVAL_MS - stopwatch.ElapsedMilliseconds; if (timeRemaining < 0) { - nextYield = Time.realtimeSinceStartup + yieldInterval; - StatusUpdate(progress); - activity = applier.Activity; + StatusUpdate(progress, currentPass.Name); + stopwatch.Reset(); + stopwatch.Start(); } - return timeRemaining; - } + }); - while (patchThread.IsRunning) - { - foreach (ILogMessage message in logQueue.TakeAll()) - { - message.LogTo(logger); - - if (updateTimeRemaining() < 0) yield return null; - } - - float timeRemaining = updateTimeRemaining(); - if (timeRemaining > 0) System.Threading.Thread.Sleep((int)(timeRemaining * 1000)); - yield return null; - } + PatchApplier applier = new PatchApplier(progress, patchLogger); + databaseConfigs = applier.ApplyPatches(patchList); + stopwatch.Stop(); 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; - } - - logger.Info("Done patching"); - yield return null; - - PurgeUnused(); + patchLogger.Info("Done patching"); #endregion Applying patches #region Saving Cache + foreach (KeyValuePair item in progress.Counter.warningFiles) + { + patchLogger.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) @@ -267,7 +174,7 @@ float updateTimeRemaining() + "\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)) @@ -277,120 +184,62 @@ float updateTimeRemaining() } 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); - yield return null; - CreateCache(progress.Counter.patchedNodes); + patchLogger.Info(status); + CreateCache(databaseConfigs, progress.Counter.patchedNodes); } StatusUpdate(progress); #endregion Saving Cache - SaveModdedTechTree(); - SaveModdedPhysics(); - } - else - { - status = "Loading from Cache"; - logger.Info(status); - yield return null; - LoadCache(); - } - - 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(); + SaveModdedTechTree(databaseConfigs); + SaveModdedPhysics(databaseConfigs); - logger.Info("Reloading Part Upgrades"); - PartUpgradeManager.Handler.FillUpgrades(); + logRunner.RequestStop(); - foreach (ModuleManagerPostPatchCallback callback in postPatchCallbacks) - { - try - { - callback(); - } - catch (Exception e) + while (loggingThreadStatus.IsRunning) { - logger.Exception("Exception while running a post patch callback", e); + System.Threading.Thread.Sleep(100); } - yield return null; - } - yield return null; - // Call all "public static void ModuleManagerPostLoad()" on all class - foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies()) - { - try + if (loggingThreadStatus.IsExitedWithError) { - 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); + logger.Error("The patching thread threw an exception"); + throw loggingThreadStatus.Exception; } } - - yield return null; - - // Call "public void ModuleManagerPostLoad()" on all active MonoBehaviour instance - foreach (MonoBehaviour obj in FindObjectsOfType()) + else { - MethodInfo method = obj.GetType().GetMethod("ModuleManagerPostLoad", BindingFlags.Public | BindingFlags.Instance); + status = "Loading from Cache"; + logger.Info(status); + databaseConfigs = LoadCache(); - if (method != null && method.GetParameters().Length == 0) + if (File.Exists(patchLogPath)) { - 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); - } + 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); } } - yield return null; + if (KSP.Localization.Localizer.Instance != null) + KSP.Localization.Localizer.SwitchToLanguage(KSP.Localization.Localizer.CurrentLanguage); - if (ModuleManager.dumpPostPatch) - ModuleManager.OutputAllConfigs(); + logger.Info(status + "\n" + errors); - yield return null; + patchSw.Stop(); + logger.Info("Ran in " + ((float)patchSw.ElapsedMilliseconds / 1000).ToString("F3") + "s"); - ready = true; + return databaseConfigs; } private void LoadPhysicsConfig() @@ -398,61 +247,65 @@ 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 ConfigNode physicsContent = ConfigNode.Load(defaultPhysicsPath); - physicsContent.name = "PHYSICSGLOBALS"; + physicsContent.name = PHYSICS_NODE_NAME; physicsUrlFile.AddConfig(physicsContent); gameDataDir.files.Add(physicsUrlFile); } - - private void SaveModdedPhysics() + private void SaveModdedPhysics(IEnumerable databaseConfigs) { - List configs = physicsUrlFile.configs; + IEnumerable configs = databaseConfigs.Where(config => config.NodeType == PHYSICS_NODE_NAME); + int count = configs.Count(); - if (configs.Count == 0) + 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 (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} {PHYSICS_NODE_NAME} nodes found. A patch may be wrong. Using the first one"); } - configs[0].config.Save(physicsPath); + configs.First().Node.Save(physicsPath); } - private void IsCacheUpToDate() + 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(); + 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 byte[] contentBytes = File.ReadAllBytes(files[i].fullPath); 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); } } @@ -464,6 +317,12 @@ private void 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); @@ -476,7 +335,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); @@ -497,6 +356,7 @@ private void IsCacheUpToDate() logger.Info("useCache = " + useCache); } } + return useCache; } private bool CheckFilesChange(UrlDir.UrlFile[] files, ConfigNode shaConfigNode) @@ -505,16 +365,17 @@ 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); + 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; @@ -522,18 +383,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) @@ -551,9 +413,9 @@ private ConfigNode GetFileNode(ConfigNode shaConfigNode, string filename) } return null; } - - private void CreateCache(int patchedNodeCount) + + private void CreateCache(IEnumerable databaseConfigs, int patchedNodeCount) { ConfigNode shaConfigNode = new ConfigNode(); shaConfigNode.AddValue("SHA", configSha); @@ -565,24 +427,27 @@ 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.GetUrlWithExtension()); + + ConfigNode urlNode = urlConfig.Node.DeepCopy(); + urlNode.EscapeValuesRecursive(); + + node.AddNode(urlNode); } 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); } } @@ -624,71 +489,73 @@ private void CreateCache(int patchedNodeCount) } } - private void SaveModdedTechTree() + private void SaveModdedTechTree(IEnumerable databaseConfigs) { - UrlDir.UrlConfig[] configs = GameDatabase.Instance.GetConfigs("TechTree"); + IEnumerable configs = databaseConfigs.Where(config => config.NodeType == TECH_TREE_NODE_NAME); + int count = configs.Count(); - if (configs.Length == 0) + 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 (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} {TECH_TREE_NODE_NAME} nodes found. A patch may be wrong. Using the first one"); } - ConfigNode techNode = new ConfigNode("TechTree"); - techNode.AddNode(configs[0].config); + ConfigNode techNode = new ConfigNode(TECH_TREE_NODE_NAME); + techNode.AddNode(configs.First().Node); 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)) status = "ModuleManager: " + patchedNodeCount + " patch" + (patchedNodeCount != 1 ? "es" : "") + " loaded from cache"; // 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); + 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); + UrlDir.UrlFile parent = gameDataDir.Find(parentUrl); if (parent != null) { - parent.AddConfig(node.nodes[0]); + node.nodes[0].UnescapeValuesRecursive(); + databaseConfigs.Add(new ProtoUrlConfig(parent, node.nodes[0])); } else { 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.ToString("P0") + ")"; + + if (activity != null) + status += "\n" + activity; + + 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" : "") + ""; @@ -697,21 +564,10 @@ private void StatusUpdate(IPatchProgress progress) 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 - 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. @@ -732,7 +588,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; @@ -748,10 +604,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) @@ -871,7 +727,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 { @@ -917,7 +773,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 @@ -993,7 +849,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 { @@ -1067,7 +923,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); @@ -1090,11 +946,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(",")) @@ -1146,7 +1001,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 @@ -1156,8 +1011,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 { @@ -1169,7 +1024,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); @@ -1186,7 +1041,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); @@ -1212,8 +1067,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: @@ -1262,11 +1117,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(",")) { @@ -1302,28 +1156,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 { @@ -1388,7 +1238,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("[")) { @@ -1400,32 +1249,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("../")) { @@ -1445,13 +1289,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]; @@ -1518,7 +1361,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); @@ -1552,7 +1395,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) @@ -1608,14 +1451,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]); } @@ -1627,7 +1466,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) @@ -1699,7 +1538,7 @@ public static List SplitConstraints(string condition) public static bool CheckConstraints(ConfigNode node, string constraints) { constraints = constraints.RemoveWS(); - + if (constraints.Length == 0) return true; @@ -1710,11 +1549,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 += 5; + remainingConstraints = constraints.Substring(hasStart, constraintList[0].LastIndexOf(']') - hasStart); + constraints = constraints.Substring(0, hasStart - 5); } string[] splits = constraints.Split(contraintSeparators, 3); @@ -1796,7 +1635,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); @@ -1820,11 +1659,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); } @@ -1859,13 +1697,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. @@ -1877,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)) { @@ -1900,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)) { @@ -1927,66 +1772,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/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs new file mode 100644 index 00000000..6c3d63a0 --- /dev/null +++ b/ModuleManager/MMPatchRunner.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using ModuleManager.Collections; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Threading; + +using static ModuleManager.FilePathRepository; + +namespace ModuleManager +{ + public class MMPatchRunner + { + 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; + + if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath); + + kspLogger.Info("Patching started on a new thread, all output will be directed to " + logPath); + + MessageQueue mmLogQueue = new MessageQueue(); + 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!"); + }); + + // 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); + + IEnumerable modsAddedByAssemblies = ModListGenerator.GetAdditionalModsFromStaticMethods(mmLogger); + + IEnumerable databaseConfigs = null; + + MMPatchLoader patchLoader = new MMPatchLoader(modsAddedByAssemblies, mmLogger); + + ITaskStatus patchingThreadStatus = BackgroundTask.Start(delegate + { + databaseConfigs = patchLoader.Run(); + }); + + while(true) + { + yield return null; + + if (!patchingThreadStatus.IsRunning) + logRunner.RequestStop(); + + Status = patchLoader.status; + Errors = patchLoader.errors; + + if (!patchingThreadStatus.IsRunning && !loggingThreadStatus.IsRunning) break; + } + + if (patchingThreadStatus.IsExitedWithError) + { + 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) + { + 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/ModListGenerator.cs b/ModuleManager/ModListGenerator.cs index cca98805..5d017232 100644 --- a/ModuleManager/ModListGenerator.cs +++ b/ModuleManager/ModListGenerator.cs @@ -1,10 +1,10 @@ using System; -using System.IO; using System.Collections.Generic; using System.Linq; using System.Text; using System.Diagnostics; using System.Reflection; +using UnityEngine; using ModuleManager.Extensions; using ModuleManager.Logging; using ModuleManager.Utils; @@ -14,7 +14,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 @@ -117,17 +117,27 @@ 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); 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(); @@ -136,5 +146,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; + } } } diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs index 96a40656..dd5e04e5 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; @@ -9,34 +10,34 @@ using UnityEngine; using Debug = UnityEngine.Debug; using ModuleManager.Cats; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.UnityLogHandle; namespace ModuleManager { - [KSPAddon(KSPAddon.Startup.Instantly, false)] + [KSPAddon(KSPAddon.Startup.Instantly, true)] public class ModuleManager : MonoBehaviour { #region state private bool inRnDCenter; - private bool reloading; - 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; private bool nyan = false; private bool nCats = false; public static bool dumpPostPatch = false; + public static bool DontCopyLogs { get; private set; } = false; private PopupDialog menu; + private MMPatchRunner patchRunner; + #endregion state private static bool loadedInScene; @@ -56,13 +57,31 @@ public static void Log(String s) print("[ModuleManager] " + s); } - private Stopwatch totalTime = new Stopwatch(); + private readonly Stopwatch totalTime = new Stopwatch(); internal void Awake() { + if (LoadingScreen.Instance == null) + { + Destroy(gameObject); + 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 + Debug.unityLogger.logHandler = new InterceptLogHandler(Debug.unityLogger.logHandler); + + // Allow loading the background in the loading screen Application.runInBackground = true; QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; @@ -81,21 +100,8 @@ 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; - version = v.Major + "." + v.Minor + "." + v.Build; - // Subscribe to the RnD center spawn/deSpawn events GameEvents.onGUIRnDComplexSpawn.Add(OnRnDCenterSpawn); GameEvents.onGUIRnDComplexDespawn.Add(OnRnDCenterDeSpawn); @@ -116,13 +122,23 @@ 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 post patch 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); + patchRunner = new MMPatchRunner(new PrefixLogger("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) + { + Fix16 fix16 = aGameObject.AddComponent(); + list.Add(fix16); + } } bool foolsDay = (DateTime.Now.Month == 4 && DateTime.Now.Day == 1); @@ -135,6 +151,8 @@ internal void Awake() dumpPostPatch = Environment.GetCommandLineArgs().Contains("-mm-dump"); + DontCopyLogs = Environment.GetCommandLineArgs().Contains("-mm-dont-copy-logs"); + loadedInScene = true; } @@ -142,7 +160,7 @@ internal void Awake() private TextMeshProUGUI errors; private TextMeshProUGUI warning; - + [SuppressMessage("Code Quality", "IDE0051", Justification = "Called by Unity")] private void Start() { if (nCats) @@ -163,6 +181,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) @@ -251,42 +281,32 @@ 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) + { + warning.text = InterceptLogHandler.Warnings; + h = warning.text.Length > 0 ? warning.textBounds.size.y : 0; + 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 += h; + status.transform.localPosition = new Vector3(0, offsetY); + } - if (reloading) - { - 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(); - - int intPercent = Mathf.CeilToInt(percent * 100f / 3f); - ScreenMessages.PostScreenMessage("Database reloading " + intPercent + "%", Time.deltaTime, - ScreenMessageStyle.UPPER_CENTER); + if (errors) + { + errors.text = patchRunner.Errors; + h = errors.text.Length > 0 ? errors.textBounds.size.y : 0; + offsetY += h; + errors.transform.localPosition = new Vector3(0, offsetY); + } } } @@ -302,30 +322,94 @@ 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 PrefixLogger("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() || !PostPatchLoader.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(); + startedReload = true; + + yield return null; + StartCoroutine(patchRunner.Run()); + // wait for it to finish while (!GameDatabase.Instance.IsReady()) yield return null; - MMPatchLoader.Instance.StartLoad(); + PostPatchLoader.Instance.StartLoad(); - while (!MMPatchLoader.Instance.IsReady()) + while (!PostPatchLoader.Instance.IsReady()) yield return null; - PartResourceLibrary.Instance.LoadDefinitions(); - - PartUpgradeManager.Handler.FillUpgrades(); - if (dump) OutputAllConfigs(); @@ -350,13 +434,13 @@ 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() { - string path = KSPUtil.ApplicationRootPath + "/_MMCfgOutput/"; + string path = Path.GetFullPath(Path.Combine(KSPUtil.ApplicationRootPath, "_MMCfgOutput")); try { Directory.CreateDirectory(path); @@ -377,51 +461,50 @@ 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 + static 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); + foreach (UrlDir.UrlConfig urlConfig in urlFile.configs) { - if (urlFile.fileType == UrlDir.FileType.Config) + try { - string dirPath = path + currentPath; - if (!Directory.Exists(dirPath)) - { - Directory.CreateDirectory(dirPath); - } + if (first) first = false; + else writer.Write("\n"); - Log("Exporting " + currentPath + urlFile.name + "." + urlFile.fileExtension); - string filePath = dirPath + urlFile.name + "." + urlFile.fileExtension; - foreach (UrlDir.UrlConfig urlConfig in urlFile.configs) - { - try - { - File.AppendAllText(filePath, urlConfig.config.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); } } + } - 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); diff --git a/ModuleManager/ModuleManager.csproj b/ModuleManager/ModuleManager.csproj index 61344d57..20fe0da1 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.1 + True @@ -21,6 +22,7 @@ 4 False default + false none @@ -29,6 +31,10 @@ prompt 4 False + false + + + 8.0 @@ -37,30 +43,60 @@ + + + + + - + + - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + @@ -68,9 +104,14 @@ + + + + + @@ -81,15 +122,43 @@ False False + + False + False + - + + False False + + False + False + + + False + False + + + False + False + + + False + False + + + False + False + False - C:\Games\KSPSteamController\KSPSteamCtrlr\KSPUnity-Steam-Symlinks\UnityEngine.UI.dll + False + + + False False @@ -138,6 +207,7 @@ + sh -c "TARGET_PATH='$(TargetPath)' TARGET_DIR='$(TargetDir)' TARGET_NAME='$(TargetName)' sh '$(ProjectDir)/copy_build.sh'" 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/ModuleManager/NeedsChecker.cs b/ModuleManager/NeedsChecker.cs index 792766cf..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; @@ -8,87 +9,123 @@ 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; + [SuppressMessage("CodeQuality", "IDE0052", Justification = "Reserved for future use")] + 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 +133,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 +155,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 new file mode 100644 index 00000000..8777de97 --- /dev/null +++ b/ModuleManager/NodeMatcher.cs @@ -0,0 +1,58 @@ +using System; +using ModuleManager.Extensions; + +namespace ModuleManager +{ + public interface INodeMatcher + { + bool IsMatch(ConfigNode node); + } + + public class NodeMatcher : INodeMatcher + { + private readonly string type; + private readonly string[] namePatterns = null; + private readonly string constraints = ""; + + public NodeMatcher(string type, string name, string constraints) + { + if (type == string.Empty) throw new ArgumentException("can't be empty", nameof(type)); + this.type = type ?? throw new ArgumentNullException(nameof(type)); + + 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)); + + if (name != null) namePatterns = name.Split(',', '|'); + if (constraints != null) + { + if (!constraints.IsBracketBalanced()) throw new ArgumentException("is not bracket balanced: " + constraints, nameof(constraints)); + this.constraints = constraints; + } + } + + 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/Pass.cs b/ModuleManager/Pass.cs new file mode 100644 index 00000000..c7db8684 --- /dev/null +++ b/ModuleManager/Pass.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using ModuleManager.Patches; + +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(IPatch 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 5e30ad02..adaade88 100644 --- a/ModuleManager/PatchApplier.cs +++ b/ModuleManager/PatchApplier.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; using ModuleManager.Logging; using ModuleManager.Extensions; +using ModuleManager.Patches; using ModuleManager.Progress; -using NodeStack = ModuleManager.Collections.ImmutableStack; namespace ModuleManager { @@ -13,185 +12,43 @@ public class PatchApplier private readonly IBasicLogger logger; private readonly IPatchProgress progress; - private readonly UrlDir databaseRoot; - private readonly PatchList patchList; - - private readonly UrlDir.UrlFile[] allConfigFiles; - - public string Activity { get; private set; } - - public PatchApplier(PatchList patchList, UrlDir databaseRoot, IPatchProgress progress, IBasicLogger logger) + public PatchApplier(IPatchProgress progress, IBasicLogger logger) { - this.patchList = patchList; - this.databaseRoot = databaseRoot; - 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 IEnumerable ApplyPatches(IEnumerable patches) { - ApplyPatches(":FIRST", patchList.firstPatches); + if (patches == null) throw new ArgumentNullException(nameof(patches)); - // any node without a :pass - ApplyPatches(":LEGACY (default)", patchList.legacyPatches); + LinkedList databaseConfigs = new LinkedList(); - foreach (PatchList.ModPass pass in patchList.modPasses) + foreach (IPass pass in patches) { - string upperModName = pass.name.ToUpper(); - ApplyPatches($":BEFORE[{upperModName}]", pass.beforePatches); - ApplyPatches($":FOR[{upperModName}]", pass.forPatches); - ApplyPatches($":AFTER[{upperModName}]", pass.afterPatches); + ApplyPatches(databaseConfigs, pass); } - // :Final node - ApplyPatches(":FINAL", patchList.finalPatches); + return databaseConfigs; } - private void ApplyPatches(string stage, IEnumerable patches) + private void ApplyPatches(LinkedList databaseConfigs, IPass pass) { - logger.Info(stage + " pass"); - Activity = "ModuleManager " + stage; + progress.PassStarted(pass); - foreach (UrlDir.UrlConfig mod in patches) + foreach (IPatch patch in pass) { try { - string name = mod.type.RemoveWS(); - Command cmd = CommandParser.Parse(name, out string tmp); - - if (cmd == Command.Insert) - { - logger.Warning("Warning - Encountered insert node that should not exist at this stage: " + mod.SafeUrl()); - continue; - } - else if (cmd != Command.Edit && cmd != Command.Copy && cmd != Command.Delete) - { - logger.Warning("Invalid command encountered on a patch: " + mod.SafeUrl()); - continue; - } - - string upperName = name.ToUpper(); - PatchContext context = new PatchContext(mod, 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].Substring(1); - - bool loop = mod.config.HasNode("MM_PATCH_LOOP"); - - foreach (UrlDir.UrlFile file in allConfigFiles) - { - if (cmd == 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()); - - do - { - progress.ApplyingUpdate(url, mod); - url.config = MMPatchLoader.ModifyNode(new NodeStack(url.config), mod.config, context); - } while (loop && IsMatch(url, type, patterns, condition)); - - if (loop) url.config.RemoveNodes("MM_PATCH_LOOP"); - } - } - else if (cmd == 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 (!IsMatch(url, type, patterns, condition)) continue; - - ConfigNode clone = MMPatchLoader.ModifyNode(new NodeStack(url.config), mod.config, 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)"); - } - else - { - progress.ApplyingCopy(url, mod); - file.AddConfig(clone); - } - } - } - else if (cmd == Command.Delete) - { - int i = 0; - while (i < file.configs.Count) - { - UrlDir.UrlConfig url = file.configs[i]; - - if (IsMatch(url, type, patterns, condition)) - { - progress.ApplyingDelete(url, mod); - file.configs.RemoveAt(i); - } - else - { - i++; - } - } - } - else - { - throw new NotImplementedException("This code should not be reachable"); - } - } - progress.PatchApplied(); + patch.Apply(databaseConfigs, progress, logger); + if (patch.CountsAsPatch) progress.PatchApplied(); } catch (Exception e) { - progress.Exception(mod, "Exception while processing node : " + mod.SafeUrl(), e); - - try - { - logger.Error("Processed node was\n" + mod.PrettyPrint()); - } - catch (Exception ex2) - { - logger.Exception("Exception while attempting to print a node", ex2); - } + progress.Exception(patch.UrlConfig, "Exception while processing node : " + patch.UrlConfig.SafeUrl(), e); + logger.Error("Processed node was\n" + patch.UrlConfig.PrettyPrint()); } } } - - 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/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 d3425918..6c8765f4 100644 --- a/ModuleManager/PatchExtractor.cs +++ b/ModuleManager/PatchExtractor.cs @@ -1,206 +1,116 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Patches; using ModuleManager.Progress; +using ModuleManager.Tags; 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); - 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 IPatchProgress progress; + [SuppressMessage("CodeQuality", "IDE0052", Justification = "Reserved for future use")] + private readonly 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.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 static PatchList SortAndExtractPatches(UrlDir databaseRoot, IEnumerable modList, IPatchProgress progress) + public IPatch ExtractPatch(UrlDir.UrlConfig urlConfig) { - PatchList list = new PatchList(modList); + if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig)); - // Have to convert to an array because we will be removing patches - foreach (UrlDir.UrlConfig url in databaseRoot.AllConfigs.ToArray()) + 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; - } - - Command command = CommandParser.Parse(url.type, out _);; - - Match firstMatch = firstRegex.Match(url.type); - Match finalMatch = finalRegex.Match(url.type); - Match beforeMatch = beforeRegex.Match(url.type); - Match forMatch = forRegex.Match(url.type); - Match afterMatch = afterRegex.Match(url.type); - - 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(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 (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 (command == Command.Insert) continue; - - url.parent.configs.Remove(url); + progress.Error(urlConfig, "Error - node name does not have balanced brackets (or a space - if so replace with ?):\n" + urlConfig.SafeUrl()); + return null; + } - Match theMatch = null; - List thePass = null; - bool modNotFound = false; + Command command = CommandParser.Parse(urlConfig.type, out string name); + + if (command == Command.Replace) + { + progress.Error(urlConfig, $"Error - replace command (%) is not valid on a root node: {urlConfig.SafeUrl()}"); + return null; + } + else if (command == Command.Create) + { + progress.Error(urlConfig, $"Error - create command (&) is not valid on a root node: {urlConfig.SafeUrl()}"); + return null; + } + else if (command == Command.Rename) + { + progress.Error(urlConfig, $"Error - rename command (|) is not valid on a root node: {urlConfig.SafeUrl()}"); + return null; + } + else if (command == Command.Paste) + { + progress.Error(urlConfig, $"Error - paste command (#) is not valid on a root node: {urlConfig.SafeUrl()}"); + return null; + } + else if (command == Command.Special) + { + progress.Error(urlConfig, $"Error - special command (*) is not valid on a root node: {urlConfig.SafeUrl()}"); + return null; + } - if (firstMatch.Success) - { - theMatch = firstMatch; - thePass = list.firstPatches; - } - else if (finalMatch.Success) - { - theMatch = finalMatch; - thePass = list.finalPatches; - } - else if (beforeMatch.Success) - { - if (CheckMod(beforeMatch, list.modPasses, out string theMod)) - { - theMatch = beforeMatch; - thePass = list.modPasses[theMod].beforePatches; - } - else - { - modNotFound = true; - progress.NeedsUnsatisfiedBefore(url); - } - } - else if (forMatch.Success) - { - if (CheckMod(forMatch, list.modPasses, out string theMod)) - { - theMatch = forMatch; - thePass = list.modPasses[theMod].forPatches; - } - else - { - modNotFound = true; - progress.NeedsUnsatisfiedFor(url); - } - } - else if (afterMatch.Success) - { - if (CheckMod(afterMatch, list.modPasses, out string theMod)) - { - theMatch = afterMatch; - thePass = list.modPasses[theMod].afterPatches; - } - else - { - modNotFound = true; - progress.NeedsUnsatisfiedAfter(url); - } - } - else - { - thePass = list.legacyPatches; - } + ITagList tagList; + try + { + tagList = tagListParser.Parse(name, urlConfig); + } + catch (FormatException ex) + { + progress.Error(urlConfig, $"Cannot parse node name as tag list: {ex.Message}\non: {urlConfig.SafeUrl()}"); + return null; + } - if (modNotFound) continue; + ProtoPatch protoPatch = protoPatchBuilder.Build(urlConfig, command, tagList); - UrlDir.UrlConfig newUrl = url; - if (theMatch != null) - { - string newName = url.type.Remove(theMatch.Index, theMatch.Length); - ConfigNode newNode = new ConfigNode(newName) { id = url.config.id }; - newNode.ShallowCopyFrom(url.config); - newUrl = new UrlDir.UrlConfig(url.parent, newNode); - } + if (protoPatch == null) + { + return null; + } - thePass.Add(newUrl); - progress.PatchAdded(); + if (protoPatch.needs != null && !needsChecker.CheckNeedsExpression(protoPatch.needs)) + { + progress.NeedsUnsatisfiedRoot(urlConfig); + return null; } - catch(Exception e) + else if (!protoPatch.passSpecifier.CheckNeeds(needsChecker, progress)) { - progress.Exception(url, $"Exception while parsing pass for config: {url.SafeUrl()}", e); + return null; } - } - - return list; - } - private static bool CheckMod(Match match, PatchList.ModPassCollection modPasses, out string theMod) - { - theMod = match.Groups[1].Value.Trim().ToLower(); - return modPasses.HasMod(theMod); + needsChecker.CheckNeedsRecursive(urlConfig.config, urlConfig); + return patchCompiler.CompilePatch(protoPatch); + } + catch(Exception e) + { + progress.Exception(urlConfig, $"Exception while attempting to create patch from config: {urlConfig.SafeUrl()}", e); + return null; + } } } } diff --git a/ModuleManager/PatchList.cs b/ModuleManager/PatchList.cs index 6143947d..d3990540 100644 --- a/ModuleManager/PatchList.cs +++ b/ModuleManager/PatchList.cs @@ -1,66 +1,138 @@ 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; namespace ModuleManager { - public class PatchList + public class PatchList : IEnumerable { - public class ModPass + private class ModPass { - 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; + public readonly Pass beforePass; + public readonly Pass forPass; + public readonly Pass afterPass; + public readonly Pass lastPass; 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}]"); + 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))); } - public class ModPassCollection : IEnumerable + 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 SortedDictionary modPasses = new SortedDictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly SortedDictionary lastPasses = new SortedDictionary(StringComparer.InvariantCultureIgnoreCase); + + public PatchList(IEnumerable modList, IEnumerable patches, IPatchProgress progress) { - private readonly ModPass[] passesArray; - private readonly Dictionary passesDict; + if (modList == null) throw new ArgumentNullException(nameof(modList)); + if (patches == null) throw new ArgumentNullException(nameof(patches)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); - public ModPassCollection(IEnumerable modList) + foreach (string mod in modList) { - int count = modList.Count(); - passesArray = new ModPass[count]; - passesDict = new Dictionary(count); + modPasses.Add(mod, new ModPass(mod)); + } - int i = 0; - foreach (string mod in modList) + foreach (IPatch patch in patches) + { + if (patch.PassSpecifier is InsertPassSpecifier) + { + insertPatches.Add(patch); + } + else 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 LastPassSpecifier lastPassSpecifier) + { + 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) + { + finalPatches.Add(patch); + } + else { - ModPass pass = new ModPass(mod); - passesArray[i] = pass; - passesDict.Add(mod.ToLowerInvariant(), pass); - i++; + throw new NotImplementedException("Don't know what to do with pass specifier: " + patch.PassSpecifier.Descriptor); } + + if (patch.CountsAsPatch) progress.PatchAdded(); } + } + + public IEnumerator GetEnumerator() + { + yield return insertPatches; + yield return firstPatches; + yield return legacyPatches; - public ModPass this[string name] => passesDict[name.ToLowerInvariant()]; + foreach (ModPass modPass in modPasses.Values) + { + yield return modPass.beforePass; + yield return modPass.forPass; + yield return modPass.afterPass; + } - public bool HasMod(string name) => passesDict.ContainsKey(name.ToLowerInvariant()); + foreach (Pass lastPass in lastPasses.Values) + { + yield return lastPass; + } - public ArrayEnumerator GetEnumerator() => new ArrayEnumerator(passesArray); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + yield return finalPatches; } - public readonly List firstPatches = new List(); - public readonly List legacyPatches = new List(); - public readonly List finalPatches = new List(); - - public readonly ModPassCollection modPasses; + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public PatchList(IEnumerable modList) + private void EnsureMod(string mod) { - modPasses = new ModPassCollection(modList); + if (mod == null) throw new ArgumentNullException(nameof(mod)); + if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod)); + if (!modPasses.ContainsKey(mod)) throw new KeyNotFoundException($"Mod '{mod}' not found"); } } } diff --git a/ModuleManager/Patches/CopyPatch.cs b/ModuleManager/Patches/CopyPatch.cs new file mode 100644 index 00000000..45ee09fe --- /dev/null +++ b/ModuleManager/Patches/CopyPatch.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using NodeStack = ModuleManager.Collections.ImmutableStack; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + 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) + { + UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); + } + + public void Apply(LinkedList databaseConfigs, IPatchProgress progress, IBasicLogger logger) + { + 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, databaseConfigs, logger, progress); + + for (LinkedListNode listNode = databaseConfigs.First; listNode != null; listNode = listNode.Next) + { + IProtoUrlConfig protoConfig = listNode.Value; + try + { + if (!NodeMatcher.IsMatch(protoConfig.Node)) continue; + + 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 {protoConfig.FullUrl} - the copy needs to have a different name than the parent (use @name = xxx)"); + } + else + { + 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 {protoConfig.FullUrl}", ex); + } + } + } + } +} diff --git a/ModuleManager/Patches/DeletePatch.cs b/ModuleManager/Patches/DeletePatch.cs new file mode 100644 index 00000000..46d46a94 --- /dev/null +++ b/ModuleManager/Patches/DeletePatch.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + 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) + { + UrlConfig = urlConfig ?? throw new ArgumentNullException(nameof(urlConfig)); + NodeMatcher = nodeMatcher ?? throw new ArgumentNullException(nameof(nodeMatcher)); + PassSpecifier = passSpecifier ?? throw new ArgumentNullException(nameof(passSpecifier)); + } + + public void Apply(LinkedList databaseConfigs, IPatchProgress progress, IBasicLogger logger) + { + if (databaseConfigs == null) throw new ArgumentNullException(nameof(databaseConfigs)); + if (progress == null) throw new ArgumentNullException(nameof(progress)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + LinkedListNode currentNode = databaseConfigs.First; + while (currentNode != null) + { + IProtoUrlConfig protoConfig = currentNode.Value; + try + { + LinkedListNode nextNode = currentNode.Next; + if (NodeMatcher.IsMatch(protoConfig.Node)) + { + progress.ApplyingDelete(protoConfig, UrlConfig); + databaseConfigs.Remove(currentNode); + } + currentNode = nextNode; + } + catch (Exception 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 new file mode 100644 index 00000000..91613879 --- /dev/null +++ b/ModuleManager/Patches/EditPatch.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using NodeStack = ModuleManager.Collections.ImmutableStack; +using ModuleManager.Extensions; +using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + public class EditPatch : IPatch + { + private readonly bool loop; + + 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) + { + 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"); + } + + public void Apply(LinkedList databaseConfigs, IPatchProgress progress, IBasicLogger logger) + { + 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, databaseConfigs, logger, progress); + for (LinkedListNode listNode = databaseConfigs.First; listNode != null; listNode = listNode.Next) + { + IProtoUrlConfig protoConfig = listNode.Value; + try + { + if (!NodeMatcher.IsMatch(protoConfig.Node)) continue; + if (loop) logger.Info($"Looping on {UrlConfig.SafeUrl()} to {protoConfig.FullUrl}"); + + do + { + 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) protoConfig.Node.RemoveNodes("MM_PATCH_LOOP"); + } + catch (Exception 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 new file mode 100644 index 00000000..a877ea80 --- /dev/null +++ b/ModuleManager/Patches/IPatch.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using ModuleManager.Logging; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManager.Patches +{ + 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 new file mode 100644 index 00000000..c5f2c17f --- /dev/null +++ b/ModuleManager/Patches/InsertPatch.cs @@ -0,0 +1,35 @@ +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 bool CountsAsPatch => false; + + 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/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/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/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 new file mode 100644 index 00000000..6c7e7f30 --- /dev/null +++ b/ModuleManager/Patches/PatchCompiler.cs @@ -0,0 +1,26 @@ +using System; + +namespace ModuleManager.Patches +{ + public interface IPatchCompiler + { + IPatch CompilePatch(ProtoPatch protoPatch); + } + + public class PatchCompiler : IPatchCompiler + { + public IPatch CompilePatch(ProtoPatch protoPatch) + { + if (protoPatch == null) throw new ArgumentNullException(nameof(protoPatch)); + + return protoPatch.command switch + { + 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/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..377855fa --- /dev/null +++ b/ModuleManager/Patches/ProtoPatchBuilder.cs @@ -0,0 +1,247 @@ +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("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) + { + 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/PostPatchLoader.cs b/ModuleManager/PostPatchLoader.cs new file mode 100644 index 00000000..1bccce78 --- /dev/null +++ b/ModuleManager/PostPatchLoader.cs @@ -0,0 +1,231 @@ +using System; +using System.IO; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using UnityEngine; +using ModuleManager.Extensions; +using ModuleManager.Logging; + +using static ModuleManager.FilePathRepository; +using System.Diagnostics.CodeAnalysis; + +namespace ModuleManager +{ + public delegate void ModuleManagerPostPatchCallback(); + + 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 PrefixLogger("ModuleManager", new UnityLogger(UnityEngine.Debug.unityLogger)); + + private bool ready = false; + + private string progressTitle = "ModuleManager: Post patch"; + + public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback) + { + if (!postPatchCallbacks.Contains(callback)) + postPatchCallbacks.Add(callback); + } + + [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")] + 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() => progressTitle; + + public override void StartLoad() + { + ready = false; + StartCoroutine(Run()); + } + + private IEnumerator Run() + { + Stopwatch waitTimer = new Stopwatch(); + waitTimer.Start(); + + progressTitle = "ModuleManager: Waiting for patching to finish"; + + 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(); + + progressTitle = "ModuleManager: Applying patched game database"; + 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; + + yield return null; + + if (File.Exists(logPath)) + { + 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 + { + logger.Error("ModuleManager log does not exist: " + logPath); + } + + yield return null; + +#if DEBUG + InGameTestRunner testRunner = new InGameTestRunner(logger); + testRunner.RunTestCases(GameDatabase.Instance.root); +#endif + + yield return null; + + progressTitle = "ModuleManager: Reloading things"; + + 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(); + + LoadModdedPhysics(); + + yield return null; + + progressTitle = "ModuleManager: Running post patch callbacks"; + 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(); + + postPatchTimer.Stop(); + logger.Info("Post patch ran in " + ((float)postPatchTimer.ElapsedMilliseconds / 1000).ToString("F3") + "s"); + + ready = true; + } + + private void LoadModdedPhysics() + { + if (!File.Exists(physicsPath)) + { + logger.Error("Physics file not found"); + return; + } + + logger.Info("Setting modded physics as the active one"); + + PhysicsGlobals.PhysicsDatabaseFilename = physicsPath; + + if (!PhysicsGlobals.Instance.LoadDatabase()) + logger.Error("Something went wrong while setting the active physics config."); + } + } +} diff --git a/ModuleManager/Progress/IPatchProgress.cs b/ModuleManager/Progress/IPatchProgress.cs index 5eabfc3c..46bcb2ad 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 { @@ -9,19 +8,25 @@ 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); 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); - 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(); + void PassStarted(IPass pass); } } diff --git a/ModuleManager/Progress/PatchProgress.cs b/ModuleManager/Progress/PatchProgress.cs index 4939ffc6..61fd0d33 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 { @@ -9,7 +8,7 @@ public class PatchProgress : IPatchProgress { public ProgressCounter Counter { get; private set; } - private IBasicLogger logger; + private readonly IBasicLogger logger; public float ProgressFraction { @@ -21,6 +20,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,27 +40,35 @@ public void PatchAdded() Counter.totalPatches.Increment(); } - public void ApplyingUpdate(UrlDir.UrlConfig original, UrlDir.UrlConfig patch) + public void PassStarted(IPass pass) { - logger.Info($"Applying update {patch.SafeUrl()} to {original.SafeUrl()}"); + 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}"); 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(); } public void PatchApplied() { Counter.appliedPatches.Increment(); + OnPatchApplied.Fire(); } public void NeedsUnsatisfiedRoot(UrlDir.UrlConfig url) @@ -67,14 +77,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) @@ -95,6 +105,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(); @@ -102,6 +119,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(); @@ -114,9 +137,21 @@ public void Exception(UrlDir.UrlConfig url, string message, Exception exception) RecordErrorFile(url); } + private void RecordWarningFile(UrlDir.UrlConfig url) + { + string key = url.parent.GetUrlWithExtension(); + 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; + string key = url.parent.GetUrlWithExtension(); if (key[0] == '/') key = key.Substring(1); diff --git a/ModuleManager/Progress/ProgressCounter.cs b/ModuleManager/Progress/ProgressCounter.cs index 1400ad4f..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/ModuleManager/Properties/AssemblyInfo.cs b/ModuleManager/Properties/AssemblyInfo.cs index a27f4b16..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("3.1.0")] +[assembly: AssemblyVersion("4.2.3")] [assembly: KSPAssembly("ModuleManager", 2, 5)] // The following attributes are used to specify the signing key for the assembly, diff --git a/ModuleManager/ProtoUrlConfig.cs b/ModuleManager/ProtoUrlConfig.cs new file mode 100644 index 00000000..71f39df3 --- /dev/null +++ b/ModuleManager/ProtoUrlConfig.cs @@ -0,0 +1,37 @@ +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; + + if (node.GetValue("name") is string nameValue) + FullUrl += '[' + nameValue + ']'; + } + } +} 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..c9ff1eed --- /dev/null +++ b/ModuleManager/Tags/TagListParser.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using ModuleManager.Progress; + +namespace ModuleManager.Tags +{ + public interface ITagListParser + { + ITagList Parse(string ToParse, UrlDir.UrlConfig urlConfig); + } + + public class TagListParser : ITagListParser + { + 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] == ':') + { + progress.Warning(urlConfig, "trailing : detected"); + toParse = toParse.TrimEnd(':'); + } + + List tags = new List(); + Tag primaryTag = ParsePrimaryTag(toParse, ref tags, urlConfig); + return new TagList(primaryTag, tags); + } + + private Tag ParsePrimaryTag(string toParse, ref List tags, UrlDir.UrlConfig urlConfig) + { + 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), urlConfig); + } + else if (c == ':') + { + ParseTag(toParse, i + 1, ref tags, urlConfig); + 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 Tag ParsePrimaryTrailer(string toParse, int start, ref List tags, string primaryKey, string primaryValue, UrlDir.UrlConfig urlConfig) + { + 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, urlConfig); + 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 void ParseTag(string toParse, int start, ref List tags, UrlDir.UrlConfig urlConfig) + { + 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), urlConfig); + return; + } + else if (c == ':') + { + if (i == start) + progress.Warning(urlConfig, "extra : detected"); + else + tags.Add(new Tag(toParse.Substring(start, i - start), null, null)); + + ParseTag(toParse, i + 1, ref tags, urlConfig); + return; + } + else if (c == ']') + { + throw new FormatException("encountered closing bracket in key"); + } + } + + tags.Add(new Tag(toParse.Substring(start), null, null)); + } + + 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++) + { + 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, urlConfig); + 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/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 73cd8dba..fcf04482 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,11 @@ 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(); + using SHA256 sha = SHA256.Create(); + using FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + byte[] data = sha.ComputeHash(fs); - 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) - { - hashedValue += String.Format("{0,2:x2}", b); - } - - return hashedValue; + return data.ToHex(); } } } 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()); + } } } 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/Collections/MessageQueueTest.cs b/ModuleManagerTests/Collections/MessageQueueTest.cs index 9ee116ff..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() @@ -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/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/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/Extensions/ConfigNodeExtensionsTest.cs b/ModuleManagerTests/Extensions/ConfigNodeExtensionsTest.cs index e82971af..471be9cb 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") @@ -48,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); @@ -63,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] @@ -92,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") @@ -108,40 +106,38 @@ 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]; 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]); - Assert.Equal("mno", innerNode1.values[0].name); - Assert.Equal("pqr", innerNode1.values[0].value); + 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); - 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] @@ -272,5 +268,72 @@ XX INNER_NODE node.PrettyPrint(ref sb, "XX"); 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]); + } + + [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); + Assert.Equal(value, nodeValue.value); + } } } diff --git a/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs b/ModuleManagerTests/Extensions/IBasicLoggerExtensionsTest.cs index 781cccb6..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,32 +8,76 @@ namespace ModuleManagerTests.Extensions { public class IBasicLoggerExtensionsTest { - private IBasicLogger logger; - - public IBasicLoggerExtensionsTest() - { - logger = Substitute.For(); - } + private readonly IBasicLogger logger = Substitute.For(); [Fact] 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/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); + }); + } } } 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/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/InGameTestRunnerTest.cs b/ModuleManagerTests/InGameTestRunnerTest.cs new file mode 100644 index 00000000..a75897f4 --- /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.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); + 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.AssertInfo("Running tests..."); + logger.AssertInfo("tests complete."); + }); + + logger.AssertNoError(); + + 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.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."); + }); + + + Assert.Empty(file1.configs); + } + } +} 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..d70fe8db --- /dev/null +++ b/ModuleManagerTests/Logging/LogMessageTest.cs @@ -0,0 +1,126 @@ +using System; +using Xunit; +using NSubstitute; +using UnityEngine; +using ModuleManager.Logging; + +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() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new LogMessage(LogType.Log, null); + }); + + 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.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); + } + + [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() + { + LogMessage message = new LogMessage(LogType.Log, "everything is ok"); + 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\] 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\] 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\] 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\] 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\] 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()); + } + + [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/LogSplitterTest.cs b/ModuleManagerTests/Logging/LogSplitterTest.cs new file mode 100644 index 00000000..4135f92d --- /dev/null +++ b/ModuleManagerTests/Logging/LogSplitterTest.cs @@ -0,0 +1,57 @@ +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); + ILogMessage message = Substitute.For(); + logSplitter.Log(message); + logger1.Received().Log(message); + logger2.Received().Log(message); + } + + [Fact] + public void TestLog__MessageNull() + { + 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 deleted file mode 100644 index 7c4bf427..00000000 --- a/ModuleManagerTests/Logging/ModLoggerTest.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using Xunit; -using NSubstitute; -using UnityEngine; -using ModuleManager.Logging; - -namespace ModuleManagerTests.Logging -{ - public class ModLoggerTest - { - private IBasicLogger innerLogger; - private ModLogger logger; - - public ModLoggerTest() - { - innerLogger = Substitute.For(); - logger = new ModLogger("MyMod", innerLogger); - } - - [Fact] - public void TestConstructor__PrefixNull() - { - ArgumentNullException e = Assert.Throws(delegate - { - new ModLogger(null, innerLogger); - }); - - Assert.Equal("prefix", e.ParamName); - } - - [Fact] - public void TestConstructor__PrefixBlank() - { - ArgumentNullException e = Assert.Throws(delegate - { - new ModLogger("", innerLogger); - }); - - Assert.Equal("prefix", e.ParamName); - } - - [Fact] - public void TestConstructor__LoggerNull() - { - ArgumentNullException e = Assert.Throws(delegate - { - new ModLogger("blah", null); - }); - - Assert.Equal("logger", e.ParamName); - } - - [Fact] - public void TestLog__Info() - { - logger.Log(LogType.Log, "well hi there"); - - innerLogger.Received().Log(LogType.Log, "[MyMod] well hi there"); - } - - [Fact] - public void TestLog__Warning() - { - logger.Log(LogType.Warning, "I'm warning you"); - - innerLogger.Received().Log(LogType.Warning, "[MyMod] I'm warning you"); - } - - [Fact] - public void TestLog__Error() - { - logger.Log(LogType.Error, "You have made a grave mistake"); - - innerLogger.Received().Log(LogType.Error, "[MyMod] You have made a grave mistake"); - } - - [Fact] - public void TestException() - { - Exception e = new Exception(); - logger.Exception("An exception was thrown", e); - - innerLogger.Received().Exception("[MyMod] An exception was thrown", e); - } - } -} 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/PrefixLoggerTest.cs b/ModuleManagerTests/Logging/PrefixLoggerTest.cs new file mode 100644 index 00000000..5aaf0113 --- /dev/null +++ b/ModuleManagerTests/Logging/PrefixLoggerTest.cs @@ -0,0 +1,80 @@ +using System; +using Xunit; +using NSubstitute; +using UnityEngine; +using ModuleManager.Logging; + +namespace ModuleManagerTests.Logging +{ + public class PrefixLoggerTest + { + private readonly IBasicLogger innerLogger = Substitute.For(); + private readonly PrefixLogger logger; + + public PrefixLoggerTest() + { + logger = new PrefixLogger("MyMod", innerLogger); + } + + [Fact] + public void TestConstructor__PrefixNull() + { + ArgumentNullException e = Assert.Throws(delegate + { + new PrefixLogger(null, innerLogger); + }); + + Assert.Equal("prefix", e.ParamName); + } + + [Fact] + public void TestConstructor__PrefixBlank() + { + ArgumentNullException e = Assert.Throws(delegate + { + new PrefixLogger("", innerLogger); + }); + + Assert.Equal("prefix", e.ParamName); + } + + [Fact] + public void TestConstructor__LoggerNull() + { + ArgumentNullException e = Assert.Throws(delegate + { + new PrefixLogger("blah", null); + }); + + Assert.Equal("logger", e.ParamName); + } + + [Fact] + public void TestLog() + { + 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)); + + logger.Log(logMessage); + + innerLogger.Received().Log(Arg.Is(msg => + msg.LogType == LogType.Log && + msg.Timestamp == logMessage.Timestamp && + msg.Message == "[MyMod] well hi there" + )); + } + + [Fact] + public void TestLog__Null() + { + ArgumentNullException ex = Assert.Throws(delegate + { + logger.Log(null); + }); + + Assert.Equal("message", ex.ParamName); + } + } +} diff --git a/ModuleManagerTests/Logging/QueueLogRunnerTest.cs b/ModuleManagerTests/Logging/QueueLogRunnerTest.cs new file mode 100644 index 00000000..6a975bd2 --- /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 + { + logger.Log(message1); + logger.Log(message2); + logger.Log(message3); + logger.Log(message4); + logger.Log(message5); + logger.Log(message6); + }); + } + + [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/Logging/QueueLoggerTest.cs b/ModuleManagerTests/Logging/QueueLoggerTest.cs index b984f8c2..25e1abbe 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; @@ -9,43 +8,43 @@ 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); } [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 new file mode 100644 index 00000000..296eeac2 --- /dev/null +++ b/ModuleManagerTests/Logging/StreamLoggerTest.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using Xunit; +using NSubstitute; +using ModuleManager.Logging; + +namespace ModuleManagerTests.Logging +{ + public class StreamLoggerTest + { + [Fact] + public void TestConstructor__StreamNull() + { + Assert.Throws(delegate + { + new StreamLogger(null); + }); + } + + [Fact] + public void TestConstructor__CantWrite() + { + using (MemoryStream stream = new MemoryStream(new byte[0], false)) + { + Assert.Throws(delegate + { + new StreamLogger(stream); + }); + } + } + + [Fact] + public void TestLog__AlreadyDisposed() + { + using (MemoryStream stream = new MemoryStream(new byte[0], true)) + { + StreamLogger streamLogger = new StreamLogger(stream); + streamLogger.Dispose(); + + InvalidOperationException ex = Assert.Throws(delegate + { + streamLogger.Log(Substitute.For()); + }); + + Assert.Contains("Object has already been disposed", ex.Message); + } + } + + [Fact] + public void TestLog() + { + 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(message); + } + } + + using (MemoryStream stream = new MemoryStream(bytes, false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string result = reader.ReadToEnd().Trim('\r', '\n', '\0'); + Assert.Equal("[OMG wtf] bbq", result); + } + } + } + + [Fact] + public void TestLog__MessageNull() + { + using (MemoryStream stream = new MemoryStream(new byte[0], true)) + { + StreamLogger streamLogger = new StreamLogger(stream); + + ArgumentNullException ex = Assert.Throws(delegate + { + streamLogger.Log(null); + }); + + Assert.Equal("message", ex.ParamName); + } + } + } +} diff --git a/ModuleManagerTests/Logging/UnityLoggerTest.cs b/ModuleManagerTests/Logging/UnityLoggerTest.cs index b2e22d4e..7c4dc4a1 100644 --- a/ModuleManagerTests/Logging/UnityLoggerTest.cs +++ b/ModuleManagerTests/Logging/UnityLoggerTest.cs @@ -2,18 +2,18 @@ using Xunit; using NSubstitute; using UnityEngine; +using ModuleManager.Extensions; using ModuleManager.Logging; 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); } @@ -31,7 +31,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 +39,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 efeb39a1..b0617fe8 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); @@ -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()); @@ -82,9 +160,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 1909a654..f3afbbf5 100644 --- a/ModuleManagerTests/ModuleManagerTests.csproj +++ b/ModuleManagerTests/ModuleManagerTests.csproj @@ -1,7 +1,8 @@  - - + + + Debug @@ -11,10 +12,11 @@ Properties ModuleManagerTests ModuleManagerTests - v3.5 + v4.7.1 512 + true @@ -24,6 +26,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -32,52 +35,107 @@ TRACE prompt 4 + false + + + 8.0 - - ..\packages\NSubstitute.2.0.3\lib\net35\NSubstitute.dll + + ..\packages\Castle.Core.4.4.1\lib\net45\Castle.Core.dll + + + + ..\packages\NSubstitute.4.2.2\lib\net46\NSubstitute.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + - - ..\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 + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + - - - - - - - + + + + + @@ -93,12 +151,18 @@ TestUtils + + + 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/NeedsCheckerTest.cs b/ModuleManagerTests/NeedsCheckerTest.cs index 73c2918b..4d3540b9 100644 --- a/ModuleManagerTests/NeedsCheckerTest.cs +++ b/ModuleManagerTests/NeedsCheckerTest.cs @@ -1,233 +1,212 @@ 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 readonly IPatchProgress progress = Substitute.For(); + private readonly IBasicLogger logger = Substitute.For(); + private readonly NeedsChecker needsChecker; public NeedsCheckerTest() { - root = UrlBuilder.CreateRoot(); - gameData = UrlBuilder.CreateGameData(root); - file = UrlBuilder.CreateFile("abc/def.cfg", gameData); - - progress = Substitute.For(); - logger = Substitute.For(); + gameData = UrlBuilder.CreateGameData(); + 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 +263,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 new file mode 100644 index 00000000..866c69fc --- /dev/null +++ b/ModuleManagerTests/NodeMatcherTest.cs @@ -0,0 +1,333 @@ +using System; +using Xunit; +using TestUtils; +using ModuleManager; + +namespace ModuleManagerTests +{ + public class NodeMatcherTest + { + #region Constructor + + [Fact] + public void TestConstructor__TypeNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new NodeMatcher(null, null, null); + }); + + Assert.Equal("type", ex.ParamName); + } + + [Fact] + public void TestConstructor__TypeBlank() + { + ArgumentException ex = Assert.Throws(delegate + { + new NodeMatcher("", null, null); + }); + + Assert.Equal("type", ex.ParamName); + Assert.Contains("can't be empty", ex.Message); + } + + [Fact] + public void TestConstructor__NameBlank() + { + ArgumentException ex = Assert.Throws(delegate + { + new NodeMatcher("NODE", "", null); + }); + + Assert.Equal("name", ex.ParamName); + Assert.Contains("can't be empty (null allowed)", ex.Message); + } + + [Fact] + public void TestConstructor__ConstraintsBlank() + { + ArgumentException ex = Assert.Throws(delegate + { + new NodeMatcher("NODE", null, ""); + }); + + Assert.Equal("constraints", ex.ParamName); + Assert.Contains("can't be empty (null allowed)", ex.Message); + } + + [Fact] + public void TestConstructor__ConstraintsNotBracketBalanced() + { + ArgumentException ex = Assert.Throws(delegate + { + new NodeMatcher("NODE", null, "stuff[blah"); + }); + + Assert.Equal("constraints", ex.ParamName); + Assert.Contains("is not bracket balanced: stuff[blah", ex.Message); + } + + #endregion + + #region IsMatch + + [Fact] + public void TestIsMatch() + { + NodeMatcher matcher = new NodeMatcher("NODE", null, null); + + 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", null); + + 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", null); + + 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*", null); + + 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", "@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", "@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/PassTest.cs b/ModuleManagerTests/PassTest.cs new file mode 100644 index 00000000..bc829816 --- /dev/null +++ b/ModuleManagerTests/PassTest.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using Xunit; +using NSubstitute; +using ModuleManager; +using ModuleManager.Patches; + +namespace ModuleManagerTests +{ + public class PassTest + { + [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.Contains("can't be empty", 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() + { + IPatch[] patches = + { + Substitute.For(), + Substitute.For(), + Substitute.For(), + }; + + Pass pass = new Pass("blah") + { + patches[0], + patches[1], + patches[2], + }; + + IPatch[] passPatches = pass.ToArray(); + Assert.Equal(patches.Length, passPatches.Length); + + 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 729de882..d54ac286 100644 --- a/ModuleManagerTests/PatchApplierTest.cs +++ b/ModuleManagerTests/PatchApplierTest.cs @@ -1,877 +1,119 @@ using System; -using System.Linq; +using System.Collections.Generic; using Xunit; using NSubstitute; using UnityEngine; -using TestUtils; using ModuleManager; +using ModuleManager.Collections; using ModuleManager.Logging; +using ModuleManager.Patches; using ModuleManager.Progress; 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 PatchList patchList; - private readonly PatchApplier patchApplier; - - public PatchApplierTest() - { - logger = Substitute.For(); - progress = Substitute.For(); - databaseRoot = UrlBuilder.CreateRoot(); - file = UrlBuilder.CreateFile("abc/def.cfg", databaseRoot); - patchList = new PatchList(modList); - patchApplier = new PatchApplier(patchList, databaseRoot, progress, logger); - } - [Fact] - public void TestApplyPatches__Edit() + public void TestConstructor__ProgressNull() { - 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") + ArgumentNullException ex = Assert.Throws(delegate { - { "@foo", "baz" }, - { "pqr", "stw" }, + new PatchApplier(null, Substitute.For()); }); - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(1).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingUpdate(config2, patch1); - - 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); + Assert.Equal("progress", ex.ParamName); } [Fact] - public void TestApplyPatches__Copy() + public void TestConstructor__LoggerNull() { - 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") + ArgumentNullException ex = Assert.Throws(delegate { - { "name", "003" }, - { "bbb", "004" }, - }, file); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART") - { - { "@name ^", ":^00:01:" }, - { "@aaa", "011" }, - { "ccc", "005" }, + new PatchApplier(Substitute.For(), null); }); - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - 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); + Assert.Equal("logger", ex.ParamName); } [Fact] - public void TestApplyPatches__Copy__AlternateCommand() + public void TestApplyPatches__PatchesNull() { - 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") + PatchApplier applier = new PatchApplier(Substitute.For(), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate { - { "@name ^", ":^00:01:" }, - { "@aaa", "011" }, - { "ccc", "005" }, + applier.ApplyPatches(null); }); - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - 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); + Assert.Equal("patches", ex.ParamName); } [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); - - 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")); - - 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__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); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000]") - { - { "@aaa", "011" }, - { "eee", "012" }, - }); - - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART[002]") - { - { "@name", "022" }, - { "@bbb", "013" }, - { "fff", "014" }, - }); - - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new TestConfigNode("!PART[004]")); - - patchList.firstPatches.Add(patch1); - patchList.firstPatches.Add(patch2); - patchList.firstPatches.Add(patch3); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingCopy(config2, patch2); - progress.Received().ApplyingDelete(config3, patch3); - - 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); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[0*0]") - { - { "@aaa", "011" }, - { "eee", "012" }, - }); - - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART[0*2]") - { - { "@name", "022" }, - { "@bbb", "013" }, - { "fff", "014" }, - }); - - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new TestConfigNode("!PART[0*4]")); - - patchList.firstPatches.Add(patch1); - patchList.firstPatches.Add(patch2); - patchList.firstPatches.Add(patch3); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingCopy(config2, patch2); - progress.Received().ApplyingDelete(config3, patch3); - - 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); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "@aaa", "011" }, - { "ddd", "006" }, - }); - - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(1).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingUpdate(config2, patch1); - - 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); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "bbb", "002" }, - }); - - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "ccc", "003" }, - }); - - UrlDir.UrlConfig patch3 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "ddd", "004" }, - }); - - UrlDir.UrlConfig patch4 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "eee", "005" }, - }); - - UrlDir.UrlConfig patch5 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "fff", "006" }, - }); - - UrlDir.UrlConfig patch6 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "ggg", "007" }, - }); - - UrlDir.UrlConfig patch7 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "hhh", "008" }, - }); - - UrlDir.UrlConfig patch8 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "iii", "009" }, - }); - - UrlDir.UrlConfig patch9 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART[000|0*2]") - { - { "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); - - patchApplier.ApplyPatches(); - - 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); - - 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") - { - { "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); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART:HAS[#aaa[001]]") - { - { "@aaa", "011" }, - { "eee", "012" }, - }); - - UrlDir.UrlConfig patch2 = new UrlDir.UrlConfig(file, 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]]")); - - patchList.firstPatches.Add(patch1); - patchList.firstPatches.Add(patch2); - patchList.firstPatches.Add(patch3); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(3).PatchApplied(); - progress.Received().ApplyingUpdate(config1, patch1); - progress.Received().ApplyingCopy(config2, patch2); - progress.Received().ApplyingDelete(config3, patch3); - - 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); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("@PART:HAS[~aaa[>10]]") - { - { "@aaa *", "2" }, - { "bbb", "002" }, - new ConfigNode("MM_PATCH_LOOP"), - }); - - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - EnsureNoErrors(); - - progress.Received(1).PatchApplied(); - progress.Received(4).ApplyingUpdate(config1, patch1); - - logger.Received().Log(LogType.Log, "Looping on abc/def/@PART:HAS[~aaa[>10]] to abc/def/PART"); - - 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); - } - - [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(); + public void TestApplyPatches() + { + IBasicLogger logger = Substitute.For(); + IPatchProgress progress = Substitute.For(); + PatchApplier patchApplier = new PatchApplier(progress, logger); + 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 < patches.Length; i++) + { + 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])); + + IPass[] patchList = new IPass[] { pass1, pass2, pass3 }; + + LinkedList databaseConfigs = Assert.IsType>(patchApplier.ApplyPatches(new[] { pass1, pass2, pass3 })); 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() - { - UrlDir.UrlConfig config1 = UrlBuilder.CreateConfig(new TestConfigNode("PART") - { - { "name", "000" }, - { "aaa", "001" }, - }, file); - - UrlDir.UrlConfig patch1 = new UrlDir.UrlConfig(file, new TestConfigNode("+PART") - { - { "@aaa", "011" }, - { "bbb", "012" }, + logger.AssertNoWarning(); + logger.AssertNoError(); + logger.AssertNoException(); + + Received.InOrder(delegate + { + progress.PassStarted(pass1); + patches[0].Apply(databaseConfigs, progress, logger); + patches[1].Apply(databaseConfigs, progress, logger); + patches[2].Apply(databaseConfigs, progress, logger); + 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(); + progress.PassStarted(pass3); + patches[6].Apply(databaseConfigs, progress, logger); + progress.PatchApplied(); + patches[7].Apply(databaseConfigs, progress, logger); + progress.PatchApplied(); + patches[8].Apply(databaseConfigs, progress, logger); + progress.PatchApplied(); }); - - patchList.firstPatches.Add(patch1); - - patchApplier.ApplyPatches(); - - 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)"); - - 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().ApplyingCopy(null, null); - - 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); - - 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()); } } } diff --git a/ModuleManagerTests/PatchExtractorTest.cs b/ModuleManagerTests/PatchExtractorTest.cs index 2a56c11a..49a12583 100644 --- a/ModuleManagerTests/PatchExtractorTest.cs +++ b/ModuleManagerTests/PatchExtractorTest.cs @@ -1,382 +1,366 @@ using System; -using System.Collections.Generic; 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 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); - + progress = Substitute.For(); + logger = Substitute.For(); + needsChecker = Substitute.For(); + tagListParser = Substitute.For(); + protoPatchBuilder = Substitute.For(); + patchCompiler = Substitute.For(); + patchExtractor = new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); } [Fact] - public void TestSortAndExtractPatches() + public void TestConstructor__ProgressNull() { - UrlDir.UrlConfig[] insertConfigs = + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("NODE"), - CreateConfig("NADE"), - }; + new PatchExtractor(null, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); + }); - UrlDir.UrlConfig[] legacyConfigs = - { - CreateConfig("@NODE"), - CreateConfig("@NADE[foo]:HAS[#bar]"), - }; + Assert.Equal("progress", ex.ParamName); + } - UrlDir.UrlConfig[] firstConfigs = + [Fact] + public void TestConstructor__LoggerNull() + { + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("@NODE:FIRST"), - CreateConfig("@NODE[foo]:HAS[#bar]:FIRST"), - CreateConfig("@NADE:First"), - CreateConfig("@NADE:first"), - }; + new PatchExtractor(progress, null, needsChecker, tagListParser, protoPatchBuilder, patchCompiler); + }); - UrlDir.UrlConfig[] finalConfigs = - { - CreateConfig("@NODE:FINAL"), - CreateConfig("@NODE[foo]:HAS[#bar]:FINAL"), - CreateConfig("@NADE:Final"), - CreateConfig("@NADE:final"), - }; + Assert.Equal("logger", ex.ParamName); + } - UrlDir.UrlConfig[] beforeMod1Configs = + [Fact] + public void TestConstructor__NeedsCheckerNull() + { + 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(progress, logger, null, tagListParser, protoPatchBuilder, patchCompiler); + }); - UrlDir.UrlConfig[] forMod1Configs = - { - CreateConfig("@NODE:FOR[mod1]"), - CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod1]"), - CreateConfig("@NADE:for[mod1]"), - CreateConfig("@NADE:FOR[MOD1]"), - }; + Assert.Equal("needsChecker", ex.ParamName); + } - UrlDir.UrlConfig[] afterMod1Configs = + [Fact] + public void TestConstructor__TagListParserNull() + { + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("@NODE:AFTER[mod1]"), - CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod1]"), - CreateConfig("@NADE:after[mod1]"), - CreateConfig("@NADE:AFTER[MOD1]"), - }; + new PatchExtractor(progress, logger, needsChecker, null, protoPatchBuilder, patchCompiler); + }); - UrlDir.UrlConfig[] beforeMod2Configs = - { - CreateConfig("@NODE:BEFORE[mod2]"), - CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod2]"), - CreateConfig("@NADE:before[mod2]"), - CreateConfig("@NADE:BEFORE[MOD2]"), - }; + Assert.Equal("tagListParser", ex.ParamName); + } - UrlDir.UrlConfig[] forMod2Configs = + [Fact] + public void TestConstructor__ProtoPatchBuilderNull() + { + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("@NODE:FOR[mod2]"), - CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod2]"), - CreateConfig("@NADE:for[mod2]"), - CreateConfig("@NADE:FOR[MOD2]"), - }; + new PatchExtractor(progress, logger, needsChecker, tagListParser, null, patchCompiler); + }); - UrlDir.UrlConfig[] afterMod2Configs = - { - CreateConfig("@NODE:AFTER[mod2]"), - CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod2]"), - CreateConfig("@NADE:after[mod2]"), - CreateConfig("@NADE:AFTER[MOD2]"), - }; + Assert.Equal("protoPatchBuilder", ex.ParamName); + } - UrlDir.UrlConfig[] beforeMod3Configs = + [Fact] + public void TestConstructor__PatchCompilerNull() + { + ArgumentNullException ex = Assert.Throws(delegate { - CreateConfig("@NODE:BEFORE[mod3]"), - CreateConfig("@NODE[foo]:HAS[#bar]:BEFORE[mod3]"), - CreateConfig("@NADE:before[mod3]"), - CreateConfig("@NADE:BEFORE[MOD3]"), - }; + new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, null); + }); - UrlDir.UrlConfig[] forMod3Configs = - { - CreateConfig("@NODE:FOR[mod3]"), - CreateConfig("@NODE[foo]:HAS[#bar]:FOR[mod3]"), - CreateConfig("@NADE:for[mod3]"), - CreateConfig("@NADE:FOR[MOD3]"), - }; + Assert.Equal("patchCompiler", ex.ParamName); + } - UrlDir.UrlConfig[] afterMod3Configs = - { - CreateConfig("@NODE:AFTER[mod3]"), - CreateConfig("@NODE[foo]:HAS[#bar]:AFTER[mod3]"), - CreateConfig("@NADE:after[mod3]"), - CreateConfig("@NADE:AFTER[MOD3]"), - }; + [Fact] + public void TestExtractPatch__ProtoPatchNull() + { + UrlDir.UrlConfig patchConfig = UrlBuilder.CreateConfig("abc/def", new ConfigNode("NODE"), root); - string[] modList = { "mod1", "mod2" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + protoPatchBuilder.Build(patchConfig, Command.Insert, Arg.Any()).Returns(null, new ProtoPatch[0]); - progress.DidNotReceiveWithAnyArgs().Error(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null); - progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + Assert.Null(patchExtractor.ExtractPatch(patchConfig)); + + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsExpression(null); + + AssertNoErrors(); - Assert.True(list.modPasses.HasMod("mod1")); - Assert.True(list.modPasses.HasMod("mod2")); - Assert.False(list.modPasses.HasMod("mod3")); - - Assert.Equal(insertConfigs, root.AllConfigs); - - Assert.Equal(legacyConfigs, list.legacyPatches); - - List currentPatches; - - 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]); - - 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]); - - 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]); - - 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]); - - 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]); - - 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]); - - 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]); - - 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]); - - 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]); + progress.DidNotReceive().PatchAdded(); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestSortAndExtractPatches__InsertWithPass() + public void TestExtractPatch() { - 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 urlConfig = CreateConfig("@NODE_TYPE"); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); - Assert.Equal(new[] { config1 }, root.AllConfigs); + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Edit, + "NODE_TYPE", + "nodeName", + null, + "has", + passSpecifier + ); - 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"); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + passSpecifier.CheckNeeds(needsChecker, progress).Returns(true); - 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); + IPatch patch = Substitute.For(); + patchCompiler.CompilePatch(protoPatch).Returns(patch); - progress.DidNotReceive().PatchAdded(); + Assert.Same(patch, patchExtractor.ExtractPatch(urlConfig)); + + AssertNoErrors(); + + needsChecker.Received().CheckNeedsRecursive(urlConfig.config, urlConfig); + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsExpression(null); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestSortAndExtractPatches__MoreThanOnePass() + public void TestExtractPatch__Needs() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE:FIRST"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:FIRST:FIRST"); - UrlDir.UrlConfig config3 = CreateConfig("@NODE:FIRST:FOR[mod1]"); + UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); - Assert.Empty(root.AllConfigs); + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Edit, + "NODE_TYPE", + "nodeName", + "needs", + "has", + passSpecifier + ); - 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]"); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + needsChecker.CheckNeedsExpression("needs").Returns(true); + passSpecifier.CheckNeeds(needsChecker, progress).Returns(true); - Assert.Equal(1, list.firstPatches.Count); - AssertUrlCorrect("@NODE", config1, list.firstPatches[0]); - 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); + IPatch patch = Substitute.For(); + patchCompiler.CompilePatch(protoPatch).Returns(patch); - progress.Received(1).PatchAdded(); + Assert.Same(patch, patchExtractor.ExtractPatch(urlConfig)); + + AssertNoErrors(); + + needsChecker.Received().CheckNeedsRecursive(urlConfig.config, urlConfig); + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestSortAndExtractPatches__Exception() + public void TestExtractPatch__NeedsUnsatisfied() { - Exception e = new Exception("an exception was thrown"); - progress.WhenForAnyArgs(p => p.Error(null, null)).Throw(e); + 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.Edit, + "NODE_TYPE", + "nodeName", + "needs", + "has", + passSpecifier + ); - UrlDir.UrlConfig config1 = CreateConfig("@NODE"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:FIRST:FIRST"); - UrlDir.UrlConfig config3 = CreateConfig("@NADE:FIRST"); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + needsChecker.CheckNeedsExpression("needs").Returns(false); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); - progress.Received().Exception(config2, "Exception while parsing pass for config: abc/def/@NODE:FIRST:FIRST", e); + AssertNoErrors(); - Assert.Equal(new[] { config1 }, list.legacyPatches); - Assert.Equal(1, list.firstPatches.Count); - AssertUrlCorrect("@NADE", config3, list.firstPatches[0]); + passSpecifier.DidNotReceiveWithAnyArgs().CheckNeeds(null, null); + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsRecursive(null, null); + patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); - progress.Received(2).PatchAdded(); + progress.Received().NeedsUnsatisfiedRoot(urlConfig); } [Fact] - public void TestSortAndExtractPatches__NotBracketBalanced() + public void TestExtractPatch__NeedsUnsatisfiedPassSpecifier() { - UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR[mod1]"); - UrlDir.UrlConfig config2 = CreateConfig("@NODE:FOR["); - UrlDir.UrlConfig config3 = CreateConfig("NODE:HAS[#foo[]"); + UrlDir.UrlConfig urlConfig = CreateConfig("@NODE_TYPE"); - string[] modList = { "mod1" }; - PatchList list = PatchExtractor.SortAndExtractPatches(root, modList, progress); + ITagList tagList = Substitute.For(); + tagListParser.Parse("NODE_TYPE", urlConfig).Returns(tagList); - Assert.Empty(root.AllConfigs); + IPassSpecifier passSpecifier = Substitute.For(); + ProtoPatch protoPatch = new ProtoPatch( + urlConfig, + Command.Edit, + "NODE_TYPE", + "nodeName", + "needs", + "has", + passSpecifier + ); - 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); - AssertUrlCorrect("@NODE", config1, list.modPasses["mod1"].forPatches[0]); - Assert.Empty(list.modPasses["mod1"].afterPatches); - - progress.Received(1).PatchAdded(); + protoPatchBuilder.Build(urlConfig, Command.Edit, tagList).Returns(protoPatch); + needsChecker.CheckNeedsExpression("needs").Returns(true); + passSpecifier.CheckNeeds(needsChecker, progress).Returns(false); + + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + AssertNoErrors(); + + needsChecker.DidNotReceiveWithAnyArgs().CheckNeedsRecursive(null, null); + patchCompiler.DidNotReceiveWithAnyArgs().CompilePatch(null); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); } [Fact] - public void TestSortAndExtractPatches__BadlyFormed() + public void TestExtractPatch__Null() { - 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); - - 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"); - - 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.Empty(list.modPasses["mod1"].afterPatches); - - progress.Received(1).PatchAdded(); + ArgumentNullException ex = Assert.Throws(delegate + { + patchExtractor.ExtractPatch(null); + }); + + Assert.Equal("urlConfig", ex.ParamName); } [Fact] - public void TestSortAndExtractPatches__InvalidCommand() + public void TestExtractPatch__NotBracketBalanced() { - 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]"); - - 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]"); - - 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.Empty(list.modPasses["mod1"].afterPatches); - - progress.Received(1).PatchAdded(); + UrlDir.UrlConfig config1 = CreateConfig("@NODE:FOR["); + UrlDir.UrlConfig config2 = CreateConfig("NODE:HAS[#foo[]"); + + patchExtractor.ExtractPatch(config1); + patchExtractor.ExtractPatch(config2); + + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, 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[]"); + }); + + progress.DidNotReceiveWithAnyArgs().NeedsUnsatisfiedRoot(null); + } + + [Fact] + public void TestExtractPatch__InvalidCommand__Replace() + { + UrlDir.UrlConfig urlConfig = CreateConfig("%NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + progress.Received().Error(urlConfig, "Error - replace command (%) is not valid on a root node: abc/def/%NODE"); + } + + [Fact] + public void TestExtractPatch__InvalidCommand__Create() + { + UrlDir.UrlConfig urlConfig = CreateConfig("&NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + progress.Received().Error(urlConfig, "Error - create command (&) is not valid on a root node: abc/def/&NODE"); + } + + [Fact] + public void TestExtractPatch__InvalidCommand__Rename() + { + UrlDir.UrlConfig urlConfig = CreateConfig("|NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + progress.Received().Error(urlConfig, "Error - rename command (|) is not valid on a root node: abc/def/|NODE"); + } + + [Fact] + public void TestExtractPatch__InvalidCommand__Paste() + { + UrlDir.UrlConfig urlConfig = CreateConfig("#NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + progress.Received().Error(urlConfig, "Error - paste command (#) is not valid on a root node: abc/def/#NODE"); + } + + [Fact] + public void TestExtractPatch__InvalidCommand__Special() + { + UrlDir.UrlConfig urlConfig = CreateConfig("*NODE"); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + progress.Received().Error(urlConfig, "Error - special command (*) is not valid on a root node: abc/def/*NODE"); + } + + [Fact] + public void TestExtractPatch__TagListBadlyFormatted() + { + UrlDir.UrlConfig urlConfig = CreateConfig("badSomehow"); + tagListParser.When(t => t.Parse("badSomehow", urlConfig)).Throw(new FormatException("badly formatted")); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + progress.Received().Error(urlConfig, "Cannot parse node name as tag list: badly formatted\non: abc/def/badSomehow"); + } + + [Fact] + public void TestExtractPatch__ProtoPatchFailed() + { + UrlDir.UrlConfig urlConfig = CreateConfig("NODE"); + protoPatchBuilder.Build(urlConfig, Command.Insert, Arg.Any()).Returns((ProtoPatch)null); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + AssertNoErrors(); + } + + [Fact] + public void TestExtractPatch__Exception() + { + UrlDir.UrlConfig urlConfig = CreateConfig("NODE"); + Exception ex = new Exception(); + tagListParser.When(t => t.Parse("NODE", urlConfig)).Throw(ex); + Assert.Null(patchExtractor.ExtractPatch(urlConfig)); + + progress.Received().Exception(urlConfig, "Exception while attempting to create patch from config: abc/def/NODE", ex); } private UrlDir.UrlConfig CreateConfig(string name) @@ -389,38 +373,17 @@ private UrlDir.UrlConfig CreateConfig(string name) new ConfigNode("wine"), new ConfigNode("fruit"), }; - + node.id = "hungry?"; - + return UrlBuilder.CreateConfig(node, file); } - private void AssertUrlCorrect(string expectedNodeName, UrlDir.UrlConfig originalUrl, UrlDir.UrlConfig observedUrl) + private void AssertNoErrors() { - 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]); - } + progress.DidNotReceiveWithAnyArgs().Error(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); } } } diff --git a/ModuleManagerTests/PatchListTest.cs b/ModuleManagerTests/PatchListTest.cs index dc19b426..4dc5c751 100644 --- a/ModuleManagerTests/PatchListTest.cs +++ b/ModuleManagerTests/PatchListTest.cs @@ -1,86 +1,223 @@ using System; using System.Collections.Generic; +using System.Linq; using Xunit; +using NSubstitute; +using TestUtils; using ModuleManager; +using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; namespace ModuleManagerTests { public class PatchListTest { [Fact] - public void TestConstructor() + public void TestConstructor__ModListNull() { - PatchList list = new PatchList(new string[0]); + ArgumentNullException ex = Assert.Throws(delegate + { + new PatchList(null, new IPatch[0], Substitute.For()); + }); + + Assert.Equal("modList", ex.ParamName); + } + + [Fact] + public void TestConstructor__PatchesNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new PatchList(new string[0], null, Substitute.For()); + }); - Assert.NotNull(list.firstPatches); - Assert.NotNull(list.legacyPatches); - Assert.NotNull(list.finalPatches); - Assert.NotNull(list.modPasses); + Assert.Equal("patches", ex.ParamName); } [Fact] - public void TestModPasses__HasMod() + public void TestConstructor__ProgressNull() { - PatchList list = new PatchList(new[] { "mod1", "Mod2", "MOD3" }); + ArgumentNullException ex = Assert.Throws(delegate + { + new PatchList(new string[0], new IPatch[0], null); + }); - PatchList.ModPassCollection collection = list.modPasses; + Assert.Equal("progress", ex.ParamName); + } - Assert.True(collection.HasMod("mod1")); - Assert.True(collection.HasMod("Mod1")); - Assert.True(collection.HasMod("MOD1")); + [Fact] + public void TestConstructor__UnknownMod() + { + 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(); - Assert.True(collection.HasMod("mod2")); - Assert.True(collection.HasMod("Mod2")); - Assert.True(collection.HasMod("MOD2")); + KeyNotFoundException ex = Assert.Throws(delegate + { + new PatchList(new[] { "mod1", "mod2" }, new[] { patch }, progress); + }); - Assert.True(collection.HasMod("mod3")); - Assert.True(collection.HasMod("Mod3")); - Assert.True(collection.HasMod("MOD3")); + Assert.Equal("Mod 'mod3' not found", ex.Message); - Assert.False(collection.HasMod("mod4")); - Assert.False(collection.HasMod("Mod4")); - Assert.False(collection.HasMod("MOD4")); + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestModPasses__Accessor() + public void TestConstructor__UnknownPassSpecifier() { - PatchList list = new PatchList(new[] { "mod1", "mod2" }); - - 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); - - 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); - - Assert.Throws(delegate + 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(); + + NotImplementedException ex = Assert.Throws(delegate { - PatchList.ModPass mod3 = list.modPasses["mod3"]; + new PatchList(new string[0], new[] { patch }, progress); }); + + Assert.Equal("Don't know what to do with pass specifier: :SOMEPASS", ex.Message); + + progress.DidNotReceive().PatchAdded(); } [Fact] - public void TestModPasses__Enumeration() + public void Test__Lifecycle() { - PatchList list = new PatchList(new[] { "mod1", "mod2" }); + 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(), + 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 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 LastPassSpecifier("mod3")); + patches[23].PassSpecifier.Returns(new FinalPassSpecifier()); + patches[24].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); + patches[24].CountsAsPatch.Returns(true); + + IPatchProgress progress = Substitute.For(); + + PatchList patchList = new PatchList(new[] { "mod1", "mod2" }, patches, progress); + + IPass[] passes = patchList.ToArray(); + + Assert.Equal(13, passes.Length); + + Assert.Equal(":INSERT (initial)", passes[0].Name); + Assert.Equal(new[] { patches[0], patches[1] }, passes[0]); + + Assert.Equal(":FIRST", passes[1].Name); + Assert.Equal(new[] { patches[2], patches[3] }, passes[1]); + + Assert.Equal(":LEGACY (default)", passes[2].Name); + Assert.Equal(new[] { patches[4], patches[5] }, passes[2]); + + Assert.Equal(":BEFORE[MOD1]", passes[3].Name); + Assert.Equal(new[] { patches[6], patches[7] }, passes[3]); + + Assert.Equal(":FOR[MOD1]", passes[4].Name); + Assert.Equal(new[] { patches[8], patches[9] }, passes[4]); + + Assert.Equal(":AFTER[MOD1]", passes[5].Name); + Assert.Equal(new[] { patches[10], patches[11] }, passes[5]); + + Assert.Equal(":BEFORE[MOD2]", passes[6].Name); + Assert.Equal(new[] { patches[14], patches[15] }, passes[6]); + + Assert.Equal(":FOR[MOD2]", passes[7].Name); + Assert.Equal(new[] { patches[16], patches[17] }, passes[7]); + + Assert.Equal(":AFTER[MOD2]", passes[8].Name); + Assert.Equal(new[] { patches[18], patches[19] }, passes[8]); + + Assert.Equal(":LAST[MOD1]", passes[9].Name); + Assert.Equal(new[] { patches[12], patches[13] }, passes[9]); + + Assert.Equal(":LAST[MOD2]", passes[10].Name); + Assert.Equal(new[] { patches[20], patches[21] }, passes[10]); + + Assert.Equal(":LAST[MOD3]", passes[11].Name); + Assert.Equal(new[] { patches[22] }, passes[11]); - PatchList.ModPass[] passes = new PatchList.ModPass[] { list.modPasses["mod1"], list.modPasses["mod2"] }; + Assert.Equal(":FINAL", passes[12].Name); + Assert.Equal(new[] { patches[23], patches[24] }, passes[12]); - Assert.Equal(passes, list.modPasses); + progress.Received(23).PatchAdded(); } } } diff --git a/ModuleManagerTests/Patches/CopyPatchTest.cs b/ModuleManagerTests/Patches/CopyPatchTest.cs new file mode 100644 index 00000000..ee6443dd --- /dev/null +++ b/ModuleManagerTests/Patches/CopyPatchTest.cs @@ -0,0 +1,344 @@ +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 CopyPatchTest + { + [Fact] + public void TestConstructor__urlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new CopyPatch(null, Substitute.For(), 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, 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(), 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, 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 TestCountsAsPatch() + { + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); + Assert.True(patch.CountsAsPatch); + } + + [Fact] + public void TestApply() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + ConfigNode config1 = new TestConfigNode("NODE") + { + { "foo", "bar" }, + }; + + ConfigNode config2 = new TestConfigNode("NODE") + { + { "foo", "bar" }, + }; + + ConfigNode config3 = new ConfigNode("NODE"); + ConfigNode config4 = new ConfigNode("NODE"); + + INodeMatcher nodeMatcher = Substitute.For(); + + 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(configs, progress, logger); + + IProtoUrlConfig[] newConfigs = configs.ToArray(); + + Assert.Equal(6, newConfigs.Length); + + Assert.Same(protoUrlConfig1, newConfigs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, newConfigs[0].Node); + + Assert.Same(protoUrlConfig2, newConfigs[1]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, newConfigs[1].Node); + + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "baz" }, + { "pqr", "stw" }, + }, 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" }, + }, newConfigs[5].Node); + Assert.Same(file, newConfigs[5].UrlFile); + + Received.InOrder(delegate + { + progress.ApplyingCopy(protoUrlConfig2, patch.UrlConfig); + progress.ApplyingCopy(protoUrlConfig4, 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"); + + ConfigNode config = new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }; + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(config).Returns(true); + + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("@NODE") + { + { "@name", "001" }, + { "@foo", "baz" }, + { "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(configs, progress, logger); + + IProtoUrlConfig[] newConfigs = configs.ToArray(); + + Assert.Equal(2, newConfigs.Length); + + Assert.Same(protoConfig, newConfigs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }, newConfigs[0].Node); + + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "001" }, + { "foo", "baz" }, + { "pqr", "stw" }, + }, newConfigs[1].Node); + Assert.Same(file, newConfigs[1].UrlFile); + + progress.Received().ApplyingCopy(protoConfig, 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() + { + ConfigNode config = new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }; + + INodeMatcher nodeMatcher = Substitute.For(); + + nodeMatcher.IsMatch(config).Returns(true); + + CopyPatch patch = new CopyPatch(UrlBuilder.CreateConfig("ghi/jkl", new TestConfigNode("+NODE") + { + { "@foo", "baz" }, + { "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(configs, progress, logger); + + Assert.Single(configs); + + Assert.Same(protoConfig, configs.First.Value); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "000" }, + { "foo", "bar" }, + }, configs.First.Value.Node); + + 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); + progress.DidNotReceiveWithAnyArgs().ApplyingDelete(null, null); + + progress.DidNotReceiveWithAnyArgs().Exception(null, null); + progress.DidNotReceiveWithAnyArgs().Exception(null, null, null); + } + + [Fact] + public void TestApply__DatabaseConfigsNullNull() + { + 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()); + }); + + Assert.Equal("databaseConfigs", ex.ParamName); + } + + [Fact] + 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(new LinkedList(), 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(), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(new LinkedList(), 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..f45916fa --- /dev/null +++ b/ModuleManagerTests/Patches/DeletePatchTest.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +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 DeletePatchTest + { + [Fact] + public void TestConstructor__urlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new DeletePatch(null, Substitute.For(), 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, 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(), 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, 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 TestCountsAsPatch() + { + DeletePatch patch = new DeletePatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); + Assert.True(patch.CountsAsPatch); + } + + [Fact] + public void TestApply() + { + 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(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(configs, progress, logger); + + Assert.Equal(new[] { urlConfig1, urlConfig3 }, 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__DatabaseConfigsNull() + { + 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()); + }); + + Assert.Equal("databaseConfigs", ex.ParamName); + } + + [Fact] + 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(new LinkedList(), 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(), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(new LinkedList(), 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..7c76cf48 --- /dev/null +++ b/ModuleManagerTests/Patches/EditPatchTest.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using NSubstitute; +using UnityEngine; +using TestUtils; +using ModuleManager; +using ModuleManager.Logging; +using ModuleManager.Patches; +using ModuleManager.Patches.PassSpecifiers; +using ModuleManager.Progress; + +namespace ModuleManagerTests.Patches +{ + public class EditPatchTest + { + [Fact] + public void TestConstructor__urlConfigNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + new EditPatch(null, Substitute.For(), 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, 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(), 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, 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 TestCountsAsPatch() + { + EditPatch patch = new EditPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), Substitute.For(), Substitute.For()); + Assert.True(patch.CountsAsPatch); + } + + [Fact] + public void TestApply() + { + UrlDir.UrlFile file = UrlBuilder.CreateFile("abc/def.cfg"); + + ConfigNode config1 = new TestConfigNode("NODE") + { + { "foo", "bar" }, + }; + + ConfigNode config2 = new TestConfigNode("NODE") + { + { "foo", "bar" }, + }; + + ConfigNode config3 = new ConfigNode("NODE"); + ConfigNode config4 = new ConfigNode("NODE"); + + INodeMatcher nodeMatcher = Substitute.For(); + + 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(configs, progress, logger); + + IProtoUrlConfig[] newConfigs = configs.ToArray(); + + Assert.Equal(4, newConfigs.Length); + + Assert.Same(urlConfig1, newConfigs[0]); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "bar" }, + }, newConfigs[0].Node); + + AssertNodesEqual(new TestConfigNode("NODE") + { + { "foo", "baz" }, + { "pqr", "stw" }, + }, newConfigs[1].Node); + Assert.Same(file, newConfigs[1].UrlFile); + + Assert.Same(urlConfig3, newConfigs[2]); + AssertNodesEqual(new ConfigNode("NODE"), newConfigs[2].Node); + + AssertNodesEqual(new TestConfigNode("NODE") + { + { "pqr", "stw" }, + }, newConfigs[3].Node); + Assert.Same(file, newConfigs[3].UrlFile); + + 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"); + + ConfigNode config = new TestConfigNode("NODE") + { + { "name", "000" }, + { "aaa", "1" }, + }; + + 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, 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); + + patch.Apply(configs, progress, logger); + + Assert.Single(configs); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "000" }, + { "aaa", "16" }, + { "bbb", "002" }, + { "bbb", "002" }, + { "bbb", "002" }, + { "bbb", "002" }, + }, configs.First.Value.Node); + Assert.Same(file, configs.First.Value.UrlFile); + + Assert.Same(urlConfig, modifiedUrlConfigs[0]); + Assert.NotSame(urlConfig, modifiedUrlConfigs[1]); + Assert.NotSame(urlConfig, modifiedUrlConfigs[2]); + Assert.NotSame(urlConfig, modifiedUrlConfigs[3]); + + Received.InOrder(delegate + { + 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); + 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__DatabaseConfigsNull() + { + 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()); + }); + + Assert.Equal("databaseConfigs", ex.ParamName); + } + + [Fact] + 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(new LinkedList(), 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(), Substitute.For()); + ArgumentNullException ex = Assert.Throws(delegate + { + patch.Apply(new LinkedList(), 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/InsertPatchTest.cs b/ModuleManagerTests/Patches/InsertPatchTest.cs new file mode 100644 index 00000000..f1e134a4 --- /dev/null +++ b/ModuleManagerTests/Patches/InsertPatchTest.cs @@ -0,0 +1,180 @@ +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 TestCountsAsPatch() + { + InsertPatch patch = new InsertPatch(UrlBuilder.CreateConfig("abc/def", new ConfigNode()), "A_NODE", Substitute.For()); + Assert.False(patch.CountsAsPatch); + } + + [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/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/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/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 new file mode 100644 index 00000000..bb786dc5 --- /dev/null +++ b/ModuleManagerTests/Patches/PatchCompilerTest.cs @@ -0,0 +1,369 @@ +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 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__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.Single(configs); + 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() + { + 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); + + ConfigNode config = new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }; + + 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(configs); + Assert.NotSame(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__Copy() + { + 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); + + ConfigNode config = new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }; + + 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().ApplyingCopy(urlConfig, protoPatch.urlConfig); + + IProtoUrlConfig[] newConfigs = configs.ToArray(); + + Assert.Equal(2, newConfigs.Length); + Assert.Same(config, newConfigs[0].Node); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }, newConfigs[0].Node); + AssertNodesEqual(new TestConfigNode("NODE") + { + { "name", "boo" }, + { "bar", "bleh" }, + }, newConfigs[1].Node); + Assert.Same(file, newConfigs[1].UrlFile); + } + + [Fact] + public void TestCompilePatch__Delete() + { + 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); + + IProtoUrlConfig urlConfig = Substitute.For(); + urlConfig.Node.Returns(new TestConfigNode("NODE") + { + { "name", "foo" }, + { "bar", "baz" }, + }); + + LinkedList configs = new LinkedList(); + configs.AddLast(urlConfig); + + patch.Apply(configs, progress, logger); + + AssertNoErrors(); + + progress.Received().ApplyingDelete(urlConfig, protoPatch.urlConfig); + + Assert.Empty(configs); + } + + [Fact] + public void TestCompilePatch__NullProtoPatch() + { + ArgumentNullException ex = Assert.Throws(delegate + { + patchCompiler.CompilePatch(null); + }); + + 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(protoPatch); + }); + + 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(protoPatch); + }); + + 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(protoPatch); + }); + + 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(protoPatch); + }); + + 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(protoPatch); + }); + + Assert.Equal("protoPatch", ex.ParamName); + Assert.Contains("invalid command for a root node: Special", 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); + } + } +} diff --git a/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs b/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs new file mode 100644 index 00000000..fd2ff4ab --- /dev/null +++ b/ModuleManagerTests/Patches/ProtoPatchBuilderTest.cs @@ -0,0 +1,1373 @@ +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__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() + { + 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__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() + { + 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 d94ae87e..c78964e3 100644 --- a/ModuleManagerTests/Progress/PatchProgressTest.cs +++ b/ModuleManagerTests/Progress/PatchProgressTest.cs @@ -1,22 +1,20 @@ using System; using Xunit; using NSubstitute; -using UnityEngine; using TestUtils; +using ModuleManager; using ModuleManager.Logging; using ModuleManager.Progress; -using NodeStack = ModuleManager.Collections.ImmutableStack; 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); } @@ -30,13 +28,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"); + logger.AssertNoLog(); + logger2.AssertInfo("Applying update ghi/jkl/@SOME_NODE to abc/def.cfg/SOME_NODE"); } [Fact] @@ -52,7 +51,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 +60,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.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/SOME_NODE"); + logger.AssertInfo("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 +79,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.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/SOME_NODE"); + logger.AssertInfo("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,21 +98,25 @@ 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.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/SOME_NODE"); + logger.AssertInfo("Applying delete pqr/stu/!SOME_NODE to abc/def.cfg/SOME_NODE"); } [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] @@ -123,49 +129,45 @@ 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] 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.AssertInfo("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.AssertInfo("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.AssertInfo("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.AssertInfo("Deleting value in file ghi/jkl value: SOME/NODE/PATH/some_other_value as it can't satisfy its NEEDS"); } [Fact] @@ -178,11 +180,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] @@ -195,11 +197,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] @@ -212,15 +214,77 @@ 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] + 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.AssertInfo(":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.AssertNoLog(); + onEvent.DidNotReceiveWithAnyArgs()(null); + } + + [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.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.AssertWarning("You should probably pay attention to this"); } [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")); @@ -231,12 +295,12 @@ public void TestError() 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] @@ -249,11 +313,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] @@ -270,12 +334,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] diff --git a/ModuleManagerTests/ProtoUrlConfigTest.cs b/ModuleManagerTests/ProtoUrlConfigTest.cs new file mode 100644 index 00000000..f16cd1bd --- /dev/null +++ b/ModuleManagerTests/ProtoUrlConfigTest.cs @@ -0,0 +1,87 @@ +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); + } + + [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); + } + } +} diff --git a/ModuleManagerTests/Tags/TagListParserTest.cs b/ModuleManagerTests/Tags/TagListParserTest.cs new file mode 100644 index 00000000..9ee395d0 --- /dev/null +++ b/ModuleManagerTests/Tags/TagListParserTest.cs @@ -0,0 +1,290 @@ +using System; +using Xunit; +using NSubstitute; +using TestUtils; +using ModuleManager.Progress; +using ModuleManager.Tags; + +namespace ModuleManagerTests.Tags +{ + public class TagListParserTest + { + 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", urlConfig); + Assert.Equal(new Tag("01", null, null), tagList.PrimaryTag); + Assert.Empty(tagList); + } + + [Fact] + public void TestParse__OnlyPrimaryKeyAndValue() + { + ITagList tagList = tagListParser.Parse("01[02]", urlConfig); + Assert.Equal(new Tag("01", "02", null), tagList.PrimaryTag); + Assert.Empty(tagList); + } + + [Fact] + public void TestParse__OnlyPrimaryKeyValueAndTrailer() + { + ITagList tagList = tagListParser.Parse("01[02]03", urlConfig); + 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", urlConfig); + 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", urlConfig); + 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]", urlConfig); + 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", urlConfig); + 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", urlConfig); + 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__StringNull() + { + ArgumentNullException ex = Assert.Throws(delegate + { + 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("", urlConfig); + }); + + 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]", urlConfig); + }); + + Assert.Equal("can't create tag list beginning with [", ex.Message); + } + + [Fact] + public void TestParse__StartsWithColon() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse(":stuff", urlConfig); + }); + + Assert.Equal("can't create tag list beginning with :", ex.Message); + } + + [Fact] + public void TestParse__EndsWithColon() + { + ITagList tagList = tagListParser.Parse("stuff:blah::", urlConfig); + + 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] + public void TestParse__ClosingBracketInPrimaryKey() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc]def", urlConfig); + }); + + 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", urlConfig); + }); + + 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]", urlConfig); + }); + + 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", urlConfig); + }); + + 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]", urlConfig); + }); + + Assert.Equal("tag can't start with [", ex.Message); + } + + [Fact] + public void TestParse__TagStartsWithColon() + { + ITagList tagList = tagListParser.Parse("abc:def::ghi", urlConfig); + + 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] + public void TestParse__ClosingBracketInKey() + { + FormatException ex = Assert.Throws(delegate + { + tagListParser.Parse("abc:def:ghi]jkl", urlConfig); + }); + + 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", urlConfig); + }); + + 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]", urlConfig); + }); + + 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", urlConfig); + }); + + 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()); + } + } +} diff --git a/ModuleManagerTests/app.config b/ModuleManagerTests/app.config new file mode 100644 index 00000000..fe205877 --- /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 dc40245d..b159434d 100644 --- a/ModuleManagerTests/packages.config +++ b/ModuleManagerTests/packages.config @@ -1,7 +1,16 @@  - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestUtils/TestConfigNode.cs b/TestUtils/TestConfigNode.cs index a954c107..694fc55e 100644 --- a/TestUtils/TestConfigNode.cs +++ b/TestUtils/TestConfigNode.cs @@ -8,8 +8,8 @@ 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(ConfigNode.Value value) => values.Add(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/TestUtils/TestUtils.csproj b/TestUtils/TestUtils.csproj index 06ab8be0..5c53b27c 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,10 @@ TRACE prompt 4 + false + + + 8.0 diff --git a/TestUtils/UrlBuilder.cs b/TestUtils/UrlBuilder.cs index 2fff4a47..e8e60bd5 100644 --- a/TestUtils/UrlBuilder.cs +++ b/TestUtils/UrlBuilder.cs @@ -118,6 +118,39 @@ 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") { cfg = true; @@ -126,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/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); } } } diff --git a/TestUtilsTests/TestConfigNodeTest.cs b/TestUtilsTests/TestConfigNodeTest.cs index 6b00b4de..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,31 +35,41 @@ 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(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"); 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); } } } diff --git a/TestUtilsTests/TestUtilsTests.csproj b/TestUtilsTests/TestUtilsTests.csproj index 1e7d65dd..cfe71bf2 100644 --- a/TestUtilsTests/TestUtilsTests.csproj +++ b/TestUtilsTests/TestUtilsTests.csproj @@ -1,7 +1,8 @@  - - + + + Debug @@ -11,10 +12,11 @@ Properties TestUtilsTests TestUtilsTests - v3.5 + v4.7.1 512 + true @@ -24,6 +26,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -32,6 +35,10 @@ TRACE prompt 4 + false + + + 8.0 @@ -42,8 +49,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 @@ -64,12 +81,18 @@ + + + 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/UrlBuilderTest.cs b/TestUtilsTests/UrlBuilderTest.cs index 956350dd..38a788f0 100644 --- a/TestUtilsTests/UrlBuilderTest.cs +++ b/TestUtilsTests/UrlBuilderTest.cs @@ -209,23 +209,37 @@ 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("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 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(extension, file.fileExtension); + 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] diff --git a/TestUtilsTests/packages.config b/TestUtilsTests/packages.config index cf3669ce..8cf69e4f 100644 --- a/TestUtilsTests/packages.config +++ b/TestUtilsTests/packages.config @@ -1,6 +1,12 @@  - - - + + + + + + + + + \ No newline at end of file