Skip to content

Commit

Permalink
feat(vnext): add Testify.Assertions project
Browse files Browse the repository at this point in the history
Add the Testify.Assertions project and the associated
Testify.Assertions.Tests test project to the solution. Implement
enough to prove the new design.
  • Loading branch information
wekempf committed Oct 1, 2023
1 parent f35e8bb commit 8ed3872
Show file tree
Hide file tree
Showing 60 changed files with 4,048 additions and 70 deletions.
388 changes: 349 additions & 39 deletions .editorconfig

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions .editorconfig.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# EditorConfig is awesome:
http://EditorConfig.org
root = true

[*]
indent_style = space
# (Please don't specify an indent_size here; that has too many unintended consequences.)

[*.cs,*.csx,*.vb,*.vbx]
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120
spaces_around_operators = true
curly_bracket_next_line = true
spaces_around_brackets = both
indent_brace_style = Allman
indent_size = 4

[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2

[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
indent_size = 2

[*.json]
indent_size = 2

[*.{cs, vb}]
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:error
dotnet_style_predefined_type_for_member_access = true:error
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion

[*.cs]
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_style_expression_bodied_methods = true:suggestion
csharp_style_expression_bodied_constructors = true:suggestion
csharp_style_expression_bodied_operators = true:suggestion
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
30 changes: 28 additions & 2 deletions Testify.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2036
# Visual Studio Version 17
VisualStudioVersion = 17.7.34024.191
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{469F900E-5D25-4CD0-A0CC-A255131F2D8F}"
ProjectSection(SolutionItems) = preProject
Expand Down Expand Up @@ -44,6 +44,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testify.Moq.Tests", "src\Te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testify.Examples", "src\Examples\Testify.Examples\Testify.Examples.csproj", "{D0F601A2-DB9F-4BFE-88FE-8035D5130980}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5F33DA26-5155-4499-A990-5042CB725506}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testify.Assertions", "src\Testify.Assertions\Testify.Assertions.csproj", "{5D45EAD5-5694-44A9-9BC5-E8D4EC223C72}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{38F9FCA7-EB52-400C-B9B9-8376AAB80C0F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testify.Assertions.Tests", "src\tests\Testify.Assertions.Tests\Testify.Assertions.Tests.csproj", "{AC504586-CB6A-4355-84D4-D18BD2B76838}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testify.xUnit.Traits", "src\Testify.xUnit.Traits\Testify.xUnit.Traits.csproj", "{E08C2037-1FF1-486D-BA93-956605D7007A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -70,6 +80,18 @@ Global
{D0F601A2-DB9F-4BFE-88FE-8035D5130980}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0F601A2-DB9F-4BFE-88FE-8035D5130980}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0F601A2-DB9F-4BFE-88FE-8035D5130980}.Release|Any CPU.Build.0 = Release|Any CPU
{5D45EAD5-5694-44A9-9BC5-E8D4EC223C72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D45EAD5-5694-44A9-9BC5-E8D4EC223C72}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D45EAD5-5694-44A9-9BC5-E8D4EC223C72}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D45EAD5-5694-44A9-9BC5-E8D4EC223C72}.Release|Any CPU.Build.0 = Release|Any CPU
{AC504586-CB6A-4355-84D4-D18BD2B76838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC504586-CB6A-4355-84D4-D18BD2B76838}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC504586-CB6A-4355-84D4-D18BD2B76838}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC504586-CB6A-4355-84D4-D18BD2B76838}.Release|Any CPU.Build.0 = Release|Any CPU
{E08C2037-1FF1-486D-BA93-956605D7007A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E08C2037-1FF1-486D-BA93-956605D7007A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E08C2037-1FF1-486D-BA93-956605D7007A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E08C2037-1FF1-486D-BA93-956605D7007A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -78,6 +100,10 @@ Global
{0E1E52AC-741D-4A4B-AB6A-BFE5AB44BF86} = {469F900E-5D25-4CD0-A0CC-A255131F2D8F}
{64174681-1570-45E3-9965-18433F644EEC} = {469F900E-5D25-4CD0-A0CC-A255131F2D8F}
{D0F601A2-DB9F-4BFE-88FE-8035D5130980} = {7F5EBA53-F3CE-42E8-AE96-35588E0EB67B}
{5D45EAD5-5694-44A9-9BC5-E8D4EC223C72} = {5F33DA26-5155-4499-A990-5042CB725506}
{38F9FCA7-EB52-400C-B9B9-8376AAB80C0F} = {5F33DA26-5155-4499-A990-5042CB725506}
{AC504586-CB6A-4355-84D4-D18BD2B76838} = {38F9FCA7-EB52-400C-B9B9-8376AAB80C0F}
{E08C2037-1FF1-486D-BA93-956605D7007A} = {5F33DA26-5155-4499-A990-5042CB725506}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {796564D4-DAEC-4717-ADD8-43C0947B2BBE}
Expand Down
5 changes: 4 additions & 1 deletion src/Directory.packages.props
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<Project>
<ItemGroup>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.VisualStudio.Validation" Version="17.6.11" />
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="xunit" Version="2.5.1" />
<PackageVersion Include="xunit.extensibility.core" Version="2.5.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.0-beta006" />
<PackageVersion Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.0" />
<PackageVersion Include="Roslynator.Analyzers" Version="1.7.0" />
<PackageVersion Include="xunit.analyzers" Version="0.8.0" PrivateAssets="all" />
<PackageVersion Include="xunit.analyzers" Version="0.8.0" />
<PackageVersion Include="Moq" Version="4.7.99" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>
</Project>
19 changes: 10 additions & 9 deletions src/Examples/Testify.Examples/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -993,15 +993,6 @@
"xunit.extensibility.execution": "[2.5.1]"
}
},
"xunit.extensibility.core": {
"type": "Transitive",
"resolved": "2.5.1",
"contentHash": "XGPiWP7D/KIY/fzdmU9gx7eDt0QD0IAWOy54LI+ckLZCqFMupIFochC3dHRxykuAz+L0nYvz6PxDdR2UcgNmDw==",
"dependencies": {
"NETStandard.Library": "1.6.1",
"xunit.abstractions": "2.0.3"
}
},
"xunit.extensibility.execution": {
"type": "Transitive",
"resolved": "2.5.1",
Expand All @@ -1022,6 +1013,16 @@
"requested": "[0.8.0, )",
"resolved": "1.3.0",
"contentHash": "gSk+8RC6UZ6Fzx1OHoB2bPyENeg3WHIeJhB/hb4oZNN0pW0dwOuplJay6OnqFIvW8T37re/RB4PWpEvayWIO1Q=="
},
"xunit.extensibility.core": {
"type": "CentralTransitive",
"requested": "[2.5.1, )",
"resolved": "2.5.1",
"contentHash": "XGPiWP7D/KIY/fzdmU9gx7eDt0QD0IAWOy54LI+ckLZCqFMupIFochC3dHRxykuAz+L0nYvz6PxDdR2UcgNmDw==",
"dependencies": {
"NETStandard.Library": "1.6.1",
"xunit.abstractions": "2.0.3"
}
}
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/Testify.Assertions/ActualValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Testify;

