diff --git a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs index 1f1c05c9a8aece..45c3428b498092 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonTestHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text.Json.Serialization.Tests; using System.Text.Json.Serialization.Tests.Schemas.OrderPayload; using System.Text.RegularExpressions; @@ -17,59 +18,99 @@ public static void AssertJsonEqual(string expected, string actual) { using JsonDocument expectedDom = JsonDocument.Parse(expected); using JsonDocument actualDom = JsonDocument.Parse(actual); - AssertJsonEqual(expectedDom.RootElement, actualDom.RootElement); + AssertJsonEqual(expectedDom.RootElement, actualDom.RootElement, new()); } - private static void AssertJsonEqual(JsonElement expected, JsonElement actual) + private static void AssertJsonEqual(JsonElement expected, JsonElement actual, Stack path) { JsonValueKind valueKind = expected.ValueKind; - Assert.Equal(valueKind, actual.ValueKind); + AssertTrue(passCondition: valueKind == actual.ValueKind); switch (valueKind) { case JsonValueKind.Object: - var propertyNames = new HashSet(); - + var expectedProperties = new List(); foreach (JsonProperty property in expected.EnumerateObject()) { - propertyNames.Add(property.Name); + expectedProperties.Add(property.Name); } + var actualProperties = new List(); foreach (JsonProperty property in actual.EnumerateObject()) { - propertyNames.Add(property.Name); + actualProperties.Add(property.Name); } - foreach (string name in propertyNames) + foreach (var property in expectedProperties.Except(actualProperties)) { - AssertJsonEqual(expected.GetProperty(name), actual.GetProperty(name)); + AssertTrue(passCondition: false, $"Property \"{property}\" missing from actual object."); + } + + foreach (var property in actualProperties.Except(expectedProperties)) + { + AssertTrue(passCondition: false, $"Actual object defines additional property \"{property}\"."); + } + + foreach (string name in expectedProperties) + { + path.Push(name); + AssertJsonEqual(expected.GetProperty(name), actual.GetProperty(name), path); + path.Pop(); } break; case JsonValueKind.Array: - JsonElement.ArrayEnumerator expectedEnumerator = actual.EnumerateArray(); - JsonElement.ArrayEnumerator actualEnumerator = expected.EnumerateArray(); + JsonElement.ArrayEnumerator expectedEnumerator = expected.EnumerateArray(); + JsonElement.ArrayEnumerator actualEnumerator = actual.EnumerateArray(); + int i = 0; while (expectedEnumerator.MoveNext()) { - Assert.True(actualEnumerator.MoveNext()); - AssertJsonEqual(expectedEnumerator.Current, actualEnumerator.Current); + AssertTrue(passCondition: actualEnumerator.MoveNext(), "Actual array contains fewer elements."); + path.Push(i++); + AssertJsonEqual(expectedEnumerator.Current, actualEnumerator.Current, path); + path.Pop(); } - Assert.False(actualEnumerator.MoveNext()); + AssertTrue(passCondition: !actualEnumerator.MoveNext(), "Actual array contains additional elements."); break; case JsonValueKind.String: - Assert.Equal(expected.GetString(), actual.GetString()); + AssertTrue(passCondition: expected.GetString() == actual.GetString()); break; case JsonValueKind.Number: case JsonValueKind.True: case JsonValueKind.False: case JsonValueKind.Null: - Assert.Equal(expected.GetRawText(), actual.GetRawText()); + AssertTrue(passCondition: expected.GetRawText() == actual.GetRawText()); break; default: Debug.Fail($"Unexpected JsonValueKind: JsonValueKind.{valueKind}."); break; } + + void AssertTrue(bool passCondition, string? message = null) + { + if (!passCondition) + { + message ??= "Expected JSON does not match actual value"; + Assert.Fail($"{message}\nExpected JSON: {expected}\n Actual JSON: {actual}\n in JsonPath: {BuildJsonPath(path)}"); + } + + // TODO replace with JsonPath implementation for JsonElement + // cf. https://github.com/dotnet/runtime/issues/31068 + static string BuildJsonPath(Stack path) + { + var sb = new StringBuilder("$"); + foreach (object node in path.Reverse()) + { + string pathNode = node is string propertyName + ? "." + propertyName + : $"[{(int)node}]"; + + sb.Append(pathNode); + } + return sb.ToString(); + } + } } public static async Task> ToListAsync(this IAsyncEnumerable source)