Skip to content

Commit

Permalink
Use JsonPolymorphic feature of System.Text.Json
Browse files Browse the repository at this point in the history
  • Loading branch information
bradwilson committed Nov 28, 2022
1 parent 6a93fdd commit 2b1f75b
Show file tree
Hide file tree
Showing 39 changed files with 610 additions and 189 deletions.
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-format": {
"version": "5.1.225507",
"version": "5.1.250801",
"commands": [
"dotnet-format"
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Serialization;
using Xunit;
using Xunit.Internal;
using Xunit.Sdk;
using Xunit.v3;

Expand All @@ -15,7 +18,12 @@ public void BaseSerializationIncludesTypeName()

var result = Encoding.UTF8.GetString(msg.ToJson());

Assert.Equal(@"{""$type"":""_MessageSinkMessage""}", result);
var expected = """
{
"$type": "_MessageSinkMessage"
}
""".Replace("\n", "");
Assert.Equal(expected, result, ignoreAllWhiteSpace: true);
}

[Fact]
Expand All @@ -35,33 +43,56 @@ public void SerializationExcludesNullValues()

var result = Encoding.UTF8.GetString(msg.ToJson());

Assert.Equal(
@"{" +
@"""$type"":""_TestAssemblyStarting""," +
@"""AssemblyName"":""asm-name""," +
@"""AssemblyUniqueID"":""asm-id""," +
@"""StartTime"":""2020-09-26T13:55:27.212-07:00""," +
@"""TestEnvironment"":""test-env""," +
@"""TestFrameworkDisplayName"":""test-framework""" +
@"}",
result
);
var expected = """
{
"$type": "_TestAssemblyStarting",
"AssemblyName": "asm-name",
"StartTime": "2020-09-26T13:55:27.212-07:00",
"TestEnvironment": "test-env",
"TestFrameworkDisplayName": "test-framework",
"AssemblyUniqueID": "asm-id"
}
""".Replace("\n", "");
Assert.Equal(expected, result, ignoreAllWhiteSpace: true);
}

[Fact]
public void SerializesEnumsAsStrings()
{
var msg = new TestMessageWithEnum { Cause = FailureCause.Assertion };
var msg = new _TestFailed
{
AssemblyUniqueID = "asm-id",
Cause = FailureCause.Assertion,
ExceptionParentIndices = new[] { -1 },
ExceptionTypes = new[] { "exception-type" },
ExecutionTime = 123.45m,
Messages = new[] { "exception-message" },
Output = "",
StackTraces = new[] { "stack-trace" },
TestCaseUniqueID = "test-case-id",
TestCollectionUniqueID = "test-collection-id",
TestUniqueID = "test-id",
};

var result = Encoding.UTF8.GetString(msg.ToJson());

Assert.Equal(
@"{" +
@"""$type"":""TestMessageWithEnum""," +
@"""Cause"":""Assertion""" +
@"}",
result
);
var expected = """
{
"$type": "_TestFailed",
"Cause": "Assertion",
"ExceptionParentIndices": [-1],
"ExceptionTypes": ["exception-type"],
"Messages": ["exception-message"],
"StackTraces": ["stack-trace"],
"ExecutionTime": 123.45,
"Output": "",
"TestUniqueID": "test-id",
"TestCaseUniqueID": "test-case-id",
"TestCollectionUniqueID": "test-collection-id",
"AssemblyUniqueID": "asm-id"
}
""".Replace("\n", "");
Assert.Equal(expected, result, ignoreAllWhiteSpace: true);
}

class TestMessageWithEnum : _MessageSinkMessage
Expand All @@ -70,13 +101,24 @@ class TestMessageWithEnum : _MessageSinkMessage
}

