summaryrefslogtreecommitdiff
path: root/src/SMAPI.Tests/Core
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-05-01 18:16:09 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-05-01 18:16:09 -0400
commitc8ad50dad1d706a1901798f9396f6becfea36c0e (patch)
tree28bd818a5db39ec5ece1bd141a28de955950463b /src/SMAPI.Tests/Core
parent451b70953ff4c0b1b27ae0de203ad99379b45b2a (diff)
parentf78093bdb58d477b400cde3f19b70ffd6ddf833d (diff)
downloadSMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.gz
SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.bz2
SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Tests/Core')
-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
4 files changed, 834 insertions, 77 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 V