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
2 changes: 2 additions & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- Allow multiple path components in file-based recorded call repository constructors ([#88](https://github.com/blairconrad/SelfInitializingFakes/pull/88))
- Create missing directories for file-based recorded call repositories ([#87](https://github.com/blairconrad/SelfInitializingFakes/pull/87))
- Serialize `Lazy<T>`, `Task`, and `Task<T>` return values and out and ref
parameters ([#81](https://github.com/blairconrad/SelfInitializingFakes/issues/81))

### With special thanks for contributions to this release from:
- [CableZa](https://github.com/CableZa)
Expand Down
48 changes: 48 additions & 0 deletions src/SelfInitializingFakes/Infrastructure/CompoundTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace SelfInitializingFakes.Infrastructure
{
using System;
using System.Reflection;

/// <summary>
/// Chains other <see cref="ITypeConverter"/>s together.
/// </summary>
internal class CompoundTypeConverter : ITypeConverter
{
private readonly ITypeConverter first;
private readonly ITypeConverter second;

/// <summary>
/// Initializes a new instance of the <see cref="CompoundTypeConverter"/> class.
/// </summary>
/// <param name="first">The first converter to try. If it can't convert the input, the second will be tried.</param>
/// <param name="second">The second converter to try, if the first was unable.</param>
public CompoundTypeConverter(ITypeConverter first, ITypeConverter second)
{
this.first = first;
this.second = second;
}

/// <summary>
/// Potentially converts an unserializable object to a more serializable form.
/// </summary>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be assigned to a simpler representation of <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output) =>
this.first.ConvertForRecording(input, mainConverter, out output) ||
this.second.ConvertForRecording(input, mainConverter, out output);

/// <summary>
/// Potentially converts the serializable form of an object back to its unserializable form.
/// </summary>
/// <param name="deserializedType">The desired deserialized type.</param>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be reconstituted from its simpler representation as <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output) =>
this.first.ConvertForPlayback(deserializedType, input, mainConverter, out output) ||
this.second.ConvertForPlayback(deserializedType, input, mainConverter, out output);
}
}
29 changes: 29 additions & 0 deletions src/SelfInitializingFakes/Infrastructure/ITypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace SelfInitializingFakes.Infrastructure
{
using System;

/// <summary>
/// Converts unserializable types to simpler types while recording, and reverses the transformation during.
/// </summary>
internal interface ITypeConverter
{
/// <summary>
/// Potentially converts an unserializable object to a more serializable form.
/// </summary>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be assigned to a simpler representation of <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output);

/// <summary>
/// Potentially converts the serializable form of an object back to its unserializable form.
/// </summary>
/// <param name="deserializedType">The desired deserialized type.</param>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be reconstituted from its simpler representation as <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output);
}
}
79 changes: 79 additions & 0 deletions src/SelfInitializingFakes/Infrastructure/LazyTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
namespace SelfInitializingFakes.Infrastructure
{
using System;
using System.Reflection;

/// <summary>
/// Converts <see cref="System.Lazy{T}" /> types to simpler types for serialization, and back again.
/// </summary>
internal class LazyTypeConverter : ITypeConverter
{
private static readonly MethodInfo CreateLazyGenericDefinition =
typeof(LazyTypeConverter).GetMethod(nameof(CreateLazy), BindingFlags.Static | BindingFlags.NonPublic);

/// <summary>
/// Potentially converts an unserializable object to a more serializable form.
/// </summary>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be assigned to a simpler representation of <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output)
{
output = null;
if (input is null)
{
return false;
}

var inputType = input.GetType();
if (inputType.IsInstanceOf(typeof(Lazy<>)))
{
output = inputType.GetProperty("Value").GetGetMethod().Invoke(input, Type.EmptyTypes);
if (mainConverter.ConvertForRecording(output, mainConverter, out object? furtherConvertedOutput))
{
output = furtherConvertedOutput;
}

return true;
}

return false;
}

/// <summary>
/// Potentially converts the serializable form of an object back to its unserializable form.
/// </summary>
/// <param name="deserializedType">The desired deserialized type.</param>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be reconstituted from its simpler representation as <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output)
{
if (deserializedType.IsInstanceOf(typeof(Lazy<>)))
{
var typeOfLazyResult = deserializedType.GetGenericArguments()[0];

if (input is null || input.GetType() == typeOfLazyResult)
{
var method = CreateLazyGenericDefinition.MakeGenericMethod(typeOfLazyResult);
output = method.Invoke(null, new object?[] { input });
return true;
}

if (mainConverter.ConvertForPlayback(typeOfLazyResult, input, mainConverter, out object? convertedInput))
{
var method = CreateLazyGenericDefinition.MakeGenericMethod(typeOfLazyResult);
output = method.Invoke(null, new object?[] { convertedInput });
return true;
}
}

output = null;
return false;
}

private static Lazy<T> CreateLazy<T>(T value) => new Lazy<T>(() => value);
}
}
33 changes: 26 additions & 7 deletions src/SelfInitializingFakes/Infrastructure/PlaybackRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ namespace SelfInitializingFakes.Infrastructure
internal class PlaybackRule : IFakeObjectCallRule
{
private readonly Queue<RecordedCall> expectedCalls;
private readonly ITypeConverter typeConverter;

/// <summary>
/// Initializes a new instance of the <see cref="PlaybackRule"/> class.
/// </summary>
/// <param name="expectedCalls">The calls that are expected to be made on the fake.</param>
public PlaybackRule(Queue<RecordedCall> expectedCalls)
/// <param name="typeConverter">A helper to convert values from serialized variants to their original representations.</param>
public PlaybackRule(Queue<RecordedCall> expectedCalls, ITypeConverter typeConverter)
{
this.expectedCalls = expectedCalls;
this.typeConverter = typeConverter;
}

/// <summary>
Expand All @@ -32,8 +35,8 @@ public PlaybackRule(Queue<RecordedCall> expectedCalls)
public void Apply(IInterceptedFakeObjectCall fakeObjectCall)
{
RecordedCall recordedCall = this.ConsumeNextExpectedCall(fakeObjectCall);
SetReturnValue(fakeObjectCall, recordedCall);
SetOutAndRefValues(fakeObjectCall, recordedCall);
this.SetReturnValue(fakeObjectCall, recordedCall);
this.SetOutAndRefValues(fakeObjectCall, recordedCall);
}

/// <summary>
Expand All @@ -44,20 +47,36 @@ public void Apply(IInterceptedFakeObjectCall fakeObjectCall)
/// <returns><c>true</c> all the time.</returns>
public bool IsApplicableTo(IFakeObjectCall fakeObjectCall) => true;

private static void SetReturnValue(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall)
private void SetReturnValue(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall)
{
fakeObjectCall.SetReturnValue(recordedCall.ReturnValue);
var returnValue = recordedCall.ReturnValue;
if (this.typeConverter.ConvertForPlayback(fakeObjectCall.Method.ReturnType, returnValue, this.typeConverter, out object? convertedReturnValue))
{
returnValue = convertedReturnValue;
}

fakeObjectCall.SetReturnValue(returnValue);
}

private static void SetOutAndRefValues(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall)
private void SetOutAndRefValues(IInterceptedFakeObjectCall fakeObjectCall, RecordedCall recordedCall)
{
int outOrRefIndex = 0;
for (int parameterIndex = 0; parameterIndex < fakeObjectCall.Method.GetParameters().Length; parameterIndex++)
{
var parameter = fakeObjectCall.Method.GetParameters()[parameterIndex];
if (parameter.ParameterType.IsByRef)
{
fakeObjectCall.SetArgumentValue(parameterIndex, recordedCall.OutAndRefValues[outOrRefIndex++]);
var parameterValue = recordedCall.OutAndRefValues[outOrRefIndex++];
if (this.typeConverter.ConvertForPlayback(
parameter.ParameterType.GetElementType(),
parameterValue,
this.typeConverter,
out object? convertedParameterValue))
{
parameterValue = convertedParameterValue;
}

fakeObjectCall.SetArgumentValue(parameterIndex, parameterValue);
}
}
}
Expand Down
24 changes: 22 additions & 2 deletions src/SelfInitializingFakes/Infrastructure/RecordingRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ namespace SelfInitializingFakes.Infrastructure
internal class RecordingRule : IFakeObjectCallRule
{
private readonly object target;
private readonly ITypeConverter typeConverter;
private Exception? recordingException;

/// <summary>
/// Initializes a new instance of the <see cref="RecordingRule"/> class.
/// </summary>
/// <param name="target">The object to which to forward calls, in order to harvest return, out, and ref values.</param>
public RecordingRule(object target)
/// <param name="typeConverter">A helper to convert values from their original representation to serializable variants.</param>
public RecordingRule(object target, ITypeConverter typeConverter)
{
this.target = target;
this.typeConverter = typeConverter;
}

/// <summary>
Expand Down Expand Up @@ -52,8 +55,9 @@ public void Apply(IInterceptedFakeObjectCall fakeObjectCall)
try
{
var recordedCall = this.BuildRecordedCall(fakeObjectCall);
this.RecordedCalls.Add(recordedCall);
ApplyRecordedCall(recordedCall, fakeObjectCall);
this.ConvertRecordedCallForSerialization(recordedCall);
this.RecordedCalls.Add(recordedCall);
}
#pragma warning disable CA1031 // We do rethrow the exception
catch (Exception e)
Expand Down Expand Up @@ -118,5 +122,21 @@ private RecordedCall BuildRecordedCall(IFakeObjectCall call)

return new RecordedCall(call.Method.ToString(), result, outAndRefValues.ToArray());
}

private void ConvertRecordedCallForSerialization(RecordedCall call)
{
if (this.typeConverter.ConvertForRecording(call.ReturnValue, this.typeConverter, out object? convertedReturnValue))
{
call.ReturnValue = convertedReturnValue;
}

for (int i = 0; i < call.OutAndRefValues.Length; ++i)
{
if (this.typeConverter.ConvertForRecording(call.OutAndRefValues[i], this.typeConverter, out object? convertedValue))
{
call.OutAndRefValues[i] = convertedValue;
}
}
}
}
}
104 changes: 104 additions & 0 deletions src/SelfInitializingFakes/Infrastructure/TaskTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace SelfInitializingFakes.Infrastructure
{
using System;
using System.Reflection;
using System.Threading.Tasks;

/// <summary>
/// Converts <see cref="Task{T}" /> types to simpler types for serialization, and back again.
/// </summary>
internal class TaskTypeConverter : ITypeConverter
{
private static readonly MethodInfo CreateTaskGenericDefinition =
typeof(TaskTypeConverter).GetMethod(nameof(CreateTask), BindingFlags.Static | BindingFlags.NonPublic);

/// <summary>
/// Potentially converts an unserializable object to a more serializable form.
/// </summary>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be assigned to a simpler representation of <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
public bool ConvertForRecording(object? input, ITypeConverter mainConverter, out object? output)
{
output = null;
if (input is null)
{
return false;
}

var inputType = input.GetType();
if (inputType.IsInstanceOf(typeof(Task<>)))
{
output = inputType.GetProperty("Result").GetGetMethod().Invoke(input, Type.EmptyTypes);
if (mainConverter.ConvertForRecording(output, mainConverter, out object? furtherConvertedOutput))
{
output = furtherConvertedOutput;
}

return true;
}
else if (inputType == typeof(Task))
{
output = "{void Task}";
if (mainConverter.ConvertForRecording(output, mainConverter, out object? furtherConvertedOutput))
{
output = furtherConvertedOutput;
}

return true;
}

return false;
}

/// <summary>
/// Potentially converts the serializable form of an object back to its unserializable form.
/// </summary>
/// <param name="deserializedType">The desired deserialized type.</param>
/// <param name="input">An input object.</param>
/// <param name="mainConverter">A comprehensive converter that may be used to further convert the output, if required.</param>
/// <param name="output">An output object. Will be reconstituted from its simpler representation as <paramref name="input"/>, if this converter knows how.</param>
/// <returns><c>true</c> if the conversion happened, otherwise <c>false</c>. Good for building a chain of responsibility.</returns>
public bool ConvertForPlayback(Type deserializedType, object? input, ITypeConverter mainConverter, out object? output)
{
if (deserializedType.IsInstanceOf(typeof(Task<>)))
{
var typeOfTaskResult = deserializedType.GetGenericArguments()[0];

if (input is null || input.GetType() == typeOfTaskResult)
{
var method = CreateTaskGenericDefinition.MakeGenericMethod(typeOfTaskResult);
output = method.Invoke(null, new object?[] { input });
return true;
}

if (mainConverter.ConvertForPlayback(typeOfTaskResult, input, mainConverter, out object? convertedInput))
{
var method = CreateTaskGenericDefinition.MakeGenericMethod(typeOfTaskResult);
output = method.Invoke(null, new object?[] { convertedInput });
return true;
}
}
else if (deserializedType == typeof(Task) && "{void Task}".Equals(input))
{
var task = new Task(() => { });
task.Start();
task.Wait();

output = task;
return true;
}

output = null;
return false;
}

private static Task<T> CreateTask<T>(T value)
{
var tcs = new TaskCompletionSource<T>();
tcs.SetResult(value);
return tcs.Task;
}
}
}
Loading