using System;

/// <summary>
/// Represents a record that holds actual value for an assertion and the
/// expression for it.
/// </summary>
/// <typeparam name="T">The type of the actual value.</typeparam>
public record ActualValue<T>(T? Value, string Expression) : IFormattable
{
/// <inheritdoc/>
public override string ToString()
{
return ToString(null, null);
}

/// <inheritdoc/>
public string ToString(string? format, IFormatProvider? formatProvider)
=> format switch
{
"e" => FormatExpression(Expression),
"v" => Format(Value),
_ => Format(Value, Expression)
};
}
120 changes: 120 additions & 0 deletions src/Testify.Assertions/Assertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
namespace Testify;

using System.Text.RegularExpressions;

using Testify.Internal;

/// <summary>
/// Provides methods used to start an assertion or for use within an assertion
/// implementation.
/// </summary>
public static partial class Assertion
{
[GeneratedRegex("{because}", RegexOptions.Compiled)]
private static partial Regex BecauseHole();

/// <summary>
/// Begins a fluent assertion by providing the actual value being asserted
/// on.
/// </summary>
/// <typeparam name="T">The type of the actual value.</typeparam>
/// <param name="value">The actual value.</param>
/// <param name="expression">The expression used when providing the actual
/// value.</param>
/// <returns>An <see cref="ActualValue{T}"/> instance representing the
/// actual value being asserted on.</returns>
public static ActualValue<T> Assert<T>(
T? value,
[Expression("value")] string? expression = null)
=> new(value, Guard.Against.Null(expression));

/// <summary>
/// Begins a fluent assertion by providing an <see cref="Action"/> as the
/// value being asserted on.
/// </summary>
/// <param name="action">The <see cref="Action"/> for the actual value.
/// </param>
/// <param name="expression">The expression used when providing the action.
/// </param>
/// <returns>An <see cref="ActualValue{Action}"/> instance representing the
/// actual value being asserted on.</returns>
public static ActualValue<Action> Assert(
Action action,
[Expression("action")] string? expression = null)
=> Assert<Action>(action, expression);

/// <summary>
/// Makes a "compound assertion" that fails with the specified message if any
/// wrapped assertions fail.
/// </summary>
/// <param name="message">The assertion failure message to report if any
/// wrapped assertions fail.</param>
/// <param name="assertions">The <see cref="Action"/> to invoke which makes
/// assertions to be wrapped in the "compound assertions".</param>
/// <remarks>
/// This is a very low level assertion generally used in the implementation
/// of other "compound assertions" and not made directly within tests. The
/// behavior of assertions are temporarily changed within the scope of the
/// invoked <paramref name="assertions"/> to combine assertion failures,
/// rather than to immediately throw them. This allows multiple assertions
/// to be combined into a single assertion with a meaningful failure
/// message. Note that only assertion methods within <b>Testify</b> will be
/// combined this way, and any other exceptions thrown within the
/// <paramref name="assertions"/> will cause an immediate test failure.
/// </remarks>
public static void Assert(string message, Action assertions)
{
AssertionScope.Push(message);
try
{
assertions.Invoke();
}
catch
{
AssertionScope.Pop(false);
throw;
}

AssertionScope.Pop();
}

/// <summary>
/// Asserts that the <paramref name="actual"/> value should satisfy all of
/// the assertions made when invoking <paramref name="assertions"/>.
/// </summary>
/// <typeparam name="T">The type of the actual value being asserted on.
/// </typeparam>
/// <param name="actual">The actual value being asserted on.</param>
/// <param name="assertions">An <see cref="Action{T}"/> that makes multiple
/// assertions on the <paramref name="actual"/> value.</param>
public static void ShouldSatisfy<T>(this ActualValue<T> actual, Action<ActualValue<T>> assertions)
=> Assert("One or more assertions were not satisfied.", () => assertions.Invoke(actual));

/// <summary>
/// Generates a test platform specific failure exception. If the test
/// platform cannot be determined then raises the non-platform specific
/// <see cref="AssertionException"/>.
/// </summary>
/// <param name="message">The assertion message, including the "{because}"
/// hole used to format the user specified reason for the failure.
/// </param>
/// <param name="because">The user specified reason for the failure.</param>
/// <exception cref="Exception"></exception>
public static void Fail(string message, string? because = null)
{
Guard.Against.NullOrWhiteSpace(message);

if (!string.IsNullOrWhiteSpace(because))
{
because = because.Trim();
if (!because.StartsWith("because"))
{
because = $"because {because}";
}

message = BecauseHole().Replace(message, " " + because);
}

AssertionScope.Fail(message);
}
}
62 changes: 62 additions & 0 deletions src/Testify.Assertions/AssertionException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Testify;

