diff --git a/Build/Build.cs b/Build/Build.cs index 756dcc2051..dd66fc1a1f 100644 --- a/Build/Build.cs +++ b/Build/Build.cs @@ -50,12 +50,15 @@ class Build : NukeBuild AbsolutePath ArtifactsDirectory => RootDirectory / "Artifacts"; + AbsolutePath TestResultsDirectory => RootDirectory / "TestResults"; + string SemVer; Target Clean => _ => _ .Executes(() => { EnsureCleanDirectory(ArtifactsDirectory); + EnsureCleanDirectory(TestResultsDirectory); }); Target CalculateNugetVersion => _ => _ @@ -140,7 +143,10 @@ class Build : NukeBuild .SetConfiguration("Debug") .EnableNoBuild() .SetDataCollector("XPlat Code Coverage") - .SetResultsDirectory(RootDirectory / "TestResults") + .SetResultsDirectory(TestResultsDirectory) + .AddRunSetting( + "DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.DoesNotReturnAttribute", + "DoesNotReturnAttribute") .CombineWith( projects, (_, project) => _ @@ -160,14 +166,15 @@ class Build : NukeBuild { ReportGenerator(s => s .SetProcessToolPath(ToolPathResolver.GetPackageExecutable("ReportGenerator", "ReportGenerator.dll", framework: "net6.0")) - .SetTargetDirectory(RootDirectory / "TestResults" / "reports") - .AddReports(RootDirectory / "TestResults/**/coverage.cobertura.xml") + .SetTargetDirectory(TestResultsDirectory / "reports") + .AddReports(TestResultsDirectory / "**/coverage.cobertura.xml") .AddReportTypes("HtmlInline_AzurePipelines_Dark", "lcov") - .SetClassFilters("-System.Diagnostics.CodeAnalysis.StringSyntaxAttribute") + .SetClassFilters( + "-System.Diagnostics.CodeAnalysis.StringSyntaxAttribute", + "-System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute") .SetAssemblyFilters("+FluentAssertions")); - string link = RootDirectory / "TestResults" / "reports" / "index.html"; - + string link = TestResultsDirectory / "reports" / "index.html"; Serilog.Log.Information($"Code coverage report: \x1b]8;;file://{link.Replace('\\', '/')}\x1b\\{link}\x1b]8;;\x1b\\"); }); @@ -192,7 +199,10 @@ class Build : NukeBuild .SetConfiguration("Debug") .EnableNoBuild() .SetDataCollector("XPlat Code Coverage") - .SetResultsDirectory(RootDirectory / "TestResults") + .SetResultsDirectory(TestResultsDirectory) + .AddRunSetting( + "DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.DoesNotReturnAttribute", + "DoesNotReturnAttribute") .CombineWith( testCombinations, (_, v) => _.SetProjectFile(v.project).SetFramework(v.framework))); diff --git a/Src/FluentAssertions/AssertionExtensions.cs b/Src/FluentAssertions/AssertionExtensions.cs index b3c0bf3ef5..4375b958c3 100644 --- a/Src/FluentAssertions/AssertionExtensions.cs +++ b/Src/FluentAssertions/AssertionExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq.Expressions; using System.Net.Http; @@ -999,6 +1000,7 @@ public static void Should(this TypeSelectorAssertions _) InvalidShouldCall(); } + [DoesNotReturn] private static void InvalidShouldCall() { throw new InvalidOperationException("You are asserting the 'AndConstraint' itself. Remove the 'Should()' method directly following 'And'."); diff --git a/Src/FluentAssertions/DoesNotReturnAttribute.cs b/Src/FluentAssertions/DoesNotReturnAttribute.cs new file mode 100644 index 0000000000..1a0daac542 --- /dev/null +++ b/Src/FluentAssertions/DoesNotReturnAttribute.cs @@ -0,0 +1,18 @@ +// copied from https://source.dot.net/#System.Private.CoreLib/NullableAttributes.cs +#if !(NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER) +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +namespace System.Diagnostics.CodeAnalysis +{ + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DoesNotReturnAttribute : Attribute + { + } +} +#endif diff --git a/Src/FluentAssertions/Execution/FallbackTestFramework.cs b/Src/FluentAssertions/Execution/FallbackTestFramework.cs index 1abd625713..34232ad62d 100644 --- a/Src/FluentAssertions/Execution/FallbackTestFramework.cs +++ b/Src/FluentAssertions/Execution/FallbackTestFramework.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace FluentAssertions.Execution { /// @@ -13,6 +15,7 @@ internal class FallbackTestFramework : ITestFramework /// /// Throws a framework-specific exception to indicate a failing unit test. /// + [DoesNotReturn] public void Throw(string message) { throw new AssertionFailedException(message); diff --git a/Src/FluentAssertions/Execution/ITestFramework.cs b/Src/FluentAssertions/Execution/ITestFramework.cs index 694dba015a..321df438ca 100644 --- a/Src/FluentAssertions/Execution/ITestFramework.cs +++ b/Src/FluentAssertions/Execution/ITestFramework.cs @@ -1,4 +1,6 @@ -namespace FluentAssertions.Execution +using System.Diagnostics.CodeAnalysis; + +namespace FluentAssertions.Execution { /// /// Represents an abstraction of a particular test framework such as MSTest, nUnit, etc. @@ -13,6 +15,7 @@ internal interface ITestFramework /// /// Throws a framework-specific exception to indicate a failing unit test. /// + [DoesNotReturn] void Throw(string message); } } diff --git a/Src/FluentAssertions/Execution/LateBoundTestFramework.cs b/Src/FluentAssertions/Execution/LateBoundTestFramework.cs index 6106e59269..7c5037ef65 100644 --- a/Src/FluentAssertions/Execution/LateBoundTestFramework.cs +++ b/Src/FluentAssertions/Execution/LateBoundTestFramework.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -8,6 +9,7 @@ internal abstract class LateBoundTestFramework : ITestFramework { private Assembly assembly; + [DoesNotReturn] public void Throw(string message) { Type exceptionType = assembly.GetType(ExceptionFullName); diff --git a/Src/FluentAssertions/Execution/NSpecFramework.cs b/Src/FluentAssertions/Execution/NSpecFramework.cs index dd1ec4a4d3..84d69e2d95 100644 --- a/Src/FluentAssertions/Execution/NSpecFramework.cs +++ b/Src/FluentAssertions/Execution/NSpecFramework.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -27,6 +28,7 @@ public bool IsAvailable } } + [DoesNotReturn] public void Throw(string message) { Type exceptionType = assembly.GetType("NSpec.Domain.AssertionException"); diff --git a/Src/FluentAssertions/Execution/TestFrameworkProvider.cs b/Src/FluentAssertions/Execution/TestFrameworkProvider.cs index 621f63e05a..730b4accb6 100644 --- a/Src/FluentAssertions/Execution/TestFrameworkProvider.cs +++ b/Src/FluentAssertions/Execution/TestFrameworkProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using FluentAssertions.Common; @@ -22,6 +23,7 @@ internal static class TestFrameworkProvider #endregion + [DoesNotReturn] public static void Throw(string message) { if (testFramework is null) diff --git a/Src/FluentAssertions/Execution/XUnit2TestFramework.cs b/Src/FluentAssertions/Execution/XUnit2TestFramework.cs index eaa3ad4670..df18b93e88 100644 --- a/Src/FluentAssertions/Execution/XUnit2TestFramework.cs +++ b/Src/FluentAssertions/Execution/XUnit2TestFramework.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace FluentAssertions.Execution @@ -25,6 +26,7 @@ public bool IsAvailable } } + [DoesNotReturn] public void Throw(string message) { Type exceptionType = assembly.GetType("Xunit.Sdk.XunitException"); diff --git a/Tests/FluentAssertions.Specs/AssertionExtensionsSpecs.cs b/Tests/FluentAssertions.Specs/AssertionExtensionsSpecs.cs index e4d55a2028..1116280c94 100644 --- a/Tests/FluentAssertions.Specs/AssertionExtensionsSpecs.cs +++ b/Tests/FluentAssertions.Specs/AssertionExtensionsSpecs.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using FluentAssertions.Numeric; +using FluentAssertions.Primitives; +using FluentAssertions.Specialized; using FluentAssertions.Types; using Xunit; @@ -32,6 +35,49 @@ private static bool OverridesEquals(Type t) return equals is not null; } + [Theory] + [InlineData(typeof(ReferenceTypeAssertions))] + [InlineData(typeof(BooleanAssertions))] + [InlineData(typeof(DateTimeAssertions))] + [InlineData(typeof(DateTimeOffsetAssertions))] +#if NET6_0_OR_GREATER + [InlineData(typeof(DateOnlyAssertions))] +#endif + [InlineData(typeof(ExecutionTimeAssertions))] + [InlineData(typeof(GuidAssertions))] + [InlineData(typeof(MethodInfoSelectorAssertions))] + [InlineData(typeof(NumericAssertions>))] + [InlineData(typeof(PropertyInfoSelectorAssertions))] + [InlineData(typeof(SimpleTimeSpanAssertions))] + [InlineData(typeof(TaskCompletionSourceAssertions))] + [InlineData(typeof(TypeSelectorAssertions))] + [InlineData(typeof(EnumAssertions>))] + public void Fake_should_method_throws(Type type) + { + // Arrange + MethodInfo fakeOverload = AllTypes.From(typeof(FluentAssertions.AssertionExtensions).Assembly) + .ThatAreClasses() + .ThatAreStatic() + .Where(t => t.IsPublic) + .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public)) + .Single(m => m.Name == "Should" && IsGuardOverload(m) + && m.GetParameters().Single().ParameterType.Name == type.Name); + + if (type.IsConstructedGenericType) + { + fakeOverload = fakeOverload.MakeGenericMethod(type.GenericTypeArguments); + } + + // Act + Action act = () => fakeOverload.Invoke(null, new object[] { null }); + + // Assert + act.Should() + .ThrowExactly() + .WithInnerExceptionExactly() + .WithMessage("You are asserting the 'AndConstraint' itself. Remove the 'Should()' method directly following 'And'."); + } + [Fact] public void Should_methods_have_a_matching_overload_to_guard_against_chaining_and_constraints() { @@ -58,7 +104,8 @@ public void Should_methods_have_a_matching_overload_to_guard_against_chaining_an // Assert fakeOverloads.Should().BeEquivalentTo(realOverloads, opt => opt .Using(ctx => ctx.Subject.Name.Should().Be(ctx.Expectation.Name)) - .WhenTypeIs()); + .WhenTypeIs(), + "AssertionExtensions.cs should have a guard overload of Should calling InvalidShouldCall()"); } private static bool IsGuardOverload(MethodInfo m) =>