From 3064b58719060a145058ab295792d8c97128b433 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 Feb 2022 22:03:09 -0500 Subject: add basic unit tests for API interface proxying --- src/SMAPI.Tests/Core/InterfaceProxyTests.cs | 345 ++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 src/SMAPI.Tests/Core/InterfaceProxyTests.cs (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs new file mode 100644 index 00000000..99c1298f --- /dev/null +++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using FluentAssertions; +using NUnit.Framework; +using SMAPI.Tests.ModApiConsumer; +using SMAPI.Tests.ModApiConsumer.Interfaces; +using SMAPI.Tests.ModApiProvider; +using StardewModdingAPI.Framework.Reflection; + +namespace SMAPI.Tests.Core +{ + /// Unit tests for . + [TestFixture] + internal class InterfaceProxyTests + { + /********* + ** Fields + *********/ + /// The mod ID providing an API. + private readonly string FromModId = "From.ModId"; + + /// The mod ID consuming an API. + private readonly string ToModId = "From.ModId"; + + /// The random number generator with which to create sample values. + private readonly Random Random = new(); + + + /********* + ** Unit tests + *********/ + /**** + ** Events + ****/ + /// Assert that an event field can be proxied correctly. + [Test] + public void CanProxy_EventField() + { + // arrange + var providerMod = new ProviderMod(); + object implementation = providerMod.GetModApi(); + int expectedValue = this.Random.Next(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + new ApiConsumer().UseEventField(proxy, out Func<(int timesCalled, int lastValue)> getValues); + providerMod.RaiseEvent(expectedValue); + (int timesCalled, int lastValue) = getValues(); + + // assert + timesCalled.Should().Be(1, "Expected the proxied event to be raised once."); + lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised."); + } + + /// Assert that an event property can be proxied correctly. + [Test] + public void CanProxy_EventProperty() + { + // arrange + var providerMod = new ProviderMod(); + object implementation = providerMod.GetModApi(); + int expectedValue = this.Random.Next(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + new ApiConsumer().UseEventProperty(proxy, out Func<(int timesCalled, int lastValue)> getValues); + providerMod.RaiseEvent(expectedValue); + (int timesCalled, int lastValue) = getValues(); + + // assert + timesCalled.Should().Be(1, "Expected the proxied event to be raised once."); + lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised."); + } + + /**** + ** Properties + ****/ + /// Assert that properties can be proxied correctly. + /// Whether to set the properties through the provider mod or proxy interface. + [TestCase("set via provider mod")] + [TestCase("set via proxy interface")] + public void CanProxy_Properties(string setVia) + { + // arrange + var providerMod = new ProviderMod(); + object implementation = providerMod.GetModApi(); + int expectedNumber = this.Random.Next(); + int expectedObject = this.Random.Next(); + string expectedListValue = this.GetRandomString(); + string expectedListWithInterfaceValue = this.GetRandomString(); + string expectedDictionaryKey = this.GetRandomString(); + string expectedDictionaryListValue = this.GetRandomString(); + string expectedInheritedString = this.GetRandomString(); + BindingFlags expectedEnum = BindingFlags.Instance | BindingFlags.Public; + + // act + ISimpleApi proxy = this.GetProxy(implementation); + switch (setVia) + { + case "set via provider mod": + providerMod.SetPropertyValues( + number: expectedNumber, + obj: expectedObject, + listValue: expectedListValue, + listWithInterfaceValue: expectedListWithInterfaceValue, + dictionaryKey: expectedDictionaryKey, + dictionaryListValue: expectedDictionaryListValue, + enumValue: expectedEnum, + inheritedValue: expectedInheritedString + ); + break; + + case "set via proxy interface": + proxy.NumberProperty = expectedNumber; + proxy.ObjectProperty = expectedObject; + proxy.ListProperty = new() { expectedListValue }; + proxy.ListPropertyWithInterface = new List { expectedListWithInterfaceValue }; + proxy.GenericsProperty = new Dictionary> + { + [expectedDictionaryKey] = new List { expectedDictionaryListValue } + }; + proxy.EnumProperty = expectedEnum; + proxy.InheritedProperty = expectedInheritedString; + break; + + default: + throw new InvalidOperationException($"Invalid 'set via' option '{setVia}."); + } + + // assert number + this + .GetPropertyValue(implementation, nameof(proxy.NumberProperty)) + .Should().Be(expectedNumber); + proxy.NumberProperty + .Should().Be(expectedNumber); + + // assert object + this + .GetPropertyValue(implementation, nameof(proxy.ObjectProperty)) + .Should().Be(expectedObject); + proxy.ObjectProperty + .Should().Be(expectedObject); + + // assert list + (this.GetPropertyValue(implementation, nameof(proxy.ListProperty)) as IList) + .Should().NotBeNull() + .And.HaveCount(1) + .And.BeEquivalentTo(expectedListValue); + proxy.ListProperty + .Should().NotBeNull() + .And.HaveCount(1) + .And.BeEquivalentTo(expectedListValue); + + // assert list with interface + (this.GetPropertyValue(implementation, nameof(proxy.ListPropertyWithInterface)) as IList) + .Should().NotBeNull() + .And.HaveCount(1) + .And.BeEquivalentTo(expectedListWithInterfaceValue); + proxy.ListPropertyWithInterface + .Should().NotBeNull() + .And.HaveCount(1) + .And.BeEquivalentTo(expectedListWithInterfaceValue); + + // assert generics + (this.GetPropertyValue(implementation, nameof(proxy.GenericsProperty)) as IDictionary>) + .Should().NotBeNull() + .And.HaveCount(1) + .And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue); + proxy.GenericsProperty + .Should().NotBeNull() + .And.HaveCount(1) + .And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue); + + // assert enum + this + .GetPropertyValue(implementation, nameof(proxy.EnumProperty)) + .Should().Be(expectedEnum); + proxy.EnumProperty + .Should().Be(expectedEnum); + + // assert getter + this + .GetPropertyValue(implementation, nameof(proxy.GetterProperty)) + .Should().Be(42); + proxy.GetterProperty + .Should().Be(42); + + // assert inherited methods + this + .GetPropertyValue(implementation, nameof(proxy.InheritedProperty)) + .Should().Be(expectedInheritedString); + proxy.InheritedProperty + .Should().Be(expectedInheritedString); + } + + /// Assert that a simple method with no return value can be proxied correctly. + [Test] + public void CanProxy_SimpleMethod_Void() + { + // arrange + object implementation = new ProviderMod().GetModApi(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + proxy.GetNothing(); + } + + /// Assert that a simple int method can be proxied correctly. + [Test] + public void CanProxy_SimpleMethod_Int() + { + // arrange + object implementation = new ProviderMod().GetModApi(); + int expectedValue = this.Random.Next(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + int actualValue = proxy.GetInt(expectedValue); + + // assert + actualValue.Should().Be(expectedValue); + } + + /// Assert that a simple object method can be proxied correctly. + [Test] + public void CanProxy_SimpleMethod_Object() + { + // arrange + object implementation = new ProviderMod().GetModApi(); + object expectedValue = new(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + object actualValue = proxy.GetObject(expectedValue); + + // assert + actualValue.Should().BeSameAs(expectedValue); + } + + /// Assert that a simple list method can be proxied correctly. + [Test] + public void CanProxy_SimpleMethod_List() + { + // arrange + object implementation = new ProviderMod().GetModApi(); + string expectedValue = this.GetRandomString(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + IList actualValue = proxy.GetList(expectedValue); + + // assert + actualValue.Should().BeEquivalentTo(expectedValue); + } + + /// Assert that a simple list with interface method can be proxied correctly. + [Test] + public void CanProxy_SimpleMethod_ListWithInterface() + { + // arrange + object implementation = new ProviderMod().GetModApi(); + string expectedValue = this.GetRandomString(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + IList actualValue = proxy.GetListWithInterface(expectedValue); + + // assert + actualValue.Should().BeEquivalentTo(expectedValue); + } + + /// Assert that a simple method which returns generic types can be proxied correctly. + [Test] + public void CanProxy_SimpleMethod_GenericTypes() + { + // arrange + object implementation = new ProviderMod().GetModApi(); + string expectedKey = this.GetRandomString(); + string expectedValue = this.GetRandomString(); + + // act + ISimpleApi proxy = this.GetProxy(implementation); + IDictionary> actualValue = proxy.GetGenerics(expectedKey, expectedValue); + + // assert + actualValue + .Should().NotBeNull() + .And.HaveCount(1) + .And.ContainKey(expectedKey).WhoseValue.Should().BeEquivalentTo(expectedValue); + } + + /// Assert that a simple lambda method can be proxied correctly. + [Test] + [SuppressMessage("ReSharper", "ConvertToLocalFunction")] + public void CanProxy_SimpleMethod_Lambda() + { + // arrange + object implementation = new ProviderMod().GetModApi(); + Func expectedValue = _ => "test"; + + // act + ISimpleApi proxy = this.GetProxy(implementation); + object actualValue = proxy.GetObject(expectedValue); + + // assert + actualValue.Should().BeSameAs(expectedValue); + } + + + /********* + ** Private methods + *********/ + /// Get a property value from an instance. + /// The instance whose property to read. + /// The property name. + private object GetPropertyValue(object parent, string name) + { + if (parent is null) + throw new ArgumentNullException(nameof(parent)); + + Type type = parent.GetType(); + PropertyInfo property = type.GetProperty(name); + if (property is null) + throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'."); + + return property.GetValue(parent); + } + + /// Get a random test string. + private string GetRandomString() + { + return this.Random.Next().ToString(); + } + + /// Get a proxy API instance. + /// The underlying API instance. + private ISimpleApi GetProxy(object implementation) + { + var proxyFactory = new InterfaceProxyFactory(); + return proxyFactory.CreateProxy(implementation, this.FromModId, this.ToModId); + } + } +} -- cgit From 4da9e954df3846d01aa0536f4e8143466a1d62f3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 Feb 2022 00:49:49 -0500 Subject: use Array.Empty to avoid unneeded array allocations --- src/SMAPI.Tests/Core/ModResolverTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index da3446bb..1755f644 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -123,7 +123,7 @@ namespace SMAPI.Tests.Core [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] public void ValidateManifests_NoMods_DoesNothing() { - new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(Array.Empty(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] @@ -144,7 +144,7 @@ namespace SMAPI.Tests.Core public void ValidateManifests_ModStatus_AssumeBroken_Fails() { // arrange - Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + Mock mock = this.GetMetadata("Mod A", Array.Empty(), allowStatusChange: true); this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields { Status = ModStatus.AssumeBroken @@ -161,7 +161,7 @@ namespace SMAPI.Tests.Core public void ValidateManifests_MinimumApiVersion_Fails() { // arrange - Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + Mock mock = this.GetMetadata("Mod A", Array.Empty(), allowStatusChange: true); mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1")); this.SetupMetadataForValidation(mock); @@ -190,9 +190,9 @@ namespace SMAPI.Tests.Core public void ValidateManifests_DuplicateUniqueID_Fails() { // arrange - Mock modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); + Mock modA = this.GetMetadata("Mod A", Array.Empty(), allowStatusChange: true); Mock modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true); - Mock modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false); + Mock modC = this.GetMetadata("Mod C", Array.Empty(), allowStatusChange: false); foreach (Mock mod in new[] { modA, modB, modC }) this.SetupMetadataForValidation(mod); @@ -236,7 +236,7 @@ namespace SMAPI.Tests.Core public void ProcessDependencies_NoMods_DoesNothing() { // act - IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0], new ModDatabase()).ToArray(); + IModMetadata[] mods = new ModResolver().ProcessDependencies(Array.Empty(), new ModDatabase()).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods."); @@ -490,8 +490,8 @@ namespace SMAPI.Tests.Core EntryDll = entryDll ?? $"{Sample.String()}.dll", ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, - Dependencies = dependencies ?? new IManifestDependency[0], - UpdateKeys = new string[0] + Dependencies = dependencies ?? Array.Empty(), + UpdateKeys = Array.Empty() }; } -- cgit From a2190df08cc3f1b4a8dcb394056d65921d10702e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 18 Feb 2022 15:39:49 -0500 Subject: add AssetName to encapsulate asset name handling (#766) --- src/SMAPI.Tests/Core/AssetNameTests.cs | 295 +++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 src/SMAPI.Tests/Core/AssetNameTests.cs (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs new file mode 100644 index 00000000..8785aab8 --- /dev/null +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using StardewModdingAPI; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; + +namespace SMAPI.Tests.Core +{ + /// Unit tests for . + [TestFixture] + internal class AssetNameTests + { + /********* + ** Unit tests + *********/ + /**** + ** Constructor + ****/ + [Test(Description = $"Assert that the {nameof(AssetName)} constructor creates an instance with the expected values.")] + [TestCase("SimpleName", "SimpleName", null, null)] + [TestCase("Data/Achievements", "Data/Achievements", null, null)] + [TestCase("Characters/Dialogue/Abigail", "Characters/Dialogue/Abigail", null, null)] + [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] + [TestCase("Characters/Dialogue\\Abigail.fr-FR", "Characters/Dialogue/Abigail.fr-FR", null, null)] + [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] + public void Constructor_Valid(string name, string expectedBaseName, string expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode) + { + // arrange + name = PathUtilities.NormalizeAssetName(name); + + // act + string calledWithLocale = null; + IAssetName assetName = AssetName.Parse(name, parseLocale: locale => expectedLanguageCode); + + // assert + assetName.Name.Should() + .NotBeNull() + .And.Be(name.Replace("\\", "/")); + assetName.BaseName.Should() + .NotBeNull() + .And.Be(expectedBaseName); + assetName.LocaleCode.Should() + .Be(expectedLocale); + assetName.LanguageCode.Should() + .Be(expectedLanguageCode); + } + + [Test(Description = $"Assert that the {nameof(AssetName)} constructor throws an exception if the value is invalid.")] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("\t")] + [TestCase(" \t ")] + public void Constructor_NullOrWhitespace(string name) + { + // act + ArgumentException exception = Assert.Throws(() => _ = AssetName.Parse(name, null)); + + // assert + exception!.ParamName.Should().Be("rawName"); + exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')"); + } + + + /**** + ** IsEquivalentTo + ****/ + [Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is included.")] + + // exact match (ignore case) + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + + // exact match (ignore formatting) + [TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)] + [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + + // whitespace-sensitive + [TestCase("Data/Achievements", " Data/Achievements ", ExpectedResult = false)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] + + // other is null or whitespace + [TestCase("Data/Achievements", null, ExpectedResult = false)] + [TestCase("Data/Achievements", "", ExpectedResult = false)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = true)] + public bool IsEquivalentTo_Name(string mainAssetName, string otherAssetName) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr); + + // assert + return name.IsEquivalentTo(otherAssetName); + } + + [Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is excluded.")] + + // a few samples from previous test to make sure + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = false)] + public bool IsEquivalentTo_BaseName(string mainAssetName, string otherAssetName) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr); + + // assert + return name.IsEquivalentTo(otherAssetName, useBaseName: true); + } + + + /**** + ** StartsWith + ****/ + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for inputs that aren't affected by the input options.")] + + // exact match (ignore case and formatting) + [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] + [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)] + [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] + [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] + + // leading-whitespace-sensitive + [TestCase("Data/Achievements", " Data/Achievements", ExpectedResult = false)] + [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] + + // invalid prefixes + [TestCase("Data/Achievements", null, ExpectedResult = false)] + [TestCase("Data/Achievements", " ", ExpectedResult = false)] + + // with locale codes + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] + public bool StartsWith_SimpleCases(string mainAssetName, string prefix) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(prefix, true, true); + foreach (bool allowPartialWord in new[] { true, false }) + { + foreach (bool allowSubfolder in new[] { true, true }) + { + if (allowPartialWord && allowSubfolder) + continue; + + name.StartsWith(prefix, allowPartialWord, allowSubfolder) + .Should().Be(result, $"the value returned for options ({nameof(allowPartialWord)}: {allowPartialWord}, {nameof(allowSubfolder)}: {allowSubfolder}) should match the base case"); + } + } + + // assert value + return result; + } + + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowPartialWord' option.")] + [TestCase("Data/AchievementsToIgnore", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/AchievementsToIgnore", "Data/Achievements", false, ExpectedResult = false)] + [TestCase("Data/Achievements X", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements X", "Data/Achievements", false, ExpectedResult = true)] + [TestCase("Data/Achievements.X", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements.X", "Data/Achievements", false, ExpectedResult = true)] + + // with locale codes + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements.fr-FR", "Data/Achievements", false, ExpectedResult = true)] + public bool StartsWith_PartialWord(string mainAssetName, string prefix, bool allowPartialWord) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(prefix, allowPartialWord: allowPartialWord, allowSubfolder: true); + name.StartsWith(prefix, allowPartialWord, allowSubfolder: false) + .Should().Be(result, "specifying allowSubfolder should have no effect for these inputs"); + + // assert value + return result; + } + + [Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowSubfolder' option.")] + + // simple cases + [TestCase("Data/Achievements/Path", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/Achievements", false, ExpectedResult = false)] + [TestCase("Data/Achievements/Path", "Data\\Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data\\Achievements", false, ExpectedResult = false)] + + // trailing slash + [TestCase("Data/Achievements/Path", "Data/", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/", false, ExpectedResult = false)] + + // normalize slash style + [TestCase("Data/Achievements/Path", "Data\\", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data\\", false, ExpectedResult = false)] + [TestCase("Data/Achievements/Path", "Data/\\/", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path", "Data/\\/", false, ExpectedResult = false)] + + // with locale code + [TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", true, ExpectedResult = true)] + [TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", false, ExpectedResult = false)] + public bool StartsWith_Subfolder(string mainAssetName, string otherAssetName, bool allowSubfolder) + { + // arrange + mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName); + + // act + AssetName name = AssetName.Parse(mainAssetName, _ => null); + + // assert value is the same for any combination of options + bool result = name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder); + name.StartsWith(otherAssetName, allowPartialWord: false, allowSubfolder: allowSubfolder) + .Should().Be(result, "specifying allowPartialWord should have no effect for these inputs"); + + // assert value + return result; + } + + + /**** + ** GetHashCode + ****/ + [Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates the same hash code for two asset names which differ only by capitalization.")] + public void GetHashCode_IsCaseInsensitive() + { + // arrange + string left = "data/ACHIEVEMENTS"; + string right = "DATA/achievements"; + + // act + int leftHash = AssetName.Parse(left, _ => null).GetHashCode(); + int rightHash = AssetName.Parse(right, _ => null).GetHashCode(); + + // assert + leftHash.Should().Be(rightHash, "two asset names which differ only by capitalization should produce the same hash code"); + } + + [Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates few hash code collisions for an arbitrary set of asset names.")] + public void GetHashCode_HasFewCollisions() + { + // generate list of names + List names = new(); + { + Random random = new(); + string characters = "abcdefghijklmnopqrstuvwxyz1234567890/"; + + while (names.Count < 1000) + { + char[] name = new char[random.Next(5, 20)]; + for (int i = 0; i < name.Length; i++) + name[i] = characters[random.Next(0, characters.Length)]; + + names.Add(new string(name)); + } + } + + // get distinct hash codes + HashSet hashCodes = new(); + foreach (string name in names) + hashCodes.Add(AssetName.Parse(name, _ => null).GetHashCode()); + + // assert a collision frequency under 0.1% + float collisionFrequency = 1 - (hashCodes.Count / (names.Count * 1f)); + collisionFrequency.Should().BeLessOrEqualTo(0.001f, "hash codes should be relatively distinct with a collision rate under 0.1% for a small sample set"); + } + } +} -- cgit From a593eda30f82af474887d91458b0e9158f66fefc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 18:24:59 -0400 Subject: use target-typed new --- src/SMAPI.Tests/Core/TranslationTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs index 457f9fad..58bc59b1 100644 --- a/src/SMAPI.Tests/Core/TranslationTests.cs +++ b/src/SMAPI.Tests/Core/TranslationTests.cs @@ -116,7 +116,7 @@ namespace SMAPI.Tests.Core public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) { // act - Translation translation = new Translation("pt-BR", "key", text); + Translation translation = new("pt-BR", "key", text); // assert if (translation.HasValue()) @@ -129,7 +129,7 @@ namespace SMAPI.Tests.Core public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) { // act - Translation translation = new Translation("pt-BR", "key", text); + Translation translation = new("pt-BR", "key", text); // assert if (translation.HasValue()) @@ -182,7 +182,7 @@ namespace SMAPI.Tests.Core string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; // act - Translation translation = new Translation("pt-BR", "key", input); + Translation translation = new("pt-BR", "key", input); switch (structure) { case "anonymous object": -- cgit From 077d8e4f401ad1806c6af0540f432366314a2300 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 18:25:00 -0400 Subject: remove some unused/redundant code --- src/SMAPI.Tests/Core/AssetNameTests.cs | 4 ++-- src/SMAPI.Tests/Core/ModResolverTests.cs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs index 8785aab8..7b817d13 100644 --- a/src/SMAPI.Tests/Core/AssetNameTests.cs +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -33,7 +33,7 @@ namespace SMAPI.Tests.Core // act string calledWithLocale = null; - IAssetName assetName = AssetName.Parse(name, parseLocale: locale => expectedLanguageCode); + IAssetName assetName = AssetName.Parse(name, parseLocale: _ => expectedLanguageCode); // assert assetName.Name.Should() @@ -161,7 +161,7 @@ namespace SMAPI.Tests.Core AssetName name = AssetName.Parse(mainAssetName, _ => null); // assert value is the same for any combination of options - bool result = name.StartsWith(prefix, true, true); + bool result = name.StartsWith(prefix); foreach (bool allowPartialWord in new[] { true, false }) { foreach (bool allowSubfolder in new[] { true, true }) diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 1755f644..86c50606 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -123,7 +123,7 @@ namespace SMAPI.Tests.Core [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] public void ValidateManifests_NoMods_DoesNothing() { - new ModResolver().ValidateManifests(Array.Empty(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(Array.Empty(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] @@ -134,7 +134,7 @@ namespace SMAPI.Tests.Core mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); @@ -151,7 +151,7 @@ namespace SMAPI.Tests.Core }); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -166,7 +166,7 @@ namespace SMAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -180,7 +180,7 @@ namespace SMAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -197,7 +197,7 @@ namespace SMAPI.Tests.Core this.SetupMetadataForValidation(mod); // act - new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); @@ -223,7 +223,7 @@ namespace SMAPI.Tests.Core mock.Setup(p => p.DirectoryPath).Returns(modFolder); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); // assert // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. -- cgit From 2e7c233f6c9bf6430672b39f970a3324deba79dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 21:48:55 -0400 Subject: enable nullable annotations by default (#837) This adds `#nullable disable` to all existing code (except where null is impossible like enum files), so it can be migrated incrementally. --- src/SMAPI.Tests/Core/AssetNameTests.cs | 2 ++ src/SMAPI.Tests/Core/InterfaceProxyTests.cs | 2 ++ src/SMAPI.Tests/Core/ModResolverTests.cs | 2 ++ src/SMAPI.Tests/Core/TranslationTests.cs | 2 ++ 4 files changed, 8 insertions(+) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs index 7b817d13..b7e34191 100644 --- a/src/SMAPI.Tests/Core/AssetNameTests.cs +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using FluentAssertions; diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs index 99c1298f..1bf2ed68 100644 --- a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs +++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 86c50606..2ce1c74e 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.IO; diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs index 58bc59b1..f8f0e315 100644 --- a/src/SMAPI.Tests/Core/TranslationTests.cs +++ b/src/SMAPI.Tests/Core/TranslationTests.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; -- cgit From b4e979cc991a0c2a45ad986210108edd2d43e43d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Apr 2022 22:46:19 -0400 Subject: fix all warnings to simplify migration to nullable annotations (#837) --- src/SMAPI.Tests/Core/AssetNameTests.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs index b7e34191..ef8a08ef 100644 --- a/src/SMAPI.Tests/Core/AssetNameTests.cs +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -34,7 +34,6 @@ namespace SMAPI.Tests.Core name = PathUtilities.NormalizeAssetName(name); // act - string calledWithLocale = null; IAssetName assetName = AssetName.Parse(name, parseLocale: _ => expectedLanguageCode); // assert -- cgit From e58e8a22836081ec4baffa5a9b4b093a329f3d88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 7 Apr 2022 01:38:02 -0400 Subject: enable nullable annotations for manifests (#837) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 43 +++++++++++++++----------------- 1 file changed, 20 insertions(+), 23 deletions(-) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 2ce1c74e..e1b56559 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -52,11 +50,11 @@ namespace SMAPI.Tests.Core // act IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); - IModMetadata mod = mods.FirstOrDefault(); + IModMetadata? mod = mods.FirstOrDefault(); // assert Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead."); - Assert.AreEqual(ModMetadataStatus.Failed, mod.Status, "The mod metadata was not marked failed."); + Assert.AreEqual(ModMetadataStatus.Failed, mod!.Status, "The mod metadata was not marked failed."); Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); } @@ -91,12 +89,12 @@ namespace SMAPI.Tests.Core // act IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray(); - IModMetadata mod = mods.FirstOrDefault(); + IModMetadata? mod = mods.FirstOrDefault(); // assert Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest."); Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); - Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); + Assert.AreEqual(null, mod!.DataRecord, "The data record should be null since we didn't provide one."); Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); @@ -215,7 +213,7 @@ namespace SMAPI.Tests.Core // create DLL string modFolder = Path.Combine(this.GetTempFolderPath(), Guid.NewGuid().ToString("N")); Directory.CreateDirectory(modFolder); - File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll), ""); + File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll!), ""); // arrange Mock mock = new Mock(MockBehavior.Strict); @@ -480,21 +478,20 @@ namespace SMAPI.Tests.Core /// The value. /// The value. /// The value. - private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null) + private Manifest GetManifest(string? id = null, string? name = null, string? version = null, string? entryDll = null, string? contentPackForID = null, string? minimumApiVersion = null, IManifestDependency[]? dependencies = null) { - return new Manifest - { - UniqueID = id ?? $"{Sample.String()}.{Sample.String()}", - Name = name ?? id ?? Sample.String(), - Author = Sample.String(), - Description = Sample.String(), - Version = version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), - EntryDll = entryDll ?? $"{Sample.String()}.dll", - ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null, - MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, - Dependencies = dependencies ?? Array.Empty(), - UpdateKeys = Array.Empty() - }; + return new Manifest( + uniqueId: id ?? $"{Sample.String()}.{Sample.String()}", + name: name ?? id ?? Sample.String(), + author: Sample.String(), + description: Sample.String(), + version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()), + entryDll: entryDll ?? $"{Sample.String()}.dll", + contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID, null) : null, + minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, + dependencies: dependencies ?? Array.Empty(), + updateKeys: Array.Empty() + ); } /// Get a randomized basic manifest. @@ -510,7 +507,7 @@ namespace SMAPI.Tests.Core /// Whether the code being tested is allowed to change the mod status. private Mock GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) { - IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray()); + IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray()); return this.GetMetadata(manifest, allowStatusChange); } @@ -538,7 +535,7 @@ namespace SMAPI.Tests.Core /// Set up a mock mod metadata for . /// The mock mod metadata. /// The extra metadata about the mod from SMAPI's internal data (if any). - private void SetupMetadataForValidation(Mock mod, ModDataRecordVersionedFields modRecord = null) + private void SetupMetadataForValidation(Mock mod, ModDataRecordVersionedFields? modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DataRecord).Returns(() => null); -- cgit From 761f2d952b073b78a06453768992b7b0fd4c6463 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Apr 2022 21:34:18 -0400 Subject: enable nullable annotations in mod data models (#837) --- src/SMAPI.Tests/Core/ModResolverTests.cs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index e1b56559..b8649a7d 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -145,7 +145,7 @@ namespace SMAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", Array.Empty(), allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields + this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields(this.GetModDataRecord()) { Status = ModStatus.AssumeBroken }); @@ -216,9 +216,9 @@ namespace SMAPI.Tests.Core File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll!), ""); // arrange - Mock mock = new Mock(MockBehavior.Strict); + Mock mock = new(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mock.Setup(p => p.DataRecord).Returns(() => null); + mock.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields()); mock.Setup(p => p.Manifest).Returns(manifest); mock.Setup(p => p.DirectoryPath).Returns(modFolder); @@ -265,7 +265,7 @@ namespace SMAPI.Tests.Core public void ProcessDependencies_Skips_Failed() { // arrange - Mock mock = new Mock(MockBehavior.Strict); + Mock mock = new(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act @@ -380,7 +380,7 @@ namespace SMAPI.Tests.Core Mock modA = this.GetMetadata("Mod A"); Mock modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" }); Mock modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); - Mock modD = new Mock(MockBehavior.Strict); + Mock modD = new(MockBehavior.Strict); modD.Setup(p => p.Manifest).Returns(null); modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); @@ -516,8 +516,8 @@ namespace SMAPI.Tests.Core /// Whether the code being tested is allowed to change the mod status. private Mock GetMetadata(IManifest manifest, bool allowStatusChange = false) { - Mock mod = new Mock(MockBehavior.Strict); - mod.Setup(p => p.DataRecord).Returns(() => null); + Mock mod = new(MockBehavior.Strict); + mod.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields()); mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); mod.Setup(p => p.Manifest).Returns(manifest); @@ -538,11 +538,22 @@ namespace SMAPI.Tests.Core private void SetupMetadataForValidation(Mock mod, ModDataRecordVersionedFields? modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.DataRecord).Returns(() => null); mod.Setup(p => p.Manifest).Returns(this.GetManifest()); mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); - mod.Setup(p => p.DataRecord).Returns(modRecord); + mod.Setup(p => p.DataRecord).Returns(modRecord ?? this.GetModDataRecordVersionedFields()); mod.Setup(p => p.GetUpdateKeys(It.IsAny())).Returns(Enumerable.Empty()); } + + /// Generate a default mod data record. + private ModDataRecord GetModDataRecord() + { + return new("Default Display Name", new ModDataModel("Sample ID", null, ModWarning.None)); + } + + /// Generate a default mod data versioned fields instance. + private ModDataRecordVersionedFields GetModDataRecordVersionedFields() + { + return new ModDataRecordVersionedFields(this.GetModDataRecord()); + } } } -- cgit From 5f7a92a74592a53529890eebb1ee9fe519afd92f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Apr 2022 20:52:01 -0400 Subject: enable nullable annotations in unit tests (#837) --- src/SMAPI.Tests/Core/AssetNameTests.cs | 10 +++--- src/SMAPI.Tests/Core/InterfaceProxyTests.cs | 12 +++----- src/SMAPI.Tests/Core/ModResolverTests.cs | 2 +- src/SMAPI.Tests/Core/TranslationTests.cs | 47 ++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 28 deletions(-) (limited to 'src/SMAPI.Tests/Core') diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs index ef8a08ef..8018442c 100644 --- a/src/SMAPI.Tests/Core/AssetNameTests.cs +++ b/src/SMAPI.Tests/Core/AssetNameTests.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using FluentAssertions; @@ -28,7 +26,7 @@ namespace SMAPI.Tests.Core [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] [TestCase("Characters/Dialogue\\Abigail.fr-FR", "Characters/Dialogue/Abigail.fr-FR", null, null)] [TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)] - public void Constructor_Valid(string name, string expectedBaseName, string expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode) + public void Constructor_Valid(string name, string expectedBaseName, string? expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode) { // arrange name = PathUtilities.NormalizeAssetName(name); @@ -55,13 +53,13 @@ namespace SMAPI.Tests.Core [TestCase(" ")] [TestCase("\t")] [TestCase(" \t ")] - public void Constructor_NullOrWhitespace(string name) + public void Constructor_NullOrWhitespace(string? name) { // act - ArgumentException exception = Assert.Throws(() => _ = AssetName.Parse(name, null)); + ArgumentException exception = Assert.Throws(() => _ = AssetName.Parse(name!, null))!; // assert - exception!.ParamName.Should().Be("rawName"); + exception.ParamName.Should().Be("rawName"); exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')"); } diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs index 1bf2ed68..0b4919ed 100644 --- a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs +++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -41,7 +39,7 @@ namespace SMAPI.Tests.Core public void CanProxy_EventField() { // arrange - var providerMod = new ProviderMod(); + ProviderMod providerMod = new(); object implementation = providerMod.GetModApi(); int expectedValue = this.Random.Next(); @@ -61,7 +59,7 @@ namespace SMAPI.Tests.Core public void CanProxy_EventProperty() { // arrange - var providerMod = new ProviderMod(); + ProviderMod providerMod = new(); object implementation = providerMod.GetModApi(); int expectedValue = this.Random.Next(); @@ -86,7 +84,7 @@ namespace SMAPI.Tests.Core public void CanProxy_Properties(string setVia) { // arrange - var providerMod = new ProviderMod(); + ProviderMod providerMod = new(); object implementation = providerMod.GetModApi(); int expectedNumber = this.Random.Next(); int expectedObject = this.Random.Next(); @@ -317,13 +315,13 @@ namespace SMAPI.Tests.Core /// Get a property value from an instance. /// The instance whose property to read. /// The property name. - private object GetPropertyValue(object parent, string name) + private object? GetPropertyValue(object parent, string name) { if (parent is null) throw new ArgumentNullException(nameof(parent)); Type type = parent.GetType(); - PropertyInfo property = type.GetProperty(name); + PropertyInfo? property = type.GetProperty(name); if (property is null) throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'."); diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index b8649a7d..bd621bbf 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -507,7 +507,7 @@ namespace SMAPI.Tests.Core /// Whether the code being tested is allowed to change the mod status. private Mock GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false) { - IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray()); + IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray()); return this.GetMetadata(manifest, allowStatusChange); } diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs index f8f0e315..a65bf772 100644 --- a/src/SMAPI.Tests/Core/TranslationTests.cs +++ b/src/SMAPI.Tests/Core/TranslationTests.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using NUnit.Framework; using StardewModdingAPI; @@ -18,7 +17,7 @@ namespace SMAPI.Tests.Core ** Data *********/ /// Sample translation text for unit tests. - public static string[] Samples = { null, "", " ", "boop", " boop " }; + public static string?[] Samples = { null, "", " ", "boop", " boop " }; /********* @@ -36,13 +35,13 @@ namespace SMAPI.Tests.Core // act ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); Translation translation = helper.Get("key"); - Translation[] translationList = helper.GetTranslations()?.ToArray(); + Translation[]? translationList = helper.GetTranslations()?.ToArray(); // assert Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value."); Assert.IsNotNull(translationList, "The full list of translations is unexpectedly null."); - Assert.AreEqual(0, translationList.Length, "The full list of translations is unexpectedly not empty."); + Assert.AreEqual(0, translationList!.Length, "The full list of translations is unexpectedly not empty."); Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation."); Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value."); @@ -56,7 +55,7 @@ namespace SMAPI.Tests.Core var expected = this.GetExpectedTranslations(); // act - var actual = new Dictionary(); + var actual = new Dictionary(); TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); foreach (string locale in expected.Keys) { @@ -109,13 +108,13 @@ namespace SMAPI.Tests.Core