summaryrefslogtreecommitdiff
path: root/src/SMAPI.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Tests')
-rw-r--r--src/SMAPI.Tests/Core/AssetNameTests.cs294
-rw-r--r--src/SMAPI.Tests/Core/InterfaceProxyTests.cs400
-rw-r--r--src/SMAPI.Tests/Core/ModResolverTests.cs130
-rw-r--r--src/SMAPI.Tests/Core/TranslationTests.cs87
-rw-r--r--src/SMAPI.Tests/SMAPI.Tests.csproj7
-rw-r--r--src/SMAPI.Tests/Sample.cs2
-rw-r--r--src/SMAPI.Tests/Utilities/KeybindListTests.cs20
-rw-r--r--src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs181
-rw-r--r--src/SMAPI.Tests/Utilities/SDateTests.cs31
-rw-r--r--src/SMAPI.Tests/Utilities/SemanticVersionTests.cs66
-rw-r--r--src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs23
11 files changed, 1005 insertions, 236 deletions
diff --git a/src/SMAPI.Tests/Core/AssetNameTests.cs b/src/SMAPI.Tests/Core/AssetNameTests.cs
new file mode 100644
index 00000000..655e9bae
--- /dev/null
+++ b/src/SMAPI.Tests/Core/AssetNameTests.cs
@@ -0,0 +1,294 @@
+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
+{
+ /// <summary>Unit tests for <see cref="AssetName"/>.</summary>
+ [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
+ IAssetName assetName = AssetName.Parse(name, parseLocale: _ => 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<ArgumentException>(() => _ = 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-insensitive
+ [TestCase("Data/Achievements", " Data/Achievements ", ExpectedResult = true)]
+ [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
+
+ // 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 = true)]
+ [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)]
+
+ // whitespace-insensitive
+ [TestCase("Data/Achievements", " Data/Achievements", ExpectedResult = true)]
+ [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
+ [TestCase("Data/Achievements", " ", ExpectedResult = true)]
+
+ // invalid prefixes
+ [TestCase("Data/Achievements", null, 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);
+ 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<string> 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<int> 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");
+ }
+ }
+}
diff --git a/src/SMAPI.Tests/Core/InterfaceProxyTests.cs b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs
new file mode 100644
index 00000000..6be97526
--- /dev/null
+++ b/src/SMAPI.Tests/Core/InterfaceProxyTests.cs
@@ -0,0 +1,400 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+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;
+using StardewModdingAPI.Utilities;
+
+namespace SMAPI.Tests.Core
+{
+ /// <summary>Unit tests for <see cref="InterfaceProxyFactory"/>.</summary>
+ [TestFixture]
+ internal class InterfaceProxyTests
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The mod ID providing an API.</summary>
+ private readonly string FromModId = "From.ModId";
+
+ /// <summary>The mod ID consuming an API.</summary>
+ private readonly string ToModId = "From.ModId";
+
+ /// <summary>The random number generator with which to create sample values.</summary>
+ private readonly Random Random = new();
+
+ /// <summary>Sample user inputs for season names.</summary>
+ private static readonly IInterfaceProxyFactory[] ProxyFactories = {
+ new InterfaceProxyFactory(),
+ new OriginalInterfaceProxyFactory()
+ };
+
+
+ /*********
+ ** Unit tests
+ *********/
+ /****
+ ** Events
+ ****/
+ /// <summary>Assert that an event field can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_EventField([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ ProviderMod providerMod = new();
+ object implementation = providerMod.GetModApi();
+ int expectedValue = this.Random.Next();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, 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.");
+ }
+
+ /// <summary>Assert that an event property can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_EventProperty([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ ProviderMod providerMod = new();
+ object implementation = providerMod.GetModApi();
+ int expectedValue = this.Random.Next();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, 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
+ ****/
+ /// <summary>Assert that properties can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ /// <param name="setVia">Whether to set the properties through the <c>provider mod</c> or <c>proxy interface</c>.</param>
+ [Test]
+ public void CanProxy_Properties([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory, [Values("set via provider mod", "set via proxy interface")] string setVia)
+ {
+ // arrange
+ ProviderMod providerMod = new();
+ 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(proxyFactory, 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<string> { expectedListWithInterfaceValue };
+ proxy.GenericsProperty = new Dictionary<string, IList<string>>
+ {
+ [expectedDictionaryKey] = new List<string> { 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<string>)
+ .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<string>)
+ .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<string, IList<string>>)
+ .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);
+ }
+
+ /// <summary>Assert that a simple method with no return value can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_SimpleMethod_Void([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ proxy.GetNothing();
+ }
+
+ /// <summary>Assert that a simple int method can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_SimpleMethod_Int([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ int expectedValue = this.Random.Next();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ int actualValue = proxy.GetInt(expectedValue);
+
+ // assert
+ actualValue.Should().Be(expectedValue);
+ }
+
+ /// <summary>Assert that a simple object method can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_SimpleMethod_Object([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ object expectedValue = new();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ object actualValue = proxy.GetObject(expectedValue);
+
+ // assert
+ actualValue.Should().BeSameAs(expectedValue);
+ }
+
+ /// <summary>Assert that a simple list method can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_SimpleMethod_List([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ string expectedValue = this.GetRandomString();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ IList<string> actualValue = proxy.GetList(expectedValue);
+
+ // assert
+ actualValue.Should().BeEquivalentTo(expectedValue);
+ }
+
+ /// <summary>Assert that a simple list with interface method can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_SimpleMethod_ListWithInterface([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ string expectedValue = this.GetRandomString();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ IList<string> actualValue = proxy.GetListWithInterface(expectedValue);
+
+ // assert
+ actualValue.Should().BeEquivalentTo(expectedValue);
+ }
+
+ /// <summary>Assert that a simple method which returns generic types can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ public void CanProxy_SimpleMethod_GenericTypes([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ string expectedKey = this.GetRandomString();
+ string expectedValue = this.GetRandomString();
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ IDictionary<string, IList<string>> actualValue = proxy.GetGenerics(expectedKey, expectedValue);
+
+ // assert
+ actualValue
+ .Should().NotBeNull()
+ .And.HaveCount(1)
+ .And.ContainKey(expectedKey).WhoseValue.Should().BeEquivalentTo(expectedValue);
+ }
+
+ /// <summary>Assert that a simple lambda method can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ [SuppressMessage("ReSharper", "ConvertToLocalFunction")]
+ public void CanProxy_SimpleMethod_Lambda([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ Func<string, string> expectedValue = _ => "test";
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ object actualValue = proxy.GetObject(expectedValue);
+
+ // assert
+ actualValue.Should().BeSameAs(expectedValue);
+ }
+
+ /// <summary>Assert that a method with out parameters can be proxied correctly.</summary>
+ /// <param name="proxyFactory">The proxy factory to test.</param>
+ [Test]
+ [SuppressMessage("ReSharper", "ConvertToLocalFunction")]
+ public void CanProxy_Method_OutParameters([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
+ {
+ // arrange
+ object implementation = new ProviderMod().GetModApi();
+ const int expectedNumber = 42;
+
+ // act
+ ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
+ bool result = proxy.TryGetOutParameter(
+ inputNumber: expectedNumber,
+
+ out int outNumber,
+ out string outString,
+ out PerScreen<int> outReference,
+ out IDictionary<int, PerScreen<int>> outComplexType
+ );
+
+ // assert
+ result.Should().BeTrue();
+
+ outNumber.Should().Be(expectedNumber);
+
+ outString.Should().Be(expectedNumber.ToString());
+
+ outReference.Should().NotBeNull();
+ outReference.Value.Should().Be(expectedNumber);
+
+ outComplexType.Should().NotBeNull();
+ outComplexType.Count.Should().Be(1);
+ outComplexType.Keys.First().Should().Be(expectedNumber);
+ outComplexType.Values.First().Should().NotBeNull();
+ outComplexType.Values.First().Value.Should().Be(expectedNumber);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a property value from an instance.</summary>
+ /// <param name="parent">The instance whose property to read.</param>
+ /// <param name="name">The property name.</param>
+ 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);
+ }
+
+ /// <summary>Get a random test string.</summary>
+ private string GetRandomString()
+ {
+ return this.Random.Next().ToString();
+ }
+
+ /// <summary>Get a proxy API instance.</summary>
+ /// <param name="proxyFactory">The proxy factory to use.</param>
+ /// <param name="implementation">The underlying API instance.</param>
+ private ISimpleApi GetProxy(IInterfaceProxyFactory proxyFactory, object implementation)
+ {
+ return proxyFactory.CreateProxy<ISimpleApi>(implementation, this.FromModId, this.ToModId);
+ }
+ }
+}
diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs
index da3446bb..6b2746f5 100644
--- a/src/SMAPI.Tests/Core/ModResolverTests.cs
+++ b/src/SMAPI.Tests/Core/ModResolverTests.cs
@@ -38,6 +38,9 @@ namespace SMAPI.Tests.Core
// assert
Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead.");
+
+ // cleanup
+ Directory.Delete(rootFolder, recursive: true);
}
[Test(Description = "Assert that the resolver correctly returns a failed metadata if there's an empty mod folder.")]
@@ -50,12 +53,15 @@ 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.");
+
+ // cleanup
+ Directory.Delete(rootFolder, recursive: true);
}
[Test(Description = "Assert that the resolver correctly reads manifest data from a randomized file.")]
@@ -89,12 +95,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.");
@@ -115,6 +121,9 @@ namespace SMAPI.Tests.Core
Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null.");
Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value.");
Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match.");
+
+ // cleanup
+ Directory.Delete(rootFolder, recursive: true);
}
/****
@@ -123,7 +132,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<ModMetadata>(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
}
[Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")]
@@ -134,7 +143,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, validateFilesExist: false);
// assert
mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status.");
@@ -144,14 +153,14 @@ namespace SMAPI.Tests.Core
public void ValidateManifests_ModStatus_AssumeBroken_Fails()
{
// arrange
- Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
- this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields
+ Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
+ mock.Setup(p => p.DataRecord).Returns(() => new ModDataRecordVersionedFields(this.GetModDataRecord())
{
Status = ModStatus.AssumeBroken
});
// 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, validateFilesExist: false);
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -161,12 +170,11 @@ namespace SMAPI.Tests.Core
public void ValidateManifests_MinimumApiVersion_Fails()
{
// arrange
- Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
+ Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1"));
- 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, validateFilesExist: false);
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -176,32 +184,33 @@ namespace SMAPI.Tests.Core
public void ValidateManifests_MissingEntryDLL_Fails()
{
// arrange
- Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true);
- this.SetupMetadataForValidation(mock);
+ string directoryPath = this.GetTempFolderPath();
+ Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true, directoryPath: directoryPath);
+ Directory.CreateDirectory(directoryPath);
// 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<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
+
+ // cleanup
+ Directory.Delete(directoryPath);
}
[Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")]
public void ValidateManifests_DuplicateUniqueID_Fails()
{
// arrange
- Mock<IModMetadata> modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
+ Mock<IModMetadata> modA = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true);
- Mock<IModMetadata> modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false);
- foreach (Mock<IModMetadata> mod in new[] { modA, modB, modC })
- 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, validateFilesExist: false);
// assert
- modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the first mod with a unique ID.");
- modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the second mod with a unique ID.");
+ modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the first mod with a unique ID.");
+ modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the second mod with a unique ID.");
}
[Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")]
@@ -213,20 +222,23 @@ 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<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
+ Mock<IModMetadata> 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);
// 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.
+
+ // cleanup
+ Directory.Delete(modFolder, recursive: true);
}
/****
@@ -236,7 +248,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<IModMetadata>(), new ModDatabase()).ToArray();
// assert
Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods.");
@@ -265,7 +277,7 @@ namespace SMAPI.Tests.Core
public void ProcessDependencies_Skips_Failed()
{
// arrange
- Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
+ Mock<IModMetadata> mock = new(MockBehavior.Strict);
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
// act
@@ -380,7 +392,7 @@ namespace SMAPI.Tests.Core
Mock<IModMetadata> modA = this.GetMetadata("Mod A");
Mock<IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" });
Mock<IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true);
- Mock<IModMetadata> modD = new Mock<IModMetadata>(MockBehavior.Strict);
+ Mock<IModMetadata> modD = new(MockBehavior.Strict);
modD.Setup(p => p.Manifest).Returns<IManifest>(null);
modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
@@ -478,21 +490,20 @@ namespace SMAPI.Tests.Core
/// <param name="contentPackForID">The <see cref="IManifest.ContentPackFor"/> value.</param>
/// <param name="minimumApiVersion">The <see cref="IManifest.MinimumApiVersion"/> value.</param>
/// <param name="dependencies">The <see cref="IManifest.Dependencies"/> value.</param>
- 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 ?? new IManifestDependency[0],
- UpdateKeys = new string[0]
- };
+ 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<IManifestDependency>(),
+ updateKeys: Array.Empty<string>()
+ );
}
/// <summary>Get a randomized basic manifest.</summary>
@@ -508,21 +519,27 @@ namespace SMAPI.Tests.Core
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> 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);
}
/// <summary>Get a randomized basic manifest.</summary>
/// <param name="manifest">The mod manifest.</param>
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
- private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false)
+ /// <param name="directoryPath">The directory path the mod metadata should be pointed at, or <c>null</c> to generate a fake path.</param>
+ private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false, string? directoryPath = null)
{
- Mock<IModMetadata> mod = new Mock<IModMetadata>(MockBehavior.Strict);
- mod.Setup(p => p.DataRecord).Returns(() => null);
+ directoryPath ??= this.GetTempFolderPath();
+
+ Mock<IModMetadata> 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.DirectoryPath).Returns(directoryPath);
mod.Setup(p => p.Manifest).Returns(manifest);
mod.Setup(p => p.HasID(It.IsAny<string>())).Returns((string id) => manifest.UniqueID == id);
+ mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>());
+ mod.Setup(p => p.GetRelativePathWithRoot()).Returns(directoryPath);
if (allowStatusChange)
{
mod
@@ -533,17 +550,16 @@ namespace SMAPI.Tests.Core
return mod;
}
- /// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary>
- /// <param name="mod">The mock mod metadata.</param>
- /// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param>
- private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields modRecord = null)
+ /// <summary>Generate a default mod data record.</summary>
+ private ModDataRecord GetModDataRecord()
{
- 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.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>());
+ return new("Default Display Name", new ModDataModel("Sample ID", null, ModWarning.None));
+ }
+
+ /// <summary>Generate a default mod data versioned fields instance.</summary>
+ private ModDataRecordVersionedFields GetModDataRecordVersionedFields()
+ {
+ return new ModDataRecordVersionedFields(this.GetModDataRecord());
}
}
}
diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs
index 457f9fad..a52df607 100644
--- a/src/SMAPI.Tests/Core/TranslationTests.cs
+++ b/src/SMAPI.Tests/Core/TranslationTests.cs
@@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Linq;
using NUnit.Framework;
using StardewModdingAPI;
+using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModHelpers;
+using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewValley;
namespace SMAPI.Tests.Core
@@ -16,7 +21,7 @@ namespace SMAPI.Tests.Core
** Data
*********/
/// <summary>Sample translation text for unit tests.</summary>
- public static string[] Samples = { null, "", " ", "boop", " boop " };
+ public static string?[] Samples = { null, "", " ", "boop", " boop " };
/*********
@@ -32,15 +37,15 @@ namespace SMAPI.Tests.Core
var data = new Dictionary<string, IDictionary<string, string>>();
// act
- ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
+ ITranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "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.");
@@ -54,8 +59,8 @@ namespace SMAPI.Tests.Core
var expected = this.GetExpectedTranslations();
// act
- var actual = new Dictionary<string, Translation[]>();
- TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
+ var actual = new Dictionary<string, Translation[]?>();
+ TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys)
{
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
@@ -79,7 +84,7 @@ namespace SMAPI.Tests.Core
// act
var actual = new Dictionary<string, Translation[]>();
- TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
+ TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
foreach (string locale in expected.Keys)
{
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
@@ -107,16 +112,16 @@ namespace SMAPI.Tests.Core
[TestCase(" ", ExpectedResult = true)]
[TestCase("boop", ExpectedResult = true)]
[TestCase(" boop ", ExpectedResult = true)]
- public bool Translation_HasValue(string text)
+ public bool Translation_HasValue(string? text)
{
return new Translation("pt-BR", "key", text).HasValue();
}
[Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")]
- public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text)
+ 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())
@@ -126,20 +131,20 @@ namespace SMAPI.Tests.Core
}
[Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")]
- public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text)
+ 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())
- Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input.");
+ Assert.AreEqual(text, (string?)translation, "The translation returned an unexpected value given a valid input.");
else
- Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input.");
+ Assert.AreEqual(this.GetPlaceholderText("key"), (string?)translation, "The translation returned an unexpected value given a null or empty input.");
}
[Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")]
- public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text)
+ public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string? text)
{
// act
Translation translation = new Translation("pt-BR", "key", text).UsePlaceholder(value);
@@ -154,7 +159,7 @@ namespace SMAPI.Tests.Core
}
[Test(Description = "Assert that the translation returns the expected text after setting the default.")]
- public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default)
+ public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string? text, [ValueSource(nameof(TranslationTests.Samples))] string? @default)
{
// act
Translation translation = new Translation("pt-BR", "key", text).Default(@default);
@@ -182,7 +187,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":
@@ -190,7 +195,7 @@ namespace SMAPI.Tests.Core
break;
case "class":
- translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end });
+ translation = translation.Tokens(new TokenModel(start, middle, end));
break;
case "IDictionary<string, object>":
@@ -324,21 +329,63 @@ namespace SMAPI.Tests.Core
return string.Format(Translation.PlaceholderText, key);
}
+ /// <summary>Create a fake mod manifest.</summary>
+ private IModMetadata CreateModMetadata()
+ {
+ string id = $"smapi.unit-tests.fake-mod-{Guid.NewGuid():N}";
+
+ string tempPath = Path.Combine(Path.GetTempPath(), id);
+ return new ModMetadata(
+ displayName: "Mod Display Name",
+ directoryPath: tempPath,
+ rootPath: tempPath,
+ manifest: new Manifest(
+ uniqueID: id,
+ name: "Mod Name",
+ author: "Mod Author",
+ description: "Mod Description",
+ version: new SemanticVersion(1, 0, 0)
+ ),
+ dataRecord: null,
+ isIgnored: false
+ );
+ }
+
/*********
** Test models
*********/
/// <summary>A model used to test token support.</summary>
+ [SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Used dynamically via translation helper.")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used dynamically via translation helper.")]
private class TokenModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>A sample token property.</summary>
- public string Start { get; set; }
+ public string Start { get; }
/// <summary>A sample token property.</summary>
- public string Middle { get; set; }
+ public string Middle { get; }
/// <summary>A sample token field.</summary>
public string End;
+
+
+ /*********
+ ** public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="start">A sample token property.</param>
+ /// <param name="middle">A sample token field.</param>
+ /// <param name="end">A sample token property.</param>
+ public TokenModel(string start, string middle, string end)
+ {
+ this.Start = start;
+ this.Middle = middle;
+ this.End = end;
+ }
}
}
}
diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj
index 8329b2e1..67997b30 100644
--- a/src/SMAPI.Tests/SMAPI.Tests.csproj
+++ b/src/SMAPI.Tests/SMAPI.Tests.csproj
@@ -1,21 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <AssemblyName>SMAPI.Tests</AssemblyName>
- <RootNamespace>SMAPI.Tests</RootNamespace>
<TargetFramework>net5.0</TargetFramework>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="..\..\build\common.targets" />
<ItemGroup>
+ <ProjectReference Include="..\SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj" />
+ <ProjectReference Include="..\SMAPI.Tests.ModApiProvider\SMAPI.Tests.ModApiProvider.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
<ProjectReference Include="..\SMAPI\SMAPI.csproj" />
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="FluentAssertions" Version="6.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
diff --git a/src/SMAPI.Tests/Sample.cs b/src/SMAPI.Tests/Sample.cs
index f4f0d88e..9587a100 100644
--- a/src/SMAPI.Tests/Sample.cs
+++ b/src/SMAPI.Tests/Sample.cs
@@ -9,7 +9,7 @@ namespace SMAPI.Tests
** Fields
*********/
/// <summary>A random number generator.</summary>
- private static readonly Random Random = new Random();
+ private static readonly Random Random = new();
/*********
diff --git a/src/SMAPI.Tests/Utilities/KeybindListTests.cs b/src/SMAPI.Tests/Utilities/KeybindListTests.cs
index 0bd6ec17..c4c086de 100644
--- a/src/SMAPI.Tests/Utilities/KeybindListTests.cs
+++ b/src/SMAPI.Tests/Utilities/KeybindListTests.cs
@@ -21,12 +21,12 @@ namespace SMAPI.Tests.Utilities
public void TryParse_SimpleValue(SButton button)
{
// act
- bool success = KeybindList.TryParse($"{button}", out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse($"{button}", out KeybindList? parsed, out string[] errors);
// assert
Assert.IsTrue(success, "Parsing unexpectedly failed.");
Assert.IsNotNull(parsed, "The parsed result should not be null.");
- Assert.AreEqual(parsed.ToString(), $"{button}");
+ Assert.AreEqual(parsed!.ToString(), $"{button}");
Assert.IsNotNull(errors, message: "The errors should never be null.");
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
}
@@ -44,17 +44,17 @@ namespace SMAPI.Tests.Utilities
[TestCase(",", ExpectedResult = "None")]
[TestCase("A,", ExpectedResult = "A")]
[TestCase(",A", ExpectedResult = "A")]
- public string TryParse_MultiValues(string input)
+ public string TryParse_MultiValues(string? input)
{
// act
- bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
// assert
Assert.IsTrue(success, "Parsing unexpectedly failed.");
Assert.IsNotNull(parsed, "The parsed result should not be null.");
Assert.IsNotNull(errors, message: "The errors should never be null.");
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
- return parsed.ToString();
+ return parsed!.ToString();
}
/// <summary>Assert invalid values are rejected.</summary>
@@ -67,7 +67,7 @@ namespace SMAPI.Tests.Utilities
public void TryParse_InvalidValues(string input, string expectedError)
{
// act
- bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
// assert
Assert.IsFalse(success, "Parsing unexpectedly succeeded.");
@@ -98,13 +98,15 @@ namespace SMAPI.Tests.Utilities
public SButtonState GetState(string input, string stateMap)
{
// act
- bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+ bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
if (success && parsed?.Keybinds != null)
{
- foreach (var keybind in parsed.Keybinds)
+ foreach (Keybind? keybind in parsed.Keybinds)
+ {
#pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests
keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap);
#pragma warning restore 618
+ }
}
// assert
@@ -112,7 +114,7 @@ namespace SMAPI.Tests.Utilities
Assert.IsNotNull(parsed, "The parsed result should not be null.");
Assert.IsNotNull(errors, message: "The errors should never be null.");
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
- return parsed.GetState();
+ return parsed!.GetState();
}
diff --git a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs
index ab4c2618..3219d89d 100644
--- a/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs
+++ b/src/SMAPI.Tests/Utilities/PathUtilitiesTests.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using NUnit.Framework;
using StardewModdingAPI.Toolkit.Utilities;
@@ -6,6 +7,7 @@ namespace SMAPI.Tests.Utilities
{
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
[TestFixture]
+ [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are standard game install paths.")]
internal class PathUtilitiesTests
{
/*********
@@ -14,136 +16,125 @@ namespace SMAPI.Tests.Utilities
/// <summary>Sample paths used in unit tests.</summary>
public static readonly SamplePath[] SamplePaths = {
// Windows absolute path
- new SamplePath
- {
- OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
+ new(
+ OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
- Segments = new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
+ Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
- NormalizedOnWindows = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
- NormalizedOnUnix = @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
- },
+ NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
+ NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
+ ),
// Windows absolute path (with trailing slash)
- new SamplePath
- {
- OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
+ new(
+ OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
- Segments = new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
+ Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
- NormalizedOnWindows = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
- NormalizedOnUnix = @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
- },
+ NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
+ NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
+ ),
// Windows relative path
- new SamplePath
- {
- OriginalPath = @"Content\Characters\Dialogue\Abigail",
+ new(
+ OriginalPath: @"Content\Characters\Dialogue\Abigail",
- Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
- SegmentsLimit3 = new [] { "Content", "Characters", @"Dialogue\Abigail" },
+ Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
+ SegmentsLimit3: new [] { "Content", "Characters", @"Dialogue\Abigail" },
- NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail",
- NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail"
- },
+ NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
+ NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
+ ),
// Windows relative path (with directory climbing)
- new SamplePath
- {
- OriginalPath = @"..\..\Content",
+ new(
+ OriginalPath: @"..\..\Content",
- Segments = new [] { "..", "..", "Content" },
- SegmentsLimit3 = new [] { "..", "..", "Content" },
+ Segments: new [] { "..", "..", "Content" },
+ SegmentsLimit3: new [] { "..", "..", "Content" },
- NormalizedOnWindows = @"..\..\Content",
- NormalizedOnUnix = @"../../Content"
- },
+ NormalizedOnWindows: @"..\..\Content",
+ NormalizedOnUnix: @"../../Content"
+ ),
// Windows UNC path
- new SamplePath
- {
- OriginalPath = @"\\unc\path",
+ new(
+ OriginalPath: @"\\unc\path",
- Segments = new [] { "unc", "path" },
- SegmentsLimit3 = new [] { "unc", "path" },
+ Segments: new [] { "unc", "path" },
+ SegmentsLimit3: new [] { "unc", "path" },
- NormalizedOnWindows = @"\\unc\path",
- NormalizedOnUnix = "/unc/path" // there's no good way to normalize this on Unix since UNC paths aren't supported; path normalization is meant for asset names anyway, so this test only ensures it returns some sort of sane value
- },
+ NormalizedOnWindows: @"\\unc\path",
+ NormalizedOnUnix: "/unc/path" // there's no good way to normalize this on Unix since UNC paths aren't supported; path normalization is meant for asset names anyway, so this test only ensures it returns some sort of sane value
+ ),
// Linux absolute path
- new SamplePath
- {
- OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley",
+ new(
+ OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley",
- Segments = new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
+ Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
- NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley",
- NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley"
- },
+ NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley",
+ NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley"
+ ),
// Linux absolute path (with trailing slash)
- new SamplePath
- {
- OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley/",
+ new(
+ OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley/",
- Segments = new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
+ Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
- NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley\",
- NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley/"
- },
+ NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley\",
+ NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley/"
+ ),
// Linux absolute path (with ~)
- new SamplePath
- {
- OriginalPath = @"~/.steam/steam/steamapps/common/Stardew Valley",
+ new(
+ OriginalPath: @"~/.steam/steam/steamapps/common/Stardew Valley",
- Segments = new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
- SegmentsLimit3 = new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
+ Segments: new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
+ SegmentsLimit3: new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
- NormalizedOnWindows = @"~\.steam\steam\steamapps\common\Stardew Valley",
- NormalizedOnUnix = @"~/.steam/steam/steamapps/common/Stardew Valley"
- },
+ NormalizedOnWindows: @"~\.steam\steam\steamapps\common\Stardew Valley",
+ NormalizedOnUnix: @"~/.steam/steam/steamapps/common/Stardew Valley"
+ ),
// Linux relative path
- new SamplePath
- {
- OriginalPath = @"Content/Characters/Dialogue/Abigail",
+ new(
+ OriginalPath: @"Content/Characters/Dialogue/Abigail",
- Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
- SegmentsLimit3 = new [] { "Content", "Characters", "Dialogue/Abigail" },
+ Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
+ SegmentsLimit3: new [] { "Content", "Characters", "Dialogue/Abigail" },
- NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail",
- NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail"
- },
+ NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
+ NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
+ ),
// Linux relative path (with directory climbing)
- new SamplePath
- {
- OriginalPath = @"../../Content",
+ new(
+ OriginalPath: @"../../Content",
- Segments = new [] { "..", "..", "Content" },
- SegmentsLimit3 = new [] { "..", "..", "Content" },
+ Segments: new [] { "..", "..", "Content" },
+ SegmentsLimit3: new [] { "..", "..", "Content" },
- NormalizedOnWindows = @"..\..\Content",
- NormalizedOnUnix = @"../../Content"
- },
+ NormalizedOnWindows: @"..\..\Content",
+ NormalizedOnUnix: @"../../Content"
+ ),
// Mixed directory separators
- new SamplePath
- {
- OriginalPath = @"C:\some/mixed\path/separators",
+ new(
+ OriginalPath: @"C:\some/mixed\path/separators",
- Segments = new [] { "C:", "some", "mixed", "path", "separators" },
- SegmentsLimit3 = new [] { "C:", "some", @"mixed\path/separators" },
+ Segments: new [] { "C:", "some", "mixed", "path", "separators" },
+ SegmentsLimit3: new [] { "C:", "some", @"mixed\path/separators" },
- NormalizedOnWindows = @"C:\some\mixed\path\separators",
- NormalizedOnUnix = @"C:/some/mixed/path/separators"
- },
+ NormalizedOnWindows: @"C:\some\mixed\path\separators",
+ NormalizedOnUnix: @"C:/some/mixed/path/separators"
+ )
};
@@ -281,14 +272,14 @@ namespace SMAPI.Tests.Utilities
/*********
** Private classes
*********/
- public class SamplePath
+ /// <summary>A sample path in multiple formats.</summary>
+ /// <param name="OriginalPath">The original path to pass to the <see cref="PathUtilities"/>.</param>
+ /// <param name="Segments">The normalized path segments.</param>
+ /// <param name="SegmentsLimit3">The normalized path segments, if we stop segmenting after the second one.</param>
+ /// <param name="NormalizedOnWindows">The normalized form on Windows.</param>
+ /// <param name="NormalizedOnUnix">The normalized form on Linux or macOS.</param>
+ public record SamplePath(string OriginalPath, string[] Segments, string[] SegmentsLimit3, string NormalizedOnWindows, string NormalizedOnUnix)
{
- public string OriginalPath { get; set; }
- public string[] Segments { get; set; }
- public string[] SegmentsLimit3 { get; set; }
- public string NormalizedOnWindows { get; set; }
- public string NormalizedOnUnix { get; set; }
-
public override string ToString()
{
return this.OriginalPath;
diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs
index 0461952e..b9c3d202 100644
--- a/src/SMAPI.Tests/Utilities/SDateTests.cs
+++ b/src/SMAPI.Tests/Utilities/SDateTests.cs
@@ -16,9 +16,12 @@ namespace SMAPI.Tests.Utilities
/*********
** Fields
*********/
- /// <summary>All valid seasons.</summary>
+ /// <summary>The valid seasons.</summary>
private static readonly string[] ValidSeasons = { "spring", "summer", "fall", "winter" };
+ /// <summary>Sample user inputs for season names.</summary>
+ private static readonly string[] SampleSeasonValues = SDateTests.ValidSeasons.Concat(new[] { " WIntEr " }).ToArray();
+
/// <summary>All valid days of a month.</summary>
private static readonly int[] ValidDays = Enumerable.Range(1, 28).ToArray();
@@ -55,19 +58,18 @@ namespace SMAPI.Tests.Utilities
** Constructor
****/
[Test(Description = "Assert that the constructor sets the expected values for all valid dates.")]
- public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.ValidSeasons))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year)
+ public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.SampleSeasonValues))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year)
{
// act
- SDate date = new SDate(day, season, year);
+ SDate date = new(day, season, year);
// assert
Assert.AreEqual(day, date.Day);
- Assert.AreEqual(season, date.Season);
+ Assert.AreEqual(season.Trim().ToLowerInvariant(), date.Season);
Assert.AreEqual(year, date.Year);
}
[Test(Description = "Assert that the constructor throws an exception if the values are invalid.")]
- [TestCase(01, "Spring", 1)] // seasons are case-sensitive
[TestCase(01, "springs", 1)] // invalid season name
[TestCase(-1, "spring", 1)] // day < 0
[TestCase(0, "spring", 1)] // day zero
@@ -252,9 +254,9 @@ namespace SMAPI.Tests.Utilities
{
foreach (int day in SDateTests.ValidDays)
{
- SDate date = new SDate(day, season, year);
+ SDate date = new(day, season, year);
int hash = date.GetHashCode();
- if (hashes.TryGetValue(hash, out SDate otherDate))
+ if (hashes.TryGetValue(hash, out SDate? otherDate))
Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}.");
if (hash < lastHash)
Assert.Fail($"Received smaller hash code for date {date} ({hash}) relative to {hashes[lastHash]} ({lastHash}).");
@@ -294,7 +296,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
- public bool Operators_Equals(string now, string other)
+ public bool Operators_Equals(string? now, string other)
{
return this.GetDate(now) == this.GetDate(other);
}
@@ -308,7 +310,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
- public bool Operators_NotEquals(string now, string other)
+ public bool Operators_NotEquals(string? now, string other)
{
return this.GetDate(now) != this.GetDate(other);
}
@@ -322,7 +324,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
- public bool Operators_LessThan(string now, string other)
+ public bool Operators_LessThan(string? now, string other)
{
return this.GetDate(now) < this.GetDate(other);
}
@@ -336,7 +338,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
- public bool Operators_LessThanOrEqual(string now, string other)
+ public bool Operators_LessThanOrEqual(string? now, string other)
{
return this.GetDate(now) <= this.GetDate(other);
}
@@ -350,7 +352,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
- public bool Operators_MoreThan(string now, string other)
+ public bool Operators_MoreThan(string? now, string other)
{
return this.GetDate(now) > this.GetDate(other);
}
@@ -364,7 +366,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
- public bool Operators_MoreThanOrEqual(string now, string other)
+ public bool Operators_MoreThanOrEqual(string? now, string other)
{
return this.GetDate(now) > this.GetDate(other);
}
@@ -375,7 +377,8 @@ namespace SMAPI.Tests.Utilities
*********/
/// <summary>Convert a string date into a game date, to make unit tests easier to read.</summary>
/// <param name="dateStr">The date string like "dd MMMM yy".</param>
- private SDate GetDate(string dateStr)
+ [return: NotNullIfNotNull("dateStr")]
+ private SDate? GetDate(string? dateStr)
{
if (dateStr == null)
return null;
diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs
index ac4ef39b..fedadba6 100644
--- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs
+++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs
@@ -61,10 +61,10 @@ namespace SMAPI.Tests.Utilities
[TestCase("apple")]
[TestCase("-apple")]
[TestCase("-5")]
- public void Constructor_FromString_WithInvalidValues(string input)
+ public void Constructor_FromString_WithInvalidValues(string? input)
{
if (input == null)
- this.AssertAndLogException<ArgumentNullException>(() => new SemanticVersion(input));
+ this.AssertAndLogException<ArgumentNullException>(() => new SemanticVersion(input!));
else
this.AssertAndLogException<FormatException>(() => new SemanticVersion(input));
}
@@ -91,7 +91,7 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.2.3.4-some-tag.4 ")]
public void Constructor_FromString_Standard_DisallowsNonStandardVersion(string input)
{
- Assert.Throws<FormatException>(() => new SemanticVersion(input));
+ Assert.Throws<FormatException>(() => _ = new SemanticVersion(input));
}
/// <summary>Assert the parsed version when constructed from standard parts.</summary>
@@ -110,7 +110,7 @@ namespace SMAPI.Tests.Utilities
[TestCase(1, 2, 3, "some-tag.4 ", null, ExpectedResult = "1.2.3-some-tag.4")]
[TestCase(1, 2, 3, "some-tag.4 ", "build.004", ExpectedResult = "1.2.3-some-tag.4+build.004")]
[TestCase(1, 2, 0, null, "3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")]
- public string Constructor_FromParts(int major, int minor, int patch, string prerelease, string build)
+ public string Constructor_FromParts(int major, int minor, int patch, string? prerelease, string? build)
{
// act
ISemanticVersion version = new SemanticVersion(major, minor, patch, prerelease, build);
@@ -217,11 +217,16 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = 1)]
[TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = 1)]
[TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = 1)]
- public int CompareTo(string versionStrA, string versionStrB)
+
+ // null
+ [TestCase("1.0.0", null, ExpectedResult = 1)] // null is always less than any value per CompareTo remarks
+ public int CompareTo(string versionStrA, string? versionStrB)
{
// arrange
ISemanticVersion versionA = new SemanticVersion(versionStrA);
- ISemanticVersion versionB = new SemanticVersion(versionStrB);
+ ISemanticVersion? versionB = versionStrB != null
+ ? new SemanticVersion(versionStrB)
+ : null;
// assert
return versionA.CompareTo(versionB);
@@ -260,14 +265,19 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = false)]
[TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = false)]
[TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = false)]
- public bool IsOlderThan(string versionStrA, string versionStrB)
+
+ // null
+ [TestCase("1.0.0", null, ExpectedResult = false)] // null is always less than any value per CompareTo remarks
+ public bool IsOlderThan(string versionStrA, string? versionStrB)
{
// arrange
ISemanticVersion versionA = new SemanticVersion(versionStrA);
- ISemanticVersion versionB = new SemanticVersion(versionStrB);
+ ISemanticVersion? versionB = versionStrB != null
+ ? new SemanticVersion(versionStrB)
+ : null;
// assert
- Assert.AreEqual(versionA.IsOlderThan(versionB), versionA.IsOlderThan(versionB.ToString()), "The two signatures returned different results.");
+ Assert.AreEqual(versionA.IsOlderThan(versionB), versionA.IsOlderThan(versionB?.ToString()), "The two signatures returned different results.");
return versionA.IsOlderThan(versionB);
}
@@ -304,14 +314,19 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.0-beta.2", "1.0-beta.1", ExpectedResult = true)]
[TestCase("1.0-beta.10", "1.0-beta.2", ExpectedResult = true)]
[TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = true)]
- public bool IsNewerThan(string versionStrA, string versionStrB)
+
+ // null
+ [TestCase("1.0.0", null, ExpectedResult = true)] // null is always less than any value per CompareTo remarks
+ public bool IsNewerThan(string versionStrA, string? versionStrB)
{
// arrange
ISemanticVersion versionA = new SemanticVersion(versionStrA);
- ISemanticVersion versionB = new SemanticVersion(versionStrB);
+ ISemanticVersion? versionB = versionStrB != null
+ ? new SemanticVersion(versionStrB)
+ : null;
// assert
- Assert.AreEqual(versionA.IsNewerThan(versionB), versionA.IsNewerThan(versionB.ToString()), "The two signatures returned different results.");
+ Assert.AreEqual(versionA.IsNewerThan(versionB), versionA.IsNewerThan(versionB?.ToString()), "The two signatures returned different results.");
return versionA.IsNewerThan(versionB);
}
@@ -322,7 +337,7 @@ namespace SMAPI.Tests.Utilities
/// <param name="versionStr">The main version.</param>
/// <param name="lowerStr">The lower version number.</param>
/// <param name="upperStr">The upper version number.</param>
- [Test(Description = "Assert that version.IsNewerThan returns the expected value.")]
+ [Test(Description = "Assert that version.IsBetween returns the expected value.")]
// is between
[TestCase("0.5.7-beta.3", "0.5.7-beta.3", "0.5.7-beta.3", ExpectedResult = true)]
[TestCase("1.0", "1.0", "1.1", ExpectedResult = true)]
@@ -330,6 +345,7 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.0", "0.5", "1.1", ExpectedResult = true)]
[TestCase("1.0-beta.2", "1.0-beta.1", "1.0-beta.3", ExpectedResult = true)]
[TestCase("1.0-beta-2", "1.0-beta-1", "1.0-beta-3", ExpectedResult = true)]
+ [TestCase("1.0.0", null, "1.0.0", ExpectedResult = true)] // null is always less than any value per CompareTo remarks
// is not between
[TestCase("1.0-beta", "1.0", "1.1", ExpectedResult = false)]
@@ -337,15 +353,20 @@ namespace SMAPI.Tests.Utilities
[TestCase("1.0-beta.2", "1.1", "1.0", ExpectedResult = false)]
[TestCase("1.0-beta.2", "1.0-beta.10", "1.0-beta.3", ExpectedResult = false)]
[TestCase("1.0-beta-2", "1.0-beta-10", "1.0-beta-3", ExpectedResult = false)]
- public bool IsBetween(string versionStr, string lowerStr, string upperStr)
+ [TestCase("1.0.0", "1.0.0", null, ExpectedResult = false)] // null is always less than any value per CompareTo remarks
+ public bool IsBetween(string versionStr, string? lowerStr, string? upperStr)
{
// arrange
- ISemanticVersion lower = new SemanticVersion(lowerStr);
- ISemanticVersion upper = new SemanticVersion(upperStr);
+ ISemanticVersion? lower = lowerStr != null
+ ? new SemanticVersion(lowerStr)
+ : null;
+ ISemanticVersion? upper = upperStr != null
+ ? new SemanticVersion(upperStr)
+ : null;
ISemanticVersion version = new SemanticVersion(versionStr);
// assert
- Assert.AreEqual(version.IsBetween(lower, upper), version.IsBetween(lower.ToString(), upper.ToString()), "The two signatures returned different results.");
+ Assert.AreEqual(version.IsBetween(lower, upper), version.IsBetween(lower?.ToString(), upper?.ToString()), "The two signatures returned different results.");
return version.IsBetween(lower, upper);
}
@@ -395,7 +416,7 @@ namespace SMAPI.Tests.Utilities
public void GameVersion(string versionStr)
{
// act
- GameVersion version = new GameVersion(versionStr);
+ GameVersion version = new(versionStr);
// assert
Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value.");
@@ -413,7 +434,7 @@ namespace SMAPI.Tests.Utilities
/// <param name="prerelease">The prerelease tag.</param>
/// <param name="build">The build metadata.</param>
/// <param name="nonStandard">Whether the version should be marked as non-standard.</param>
- private void AssertParts(ISemanticVersion version, int major, int minor, int patch, string prerelease, string build, bool nonStandard)
+ private void AssertParts(ISemanticVersion version, int major, int minor, int patch, string? prerelease, string? build, bool nonStandard)
{
Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match.");
Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match.");
@@ -426,9 +447,8 @@ namespace SMAPI.Tests.Utilities
/// <summary>Assert that the expected exception type is thrown, and log the action output and thrown exception.</summary>
/// <typeparam name="T">The expected exception type.</typeparam>
/// <param name="action">The action which may throw the exception.</param>
- /// <param name="message">The message to log if the expected exception isn't thrown.</param>
[SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")]
- private void AssertAndLogException<T>(Func<object> action, string message = null)
+ private void AssertAndLogException<T>(Func<object> action)
where T : Exception
{
this.AssertAndLogException<T>(() =>
@@ -443,7 +463,7 @@ namespace SMAPI.Tests.Utilities
/// <param name="action">The action which may throw the exception.</param>
/// <param name="message">The message to log if the expected exception isn't thrown.</param>
[SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The message argument is deliberately only used in precondition checks since this is an assertion method.")]
- private void AssertAndLogException<T>(Action action, string message = null)
+ private void AssertAndLogException<T>(Action action, string? message = null)
where T : Exception
{
try
@@ -455,7 +475,7 @@ namespace SMAPI.Tests.Utilities
TestContext.WriteLine($"Exception thrown:\n{ex}");
return;
}
- catch (Exception ex) when (!(ex is AssertionException))
+ catch (Exception ex) when (ex is not AssertionException)
{
TestContext.WriteLine($"Exception thrown:\n{ex}");
Assert.Fail(message ?? $"Didn't throw the expected exception; expected {typeof(T).FullName}, got {ex.GetType().FullName}.");
diff --git a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs
index b896b09c..8e7e1fb8 100644
--- a/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs
+++ b/src/SMAPI.Tests/WikiClient/ChangeDescriptorTests.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Linq;
using NUnit.Framework;
using StardewModdingAPI;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
@@ -21,15 +20,14 @@ namespace SMAPI.Tests.WikiClient
{
// arrange
string rawDescriptor = "-Nexus:2400, -B, XX → YY, Nexus:451,+A, XXX → YYY, invalidA →, → invalidB";
- string[] expectedAdd = new[] { "Nexus:451", "A" };
- string[] expectedRemove = new[] { "Nexus:2400", "B" };
+ string[] expectedAdd = { "Nexus:451", "A" };
+ string[] expectedRemove = { "Nexus:2400", "B" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{
["XX"] = "YY",
["XXX"] = "YYY"
};
- string[] expectedErrors = new[]
- {
+ string[] expectedErrors = {
"Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.",
"Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value."
};
@@ -49,15 +47,14 @@ namespace SMAPI.Tests.WikiClient
{
// arrange
string rawDescriptor = "-1.0.1, -2.0-beta, 1.00 → 1.0, 1.0.0,+2.0-beta.15, 2.0 → 2.0-beta, invalidA →, → invalidB";
- string[] expectedAdd = new[] { "1.0.0", "2.0.0-beta.15" };
- string[] expectedRemove = new[] { "1.0.1", "2.0.0-beta" };
+ string[] expectedAdd = { "1.0.0", "2.0.0-beta.15" };
+ string[] expectedRemove = { "1.0.1", "2.0.0-beta" };
IDictionary<string, string> expectedReplace = new Dictionary<string, string>
{
["1.00"] = "1.0.0",
["2.0.0"] = "2.0.0-beta"
};
- string[] expectedErrors = new[]
- {
+ string[] expectedErrors = {
"Failed parsing ' invalidA →': can't map to a blank value. Use the '-value' format to remove a value.",
"Failed parsing ' → invalidB': can't map from a blank old value. Use the '+value' format to add a value."
};
@@ -66,7 +63,7 @@ namespace SMAPI.Tests.WikiClient
ChangeDescriptor parsed = ChangeDescriptor.Parse(
rawDescriptor,
out string[] errors,
- formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion version)
+ formatValue: raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version)
? version.ToString()
: raw
);
@@ -110,9 +107,9 @@ namespace SMAPI.Tests.WikiClient
[TestCase("", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:A, Nexus:B")]
[TestCase("Nexus:2400", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:A, Nexus:B")]
[TestCase("Nexus:2400, Nexus:2401, Nexus:B,Chucklefish:14", "+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "Nexus:2401, Nexus:2401, Nexus:B, Nexus:A")]
- public string Apply_Raw(string input, string descriptor)
+ public string Apply_Raw(string input, string? descriptor)
{
- var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);
+ ChangeDescriptor parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);
Assert.IsEmpty(errors, "Parsing the descriptor failed.");
@@ -127,7 +124,7 @@ namespace SMAPI.Tests.WikiClient
[TestCase("-Nexus:2400", ExpectedResult = "-Nexus:2400")]
[TestCase(" Nexus:2400 →Nexus:2401 ", ExpectedResult = "Nexus:2400 → Nexus:2401")]
[TestCase("+Nexus:A, Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A→Nexus:B", ExpectedResult = "+Nexus:A, +Nexus:B, -Chucklefish:14, Nexus:2400 → Nexus:2401, Nexus:A → Nexus:B")]
- public string ToString(string descriptor)
+ public string ToString(string? descriptor)
{
var parsed = ChangeDescriptor.Parse(descriptor, out string[] errors);