Skip to content

Commit

Permalink
Add static Redirect proxy methods
Browse files Browse the repository at this point in the history
  • Loading branch information
devodo committed Dec 18, 2023
1 parent 2c16d45 commit 3108e4f
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 45 deletions.
4 changes: 2 additions & 2 deletions src/DivertR/ISpy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace DivertR
///
/// <see cref="ISpy"/> inherits from <see cref="IRedirect"/> and therefore its Mock behaviour can be configured using the same Redirect fluent interface e.g. to add one or more <see cref="IVia"/> instances or reset.
/// The Spy is preconfigured with a Via that records the mock object calls readable from the <see cref="ISpy.Calls"/> property.
/// As with a normal Redirect, Spy reset removes all Vias, however it also adds a new Mock call record Via and the <see cref="ISpy.Calls"/> property is replaced.
/// Spy.Reset behaves the sames as the base Redirect class and removes all Vias except a new call record Via is then added back. Reset also sets a new <see cref="ISpy.Calls"/> property.
/// </summary>
public interface ISpy : IRedirect
{
Expand All @@ -26,7 +26,7 @@ public interface ISpy : IRedirect
IRecordStream Calls { get; }

/// <summary>
/// Insert a <see cref="IVia"/> into this Spy.
/// Insert an <see cref="IVia"/> into this Spy.
/// </summary>
/// <param name="via">The Via instance to insert.</param>
/// <param name="optionsAction">Optional <see cref="IViaOptionsBuilder"/> action.</param>
Expand Down
30 changes: 30 additions & 0 deletions src/DivertR/Internal/RedirectTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace DivertR.Internal
{
internal class RedirectTracker
{
private readonly ConditionalWeakTable<object, IRedirect> _redirectTable = new();

public void AddRedirect<TTarget>(Redirect<TTarget> redirect, [DisallowNull] TTarget proxy) where TTarget : class?
{
_redirectTable.Add(proxy, redirect);
}

public Redirect<TTarget> GetRedirect<TTarget>([DisallowNull] TTarget proxy) where TTarget : class?
{
if (!_redirectTable.TryGetValue(proxy, out var redirect))
{
throw new DiverterException("Redirect not found");
}

if (redirect is not Redirect<TTarget> redirectOf)
{
throw new DiverterException($"Redirect target type: {redirect.RedirectId.Type} does not match proxy type: {typeof(TTarget)}");
}

return redirectOf;
}
}
}
30 changes: 0 additions & 30 deletions src/DivertR/Internal/SpyTracker.cs

This file was deleted.

62 changes: 60 additions & 2 deletions src/DivertR/Redirect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,19 @@ public TTarget Proxy(TTarget? root)
{
if (root is null || !RedirectSet.Settings.CacheRedirectProxies)
{
return _proxyFactory.CreateProxy(_redirectProxyCall, root);
var createdProxy = _proxyFactory.CreateProxy(_redirectProxyCall, root);
Redirect.Track(this, createdProxy);

return createdProxy;
}

var proxy = _proxyCache.GetValue(root, x => _proxyFactory.CreateProxy(_redirectProxyCall, x));
var proxy = _proxyCache.GetValue(root, x =>
{
var createdProxy = _proxyFactory.CreateProxy(_redirectProxyCall, x);
Redirect.Track(this, createdProxy);

return createdProxy;
});

return proxy!;
}
Expand Down Expand Up @@ -250,4 +259,53 @@ protected virtual void ResetInternal()
RedirectRepository.Reset();
}
}

/// <summary>
/// Redirect helper class for creating proxy objects directly.
/// </summary>
public static class Redirect
{
private static readonly RedirectTracker RedirectTracker = new();

/// <summary>
/// Creates and returns a <see cref="Redirect{TTarget}"/> proxy of the given target type.
/// </summary>
/// <typeparam name="TTarget">The proxy target type.</typeparam>
/// <returns>The created Redirect proxy object.</returns>
public static TTarget Proxy<TTarget>() where TTarget : class?
{
var redirect = new Redirect<TTarget>();

return redirect.Proxy();
}

/// <summary>
/// Creates and returns a <see cref="Redirect{TTarget}"/> proxy of the given target type.
/// </summary>
/// /// <typeparam name="TTarget">The proxy target type.</typeparam>
/// <param name="root">The root instance the proxy will wrap and relay calls to.</param>
/// <returns>The created Redirect proxy object.</returns>
public static TTarget Proxy<TTarget>(TTarget? root) where TTarget : class?
{
var redirect = new Redirect<TTarget>();

return redirect.Proxy(root);
}

/// <summary>
/// Gets the <see cref="IRedirect{TTarget}"/> of the proxy object.
/// </summary>
/// <param name="proxy">The Redirect proxy object.</param>
/// <returns>The Redirect instance.</returns>
/// <exception cref="DiverterException">Thrown if if the given <paramref name="proxy"/> object does not have an associated <see cref="IRedirect{TTarget}"/> </exception>
public static IRedirect<TTarget> Of<TTarget>([DisallowNull] TTarget proxy) where TTarget : class?
{
return RedirectTracker.GetRedirect<TTarget>(proxy);
}

internal static void Track<TTarget>(Redirect<TTarget> redirect, [DisallowNull] TTarget proxy) where TTarget : class?
{
RedirectTracker.AddRedirect(redirect, proxy);
}
}
}
2 changes: 1 addition & 1 deletion src/DivertR/RedirectId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public bool Equals(RedirectId other)
return _id.Equals(other._id);
}

