diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index ffbe1d2f..2a1d760d 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -23,13 +23,13 @@ defaults: jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.100 + dotnet-version: 6.0.202 # Run unit tests - name: Test @@ -53,7 +53,16 @@ jobs: } dotnet pack --configuration Release --verbosity normal --output . - + + - name: Upload NuGet + uses: actions/upload-artifact@v3 + with: + name: NuGet + if-no-files-found: error + path: | + **/*.nupkg + **/*.snupkg + # Update the docs - name: Update Docs if: github.event_name == 'push' diff --git a/Directory.Build.props b/Directory.Build.props index 9ddb2eac..82dc6821 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,10 +1,11 @@ - + 10.0 enable + enable true true - + \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 00000000..6132714b --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/ControllerTests.cs b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/ControllerTests.cs new file mode 100644 index 00000000..09e40d04 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/ControllerTests.cs @@ -0,0 +1,14 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Moq.AutoMock.Generator.Example.MSTest; + +[TestClass] +[ConstructorTests(TargetType = typeof(Controller))] +public partial class ControllerTests +{ + partial void ControllerConstructor_WithNullIService3_ThrowsArgumentNullExceptionSetup(AutoMocker mocker) + { + mocker.Use(""); + } +} + diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/Moq.AutoMock.Generator.Example.MSTest.csproj b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/Moq.AutoMock.Generator.Example.MSTest.csproj new file mode 100644 index 00000000..ee601859 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.MSTest/Moq.AutoMock.Generator.Example.MSTest.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Analyzer + false + + + + + + + diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/ControllerTests.cs b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/ControllerTests.cs new file mode 100644 index 00000000..24f60171 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/ControllerTests.cs @@ -0,0 +1,10 @@ +namespace Moq.AutoMock.Generator.Example.NUnit; + +[ConstructorTests(TargetType = typeof(Controller))] +public partial class ControllerTests +{ + partial void AutoMockerTestSetup(Moq.AutoMock.AutoMocker mocker, string testName) + { + mocker.Use(""); + } +} diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/Moq.AutoMock.Generator.Example.NUnit.csproj b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/Moq.AutoMock.Generator.Example.NUnit.csproj new file mode 100644 index 00000000..7f080ee3 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.NUnit/Moq.AutoMock.Generator.Example.NUnit.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Analyzer + false + + + + + + diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/ControllerTests.cs b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/ControllerTests.cs new file mode 100644 index 00000000..782549d8 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/ControllerTests.cs @@ -0,0 +1,11 @@ +namespace Moq.AutoMock.Generator.Example.xUnit; + +[ConstructorTests(TargetType = typeof(Controller))] +public partial class ControllerTests +{ + partial void AutoMockerTestSetup(Moq.AutoMock.AutoMocker mocker, string testName) + { + mocker.Use(""); + } +} + diff --git a/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/Moq.AutoMock.Generator.Example.xUnit.csproj b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/Moq.AutoMock.Generator.Example.xUnit.csproj new file mode 100644 index 00000000..b1dc24a8 --- /dev/null +++ b/GeneratorTests/Moq.AutoMock.Generator.Example.xUnit/Moq.AutoMock.Generator.Example.xUnit.csproj @@ -0,0 +1,40 @@ + + + + net6.0 + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + Analyzer + false + + + + + + + + + diff --git a/GeneratorTests/Moq.AutoMocker.Generator.Example/Controller.cs b/GeneratorTests/Moq.AutoMocker.Generator.Example/Controller.cs new file mode 100644 index 00000000..e9a2d129 --- /dev/null +++ b/GeneratorTests/Moq.AutoMocker.Generator.Example/Controller.cs @@ -0,0 +1,23 @@ +namespace Moq.AutoMock.Generator.Example; + +public class Controller +{ + public Controller(IService service) + { + _ = service ?? throw new ArgumentNullException(nameof(service)); + } + + public Controller(IService service1, IService service2) + { + _ = service1 ?? throw new ArgumentNullException(nameof(service1)); + _ = service2 ?? throw new ArgumentNullException(nameof(service2)); + } + + public Controller(IService service, string name) + { + _ = service ?? throw new ArgumentNullException(nameof(service)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + public string Name { get; } = ""; +} diff --git a/GeneratorTests/Moq.AutoMocker.Generator.Example/IService.cs b/GeneratorTests/Moq.AutoMocker.Generator.Example/IService.cs new file mode 100644 index 00000000..8cb15948 --- /dev/null +++ b/GeneratorTests/Moq.AutoMocker.Generator.Example/IService.cs @@ -0,0 +1,6 @@ +namespace Moq.AutoMock.Generator.Example; + +public interface IService +{ + +} diff --git a/GeneratorTests/Moq.AutoMocker.Generator.Example/Moq.AutoMock.Generator.Example.csproj b/GeneratorTests/Moq.AutoMocker.Generator.Example/Moq.AutoMock.Generator.Example.csproj new file mode 100644 index 00000000..268cbace --- /dev/null +++ b/GeneratorTests/Moq.AutoMocker.Generator.Example/Moq.AutoMock.Generator.Example.csproj @@ -0,0 +1,10 @@ + + + + net6.0 + enable + enable + false + + + diff --git a/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj b/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj index 8862e1c0..a2b541ba 100644 --- a/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj +++ b/Moq.AutoMock.Tests/Moq.AutoMock.Tests.csproj @@ -24,8 +24,8 @@ - - + + \ No newline at end of file diff --git a/Moq.AutoMock.sln b/Moq.AutoMock.sln index 2386be81..3216af2e 100644 --- a/Moq.AutoMock.sln +++ b/Moq.AutoMock.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml LICENSE.md = LICENSE.md README.md = README.md @@ -18,6 +19,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators", "Generators\Generators.csproj", "{98CADDA9-5D66-405A-8A59-90636A242869}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GeneratorTests", "GeneratorTests", "{1D10BDDE-2E03-42E4-AA7A-B12AF01E7FE8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.AutoMock.Generator.Example", "GeneratorTests\Moq.AutoMocker.Generator.Example\Moq.AutoMock.Generator.Example.csproj", "{F142399A-FAB8-4097-BC7B-0266200FA14D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.AutoMock.Generator.Example.MSTest", "GeneratorTests\Moq.AutoMock.Generator.Example.MSTest\Moq.AutoMock.Generator.Example.MSTest.csproj", "{BAFB5236-9C52-48D2-8CD5-E214652F38D3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.AutoMock.Generator.Example.NUnit", "GeneratorTests\Moq.AutoMock.Generator.Example.NUnit\Moq.AutoMock.Generator.Example.NUnit.csproj", "{0E545A4D-809E-47CA-AC16-5CB80CD231F6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.AutoMock.Generator.Example.xUnit", "GeneratorTests\Moq.AutoMock.Generator.Example.xUnit\Moq.AutoMock.Generator.Example.xUnit.csproj", "{586D1D6A-3EE2-4AB8-988A-CEB810EF7787}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.AutoMocker.TestGenerator", "Moq.AutoMocker.TestGenerator\Moq.AutoMocker.TestGenerator.csproj", "{710AF8FE-BB9E-4FE9-ABF7-0CE8BD212BB4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.AutoMocker.TestGenerator.Tests", "Moq.AutoMocker.TestGenerator.Tests\Moq.AutoMocker.TestGenerator.Tests.csproj", "{F3663663-0123-4DA3-9BDD-95C91AFA4596}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,10 +51,40 @@ Global {98CADDA9-5D66-405A-8A59-90636A242869}.Debug|Any CPU.Build.0 = Debug|Any CPU {98CADDA9-5D66-405A-8A59-90636A242869}.Release|Any CPU.ActiveCfg = Release|Any CPU {98CADDA9-5D66-405A-8A59-90636A242869}.Release|Any CPU.Build.0 = Release|Any CPU + {F142399A-FAB8-4097-BC7B-0266200FA14D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F142399A-FAB8-4097-BC7B-0266200FA14D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F142399A-FAB8-4097-BC7B-0266200FA14D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F142399A-FAB8-4097-BC7B-0266200FA14D}.Release|Any CPU.Build.0 = Release|Any CPU + {BAFB5236-9C52-48D2-8CD5-E214652F38D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAFB5236-9C52-48D2-8CD5-E214652F38D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAFB5236-9C52-48D2-8CD5-E214652F38D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAFB5236-9C52-48D2-8CD5-E214652F38D3}.Release|Any CPU.Build.0 = Release|Any CPU + {0E545A4D-809E-47CA-AC16-5CB80CD231F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E545A4D-809E-47CA-AC16-5CB80CD231F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E545A4D-809E-47CA-AC16-5CB80CD231F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E545A4D-809E-47CA-AC16-5CB80CD231F6}.Release|Any CPU.Build.0 = Release|Any CPU + {586D1D6A-3EE2-4AB8-988A-CEB810EF7787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {586D1D6A-3EE2-4AB8-988A-CEB810EF7787}.Debug|Any CPU.Build.0 = Debug|Any CPU + {586D1D6A-3EE2-4AB8-988A-CEB810EF7787}.Release|Any CPU.ActiveCfg = Release|Any CPU + {586D1D6A-3EE2-4AB8-988A-CEB810EF7787}.Release|Any CPU.Build.0 = Release|Any CPU + {710AF8FE-BB9E-4FE9-ABF7-0CE8BD212BB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {710AF8FE-BB9E-4FE9-ABF7-0CE8BD212BB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {710AF8FE-BB9E-4FE9-ABF7-0CE8BD212BB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {710AF8FE-BB9E-4FE9-ABF7-0CE8BD212BB4}.Release|Any CPU.Build.0 = Release|Any CPU + {F3663663-0123-4DA3-9BDD-95C91AFA4596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3663663-0123-4DA3-9BDD-95C91AFA4596}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3663663-0123-4DA3-9BDD-95C91AFA4596}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3663663-0123-4DA3-9BDD-95C91AFA4596}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F142399A-FAB8-4097-BC7B-0266200FA14D} = {1D10BDDE-2E03-42E4-AA7A-B12AF01E7FE8} + {BAFB5236-9C52-48D2-8CD5-E214652F38D3} = {1D10BDDE-2E03-42E4-AA7A-B12AF01E7FE8} + {0E545A4D-809E-47CA-AC16-5CB80CD231F6} = {1D10BDDE-2E03-42E4-AA7A-B12AF01E7FE8} + {586D1D6A-3EE2-4AB8-988A-CEB810EF7787} = {1D10BDDE-2E03-42E4-AA7A-B12AF01E7FE8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1A499C6A-1AB1-4A04-A133-BBCF4759DACA} EndGlobalSection diff --git a/Moq.AutoMock/AutoMocker.Verify.cs b/Moq.AutoMock/AutoMocker.Verify.cs index bc826643..767c7820 100644 --- a/Moq.AutoMock/AutoMocker.Verify.cs +++ b/Moq.AutoMock/AutoMocker.Verify.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace Moq.AutoMock; diff --git a/Moq.AutoMock/AutoMocker.cs b/Moq.AutoMock/AutoMocker.cs index 9025e874..0b84012f 100644 --- a/Moq.AutoMock/AutoMocker.cs +++ b/Moq.AutoMock/AutoMocker.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.ExceptionServices; @@ -262,10 +259,10 @@ public T CreateSelfMock(bool enablePrivate) where T : class? /// Sets the CallBase property on the created Mock. /// An instance with virtual and abstract members mocked public T CreateSelfMock( - bool enablePrivate = false, - MockBehavior? mockBehavior = null, - DefaultValue? defaultValue = null, - bool? callBase = null) + bool enablePrivate = false, + MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + bool? callBase = null) where T : class? { return BuildSelfMock(enablePrivate, mockBehavior ?? MockBehavior, defaultValue ?? DefaultValue, callBase ?? CallBase).Object; @@ -284,13 +281,13 @@ public T CreateSelfMock( /// An instance with virtual and abstract members mocked public TImplementation WithSelfMock( bool enablePrivate = false, - MockBehavior? mockBehavior = null, + MockBehavior? mockBehavior = null, DefaultValue? defaultValue = null, bool? callBase = null) where TImplementation : class, TService where TService : class { - Mock selfMock = BuildSelfMock(enablePrivate, + Mock selfMock = BuildSelfMock(enablePrivate, mockBehavior ?? MockBehavior, defaultValue ?? DefaultValue, callBase ?? CallBase); @@ -391,7 +388,7 @@ public object WithSelfMock( } - private Mock BuildSelfMock(bool enablePrivate, MockBehavior mockBehavior, DefaultValue defaultValue, bool callBase) + private Mock BuildSelfMock(bool enablePrivate, MockBehavior mockBehavior, DefaultValue defaultValue, bool callBase) where T : class? { var context = new ObjectGraphContext(enablePrivate); diff --git a/Moq.AutoMock/CastChecker.cs b/Moq.AutoMock/CastChecker.cs index b16b67d5..23adf513 100644 --- a/Moq.AutoMock/CastChecker.cs +++ b/Moq.AutoMock/CastChecker.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; namespace Moq.AutoMock; diff --git a/Moq.AutoMock/ConstructorSelector.cs b/Moq.AutoMock/ConstructorSelector.cs index 02bce0a8..a05e798d 100644 --- a/Moq.AutoMock/ConstructorSelector.cs +++ b/Moq.AutoMock/ConstructorSelector.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Moq.AutoMock.Extensions; diff --git a/Moq.AutoMock/ConstructorTestsAttribute.cs b/Moq.AutoMock/ConstructorTestsAttribute.cs new file mode 100644 index 00000000..bc4fa595 --- /dev/null +++ b/Moq.AutoMock/ConstructorTestsAttribute.cs @@ -0,0 +1,26 @@ +namespace Moq.AutoMock; + +/// +/// An attribute used by Moq.AutoMock.TestGenerator to generate unit tests for null constructor arguments. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public class ConstructorTestsAttribute : Attribute +{ + /// + /// The type of service to generate tests for. + /// + public Type? TargetType { get; set; } + /// + /// Create a new instance of the ConstructorTestsAttribute + /// + public ConstructorTestsAttribute() + { } + /// + /// Create a new instance of the ConstructorTestsAttribute specifying the targetType + /// + /// + public ConstructorTestsAttribute(Type targetType) + { + TargetType = targetType; + } +} diff --git a/Moq.AutoMock/IAutoMockerDisposable.cs b/Moq.AutoMock/IAutoMockerDisposable.cs index 4ac33115..a7bf203d 100644 --- a/Moq.AutoMock/IAutoMockerDisposable.cs +++ b/Moq.AutoMock/IAutoMockerDisposable.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; - -namespace Moq.AutoMock; +namespace Moq.AutoMock; /// /// An interface that is used to clean up AutoMocker instances. diff --git a/Moq.AutoMock/MockArrayInstance.cs b/Moq.AutoMock/MockArrayInstance.cs index f0f9010a..162fa0fe 100644 --- a/Moq.AutoMock/MockArrayInstance.cs +++ b/Moq.AutoMock/MockArrayInstance.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Moq.AutoMock; +namespace Moq.AutoMock; internal sealed class MockArrayInstance : IInstance { diff --git a/Moq.AutoMock/MockExtensions.cs b/Moq.AutoMock/MockExtensions.cs index f4351441..de3e920e 100644 --- a/Moq.AutoMock/MockExtensions.cs +++ b/Moq.AutoMock/MockExtensions.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; using Moq.Language.Flow; diff --git a/Moq.AutoMock/Moq.AutoMock.csproj b/Moq.AutoMock/Moq.AutoMock.csproj index 2303f320..e58fd50a 100644 --- a/Moq.AutoMock/Moq.AutoMock.csproj +++ b/Moq.AutoMock/Moq.AutoMock.csproj @@ -24,6 +24,7 @@ snupkg true + $(NoWarn);AMG0002 @@ -36,6 +37,7 @@ Analyzer false + @@ -45,6 +47,7 @@ + diff --git a/Moq.AutoMock/ObjectGraphContext.cs b/Moq.AutoMock/ObjectGraphContext.cs index de19fc41..d55b4eff 100644 --- a/Moq.AutoMock/ObjectGraphContext.cs +++ b/Moq.AutoMock/ObjectGraphContext.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Reflection; namespace Moq.AutoMock; diff --git a/Moq.AutoMock/Resolvers/ArrayResolver.cs b/Moq.AutoMock/Resolvers/ArrayResolver.cs index 045af009..e0890ed5 100644 --- a/Moq.AutoMock/Resolvers/ArrayResolver.cs +++ b/Moq.AutoMock/Resolvers/ArrayResolver.cs @@ -1,6 +1,4 @@ -using System; - -namespace Moq.AutoMock.Resolvers; +namespace Moq.AutoMock.Resolvers; /// /// Provides a means to create arrays. diff --git a/Moq.AutoMock/Resolvers/CacheResolver.cs b/Moq.AutoMock/Resolvers/CacheResolver.cs index 1ed1f08b..f05ac5b6 100644 --- a/Moq.AutoMock/Resolvers/CacheResolver.cs +++ b/Moq.AutoMock/Resolvers/CacheResolver.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Moq.AutoMock.Resolvers; +namespace Moq.AutoMock.Resolvers; /// /// Provides the cache used by AutoMocker. diff --git a/Moq.AutoMock/Resolvers/EnumerableResolver.cs b/Moq.AutoMock/Resolvers/EnumerableResolver.cs index 2c174766..e81d96ec 100644 --- a/Moq.AutoMock/Resolvers/EnumerableResolver.cs +++ b/Moq.AutoMock/Resolvers/EnumerableResolver.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using System.Reflection; namespace Moq.AutoMock.Resolvers; diff --git a/Moq.AutoMock/Resolvers/FuncResolver.cs b/Moq.AutoMock/Resolvers/FuncResolver.cs index 3751258e..5e285d6d 100644 --- a/Moq.AutoMock/Resolvers/FuncResolver.cs +++ b/Moq.AutoMock/Resolvers/FuncResolver.cs @@ -1,5 +1,4 @@ -using System; -using Moq.AutoMock.Extensions; +using Moq.AutoMock.Extensions; namespace Moq.AutoMock.Resolvers; diff --git a/Moq.AutoMock/Resolvers/LazyResolver.cs b/Moq.AutoMock/Resolvers/LazyResolver.cs index f4bc8085..630f541c 100644 --- a/Moq.AutoMock/Resolvers/LazyResolver.cs +++ b/Moq.AutoMock/Resolvers/LazyResolver.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using System.Reflection; +using System.Reflection; using Moq.AutoMock.Extensions; namespace Moq.AutoMock.Resolvers; diff --git a/Moq.AutoMock/Resolvers/MockResolutionContext.cs b/Moq.AutoMock/Resolvers/MockResolutionContext.cs index 04171afc..3f002813 100644 --- a/Moq.AutoMock/Resolvers/MockResolutionContext.cs +++ b/Moq.AutoMock/Resolvers/MockResolutionContext.cs @@ -1,6 +1,4 @@ -using System; - -namespace Moq.AutoMock.Resolvers; +namespace Moq.AutoMock.Resolvers; /// /// The context used to resolve types from an AutoMocker instance. diff --git a/Moq.AutoMocker.TestGenerator.Tests/CSharpSourceGeneratorVerifier.cs b/Moq.AutoMocker.TestGenerator.Tests/CSharpSourceGeneratorVerifier.cs new file mode 100644 index 00000000..cd2f9b0f --- /dev/null +++ b/Moq.AutoMocker.TestGenerator.Tests/CSharpSourceGeneratorVerifier.cs @@ -0,0 +1,50 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace Moq.AutoMocker.TestGenerator.Tests; + +public static class CSharpSourceGeneratorVerifier + where TSourceGenerator : ISourceGenerator, new() +{ + public class Test : CSharpSourceGeneratorTest + { + public bool ReferenceAutoMocker { get; set; } = true; + + protected override Project ApplyCompilationOptions(Project project) + { + if (ReferenceAutoMocker) + { + string fullPath = Path.GetFullPath($"{AutoMock.AssemblyName}.dll"); + project = project.AddMetadataReference(MetadataReference.CreateFromFile(fullPath)); + } + return base.ApplyCompilationOptions(project); + } + + protected override CompilationOptions CreateCompilationOptions() + { + var compilationOptions = base.CreateCompilationOptions(); + return compilationOptions.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); + } + + public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + return nullableWarnings; + } + + protected override ParseOptions CreateParseOptions() + { + return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); + } + } +} diff --git a/Moq.AutoMocker.TestGenerator.Tests/Moq.AutoMocker.TestGenerator.Tests.csproj b/Moq.AutoMocker.TestGenerator.Tests/Moq.AutoMocker.TestGenerator.Tests.csproj new file mode 100644 index 00000000..7967cf31 --- /dev/null +++ b/Moq.AutoMocker.TestGenerator.Tests/Moq.AutoMocker.TestGenerator.Tests.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + + false + true + true + + + + + + + + + + + + + + + + diff --git a/Moq.AutoMocker.TestGenerator.Tests/UnitTests.cs b/Moq.AutoMocker.TestGenerator.Tests/UnitTests.cs new file mode 100644 index 00000000..68d52fdc --- /dev/null +++ b/Moq.AutoMocker.TestGenerator.Tests/UnitTests.cs @@ -0,0 +1,94 @@ +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Text; +using System.Threading.Tasks; + +using VerifyCS = Moq.AutoMocker.TestGenerator.Tests.CSharpSourceGeneratorVerifier; + +namespace Moq.AutoMocker.TestGenerator.Tests; + +[TestClass] +public class TestGeneratorTests +{ + [TestMethod] + public async Task Generation_WithProjectThatDoesNotReferenceAutoMocker_ProducesDiagnosticWarning() + { + var expectedResult = + DiagnosticResult.CompilerWarning(Diagnostics.MustReferenceAutoMock.DiagnosticId); + await new VerifyCS.Test + { + ReferenceAutoMocker = false, + ExpectedDiagnostics = + { + expectedResult + } + }.RunAsync(); + } + + [TestMethod] + public async Task Generation_WithDecoratedNonPartialClass_ProducesDiagnosticError() + { + var code = @" +using Moq.AutoMock; + +namespace TestNamespace; + +[ConstructorTests(TargetType = typeof(Controller))] +public class ControllerTests +{ + +} + +public class Controller { } +"; + var expectedResult = + DiagnosticResult.CompilerError(Diagnostics.TestClassesMustBePartial.DiagnosticId) + .WithSpan(6, 1, 10, 2) + .WithArguments("TestNamespace.ControllerTests"); + await new VerifyCS.Test + { + TestState = + { + Sources = { code }, + }, + ExpectedDiagnostics = + { + expectedResult + } + }.RunAsync(); + } + + [TestMethod] + public async Task Generation_WithNoTargetTypeSpecified_ProducesDiagnosticError() + { + var code = @" +using Moq.AutoMock; + +namespace TestNamespace; + +[ConstructorTests] +public class ControllerTests +{ + +} + +public class Controller { } +"; + var expectedResult = + DiagnosticResult.CompilerError(Diagnostics.MustSpecifyTargetType.DiagnosticId) + .WithSpan(6, 2, 6, 18) + .WithArguments("TestNamespace.ControllerTests"); + await new VerifyCS.Test + { + TestState = + { + Sources = { code }, + }, + ExpectedDiagnostics = + { + expectedResult + } + }.RunAsync(); + } +} diff --git a/Moq.AutoMocker.TestGenerator/AnalyzerReleases.Shipped.md b/Moq.AutoMocker.TestGenerator/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/AnalyzerReleases.Shipped.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Moq.AutoMocker.TestGenerator/AnalyzerReleases.Unshipped.md b/Moq.AutoMocker.TestGenerator/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..f7f5d6ce --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,7 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +AMG0001 | AutoMocker.TestGenerator | Error | TestClassesMustBePartial +AMG0002 | AutoMocker.TestGenerator | Warning | MustReferenceAutoMock +AMG0003 | AutoMocker.TestGenerator | Error | MustSpecifyTargetType \ No newline at end of file diff --git a/Moq.AutoMocker.TestGenerator/AutoMock.cs b/Moq.AutoMocker.TestGenerator/AutoMock.cs new file mode 100644 index 00000000..c3170c81 --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/AutoMock.cs @@ -0,0 +1,14 @@ +namespace Moq.AutoMocker.TestGenerator; + +public static class AutoMock +{ + public const string ConstructorTestsAttribute = "ConstructorTestsAttribute"; + + public const string TargetTypePropertyName = "TargetType"; + + + public const string AssemblyName = "Moq.AutoMock"; + + public const string NuGetPackageName = "Moq.AutoMock"; + +} diff --git a/Moq.AutoMocker.TestGenerator/Diagnostics.cs b/Moq.AutoMocker.TestGenerator/Diagnostics.cs new file mode 100644 index 00000000..675bf1db --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/Diagnostics.cs @@ -0,0 +1,55 @@ +using Microsoft.CodeAnalysis; + +namespace Moq.AutoMocker.TestGenerator; + +public static class Diagnostics +{ + private const string Category = "AutoMocker.TestGenerator"; + public static class TestClassesMustBePartial + { + public const string DiagnosticId = "AMG0001"; + private const string Title = "Test class must be partial"; + private const string MessageFormat = $"Class {{0}} is decorated with {AutoMock.ConstructorTestsAttribute} attribute but is not declared as a partial class"; + private const string Description = "To generate constructor tests, the test class must be declared as partial. The tests are then generated into the partial class."; + + //NB: Do not make a property or use target-typed new expression + //https://github.com/dotnet/roslyn-analyzers/issues/5890?msclkid=db74545bc13811ecac296aa1a3a09b53 + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, + Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public static Diagnostic Create(Location? location, string className) + => Diagnostic.Create(Rule, location, className); + } + + public static class MustReferenceAutoMock + { + public const string DiagnosticId = "AMG0002"; + private const string Title = $"Test projects must reference {AutoMock.AssemblyName}"; + private const string MessageFormat = $"To use Moq.AutoMocker.TestGenerator, your test project must also reference {AutoMock.AssemblyName}"; + private const string Description = $"Add a reference to the {AutoMock.AssemblyName} assembly or reference the {AutoMock.NuGetPackageName} NuGet package by running \"Install-Package Moq.AutoMock\"."; + + //NB: Do not make a property or use target-typed new expression + //https://github.com/dotnet/roslyn-analyzers/issues/5890?msclkid=db74545bc13811ecac296aa1a3a09b53 + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, + Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + + public static Diagnostic Create() + => Diagnostic.Create(Rule, null); + } + + public static class MustSpecifyTargetType + { + public const string DiagnosticId = "AMG0003"; + private const string Title = $"{AutoMock.ConstructorTestsAttribute} must specify a {AutoMock.TargetTypePropertyName}"; + private const string MessageFormat = $"Class {{0}} is decorated with {AutoMock.ConstructorTestsAttribute} attribute but it does not specify a {AutoMock.TargetTypePropertyName}. Set this property to the type you want to generate tests for: [Moq.AutoMock.ConstructorTests(TargetType = typeof(TypeToGenerateTestsFor))]."; + private const string Description = $"Add the {AutoMock.TargetTypePropertyName} to the {AutoMock.ConstructorTestsAttribute} attribute."; + + //NB: Do not make a property or use target-typed new expression + //https://github.com/dotnet/roslyn-analyzers/issues/5890?msclkid=db74545bc13811ecac296aa1a3a09b53 + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, + Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); + + public static Diagnostic Create(Location? location, string className) + => Diagnostic.Create(Rule, location, className); + } +} diff --git a/Moq.AutoMocker.TestGenerator/GeneratorTargetClass.cs b/Moq.AutoMocker.TestGenerator/GeneratorTargetClass.cs new file mode 100644 index 00000000..e73a09c1 --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/GeneratorTargetClass.cs @@ -0,0 +1,40 @@ +using Microsoft.CodeAnalysis; + +namespace Moq.AutoMocker.TestGenerator; + +public class GeneratorTargetClass +{ + public string? Namespace { get; set; } + public string? TestClassName { get; set; } + + public SutClass? Sut { get; set; } +} + + +public class SutClass +{ + public string? Name { get; set; } + public string? FullName { get; set; } + + public List NullConstructorParameterTests { get; } = new(); +} + +public class NullConstructorParameterTest +{ + public List? Parameters { get; set; } + public int NullParameterIndex { get; set; } + public string? NullTypeName { get; set; } + public string? NullParameterName => Parameters?[NullParameterIndex].Name; +} + +public class Parameter +{ + public Parameter(IParameterSymbol symbol) + { + Symbol = symbol; + } + private IParameterSymbol Symbol { get; } + + public string Name => Symbol.Name; + public string ParameterType => Symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); +} diff --git a/Moq.AutoMocker.TestGenerator/Moq.AutoMocker.TestGenerator.csproj b/Moq.AutoMocker.TestGenerator/Moq.AutoMocker.TestGenerator.csproj new file mode 100644 index 00000000..03ddeb65 --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/Moq.AutoMocker.TestGenerator.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + false + + + + + + + + + + + diff --git a/Moq.AutoMocker.TestGenerator/SyntaxReceiver.cs b/Moq.AutoMocker.TestGenerator/SyntaxReceiver.cs new file mode 100644 index 00000000..0cad2bba --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/SyntaxReceiver.cs @@ -0,0 +1,89 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Moq.AutoMocker.TestGenerator; + +public class SyntaxReceiver : ISyntaxContextReceiver +{ + public List TestClasses { get; } = new(); + + public List DiagnosticMessages { get; } = new(); + + private TypeSyntax? GetTargetType(AttributeSyntax attributeSyntax) + { + return attributeSyntax.ArgumentList?.Arguments.Count > 0 && + attributeSyntax.ArgumentList.Arguments + .Select(x => x.Expression) + .OfType() + .FirstOrDefault() is { } typeExpression + ? typeExpression.Type + : null; + } + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is ClassDeclarationSyntax classDeclaration && + context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol symbol && + classDeclaration.AttributeLists.SelectMany(x => x.Attributes) + .Select(a => + { + if (context.SemanticModel.GetTypeInfo(a).Type?.Name == AutoMock.ConstructorTestsAttribute) + { + if (GetTargetType(a) is { } targetType && + context.SemanticModel.GetTypeInfo(targetType).Type is INamedTypeSymbol sutType) + { + return sutType; + } + Diagnostic diagnostic = Diagnostics.MustSpecifyTargetType.Create(a.GetLocation(), + symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + DiagnosticMessages.Add(diagnostic); + } + return null; + }) + .FirstOrDefault(a => a is not null) is { } sutType + ) + { + if (!classDeclaration.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword))) + { + Diagnostic diagnostic = Diagnostics.TestClassesMustBePartial.Create(classDeclaration.GetLocation(), + symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + DiagnosticMessages.Add(diagnostic); + return; + } + string testClassName = symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + string namespaceDeclaration = symbol.ContainingNamespace.ToDisplayString(); + + SutClass sut = new() + { + Name = sutType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + FullName = sutType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + }; + + foreach (IMethodSymbol ctor in sutType.Constructors) + { + var parameters = ctor.Parameters.Select(x => new Parameter(x)).ToList(); + int nullIndex = 0; + foreach (IParameterSymbol parameter in ctor.Parameters) + { + sut.NullConstructorParameterTests.Add(new NullConstructorParameterTest() + { + Parameters = parameters, + NullParameterIndex = nullIndex, + NullTypeName = parameter.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) + }); + nullIndex++; + } + } + + GeneratorTargetClass targetClass = new() + { + Namespace = namespaceDeclaration, + TestClassName = testClassName, + Sut = sut + }; + + TestClasses.Add(targetClass); + } + } +} diff --git a/Moq.AutoMocker.TestGenerator/UnitTestSourceGenerator.cs b/Moq.AutoMocker.TestGenerator/UnitTestSourceGenerator.cs new file mode 100644 index 00000000..a506e74d --- /dev/null +++ b/Moq.AutoMocker.TestGenerator/UnitTestSourceGenerator.cs @@ -0,0 +1,164 @@ +using System.Text; +using Microsoft.CodeAnalysis; + +namespace Moq.AutoMocker.TestGenerator; + +[Generator] +public class UnitTestSourceGenerator : ISourceGenerator +{ + public void Execute(GeneratorExecutionContext context) + { + if (context.Compilation.Language is not LanguageNames.CSharp) return; + + if (!context.Compilation.ReferencedAssemblyNames.Any(ai => ai.Name.Equals(AutoMock.AssemblyName, StringComparison.OrdinalIgnoreCase))) + { + context.ReportDiagnostic(Diagnostics.MustReferenceAutoMock.Create()); + return; + } + + SyntaxReceiver rx = (SyntaxReceiver)context.SyntaxContextReceiver!; + + foreach (Diagnostic diagnostic in rx.DiagnosticMessages) + { + context.ReportDiagnostic(diagnostic); + } + + var testingFramework = GetTestingFramework(context.Compilation.ReferencedAssemblyNames); + + foreach (GeneratorTargetClass testClass in rx.TestClasses) + { + StringBuilder builder = new(); + + builder.AppendLine($"namespace {testClass.Namespace}"); + builder.AppendLine("{"); + + builder.AppendLine($" partial class {testClass.TestClassName}"); + builder.AppendLine(" {"); + builder.AppendLine($" partial void AutoMockerTestSetup(Moq.AutoMock.AutoMocker mocker, string testName);"); + builder.AppendLine(); + + HashSet testNames = new(); + + foreach (var test in testClass.Sut?.NullConstructorParameterTests ?? Enumerable.Empty()) + { + string testName; + int testNameIndex = 0; + for (testName = $"{testClass.Sut!.Name}Constructor_WithNull{test.NullTypeName}_ThrowsArgumentNullException"; + !testNames.Add(testName); + testName = $"{testClass.Sut!.Name}Constructor_WithNull{test.NullTypeName}{++testNameIndex}_ThrowsArgumentNullException") + { } + + builder.AppendLine($" partial void {testName}Setup(Moq.AutoMock.AutoMocker mocker);"); + builder.AppendLine(); + + switch (testingFramework) + { + case TargetTestingFramework.MSTest: + builder.AppendLine(" [global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]"); + break; + case TargetTestingFramework.Xunit: + builder.AppendLine(" [global::Xunit.Fact]"); + break; + case TargetTestingFramework.NUnit: + builder.AppendLine(" [global::NUnit.Framework.Test]"); + break; + } + + + builder.AppendLine($" public void {testName}()"); + builder.AppendLine(" {"); + builder.AppendLine(" Moq.AutoMock.AutoMocker mocker = new Moq.AutoMock.AutoMocker();"); + builder.AppendLine($" AutoMockerTestSetup(mocker, \"{testName}\");"); + builder.AppendLine($" {testName}Setup(mocker);"); + + for (int i = 0; i < test.Parameters?.Count; i++) + { + if (i == test.NullParameterIndex) continue; + Parameter parameter = test.Parameters[i]; + builder.AppendLine($" var {parameter.Name} = mocker.Get<{parameter.ParameterType}>();"); + } + + string constructorInvocation = $"_ = new {testClass.Sut.FullName}({string.Join(",", GetParameterNames(test))})"; + + switch (testingFramework) + { + case TargetTestingFramework.MSTest: + builder.AppendLine($" System.ArgumentNullException ex = global::Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ThrowsException(() => {constructorInvocation});"); + builder.AppendLine($" global::Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(\"{test.NullParameterName}\", ex.ParamName);"); + break; + case TargetTestingFramework.Xunit: + builder.AppendLine($" System.ArgumentNullException ex = global::Xunit.Assert.Throws(() => {constructorInvocation});"); + builder.AppendLine($" global::Xunit.Assert.Equal(\"{test.NullParameterName}\", ex.ParamName);"); + break; + case TargetTestingFramework.NUnit: + builder.AppendLine($" System.ArgumentNullException ex = global::NUnit.Framework.Assert.Throws(() => {constructorInvocation});"); + builder.AppendLine($" global::NUnit.Framework.Assert.AreEqual(\"{test.NullParameterName}\", ex.ParamName);"); + break; + } + + builder.AppendLine(" }"); + builder.AppendLine(); + } + + builder.AppendLine(" }"); + builder.AppendLine("}"); + + context.AddSource($"{testClass.TestClassName}.g.cs", builder.ToString()); + + } + + static IEnumerable GetParameterNames(NullConstructorParameterTest test) + { + for (int i = 0; i < test.Parameters?.Count; i++) + { + if (i == test.NullParameterIndex) + { + yield return $"default({test.Parameters[i].ParameterType})"; + } + else + { + yield return test.Parameters[i].Name; + } + } + } + } + + public void Initialize(GeneratorInitializationContext context) + { +#if DEBUG + if (!System.Diagnostics.Debugger.IsAttached) + { + //System.Diagnostics.Debugger.Launch(); + } +#endif + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + private static TargetTestingFramework GetTestingFramework(IEnumerable assemblies) + { + foreach (AssemblyIdentity assembly in assemblies) + { + if (assembly.Name.StartsWith("Microsoft.VisualStudio.TestPlatform.TestFramework")) + { + return TargetTestingFramework.MSTest; + } + if (assembly.Name.StartsWith("nunit.")) + { + return TargetTestingFramework.NUnit; + } + if (assembly.Name.StartsWith("xunit.")) + { + return TargetTestingFramework.Xunit; + } + } + return TargetTestingFramework.Unknown; + } +} + +public enum TargetTestingFramework +{ + Unknown, + MSTest, + Xunit, + NUnit +}