[Fact]
public void CanDeserializeStringEnum()
public void DeserializesEnumsAsStrings()
{
var msg =
@"{" +
@"""$type"":""_TestFailed""," +
@"""Cause"":""Assertion""" +
@"}";
var msg = """
{
"$type": "_TestFailed",
"Cause": "Assertion",
"ExceptionParentIndices": [-1],
"ExceptionTypes": ["exception-type"],
"Messages": ["exception-message"],
"StackTraces": ["stack-trace"],
"ExecutionTime": 123.45,
"Output": "",
"TestUniqueID": "test-id",
"TestCaseUniqueID": "test-case-id",
"TestCollectionUniqueID": "test-collection-id",
"AssemblyUniqueID": "asm-id"
}
""";

var result = _MessageSinkMessage.ParseJson(Encoding.UTF8.GetBytes(msg));

Expand Down Expand Up @@ -106,23 +148,23 @@ public void CanRoundTripTraits()

var serialized = msg.ToJson();

Assert.Equal(
@"{" +
@"""$type"":""_TestCaseDiscovered""," +
@"""AssemblyUniqueID"":""asm-id""," +
@"""Serialization"":""serialized-value""," +
@"""TestCaseDisplayName"":""test-case-display-name""," +
@"""TestCaseUniqueID"":""test-case-id""," +
@"""TestCollectionUniqueID"":""test-collection-id""," +
@"""Traits"":" +
@"{" +
@"""foo"":[""bar"",""baz""]," +
@"""abc"":[""123""]," +
@"""empty"":[]" +
@"}" +
@"}",
Encoding.UTF8.GetString(serialized)
);
var expected = """
{
"$type": "_TestCaseDiscovered",
"Serialization": "serialized-value",
"TestCaseDisplayName": "test-case-display-name",
"Traits":
{
"foo": ["bar", "baz"],
"abc": ["123"],
"empty": []
},
"TestCaseUniqueID": "test-case-id",
"TestCollectionUniqueID": "test-collection-id",
"AssemblyUniqueID": "asm-id"
}
""".Replace("\n", "");
Assert.Equal(expected, Encoding.UTF8.GetString(serialized), ignoreAllWhiteSpace: true);

// Validate deserialization

Expand Down Expand Up @@ -160,4 +202,34 @@ public void RoundTrip()
var deserializedTAM = Assert.IsType<_TestAssemblyMessage>(deserialized);
Assert.Equal("asm-id", deserializedTAM.AssemblyUniqueID);
}

[Fact]
public void ValidatesAllDerivedTypesAreSupported()
{
var messageSinkMessageType = typeof(_MessageSinkMessage);
var missingTypes = new List<Type>();
var decoratedTypes =
messageSinkMessageType
.GetCustomAttributes(typeof(JsonDerivedTypeAttribute))
.Cast<JsonDerivedTypeAttribute>()
.Select(a => a.DerivedType)
.ToHashSet();

Type[] publicTypes;
try
{
publicTypes = messageSinkMessageType.Assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
publicTypes = ex.Types.WhereNotNull().ToArray();
}

foreach (var type in publicTypes.Where(t => !t.IsAbstract && messageSinkMessageType.IsAssignableFrom(t)))
if (!decoratedTypes.Contains(type))
missingTypes.Add(type);

if (missingTypes.Count > 0)
throw new XunitException($"The following attributes are missing from {nameof(_MessageSinkMessage)}:{Environment.NewLine}{string.Join(Environment.NewLine, missingTypes.OrderBy(t => t.Name).Select(t => $" [JsonDerivedType(typeof({t.Name}), nameof({t.Name}))]"))}");
}
}
39 changes: 39 additions & 0 deletions src/xunit.v3.common/Utility/UnsetPropertiesException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit.Internal;

namespace Xunit.Sdk;

/// <summary>
/// An exception which indicates an object had several properties that were not properly initialized.
/// </summary>
public class UnsetPropertiesException : InvalidOperationException
{
/// <summary>
/// Initializes a new instance of the <see cref="UnsetPropertiesException"/> class.
/// </summary>
/// <param name="propertyNames">The properties that were not set</param>
/// <param name="type">The type that the property belongs to</param>
public UnsetPropertiesException(
IEnumerable<string> propertyNames,
Type type)
{
PropertyNames = Guard.ArgumentNotNull(propertyNames).OrderBy(x => x).ToArray();
TypeName = Guard.ArgumentNotNull(type).SafeName();
}

/// <inheritdoc/>
public override string Message =>
$"Object of type '{TypeName}' had one or more properties that were not set: {string.Join(", ", PropertyNames)}";

/// <summary>
/// Gets the property names of the uninitialized properties.
/// </summary>
public string[] PropertyNames { get; }

/// <summary>
/// Gets the type name of the uninitialized property.
/// </summary>
public string TypeName { get; }
}
4 changes: 2 additions & 2 deletions src/xunit.v3.common/Utility/UnsetPropertyException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public class UnsetPropertyException : InvalidOperationException
/// <summary>
/// Initializes a new instance of the <see cref="UnsetPropertyException"/> class.
/// </summary>
/// <param name="propertyName"></param>
/// <param name="type"></param>
/// <param name="propertyName">The property that was not set</param>
/// <param name="type">The type that the property belongs to</param>
public UnsetPropertyException(
string propertyName,
Type type)
Expand Down

0 comments on commit 2b1f75b

Please sign in to comment.