public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return obj is RedirectId other && Equals(other);
}
Expand Down
17 changes: 8 additions & 9 deletions src/DivertR/Spy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ private Spy(DiverterSettings? diverterSettings, TTarget? root, bool hasRoot)
: base(diverterSettings)
{
Mock = hasRoot ? Proxy() : Proxy(root);
Spy.AddSpy(this, Mock);
ResetAndConfigureRecord();
}

Expand Down Expand Up @@ -134,8 +133,6 @@ private RecordStream<TTarget> CallsLocked
/// </summary>
public static class Spy
{
private static readonly SpyTracker SpyTracker = new();

/// <summary>
/// Creates a spy of the given target type and returns its mock object.
/// </summary>
Expand Down Expand Up @@ -167,12 +164,14 @@ public static TTarget On<TTarget>(TTarget? root) where TTarget : class?
/// <exception cref="DiverterException">Thrown if if the given <paramref name="mock"/> object does not have an associated <see cref="ISpy{TTarget}"/> </exception>
public static ISpy<TTarget> Of<TTarget>([DisallowNull] TTarget mock) where TTarget : class?
{
return SpyTracker.GetSpy<TTarget>(mock);
}

internal static void AddSpy<TTarget>(Spy<TTarget> spy, [DisallowNull] TTarget mock) where TTarget : class?
{
SpyTracker.AddSpy(spy, mock);
var redirect = Redirect.Of(mock);

if (redirect is not Spy<TTarget> spyOf)
{
throw new DiverterException("Spy not found");
}

return spyOf;
}
}
}
1 change: 0 additions & 1 deletion test/DivertR.UnitTests/QuickstartExamples.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using DivertR.DependencyInjection;
using DivertR.UnitTests.Model;
Expand Down
90 changes: 90 additions & 0 deletions test/DivertR.UnitTests/RedirectStaticTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using DivertR.UnitTests.Model;
using Shouldly;
using Xunit;

namespace DivertR.UnitTests;

public class RedirectStaticTests
{
[Fact]
public void GivenProxy_WhenProxyMethodCalled_ThenRelayToRoot()
{
// ARRANGE
var proxy = Redirect.Proxy<IFoo>(new Foo("test"));

// ACT
var callResult = proxy.Name;

// ASSERT
callResult.ShouldBe("test");
}

[Fact]
public void GivenProxyWithNoRoot_WhenProxyCalled_ShouldReturnDefault()
{
// ARRANGE
var proxy = Redirect.Proxy<IFoo>();

// ACT
var callResult = proxy.Name;

// ASSERT
callResult.ShouldBeNull();
}

[Fact]
public void GivenProxyWithVia_WhenMockCalled_ThenRedirects()
{
// ARRANGE
var proxy = Redirect.Proxy<IFoo>();
var redirect = Redirect.Of(proxy);

redirect
.To(x => x.Echo(Is<string>.Any))
.Via<(string input, __)>(call => $"{call.Args.input} redirected");

// ACT
var result = proxy.Echo("test");

// ASSERT
result.ShouldBe("test redirected");
}

[Fact]
public void GivenProxyAsObject_WhenRedirectOf_ThenThrowsDiverterException()
{
// ARRANGE
object fooProxy = Redirect.Proxy<IFoo>();

// ACT
var testAction = () => Redirect.Of(fooProxy);

// ASSERT
testAction.ShouldThrow<DiverterException>();
}

[Fact]
public void GivenNonProxyObject_WhenRedirectOfObject_ThenThrowsDiverterException()
{
// ARRANGE
IFoo foo = new Foo();

// ACT
var testAction = () => Redirect.Of(foo);

// ASSERT
testAction.ShouldThrow<DiverterException>();
}

[Fact]
public void GivenDispatchProxyFactory_WhenSpyOnClassType_ThenThrowsDiverterException()
{
// ARRANGE

// ACT
var testAction = () => Redirect.Proxy<Foo>();

// ASSERT
testAction.ShouldThrow<DiverterException>();
}
}

0 comments on commit 3108e4f

Please sign in to comment.