diff --git a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs new file mode 100644 index 000000000..d210c1d39 --- /dev/null +++ b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs @@ -0,0 +1,138 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace aweXpect.SourceGenerators; + +/// +/// The for simple expectations. +/// +[Generator] +public class ExpectationGenerator : IIncrementalGenerator +{ + void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext context) + { + // Add the marker attribute to the compilation + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "CreateExpectationOnAttribute.g.cs", + SourceText.From(SourceGenerationHelper.CreateExpectationOnAttribute, Encoding.UTF8))); + + HashSet files = new(); + IncrementalValuesProvider expectationsToGenerate = context.SyntaxProvider + .CreateSyntaxProvider( + static (s, _) => IsSyntaxTargetForGeneration(s), + (ctx, _) => GetSemanticTargetForGeneration(ctx, files)) + .Where(static m => m is not null) + .SelectMany((x, _) => x!.ToImmutableArray()); + + context.RegisterSourceOutput(expectationsToGenerate, + static (spc, source) => Execute(source, spc)); + } + + private static bool IsSyntaxTargetForGeneration(SyntaxNode node) + => node is ClassDeclarationSyntax { AttributeLists.Count: > 0, }; + + private static IEnumerable GetSemanticTargetForGeneration(GeneratorSyntaxContext context, + HashSet files) + { + // we know the node is a ClassDeclarationSyntax thanks to IsSyntaxTargetForGeneration + ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + + SemanticModel semanticModel = context.SemanticModel; + if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol) + { + yield break; + } + + foreach (AttributeData? attributeData in classSymbol.GetAttributes()) + { + INamedTypeSymbol? attributeClass = attributeData.AttributeClass; + if (attributeClass == null || !attributeClass.IsGenericType || + attributeClass.Name != "CreateExpectationOnAttribute") + { + continue; + } + + // Extract the target type from the generic type argument + INamedTypeSymbol? targetType = attributeClass.TypeArguments[0] as INamedTypeSymbol; + if (targetType == null) + { + continue; + } + + ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol, + attributeData); + if (expectationToGenerate != null && + files.Add(expectationToGenerate.Value.FileName)) + { + yield return expectationToGenerate.Value; + } + } + } + + private static void Execute(ExpectationToGenerate expectationToGenerate, SourceProductionContext context) + { + string result = SourceGenerationHelper.GenerateExtensionClass(expectationToGenerate); + // Create a separate partial class file for each enum + context.AddSource(expectationToGenerate.FileName, SourceText.From(result, Encoding.UTF8)); + } + + private static ExpectationToGenerate? GetExpectationToGenerate(INamedTypeSymbol classSymbol, + AttributeData attributeData) + { + string containingNamespace = classSymbol.ContainingNamespace.ToString(); + if (containingNamespace is null) + { + return null; + } + + INamedTypeSymbol? targetType = attributeData.AttributeClass?.TypeArguments[0] as INamedTypeSymbol; + if (targetType == null) + { + return null; + } + + string? outcomeMethod = null; + string? name = null; + if (attributeData.ConstructorArguments.Length == 2) + { + name = attributeData.ConstructorArguments[0].Value?.ToString(); + outcomeMethod = attributeData.ConstructorArguments[1].Value?.ToString(); + } + + if (outcomeMethod == null || name == null) + { + return null; + } + + if (targetType.TypeKind == TypeKind.Error) + { + return null; + } + + string expectationText = outcomeMethod; + string? remarks = null; + string[] usings = []; + foreach (KeyValuePair namedArgument in attributeData.NamedArguments) + { + switch (namedArgument.Key) + { + case "ExpectationText": + expectationText = namedArgument.Value.Value?.ToString() ?? expectationText; + break; + case "Remarks": + remarks = namedArgument.Value.Value?.ToString(); + break; + case "Using": + usings = + namedArgument.Value.Values.Select(x => x.Value?.ToString()).Where(x => x != null).ToArray()!; + break; + } + } + + return new ExpectationToGenerate(containingNamespace, classSymbol.Name, targetType, name, outcomeMethod, + expectationText, usings, remarks); + } +} diff --git a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs b/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs new file mode 100644 index 000000000..2a97abcf7 --- /dev/null +++ b/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs @@ -0,0 +1,57 @@ +using Microsoft.CodeAnalysis; + +namespace aweXpect.SourceGenerators; + +internal readonly record struct ExpectationToGenerate +{ + public ExpectationToGenerate(string @namespace, + string className, + INamedTypeSymbol targetType, + string name, + string outcomeMethod, + string expectationText, + string[] usings, + string? remarks) + { + Namespace = @namespace; + ClassName = className; + TargetType = targetType; + Name = name.Replace("{Not}", ""); + NegatedName = name.Replace("{Not}", "Not"); + IncludeNegated = name.Contains("{Not}"); + OutcomeMethod = outcomeMethod; + ExpectationText = expectationText.Replace("{not}", "").Replace(" ", " "); + NegatedExpectationText = expectationText.Replace("{not}", " not ").Replace(" ", " "); + Remarks = remarks; + Usings = usings; + FileName = $"{ClassName}.{Name}.g.cs"; + } + + public string[] Usings { get; } + public string FileName { get; } + public bool IncludeNegated { get; } + public string NegatedName { get; } + public string Namespace { get; } + public string ClassName { get; } + public INamedTypeSymbol TargetType { get; } + public string Name { get; } + public string OutcomeMethod { get; } + public string ExpectationText { get; } + public string NegatedExpectationText { get; } + public string? Remarks { get; } + + public string AppendRemarks() + { + if (string.IsNullOrEmpty(Remarks)) + { + return ""; + } + + return $$""" + + /// + /// {{Remarks!.Replace("\n", "\n/// ")}} + /// + """.Replace("\n", "\n\t"); + } +} diff --git a/Source/aweXpect.SourceGenerators/Properties/launchSettings.json b/Source/aweXpect.SourceGenerators/Properties/launchSettings.json new file mode 100644 index 000000000..93c85a58e --- /dev/null +++ b/Source/aweXpect.SourceGenerators/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Generators": { + "commandName": "DebugRoslynComponent", + "targetProject": "../aweXpect/aweXpect.csproj" + } + } +} diff --git a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs b/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs new file mode 100644 index 000000000..8659a9bc6 --- /dev/null +++ b/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs @@ -0,0 +1,107 @@ +namespace aweXpect.SourceGenerators; + +internal static class SourceGenerationHelper +{ + public const string CreateExpectationOnAttribute = + """ + using System; + + namespace aweXpect.SourceGenerators; + + #nullable enable + /// + /// Create an assertion on the attribute. + /// + /// The target type for the assertion + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] + internal class CreateExpectationOnAttribute : System.Attribute + { + public CreateExpectationOnAttribute(string methodName, string name) + { + TargetType = typeof(TTarget); + MethodName = methodName; + Name = name; + } + + public Type TargetType { get; } + public string MethodName { get; } + public string Name { get; set; } + public string? ExpectationText { get; set; } + public string? Remarks { get; set; } + public string[] Using { get; set; } = []; + } + #nullable disable + """; + + public static string GenerateExtensionClass(ExpectationToGenerate expectationToGenerate) + { + string result = $$""" + {{string.Join("\n", expectationToGenerate.Usings.Select(x => $"using {x};"))}} + using aweXpect.Core; + using aweXpect.Core.Constraints; + using aweXpect.Helpers; + using aweXpect.Results; + + namespace {{expectationToGenerate.Namespace}}; + + #nullable enable + public static partial class {{expectationToGenerate.ClassName}} + { + /// + /// Verifies that the subject {{expectationToGenerate.ExpectationText}}. + /// {{expectationToGenerate.AppendRemarks()}} + public static AndOrResult<{{expectationToGenerate.TargetType.ToDisplayString()}}, IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}>> {{expectationToGenerate.Name}}(this IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}> source) + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => + new {{expectationToGenerate.Name}}Constraint(it, grammars)), + source); + + + """; + if (expectationToGenerate.IncludeNegated) + { + result += $$""" + /// + /// Verifies that the subject {{expectationToGenerate.NegatedExpectationText}}. + /// {{expectationToGenerate.AppendRemarks()}} + public static AndOrResult<{{expectationToGenerate.TargetType.ToDisplayString()}}, IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}>> {{expectationToGenerate.NegatedName}}(this IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}> source) + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => + new {{expectationToGenerate.Name}}Constraint(it, grammars).Invert()), + source); + + + """; + } + + result += $$""" + private sealed class {{expectationToGenerate.Name}}Constraint(string it, ExpectationGrammars grammars) + : ConstraintResult.WithValue<{{expectationToGenerate.TargetType.ToDisplayString()}}>(grammars), + IValueConstraint<{{expectationToGenerate.TargetType.ToDisplayString()}}> + { + public ConstraintResult IsMetBy({{expectationToGenerate.TargetType.ToDisplayString()}} actual) + { + Actual = actual; + Outcome = {{expectationToGenerate.OutcomeMethod.Replace("{value}", "actual")}} ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("{{expectationToGenerate.ExpectationText}}"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" was "); + Formatter.Format(stringBuilder, Actual); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("{{expectationToGenerate.NegatedExpectationText}}"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => AppendNormalResult(stringBuilder, indentation); + } + } + #nullable disable + """; + return result.TrimStart(); + } +} diff --git a/Source/aweXpect.SourceGenerators/aweXpect.SourceGenerators.csproj b/Source/aweXpect.SourceGenerators/aweXpect.SourceGenerators.csproj new file mode 100644 index 000000000..b08d8e852 --- /dev/null +++ b/Source/aweXpect.SourceGenerators/aweXpect.SourceGenerators.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + false + enable + true + Latest + true + false + + + + + + + + + + + diff --git a/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs b/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs index 5942ce615..e8bd10988 100644 --- a/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs +++ b/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs @@ -1,48 +1,12 @@ -using aweXpect.Core; -using aweXpect.Core.Constraints; -using aweXpect.Helpers; -using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatChar -{ - /// - /// Verifies that the subject is a letter. - /// - /// - /// This means, that the specified Unicode character is categorized as a Unicode letter.
- /// - ///
- public static AndOrResult> IsALetter(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsALetterConstraint(it, grammars)), - source); - - private sealed class IsALetterConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithValue(grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char actual) - { - Actual = actual; - Outcome = char.IsLetter(actual) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is a letter"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(it).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not a letter"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOn("Is{Not}ALetter", "char.IsLetter({value})", + ExpectationText = "is {not} a letter", + Remarks = """ + This means, that the specified Unicode character is categorized as a Unicode letter.
+ + """ +)] +public static partial class ThatChar; diff --git a/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs b/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs index fcece655a..c6fec3848 100644 --- a/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs +++ b/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs @@ -1,48 +1,12 @@ -using aweXpect.Core; -using aweXpect.Core.Constraints; -using aweXpect.Helpers; -using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatChar -{ - /// - /// Verifies that the subject is a number. - /// - /// - /// This means, that the specified Unicode character is categorized as a number.
- /// - ///
- public static AndOrResult> IsANumber(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsANumberConstraint(it, grammars)), - source); - - private sealed class IsANumberConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithValue(grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char actual) - { - Actual = actual; - Outcome = char.IsNumber(actual) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is a number"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(it).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not a number"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOn("Is{Not}ANumber", "char.IsNumber({value})", + ExpectationText = "is {not} a number", + Remarks = """ + This means, that the specified Unicode character is categorized as a number.
+ + """ +)] +public static partial class ThatChar; diff --git a/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs b/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs index 0087941fc..35be2ba6d 100644 --- a/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs +++ b/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs @@ -1,57 +1,18 @@ -using aweXpect.Core; -using aweXpect.Core.Constraints; -using aweXpect.Helpers; -using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatChar -{ #if NET8_0_OR_GREATER - /// - /// Verifies that the subject is an ASCII letter. - /// - /// - /// - /// +[CreateExpectationOn("Is{Not}AnAsciiLetter", "char.IsAsciiLetter({value})", + ExpectationText = "is {not} an ASCII letter", + Remarks = """ + This means, that the specified Unicode character is categorized as an ASCII letter.
+ + """ +)] #else - /// - /// Verifies that the subject is an ASCII letter. - /// +[CreateExpectationOn("Is{Not}AnAsciiLetter", "{value} is >= 'a' and <= 'z' or >= 'A' and <= 'Z'", + ExpectationText = "is {not} an ASCII letter" +)] #endif - public static AndOrResult> IsAnAsciiLetter(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsAnAsciiLetterConstraint(it, grammars)), - source); - - private sealed class IsAnAsciiLetterConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithValue(grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char actual) - { - Actual = actual; -#if NET8_0_OR_GREATER - Outcome = char.IsAsciiLetter(actual) ? Outcome.Success : Outcome.Failure; -#else - Outcome = actual is >= 'a' and <= 'z' or >= 'A' and <= 'Z' ? Outcome.Success : Outcome.Failure; -#endif - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is an ASCII letter"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(it).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not an ASCII letter"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +public static partial class ThatChar; diff --git a/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs b/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs index 79ed47da4..6ca36cf46 100644 --- a/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs +++ b/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs @@ -1,48 +1,12 @@ -using aweXpect.Core; -using aweXpect.Core.Constraints; -using aweXpect.Helpers; -using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatChar -{ - /// - /// Verifies that the subject is white-space. - /// - /// - /// This means, that the specified Unicode character is categorized as white-space.
- /// - ///
- public static AndOrResult> IsWhiteSpace(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsWhiteSpaceConstraint(it, grammars)), - source); - - private sealed class IsWhiteSpaceConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithValue(grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char actual) - { - Actual = actual; - Outcome = char.IsWhiteSpace(actual) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is white-space"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(it).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not white-space"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOn("Is{Not}WhiteSpace", "char.IsWhiteSpace({value})", + ExpectationText = "is {not} white-space", + Remarks = """ + This means, that the specified Unicode character is categorized as white-space.
+ + """ +)] +public static partial class ThatChar; diff --git a/Source/aweXpect/That/Guids/ThatGuid.IsEmpty.cs b/Source/aweXpect/That/Guids/ThatGuid.IsEmpty.cs index e857fa892..7173bdd48 100644 --- a/Source/aweXpect/That/Guids/ThatGuid.IsEmpty.cs +++ b/Source/aweXpect/That/Guids/ThatGuid.IsEmpty.cs @@ -1,53 +1,10 @@ using System; -using aweXpect.Core; -using aweXpect.Core.Constraints; -using aweXpect.Helpers; -using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatGuid -{ - /// - /// Verifies that the subject is empty. - /// - public static AndOrResult> IsEmpty(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsEmptyConstraint(it, grammars)), - source); - - /// - /// Verifies that the subject is not empty. - /// - public static AndOrResult> IsNotEmpty(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsEmptyConstraint(it, grammars).Invert()), - source); - - private sealed class IsEmptyConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(Guid actual) - { - Actual = actual; - Outcome = actual == Guid.Empty ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is empty"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(It).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not empty"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOn("Is{Not}Empty", "{value} == Guid.Empty", + ExpectationText = "is {not} empty", + Using = ["System"] +)] +public static partial class ThatGuid; diff --git a/Source/aweXpect/aweXpect.csproj b/Source/aweXpect/aweXpect.csproj index f0e5ada47..0b7dd50a9 100644 --- a/Source/aweXpect/aweXpect.csproj +++ b/Source/aweXpect/aweXpect.csproj @@ -20,6 +20,7 @@ + diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index ec282ac3d..11fc86bc9 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -193,10 +193,14 @@ namespace aweXpect public static aweXpect.Results.AndOrResult> IsANumber(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsEqualTo(this aweXpect.Core.IThat source, char? expected) { } + public static aweXpect.Results.AndOrResult> IsNotALetter(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotANumber(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsNotEqualTo(this aweXpect.Core.IThat source, char? unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, params char?[] unexpected) { } + public static aweXpect.Results.AndOrResult> IsNotWhiteSpace(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, params char?[] expected) { } diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt index 7b7c4406c..5c4e7c2f1 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt @@ -51,10 +51,14 @@ namespace aweXpect public static aweXpect.Results.AndOrResult> IsANumber(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsEqualTo(this aweXpect.Core.IThat source, char? expected) { } + public static aweXpect.Results.AndOrResult> IsNotALetter(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotANumber(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsNotEqualTo(this aweXpect.Core.IThat source, char? unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, params char?[] unexpected) { } + public static aweXpect.Results.AndOrResult> IsNotWhiteSpace(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, params char?[] expected) { } diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.IsNotALetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotALetter.Tests.cs new file mode 100644 index 000000000..cbd7e0ed1 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotALetter.Tests.cs @@ -0,0 +1,87 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed class IsNotALetter + { + public sealed class Tests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNotALetter_ShouldFail(char subject) + { + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not a letter, + but it was {Formatter.Format(subject)} + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldFail(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is a letter, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNotALetter_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.IsNotANumber.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotANumber.Tests.cs new file mode 100644 index 000000000..16e5b3c8b --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotANumber.Tests.cs @@ -0,0 +1,93 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed class IsNotANumber + { + public sealed class Tests + { + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + public async Task WhenSubjectIsNotANumber_ShouldFail(char subject) + { + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not a number, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('\t')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).DoesNotThrow(); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + public async Task WhenSubjectIsNotANumber_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('\t')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldFail(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is a number, + but it was {Formatter.Format(subject)} + """); + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.IsNotAnAsciiLetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotAnAsciiLetter.Tests.cs new file mode 100644 index 000000000..7ec0597e2 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotAnAsciiLetter.Tests.cs @@ -0,0 +1,87 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed class IsNotAnAsciiLetter + { + public sealed class Tests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNoAsciiLetter_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + public async Task WhenSubjectIsNotAnAsciiLetter_ShouldFail(char subject) + { + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not an ASCII letter, + but it was {Formatter.Format(subject)} + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNoAsciiLetter_ShouldFail(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is an ASCII letter, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + public async Task WhenSubjectIsNotAnAsciiLetter_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.IsNotWhiteSpace.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotWhiteSpace.Tests.cs new file mode 100644 index 000000000..74841780b --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotWhiteSpace.Tests.cs @@ -0,0 +1,99 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed class IsNotWhiteSpace + { + public sealed class Tests + { + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData(' ')] + [InlineData('\t')] + [InlineData('\r')] + [InlineData('\n')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldFail(char subject) + { + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not white-space, + but it was {Formatter.Format(subject)} + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldFail(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is white-space, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData(' ')] + [InlineData('\t')] + [InlineData('\r')] + [InlineData('\n')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldSucceed(char subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/aweXpect.sln b/aweXpect.sln index 1d564c714..21085e6fe 100644 --- a/aweXpect.sln +++ b/aweXpect.sln @@ -93,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aweXpect.Analyzers.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aweXpect.Analyzers.CodeFixers", "Source\aweXpect.Analyzers.CodeFixers\aweXpect.Analyzers.CodeFixers.csproj", "{117FF28C-3628-47F1-8567-008265AE211F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aweXpect.SourceGenerators", "Source\aweXpect.SourceGenerators\aweXpect.SourceGenerators.csproj", "{C4F90F26-54CD-447E-870D-6F1C05729155}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -177,6 +179,10 @@ Global {117FF28C-3628-47F1-8567-008265AE211F}.Debug|Any CPU.Build.0 = Debug|Any CPU {117FF28C-3628-47F1-8567-008265AE211F}.Release|Any CPU.ActiveCfg = Release|Any CPU {117FF28C-3628-47F1-8567-008265AE211F}.Release|Any CPU.Build.0 = Release|Any CPU + {C4F90F26-54CD-447E-870D-6F1C05729155}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4F90F26-54CD-447E-870D-6F1C05729155}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4F90F26-54CD-447E-870D-6F1C05729155}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4F90F26-54CD-447E-870D-6F1C05729155}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {97D64B45-D97E-4A94-9EF0-37BF25310EBA} = {9CC57AD0-4984-4618-96EA-01FFFCCD84FA}