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
{
/// Unit tests for .
[TestFixture]
internal class InterfaceProxyTests
{
/*********
** Fields
*********/
/// The mod ID providing an API.
private readonly string FromModId = "From.ModId";
/// The mod ID consuming an API.
private readonly string ToModId = "From.ModId";
/// The random number generator with which to create sample values.
private readonly Random Random = new();
/*********
** Unit tests
*********/
/****
** Events
****/
/// Assert that an event field can be proxied correctly.
[Test]
public void CanProxy_EventField()
{
// arrange
ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next();
// act
ISimpleApi proxy = this.GetProxy(implementation);
new ApiConsumer().UseEventField(proxy, out Func<(int timesCalled, int lastValue)> getValues);
providerMod.RaiseEvent(expectedValue);
(int timesCalled, int lastValue) = getValues();
// assert
timesCalled.Should().Be(1, "Expected the proxied event to be raised once.");
lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised.");
}
/// Assert that an event property can be proxied correctly.
[Test]
public void CanProxy_EventProperty()
{
// arrange
ProviderMod providerMod = new();
object implementation = providerMod.GetModApi();
int expectedValue = this.Random.Next();
// act
ISimpleApi proxy = this.GetProxy(implementation);
new ApiConsumer().UseEventProperty(proxy, out Func<(int timesCalled, int lastValue)> getValues);
providerMod.RaiseEvent(expectedValue);
(int timesCalled, int lastValue) = getValues();
// assert
timesCalled.Should().Be(1, "Expected the proxied event to be raised once.");
lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised.");
}
/****
** Properties
****/
/// Assert that properties can be proxied correctly.
/// Whether to set the properties through the provider mod or proxy interface.
[TestCase("set via provider mod")]
[TestCase("set via proxy interface")]
public void CanProxy_Properties(string setVia)
{
// arrange
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(implementation);
switch (setVia)
{
case "set via provider mod":
providerMod.SetPropertyValues(
number: expectedNumber,
obj: expectedObject,
listValue: expectedListValue,
listWithInterfaceValue: expectedListWithInterfaceValue,
dictionaryKey: expectedDictionaryKey,
dictionaryListValue: expectedDictionaryListValue,
enumValue: expectedEnum,
inheritedValue: expectedInheritedString
);
break;
case "set via proxy interface":
proxy.NumberProperty = expectedNumber;
proxy.ObjectProperty = expectedObject;
proxy.ListProperty = new() { expectedListValue };
proxy.ListPropertyWithInterface = new List { expectedListWithInterfaceValue };
proxy.GenericsProperty = new Dictionary>
{
[expectedDictionaryKey] = new List { expectedDictionaryListValue }
};
proxy.EnumProperty = expectedEnum;
proxy.InheritedProperty = expectedInheritedString;
break;
default:
throw new InvalidOperationException($"Invalid 'set via' option '{setVia}.");
}
// assert number
this
.GetPropertyValue(implementation, nameof(proxy.NumberProperty))
.Should().Be(expectedNumber);
proxy.NumberProperty
.Should().Be(expectedNumber);
// assert object
this
.GetPropertyValue(implementation, nameof(proxy.ObjectProperty))
.Should().Be(expectedObject);
proxy.ObjectProperty
.Should().Be(expectedObject);
// assert list
(this.GetPropertyValue(implementation, nameof(proxy.ListProperty)) as IList)
.Should().NotBeNull()
.And.HaveCount(1)
.And.BeEquivalentTo(expectedListValue);
proxy.ListProperty
.Should().NotBeNull()
.And.HaveCount(1)
.And.BeEquivalentTo(expectedListValue);
// assert list with interface
(this.GetPropertyValue(implementation, nameof(proxy.ListPropertyWithInterface)) as IList)
.Should().NotBeNull()
.And.HaveCount(1)
.And.BeEquivalentTo(expectedListWithInterfaceValue);
proxy.ListPropertyWithInterface
.Should().NotBeNull()
.And.HaveCount(1)
.And.BeEquivalentTo(expectedListWithInterfaceValue);
// assert generics
(this.GetPropertyValue(implementation, nameof(proxy.GenericsProperty)) as IDictionary>)
.Should().NotBeNull()
.And.HaveCount(1)
.And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue);
proxy.GenericsProperty
.Should().NotBeNull()
.And.HaveCount(1)
.And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue);
// assert enum
this
.GetPropertyValue(implementation, nameof(proxy.EnumProperty))
.Should().Be(expectedEnum);
proxy.EnumProperty
.Should().Be(expectedEnum);
// assert getter
this
.GetPropertyValue(implementation, nameof(proxy.GetterProperty))
.Should().Be(42);
proxy.GetterProperty
.Should().Be(42);
// assert inherited methods
this
.GetPropertyValue(implementation, nameof(proxy.InheritedProperty))
.Should().Be(expectedInheritedString);
proxy.InheritedProperty
.Should().Be(expectedInheritedString);
}
/// Assert that a simple method with no return value can be proxied correctly.
[Test]
public void CanProxy_SimpleMethod_Void()
{
// arrange
object implementation = new ProviderMod().GetModApi();
// act
ISimpleApi proxy = this.GetProxy(implementation);
proxy.GetNothing();
}
/// Assert that a simple int method can be proxied correctly.
[Test]
public void CanProxy_SimpleMethod_Int()
{
// arrange
object implementation = new ProviderMod().GetModApi();
int expectedValue = this.Random.Next();
// act
ISimpleApi proxy = this.GetProxy(implementation);
int actualValue = proxy.GetInt(expectedValue);
// assert
actualValue.Should().Be(expectedValue);
}
/// Assert that a simple object method can be proxied correctly.
[Test]
public void CanProxy_SimpleMethod_Object()
{
// arrange
object implementation = new ProviderMod().GetModApi();
object expectedValue = new();
// act
ISimpleApi proxy = this.GetProxy(implementation);
object actualValue = proxy.GetObject(expectedValue);
// assert
actualValue.Should().BeSameAs(expectedValue);
}
/// Assert that a simple list method can be proxied correctly.
[Test]
public void CanProxy_SimpleMethod_List()
{
// arrange
object implementation = new ProviderMod().GetModApi();
string expectedValue = this.GetRandomString();
// act
ISimpleApi proxy = this.GetProxy(implementation);
IList actualValue = proxy.GetList(expectedValue);
// assert
actualValue.Should().BeEquivalentTo(expectedValue);
}
/// Assert that a simple list with interface method can be proxied correctly.
[Test]
public void CanProxy_SimpleMethod_ListWithInterface()
{
// arrange
object implementation = new ProviderMod().GetModApi();
string expectedValue = this.GetRandomString();
// act
ISimpleApi proxy = this.GetProxy(implementation);
IList actualValue = proxy.GetListWithInterface(expectedValue);
// assert
actualValue.Should().BeEquivalentTo(expectedValue);
}
/// Assert that a simple method which returns generic types can be proxied correctly.
[Test]
public void CanProxy_SimpleMethod_GenericTypes()
{
// arrange
object implementation = new ProviderMod().GetModApi();
string expectedKey = this.GetRandomString();
string expectedValue = this.GetRandomString();
// act
ISimpleApi proxy = this.GetProxy(implementation);
IDictionary> actualValue = proxy.GetGenerics(expectedKey, expectedValue);
// assert
actualValue
.Should().NotBeNull()
.And.HaveCount(1)
.And.ContainKey(expectedKey).WhoseValue.Should().BeEquivalentTo(expectedValue);
}
/// Assert that a simple lambda method can be proxied correctly.
[Test]
[SuppressMessage("ReSharper", "ConvertToLocalFunction")]
public void CanProxy_SimpleMethod_Lambda()
{
// arrange
object implementation = new ProviderMod().GetModApi();
Func expectedValue = _ => "test";
// act
ISimpleApi proxy = this.GetProxy(implementation);
object actualValue = proxy.GetObject(expectedValue);
// assert
actualValue.Should().BeSameAs(expectedValue);
}
/// Assert that a method with out parameters can be proxied correctly.
[Test]
[SuppressMessage("ReSharper", "ConvertToLocalFunction")]
public void CanProxy_Method_OutParameters()
{
// arrange
object implementation = new ProviderMod().GetModApi();
const int expectedNumber = 42;
// act
ISimpleApi proxy = this.GetProxy(implementation);
bool result = proxy.TryGetOutParameter(
inputNumber: expectedNumber,
out int outNumber,
out string outString,
out PerScreen outReference,
out IDictionary> 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
*********/
/// Get a property value from an instance.
/// The instance whose property to read.
/// The property name.
private object? GetPropertyValue(object parent, string name)
{
if (parent is null)
throw new ArgumentNullException(nameof(parent));
Type type = parent.GetType();
PropertyInfo? property = type.GetProperty(name);
if (property is null)
throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'.");
return property.GetValue(parent);
}
/// Get a random test string.
private string GetRandomString()
{
return this.Random.Next().ToString();
}
/// Get a proxy API instance.
/// The underlying API instance.
private ISimpleApi GetProxy(object implementation)
{
var proxyFactory = new InterfaceProxyFactory();
return proxyFactory.CreateProxy(implementation, this.FromModId, this.ToModId);
}
}
}