From 2a2f46def7a1038fa52bde0e9324840ca1734275 Mon Sep 17 00:00:00 2001 From: Mohammadreza Taikandi Date: Sat, 30 Jan 2021 08:59:39 +0000 Subject: [PATCH] Move tests into separate classes. --- test/MapTo.Tests/Common.cs | 131 +++ .../IgnorePropertyAttributeTests.cs | 73 ++ test/MapTo.Tests/MapPropertyTests.cs | 83 ++ test/MapTo.Tests/MapToTests.cs | 342 +++++++ test/MapTo.Tests/MapTypeConverterTests.cs | 283 ++++++ test/MapTo.Tests/Tests.cs | 855 ------------------ 6 files changed, 912 insertions(+), 855 deletions(-) create mode 100644 test/MapTo.Tests/Common.cs create mode 100644 test/MapTo.Tests/IgnorePropertyAttributeTests.cs create mode 100644 test/MapTo.Tests/MapPropertyTests.cs create mode 100644 test/MapTo.Tests/MapToTests.cs create mode 100644 test/MapTo.Tests/MapTypeConverterTests.cs delete mode 100644 test/MapTo.Tests/Tests.cs diff --git a/test/MapTo.Tests/Common.cs b/test/MapTo.Tests/Common.cs new file mode 100644 index 0000000..7b3c8bc --- /dev/null +++ b/test/MapTo.Tests/Common.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using MapTo.Extensions; +using MapTo.Sources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo.Tests +{ + internal static class Common + { + internal const int Indent1 = 4; + internal const int Indent2 = Indent1 * 2; + internal const int Indent3 = Indent1 * 3; + internal static readonly Location IgnoreLocation = Location.None; + + internal static readonly Dictionary DefaultAnalyzerOptions = new() + { + [GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" + }; + + internal static string GetSourceText(SourceGeneratorOptions options = null) + { + const string ns = "Test"; + options ??= new SourceGeneratorOptions(); + var hasDifferentSourceNamespace = options.SourceClassNamespace != ns; + var builder = new StringBuilder(); + + builder.AppendLine("//"); + builder.AppendLine("// Test source code."); + builder.AppendLine("//"); + builder.AppendLine(); + + if (options.UseMapToNamespace) + { + builder.AppendFormat("using {0};", Constants.RootNamespace).AppendLine(); + } + + builder + .AppendFormat("using {0};", options.SourceClassNamespace) + .AppendLine() + .AppendLine(); + + builder + .AppendFormat("namespace {0}", ns) + .AppendOpeningBracket(); + + if (hasDifferentSourceNamespace && options.UseMapToNamespace) + { + builder + .PadLeft(Indent1) + .AppendFormat("using {0};", options.SourceClassNamespace) + .AppendLine() + .AppendLine(); + } + + builder + .PadLeft(Indent1) + .AppendLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]") + .PadLeft(Indent1).Append("public partial class Foo") + .AppendOpeningBracket(Indent1); + + for (var i = 1; i <= options.ClassPropertiesCount; i++) + { + builder + .PadLeft(Indent2) + .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); + } + + options.PropertyBuilder?.Invoke(builder); + + builder + .AppendClosingBracket(Indent1, false) + .AppendClosingBracket() + .AppendLine() + .AppendLine(); + + builder + .AppendFormat("namespace {0}", options.SourceClassNamespace) + .AppendOpeningBracket() + .PadLeft(Indent1).Append("public class Baz") + .AppendOpeningBracket(Indent1); + + for (var i = 1; i <= options.SourceClassPropertiesCount; i++) + { + builder + .PadLeft(Indent2) + .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); + } + + options.SourcePropertyBuilder?.Invoke(builder); + + builder + .AppendClosingBracket(Indent1, false) + .AppendClosingBracket(); + + return builder.ToString(); + } + + internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo") + { + return syntaxTree.GetRoot() + .DescendantNodes() + .OfType() + .Single(c => c.Identifier.ValueText == targetClass) + .DescendantNodes() + .OfType() + .Single(p => p.Identifier.ValueText == targetPropertyName); + } + + internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo") + { + var syntaxTree = compilation.SyntaxTrees.First(); + var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass); + + var semanticModel = compilation.GetSemanticModel(syntaxTree); + return semanticModel.GetDeclaredSymbol(propSyntax); + } + + internal record SourceGeneratorOptions( + bool UseMapToNamespace = false, + string SourceClassNamespace = "Test.Models", + int ClassPropertiesCount = 3, + int SourceClassPropertiesCount = 3, + Action PropertyBuilder = null, + Action SourcePropertyBuilder = null); + } +} \ No newline at end of file diff --git a/test/MapTo.Tests/IgnorePropertyAttributeTests.cs b/test/MapTo.Tests/IgnorePropertyAttributeTests.cs new file mode 100644 index 0000000..0434803 --- /dev/null +++ b/test/MapTo.Tests/IgnorePropertyAttributeTests.cs @@ -0,0 +1,73 @@ +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Shouldly; +using Xunit; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class IgnorePropertyAttributeTests + { + [Fact] + public void VerifyIgnorePropertyAttribute() + { + // Arrange + const string source = ""; + var expectedAttribute = $@" +{Constants.GeneratedFilesHeader} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class IgnorePropertyAttribute : Attribute {{ }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute); + } + + [Fact] + public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("[IgnoreProperty]") + .PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + { + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + } + } +} \ No newline at end of file diff --git a/test/MapTo.Tests/MapPropertyTests.cs b/test/MapTo.Tests/MapPropertyTests.cs new file mode 100644 index 0000000..daaeb6a --- /dev/null +++ b/test/MapTo.Tests/MapPropertyTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Microsoft.CodeAnalysis; +using Shouldly; +using Xunit; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MapPropertyTests + { + [Theory] + [InlineData(NullableContextOptions.Disable)] + [InlineData(NullableContextOptions.Enable)] + public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions) + { + // Arrange + const string source = ""; + var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public sealed class MapPropertyAttribute : Attribute + {{ + public string{nullableSyntax} SourcePropertyName {{ get; set; }} + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface); + } + + [Fact] + public void When_MapPropertyFound_Should_UseItToMapToSourceProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]") + .PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + { + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + Prop4 = baz.Prop3; + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + } + } +} \ No newline at end of file diff --git a/test/MapTo.Tests/MapToTests.cs b/test/MapTo.Tests/MapToTests.cs new file mode 100644 index 0000000..4346368 --- /dev/null +++ b/test/MapTo.Tests/MapToTests.cs @@ -0,0 +1,342 @@ +using System.Collections.Generic; +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Shouldly; +using Xunit; +using static MapTo.Extensions.GeneratorExecutionContextExtensions; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MapToTests + { + private static readonly string ExpectedAttribute = $@"{Constants.GeneratedFilesHeader} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class MapFromAttribute : Attribute + {{ + public MapFromAttribute(Type sourceType) + {{ + SourceType = sourceType; + }} + + public Type SourceType {{ get; }} + }} +}}"; + + [Fact] + public void VerifyMapToAttribute() + { + // Arrange + const string source = ""; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeName, ExpectedAttribute); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithDifferentTypes_Should_ReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation)); + + diagnostics.ShouldBeUnsuccessful(expectedError); + } + + [Fact] + public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier() + { + // Arrange + var source = GetSourceText(); + var configOptions = new Dictionary + { + [GetBuildPropertyName(nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier))] = "Internal", + [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" + }; + + var expectedExtension = @" + internal static partial class BazToFooExtensions + { + internal static Foo ToFoo(this Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + } + }".Trim(); + + var expectedFactory = @" + internal static Foo From(Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + }".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + + var syntaxTree = compilation.SyntaxTrees.Last().ToString(); + syntaxTree.ShouldContain(expectedFactory); + syntaxTree.ShouldContain(expectedExtension); + } + + [Fact] + public void When_MapToAttributeFound_Should_GenerateTheClass() + { + // Arrange + const string source = @" +using MapTo; + +namespace Test +{ + [MapFrom(typeof(Baz))] + public partial class Foo + { + public int Prop1 { get; set; } + } + + public class Baz + { + public int Prop1 { get; set; } + } +} +"; + + const string expectedResult = @" +// + +using System; + +namespace Test +{ + partial class Foo + { + public Foo(Test.Baz baz) + { + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); + } + + [Fact] + public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError() + { + // Arrange + const string source = @" +using MapTo; + +namespace Test +{ + [MapFrom(typeof(Baz))] + public partial class Foo { } + + public class Baz { public int Prop1 { get; set; } } +} +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + var fooType = compilation.GetTypeByMetadataName("Test.Foo"); + fooType.ShouldNotBeNull(); + + var bazType = compilation.GetTypeByMetadataName("Test.Baz"); + bazType.ShouldNotBeNull(); + + var expectedDiagnostic = DiagnosticProvider.NoMatchingPropertyFoundError(fooType.Locations.Single(), fooType, bazType); + var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id); + error.ShouldNotBeNull(); + } + + [Fact] + public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass() + { + // Arrange + const string source = @" +namespace Test +{ + [MapTo.MapFrom(typeof(Baz))] + public partial class Foo { public int Prop1 { get; set; } } + + public class Baz { public int Prop1 { get; set; } } +} +"; + + const string expectedResult = @" +// + +using System; + +namespace Test +{ + partial class Foo + { + public Foo(Test.Baz baz) + { + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); + } + + [Fact] + public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute() + { + // Arrange + const string source = ""; + var expectedTypes = new[] + { + IgnorePropertyAttributeSource.AttributeName, + MapFromAttributeSource.AttributeName, + ITypeConverterSource.InterfaceName, + MapPropertyAttributeSource.AttributeName + }; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees + .Select(s => s.ToString()) + .Where(s => !string.IsNullOrWhiteSpace(s.ToString())) + .All(s => expectedTypes.Any(s.Contains)) + .ShouldBeTrue(); + } + + [Fact] + public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar")); + + const string expectedResult = @" +// + +using System; + +namespace Test +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); + } + + [Fact] + public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest() + { + // Arrange + var source = GetSourceText(); + + const string expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + { + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); + } + + [Fact] + public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod() + { + // Arrange + var source = GetSourceText(); + + const string expectedResult = @" + public static Foo From(Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); + } + + [Fact] + public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType() + { + // Arrange + var source = GetSourceText(); + + const string expectedResult = @" + public static partial class BazToFooExtensions + { + public static Foo ToFoo(this Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + } + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); + } + } +} \ No newline at end of file diff --git a/test/MapTo.Tests/MapTypeConverterTests.cs b/test/MapTo.Tests/MapTypeConverterTests.cs new file mode 100644 index 0000000..f79e952 --- /dev/null +++ b/test/MapTo.Tests/MapTypeConverterTests.cs @@ -0,0 +1,283 @@ +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Microsoft.CodeAnalysis; +using Shouldly; +using Xunit; +using static MapTo.Extensions.GeneratorExecutionContextExtensions; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MapTypeConverterTests + { + [Fact] + public void VerifyTypeConverterInterface() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} + +namespace MapTo +{{ + public interface ITypeConverter + {{ + TDestination Convert(TSource source, object[] converterParameters); + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); + } + + [Fact] + public void VerifyTypeConverterInterfaceWithNullableOptionOn() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +#nullable enable + +namespace MapTo +{{ + public interface ITypeConverter + {{ + TDestination Convert(TSource source, object[]? converterParameters); + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); + } + + [Fact] + public void VerifyMapTypeConverterAttribute() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} + +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class MapTypeConverterAttribute : Attribute + {{ + public MapTypeConverterAttribute(Type converter, object[] converterParameters = null) + {{ + Converter = converter; + ConverterParameters = converterParameters; + }} + + public Type Converter {{ get; }} + + public object[] ConverterParameters {{ get; }} + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); + } + + [Fact] + public void VerifyMapTypeConverterAttributeWithNullableOptionOn() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +#nullable enable + +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class MapTypeConverterAttribute : Attribute + {{ + public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null) + {{ + Converter = converter; + ConverterParameters = converterParameters; + }} + + public Type Converter {{ get; }} + + public object[]? ConverterParameters {{ get; }} + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + { + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + Prop4 = baz.Prop4; + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("[IgnoreProperty]") + .PadLeft(Indent2).AppendLine("public long IgnoreMe { get; set; }") + .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter))]") + .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"))); + + source += @" +namespace Test +{ + using MapTo; + + public class Prop4Converter: ITypeConverter + { + public int Convert(string source, object[] converterParameters) => int.Parse(source); + } +} +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticProvider.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz")); + diagnostics.ShouldBeUnsuccessful(expectedError); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter))]") + .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"))); + + source += @" +namespace Test +{ + using MapTo; + + public class Prop4Converter: ITypeConverter + { + public long Convert(string source, object[] converterParameters) => long.Parse(source); + } +} +"; + + const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]") + .PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"))); + + source += @" +namespace Test +{ + using MapTo; + + public class Prop4Converter: ITypeConverter + { + public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string); + } +} +"; + + const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); + } + } +} \ No newline at end of file diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs deleted file mode 100644 index a91eb2e..0000000 --- a/test/MapTo.Tests/Tests.cs +++ /dev/null @@ -1,855 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using MapTo.Extensions; -using MapTo.Sources; -using MapTo.Tests.Extensions; -using MapTo.Tests.Infrastructure; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Shouldly; -using Xunit; -using static MapTo.Extensions.GeneratorExecutionContextExtensions; - -namespace MapTo.Tests -{ - public class Tests - { - private const int Indent1 = 4; - private const int Indent2 = Indent1 * 2; - private const int Indent3 = Indent1 * 3; - private static readonly Location IgnoreLocation = Location.None; - - private static readonly Dictionary DefaultAnalyzerOptions = new() - { - [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" - }; - - private static readonly string ExpectedAttribute = $@"{Constants.GeneratedFilesHeader} -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class MapFromAttribute : Attribute - {{ - public MapFromAttribute(Type sourceType) - {{ - SourceType = sourceType; - }} - - public Type SourceType {{ get; }} - }} -}}"; - - private record SourceGeneratorOptions( - bool UseMapToNamespace = false, - string SourceClassNamespace = "Test.Models", - int ClassPropertiesCount = 3, - int SourceClassPropertiesCount = 3, - Action PropertyBuilder = null, - Action SourcePropertyBuilder = null); - - private static string GetSourceText(SourceGeneratorOptions options = null) - { - const string ns = "Test"; - options ??= new SourceGeneratorOptions(); - var hasDifferentSourceNamespace = options.SourceClassNamespace != ns; - var builder = new StringBuilder(); - - builder.AppendLine("//"); - builder.AppendLine("// Test source code."); - builder.AppendLine("//"); - builder.AppendLine(); - - if (options.UseMapToNamespace) - { - builder.AppendFormat("using {0};", Constants.RootNamespace).AppendLine(); - } - - builder - .AppendFormat("using {0};", options.SourceClassNamespace) - .AppendLine() - .AppendLine(); - - builder - .AppendFormat("namespace {0}", ns) - .AppendOpeningBracket(); - - if (hasDifferentSourceNamespace && options.UseMapToNamespace) - { - builder - .PadLeft(Indent1) - .AppendFormat("using {0};", options.SourceClassNamespace) - .AppendLine() - .AppendLine(); - } - - builder - .PadLeft(Indent1) - .AppendLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]") - .PadLeft(Indent1).Append("public partial class Foo") - .AppendOpeningBracket(Indent1); - - for (var i = 1; i <= options.ClassPropertiesCount; i++) - { - builder - .PadLeft(Indent2) - .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); - } - - options.PropertyBuilder?.Invoke(builder); - - builder - .AppendClosingBracket(Indent1, false) - .AppendClosingBracket() - .AppendLine() - .AppendLine(); - - builder - .AppendFormat("namespace {0}", options.SourceClassNamespace) - .AppendOpeningBracket() - .PadLeft(Indent1).Append("public class Baz") - .AppendOpeningBracket(Indent1); - - for (var i = 1; i <= options.SourceClassPropertiesCount; i++) - { - builder - .PadLeft(Indent2) - .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); - } - - options.SourcePropertyBuilder?.Invoke(builder); - - builder - .AppendClosingBracket(Indent1, false) - .AppendClosingBracket(); - - return builder.ToString(); - } - - [Fact] - public void VerifyIgnorePropertyAttribute() - { - // Arrange - const string source = ""; - var expectedAttribute = $@" -{Constants.GeneratedFilesHeader} -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class IgnorePropertyAttribute : Attribute {{ }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute); - } - - [Fact] - public void VerifyMapToAttribute() - { - // Arrange - const string source = ""; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeName, ExpectedAttribute); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithDifferentTypes_Should_ReportError() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation)); - - diagnostics.ShouldBeUnsuccessful(expectedError); - } - - [Fact] - public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("[IgnoreProperty]") - .PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); - - var expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - { - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - } -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); - } - - [Fact] - public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier() - { - // Arrange - var source = GetSourceText(); - var configOptions = new Dictionary - { - [GetBuildPropertyName(nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier))] = "Internal", - [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" - }; - - var expectedExtension = @" - internal static partial class BazToFooExtensions - { - internal static Foo ToFoo(this Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - } - }".Trim(); - - var expectedFactory = @" - internal static Foo From(Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - }".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - - var syntaxTree = compilation.SyntaxTrees.Last().ToString(); - syntaxTree.ShouldContain(expectedFactory); - syntaxTree.ShouldContain(expectedExtension); - } - - [Fact] - public void When_MapToAttributeFound_Should_GenerateTheClass() - { - // Arrange - const string source = @" -using MapTo; - -namespace Test -{ - [MapFrom(typeof(Baz))] - public partial class Foo - { - public int Prop1 { get; set; } - } - - public class Baz - { - public int Prop1 { get; set; } - } -} -"; - - const string expectedResult = @" -// - -using System; - -namespace Test -{ - partial class Foo - { - public Foo(Test.Baz baz) - { - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - Prop1 = baz.Prop1; - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); - } - - [Fact] - public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError() - { - // Arrange - const string source = @" -using MapTo; - -namespace Test -{ - [MapFrom(typeof(Baz))] - public partial class Foo { } - - public class Baz { public int Prop1 { get; set; } } -} -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - var fooType = compilation.GetTypeByMetadataName("Test.Foo"); - fooType.ShouldNotBeNull(); - - var bazType = compilation.GetTypeByMetadataName("Test.Baz"); - bazType.ShouldNotBeNull(); - - var expectedDiagnostic = DiagnosticProvider.NoMatchingPropertyFoundError(fooType.Locations.Single(), fooType, bazType); - var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id); - error.ShouldNotBeNull(); - } - - [Fact] - public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass() - { - // Arrange - const string source = @" -namespace Test -{ - [MapTo.MapFrom(typeof(Baz))] - public partial class Foo { public int Prop1 { get; set; } } - - public class Baz { public int Prop1 { get; set; } } -} -"; - - const string expectedResult = @" -// - -using System; - -namespace Test -{ - partial class Foo - { - public Foo(Test.Baz baz) - { - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - Prop1 = baz.Prop1; - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); - } - - [Fact] - public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute() - { - // Arrange - const string source = ""; - var expectedTypes = new[] - { - IgnorePropertyAttributeSource.AttributeName, - MapFromAttributeSource.AttributeName, - ITypeConverterSource.InterfaceName, - MapPropertyAttributeSource.AttributeName - }; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees - .Select(s => s.ToString()) - .Where(s => !string.IsNullOrWhiteSpace(s.ToString())) - .All(s => expectedTypes.Any(s.Contains)) - .ShouldBeTrue(); - } - - [Fact] - public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar")); - - const string expectedResult = @" -// - -using System; - -namespace Test -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); - } - - [Fact] - public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest() - { - // Arrange - var source = GetSourceText(); - - const string expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - { - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); - } - - [Fact] - public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod() - { - // Arrange - var source = GetSourceText(); - - const string expectedResult = @" - public static Foo From(Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); - } - - [Fact] - public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType() - { - // Arrange - var source = GetSourceText(); - - const string expectedResult = @" - public static partial class BazToFooExtensions - { - public static Foo ToFoo(this Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - } - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim()); - } - - [Fact] - public void VerifyTypeConverterInterface() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} - -namespace MapTo -{{ - public interface ITypeConverter - {{ - TDestination Convert(TSource source, object[] converterParameters); - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); - } - - [Fact] - public void VerifyTypeConverterInterfaceWithNullableOptionOn() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} -#nullable enable - -namespace MapTo -{{ - public interface ITypeConverter - {{ - TDestination Convert(TSource source, object[]? converterParameters); - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); - } - - [Fact] - public void VerifyMapTypeConverterAttribute() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} - -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public sealed class MapTypeConverterAttribute : Attribute - {{ - public MapTypeConverterAttribute(Type converter, object[] converterParameters = null) - {{ - Converter = converter; - ConverterParameters = converterParameters; - }} - - public Type Converter {{ get; }} - - public object[] ConverterParameters {{ get; }} - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); - } - - [Fact] - public void VerifyMapTypeConverterAttributeWithNullableOptionOn() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} -#nullable enable - -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] - public sealed class MapTypeConverterAttribute : Attribute - {{ - public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null) - {{ - Converter = converter; - ConverterParameters = converterParameters; - }} - - public Type Converter {{ get; }} - - public object[]? ConverterParameters {{ get; }} - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); - - var expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - { - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - Prop4 = baz.Prop4; - } -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("[IgnoreProperty]") - .PadLeft(Indent2).AppendLine("public long IgnoreMe { get; set; }") - .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter))]") - .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"))); - - source += @" -namespace Test -{ - using MapTo; - - public class Prop4Converter: ITypeConverter - { - public int Convert(string source, object[] converterParameters) => int.Parse(source); - } -} -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - var expectedError = DiagnosticProvider.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz")); - diagnostics.ShouldBeUnsuccessful(expectedError); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter))]") - .PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"))); - - source += @" -namespace Test -{ - using MapTo; - - public class Prop4Converter: ITypeConverter - { - public long Convert(string source, object[] converterParameters) => long.Parse(source); - } -} -"; - - const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]") - .PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }"))); - - source += @" -namespace Test -{ - using MapTo; - - public class Prop4Converter: ITypeConverter - { - public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string); - } -} -"; - - const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax); - } - - [Theory] - [InlineData(NullableContextOptions.Disable)] - [InlineData(NullableContextOptions.Enable)] - public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions) - { - // Arrange - const string source = ""; - var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} -{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}": string.Empty)} -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - public sealed class MapPropertyAttribute : Attribute - {{ - public string{nullableSyntax} SourcePropertyName {{ get; set; }} - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface); - } - - [Fact] - public void When_MapPropertyFound_Should_UseItToMapToSourceProperty() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .PadLeft(Indent2).AppendLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]") - .PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }"))); - - var expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - { - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - Prop4 = baz.Prop3; - } -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult); - } - - private static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo") - { - return syntaxTree.GetRoot() - .DescendantNodes() - .OfType() - .Single(c => c.Identifier.ValueText == targetClass) - .DescendantNodes() - .OfType() - .Single(p => p.Identifier.ValueText == targetPropertyName); - } - - private static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo") - { - var syntaxTree = compilation.SyntaxTrees.First(); - var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass); - - var semanticModel = compilation.GetSemanticModel(syntaxTree); - return semanticModel.GetDeclaredSymbol(propSyntax); - } - } -} \ No newline at end of file