using System;
using System.Runtime.Serialization;

/// <summary>
/// Represents assertion failures that occur during test execution.
/// </summary>
/// <remarks>
/// This is the exception type thrown by assertions in the <b>Testify</b>
/// framework when no unit test framework can be detected.
/// </remarks>
[Serializable]
public class AssertionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="AssertionException"/>
/// class.
/// </summary>
public AssertionException() { }

/// <summary>
/// Initializes a new instance of the <see cref="AssertionException"/> class
/// with a specified failure message.
/// </summary>
/// <param name="message">The message that describes the reason for an
/// assertion failure.</param>
public AssertionException(string message) : base(message) { }

/// <summary>
/// Initializes a new instance of the <see cref="AssertionException"/> class
/// with a specified failure message and a reference to the inner exception
/// that is the cause of this exception.
/// </summary>
/// <param name="message">The message that describes the reason for an
/// assertion failure.</param>
/// <param name="inner">The exception that is the cause of the current
/// exception, or a <see langword="null"/> reference if no inner
/// exception is specified.</param>
public AssertionException(string message, Exception inner) : base(message, inner) { }

/// <summary>
/// Initializes a new instance of the <see cref="AssertionException"/> class
/// with serialized data.
/// </summary>
/// <param name="info">The
/// <see cref="T:System.Runtime.Serialization.SerializationInfo"/> that
/// holds the serialized object data about the exception being thrown.
/// </param>
/// <param name="context">The
/// <see cref="T:System.Runtime.Serialization.StreamingContext"/> that
/// contains contextual information about the source or destination.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="info"/> is
/// <see langword="null"/>.</exception>
/// <exception cref="SerializationException">The class name is
/// <see langword="null"/> or <see cref="Exception.HResult"/> is zero
/// (0).</exception>
protected AssertionException(
SerializationInfo info,
StreamingContext context) : base(info, context) { }
}
Loading

0 comments on commit 8ed3872

Please sign in to comment.