Skip to content

Commit 92a47a6

Browse files
committed
chore: add json/yaml serialization test infrastructure
1 parent fda9353 commit 92a47a6

File tree

6 files changed

+256
-1
lines changed

6 files changed

+256
-1
lines changed

src/Docfx.Common/Docfx.Common.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
<ItemGroup>
1919
<InternalsVisibleTo Include="docfx.Build" />
2020
<InternalsVisibleTo Include="Docfx.Build.Tests" />
21+
<InternalsVisibleTo Include="docfx.Tests" />
2122
</ItemGroup>
2223
</Project>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
using Docfx;
6+
using Docfx.Common;
7+
using FluentAssertions;
8+
using FluentAssertions.Equivalency;
9+
10+
namespace docfx.Tests;
11+
12+
public partial class JsonSerializationTest
13+
{
14+
/// <summary>
15+
/// Helper method to validate serialize/deserialize results.
16+
/// </summary>
17+
protected void ValidateJsonRoundTrip<T>(T model)
18+
{
19+
// 1. Validate serialized result.
20+
var newtonsoftJson = NewtonsoftJsonUtility.Serialize(model);
21+
var systemTextJson = SystemTextJsonUtility.Serialize(model);
22+
systemTextJson.Should().Be(newtonsoftJson);
23+
24+
// 2. Validate deserialized result.
25+
var json = systemTextJson;
26+
var systemTextJsonModel = SystemTextJsonUtility.Deserialize<T>(json);
27+
var newtonsoftJsonModel = NewtonsoftJsonUtility.Deserialize<T>(new StringReader(json));
28+
29+
// Assert
30+
systemTextJsonModel.Should().BeEquivalentTo(model, assertionOptions);
31+
newtonsoftJsonModel.Should().BeEquivalentTo(model, assertionOptions);
32+
33+
// Helper local function
34+
static EquivalencyAssertionOptions<T> assertionOptions(EquivalencyAssertionOptions<T> opt)
35+
{
36+
// By default. JsonElement is compared by reference because JsonElement don't override Equals.
37+
return opt.ComparingByMembers<JsonElement>();
38+
}
39+
}
40+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Text.Json;
6+
using Docfx.Build.ApiPage;
7+
using Docfx.Common;
8+
using Docfx.Tests;
9+
using YamlDotNet.Serialization;
10+
11+
namespace docfx.Tests;
12+
13+
public static class TestData
14+
{
15+
/// <summary>
16+
/// Load test data from specified path.
17+
/// </summary>
18+
public static T Load<T>(string path)
19+
{
20+
if (typeof(T) == typeof(ApiPage))
21+
throw new NotSupportedException(); // It should be handled separately.
22+
23+
var testDataPath = PathHelper.ResolveTestDataPath(path);
24+
25+
switch (Path.GetExtension(path))
26+
{
27+
case ".yml":
28+
return YamlUtility.Deserialize<T>(testDataPath);
29+
case ".json":
30+
return JsonUtility.Deserialize<T>(testDataPath);
31+
default:
32+
throw new NotSupportedException();
33+
}
34+
}
35+
36+
/// <summary>
37+
/// Gets test data relative file paths.
38+
/// </summary>
39+
public static string[] GetTestDataFilePaths(string key)
40+
{
41+
var testDataRootDir = PathHelper.GetTestDataDirectory();
42+
var basePathLength = testDataRootDir.Length + 1;
43+
44+
var testDataDir = Path.Combine(testDataRootDir, key);
45+
var directoryInfo = new DirectoryInfo(testDataDir);
46+
47+
// Gets TestData directory relative paths
48+
var relativePaths = directoryInfo.EnumerateFiles()
49+
.Select(x => x.FullName)
50+
.Select(x => x.Substring(basePathLength));
51+
52+
return relativePaths.ToArray();
53+
}
54+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Reflection;
5+
using Xunit.Sdk;
6+
7+
namespace docfx.Tests;
8+
9+
public class TestDataAttribute<T> : DataAttribute
10+
{
11+
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
12+
{
13+
var key = GetTestDataKey();
14+
var paths = TestData.GetTestDataFilePaths(key);
15+
16+
var className = testMethod.DeclaringType.Name;
17+
18+
// Filter test data.
19+
switch (className)
20+
{
21+
case nameof(JsonSerializationTest):
22+
paths = paths.Where(x => x.EndsWith(".json")).ToArray();
23+
break;
24+
case nameof(YamlSerializationTest):
25+
paths = paths.Where(x => x.EndsWith(".yml")).ToArray();
26+
break;
27+
default:
28+
throw new NotSupportedException($"{className} is not supported.");
29+
}
30+
31+
return new TheoryData<string>(paths);
32+
}
33+
34+
private static string GetTestDataKey()
35+
{
36+
var type = typeof(T);
37+
var fullname = type.FullName;
38+
39+
switch (fullname)
40+
{
41+
case "Docfx.DataContracts.ManagedReference.PageViewModel":
42+
return "ManagedReference";
43+
case "Docfx.DataContracts.UniversalReference.PageViewModel":
44+
return "UniversalReference";
45+
default:
46+
return type.Name;
47+
}
48+
}
49+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel.DataAnnotations;
5+
using System.Diagnostics;
6+
using System.IO;
7+
using System.Runtime.CompilerServices;
8+
using Docfx.Common;
9+
using Docfx.Tests;
10+
using Docfx.YamlSerialization;
11+
using FluentAssertions;
12+
using Microsoft.AspNetCore.Http;
13+
using YamlDotNet.Serialization;
14+
namespace docfx.Tests;
15+
16+
public partial class YamlSerializationTest
17+
{
18+
private static readonly ThreadLocal<YamlSerializer> YamlJsonSerializer = new ThreadLocal<YamlSerializer>(() => new YamlSerializer(SerializationOptions.JsonCompatible | SerializationOptions.DisableAliases));
19+
20+
/// <summary>
21+
/// Helper method to validate serialize/deserialize results.
22+
/// </summary>
23+
protected void ValidateYamlRoundTrip<T>(T model)
24+
{
25+
// Act
26+
using var writer = new StringWriter();
27+
YamlUtility.Serialize(writer, model);
28+
var yaml = writer.ToString();
29+
30+
var result = YamlUtility.Deserialize<T>(new StringReader(yaml));
31+
32+
// Assert
33+
result.Should().BeEquivalentTo(model);
34+
}
35+
36+
/// <summary>
37+
/// Helper method to validate serialize/deserialize results.
38+
/// </summary>
39+
protected void ValidateYamlJsonRoundTrip<T>(T model)
40+
{
41+
// 1. Serialize to JSON with YamlDotNet
42+
using var writer = new StringWriter();
43+
YamlJsonSerializer.Value.Serialize(writer, model);
44+
var json = writer.ToString();
45+
46+
// 2. Deserialize JSON to model
47+
var result = JsonUtility.Deserialize<T>(new StringReader(json));
48+
49+
// Assert
50+
result.Should().BeEquivalentTo(model);
51+
}
52+
}

test/docfx.Tests/Utilities/PathHelper.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static string GetSolutionFolder([CallerFilePath] string callerFilePath =
2222
{
2323
// CallerFilePath is resolved at build timing.
2424
// If build/test is executed on separated machine. It failed to find file.
25-
throw new Exception($"File is not found. callerFilePath: {callerFilePath}");
25+
throw new FileNotFoundException($"File is not found. callerFilePath: {callerFilePath}");
2626
}
2727

2828
return FindSolutionFolder(callerFilePath, "docfx");
@@ -46,4 +46,63 @@ private static string FindSolutionFolder(string callerFilePath, string solutionN
4646

4747
return dir.FullName;
4848
}
49+
50+
/// <summary>
51+
/// Find TestData from callerFilePath.
52+
/// </summary>
53+
public static string ResolveTestDataPath(string path = "", [CallerFilePath] string callerFilePath = "")
54+
{
55+
if (Path.IsPathFullyQualified(path))
56+
return path;
57+
58+
var dir = GetTestDataDirectory(callerFilePath);
59+
60+
var resultPath = Path.Combine(dir, path);
61+
if (!File.Exists(callerFilePath) && !Directory.Exists(callerFilePath))
62+
{
63+
throw new FileNotFoundException($"Specified TestData file/directory is not found. path: {resultPath}");
64+
}
65+
66+
return Path.GetFullPath(resultPath);
67+
}
68+
69+
/// <summary>
70+
/// Find TestData from callerFilePath.
71+
/// </summary>
72+
public static string GetTestDataDirectory([CallerFilePath] string callerFilePath = "")
73+
{
74+
if (callerFilePath.StartsWith("/_/"))
75+
{
76+
var workspace = Environment.GetEnvironmentVariable("GITHUB_WORKSPACE");
77+
if (workspace != null)
78+
callerFilePath = callerFilePath.Replace("/_/", workspace);
79+
}
80+
81+
if (!File.Exists(callerFilePath))
82+
{
83+
// CallerFilePath is resolved at build timing.
84+
// If build/test is executed on separated machine. It failed to find file.
85+
throw new FileNotFoundException($"File is not found. callerFilePath: {callerFilePath}");
86+
}
87+
88+
// Find closest `TestData` directory.
89+
var dir = new FileInfo(callerFilePath).Directory;
90+
while (dir != null)
91+
{
92+
var testDataDir = dir.EnumerateDirectories()
93+
.FirstOrDefault(d => d.Name == "TestData");
94+
if (testDataDir != null)
95+
{
96+
dir = testDataDir;
97+
break;
98+
}
99+
100+
dir = dir.Parent;
101+
}
102+
103+
if (dir == null)
104+
throw new DirectoryNotFoundException("Failed to find TestData folder.");
105+
106+
return dir.FullName;
107+
}
49108
}

0 commit comments

Comments
 (0)