Skip to content
Merged
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
61 changes: 61 additions & 0 deletions UndoRedo.Test/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,8 @@ public async Task UndoRedoService_SaveLoadState_ReconstructsCommandWithEmptyData
private const string InvalidVersion = "invalid assembly version";
private const string NotACommand = "serializable type that is not a command";
private const string DataParseFailure = "command data its parser rejects";
private const string ThrowingConstructor = "command whose constructor throws";
private const string OpenGeneric = "open generic command type";

private static async Task<byte[]> SerializeWithCommandTypeAsync(string caseName)
{
Expand All @@ -581,6 +583,8 @@ private static async Task<byte[]> SerializeWithCommandTypeAsync(string caseName)
InvalidVersion => "Foo, Bar, Version=abc",
NotACommand => typeof(SerializableNonCommand).AssemblyQualifiedName!,
DataParseFailure => typeof(IntParsingSerializableCommand).AssemblyQualifiedName!,
ThrowingConstructor => typeof(ThrowingConstructorSerializableCommand).AssemblyQualifiedName!,
OpenGeneric => typeof(GenericSerializableCommand<>).AssemblyQualifiedName!,
_ => throw new ArgumentOutOfRangeException(nameof(caseName)),
};

Expand All @@ -600,6 +604,8 @@ private static async Task<byte[]> SerializeWithCommandTypeAsync(string caseName)
[DataRow(InvalidVersion)]
[DataRow(NotACommand)]
[DataRow(DataParseFailure)]
[DataRow(ThrowingConstructor)]
[DataRow(OpenGeneric)]
public async Task JsonSerializer_DeserializeUnloadableCommand_ThrowsInvalidOperationException(string caseName)
{
// Arrange
Expand All @@ -617,6 +623,8 @@ await Assert.ThrowsExactlyAsync<InvalidOperationException>(
[DataRow(InvalidVersion)]
[DataRow(NotACommand)]
[DataRow(DataParseFailure)]
[DataRow(ThrowingConstructor)]
[DataRow(OpenGeneric)]
public async Task UndoRedoService_LoadStateUnloadableCommand_ReturnsFalseAndKeepsHistory(string caseName)
{
// Arrange
Expand Down Expand Up @@ -673,6 +681,59 @@ public override void Undo()
}
#pragma warning restore CA1812

#pragma warning disable CA1812 // Instantiated by reflection during deserialization
private sealed class ThrowingConstructorSerializableCommand : BaseCommand, ISerializableCommand
{
public ThrowingConstructorSerializableCommand() : base(ChangeType.Modify, ["test"]) =>
throw new InvalidDataException("Needs context the loader cannot supply");

public override string Description => "Throwing constructor";

public override void Execute()
{
// Test implementation
}

public override void Undo()
{
// Test implementation
}

public string SerializeData() => string.Empty;

public void DeserializeData(string data)
{
// Nothing to restore
}
}

private sealed class GenericSerializableCommand<T> : BaseCommand, ISerializableCommand
{
public GenericSerializableCommand() : base(ChangeType.Modify, ["test"])
{
}

public override string Description => $"Generic command of {typeof(T).Name}";

public override void Execute()
{
// Test implementation
}

public override void Undo()
{
// Test implementation
}

public string SerializeData() => string.Empty;

public void DeserializeData(string data)
{
// Nothing to restore
}
}
#pragma warning restore CA1812

private sealed class EmptyDataSerializableCommand : BaseCommand, ISerializableCommand
{
public EmptyDataSerializableCommand() : base(ChangeType.Modify, ["test"])
Expand Down
11 changes: 11 additions & 0 deletions UndoRedo/Services/JsonUndoRedoSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ private static ICommand ConvertFromSerializableCommand(SerializableCommand seria
$"Cannot reconstruct command type '{commandType.FullName}': {nameof(ISerializableCommand)} implementations must have a public parameterless constructor for DeserializeData to populate.",
ex);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex) when (ex is not OperationCanceledException)
#pragma warning restore CA1031 // Do not catch general exception types
{
// The constructor itself can throw (surfacing as TargetInvocationException or
// TypeInitializationException), and an open generic type cannot be constructed at all
// (ArgumentException). Report these through the deserialization contract as well.
throw new InvalidOperationException(
$"Cannot reconstruct command type '{commandType.FullName}': its public parameterless constructor could not create an instance.",
ex);
}

try
{
Expand Down
Loading