Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>();
}
}