Files
QWERTYkez.Mensura/QWERTYkez.Mensura.Generator/TestsGenerator.cs
2026-06-10 16:05:42 +07:00

419 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace G;
[Generator]
public class TestsGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var compilationProvider = context.CompilationProvider;
context.RegisterSourceOutput(compilationProvider, (spc, compilation) =>
{
if (compilation.AssemblyName == null || !compilation.AssemblyName.EndsWith(".Tests"))
return;
var mensuraAssembly = compilation.References
.Select(r => compilation.GetAssemblyOrModuleSymbol(r) as IAssemblySymbol)
.FirstOrDefault(a => a?.Name == "QWERTYkez.Mensura");
if (mensuraAssembly == null)
return;
var unitTypes = CollectAllUnitTypes(mensuraAssembly.GlobalNamespace);
if (unitTypes.Count == 0) return;
// 1. Сериализация
GenerateSerializationTests(spc, unitTypes);
// 2. Операторы (с атрибутом)
var operators = CollectOperatorsFromUnitTypes(unitTypes);
if (operators.Length > 0)
GenerateOperatorTests(spc, operators);
// 3. Коллекционные методы
GenerateCollectionTests(spc, unitTypes);
});
}
// ========== 1. СЕРИАЛИЗАЦИЯ ==========
private static void GenerateSerializationTests(SourceProductionContext spc, List<INamedTypeSymbol> types)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#pragma warning disable");
sb.AppendLine();
sb.AppendLine("using Xunit;");
sb.AppendLine("using System.Text.Json;");
sb.AppendLine("using Newtonsoft.Json;");
sb.AppendLine("using QWERTYkez.Mensura.Units;");
sb.AppendLine();
sb.AppendLine("namespace QWERTYkez.Mensura.Tests");
sb.AppendLine("{");
sb.AppendLine(" public class SerializationTests");
sb.AppendLine(" {");
sb.AppendLine(" private const double Tolerance = 1e-12;");
sb.AppendLine();
foreach (var type in types)
{
var baseProp = GetBaseUnitProperty(type);
if (baseProp == null) continue;
var typeName = type.Name;
var propName = baseProp.Name;
sb.AppendLine($" [Fact] public void {typeName}_SystemTextJson_SerializeDeserialize()");
sb.AppendLine($" {{");
sb.AppendLine($" var original = {typeName}.{propName};");
sb.AppendLine($" var json = System.Text.Json.JsonSerializer.Serialize(original);");
sb.AppendLine($" var deserialized = System.Text.Json.JsonSerializer.Deserialize<{typeName}>(json);");
sb.AppendLine($" Assert.Equal((double)original, (double)deserialized, Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
sb.AppendLine($" [Fact] public void {typeName}_NewtonsoftJson_SerializeDeserialize()");
sb.AppendLine($" {{");
sb.AppendLine($" var original = {typeName}.{propName};");
sb.AppendLine($" var json = Newtonsoft.Json.JsonConvert.SerializeObject(original);");
sb.AppendLine($" var deserialized = Newtonsoft.Json.JsonConvert.DeserializeObject<{typeName}>(json);");
sb.AppendLine($" Assert.Equal((double)original, (double)deserialized, Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
spc.AddSource("SerializationTests.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
}
// ========== 2. ОПЕРАТОРЫ (только с атрибутом) ==========
private static ImmutableArray<OperatorInfo> CollectOperatorsFromUnitTypes(List<INamedTypeSymbol> unitTypes)
{
var result = new List<OperatorInfo>();
foreach (var containingType in unitTypes)
{
foreach (var member in containingType.GetMembers())
{
if (member is not IMethodSymbol method || method.MethodKind != MethodKind.UserDefinedOperator)
continue;
bool hasAttr = method.GetAttributes().Any(attr =>
attr.AttributeClass?.Name == "OperatorsGeneratorAttribute" ||
attr.AttributeClass?.ToString() == "QWERTYkez.Mensura.OperatorsGeneratorAttribute");
if (!hasAttr) continue;
string opSymbol = method.Name switch
{
"op_Multiply" => "*",
"op_Division" => "/",
_ => null
};
if (opSymbol == null) continue;
var pars = method.Parameters;
if (pars.Length != 2) continue;
var leftType = pars[0].Type as INamedTypeSymbol;
var rightType = pars[1].Type as INamedTypeSymbol;
var returnType = method.ReturnType as INamedTypeSymbol;
if (leftType == null || rightType == null || returnType == null) continue;
if (!unitTypes.Contains(leftType, SymbolEqualityComparer.Default) ||
!unitTypes.Contains(rightType, SymbolEqualityComparer.Default) ||
!unitTypes.Contains(returnType, SymbolEqualityComparer.Default))
continue;
string? coeffField = null;
foreach (var attr in method.GetAttributes())
{
if (attr.AttributeClass?.Name == "OperatorsGeneratorAttribute" ||
attr.AttributeClass?.ToString() == "QWERTYkez.Mensura.OperatorsGeneratorAttribute")
{
foreach (var arg in attr.ConstructorArguments)
{
if (arg.Value is string s)
{
coeffField = s;
break;
}
}
break;
}
}
result.Add(new OperatorInfo(opSymbol, leftType, rightType, returnType, coeffField, containingType));
}
}
return result.ToImmutableArray();
}
private static void GenerateOperatorTests(SourceProductionContext spc, ImmutableArray<OperatorInfo> operators)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#pragma warning disable");
sb.AppendLine();
sb.AppendLine("using Xunit;");
sb.AppendLine("using QWERTYkez.Mensura.Units;");
sb.AppendLine("using System.Reflection;");
sb.AppendLine();
sb.AppendLine("namespace QWERTYkez.Mensura.Tests");
sb.AppendLine("{");
sb.AppendLine(" public class OperatorsTests");
sb.AppendLine(" {");
sb.AppendLine(" private const double Tolerance = 1e-12;");
sb.AppendLine();
foreach (var op in operators)
{
var leftBase = GetBaseUnitProperty(op.LeftType);
var rightBase = GetBaseUnitProperty(op.RightType);
if (leftBase == null || rightBase == null) continue;
string leftTypeName = op.LeftType.Name;
string rightTypeName = op.RightType.Name;
string returnTypeName = op.ReturnType.Name;
string leftProp = leftBase.Name;
string rightProp = rightBase.Name;
string testName = $"{op.OperatorSymbol}_{leftTypeName}_{rightTypeName}_Returns_{returnTypeName}"
.Replace("<", "_").Replace(">", "_").Replace(",", "_").Replace(" ", "")
.Replace("*", "Mul").Replace("/", "Div");
string expectedExpr;
if (op.CoefficientField != null)
{
expectedExpr = $@"(double)left {op.OperatorSymbol} (double)right * " +
$@"(double)typeof({op.ContainingType.Name}).GetField(""{op.CoefficientField}"", " +
$@"BindingFlags.NonPublic | BindingFlags.Static).GetValue(null)";
}
else
{
expectedExpr = $"(double)left {op.OperatorSymbol} (double)right";
}
sb.AppendLine($" [Fact] public void {testName}()");
sb.AppendLine($" {{");
sb.AppendLine($" var left = {leftTypeName}.{leftProp};");
sb.AppendLine($" var right = {rightTypeName}.{rightProp};");
sb.AppendLine($" var result = left {op.OperatorSymbol} right;");
sb.AppendLine($" var expected = {expectedExpr};");
sb.AppendLine($" Assert.Equal(expected, (double)result, Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
}
if (operators.Length == 0)
sb.AppendLine(" // No operators with [OperatorsGenerator] found");
sb.AppendLine(" }");
sb.AppendLine("}");
spc.AddSource("OperatorsTests.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
}
// ========== 3. КОЛЛЕКЦИОННЫЕ МЕТОДЫ ==========
private static void GenerateCollectionTests(SourceProductionContext spc, List<INamedTypeSymbol> types)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#pragma warning disable");
sb.AppendLine();
sb.AppendLine("using Xunit;");
sb.AppendLine("using QWERTYkez.Mensura.Units;");
sb.AppendLine("using QWERTYkez.Mensura.Extensions;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using System.Linq;");
sb.AppendLine();
sb.AppendLine("namespace QWERTYkez.Mensura.Tests");
sb.AppendLine("{");
sb.AppendLine(" public class CollectionTests");
sb.AppendLine(" {");
sb.AppendLine(" private const double Tolerance = 1e-12;");
sb.AppendLine(" private const double scalar = 9d;");
sb.AppendLine(" private static double[] dArray = new double[] { 5d, 3d };");
sb.AppendLine();
foreach (var type in types)
{
var baseProp = GetBaseUnitProperty(type);
if (baseProp == null) continue;
string typeName = type.Name;
string propName = baseProp.Name;
// Создаём массив из двух тестовых значений
sb.AppendLine($" private {typeName}[] {typeName}_GetTestArray() => {typeName}.{propName} * dArray;");
sb.AppendLine();
// ========== Multiply ==========
// scalar * array
sb.AppendLine($" [Fact] public void {typeName}_Multiply_ScalarByArray_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var arr = {typeName}_GetTestArray();");
sb.AppendLine($" var result = scalar.Mul(arr);");
sb.AppendLine($" Assert.Equal(2, result.Length);");
sb.AppendLine($" Assert.Equal(scalar * 5d, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(scalar * 3d, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// array * scalar
sb.AppendLine($" [Fact] public void {typeName}_Multiply_ArrayByScalar_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var arr = {typeName}_GetTestArray();");
sb.AppendLine($" var result = arr.Mul(scalar);");
sb.AppendLine($" Assert.Equal(2, result.Length);");
sb.AppendLine($" Assert.Equal(scalar * 5d, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(scalar * 3d, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// scalar * List
sb.AppendLine($" [Fact] public void {typeName}_Multiply_ScalarByList_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var list = {typeName}_GetTestArray().ToList();");
sb.AppendLine($" var result = scalar.Mul(list);");
sb.AppendLine($" var count = result.Count;");
sb.AppendLine($" Assert.Equal(2, count);");
sb.AppendLine($" Assert.Equal(scalar * 5d, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(scalar * 3d, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// List * scalar
sb.AppendLine($" [Fact] public void {typeName}_Multiply_ListByScalar_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var list = {typeName}_GetTestArray().ToList();");
sb.AppendLine($" var result = list.Mul(scalar);");
sb.AppendLine($" var count = result.Count;");
sb.AppendLine($" Assert.Equal(2, count);");
sb.AppendLine($" Assert.Equal(scalar * 5d, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(scalar * 3d, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// scalar * IEnumerable
sb.AppendLine($" [Fact] public void {typeName}_Multiply_ScalarByEnumerable_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var enumerable = {typeName}_GetTestArray().AsEnumerable();");
sb.AppendLine($" var result = scalar.Mul(enumerable);");
sb.AppendLine($" Assert.Equal(2, result.Count());");
sb.AppendLine($" Assert.Equal(scalar * 5d, (double)result.ElementAt(0), Tolerance);");
sb.AppendLine($" Assert.Equal(scalar * 3d, (double)result.ElementAt(1), Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// IEnumerable * scalar
sb.AppendLine($" [Fact] public void {typeName}_Multiply_EnumerableByScalar_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var enumerable = {typeName}_GetTestArray().AsEnumerable();");
sb.AppendLine($" var result = enumerable.Mul(scalar);");
sb.AppendLine($" Assert.Equal(2, result.Count());");
sb.AppendLine($" Assert.Equal(scalar * 5d, (double)result.ElementAt(0), Tolerance);");
sb.AppendLine($" Assert.Equal(scalar * 3d, (double)result.ElementAt(1), Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// ========== Divide ==========
// scalar / array
sb.AppendLine($" [Fact] public void {typeName}_Divide_ScalarByArray_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var arr = {typeName}_GetTestArray();");
sb.AppendLine($" var result = scalar.Div(arr);");
sb.AppendLine($" Assert.Equal(2, result.Length);");
sb.AppendLine($" Assert.Equal(scalar / 5d, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(scalar / 3d, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// array / scalar
sb.AppendLine($" [Fact] public void {typeName}_Divide_ArrayByScalar_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var arr = {typeName}_GetTestArray();");
sb.AppendLine($" var result = arr.Div(scalar);");
sb.AppendLine($" Assert.Equal(2, result.Length);");
sb.AppendLine($" Assert.Equal(5d / scalar, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(3d / scalar, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// scalar / List
sb.AppendLine($" [Fact] public void {typeName}_Divide_ScalarByList_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var list = {typeName}_GetTestArray().ToList();");
sb.AppendLine($" var result = scalar.Div(list);");
sb.AppendLine($" var count = result.Count;");
sb.AppendLine($" Assert.Equal(2, count);");
sb.AppendLine($" Assert.Equal(scalar / 5d, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(scalar / 3d, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// List / scalar
sb.AppendLine($" [Fact] public void {typeName}_Divide_ListByScalar_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var list = {typeName}_GetTestArray().ToList();");
sb.AppendLine($" var result = list.Div(scalar);");
sb.AppendLine($" var count = result.Count;");
sb.AppendLine($" Assert.Equal(2, count);");
sb.AppendLine($" Assert.Equal(5d / scalar, (double)result[0], Tolerance);");
sb.AppendLine($" Assert.Equal(3d / scalar, (double)result[1], Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// scalar / IEnumerable
sb.AppendLine($" [Fact] public void {typeName}_Divide_ScalarByEnumerable_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var enumerable = {typeName}_GetTestArray().AsEnumerable();");
sb.AppendLine($" var result = scalar.Div(enumerable);");
sb.AppendLine($" Assert.Equal(2, result.Count());");
sb.AppendLine($" Assert.Equal(scalar / 5d, (double)result.ElementAt(0), Tolerance);");
sb.AppendLine($" Assert.Equal(scalar / 3d, (double)result.ElementAt(1), Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
// IEnumerable / scalar
sb.AppendLine($" [Fact] public void {typeName}_Divide_EnumerableByScalar_Works()");
sb.AppendLine($" {{");
sb.AppendLine($" var enumerable = {typeName}_GetTestArray().AsEnumerable();");
sb.AppendLine($" var result = enumerable.Div(scalar);");
sb.AppendLine($" Assert.Equal(2, result.Count());");
sb.AppendLine($" Assert.Equal(5d / scalar, (double)result.ElementAt(0), Tolerance);");
sb.AppendLine($" Assert.Equal(3d / scalar, (double)result.ElementAt(1), Tolerance);");
sb.AppendLine($" }}");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
spc.AddSource("CollectionTests.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
}
// ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
private static List<INamedTypeSymbol> CollectAllUnitTypes(INamespaceSymbol root)
{
var result = new List<INamedTypeSymbol>();
CollectTypesRecursive(root, result, "QWERTYkez.Mensura.Units");
return result;
}
private static void CollectTypesRecursive(INamespaceSymbol ns, List<INamedTypeSymbol> result, string targetNamespacePrefix)
{
var nsFullName = ns.ToString();
if (nsFullName.StartsWith(targetNamespacePrefix))
{
foreach (var type in ns.GetTypeMembers())
result.Add(type);
}
foreach (var childNs in ns.GetNamespaceMembers())
CollectTypesRecursive(childNs, result, targetNamespacePrefix);
}
private static IPropertySymbol? GetBaseUnitProperty(ITypeSymbol type)
{
foreach (var member in type.GetMembers())
{
if (member is IPropertySymbol prop && prop.IsStatic && prop.Name.StartsWith("_") && SymbolEqualityComparer.Default.Equals(prop.Type, type))
return prop;
}
return null;
}
private readonly struct OperatorInfo(string symbol, ITypeSymbol left, ITypeSymbol right, ITypeSymbol ret, string? coeff, ITypeSymbol containing)
{
public string OperatorSymbol { get; } = symbol;
public ITypeSymbol LeftType { get; } = left;
public ITypeSymbol RightType { get; } = right;
public ITypeSymbol ReturnType { get; } = ret;
public string? CoefficientField { get; } = coeff;
public ITypeSymbol ContainingType { get; } = containing;
}
}