From 27cce36b954fc65ea52fe5a18ca4d11ca336cf21 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Fri, 26 Apr 2024 23:33:29 +0100 Subject: [PATCH 01/48] Add core implementation from PoC --- Reqnroll.FeatureSourceGenerator/.editorconfig | 10 + .../AttributeDescriptor.cs | 29 ++ .../BuiltInTestFrameworkHandlers.cs | 26 ++ .../CSharp/CSharpSourceTextBuilder.cs | 218 +++++++++++++ .../CSharpSourceTextBuilderResources.resx | 132 ++++++++ .../CSharp/CSharpSyntax.cs | 79 +++++ .../CSharp/CSharpTestFixtureGeneration.cs | 303 ++++++++++++++++++ .../CSharpTestFixtureSourceGenerator.cs | 26 ++ .../CompilationInformation.cs | 42 +++ .../DiagnosticIds.cs | 10 + .../EnumerableEqualityExtensions.cs | 55 ++++ .../FeatureInformation.cs | 10 + .../Gherkin/GherkinDocumentComparer.cs | 24 ++ .../Gherkin/GherkinSyntaxParser.cs | 60 ++++ .../Gherkin/GherkinSyntaxParserResources.resx | 126 ++++++++ .../Gherkin/GherkinSyntaxTree.cs | 58 ++++ .../Gherkin/SourceTokenScanner.cs | 27 ++ .../ITestFrameworkHandler.cs | 12 + .../IsExternalInit.cs | 22 ++ .../KeyValuePairDeconstruct.cs | 10 + .../MSTestCSharpTestFixtureGeneration.cs | 84 +++++ .../MSTest/MSTestHandler.cs | 26 ++ .../NUnit/NUnitCSharpSyntaxGeneration.cs | 7 + .../NUnit/NUnitHandler.cs | 25 ++ .../Reqnroll.FeatureSourceGenerator.csproj | 63 ++++ Reqnroll.FeatureSourceGenerator/StepResult.cs | 82 +++++ .../TestFixtureSourceGenerator.cs | 213 ++++++++++++ .../TestFixtureSourceGeneratorResources.resx | 132 ++++++++ .../TestFrameworkInformation.cs | 23 ++ .../XUnit/XUnitHandler.cs | 21 ++ Reqnroll.sln | 13 + .../.editorconfig | 10 + .../AssertionExtensions.cs | 31 ++ .../CSharpClassDeclarationAssertions.cs | 82 +++++ .../CSharpNamespaceDeclarationAssertions.cs | 90 ++++++ .../CSharpSyntaxAssertions.cs | 91 ++++++ .../FakeAnalyzerConfigOptionsProvider.cs | 12 + .../FeatureFile.cs | 16 + .../InMemoryAnalyzerConfigOptions.cs | 12 + .../MSTestFeatureSourceGeneratorTests.cs | 58 ++++ ...eqnroll.FeatureSourceGeneratorTests.csproj | 31 ++ 41 files changed, 2401 insertions(+) create mode 100644 Reqnroll.FeatureSourceGenerator/.editorconfig create mode 100644 Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs create mode 100644 Reqnroll.FeatureSourceGenerator/BuiltInTestFrameworkHandlers.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilderResources.resx create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CompilationInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/DiagnosticIds.cs create mode 100644 Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs create mode 100644 Reqnroll.FeatureSourceGenerator/FeatureInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs create mode 100644 Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs create mode 100644 Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParserResources.resx create mode 100644 Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs create mode 100644 Reqnroll.FeatureSourceGenerator/Gherkin/SourceTokenScanner.cs create mode 100644 Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs create mode 100644 Reqnroll.FeatureSourceGenerator/IsExternalInit.cs create mode 100644 Reqnroll.FeatureSourceGenerator/KeyValuePairDeconstruct.cs create mode 100644 Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs create mode 100644 Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs create mode 100644 Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs create mode 100644 Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs create mode 100644 Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj create mode 100644 Reqnroll.FeatureSourceGenerator/StepResult.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx create mode 100644 Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/.editorconfig create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpNamespaceDeclarationAssertions.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpSyntaxAssertions.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/FakeAnalyzerConfigOptionsProvider.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/FeatureFile.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/InMemoryAnalyzerConfigOptions.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj diff --git a/Reqnroll.FeatureSourceGenerator/.editorconfig b/Reqnroll.FeatureSourceGenerator/.editorconfig new file mode 100644 index 000000000..4c0a7ea39 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/.editorconfig @@ -0,0 +1,10 @@ +[*] +insert_final_newline = true +indent_size = 4 +indent_style = space + +[*.cs] +csharp_style_namespace_declarations = file_scoped + +[*.{csproj,targets,props}] +indent_size = 2 diff --git a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs new file mode 100644 index 000000000..7dae725e3 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs @@ -0,0 +1,29 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Provides a description of a .NET attribute. +/// +/// The name of the attribute's type. +/// The namespace of the attribute's type. +/// The arguments passed to the attribute's constructor. +/// The property values of the attribute. +public record AttributeDescriptor( + string TypeName, + string Namespace, + ImmutableArray Arguments, + ImmutableArray> PropertyValues) +{ + public AttributeDescriptor( + string TypeName, + string Namespace, + ImmutableArray? Arguments = null, + ImmutableArray>? PropertyValues = null) : this( + TypeName, + Namespace, + Arguments ?? ImmutableArray.Empty, + PropertyValues ?? ImmutableArray>.Empty) + { + } +} diff --git a/Reqnroll.FeatureSourceGenerator/BuiltInTestFrameworkHandlers.cs b/Reqnroll.FeatureSourceGenerator/BuiltInTestFrameworkHandlers.cs new file mode 100644 index 000000000..492bf7ac7 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/BuiltInTestFrameworkHandlers.cs @@ -0,0 +1,26 @@ +using Reqnroll.FeatureSourceGenerator.MSTest; +using Reqnroll.FeatureSourceGenerator.NUnit; +using Reqnroll.FeatureSourceGenerator.XUnit; + +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Provides instances of the set of built-in handlers. +/// +internal static class BuiltInTestFrameworkHandlers +{ + /// + /// Gets the handler instance for NUnit. + /// + public static NUnitHandler NUnit { get; } = new(); + + /// + /// Gets the handler instance for MSTest. + /// + public static MSTestHandler MSTest { get; } = new(); + + /// + /// Gets the handler instance for xUnit. + /// + public static XUnitHandler XUnit { get; } = new(); +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs new file mode 100644 index 000000000..df9f57dca --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -0,0 +1,218 @@ +using System.Text; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +public class CSharpSourceTextBuilder +{ + private static readonly UTF8Encoding Encoding = new(false); + + private readonly StringBuilder _buffer = new(); + + private bool _isFreshLine = true; + + private const string Indent = " "; + + private CodeBlock _context = new(); + + /// + /// Gets the depth of the current block. + /// + public int Depth => _context.Depth; + + public CSharpSourceTextBuilder Append(char c) + { + AppendIndentIfIsFreshLine(); + + _buffer.Append(c); + return this; + } + + public CSharpSourceTextBuilder Append(string text) + { + AppendIndentIfIsFreshLine(); + + _buffer.Append(text); + return this; + } + + public CSharpSourceTextBuilder AppendConstantList(IEnumerable values) + { + var first = true; + + foreach (var value in values) + { + if (first) + { + first = false; + } + else + { + Append(", "); + } + + AppendConstant(value); + } + + return this; + } + + public CSharpSourceTextBuilder AppendConstant(object? value) + { + return value switch + { + null => Append("null"), + string s => AppendConstant(s), + _ => throw new NotSupportedException($"Values of type {value.GetType().FullName} cannot be encoded as a constant in C#.") + }; + } + + private CSharpSourceTextBuilder AppendConstant(string? s) + { + if (s == null) + { + return Append("null"); + } + + _buffer.Append('"').Append(s).Append('"'); + return this; + } + + private void AppendIndentIfIsFreshLine() + { + if (_isFreshLine) + { + AppendIndentToDepth(); + + _isFreshLine = false; + } + } + + private void AppendIndentToDepth() + { + for (var i = 0; i < Depth; i++) + { + _buffer.Append(Indent); + } + } + + internal void Reset() => _buffer.Clear(); + + public CSharpSourceTextBuilder AppendDirective(string directive) + { + if (!_isFreshLine) + { + throw new InvalidOperationException(CSharpSourceTextBuilderResources.CannotAppendDirectiveUnlessAtStartOfLine); + } + + _buffer.Append(directive); + + _buffer.AppendLine(); + + _isFreshLine = true; + return this; + } + + public CSharpSourceTextBuilder AppendLine() + { + AppendIndentIfIsFreshLine(); + + _buffer.AppendLine(); + + _isFreshLine = true; + return this; + } + + public CSharpSourceTextBuilder AppendLine(string text) + { + AppendIndentIfIsFreshLine(); + + _buffer.AppendLine(text); + + _isFreshLine = true; + return this; + } + + /// + /// Starts a new code block. + /// + public CSharpSourceTextBuilder BeginBlock() + { + AppendLine(); + _context = new CodeBlock(_context); + return this; + } + + /// + /// Appends the specified text and starts a new block. + /// + /// The text to append. + public CSharpSourceTextBuilder BeginBlock(string text) => Append(text).BeginBlock(); + + /// + /// Ends the current block and begins a new line. + /// + /// + /// The builder is not currently in a block (the is zero.) + /// + public CSharpSourceTextBuilder EndBlock() + { + if (_context.Parent == null) + { + throw new InvalidOperationException(CSharpSourceTextBuilderResources.NotInCodeBlock); + } + + _context = _context.Parent; + return AppendLine(); + } + + /// + /// Ends the current block, appends the specified text and begins a new line. + /// + /// The text to append. + /// + /// The builder is not currently in a block (the is zero.) + /// + public CSharpSourceTextBuilder EndBlock(string text) + { + if (_context.Parent == null) + { + throw new InvalidOperationException(CSharpSourceTextBuilderResources.NotInCodeBlock); + } + + _context = _context.Parent; + return AppendLine(text); + } + + /// + /// Gets the value of this instance as a string. + /// + /// A string containing all text appended to the builder. + public override string ToString() => _buffer.ToString(); + + private class CodeBlock + { + public CodeBlock() + { + Depth = 0; + } + + public CodeBlock(CodeBlock parent) + { + Parent = parent; + Depth = parent.Depth + 1; + } + + public int Depth { get; } + + public CodeBlock? Parent { get; } + + public bool InSection { get; set; } + + public bool HasSection { get; set; } + } + + public SourceText ToSourceText() + { + return SourceText.From(_buffer.ToString(), Encoding); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilderResources.resx b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilderResources.resx new file mode 100644 index 000000000..a50b6fcb6 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilderResources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The CSharpSourceBuilder is already in a code section. + + + Cannot append a directive unless at the start of a line. + + + The CSharpSourceBuilder is not in a code block. + + + The CSharpSourceBuilder is not in a code section. + + \ No newline at end of file diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs new file mode 100644 index 000000000..1d8ec891c --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs @@ -0,0 +1,79 @@ +using System.Globalization; +using System.Text; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +internal static class CSharpSyntax +{ + public static string CreateIdentifier(string s) + { + var sb = new StringBuilder(); + var newWord = true; + + foreach (char c in s) + { + if (char.IsWhiteSpace(c)) + { + newWord = true; + continue; + } + + if (!IsValidInIdentifier(c)) + { + continue; + } + + if (sb.Length == 0 && !IsValidAsFirstCharacterInIdentifier(c)) + { + sb.Append('_'); + sb.Append(c); + continue; + } + + if (newWord) + { + sb.Append(char.ToUpper(c)); + newWord = false; + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + private static bool IsValidAsFirstCharacterInIdentifier(char c) + { + if (c == '_') + { + return true; + } + + var category = char.GetUnicodeCategory(c); + + return category == UnicodeCategory.UppercaseLetter + || category == UnicodeCategory.LowercaseLetter + || category == UnicodeCategory.TitlecaseLetter + || category == UnicodeCategory.ModifierLetter + || category == UnicodeCategory.OtherLetter; + } + + private static bool IsValidInIdentifier(char c) + { + var category = char.GetUnicodeCategory(c); + + return category == UnicodeCategory.UppercaseLetter + || category == UnicodeCategory.LowercaseLetter + || category == UnicodeCategory.TitlecaseLetter + || category == UnicodeCategory.ModifierLetter + || category == UnicodeCategory.OtherLetter + || category == UnicodeCategory.LetterNumber + || category == UnicodeCategory.NonSpacingMark + || category == UnicodeCategory.SpacingCombiningMark + || category == UnicodeCategory.DecimalDigitNumber + || category == UnicodeCategory.ConnectorPunctuation + || category == UnicodeCategory.Format; + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs new file mode 100644 index 000000000..10539a557 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -0,0 +1,303 @@ +using Gherkin.Ast; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +/// +/// Represents a generation of a C# test fixture to allow the execution of a Gherkin feature. +/// +/// +/// This class provides the base for all Reqnroll's built-in test-framework implementations. It lays out the fundamental +/// common structure: +/// +/// A test class named after the feature title. +/// Every scenario/example and outline mapped to a method which passes the steps to the Reqnroll runtime. +/// +/// +public abstract class CSharpTestFixtureGeneration(FeatureInformation featureInfo) +{ + public FeatureInformation FeatureInformation { get; } = featureInfo; + + private bool IsLineMappingEnabled { get; } = featureInfo.FeatureSyntax.FilePath != null; + + protected GherkinDocument Document { get; } = featureInfo.FeatureSyntax.GetRoot(); + + protected CSharpSourceTextBuilder SourceBuilder { get; } = new(); + + internal SourceText GetSourceText() + { + SourceBuilder.Reset(); + + SourceBuilder.Append("namespace ").Append(FeatureInformation.FeatureNamespace).AppendLine(); + SourceBuilder.BeginBlock("{"); + + AppendTestFixtureClass(); + + SourceBuilder.EndBlock("}"); + + return SourceBuilder.ToSourceText(); + } + + protected virtual void AppendTestFixtureClass() + { + var attributes = GetTestFixtureAttributes(); + + foreach (var attribute in attributes) + { + AppendAttribute(attribute); + } + + var feature = Document.Feature; + var className = CSharpSyntax.CreateIdentifier(feature.Name + feature.Keyword); + + SourceBuilder.Append("public class ").Append(className).AppendLine(); + + SourceBuilder.BeginBlock("{"); + + AppendTestFixturePreamble(); + + foreach (var child in feature.Children) + { + switch (child) + { + case Scenario scenario: + SourceBuilder.AppendLine(); + AppendTestMethodForScenario(scenario); + break; + } + } + + SourceBuilder.EndBlock("}"); + } + + private void AppendAttribute(AttributeDescriptor attribute) + { + SourceBuilder.Append("[global::").Append(attribute.Namespace).Append(".").Append(attribute.TypeName); + + if (attribute.Arguments.Any() || attribute.PropertyValues.Any()) + { + var first = true; + + SourceBuilder.Append("("); + + foreach (var argument in attribute.Arguments) + { + if (!first) + { + SourceBuilder.Append(", "); + } + + first = false; + + SourceBuilder.AppendConstant(argument); + } + + foreach (var (propertyName, propertyValue) in attribute.PropertyValues) + { + if (!first) + { + SourceBuilder.Append(", "); + } + + first = false; + + SourceBuilder.Append(propertyName).Append(" = ").AppendConstant(propertyValue); + } + + SourceBuilder.Append(")"); + } + + SourceBuilder.AppendLine("]"); + } + + protected virtual IEnumerable GetTestFixtureAttributes() => []; + + protected virtual void AppendTestFixturePreamble() + { + SourceBuilder.AppendLine("// start: shared service method & consts, NO STATE!"); + SourceBuilder.AppendLine(); + + if (IsLineMappingEnabled && FeatureInformation.FeatureSyntax.FilePath != null) + { + SourceBuilder.AppendDirective($"#line 1 \"{FeatureInformation.FeatureSyntax.FilePath}\""); + SourceBuilder.AppendDirective("#line hidden"); + SourceBuilder.AppendLine(); + } + + var feature = FeatureInformation.FeatureSyntax.GetRoot().Feature; + + SourceBuilder + .Append("private static readonly string[] featureTags = new string[] { ") + .AppendConstantList(feature.Tags.Select(tag => tag.Name.TrimStart('@'))) + .AppendLine(" };") + .AppendLine(); + + SourceBuilder + .Append("private static readonly global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(") + .Append("new global::System.Globalization.CultureInfo(\"").Append(feature.Language).Append("\"), ") + .AppendConstant(Path.GetDirectoryName(FeatureInformation.FeatureSyntax.FilePath).Replace("\\", "\\\\")).Append(", ") + .AppendConstant(feature.Name).Append(", ") + .AppendLine("null, global::Reqnroll.ProgrammingLanguage.CSharp, featureTags);") + .AppendLine(); + + AppendScenarioInitializeMethod(); + + SourceBuilder.AppendLine(); + SourceBuilder.AppendLine("// end: shared service method & consts, NO STATE!"); + } + + private void AppendScenarioInitializeMethod() + { + SourceBuilder.AppendLine( + "public async global::System.Threading.Tasks.Task ScenarioInitialize(" + + "global::Reqnroll.ITestRunner testRunner, " + + "global::Reqnroll.ScenarioInfo scenarioInfo)"); + + SourceBuilder.BeginBlock("{"); + + AppendScenarioInitializeMethodBody(); + + SourceBuilder.EndBlock("}"); + } + + protected virtual void AppendScenarioInitializeMethodBody() + { + SourceBuilder.AppendLine("// handle feature initialization"); + SourceBuilder.AppendLine( + "if (testRunner.FeatureContext == null || !object.ReferenceEquals(testRunner.FeatureContext.FeatureInfo, featureInfo))"); + SourceBuilder.AppendLine("await testRunner.OnFeatureStartAsync(featureInfo);"); + SourceBuilder.AppendLine(); + + SourceBuilder.AppendLine("// handle scenario initialization"); + SourceBuilder.AppendLine("testRunner.OnScenarioInitialize(scenarioInfo);"); + } + + protected virtual void AppendTestMethodForScenario(Scenario scenario) + { + var attributes = GetTestMethodAttributes(scenario); + + foreach (var attribute in attributes) + { + AppendAttribute(attribute); + } + + SourceBuilder.Append("public async Task ").Append(CSharpSyntax.CreateIdentifier(scenario.Name)).AppendLine("()"); + SourceBuilder.BeginBlock("{"); + + AppendTestMethodBodyForScenario(scenario); + + SourceBuilder.EndBlock("}"); + } + + protected virtual void AppendTestMethodBodyForScenario(Scenario scenario) + { + AppendTestRunnerLookupForScenario(); + SourceBuilder.AppendLine(); + + AppendScenarioInfo(scenario); + SourceBuilder.AppendLine(); + + SourceBuilder.AppendLine("try"); + SourceBuilder.BeginBlock("{"); + + if (IsLineMappingEnabled) + { + SourceBuilder.AppendDirective($"#line {scenario.Location.Line}"); + } + + SourceBuilder.AppendLine("await ScenarioInitialize(testRunner, scenarioInfo);"); + + if (IsLineMappingEnabled) + { + SourceBuilder.AppendDirective("#line hidden"); + } + + SourceBuilder.AppendLine(); + + SourceBuilder.AppendLine("if (global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags))"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.AppendLine("testRunner.SkipScenario();"); + SourceBuilder.EndBlock("}"); + SourceBuilder.AppendLine("else"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.AppendLine("await testRunner.OnScenarioStartAsync();"); + SourceBuilder.AppendLine(); + SourceBuilder.AppendLine("// start: invocation of scenario steps"); + + foreach (var step in scenario.Steps) + { + AppendScenarioStepInvocation(step, scenario); + } + + SourceBuilder.AppendLine("// end: invocation of scenario steps"); + SourceBuilder.EndBlock("}"); + SourceBuilder.AppendLine(); + SourceBuilder.AppendLine("// finishing the scenario"); + SourceBuilder.AppendLine("await testRunner.CollectScenarioErrorsAsync();"); + + SourceBuilder.EndBlock("}"); + SourceBuilder.AppendLine("finally"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.AppendLine("await testRunner.OnScenarioEndAsync();"); + SourceBuilder.EndBlock("}"); + } + + protected virtual void AppendScenarioStepInvocation(Step step, Scenario scenario) + { + if (IsLineMappingEnabled) + { + SourceBuilder.AppendDirective($"#line {step.Location.Line}"); + } + + SourceBuilder + .Append("await testRunner.") + .Append( + step.KeywordType switch + { + global::Gherkin.StepKeywordType.Context => "Given", + global::Gherkin.StepKeywordType.Action => "When", + global::Gherkin.StepKeywordType.Outcome => "Then", + global::Gherkin.StepKeywordType.Conjunction => "And", + _ => throw new NotSupportedException($"Steps of type \"{step.Keyword}\" are not supported.") // TODO: Add message from resx + }) + .Append("Async(") + .AppendConstant(step.Text) + .Append(", null, null, ") + .AppendConstant(step.Keyword) + .AppendLine(");"); + + if (IsLineMappingEnabled) + { + SourceBuilder.AppendDirective("#line hidden"); + } + } + + protected virtual void AppendScenarioInfo(Scenario scenario) + { + SourceBuilder.AppendLine("// start: calculate ScenarioInfo"); + SourceBuilder + .Append("string[] tagsOfScenario = new string[] { ") + .AppendConstantList(scenario.Tags.Select(tag => tag.Name.TrimStart('@'))) + .AppendLine(" };"); + SourceBuilder.AppendLine( + "var argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); // needed for scenario outlines"); + + // TODO: Add support for rules. + SourceBuilder.AppendLine("var inheritedTags = featureTags; // will be more complex if there are rules"); + + SourceBuilder + .Append("var scenarioInfo = new global::Reqnroll.ScenarioInfo(") + .AppendConstant(scenario.Name) + .AppendLine(", null, tagsOfScenario, argumentsOfScenario, inheritedTags);"); + SourceBuilder.AppendLine("// end: calculate ScenarioInfo"); + } + + protected virtual void AppendTestRunnerLookupForScenario() + { + SourceBuilder.AppendLine("// getting test runner"); + SourceBuilder.AppendLine("string testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString(); " + + "// this might be different with other test runners"); + SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); + } + + protected virtual IEnumerable GetTestMethodAttributes(Scenario scenario) => []; +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs new file mode 100644 index 000000000..4b0d53f77 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs @@ -0,0 +1,26 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +/// +/// A generator of Reqnroll test fixtures for the C# language. +/// +[Generator(LanguageNames.CSharp)] +public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator +{ + /// + /// Initializes a new instance of the class. + /// + public CSharpTestFixtureSourceGenerator() + { + } + + /// + /// Initializes a new instance of the class specifying + /// the test framework handlers to be used. + /// + /// The handlers to use. + internal CSharpTestFixtureSourceGenerator(params ITestFrameworkHandler[] handlers) : base(handlers.ToImmutableArray()) + { + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs b/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs new file mode 100644 index 000000000..8333acb05 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; +public sealed record CompilationInformation( + string? AssemblyName, + string Language, + ImmutableArray ReferencedAssemblies) +{ + public bool Equals(CompilationInformation other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return string.Equals(AssemblyName, other.AssemblyName, StringComparison.Ordinal) + && ReferencedAssemblies.SequenceEqual(other.ReferencedAssemblies); + } + + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hash = 27; + + hash = hash * 43 + AssemblyName?.GetHashCode() ?? 0; + + foreach (var assembly in ReferencedAssemblies) + { + hash = hash * 43 + assembly?.GetHashCode() ?? 0; + } + + return hash; + } + } +} + diff --git a/Reqnroll.FeatureSourceGenerator/DiagnosticIds.cs b/Reqnroll.FeatureSourceGenerator/DiagnosticIds.cs new file mode 100644 index 000000000..6e78c0eb8 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/DiagnosticIds.cs @@ -0,0 +1,10 @@ +namespace Reqnroll.FeatureSourceGenerator; + +internal static class DiagnosticIds +{ + public const string SyntaxError = "GH1001"; + + public const string NoTestFrameworkFound = "RQ1001"; + + public const string TestFrameworkNotSupported = "RQ1002"; +} diff --git a/Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs b/Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs new file mode 100644 index 000000000..bb10339a7 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs @@ -0,0 +1,55 @@ +namespace Reqnroll.FeatureSourceGenerator; + +internal static class EnumerableEqualityExtensions +{ + public static bool SetEquals(this IEnumerable source, IEnumerable other) where T : IEquatable + { + if (source is null && other is null) + { + return true; + } + + if (source is null || other is null) + { + return false; + } + + var otherItems = other.ToList(); + + foreach (var item in source) + { + if (!otherItems.Remove(item)) + { + return false; + } + } + + if (otherItems.Count != 0) + { + return false; + } + + return true; + } + + /// + /// Gets a hash code for a sequence where re-ordering the elements does not affect the hash code. + /// + /// The type of items in the sequence. + /// The sequence to treat as a set. + /// A hash code for the sequence. + public static int GetSetHashCode(this IEnumerable set) + { + unchecked + { + var hash = 23; + + foreach (var item in set) + { + hash += 13 + item?.GetHashCode() ?? 0; + } + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs b/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs new file mode 100644 index 000000000..0dd8a8758 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs @@ -0,0 +1,10 @@ +using Reqnroll.FeatureSourceGenerator.Gherkin; + +namespace Reqnroll.FeatureSourceGenerator; + +public record FeatureInformation( + GherkinSyntaxTree FeatureSyntax, + string FeatureHintName, + string FeatureNamespace, + CompilationInformation CompilationInformation, + ITestFrameworkHandler TestFrameworkHandler); diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs new file mode 100644 index 000000000..2badad9f7 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs @@ -0,0 +1,24 @@ +using Gherkin.Ast; + +namespace Reqnroll.FeatureSourceGenerator.Gherkin; + +internal class GherkinDocumentComparer : IEqualityComparer +{ + public static GherkinDocumentComparer Default { get; } = new GherkinDocumentComparer(); + + public bool Equals(GherkinDocument x, GherkinDocument y) + { + return false; + } + + public int GetHashCode(GherkinDocument obj) + { + if (obj == null) + { + return 0; + } + + return obj.GetHashCode(); + } +} + diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs new file mode 100644 index 000000000..2a0ac6679 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs @@ -0,0 +1,60 @@ +using Gherkin; +using Gherkin.Ast; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.Gherkin; + +using Location = global::Gherkin.Ast.Location; + +public class GherkinSyntaxParser +{ + public static readonly DiagnosticDescriptor SyntaxError = new( + id: DiagnosticIds.SyntaxError, + title: GherkinSyntaxParserResources.SyntaxErrorTitle, + messageFormat: GherkinSyntaxParserResources.SyntaxErrorMessage, + "Reqnroll.Gherkin", + DiagnosticSeverity.Error, + true); + + public GherkinSyntaxTree Parse(SourceText text, string path, CancellationToken cancellationToken = default) + { + var parser = new Parser { StopAtFirstError = false }; + + GherkinDocument? document = null; + ImmutableArray? diagnostics = null; + + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // CONSIDER: Using a parser that doesn't throw exceptions for syntax errors. + document = parser.Parse(new SourceTokenScanner(text)); + } + catch (CompositeParserException ex) + { + diagnostics = ex.Errors.Select(error => CreateGherkinDiagnostic(error, text, path)).ToImmutableArray(); + } + + return new GherkinSyntaxTree( + document ?? new GherkinDocument(null, []), + diagnostics ?? ImmutableArray.Empty, + path); + } + + private Diagnostic CreateGherkinDiagnostic(ParserException exception, SourceText text, string path) + { + return Diagnostic.Create(SyntaxError, CreateLocation(exception.Location, text, path), exception.Message); + } + + private Microsoft.CodeAnalysis.Location CreateLocation(Location location, SourceText text, string path) + { + var start = text.Lines[location.Line].Start + location.Column; + + return Microsoft.CodeAnalysis.Location.Create( + path, + new TextSpan(start, 0), + new LinePositionSpan( + new LinePosition(location.Line, location.Column), + new LinePosition(location.Line, location.Column))); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParserResources.resx b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParserResources.resx new file mode 100644 index 000000000..1c21ee457 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParserResources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {0} + + + Syntax error in Gherkin document + + \ No newline at end of file diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs new file mode 100644 index 000000000..818e5ef27 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs @@ -0,0 +1,58 @@ +using Gherkin.Ast; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.Gherkin; +public class GherkinSyntaxTree : IEquatable +{ + private readonly GherkinDocument _root; + private readonly ImmutableArray _diagnostics; + + internal GherkinSyntaxTree(GherkinDocument root, ImmutableArray diagnostics, string? path) + { + _root = root; + _diagnostics = diagnostics; + FilePath = path; + } + + public string? FilePath { get; } + + public GherkinSyntaxTree WithPath(string? path) => new(_root, _diagnostics, path); + + public override bool Equals(object obj) => obj is GherkinSyntaxTree tree && Equals(tree); + + public bool Equals(GherkinSyntaxTree other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return _diagnostics.SetEquals(other._diagnostics) && GherkinDocumentComparer.Default.Equals(_root, other._root); + } + + public override int GetHashCode() + { + const int multiplier = 31; + + var hash = 17; + + hash *= multiplier + GherkinDocumentComparer.Default.GetHashCode(_root); + + foreach (var diagnostic in _diagnostics) + { + hash *= multiplier + diagnostic.GetHashCode(); + } + + return hash; + } + + public ImmutableArray GetDiagnostics() => _diagnostics; + + public GherkinDocument GetRoot() => _root; +} + diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/SourceTokenScanner.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/SourceTokenScanner.cs new file mode 100644 index 000000000..100d39d80 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/SourceTokenScanner.cs @@ -0,0 +1,27 @@ +using Gherkin; + +namespace Reqnroll.FeatureSourceGenerator.Gherkin; + +using Location = global::Gherkin.Ast.Location; + +class SourceTokenScanner(SourceText source) : ITokenScanner +{ + private readonly SourceText _source = source; + + private int _lineNumber = -1; + + public Token Read() + { + var lineNumber = ++_lineNumber; + + if (lineNumber >= _source.Lines.Count) + { + return new Token(null, new Location(lineNumber + 1)); + } + + var line = _source.Lines[lineNumber]; + var location = new Location(line.LineNumber + 1); + + return new Token(new GherkinLine(_source.ToString(line.Span), line.LineNumber + 1), location); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs new file mode 100644 index 000000000..846e1ddbf --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs @@ -0,0 +1,12 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public interface ITestFrameworkHandler +{ + string FrameworkName { get; } + + bool CanGenerateLanguage(string language); + + SourceText GenerateTestFixture(FeatureInformation feature); + + bool IsTestFrameworkReferenced(CompilationInformation compilationInformation); +} \ No newline at end of file diff --git a/Reqnroll.FeatureSourceGenerator/IsExternalInit.cs b/Reqnroll.FeatureSourceGenerator/IsExternalInit.cs new file mode 100644 index 000000000..e251a1ef9 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/IsExternalInit.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} + +#endif diff --git a/Reqnroll.FeatureSourceGenerator/KeyValuePairDeconstruct.cs b/Reqnroll.FeatureSourceGenerator/KeyValuePairDeconstruct.cs new file mode 100644 index 000000000..c49ac61df --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/KeyValuePairDeconstruct.cs @@ -0,0 +1,10 @@ +namespace Reqnroll.FeatureSourceGenerator; + +internal static class KeyValuePairDeconstruct +{ + public static void Deconstruct(this KeyValuePair tuple, out T1 key, out T2 value) + { + key = tuple.Key; + value = tuple.Value; + } +} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs new file mode 100644 index 000000000..cac1f83cc --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs @@ -0,0 +1,84 @@ +using Gherkin.Ast; +using Reqnroll.FeatureSourceGenerator.CSharp; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.MSTest; + +internal class MSTestCSharpTestFixtureGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) +{ + const string MSTestNamespace = "Microsoft.VisualStudio.TestTools.UnitTesting"; + + protected override IEnumerable GetTestFixtureAttributes() + { + return base.GetTestFixtureAttributes().Concat( + [ + new AttributeDescriptor( + "TestClass", + MSTestNamespace, + ImmutableArray.Empty, + ImmutableArray>.Empty) + ]); + } + + protected override void AppendTestFixturePreamble() + { + SourceBuilder.AppendLine("// start: MSTest Specific part"); + SourceBuilder.AppendLine(); + + SourceBuilder.AppendLine("private global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext? _testContext;"); + SourceBuilder.AppendLine(); + + SourceBuilder.AppendLine("public virtual global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext? TestContext"); + SourceBuilder.BeginBlock("{"); + + SourceBuilder + .AppendLine("get") + .BeginBlock("{") + .AppendLine("return this._testContext;") + .EndBlock("}"); + + SourceBuilder + .AppendLine("set") + .BeginBlock("{") + .AppendLine("this._testContext = value;") + .EndBlock("}"); + + SourceBuilder.EndBlock("}"); + + SourceBuilder.AppendLine(); + SourceBuilder.AppendLine("// end: MSTest Specific part"); + SourceBuilder.AppendLine(); + + base.AppendTestFixturePreamble(); + } + + protected override IEnumerable GetTestMethodAttributes(Scenario scenario) + { + var attributes = new List + { + new("TestMethod", MSTestNamespace), + new("Description", MSTestNamespace, ImmutableArray.Create(scenario.Name)), + new("TestProperty", MSTestNamespace, ImmutableArray.Create("FeatureTitle", Document.Feature.Name)) + }; + + foreach (var tag in Document.Feature.Tags.Concat(scenario.Tags)) + { + attributes.Add( + new AttributeDescriptor( + "TestCategory", + MSTestNamespace, + ImmutableArray.Create(tag.Name.TrimStart('@')))); + } + + return base.GetTestMethodAttributes(scenario).Concat(attributes); + } + + protected override void AppendScenarioInitializeMethodBody() + { + base.AppendScenarioInitializeMethodBody(); + + SourceBuilder.AppendLine(); + SourceBuilder.AppendLine("// MsTest specific customization:"); + SourceBuilder.AppendLine("testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testContext);"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs new file mode 100644 index 000000000..e196be522 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs @@ -0,0 +1,26 @@ +namespace Reqnroll.FeatureSourceGenerator.MSTest; + +/// +/// The handler for MSTest. +/// +public class MSTestHandler : ITestFrameworkHandler +{ + public string FrameworkName => "MSTest"; + + public bool CanGenerateLanguage(string language) => string.Equals(language, LanguageNames.CSharp, StringComparison.Ordinal); + + public SourceText GenerateTestFixture(FeatureInformation feature) + { + return feature.CompilationInformation.Language switch + { + LanguageNames.CSharp => new MSTestCSharpTestFixtureGeneration(feature).GetSourceText(), + _ => throw new NotSupportedException(), + }; + } + + public bool IsTestFrameworkReferenced(CompilationInformation compilationInformation) + { + return compilationInformation.ReferencedAssemblies + .Any(assembly => assembly.Name == "Microsoft.VisualStudio.TestPlatform.TestFramework"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs new file mode 100644 index 000000000..afe43fd2f --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs @@ -0,0 +1,7 @@ +using Reqnroll.FeatureSourceGenerator.CSharp; + +namespace Reqnroll.FeatureSourceGenerator.NUnit; + +public class NUnitCSharpSyntaxGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) +{ +} diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs new file mode 100644 index 000000000..449ba4f7f --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs @@ -0,0 +1,25 @@ +namespace Reqnroll.FeatureSourceGenerator.NUnit; + +/// +/// The handler for NUnit. +/// +public class NUnitHandler : ITestFrameworkHandler +{ + public string FrameworkName => "NUnit"; + + public bool CanGenerateLanguage(string language) => string.Equals(language, LanguageNames.CSharp, StringComparison.Ordinal); + + public SourceText GenerateTestFixture(FeatureInformation feature) + { + return feature.CompilationInformation.Language switch + { + LanguageNames.CSharp => new NUnitCSharpSyntaxGeneration(feature).GetSourceText(), + _ => throw new NotSupportedException(), + }; + } + + public bool IsTestFrameworkReferenced(CompilationInformation compilationInformation) + { + return compilationInformation.ReferencedAssemblies.Any(assembly => assembly.Name == "nunit.framework"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj new file mode 100644 index 000000000..1614742d2 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj @@ -0,0 +1,63 @@ + + + + netstandard2.0 + 12.0 + true + true + enable + enable + + + + + true + false + $(NoWarn);NU5128 + + + + + + + + + + + all + true + + + + + + + + + + + + + + + true + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + diff --git a/Reqnroll.FeatureSourceGenerator/StepResult.cs b/Reqnroll.FeatureSourceGenerator/StepResult.cs new file mode 100644 index 000000000..07c4644c7 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/StepResult.cs @@ -0,0 +1,82 @@ +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// A "discriminated union" that represents the result of a step in the generator, either the successful +/// output or a diagnostic explaining why the output could not be produced. +/// +/// The type of output from the step. +/// The value from the step, if one was produced. +/// The diagnostic of the step, if one was produced. +internal readonly struct StepResult : IEquatable> +{ + private readonly T? _value; + private readonly Diagnostic? _diagnostic; + + private StepResult(T? value = default, Diagnostic? diagnostic = default) + { + _value = value; + _diagnostic = diagnostic; + } + + public readonly bool IsSuccess => _diagnostic == null; + + public static implicit operator T (StepResult result) + { + if (!result.IsSuccess) + { + throw new InvalidCastException("Cannot cast a non-success result to a value."); + } + + return result._value!; + } + + public static implicit operator Diagnostic (StepResult result) + { + if (result.IsSuccess) + { + throw new InvalidCastException("Cannot cast a success result to a diagnostic."); + } + + return result._diagnostic!; + } + + public static implicit operator StepResult (T value) => new(value); + + public static implicit operator StepResult(Diagnostic diagniostic) => new(diagnostic: diagniostic); + + public override int GetHashCode() => IsSuccess ? _value?.GetHashCode() ?? 0 : _diagnostic!.GetHashCode(); + + public override bool Equals(object obj) + { + if (obj is StepResult other) + { + return Equals(other); + } + + return false; + } + + public override string ToString() => IsSuccess ? _value?.ToString() ?? "null" : _diagnostic!.ToString(); + + public bool Equals(StepResult other) + { + if (IsSuccess != other.IsSuccess) + { + return false; + } + + if (IsSuccess) + { + if (_value is null) + { + return other._value is null; + } + + return _value.Equals(other._value); + } + else + { + return _diagnostic!.Equals(other._diagnostic); + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs new file mode 100644 index 000000000..d3da9054f --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -0,0 +1,213 @@ +using Microsoft.CodeAnalysis; +using Reqnroll.FeatureSourceGenerator.Gherkin; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Defines the basis of a source-generator which processes Gherkin feature files into test fixtures. +/// +public abstract class TestFixtureSourceGenerator(ImmutableArray testFrameworkHandlers) : IIncrementalGenerator +{ + public static readonly DiagnosticDescriptor NoTestFrameworkFound = new( + id: DiagnosticIds.NoTestFrameworkFound, + title: TestFixtureSourceGeneratorResources.NoTestFrameworkFoundTitle, + messageFormat: TestFixtureSourceGeneratorResources.NoTestFrameworkFoundMessage, + "Reqnroll", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor TestFrameworkNotSupported = new( + id: DiagnosticIds.TestFrameworkNotSupported, + title: TestFixtureSourceGeneratorResources.TestFrameworkNotSupportedTitle, + messageFormat: TestFixtureSourceGeneratorResources.TestFrameworkNotSupportedMessage, + "Reqnroll", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private readonly ImmutableArray _testFrameworkHandlers = testFrameworkHandlers; + + protected TestFixtureSourceGenerator() : this( + ImmutableArray.Create( + BuiltInTestFrameworkHandlers.NUnit, + BuiltInTestFrameworkHandlers.MSTest, + BuiltInTestFrameworkHandlers.XUnit)) + { + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Get all feature files in the solution. + var featureFiles = context.AdditionalTextsProvider + .Where(text => text.Path.EndsWith(".feature", StringComparison.OrdinalIgnoreCase)); + + // Extract information about the compilation. + var compilationInformation = context.CompilationProvider + .Select(static (compilation, cancellationToken) => + { + return new CompilationInformation( + AssemblyName: compilation.AssemblyName, + Language: compilation.Language, + ReferencedAssemblies: compilation.ReferencedAssemblyNames.ToImmutableArray()); + }); + + // Find compatible test frameworks and choose a default based on referenced assemblies. + var testFrameworkInformation = compilationInformation + .Select((compilationInfo, cancellationToken) => + { + var compatibleHandlers = _testFrameworkHandlers + .Where(handler => handler.CanGenerateLanguage(compilationInfo.Language)) + .ToImmutableArray(); + + if (!compatibleHandlers.Any()) + { + // This condition should only be possible if Roslyn is compiling a language we have produced a generator for + // without also including a compatible test framework handler; it should never occur in practice. + throw new InvalidOperationException( + $"No test framework handlers are available which can generate {compilationInfo.Language}."); + } + + var availableHandlers = compatibleHandlers + .Where(handler => handler.IsTestFrameworkReferenced(compilationInfo)) + .ToImmutableArray(); + + var defaultHandlers = availableHandlers.FirstOrDefault(); + + return new TestFrameworkInformation( + compatibleHandlers, + availableHandlers, + defaultHandlers); + }); + + // Obtain the options which will influence the generation of features and combine with all other information + // to produce parsed syntax ready for translation into test fixtures. + var featureInformationOrErrors = featureFiles + .Combine(context.AnalyzerConfigOptionsProvider) + .Combine(compilationInformation) + .Combine(testFrameworkInformation) + .SelectMany(static IEnumerable> (input, cancellationToken) => + { + var (((featureFile, optionsProvider), compilationInfo), testFrameworkInformation) = input; + + var options = optionsProvider.GetOptions(featureFile); + + var source = featureFile.GetText(cancellationToken); + + // If there is no source text, we can skip this file completely. + if (source == null) + { + return []; + } + + // Select the test framework from the following sources: + // 1. The reqnroll.target_test_framework value from .editorconfig + // 2. The ReqnrollTargetTestFramework from the build system properties (MSBuild project files or command-line argument) + // 3. The assemblies referenced by the compilation indicating the presence of a test framework. + ITestFrameworkHandler? testFramework; + if (options.TryGetValue("reqnroll.target_test_framework", out var targetTestFrameworkIdentifier) + || options.TryGetValue("build_property.ReqnrollTargetTestFramework", out targetTestFrameworkIdentifier)) + { + // Select the target framework from the option specified. + testFramework = testFrameworkInformation.CompatibleHandlers + .SingleOrDefault(handler => + string.Equals(handler.FrameworkName, targetTestFrameworkIdentifier, StringComparison.OrdinalIgnoreCase)); + + if (testFramework == null) + { + // The properties specified a test framework which is not recognised or not supported for this language. + var frameworkNames = testFrameworkInformation.CompatibleHandlers.Select(framework => framework.FrameworkName); + var frameworks = string.Join(", ", frameworkNames); + + return + [ + Diagnostic.Create( + TestFrameworkNotSupported, + Location.None, + targetTestFrameworkIdentifier, + compilationInfo.Language, + frameworks) + ]; + } + } + else if (testFrameworkInformation.DefaultHandler != null) + { + // Use the default handler. + testFramework = testFrameworkInformation.DefaultHandler; + } + else + { + // Report that no suitable target test framework could be determined. + var frameworkNames = testFrameworkInformation.CompatibleHandlers.Select(framework => framework.FrameworkName); + var frameworks = string.Join(", ", frameworkNames); + + return + [ + Diagnostic.Create( + NoTestFrameworkFound, + Location.None, + frameworks) + ]; + } + + // Determine the namespace of the feature from the project. + if (!optionsProvider.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace)) + { + rootNamespace = compilationInfo.AssemblyName ?? "ReqnrollFeatures"; + } + + var featureHintName = Path.GetFileNameWithoutExtension(featureFile.Path); + var featureNamespace = rootNamespace; + if (options.TryGetValue("build_metadata.AdditionalFiles.RelativeDir", out var relativeDir)) + { + featureNamespace = relativeDir + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + + featureHintName = relativeDir + featureHintName; + } + + // Parse the feature file and output the result. + var parser = new GherkinSyntaxParser(); + + var featureSyntax = parser.Parse(source, featureFile.Path, cancellationToken); + + return + [ + new FeatureInformation( + FeatureSyntax: featureSyntax, + FeatureHintName: featureHintName, + FeatureNamespace: featureNamespace, + CompilationInformation: compilationInfo, + TestFrameworkHandler: testFramework) + ]; + }); + + // Emit source files for each feature by invoking the generator. + context.RegisterSourceOutput(featureInformationOrErrors, static (context, featureOrError) => + { + // If an error, report diagnostic. + if (!featureOrError.IsSuccess) + { + var error = (Diagnostic)featureOrError; + context.ReportDiagnostic(error); + return; + } + + var feature = (FeatureInformation)featureOrError; + + // Report any syntax errors in the parsing of the document. + foreach (var diagnostic in feature.FeatureSyntax.GetDiagnostics()) + { + context.ReportDiagnostic(diagnostic); + } + + // Generate the test fixture source. + var source = feature.TestFrameworkHandler.GenerateTestFixture(feature); + + if (source != null) + { + context.AddSource(feature.FeatureHintName, source); + } + }); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx new file mode 100644 index 000000000..b546a6a4f --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + No compatible test framework is referenced by the project. Add a reference to one of the supported test frameworks: {0} + + + No test framework detected + + + The specified test framework "{0}" is not supported by Reqnroll in {1} projects. Supported frameworks identifiers: {2} + + + Test framework not supported + + \ No newline at end of file diff --git a/Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs b/Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs new file mode 100644 index 000000000..5c49ee248 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +internal record TestFrameworkInformation( + ImmutableArray CompatibleHandlers, + ImmutableArray ReferencedHandlers, + ITestFrameworkHandler? DefaultHandler) +{ + public override int GetHashCode() + { + unchecked + { + var hash = 47; + + hash *= 13 + CompatibleHandlers.GetSetHashCode(); + hash *= 13 + ReferencedHandlers.GetSetHashCode(); + hash *= 13 + DefaultHandler?.GetHashCode() ?? 0; + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs new file mode 100644 index 000000000..626dbc3b5 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -0,0 +1,21 @@ +namespace Reqnroll.FeatureSourceGenerator.XUnit; + +/// +/// The handler for xUnit. +/// +public class XUnitHandler : ITestFrameworkHandler +{ + public string FrameworkName => "xUnit"; + + public bool CanGenerateLanguage(string language) => string.Equals(language, LanguageNames.CSharp, StringComparison.Ordinal); + + public SourceText GenerateTestFixture(FeatureInformation feature) + { + throw new NotImplementedException(); + } + + public bool IsTestFrameworkReferenced(CompilationInformation compilationInformation) + { + return false; + } +} diff --git a/Reqnroll.sln b/Reqnroll.sln index 4828a1c46..f4148dd30 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -112,6 +112,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHubWorkflows", "GitHubWo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnroll.SystemTests", "Tests\Reqnroll.SystemTests\Reqnroll.SystemTests.csproj", "{C658B37D-FD36-4868-9070-4EB452FAE526}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator\Reqnroll.FeatureSourceGenerator.csproj", "{7B10FB2A-8634-48DC-94F8-6E451BBDD4A8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnroll.FeatureSourceGeneratorTests", "Tests\Reqnroll.FeatureSourceGeneratorTests\Reqnroll.FeatureSourceGeneratorTests.csproj", "{31ACFA55-9071-48A3-9C91-EF491A2A1F95}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -234,6 +238,14 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526}.Debug|Any CPU.Build.0 = Debug|Any CPU {C658B37D-FD36-4868-9070-4EB452FAE526}.Release|Any CPU.ActiveCfg = Release|Any CPU {C658B37D-FD36-4868-9070-4EB452FAE526}.Release|Any CPU.Build.0 = Release|Any CPU + {7B10FB2A-8634-48DC-94F8-6E451BBDD4A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B10FB2A-8634-48DC-94F8-6E451BBDD4A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B10FB2A-8634-48DC-94F8-6E451BBDD4A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B10FB2A-8634-48DC-94F8-6E451BBDD4A8}.Release|Any CPU.Build.0 = Release|Any CPU + {31ACFA55-9071-48A3-9C91-EF491A2A1F95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31ACFA55-9071-48A3-9C91-EF491A2A1F95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31ACFA55-9071-48A3-9C91-EF491A2A1F95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31ACFA55-9071-48A3-9C91-EF491A2A1F95}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -270,6 +282,7 @@ Global {C073A609-8A6A-4707-86B0-7BB32EF8ACEE} = {6070E0CF-FA21-4E82-8A22-3B638CA84525} {B6B374C2-ABD8-4F3B-BBF4-505992309168} = {577A0375-1436-446C-802B-3C75C8CEF94F} {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} + {31ACFA55-9071-48A3-9C91-EF491A2A1F95} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/.editorconfig b/Tests/Reqnroll.FeatureSourceGeneratorTests/.editorconfig new file mode 100644 index 000000000..4c0a7ea39 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/.editorconfig @@ -0,0 +1,10 @@ +[*] +insert_final_newline = true +indent_size = 4 +indent_style = space + +[*.cs] +csharp_style_namespace_declarations = file_scoped + +[*.{csproj,targets,props}] +indent_size = 2 diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs new file mode 100644 index 000000000..101849810 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Diagnostics.Contracts; + +namespace Reqnroll.FeatureSourceGenerator; +public static class AssertionExtensions +{ + /// + /// Returns an object that can be used to assert the + /// current . + /// + [Pure] + public static CSharpSyntaxAssertions Should(this TNode? actualValue) where TNode : CSharpSyntaxNode => + new(actualValue); + + /// + /// Returns an object that can be used to assert the + /// current . + /// + [Pure] + public static CSharpClassDeclarationAssertions Should(this ClassDeclarationSyntax? actualValue) => + new(actualValue); + + /// + /// Returns an object that can be used to assert the + /// current . + /// + [Pure] + public static CSharpNamespaceDeclarationAssertions Should(this NamespaceDeclarationSyntax? actualValue) => + new(actualValue); +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs new file mode 100644 index 000000000..c1b25da8c --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs @@ -0,0 +1,82 @@ +using FluentAssertions.Execution; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Reqnroll.FeatureSourceGenerator; +public class CSharpClassDeclarationAssertions(ClassDeclarationSyntax? subject) : + CSharpClassDeclarationAssertions(subject) +{ +} + +public class CSharpClassDeclarationAssertions(ClassDeclarationSyntax? subject) : + CSharpSyntaxAssertions(subject) + where TAssertions : CSharpClassDeclarationAssertions +{ + protected override string Identifier => "class"; + + /// + /// Expects the class declaration have only a single attribute with a specific identifier. + /// + /// + /// The declared type of the attribute. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint HaveSingleAttribute( + string type, + string because = "", + params object[] becauseArgs) + { + var expectation = "Expected {context:class} to have a single attribute " + + $"which is of type \"{type}\" {{reason}}"; + + bool notNull = Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject is not null) + .FailWith(expectation + ", but found ."); + + AttributeSyntax? match = default; + + if (notNull) + { + var attributes = Subject!.AttributeLists.SelectMany(list => list.Attributes).ToList(); + + switch (attributes.Count) + { + case 0: // Fail, Collection is empty + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but the class has no attributes."); + + break; + case 1: // Success Condition + var single = attributes.Single(); + + if (single.Name.ToString() != type) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found the attribute \"{0}\".", single.Name); + } + else + { + match = single; + } + + break; + default: // Fail, Collection contains more than a single item + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", attributes); + + break; + } + } + + return new AndWhichConstraint((TAssertions)this, match!); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpNamespaceDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpNamespaceDeclarationAssertions.cs new file mode 100644 index 000000000..3eb0686f7 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpNamespaceDeclarationAssertions.cs @@ -0,0 +1,90 @@ +using FluentAssertions.Execution; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Reqnroll.FeatureSourceGenerator; + +public class CSharpNamespaceDeclarationAssertions(NamespaceDeclarationSyntax? subject) : + CSharpNamespaceDeclarationAssertions(subject) +{ +} + +public class CSharpNamespaceDeclarationAssertions(NamespaceDeclarationSyntax? subject) : + CSharpSyntaxAssertions(subject) + where TAssertions : CSharpNamespaceDeclarationAssertions +{ + protected override string Identifier => "namespace"; + + /// + /// Expects the namespace contain only a single child which is a class declaration with a specific identifier. + /// + /// + /// The identifier of the class. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint ContainSingleClassDeclaration( + string identifier, + string because = "", + params object[] becauseArgs) + { + var expectation = "Expected {context:namespace} to contain a single child node " + + $"which is the declaration of the class \"{identifier}\" {{reason}}"; + + bool notNull = Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject is not null) + .FailWith(expectation + ", but found ."); + + ClassDeclarationSyntax? match = default; + + if (notNull) + { + var members = Subject!.Members; + + switch (members.Count) + { + case 0: // Fail, Collection is empty + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but the node has no children."); + + break; + case 1: // Success Condition + var single = members.Single(); + + if (single is not ClassDeclarationSyntax declaration) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", Subject); + } + else if (declaration.Identifier.ToString() != identifier) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found the class \"{0}\".", declaration.Identifier.ToString()); + } + else + { + match = declaration; + } + + break; + default: // Fail, Collection contains more than a single item + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", members); + + break; + } + } + + return new AndWhichConstraint((TAssertions)this, match!); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpSyntaxAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpSyntaxAssertions.cs new file mode 100644 index 000000000..9966a682b --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpSyntaxAssertions.cs @@ -0,0 +1,91 @@ +using FluentAssertions.Execution; +using FluentAssertions.Primitives; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Reqnroll.FeatureSourceGenerator; + +public class CSharpSyntaxAssertions(TNode? subject) : CSharpSyntaxAssertions>(subject) + where TNode : CSharpSyntaxNode +{ +} + +public class CSharpSyntaxAssertions(TNode? subject) : ReferenceTypeAssertions(subject!) + where TNode: CSharpSyntaxNode + where TAssertions : CSharpSyntaxAssertions +{ + protected override string Identifier => "node"; + + /// + /// Expects the node contain only a single child which is a namespace declaration with a specific identifier. + /// + /// + /// The identifier of the namespace. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint ContainSingleNamespaceDeclaration( + string identifier, + string because = "", + params object[] becauseArgs) + { + var expectation = "Expected {context:node} to contain a single child node " + + $"which is the declaration of the namespace \"{identifier}\" {{reason}}"; + + bool notNull = Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject is not null) + .FailWith(expectation + ", but found ."); + + NamespaceDeclarationSyntax? match = default; + + if (notNull) + { + var actualChildNodes = Subject!.ChildNodes().Cast().ToList(); + + switch (actualChildNodes.Count) + { + case 0: // Fail, Collection is empty + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but the node has no children."); + + break; + case 1: // Success Condition + var single = actualChildNodes.Single(); + + if (single is not NamespaceDeclarationSyntax ns) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", Subject); + } + else if (ns.Name.ToString() != identifier) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found the namespace \"{0}\".", ns.Name.ToString()); + } + else + { + match = ns; + } + + break; + default: // Fail, Collection contains more than a single item + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", actualChildNodes); + + break; + } + } + + return new AndWhichConstraint((TAssertions)this, match!); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/FakeAnalyzerConfigOptionsProvider.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/FakeAnalyzerConfigOptionsProvider.cs new file mode 100644 index 000000000..a873ccff8 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/FakeAnalyzerConfigOptionsProvider.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Reqnroll.FeatureSourceGenerator; +internal class FakeAnalyzerConfigOptionsProvider(AnalyzerConfigOptions globalOptions) : AnalyzerConfigOptionsProvider +{ + public override AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GlobalOptions; + + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => GlobalOptions; +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/FeatureFile.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/FeatureFile.cs new file mode 100644 index 000000000..a309f21c0 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/FeatureFile.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Reqnroll.FeatureSourceGenerator; + +internal class FeatureFile(string path, string content) : AdditionalText +{ + private readonly SourceText _sourceText = SourceText.From(content); + + public override string Path { get; } = path; + + public override SourceText? GetText(CancellationToken cancellationToken = default) + { + return _sourceText; + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/InMemoryAnalyzerConfigOptions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/InMemoryAnalyzerConfigOptions.cs new file mode 100644 index 000000000..dd2890062 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/InMemoryAnalyzerConfigOptions.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Reqnroll.FeatureSourceGenerator; + +public class InMemoryAnalyzerConfigOptions(IEnumerable> values) : AnalyzerConfigOptions +{ + private readonly ImmutableDictionary _values = values.ToImmutableDictionary(KeyComparer); + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) => _values.TryGetValue(key, out value); +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs new file mode 100644 index 000000000..5ff34d862 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs @@ -0,0 +1,58 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp; + +namespace Reqnroll.FeatureSourceGenerator.MSTest; + +public class MSTestFeatureSourceGeneratorTests +{ + [Fact] + public void GeneratorProducesMSTestOutputWhenWhenBuildPropertyConfiguredForMSTest() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); + + const string featureText = + """ + #language: en + @featureTag1 + Feature: Calculator + + @mytag + Scenario: Add two numbers + Given the first number is 50 + And the second number is 70 + When the two numbers are added + Then the result should be 120 + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { "build_property.ReqnrollTargetTestFramework", "MSTest" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() + .Which.Should().BeAssignableTo().Subject!; + + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); + + var generatedSyntaxRoot = generatedSyntaxTree.GetRoot(); + var generatedNamespace = generatedSyntaxRoot.Should().ContainSingleNamespaceDeclaration("test").Subject; + var generatedClass = generatedNamespace.Should().ContainSingleClassDeclaration("CalculatorFeature").Subject; + generatedClass.Should().HaveSingleAttribute("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass"); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj new file mode 100644 index 000000000..607414c9d --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + Reqnroll.FeatureSourceGenerator + + + + + + + + + + + + + + + + + + + + + From 9731e1a402e6e6bad2c2fd45bcbdea1df22bd9ae Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sat, 27 Apr 2024 15:59:23 +0100 Subject: [PATCH 02/48] Add test for .editorconfig setting --- .../MSTestFeatureSourceGeneratorTests.cs | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs index 5ff34d862..538cb36ac 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs @@ -50,9 +50,57 @@ Then the result should be 120 generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); - var generatedSyntaxRoot = generatedSyntaxTree.GetRoot(); - var generatedNamespace = generatedSyntaxRoot.Should().ContainSingleNamespaceDeclaration("test").Subject; - var generatedClass = generatedNamespace.Should().ContainSingleClassDeclaration("CalculatorFeature").Subject; - generatedClass.Should().HaveSingleAttribute("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass"); + generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") + .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") + .Which.Should().HaveSingleAttribute("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass"); + } + + [Fact] + public void GeneratorProducesMSTestOutputWhenWhenEditorConfigConfiguredForMSTest() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); + + const string featureText = + """ + #language: en + @featureTag1 + Feature: Calculator + + @mytag + Scenario: Add two numbers + Given the first number is 50 + And the second number is 70 + When the two numbers are added + Then the result should be 120 + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { "reqnroll.target_test_framework", "MSTest" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() + .Which.Should().BeAssignableTo().Subject!; + + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); + + generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") + .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") + .Which.Should().HaveSingleAttribute("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass"); } } From 20c4551dce745cbca3832f4a272c861dd40309db Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sat, 27 Apr 2024 16:44:51 +0100 Subject: [PATCH 03/48] Add tests for xUnit generation --- .../AssertionExtensions.cs | 8 ++ .../CSharpClassDeclarationAssertions.cs | 41 ++++++ .../CSharpMethodDeclarationAssertions.cs | 123 ++++++++++++++++++ .../MSTestFeatureSourceGeneratorTests.cs | 3 +- ...eqnroll.FeatureSourceGeneratorTests.csproj | 2 +- .../XUnitFeatureSourceGeneratorTests.cs | 108 +++++++++++++++ 6 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs rename Tests/Reqnroll.FeatureSourceGeneratorTests/{MSTest => }/MSTestFeatureSourceGeneratorTests.cs (98%) create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs index 101849810..099724a30 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs @@ -28,4 +28,12 @@ public static CSharpClassDeclarationAssertions Should(this ClassDeclarationSynta [Pure] public static CSharpNamespaceDeclarationAssertions Should(this NamespaceDeclarationSyntax? actualValue) => new(actualValue); + + /// + /// Returns an object that can be used to assert the + /// current . + /// + [Pure] + public static CSharpMethodDeclarationAssertions Should(this MethodDeclarationSyntax? actualValue) => + new(actualValue); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs index c1b25da8c..af9f13013 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpClassDeclarationAssertions.cs @@ -79,4 +79,45 @@ public AndWhichConstraint HaveSingleAttribute( return new AndWhichConstraint((TAssertions)this, match!); } + + public AndWhichConstraint ContainMethod( + string identifier, + string because = "", + params object[] becauseArgs) + { + var expectation = "Expected {context:class} to have a method " + + $"named \"{identifier}\" {{reason}}"; + + bool notNull = Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject is not null) + .FailWith(expectation + ", but found ."); + + MethodDeclarationSyntax? match = default; + + if (notNull) + { + var methods = Subject!.Members.OfType().ToList(); + + if (methods.Count == 0) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but the class has no methods."); + } + else + { + match = methods.FirstOrDefault(method => method.Identifier.Text == identifier); + + if (match == null) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", methods); + } + } + } + + return new AndWhichConstraint((TAssertions)this, match!); + } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs new file mode 100644 index 000000000..a8d165b8a --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs @@ -0,0 +1,123 @@ +using FluentAssertions.Execution; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Reqnroll.FeatureSourceGenerator; +public class CSharpMethodDeclarationAssertions(MethodDeclarationSyntax? subject) : + CSharpMethodDeclarationAssertions(subject) +{ +} + +public class CSharpMethodDeclarationAssertions(MethodDeclarationSyntax? subject) : + CSharpSyntaxAssertions(subject) + where TAssertions : CSharpMethodDeclarationAssertions +{ + protected override string Identifier => "method"; + + /// + /// Expects the class declaration have only a single attribute with a specific identifier. + /// + /// + /// The declared type of the attribute. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint HaveSingleAttribute( + string type, + string because = "", + params object[] becauseArgs) + { + var expectation = "Expected {context:method} to have a single attribute " + + $"which is of type \"{type}\" {{reason}}"; + + bool notNull = Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject is not null) + .FailWith(expectation + ", but found ."); + + AttributeSyntax? match = default; + + if (notNull) + { + var attributes = Subject!.AttributeLists.SelectMany(list => list.Attributes).ToList(); + + switch (attributes.Count) + { + case 0: // Fail, Collection is empty + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but the class has no attributes."); + + break; + case 1: // Success Condition + var single = attributes.Single(); + + if (single.Name.ToString() != type) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found the attribute \"{0}\".", single.Name); + } + else + { + match = single; + } + + break; + default: // Fail, Collection contains more than a single item + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", attributes); + + break; + } + } + + return new AndWhichConstraint((TAssertions)this, match!); + } + + public AndWhichConstraint HaveAttribute( + string type, + string because = "", + params object[] becauseArgs) + { + var expectation = "Expected {context:method} to have an attribute " + + $"of type \"{type}\" {{reason}}"; + + bool notNull = Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(Subject is not null) + .FailWith(expectation + ", but found ."); + + AttributeSyntax? match = default; + + if (notNull) + { + var attributes = Subject!.AttributeLists.SelectMany(list => list.Attributes).ToList(); + + if (attributes.Count == 0) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but the method has no attributes."); + } + else + { + match = attributes.FirstOrDefault(attribute => attribute.Name.ToString() == type); + + if (match == null) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith(expectation + ", but found {0}.", attributes); + } + } + } + + return new AndWhichConstraint((TAssertions)this, match!); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs similarity index 98% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs index 538cb36ac..3e1e852ad 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs @@ -1,8 +1,9 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Reqnroll.FeatureSourceGenerator; using Reqnroll.FeatureSourceGenerator.CSharp; -namespace Reqnroll.FeatureSourceGenerator.MSTest; +namespace Reqnroll.FeatureSourceGenerator; public class MSTestFeatureSourceGeneratorTests { diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj index 607414c9d..c65caa37e 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj @@ -20,7 +20,7 @@ - + diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs new file mode 100644 index 000000000..fe7bf7f4b --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs @@ -0,0 +1,108 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp; + +namespace Reqnroll.FeatureSourceGenerator; + +public class XUnitFeatureSourceGeneratorTests +{ + [Fact] + public void GeneratorProducesXUnitOutputWhenWhenBuildPropertyConfiguredForXUnit() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); + + const string featureText = + """ + #language: en + @featureTag1 + Feature: Calculator + + @mytag + Scenario: Add two numbers + Given the first number is 50 + And the second number is 70 + When the two numbers are added + Then the result should be 120 + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { "build_property.ReqnrollTargetTestFramework", "xunit" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() + .Which.Should().BeAssignableTo().Subject!; + + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); + + generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") + .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") + .Which.Should().ContainMethod("AddTwoNumbers") + .Which.Should().HaveAttribute("global:XUnit.Fact"); + } + + [Fact] + public void GeneratorProducesMSTestOutputWhenWhenEditorConfigConfiguredForMSTest() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); + + const string featureText = + """ + #language: en + @featureTag1 + Feature: Calculator + + @mytag + Scenario: Add two numbers + Given the first number is 50 + And the second number is 70 + When the two numbers are added + Then the result should be 120 + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { "reqnroll.target_test_framework", "xunit" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() + .Which.Should().BeAssignableTo().Subject!; + + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); + + generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") + .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") + .Which.Should().ContainMethod("AddTwoNumbers") + .Which.Should().HaveAttribute("global:XUnit.Fact"); + } +} From 675e969ab38598930eb744f20e4b825e34104431 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Mon, 29 Apr 2024 21:35:47 +0100 Subject: [PATCH 04/48] Add initial XUnit syntax generation --- .../CSharp/CSharpTestFixtureGeneration.cs | 66 +++++++++++++--- .../XUnit/XUnitCSharpSyntaxGeneration.cs | 77 +++++++++++++++++++ .../XUnit/XUnitHandler.cs | 9 ++- .../CSharpMethodDeclarationAssertions.cs | 4 +- .../XUnitFeatureSourceGeneratorTests.cs | 10 +-- 5 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs index 10539a557..4fd8a2175 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -37,6 +37,12 @@ internal SourceText GetSourceText() return SourceBuilder.ToSourceText(); } + protected virtual string GetClassName() => CSharpSyntax.CreateIdentifier(Document.Feature.Name + Document.Feature.Keyword); + + protected virtual string? GetBaseType() => null; + + protected virtual IEnumerable GetInterfaces() => []; + protected virtual void AppendTestFixtureClass() { var attributes = GetTestFixtureAttributes(); @@ -47,9 +53,41 @@ protected virtual void AppendTestFixtureClass() } var feature = Document.Feature; - var className = CSharpSyntax.CreateIdentifier(feature.Name + feature.Keyword); + var className = GetClassName(); + var baseType = GetBaseType(); + var interfaces = GetInterfaces().ToList(); - SourceBuilder.Append("public class ").Append(className).AppendLine(); + SourceBuilder.Append("public class ").Append(className); + + if (baseType != null || interfaces.Count > 0) + { + var baseTypes = new List(); + + if (baseType != null) + { + baseTypes.Add(baseType); + } + + baseTypes.AddRange(interfaces); + + SourceBuilder.Append(" : "); + + var first = true; + + foreach (var value in baseTypes) + { + if (first) + { + first = false; + } + else + { + SourceBuilder.Append(", "); + } + + SourceBuilder.Append(value); + } + } SourceBuilder.BeginBlock("{"); @@ -132,7 +170,7 @@ protected virtual void AppendTestFixturePreamble() .AppendLine(); SourceBuilder - .Append("private static readonly global::Reqnroll.FeatureInfo featureInfo = new global::Reqnroll.FeatureInfo(") + .Append("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") .Append("new global::System.Globalization.CultureInfo(\"").Append(feature.Language).Append("\"), ") .AppendConstant(Path.GetDirectoryName(FeatureInformation.FeatureSyntax.FilePath).Replace("\\", "\\\\")).Append(", ") .AppendConstant(feature.Name).Append(", ") @@ -163,8 +201,8 @@ protected virtual void AppendScenarioInitializeMethodBody() { SourceBuilder.AppendLine("// handle feature initialization"); SourceBuilder.AppendLine( - "if (testRunner.FeatureContext == null || !object.ReferenceEquals(testRunner.FeatureContext.FeatureInfo, featureInfo))"); - SourceBuilder.AppendLine("await testRunner.OnFeatureStartAsync(featureInfo);"); + "if (testRunner.FeatureContext == null || !object.ReferenceEquals(testRunner.FeatureContext.FeatureInfo, FeatureInfo))"); + SourceBuilder.AppendLine("await testRunner.OnFeatureStartAsync(FeatureInfo);"); SourceBuilder.AppendLine(); SourceBuilder.AppendLine("// handle scenario initialization"); @@ -190,7 +228,7 @@ protected virtual void AppendTestMethodForScenario(Scenario scenario) protected virtual void AppendTestMethodBodyForScenario(Scenario scenario) { - AppendTestRunnerLookupForScenario(); + AppendTestRunnerLookupForScenario(scenario); SourceBuilder.AppendLine(); AppendScenarioInfo(scenario); @@ -291,11 +329,21 @@ protected virtual void AppendScenarioInfo(Scenario scenario) SourceBuilder.AppendLine("// end: calculate ScenarioInfo"); } - protected virtual void AppendTestRunnerLookupForScenario() + /// + /// Appends the code to provide the test runner instance for the scenario execution. + /// + /// The scenario to append code for. + /// + /// Implementations of this method must append code to achieve the following: + /// + /// Declare a local variable named testRunner of a type which can be assigned to type Reqnroll.TestRunner. + /// Assign to the testRunner variable the instance to use as the test runner for the scenario. + /// + /// + protected virtual void AppendTestRunnerLookupForScenario(Scenario scenario) { SourceBuilder.AppendLine("// getting test runner"); - SourceBuilder.AppendLine("string testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString(); " + - "// this might be different with other test runners"); + SourceBuilder.AppendLine("var testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString();"); SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); } diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs new file mode 100644 index 000000000..d044dc63e --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs @@ -0,0 +1,77 @@ +using Gherkin.Ast; +using Reqnroll.FeatureSourceGenerator.CSharp; + +namespace Reqnroll.FeatureSourceGenerator.XUnit; +public class XUnitCSharpSyntaxGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) +{ + const string XUnitNamespace = "Xunit"; + + protected override IEnumerable GetTestMethodAttributes(Scenario scenario) + { + var attributes = new List + { + new("Fact", XUnitNamespace) + }; + + return base.GetTestMethodAttributes(scenario).Concat(attributes); + } + + protected override IEnumerable GetInterfaces() => + base.GetInterfaces().Concat( + [ + $"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>", + "global::Xunit.IAsyncLifetime" + ]); + + protected override void AppendTestFixturePreamble() + { + AppendLifetimeClass(); + + AppendConstructor(); + + base.AppendTestFixturePreamble(); + } + + protected virtual void AppendConstructor() + { + // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. + SourceBuilder.AppendLine($"public {GetClassName()}(Lifetime lifetime)"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.AppendLine("Lifetime = lifetime"); + SourceBuilder.EndBlock("}"); + } + + protected virtual void AppendLifetimeClass() + { + // This class represents the feature lifetime in the xUnit framework. + SourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); + SourceBuilder.BeginBlock("{"); + + SourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); + + SourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); + SourceBuilder.BeginBlock("{"); + // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. + SourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); + SourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); + SourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); + SourceBuilder.EndBlock("}"); + + SourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); + SourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); + SourceBuilder.BeginBlock("TestRunner = null;"); + SourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); + SourceBuilder.EndBlock("}"); + + SourceBuilder.EndBlock("}"); + SourceBuilder.AppendLine(); + } + + protected override void AppendTestRunnerLookupForScenario(Scenario scenario) + { + // For xUnit test runners are scoped to the whole feature execution lifetime + SourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs index 626dbc3b5..53d35746b 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -11,11 +11,16 @@ public class XUnitHandler : ITestFrameworkHandler public SourceText GenerateTestFixture(FeatureInformation feature) { - throw new NotImplementedException(); + return feature.CompilationInformation.Language switch + { + LanguageNames.CSharp => new XUnitCSharpSyntaxGeneration(feature).GetSourceText(), + _ => throw new NotSupportedException(), + }; } public bool IsTestFrameworkReferenced(CompilationInformation compilationInformation) { - return false; + return compilationInformation.ReferencedAssemblies + .Any(assembly => assembly.Name == "xunit.core"); } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs index a8d165b8a..0eb5336ab 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs @@ -32,7 +32,7 @@ public AndWhichConstraint HaveSingleAttribute( params object[] becauseArgs) { var expectation = "Expected {context:method} to have a single attribute " + - $"which is of type \"{type}\" {{reason}}"; + $"which is of type \"{type}\"{{reason}}"; bool notNull = Execute.Assertion .BecauseOf(because, becauseArgs) @@ -86,7 +86,7 @@ public AndWhichConstraint HaveAttribute( params object[] becauseArgs) { var expectation = "Expected {context:method} to have an attribute " + - $"of type \"{type}\" {{reason}}"; + $"of type \"{type}\"{{reason}}"; bool notNull = Execute.Assertion .BecauseOf(because, becauseArgs) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs index fe7bf7f4b..639798aca 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs @@ -15,7 +15,7 @@ public void GeneratorProducesXUnitOutputWhenWhenBuildPropertyConfiguredForXUnit( var compilation = CSharpCompilation.Create("test", references: references); - var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.XUnit]); const string featureText = """ @@ -53,11 +53,11 @@ Then the result should be 120 generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().ContainMethod("AddTwoNumbers") - .Which.Should().HaveAttribute("global:XUnit.Fact"); + .Which.Should().HaveAttribute("global::XUnit.Fact"); } [Fact] - public void GeneratorProducesMSTestOutputWhenWhenEditorConfigConfiguredForMSTest() + public void GeneratorProducesXUnitOutputWhenWhenEditorConfigConfiguredForXUnit() { var references = AppDomain.CurrentDomain.GetAssemblies() .Where(asm => !asm.IsDynamic) @@ -65,7 +65,7 @@ public void GeneratorProducesMSTestOutputWhenWhenEditorConfigConfiguredForMSTest var compilation = CSharpCompilation.Create("test", references: references); - var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.XUnit]); const string featureText = """ @@ -103,6 +103,6 @@ Then the result should be 120 generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().ContainMethod("AddTwoNumbers") - .Which.Should().HaveAttribute("global:XUnit.Fact"); + .Which.Should().HaveAttribute("global::XUnit.Fact"); } } From e045f05f29c0b274542df605501e5a08a26f1e62 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Thu, 2 May 2024 22:56:32 +0100 Subject: [PATCH 05/48] Add feature lifecycle to MSTest and xunit generator --- .../CSharp/CSharpTestFixtureGeneration.cs | 7 --- .../MSTestCSharpTestFixtureGeneration.cs | 47 +++++++++++-------- .../XUnit/XUnitCSharpSyntaxGeneration.cs | 10 ++-- .../XUnitFeatureSourceGeneratorTests.cs | 4 +- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs index 4fd8a2175..188e65bc2 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -199,13 +199,6 @@ private void AppendScenarioInitializeMethod() protected virtual void AppendScenarioInitializeMethodBody() { - SourceBuilder.AppendLine("// handle feature initialization"); - SourceBuilder.AppendLine( - "if (testRunner.FeatureContext == null || !object.ReferenceEquals(testRunner.FeatureContext.FeatureInfo, FeatureInfo))"); - SourceBuilder.AppendLine("await testRunner.OnFeatureStartAsync(FeatureInfo);"); - SourceBuilder.AppendLine(); - - SourceBuilder.AppendLine("// handle scenario initialization"); SourceBuilder.AppendLine("testRunner.OnScenarioInitialize(scenarioInfo);"); } diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs index cac1f83cc..87340eae4 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs @@ -25,33 +25,42 @@ protected override void AppendTestFixturePreamble() SourceBuilder.AppendLine("// start: MSTest Specific part"); SourceBuilder.AppendLine(); - SourceBuilder.AppendLine("private global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext? _testContext;"); + SourceBuilder.AppendLine("public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext { get; set; }"); SourceBuilder.AppendLine(); - SourceBuilder.AppendLine("public virtual global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext? TestContext"); - SourceBuilder.BeginBlock("{"); - - SourceBuilder - .AppendLine("get") - .BeginBlock("{") - .AppendLine("return this._testContext;") - .EndBlock("}"); - - SourceBuilder - .AppendLine("set") - .BeginBlock("{") - .AppendLine("this._testContext = value;") - .EndBlock("}"); - - SourceBuilder.EndBlock("}"); + AppendClassInitializeMethod(); + SourceBuilder.AppendLine(); + AppendClassCleanupMethod(); SourceBuilder.AppendLine(); + SourceBuilder.AppendLine("// end: MSTest Specific part"); - SourceBuilder.AppendLine(); base.AppendTestFixturePreamble(); } + protected virtual void AppendClassInitializeMethod() + { + SourceBuilder.AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitialize]"); + SourceBuilder.AppendLine("public static Task IntializeFeatureAsync(TestContext testContext)"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.AppendLine("var testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString();"); + SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); + SourceBuilder.AppendLine("return testRunner.OnFeatureStartAsync(featureInfo);"); + SourceBuilder.EndBlock("}"); + } + + protected virtual void AppendClassCleanupMethod() + { + SourceBuilder.AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanup]"); + SourceBuilder.AppendLine("public static Task TeardownFeatureAsync()"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.AppendLine("var testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString();"); + SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); + SourceBuilder.AppendLine("return testRunner.OnFeatureEndAsync();"); + SourceBuilder.EndBlock("}"); + } + protected override IEnumerable GetTestMethodAttributes(Scenario scenario) { var attributes = new List @@ -79,6 +88,6 @@ protected override void AppendScenarioInitializeMethodBody() SourceBuilder.AppendLine(); SourceBuilder.AppendLine("// MsTest specific customization:"); - SourceBuilder.AppendLine("testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testContext);"); + SourceBuilder.AppendLine("testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(TestContext);"); } } diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs index d044dc63e..cb33937a3 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs @@ -17,11 +17,7 @@ protected override IEnumerable GetTestMethodAttributes(Scen } protected override IEnumerable GetInterfaces() => - base.GetInterfaces().Concat( - [ - $"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>", - "global::Xunit.IAsyncLifetime" - ]); + base.GetInterfaces().Concat([ $"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>" ]); protected override void AppendTestFixturePreamble() { @@ -37,7 +33,7 @@ protected virtual void AppendConstructor() // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. SourceBuilder.AppendLine($"public {GetClassName()}(Lifetime lifetime)"); SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("Lifetime = lifetime"); + SourceBuilder.AppendLine("Lifetime = lifetime;"); SourceBuilder.EndBlock("}"); } @@ -72,6 +68,6 @@ protected virtual void AppendLifetimeClass() protected override void AppendTestRunnerLookupForScenario(Scenario scenario) { // For xUnit test runners are scoped to the whole feature execution lifetime - SourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner"); + SourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner;"); } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs index 639798aca..edb6a7c0c 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs @@ -53,7 +53,7 @@ Then the result should be 120 generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().ContainMethod("AddTwoNumbers") - .Which.Should().HaveAttribute("global::XUnit.Fact"); + .Which.Should().HaveAttribute("global::Xunit.Fact"); } [Fact] @@ -103,6 +103,6 @@ Then the result should be 120 generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().ContainMethod("AddTwoNumbers") - .Which.Should().HaveAttribute("global::XUnit.Fact"); + .Which.Should().HaveAttribute("global::Xunit.Fact"); } } From 1be4d4a057c529055db63921e3550c6730c20438 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 5 May 2024 14:19:43 +0100 Subject: [PATCH 06/48] Add Rosyln flavor to specifications --- .../Factories/ProjectBuilderFactory.cs | 16 ++++++++++++++-- .../ProjectBuilder.cs | 5 ++++- .../SourceGeneratorPlatform.cs | 8 ++++++++ .../TestRunConfiguration.cs | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/SourceGeneratorPlatform.cs diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs index 500ac8098..becdf2c1c 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs @@ -11,6 +11,7 @@ public class ProjectBuilderFactory protected readonly FeatureFileGenerator _featureFileGenerator; protected readonly Folders _folders; protected readonly TargetFrameworkMonikerStringBuilder _targetFrameworkMonikerStringBuilder; + protected readonly SourceGeneratorPlatform _sourceGenerator; protected readonly BindingsGeneratorFactory _bindingsGeneratorFactory; protected readonly ConfigurationGeneratorFactory _configurationGeneratorFactory; protected readonly CurrentVersionDriver _currentVersionDriver; @@ -25,7 +26,8 @@ public ProjectBuilderFactory( BindingsGeneratorFactory bindingsGeneratorFactory, FeatureFileGenerator featureFileGenerator, Folders folders, - TargetFrameworkMonikerStringBuilder targetFrameworkMonikerStringBuilder) + TargetFrameworkMonikerStringBuilder targetFrameworkMonikerStringBuilder, + SourceGeneratorPlatform sourceGenerator) { _testProjectFolders = testProjectFolders; _testRunConfiguration = testRunConfiguration; @@ -35,6 +37,7 @@ public ProjectBuilderFactory( _featureFileGenerator = featureFileGenerator; _folders = folders; _targetFrameworkMonikerStringBuilder = targetFrameworkMonikerStringBuilder; + _sourceGenerator = sourceGenerator; } public ProjectBuilder CreateProject(string language) @@ -112,7 +115,16 @@ protected virtual ProjectBuilder CreateProjectBuilder() { var configuration = new Configuration(); - return new ProjectBuilder(_testProjectFolders, _featureFileGenerator, _bindingsGeneratorFactory, _configurationGeneratorFactory, configuration, _currentVersionDriver, _folders, _targetFrameworkMonikerStringBuilder); + return new ProjectBuilder( + _testProjectFolders, + _featureFileGenerator, + _bindingsGeneratorFactory, + _configurationGeneratorFactory, + configuration, + _currentVersionDriver, + _folders, + _targetFrameworkMonikerStringBuilder, + _sourceGenerator); } } } diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs index ef8329902..52b59e9e8 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs @@ -37,7 +37,8 @@ public ProjectBuilder( Configuration configuration, CurrentVersionDriver currentVersionDriver, Folders folders, - TargetFrameworkMonikerStringBuilder targetFrameworkMonikerStringBuilder) + TargetFrameworkMonikerStringBuilder targetFrameworkMonikerStringBuilder, + SourceGeneratorPlatform sourceGenerator) { _testProjectFolders = testProjectFolders; _featureFileGenerator = featureFileGenerator; @@ -47,12 +48,14 @@ public ProjectBuilder( _currentVersionDriver = currentVersionDriver; _folders = folders; _targetFrameworkMonikerStringBuilder = targetFrameworkMonikerStringBuilder; + SourceGenerator = sourceGenerator; var projectGuidString = $"{ProjectGuid:N}".Substring(24); ProjectName = $"TestProj_{projectGuidString}"; } public Guid ProjectGuid { get; } = Guid.NewGuid(); public Configuration Configuration { get; } + public SourceGeneratorPlatform SourceGenerator { get; } public string ProjectName { get; set; } public ProgrammingLanguage Language { get; set; } = ProgrammingLanguage.CSharp; public TargetFramework TargetFramework { get; set; } = TargetFramework.Netcoreapp31; diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/SourceGeneratorPlatform.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/SourceGeneratorPlatform.cs new file mode 100644 index 000000000..3366accab --- /dev/null +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/SourceGeneratorPlatform.cs @@ -0,0 +1,8 @@ +namespace Reqnroll.TestProjectGenerator +{ + public enum SourceGeneratorPlatform + { + MSBuild, + Roslyn + } +} \ No newline at end of file diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/TestRunConfiguration.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/TestRunConfiguration.cs index f90f76e5a..0e24687d3 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/TestRunConfiguration.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/TestRunConfiguration.cs @@ -13,7 +13,7 @@ public class TestRunConfiguration public string ReqnrollVersion { get; set; } public string ReqnrollNuGetVersion { get; set; } public ConfigurationFormat ConfigurationFormat { get; set; } - + public SourceGeneratorPlatform SourceGenerator { get; set; } public string ReqnrollAllowedNuGetVersion { get; set; } } } \ No newline at end of file From 35e2857f5081eb32c34b03ac334785fc55c3c4a8 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 5 May 2024 15:04:13 +0100 Subject: [PATCH 07/48] Add on/off switch for MSBuild generation. --- .../build/Reqnroll.Tools.MsBuild.Generation.props | 4 ++-- .../Reqnroll.Tools.MsBuild.Generation.targets | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.props b/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.props index b8b91dca9..c8ddb3fb6 100644 --- a/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.props +++ b/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.props @@ -1,11 +1,11 @@ + true $(MSBuildThisFileDirectory)CPS\Buildsystem\CpsExtension.DesignTime.targets - - + false diff --git a/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.targets b/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.targets index ffcd60e86..e0f2346f3 100644 --- a/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.targets +++ b/Reqnroll.Tools.MsBuild.Generation/build/Reqnroll.Tools.MsBuild.Generation.targets @@ -41,7 +41,7 @@ - + + Condition="'$(Reqnroll_EnableWarnForFeatureCodeBehindFilesWithoutCorrespondingFeatureFile)' == 'true' AND '$(ReqnrollEnableMSBuildGeneration)' == 'true'"> @@ -66,7 +66,8 @@ + DependsOnTargets="BeforeUpdateFeatureFilesInProject" + Condition="'$(ReqnrollEnableMSBuildGeneration)' == 'true'"> @@ -139,11 +140,11 @@ - + - + @@ -156,11 +157,11 @@ - + - + From 435667de279400aa9dd31310695e750747b9e4c8 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Thu, 9 May 2024 23:47:44 +0100 Subject: [PATCH 08/48] Integrate FeatureSourceGenerator with SystemTests project --- .../Reqnroll.FeatureSourceGenerator.csproj | 7 ++-- .../Reqnroll.FeatureSourceGenerator.props | 5 +++ .../Data/NuGetPackage.cs | 17 ++++++++- .../Data/Project.cs | 13 +++++-- .../Factories/ProjectBuilderFactory.cs | 7 ++-- .../NewFormatProjectWriter.cs | 6 ++++ .../PackagesConfigGenerator.cs | 5 +++ .../ProjectBuilder.cs | 35 +++++++++++++++---- .../Generation/MsTestRoslynGenerationTest.cs | 16 +++++++++ .../Generation/NUnitRoslynGenerationTest.cs | 16 +++++++++ .../Generation/XUnitRoslynGenerationTest.cs | 16 +++++++++ .../NuGetPackageVersion.cs | 2 +- .../Reqnroll.SystemTests.csproj | 3 ++ 13 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props create mode 100644 Tests/Reqnroll.SystemTests/Generation/MsTestRoslynGenerationTest.cs create mode 100644 Tests/Reqnroll.SystemTests/Generation/NUnitRoslynGenerationTest.cs create mode 100644 Tests/Reqnroll.SystemTests/Generation/XUnitRoslynGenerationTest.cs diff --git a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj index 1614742d2..f49ede2d0 100644 --- a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj +++ b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj @@ -7,12 +7,15 @@ true enable enable + + true true false + true $(NoWarn);NU5128 @@ -56,8 +59,8 @@ - - + + diff --git a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props new file mode 100644 index 000000000..a32d84f76 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props @@ -0,0 +1,5 @@ + + + false + + diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/NuGetPackage.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/NuGetPackage.cs index 01d8c3460..4c1603137 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/NuGetPackage.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/NuGetPackage.cs @@ -13,17 +13,32 @@ public NuGetPackage(string name, string version, params NuGetPackageAssembly[] a Assemblies = new ReadOnlyCollection(assemblies.Where(a => a != null).ToList()); } - public NuGetPackage(string name, string version, string allowedVersions, params NuGetPackageAssembly[] assemblies) + public NuGetPackage( + string name, + string version, + string allowedVersions = null, + bool isDevelopmentDependency = false, + params NuGetPackageAssembly[] assemblies) { Name = name; Version = version; AllowedVersions = allowedVersions; + IsDevelopmentDependency = isDevelopmentDependency; Assemblies = new ReadOnlyCollection(assemblies.Where(a => a != null).ToList()); } public string Name { get; } public string Version { get; } public string AllowedVersions { get; } + public bool IsDevelopmentDependency { get; } public IReadOnlyList Assemblies { get; } } + + public class PackageReference(string include, string version, string privateAssets = null, string includeAssets = null) + { + public string Include { get; } = include; + public string Version { get; } = version; + public string PrivateAssets { get; } = privateAssets; + public string IncludeAssets { get; } = includeAssets; + } } \ No newline at end of file diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/Project.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/Project.cs index c3576fa57..96c3ac2d2 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/Project.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Data/Project.cs @@ -68,7 +68,8 @@ public void AddNuGetPackage(string name, string version, string allowedVersions) { if (_nuGetPackages.All(n => n.Name != name)) { - _nuGetPackages.Add(new NuGetPackage(name, version, allowedVersions, KnownAssemblyNames.Get(name, version))); + _nuGetPackages.Add( + new NuGetPackage(name, version, allowedVersions: allowedVersions, assemblies: KnownAssemblyNames.Get(name, version))); } } @@ -80,11 +81,17 @@ public void AddNuGetPackage(string name, string version, params NuGetPackageAsse } } - public void AddNuGetPackage(string name, string version, string allowedVersions, params NuGetPackageAssembly[] assemblies) + public void AddNuGetPackage( + string name, + string version, + string allowedVersions = null, + bool isDevelopmentDependency = false, + params NuGetPackageAssembly[] assemblies) { if (_nuGetPackages.All(n => n.Name != name)) { - _nuGetPackages.Add(new NuGetPackage(name, version, allowedVersions, assemblies)); + _nuGetPackages.Add( + new NuGetPackage(name, version, allowedVersions, isDevelopmentDependency, assemblies)); } } diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs index becdf2c1c..461a6f62c 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/ProjectBuilderFactory.cs @@ -11,7 +11,6 @@ public class ProjectBuilderFactory protected readonly FeatureFileGenerator _featureFileGenerator; protected readonly Folders _folders; protected readonly TargetFrameworkMonikerStringBuilder _targetFrameworkMonikerStringBuilder; - protected readonly SourceGeneratorPlatform _sourceGenerator; protected readonly BindingsGeneratorFactory _bindingsGeneratorFactory; protected readonly ConfigurationGeneratorFactory _configurationGeneratorFactory; protected readonly CurrentVersionDriver _currentVersionDriver; @@ -26,8 +25,7 @@ public ProjectBuilderFactory( BindingsGeneratorFactory bindingsGeneratorFactory, FeatureFileGenerator featureFileGenerator, Folders folders, - TargetFrameworkMonikerStringBuilder targetFrameworkMonikerStringBuilder, - SourceGeneratorPlatform sourceGenerator) + TargetFrameworkMonikerStringBuilder targetFrameworkMonikerStringBuilder) { _testProjectFolders = testProjectFolders; _testRunConfiguration = testRunConfiguration; @@ -37,7 +35,6 @@ public ProjectBuilderFactory( _featureFileGenerator = featureFileGenerator; _folders = folders; _targetFrameworkMonikerStringBuilder = targetFrameworkMonikerStringBuilder; - _sourceGenerator = sourceGenerator; } public ProjectBuilder CreateProject(string language) @@ -124,7 +121,7 @@ protected virtual ProjectBuilder CreateProjectBuilder() _currentVersionDriver, _folders, _targetFrameworkMonikerStringBuilder, - _sourceGenerator); + _testRunConfiguration.SourceGenerator); } } } diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/FilesystemWriter/NewFormatProjectWriter.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/FilesystemWriter/NewFormatProjectWriter.cs index 4ee1c87cc..15e1d0737 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/FilesystemWriter/NewFormatProjectWriter.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/FilesystemWriter/NewFormatProjectWriter.cs @@ -165,6 +165,12 @@ private void WritePackageReference(XmlWriter xw, NuGetPackage nuGetPackage) xw.WriteAttributeString("Version", nuGetPackage.Version); } + if (nuGetPackage.IsDevelopmentDependency) + { + xw.WriteElementString("PrivateAssets", "all"); + xw.WriteElementString("IncludeAssets", "runtime; build; native; contentfiles; analyzers"); + } + xw.WriteEndElement(); } diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/PackagesConfigGenerator.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/PackagesConfigGenerator.cs index d0c4364d4..25f2e5f6f 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/PackagesConfigGenerator.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/PackagesConfigGenerator.cs @@ -38,6 +38,11 @@ public ProjectFile Generate(IEnumerable nuGetPackages, TargetFrame xw.WriteAttributeString("allowedVersions", package.AllowedVersions); } + if (package.IsDevelopmentDependency) + { + xw.WriteAttributeString("developmentDependency", "true"); + } + if (!(tfm is null)) { xw.WriteAttributeString("targetFramework", tfm); diff --git a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs index 52b59e9e8..be489ffa7 100644 --- a/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs +++ b/Reqnroll.TestProjectGenerator/Reqnroll.TestProjectGenerator/ProjectBuilder.cs @@ -242,8 +242,8 @@ private void EnsureProjectExists() // TODO: dei replace this hack with better logic when SpecFlow 3 can be strong name signed _project.AddNuGetPackage("Reqnroll", _currentVersionDriver.ReqnrollNuGetVersion, new NuGetPackageAssembly("Reqnroll", "net462\\Reqnroll.dll")); - _project.AddNuGetPackage("Gherkin", "19.0.3", new NuGetPackageAssembly("Gherkin, Version=19.0.3.0, Culture=neutral, PublicKeyToken=86496cfa5b4a5851", "net45\\Gherkin.dll")); - _project.AddNuGetPackage("Cucumber.CucumberExpressions", "16.0.0", new NuGetPackageAssembly("CucumberExpressions, Version=16.0.0.0, Culture=neutral, PublicKeyToken=86496cfa5b4a5851", "netstandard2.0\\CucumberExpressions.dll")); + //_project.AddNuGetPackage("Gherkin", "19.0.3", new NuGetPackageAssembly("Gherkin, Version=19.0.3.0, Culture=neutral, PublicKeyToken=86496cfa5b4a5851", "net45\\Gherkin.dll")); + //_project.AddNuGetPackage("Cucumber.CucumberExpressions", "16.0.0", new NuGetPackageAssembly("CucumberExpressions, Version=16.0.0.0, Culture=neutral, PublicKeyToken=86496cfa5b4a5851", "netstandard2.0\\CucumberExpressions.dll")); _project.AddNuGetPackage("System.Threading.Tasks.Extensions", "4.5.4", new NuGetPackageAssembly("System.Threading.Tasks.Extensions", "netstandard2.0\\System.Threading.Tasks.Extensions.dll")); _project.AddNuGetPackage("Microsoft.Bcl.AsyncInterfaces", "6.0.0", new NuGetPackageAssembly("Microsoft.Bcl.AsyncInterfaces", "netstandard2.0\\Microsoft.Bcl.AsyncInterfaces.dll")); @@ -261,11 +261,6 @@ private void EnsureProjectExists() break; } - if (IsReqnrollFeatureProject) - { - _project.AddNuGetPackage("Reqnroll.Tools.MsBuild.Generation", _currentVersionDriver.ReqnrollNuGetVersion); - } - switch (Configuration.UnitTestProvider) { case UnitTestProvider.SpecRun: @@ -299,6 +294,11 @@ private void ConfigureNUnit() if (IsReqnrollFeatureProject) { + if (SourceGenerator == SourceGeneratorPlatform.Roslyn) + { + ConfigureFeatureSourceGenerator(); + } + _project.AddNuGetPackage("Reqnroll.NUnit", _currentVersionDriver.ReqnrollNuGetVersion, new NuGetPackageAssembly(GetReqnrollPublicAssemblyName("Reqnroll.NUnit.ReqnrollPlugin.dll"), "net462\\Reqnroll.NUnit.ReqnrollPlugin.dll")); Configuration.Plugins.Add(new ReqnrollPlugin("Reqnroll.NUnit", ReqnrollPluginType.Runtime)); @@ -334,12 +334,28 @@ private void ConfigureXUnit() if (IsReqnrollFeatureProject) { + if (SourceGenerator == SourceGeneratorPlatform.Roslyn) + { + ConfigureFeatureSourceGenerator(); + } + _project.AddNuGetPackage("Reqnroll.xUnit", _currentVersionDriver.ReqnrollNuGetVersion, new NuGetPackageAssembly(GetReqnrollPublicAssemblyName("Reqnroll.xUnit.ReqnrollPlugin.dll"), "net462\\Reqnroll.xUnit.ReqnrollPlugin.dll")); Configuration.Plugins.Add(new ReqnrollPlugin("Reqnroll.xUnit", ReqnrollPluginType.Runtime)); } } + private void ConfigureFeatureSourceGenerator() + { + _project.AddNuGetPackage( + "Reqnroll.FeatureSourceGenerator", + _currentVersionDriver.ReqnrollNuGetVersion, + isDevelopmentDependency: true, + assemblies: new NuGetPackageAssembly( + GetReqnrollPublicAssemblyName("Reqnroll.FeatureSourceGenerator.dll"), + "netstandard2.0\\Reqnroll.FeatureSourceGenerator.dll")); + } + private void ConfigureMSTest() { _project.AddNuGetPackage("MSTest.TestAdapter", MSTestPackageVersion); @@ -355,6 +371,11 @@ private void ConfigureMSTest() if (IsReqnrollFeatureProject) { + if (SourceGenerator == SourceGeneratorPlatform.Roslyn) + { + ConfigureFeatureSourceGenerator(); + } + _project.AddNuGetPackage("Reqnroll.MSTest", _currentVersionDriver.ReqnrollNuGetVersion, new NuGetPackageAssembly(GetReqnrollPublicAssemblyName("Reqnroll.MSTest.ReqnrollPlugin.dll"), "net462\\Reqnroll.MSTest.ReqnrollPlugin.dll")); Configuration.Plugins.Add(new ReqnrollPlugin("Reqnroll.MSTest", ReqnrollPluginType.Runtime)); diff --git a/Tests/Reqnroll.SystemTests/Generation/MsTestRoslynGenerationTest.cs b/Tests/Reqnroll.SystemTests/Generation/MsTestRoslynGenerationTest.cs new file mode 100644 index 000000000..bfcf65ed9 --- /dev/null +++ b/Tests/Reqnroll.SystemTests/Generation/MsTestRoslynGenerationTest.cs @@ -0,0 +1,16 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Reqnroll.TestProjectGenerator; + +namespace Reqnroll.SystemTests.Generation; + +[TestClass] +[TestCategory("MsTest")] +[TestCategory("Roslyn")] +public class MsTestRoslynGenerationTest : MsTestGenerationTest +{ + protected override void TestInitialize() + { + base.TestInitialize(); + _testRunConfiguration.SourceGenerator = SourceGeneratorPlatform.Roslyn; + } +} diff --git a/Tests/Reqnroll.SystemTests/Generation/NUnitRoslynGenerationTest.cs b/Tests/Reqnroll.SystemTests/Generation/NUnitRoslynGenerationTest.cs new file mode 100644 index 000000000..7dc050cfd --- /dev/null +++ b/Tests/Reqnroll.SystemTests/Generation/NUnitRoslynGenerationTest.cs @@ -0,0 +1,16 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Reqnroll.TestProjectGenerator; + +namespace Reqnroll.SystemTests.Generation; + +[TestClass] +[TestCategory("NUnit")] +[TestCategory("Roslyn")] +public class NUnitRoslynGenerationTest : NUnitGenerationTest +{ + protected override void TestInitialize() + { + base.TestInitialize(); + _testRunConfiguration.SourceGenerator = SourceGeneratorPlatform.Roslyn; + } +} diff --git a/Tests/Reqnroll.SystemTests/Generation/XUnitRoslynGenerationTest.cs b/Tests/Reqnroll.SystemTests/Generation/XUnitRoslynGenerationTest.cs new file mode 100644 index 000000000..e1622112a --- /dev/null +++ b/Tests/Reqnroll.SystemTests/Generation/XUnitRoslynGenerationTest.cs @@ -0,0 +1,16 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Reqnroll.TestProjectGenerator; + +namespace Reqnroll.SystemTests.Generation; + +[TestClass] +[TestCategory("xUnit")] +[TestCategory("Roslyn")] +public class XUnitRoslynGenerationTest : XUnitGenerationTest +{ + protected override void TestInitialize() + { + base.TestInitialize(); + _testRunConfiguration.SourceGenerator = SourceGeneratorPlatform.Roslyn; + } +} diff --git a/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs b/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs index 404645feb..c5c4efe8d 100644 --- a/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs +++ b/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs @@ -2,5 +2,5 @@ namespace Reqnroll.SystemTests; public static class NuGetPackageVersion { - public const string Version = "1.0.2-local"; + public const string Version = "1.0.5-local"; } \ No newline at end of file diff --git a/Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj b/Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj index 00b6cf412..34e36377d 100644 --- a/Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj +++ b/Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj @@ -24,6 +24,9 @@ + + false + false From 1dd161ed1ba3739d2b167eca98c3f934cdc2b0a6 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 12 May 2024 08:56:17 +0100 Subject: [PATCH 09/48] Add full MSBuild integration for generator --- .../build/Reqnroll.FeatureSourceGenerator.props | 4 ++++ .../build/Reqnroll.FeatureSourceGenerator.targets | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets diff --git a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props index a32d84f76..b73a6d13e 100644 --- a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props +++ b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props @@ -2,4 +2,8 @@ false + + + + diff --git a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets new file mode 100644 index 000000000..d5be3d539 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets @@ -0,0 +1,7 @@ + + + + + + + From 785e94607860952fd69e5b93b01e67d15d772673 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 12 May 2024 08:56:32 +0100 Subject: [PATCH 10/48] Make generator dependencies private --- .../Reqnroll.FeatureSourceGenerator.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj index f49ede2d0..11031b8d5 100644 --- a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj +++ b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj @@ -42,11 +42,11 @@ - + true - - + + all runtime; build; native; contentfiles; analyzers From 633ae40aaf18352259856138e8ddaa46af4d2bc9 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 12 May 2024 08:59:02 +0100 Subject: [PATCH 11/48] Add target test-framework properties --- .../build/Reqnroll.MsTest.props | 4 ++++ .../build/Reqnroll.NUnit.props | 4 ++++ .../build/Reqnroll.xUnit.props | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/Reqnroll.MsTest.props b/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/Reqnroll.MsTest.props index a4e2112a1..fdd491d83 100644 --- a/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/Reqnroll.MsTest.props +++ b/Plugins/Reqnroll.MSTest.Generator.ReqnrollPlugin/build/Reqnroll.MsTest.props @@ -1,5 +1,9 @@ + + MsTest + + diff --git a/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props b/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props index 0192d6e96..a3d0bf7cc 100644 --- a/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props +++ b/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props @@ -1,5 +1,9 @@ + + NUnit + + diff --git a/Plugins/Reqnroll.xUnit.Generator.ReqnrollPlugin/build/Reqnroll.xUnit.props b/Plugins/Reqnroll.xUnit.Generator.ReqnrollPlugin/build/Reqnroll.xUnit.props index d2d3733b7..72a2fa1fe 100644 --- a/Plugins/Reqnroll.xUnit.Generator.ReqnrollPlugin/build/Reqnroll.xUnit.props +++ b/Plugins/Reqnroll.xUnit.Generator.ReqnrollPlugin/build/Reqnroll.xUnit.props @@ -1,4 +1,8 @@ + + + xunit + From 1588b431f32fb09762ac385e9bd9ada09822de49 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 12 May 2024 08:59:41 +0100 Subject: [PATCH 12/48] Fix ambuiguous syntax generation --- .../MSTest/MSTestCSharpTestFixtureGeneration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs index 87340eae4..6f7913b0e 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs @@ -42,11 +42,11 @@ protected override void AppendTestFixturePreamble() protected virtual void AppendClassInitializeMethod() { SourceBuilder.AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitialize]"); - SourceBuilder.AppendLine("public static Task IntializeFeatureAsync(TestContext testContext)"); + SourceBuilder.AppendLine("public static Task IntializeFeatureAsync(global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)"); SourceBuilder.BeginBlock("{"); SourceBuilder.AppendLine("var testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString();"); SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); - SourceBuilder.AppendLine("return testRunner.OnFeatureStartAsync(featureInfo);"); + SourceBuilder.AppendLine("return testRunner.OnFeatureStartAsync(FeatureInfo);"); SourceBuilder.EndBlock("}"); } From fa5e031a6b8eed24a0800631acac3f4acd2bbd63 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Mon, 13 May 2024 23:13:37 +0100 Subject: [PATCH 13/48] Add language information --- .../CSharp/CSharpLanguageInformation.cs | 8 ++++ .../CSharp/CSharpTestFixtureGeneration.cs | 3 ++ .../CSharpTestFixtureSourceGenerator.cs | 13 ++++++- .../CompilationInformation.cs | 37 +++++++++++++++++-- .../ITestFrameworkHandler.cs | 4 +- .../LanguageInformation.cs | 6 +++ .../MSTest/MSTestHandler.cs | 10 +++-- .../NUnit/NUnitHandler.cs | 10 +++-- .../Reqnroll.FeatureSourceGenerator.csproj | 3 +- .../TestFixtureSourceGenerator.cs | 14 +++++-- .../VisualBasicLanguageInformation.cs | 8 ++++ .../XUnit/XUnitHandler.cs | 10 +++-- ...eqnroll.FeatureSourceGeneratorTests.csproj | 4 ++ 13 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/LanguageInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs new file mode 100644 index 000000000..ec73f6ebe --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs @@ -0,0 +1,8 @@ +using Microsoft.CodeAnalysis.CSharp; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +public class CSharpLanguageInformation(LanguageVersion version) : LanguageInformation(LanguageNames.CSharp) +{ + public LanguageVersion Version { get; } = version; +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs index 188e65bc2..921eaa59b 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -1,4 +1,5 @@ using Gherkin.Ast; +using Microsoft.CodeAnalysis.CSharp; namespace Reqnroll.FeatureSourceGenerator.CSharp; @@ -17,6 +18,8 @@ public abstract class CSharpTestFixtureGeneration(FeatureInformation featureInfo { public FeatureInformation FeatureInformation { get; } = featureInfo; + protected bool SupportsNullable { get; } + private bool IsLineMappingEnabled { get; } = featureInfo.FeatureSyntax.FilePath != null; protected GherkinDocument Document { get; } = featureInfo.FeatureSyntax.GetRoot(); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs index 4b0d53f77..757e2a937 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp; @@ -6,7 +7,7 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; /// A generator of Reqnroll test fixtures for the C# language. /// [Generator(LanguageNames.CSharp)] -public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator +public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator { /// /// Initializes a new instance of the class. @@ -23,4 +24,12 @@ public CSharpTestFixtureSourceGenerator() internal CSharpTestFixtureSourceGenerator(params ITestFrameworkHandler[] handlers) : base(handlers.ToImmutableArray()) { } + + /// + protected override CSharpLanguageInformation GetLanguageInformation(Compilation compilation) + { + var cSharpCompilation = (CSharpCompilation)compilation; + + return new CSharpLanguageInformation(cSharpCompilation.LanguageVersion); + } } diff --git a/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs b/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs index 8333acb05..9069663e7 100644 --- a/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs +++ b/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs @@ -1,12 +1,12 @@ using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator; -public sealed record CompilationInformation( + +public abstract record CompilationInformation( string? AssemblyName, - string Language, ImmutableArray ReferencedAssemblies) { - public bool Equals(CompilationInformation other) + public virtual bool Equals(CompilationInformation other) { if (other is null) { @@ -32,7 +32,7 @@ public override int GetHashCode() foreach (var assembly in ReferencedAssemblies) { - hash = hash * 43 + assembly?.GetHashCode() ?? 0; + hash = hash * 43 + assembly.GetHashCode(); } return hash; @@ -40,3 +40,32 @@ public override int GetHashCode() } } +public record CompilationInformation( + string? AssemblyName, + ImmutableArray ReferencedAssemblies, + TLanguage Language) : CompilationInformation(AssemblyName, ReferencedAssemblies) + where TLanguage : LanguageInformation +{ + public virtual bool Equals(CompilationInformation other) + { + if (!base.Equals(other)) + { + return false; + } + + return Language.Equals(other.Language); + } + + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hash = base.GetHashCode(); + + hash = hash * 43 + Language.GetHashCode(); + + return hash; + } + } +} + diff --git a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs index 846e1ddbf..a804f18ec 100644 --- a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs @@ -4,9 +4,9 @@ public interface ITestFrameworkHandler { string FrameworkName { get; } - bool CanGenerateLanguage(string language); + bool CanGenerateLanguage(LanguageInformation language); SourceText GenerateTestFixture(FeatureInformation feature); bool IsTestFrameworkReferenced(CompilationInformation compilationInformation); -} \ No newline at end of file +} diff --git a/Reqnroll.FeatureSourceGenerator/LanguageInformation.cs b/Reqnroll.FeatureSourceGenerator/LanguageInformation.cs new file mode 100644 index 000000000..876c5bb84 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/LanguageInformation.cs @@ -0,0 +1,6 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public abstract class LanguageInformation(string name) +{ + public string Name { get; } = name; +} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs index e196be522..ee431c315 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator.MSTest; +using Reqnroll.FeatureSourceGenerator.CSharp; + +namespace Reqnroll.FeatureSourceGenerator.MSTest; /// /// The handler for MSTest. @@ -7,13 +9,13 @@ public class MSTestHandler : ITestFrameworkHandler { public string FrameworkName => "MSTest"; - public bool CanGenerateLanguage(string language) => string.Equals(language, LanguageNames.CSharp, StringComparison.Ordinal); + public bool CanGenerateLanguage(LanguageInformation language) => language is CSharpLanguageInformation; public SourceText GenerateTestFixture(FeatureInformation feature) { - return feature.CompilationInformation.Language switch + return feature.CompilationInformation switch { - LanguageNames.CSharp => new MSTestCSharpTestFixtureGeneration(feature).GetSourceText(), + CompilationInformation => new MSTestCSharpTestFixtureGeneration(feature).GetSourceText(), _ => throw new NotSupportedException(), }; } diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs index 449ba4f7f..a1dd0a71c 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator.NUnit; +using Reqnroll.FeatureSourceGenerator.CSharp; + +namespace Reqnroll.FeatureSourceGenerator.NUnit; /// /// The handler for NUnit. @@ -7,13 +9,13 @@ public class NUnitHandler : ITestFrameworkHandler { public string FrameworkName => "NUnit"; - public bool CanGenerateLanguage(string language) => string.Equals(language, LanguageNames.CSharp, StringComparison.Ordinal); + public bool CanGenerateLanguage(LanguageInformation language) => language is CSharpLanguageInformation; public SourceText GenerateTestFixture(FeatureInformation feature) { - return feature.CompilationInformation.Language switch + return feature.CompilationInformation switch { - LanguageNames.CSharp => new NUnitCSharpSyntaxGeneration(feature).GetSourceText(), + CompilationInformation => new NUnitCSharpSyntaxGeneration(feature).GetSourceText(), _ => throw new NotSupportedException(), }; } diff --git a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj index 11031b8d5..1c146af78 100644 --- a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj +++ b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj @@ -45,8 +45,9 @@ true - + + all runtime; build; native; contentfiles; analyzers diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index d3da9054f..26829886f 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -1,4 +1,6 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.VisualBasic; using Reqnroll.FeatureSourceGenerator.Gherkin; using System.Collections.Immutable; @@ -7,7 +9,9 @@ namespace Reqnroll.FeatureSourceGenerator; /// /// Defines the basis of a source-generator which processes Gherkin feature files into test fixtures. /// -public abstract class TestFixtureSourceGenerator(ImmutableArray testFrameworkHandlers) : IIncrementalGenerator +public abstract class TestFixtureSourceGenerator( + ImmutableArray testFrameworkHandlers) : IIncrementalGenerator + where TLanguage : LanguageInformation { public static readonly DiagnosticDescriptor NoTestFrameworkFound = new( id: DiagnosticIds.NoTestFrameworkFound, @@ -43,11 +47,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Extract information about the compilation. var compilationInformation = context.CompilationProvider - .Select(static (compilation, cancellationToken) => + .Select((compilation, cancellationToken) => { - return new CompilationInformation( + return new CompilationInformation( AssemblyName: compilation.AssemblyName, - Language: compilation.Language, + Language: GetLanguageInformation(compilation), ReferencedAssemblies: compilation.ReferencedAssemblyNames.ToImmutableArray()); }); @@ -210,4 +214,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } }); } + + protected abstract TLanguage GetLanguageInformation(Compilation compilation); } diff --git a/Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs b/Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs new file mode 100644 index 000000000..562a7d27b --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs @@ -0,0 +1,8 @@ +using Microsoft.CodeAnalysis.VisualBasic; + +namespace Reqnroll.FeatureSourceGenerator.VisualBasic; + +public class VisualBasicLanguageInformation(LanguageVersion version) : LanguageInformation(LanguageNames.VisualBasic) +{ + public LanguageVersion Version { get; } = version; +} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs index 53d35746b..65df2c1c6 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator.XUnit; +using Reqnroll.FeatureSourceGenerator.CSharp; + +namespace Reqnroll.FeatureSourceGenerator.XUnit; /// /// The handler for xUnit. @@ -7,13 +9,13 @@ public class XUnitHandler : ITestFrameworkHandler { public string FrameworkName => "xUnit"; - public bool CanGenerateLanguage(string language) => string.Equals(language, LanguageNames.CSharp, StringComparison.Ordinal); + public bool CanGenerateLanguage(LanguageInformation language) => language is CSharpLanguageInformation; public SourceText GenerateTestFixture(FeatureInformation feature) { - return feature.CompilationInformation.Language switch + return feature.CompilationInformation switch { - LanguageNames.CSharp => new XUnitCSharpSyntaxGeneration(feature).GetSourceText(), + CompilationInformation => new XUnitCSharpSyntaxGeneration(feature).GetSourceText(), _ => throw new NotSupportedException(), }; } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj index c65caa37e..6e04d4425 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj @@ -11,8 +11,12 @@ + + + + From 5b1320c09246571877050f7c51725df85b82c519 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 14 May 2024 07:33:41 +0100 Subject: [PATCH 14/48] Add cleaner namespace generation --- .../DotNetSyntax.cs | 87 +++++++++++++++++++ .../TestFixtureSourceGenerator.cs | 19 ++-- 2 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/DotNetSyntax.cs diff --git a/Reqnroll.FeatureSourceGenerator/DotNetSyntax.cs b/Reqnroll.FeatureSourceGenerator/DotNetSyntax.cs new file mode 100644 index 000000000..8e4974f93 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/DotNetSyntax.cs @@ -0,0 +1,87 @@ +using System.Globalization; +using System.Text; + +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Provides methods to support generating syntax following common .NET rules and conventions. +/// +internal static class DotNetSyntax +{ + /// + /// Creates an identifier which follows common .NET naming rules and conventions. + /// + /// The input string to use as the basis of the identifier. + /// A string which is safe to use as an identifier across .NET languages. + public static string CreateIdentifier(string s) + { + var sb = new StringBuilder(); + var newWord = true; + + foreach (char c in s) + { + if (char.IsWhiteSpace(c)) + { + newWord = true; + continue; + } + + if (!IsValidInIdentifier(c)) + { + continue; + } + + if (sb.Length == 0 && !IsValidAsFirstCharacterInIdentifier(c)) + { + sb.Append('_'); + sb.Append(c); + continue; + } + + if (newWord) + { + sb.Append(char.ToUpper(c)); + newWord = false; + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + private static bool IsValidAsFirstCharacterInIdentifier(char c) + { + if (c == '_') + { + return true; + } + + var category = char.GetUnicodeCategory(c); + + return category == UnicodeCategory.UppercaseLetter + || category == UnicodeCategory.LowercaseLetter + || category == UnicodeCategory.TitlecaseLetter + || category == UnicodeCategory.ModifierLetter + || category == UnicodeCategory.OtherLetter; + } + + private static bool IsValidInIdentifier(char c) + { + var category = char.GetUnicodeCategory(c); + + return category == UnicodeCategory.UppercaseLetter + || category == UnicodeCategory.LowercaseLetter + || category == UnicodeCategory.TitlecaseLetter + || category == UnicodeCategory.ModifierLetter + || category == UnicodeCategory.OtherLetter + || category == UnicodeCategory.LetterNumber + || category == UnicodeCategory.NonSpacingMark + || category == UnicodeCategory.SpacingCombiningMark + || category == UnicodeCategory.DecimalDigitNumber + || category == UnicodeCategory.ConnectorPunctuation + || category == UnicodeCategory.Format; + } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 26829886f..6dd14f448 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.VisualBasic; +using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.Gherkin; using System.Collections.Immutable; @@ -125,11 +126,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return [ Diagnostic.Create( - TestFrameworkNotSupported, - Location.None, - targetTestFrameworkIdentifier, - compilationInfo.Language, - frameworks) + TestFrameworkNotSupported, + Location.None, + targetTestFrameworkIdentifier, + compilationInfo.Language, + frameworks) ]; } } @@ -163,9 +164,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var featureNamespace = rootNamespace; if (options.TryGetValue("build_metadata.AdditionalFiles.RelativeDir", out var relativeDir)) { - featureNamespace = relativeDir + var featureNamespaceParts = relativeDir .Replace(Path.DirectorySeparatorChar, '.') - .Replace(Path.AltDirectorySeparatorChar, '.'); + .Replace(Path.AltDirectorySeparatorChar, '.') + .Split('.') + .Select(part => DotNetSyntax.CreateIdentifier(part)); + + featureNamespace = string.Join(".", featureNamespaceParts); featureHintName = relativeDir + featureHintName; } From 0f9b461a7c45ea2231361f5b973523be942cfa6d Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 14 May 2024 20:29:50 +0100 Subject: [PATCH 15/48] Add support for generating nullable references --- .../CSharp/CSharpCompilationInformation.cs | 35 +++++++++++++++++++ .../CSharp/CSharpLanguageInformation.cs | 8 ----- .../CSharp/CSharpTestFixtureGeneration.cs | 5 ++- .../CSharpTestFixtureSourceGenerator.cs | 11 +++--- .../CompilationInformation.cs | 30 ---------------- .../ITestFrameworkHandler.cs | 2 +- .../LanguageInformation.cs | 6 ---- .../MSTestCSharpTestFixtureGeneration.cs | 7 +++- .../MSTest/MSTestHandler.cs | 5 +-- .../NUnit/NUnitHandler.cs | 5 +-- .../TestFixtureSourceGenerator.cs | 24 ++++--------- .../TestFixtureSourceGeneratorResources.resx | 2 +- .../VisualBasicLanguageInformation.cs | 8 ----- .../XUnit/XUnitHandler.cs | 5 +-- .../NuGetPackageVersion.cs | 2 +- 15 files changed, 70 insertions(+), 85 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpCompilationInformation.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/LanguageInformation.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpCompilationInformation.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpCompilationInformation.cs new file mode 100644 index 000000000..0e45eb769 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpCompilationInformation.cs @@ -0,0 +1,35 @@ +using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +public record CSharpCompilationInformation( + string? AssemblyName, + ImmutableArray ReferencedAssemblies, + LanguageVersion LanguageVersion, + bool HasNullableReferencesEnabled) : CompilationInformation(AssemblyName, ReferencedAssemblies) +{ + public virtual bool Equals(CSharpCompilationInformation other) + { + if (!base.Equals(other)) + { + return false; + } + + return LanguageVersion.Equals(other.LanguageVersion) && + HasNullableReferencesEnabled.Equals(other.HasNullableReferencesEnabled); + } + + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hash = base.GetHashCode(); + + hash = hash * 43 + LanguageVersion.GetHashCode(); + hash = hash * 43 + HasNullableReferencesEnabled.GetHashCode(); + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs deleted file mode 100644 index ec73f6ebe..000000000 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpLanguageInformation.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp; - -namespace Reqnroll.FeatureSourceGenerator.CSharp; - -public class CSharpLanguageInformation(LanguageVersion version) : LanguageInformation(LanguageNames.CSharp) -{ - public LanguageVersion Version { get; } = version; -} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs index 921eaa59b..b5faf3002 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -18,7 +18,10 @@ public abstract class CSharpTestFixtureGeneration(FeatureInformation featureInfo { public FeatureInformation FeatureInformation { get; } = featureInfo; - protected bool SupportsNullable { get; } + public CSharpCompilationInformation CompilationInformation { get; } = + (CSharpCompilationInformation)featureInfo.CompilationInformation; + + protected bool AreNullableReferenceTypesEnabled => CompilationInformation.HasNullableReferencesEnabled; private bool IsLineMappingEnabled { get; } = featureInfo.FeatureSyntax.FilePath != null; diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs index 757e2a937..9e0337104 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs @@ -7,7 +7,7 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; /// A generator of Reqnroll test fixtures for the C# language. /// [Generator(LanguageNames.CSharp)] -public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator +public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator { /// /// Initializes a new instance of the class. @@ -25,11 +25,14 @@ internal CSharpTestFixtureSourceGenerator(params ITestFrameworkHandler[] handler { } - /// - protected override CSharpLanguageInformation GetLanguageInformation(Compilation compilation) + protected override CompilationInformation GetCompilationInformation(Compilation compilation) { var cSharpCompilation = (CSharpCompilation)compilation; - return new CSharpLanguageInformation(cSharpCompilation.LanguageVersion); + return new CSharpCompilationInformation( + compilation.AssemblyName, + compilation.ReferencedAssemblyNames.ToImmutableArray(), + cSharpCompilation.LanguageVersion, + cSharpCompilation.Options.NullableContextOptions != NullableContextOptions.Disable); } } diff --git a/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs b/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs index 9069663e7..6babe53db 100644 --- a/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs +++ b/Reqnroll.FeatureSourceGenerator/CompilationInformation.cs @@ -39,33 +39,3 @@ public override int GetHashCode() } } } - -public record CompilationInformation( - string? AssemblyName, - ImmutableArray ReferencedAssemblies, - TLanguage Language) : CompilationInformation(AssemblyName, ReferencedAssemblies) - where TLanguage : LanguageInformation -{ - public virtual bool Equals(CompilationInformation other) - { - if (!base.Equals(other)) - { - return false; - } - - return Language.Equals(other.Language); - } - - public override int GetHashCode() - { - unchecked // Overflow is fine, just wrap - { - int hash = base.GetHashCode(); - - hash = hash * 43 + Language.GetHashCode(); - - return hash; - } - } -} - diff --git a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs index a804f18ec..2223a5e22 100644 --- a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs @@ -4,7 +4,7 @@ public interface ITestFrameworkHandler { string FrameworkName { get; } - bool CanGenerateLanguage(LanguageInformation language); + bool CanGenerateForCompilation(CompilationInformation compilationInformation); SourceText GenerateTestFixture(FeatureInformation feature); diff --git a/Reqnroll.FeatureSourceGenerator/LanguageInformation.cs b/Reqnroll.FeatureSourceGenerator/LanguageInformation.cs deleted file mode 100644 index 876c5bb84..000000000 --- a/Reqnroll.FeatureSourceGenerator/LanguageInformation.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Reqnroll.FeatureSourceGenerator; - -public abstract class LanguageInformation(string name) -{ - public string Name { get; } = name; -} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs index 6f7913b0e..23694255d 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs @@ -25,7 +25,12 @@ protected override void AppendTestFixturePreamble() SourceBuilder.AppendLine("// start: MSTest Specific part"); SourceBuilder.AppendLine(); - SourceBuilder.AppendLine("public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext { get; set; }"); + SourceBuilder.Append("public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext"); + if (AreNullableReferenceTypesEnabled) + { + SourceBuilder.Append("?"); + } + SourceBuilder.AppendLine(" TestContext { get; set; }"); SourceBuilder.AppendLine(); AppendClassInitializeMethod(); diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs index ee431c315..b6525a9b8 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs @@ -9,13 +9,14 @@ public class MSTestHandler : ITestFrameworkHandler { public string FrameworkName => "MSTest"; - public bool CanGenerateLanguage(LanguageInformation language) => language is CSharpLanguageInformation; + public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => + compilationInformation is CSharpCompilationInformation; public SourceText GenerateTestFixture(FeatureInformation feature) { return feature.CompilationInformation switch { - CompilationInformation => new MSTestCSharpTestFixtureGeneration(feature).GetSourceText(), + CSharpCompilationInformation => new MSTestCSharpTestFixtureGeneration(feature).GetSourceText(), _ => throw new NotSupportedException(), }; } diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs index a1dd0a71c..c166d9632 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs @@ -9,13 +9,14 @@ public class NUnitHandler : ITestFrameworkHandler { public string FrameworkName => "NUnit"; - public bool CanGenerateLanguage(LanguageInformation language) => language is CSharpLanguageInformation; + public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => + compilationInformation is CSharpCompilationInformation; public SourceText GenerateTestFixture(FeatureInformation feature) { return feature.CompilationInformation switch { - CompilationInformation => new NUnitCSharpSyntaxGeneration(feature).GetSourceText(), + CSharpCompilationInformation => new NUnitCSharpSyntaxGeneration(feature).GetSourceText(), _ => throw new NotSupportedException(), }; } diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 6dd14f448..674fe4931 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -1,8 +1,4 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.VisualBasic; -using Reqnroll.FeatureSourceGenerator.CSharp; -using Reqnroll.FeatureSourceGenerator.Gherkin; +using Reqnroll.FeatureSourceGenerator.Gherkin; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator; @@ -10,9 +6,8 @@ namespace Reqnroll.FeatureSourceGenerator; /// /// Defines the basis of a source-generator which processes Gherkin feature files into test fixtures. /// -public abstract class TestFixtureSourceGenerator( +public abstract class TestFixtureSourceGenerator( ImmutableArray testFrameworkHandlers) : IIncrementalGenerator - where TLanguage : LanguageInformation { public static readonly DiagnosticDescriptor NoTestFrameworkFound = new( id: DiagnosticIds.NoTestFrameworkFound, @@ -48,20 +43,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Extract information about the compilation. var compilationInformation = context.CompilationProvider - .Select((compilation, cancellationToken) => - { - return new CompilationInformation( - AssemblyName: compilation.AssemblyName, - Language: GetLanguageInformation(compilation), - ReferencedAssemblies: compilation.ReferencedAssemblyNames.ToImmutableArray()); - }); + .Select((compilation, _) => GetCompilationInformation(compilation)); // Find compatible test frameworks and choose a default based on referenced assemblies. var testFrameworkInformation = compilationInformation .Select((compilationInfo, cancellationToken) => { var compatibleHandlers = _testFrameworkHandlers - .Where(handler => handler.CanGenerateLanguage(compilationInfo.Language)) + .Where(handler => handler.CanGenerateForCompilation(compilationInfo)) .ToImmutableArray(); if (!compatibleHandlers.Any()) @@ -69,7 +58,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // This condition should only be possible if Roslyn is compiling a language we have produced a generator for // without also including a compatible test framework handler; it should never occur in practice. throw new InvalidOperationException( - $"No test framework handlers are available which can generate {compilationInfo.Language}."); + $"No test framework handlers are available which can generate code for the current compilation."); } var availableHandlers = compatibleHandlers @@ -129,7 +118,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) TestFrameworkNotSupported, Location.None, targetTestFrameworkIdentifier, - compilationInfo.Language, frameworks) ]; } @@ -220,5 +208,5 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); } - protected abstract TLanguage GetLanguageInformation(Compilation compilation); + protected abstract CompilationInformation GetCompilationInformation(Compilation compilation); } diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx index b546a6a4f..0f93cf687 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGeneratorResources.resx @@ -124,7 +124,7 @@ No test framework detected - The specified test framework "{0}" is not supported by Reqnroll in {1} projects. Supported frameworks identifiers: {2} + The specified test framework "{0}" is not supported by the current compilation. Supported test framework identifiers: {1} Test framework not supported diff --git a/Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs b/Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs deleted file mode 100644 index 562a7d27b..000000000 --- a/Reqnroll.FeatureSourceGenerator/VisualBasic/VisualBasicLanguageInformation.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.CodeAnalysis.VisualBasic; - -namespace Reqnroll.FeatureSourceGenerator.VisualBasic; - -public class VisualBasicLanguageInformation(LanguageVersion version) : LanguageInformation(LanguageNames.VisualBasic) -{ - public LanguageVersion Version { get; } = version; -} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs index 65df2c1c6..13f0b1ca1 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -9,13 +9,14 @@ public class XUnitHandler : ITestFrameworkHandler { public string FrameworkName => "xUnit"; - public bool CanGenerateLanguage(LanguageInformation language) => language is CSharpLanguageInformation; + public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => + compilationInformation is CSharpCompilationInformation; public SourceText GenerateTestFixture(FeatureInformation feature) { return feature.CompilationInformation switch { - CompilationInformation => new XUnitCSharpSyntaxGeneration(feature).GetSourceText(), + CSharpCompilationInformation => new XUnitCSharpSyntaxGeneration(feature).GetSourceText(), _ => throw new NotSupportedException(), }; } diff --git a/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs b/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs index c5c4efe8d..404645feb 100644 --- a/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs +++ b/Tests/Reqnroll.SystemTests/NuGetPackageVersion.cs @@ -2,5 +2,5 @@ namespace Reqnroll.SystemTests; public static class NuGetPackageVersion { - public const string Version = "1.0.5-local"; + public const string Version = "1.0.2-local"; } \ No newline at end of file From 46dce379187590c054e2412bf43d0dd478ba2f47 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 14 May 2024 20:43:30 +0100 Subject: [PATCH 16/48] Remove long line from generator --- .../CSharp/CSharpTestFixtureGeneration.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs index b5faf3002..26f8c1bf4 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -170,17 +170,21 @@ protected virtual void AppendTestFixturePreamble() var feature = FeatureInformation.FeatureSyntax.GetRoot().Feature; SourceBuilder - .Append("private static readonly string[] featureTags = new string[] { ") + .Append("private static readonly string[] FeatureTags = new string[] { ") .AppendConstantList(feature.Tags.Select(tag => tag.Name.TrimStart('@'))) .AppendLine(" };") .AppendLine(); SourceBuilder - .Append("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") - .Append("new global::System.Globalization.CultureInfo(\"").Append(feature.Language).Append("\"), ") - .AppendConstant(Path.GetDirectoryName(FeatureInformation.FeatureSyntax.FilePath).Replace("\\", "\\\\")).Append(", ") - .AppendConstant(feature.Name).Append(", ") - .AppendLine("null, global::Reqnroll.ProgrammingLanguage.CSharp, featureTags);") + .AppendLine("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") + .BeginBlock() + .AppendLine("new global::System.Globalization.CultureInfo(").AppendConstant(feature.Language).AppendLine("), ") + .AppendConstant(Path.GetDirectoryName(FeatureInformation.FeatureSyntax.FilePath).Replace("\\", "\\\\")).AppendLine(", ") + .AppendConstant(feature.Name).AppendLine(", ") + .AppendLine("null, ") + .AppendLine("global::Reqnroll.ProgrammingLanguage.CSharp, ") + .AppendLine("FeatureTags);") + .EndBlock() .AppendLine(); AppendScenarioInitializeMethod(); @@ -319,7 +323,7 @@ protected virtual void AppendScenarioInfo(Scenario scenario) "var argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); // needed for scenario outlines"); // TODO: Add support for rules. - SourceBuilder.AppendLine("var inheritedTags = featureTags; // will be more complex if there are rules"); + SourceBuilder.AppendLine("var inheritedTags = FeatureTags; // will be more complex if there are rules"); SourceBuilder .Append("var scenarioInfo = new global::Reqnroll.ScenarioInfo(") From f7080e04ea40ef0a11eac8c9ed587bef82e4ebcd Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Thu, 30 May 2024 00:09:11 +0100 Subject: [PATCH 17/48] Extend attribute descriptor as value-like object --- .../AttributeDescriptor.cs | 389 +++++++++++++++++- .../CSharp/CSharpSyntax.cs | 20 + .../AttributeDescriptorTests.cs | 167 ++++++++ 3 files changed, 556 insertions(+), 20 deletions(-) create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs diff --git a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs index 7dae725e3..fc3139975 100644 --- a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs @@ -1,29 +1,378 @@ -using System.Collections.Immutable; +using System.Collections; +using System.Collections.Immutable; +using System.ComponentModel; namespace Reqnroll.FeatureSourceGenerator; /// -/// Provides a description of a .NET attribute. +/// Provides a description of a .NET attribute instance. /// -/// The name of the attribute's type. -/// The namespace of the attribute's type. -/// The arguments passed to the attribute's constructor. -/// The property values of the attribute. -public record AttributeDescriptor( - string TypeName, - string Namespace, - ImmutableArray Arguments, - ImmutableArray> PropertyValues) +/// The name of the attribute's type. +/// The namespace of the attribute's type. +/// The positional arguments of the attribute. +/// The named arguments of the attribute. +public class AttributeDescriptor( + string typeName, + string ns, + ImmutableArray? positionalArguments = null, + ImmutableDictionary? namedArguments = null) + : IEquatable { - public AttributeDescriptor( - string TypeName, - string Namespace, - ImmutableArray? Arguments = null, - ImmutableArray>? PropertyValues = null) : this( - TypeName, - Namespace, - Arguments ?? ImmutableArray.Empty, - PropertyValues ?? ImmutableArray>.Empty) + public string TypeName { get; } = typeName; + + public string Namespace { get; } = ns; + + public ImmutableArray PositionalArguments { get; } = + ThrowIfArgumentTypesNotValid( + positionalArguments.GetValueOrDefault(ImmutableArray.Empty), + nameof(positionalArguments)); + + public ImmutableDictionary NamedArguments { get; } = + ThrowIfArgumentTypesNotValid(namedArguments ?? ImmutableDictionary.Empty, nameof(namedArguments)); + + private int? _hashCode; + + private static ImmutableArray ThrowIfArgumentTypesNotValid( + ImmutableArray positionalArguments, + string paramName) + { + foreach (var item in positionalArguments) + { + ThrowIfArgumentTypeNotValid(item, paramName); + } + + return positionalArguments; + } + + private static void ThrowIfArgumentTypeNotValid(object? value, string paramName) + { + switch (value) + { + case null: + case bool _: + case byte _: + case char _: + case double _: + case float _: + case int _: + case long _: + case sbyte _: + case short _: + case string _: + case uint _: + case ulong _: + case ushort _: + case Type _: + case Enum _: + break; + + case Array _: + throw new ArgumentException( + "Mutable arrays may not be used with AttributeDescriptor. To represent an array use ImmutableArray instead.", + paramName); + + default: + var valueType = value.GetType(); + if (valueType == typeof(object)) + { + break; + } + + if (IsImmutableArrayType(valueType)) + { + var array = (IEnumerable)value; + foreach(var item in array) + { + ThrowIfArgumentTypeNotValid(item, paramName); + } + + break; + } + + throw new ArgumentException( + $"Instances of type {value.GetType()} cannot be used as attribute arguments.", + paramName); + } + } + + private static ImmutableDictionary ThrowIfArgumentTypesNotValid( + ImmutableDictionary namedArguments, + string paramName) + { + foreach (var item in namedArguments.Values) + { + ThrowIfArgumentTypeNotValid(item, paramName); + } + + return namedArguments; + } + + public override int GetHashCode() + { + _hashCode ??= CalculateHashCode(); + + return _hashCode.Value; + } + + private int CalculateHashCode() + { + unchecked + { + var hash = 47; + + hash *= 13 + TypeName.GetHashCode(); + hash *= 13 + Namespace.GetHashCode(); + + var index = 0; + foreach (var item in PositionalArguments) + { + hash *= 13 + index++ + GetItemHash(item); + } + + foreach(var (name, value) in NamedArguments) + { + hash *= 13 + name.GetHashCode() + GetItemHash(value); + } + + return hash; + } + } + + private int GetItemHash(object? item) + { + if (item is null) + { + return 0; + } + + if (IsImmutableArray(item)) + { + return GetSequenceHash((IEnumerable)item); + } + + return item.GetHashCode(); + } + + private int GetSequenceHash(IEnumerable sequence) + { + unchecked + { + var hash = 127; + var index = 0; + + foreach (var item in sequence) + { + hash *= 13 + index++ + GetItemHash(item); + } + + return hash; + } + } + + private static bool IsImmutableArray(object value) => IsImmutableArrayType(value.GetType()); + + private static bool IsImmutableArrayType(Type type) => + type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ImmutableArray<>); + + public override bool Equals(object obj) => Equals(obj as AttributeDescriptor); + + public virtual bool Equals(AttributeDescriptor? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(other, this)) + { + return true; + } + + if (GetHashCode() != other.GetHashCode()) + { + return false; + } + + return TypeName.Equals(other.TypeName) && + Namespace.Equals(other.Namespace) && + ArgumentsEqual(other); + } + + private bool ArgumentsEqual(AttributeDescriptor other) { + return ArgumentSequenceEqual(PositionalArguments, other.PositionalArguments) && + ArgumentDictionaryEqual(NamedArguments, other.NamedArguments); + } + + private static bool ArgumentDictionaryEqual( + ImmutableDictionary first, + ImmutableDictionary second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first.Count != second.Count) + { + return false; + } + + foreach (var (name, firstArgument) in first) + { + if (!second.TryGetValue(name, out var secondArgument)) + { + return false; + } + + if (!ArgumentEquals(firstArgument, secondArgument)) + { + return false; + } + } + + return true; + } + + private static bool ArgumentEquals(object? first, object? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first is null || second is null) + { + return false; + } + + var firstType = first.GetType(); + if (IsImmutableArrayType(firstType)) + { + if (second.GetType() != firstType) + { + return false; + } + + return ArgumentSequenceEqual((IList)first, (IList)second); + } + + return first.Equals(second); + } + + private static bool ArgumentSequenceEqual(IList first, IList second) + { + if (first.Count != second.Count) + { + return false; + } + + for (int i = 0; i < first.Count; i++) + { + if (!ArgumentEquals(first[i], second[i])) + { + return false; + } + } + + return true; + } + + private static bool ArgumentSequenceEqual(ImmutableArray first, ImmutableArray second) + { + if (first.Length != second.Length) + { + return false; + } + + for (int i = 0; i < first.Length; i++) + { + if (!ArgumentEquals(first[i], second[i])) + { + return false; + } + } + + return true; + } + + public override string ToString() + { + var positional = PositionalArguments.Length > 0 ? string.Join(", ", PositionalArguments.Select(ToLiteralString)) : null; + var named = NamedArguments.Count > 0 ? + string.Join(", ", NamedArguments.Select(kvp => $"{kvp.Key}={ToLiteralString(kvp.Value)}")) : + null; + + string argumentList; + + if (positional != null && named == null) + { + argumentList = $"({positional})"; + } + else if (positional == null && named != null) + { + argumentList = $"({named})"; + } + else if (positional != null && named != null) + { + argumentList = $"({positional}, {named})"; + } + else + { + argumentList = string.Empty; + } + + return $"[{Namespace}.{TypeName}{argumentList}]"; + } + + private static string ToLiteralString(object? value) + { + if (value is null) + { + return "null"; + } + + if (value is string s) + { + return $"\"{s}\""; + } + + var type = value.GetType(); + if (IsImmutableArrayType(type)) + { + var itemType = type.GetGenericArguments()[0]; + var values = ((IEnumerable)value).Cast().Select(ToLiteralString); + + return $"new {GetTypeIdentifier(itemType)}[] {{{string.Join(", ", values)}}}"; + } + + return value.ToString(); + } + + private static string GetTypeIdentifier(Type type) + { + if (CSharp.CSharpSyntax.TypeAliases.TryGetValue(type, out var alias)) + { + return alias; + } + + return type.FullName; + } + + public AttributeDescriptor WithPositionalArguments(params object?[] positionalArguments) + { + return new AttributeDescriptor(TypeName, Namespace, positionalArguments.ToImmutableArray(), NamedArguments); + } + + public AttributeDescriptor WithNamedArguments(object namedArguments) + { + var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(namedArguments)) + { + arguments.Add(propertyDescriptor.Name, propertyDescriptor.GetValue(namedArguments)); + } + + return new AttributeDescriptor(TypeName, Namespace, PositionalArguments, arguments.ToImmutableDictionary()); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs index 1d8ec891c..5070355aa 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs @@ -5,6 +5,26 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; internal static class CSharpSyntax { + public static readonly Dictionary TypeAliases = new() + { + { typeof(byte), "byte" }, + { typeof(sbyte), "sbyte" }, + { typeof(short), "short" }, + { typeof(ushort), "ushort" }, + { typeof(int), "int" }, + { typeof(uint), "uint" }, + { typeof(long), "long" }, + { typeof(ulong), "ulong" }, + { typeof(float), "float" }, + { typeof(double), "double" }, + { typeof(decimal), "decimal" }, + { typeof(object), "object" }, + { typeof(bool), "bool" }, + { typeof(char), "char" }, + { typeof(string), "string" }, + { typeof(void), "void" } + }; + public static string CreateIdentifier(string s) { var sb = new StringBuilder(); diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs new file mode 100644 index 000000000..4d1b59f5f --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs @@ -0,0 +1,167 @@ +using FluentAssertions.Execution; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +public class AttributeDescriptorTests +{ + public static IEnumerable StringRepresentationExamples { get; } = + [ + [ new AttributeDescriptor("Foo", "Bar"), "[Bar.Foo]" ], + [ new AttributeDescriptor("Foo", "Bar", [ "Fizz" ]), "[Bar.Foo(\"Fizz\")]" ], + [ new AttributeDescriptor("Foo", "Bar", [ "Fizz", "Buzz" ]), "[Bar.Foo(\"Fizz\", \"Buzz\")]" ], + [ new AttributeDescriptor("Foo", "Bar", [ 1, 2 ]), "[Bar.Foo(1, 2)]" ], + [ new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create() ]), "[Bar.Foo(new string[] {})]" ], + [ + new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create("potato", "pancakes") ]), + "[Bar.Foo(new string[] {\"potato\", \"pancakes\"})]" + ] + ]; + + [Theory] + [MemberData(nameof(StringRepresentationExamples))] + public void DescriptorsCanBeRepresntedAsStrings(AttributeDescriptor attribute, string expected) + { + attribute.ToString().Should().BeEquivalentTo(expected); + } + + public static IEnumerable DescriptorExamples { get; } = + [ + [ () => new AttributeDescriptor("Foo", "Bar") ], + [ () => new AttributeDescriptor("Foo", "Bar", [ "Fizz" ]) ], + [ () => new AttributeDescriptor("Foo", "Bar", [ "Fizz", "Buzz" ]) ], + [ () => new AttributeDescriptor("Foo", "Bar", [ 1, 2 ]) ], + [ () => new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create() ]) ], + [ () => new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create("potato") ]) ] + ]; + + [Theory] + [MemberData(nameof(DescriptorExamples))] + public void DescriptorsAreEqualWhenTypeNamespaceAndArgumentsAreEquivalent(Func example) + { + var a = example(); + var b = example(); + + using var assertions = new AssertionScope(); + + a.GetHashCode().Should().Be(b.GetHashCode()); + a.Equals(b).Should().BeTrue(); + } + + [Theory] + [InlineData(true)] + [InlineData((byte)100)] + [InlineData('m')] + [InlineData(100.01d)] + [InlineData(100.01f)] + [InlineData(100)] + [InlineData(1000L)] + [InlineData((sbyte)100)] + [InlineData((short)100.01)] + [InlineData("muffins")] + [InlineData(100u)] + [InlineData(1000ul)] + [InlineData((ushort)100.01)] + public void DescriptorsCanBeCreatedWithSomeBuiltInTypesAsArguments(object? argument) + { + var attribute = new AttributeDescriptor("Foo", "Bar") + .WithPositionalArguments(argument) + .WithNamedArguments(new { Property = argument }); + + using var assertions = new AssertionScope(); + + attribute.PositionalArguments[0].Should().Be(argument); + attribute.NamedArguments["Property"].Should().Be(argument); + } + + [Theory] + [InlineData(AttributeTargets.Assembly)] + [InlineData(ConsoleKey.Add)] + public void DescriptorsCanBeCreatedWithEnumsAsArguments(object? argument) + { + var attribute = new AttributeDescriptor("Foo", "Bar") + .WithPositionalArguments(argument) + .WithNamedArguments(new { Property = argument }); + + using var assertions = new AssertionScope(); + + attribute.PositionalArguments[0].Should().Be(argument); + attribute.NamedArguments["Property"].Should().Be(argument); + } + + [Theory] + [InlineData(true)] + [InlineData((byte)100)] + [InlineData('m')] + [InlineData(100.01d)] + [InlineData(100.01f)] + [InlineData(100)] + [InlineData(1000L)] + [InlineData((sbyte)100)] + [InlineData((short)100.01)] + [InlineData("muffins")] + [InlineData(100u)] + [InlineData(1000ul)] + [InlineData((ushort)100.01)] + public void DescriptorsCanBeCreatedWithImmutableArraysOfSomeBuiltInTypesAsArguments(T value) + { + var argument = ImmutableArray.Create(value, value); + + var attribute = new AttributeDescriptor("Foo", "Bar") + .WithPositionalArguments(argument) + .WithNamedArguments(new { Property = argument }); + + using var assertions = new AssertionScope(); + + attribute.PositionalArguments[0].Should().Be(argument); + attribute.NamedArguments["Property"].Should().Be(argument); + } + + [Theory] + [InlineData(AttributeTargets.Assembly)] + [InlineData(ConsoleKey.Add)] + public void DescriptorsCanBeCreatedWithImmutableArraysOfEnumsAsArguments(T value) + { + var argument = ImmutableArray.Create(value, value); + + var attribute = new AttributeDescriptor("Foo", "Bar") + .WithPositionalArguments(argument) + .WithNamedArguments(new { Property = argument }); + + using var assertions = new AssertionScope(); + + attribute.PositionalArguments[0].Should().Be(argument); + attribute.NamedArguments["Property"].Should().Be(argument); + } + + [Theory] + [InlineData(typeof(string))] + [InlineData(typeof(AttributeDescriptorTests))] + public void DescriptorsCanBeCreatedWithTypesAsArguments(Type argument) + { + var attribute = new AttributeDescriptor("Foo", "Bar") + .WithPositionalArguments(argument) + .WithNamedArguments(new { Property = argument }); + + using var assertions = new AssertionScope(); + + attribute.PositionalArguments[0].Should().Be(argument); + attribute.NamedArguments["Property"].Should().Be(argument); + } + + [Fact] + public void DescriptorsCannotBeCreatedWithArraysAsArguments() + { + var argument = Array.Empty(); + + var attribute = new AttributeDescriptor("Foo", "Bar"); + + attribute + .Invoking(attribute => attribute.WithPositionalArguments([ argument ])) + .Should().Throw(); + + attribute + .Invoking(attribute => attribute.WithNamedArguments(new { Property = argument })) + .Should().Throw(); + } +} From 3fdb30e8af5adbc1af6a19c8e62f9cd7e5dfbd38 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 11 Jun 2024 17:19:56 +0100 Subject: [PATCH 18/48] Shift to object-model-based generation approach --- .../AttributeDescriptor.cs | 22 +- .../CSharp/CSharpScenarioMethod.cs | 90 +++++++ .../CSharp/CSharpSourceTextBuilder.cs | 68 ++++- .../CSharp/CSharpSyntax.cs | 18 +- .../CSharp/CSharpTestFixtureGeneration.cs | 165 ++++++++---- .../EnumerableEqualityExtensions.cs | 49 +++- .../ITestFrameworkHandler.cs | 7 +- .../IdentifierString.cs | 123 +++++++++ .../ImmutableArrayEqualityComparer.cs | 26 ++ .../MSTestCSharpTestFixtureGeneration.cs | 56 ++++- .../NamespaceString.cs | 103 ++++++++ .../ParameterDescriptor.cs | 41 +++ .../ScenarioInformation.cs | 12 + .../TestFixture.cs | 21 ++ .../TestFixtureComposition.cs | 30 +++ .../TestFixtureMethod.cs | 5 + .../TestFixtureSourceGenerator.cs | 232 +++++++++++++++-- .../TypeIdentifier.cs | 102 ++++++++ .../XUnit/XUnitCSharpSyntaxGeneration.cs | 4 +- .../AttributeDescriptorFormatter.cs | 22 ++ .../AttributeDescriptorTests.cs | 63 +++-- .../CSharpMethodDeclarationAssertions.cs | 238 +++++++++++++++++- .../IdentifierStringTests.cs | 147 +++++++++++ .../Initializer.cs | 12 + .../MSTestFeatureSourceGeneratorTests.cs | 147 ++++++++++- .../NamespaceStringTests.cs | 132 ++++++++++ .../ParameterSyntaxFormatter.cs | 31 +++ .../TypeIdentifierTests.cs | 228 +++++++++++++++++ 28 files changed, 2075 insertions(+), 119 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpScenarioMethod.cs create mode 100644 Reqnroll.FeatureSourceGenerator/IdentifierString.cs create mode 100644 Reqnroll.FeatureSourceGenerator/ImmutableArrayEqualityComparer.cs create mode 100644 Reqnroll.FeatureSourceGenerator/NamespaceString.cs create mode 100644 Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs create mode 100644 Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixture.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/IdentifierStringTests.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/Initializer.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/NamespaceStringTests.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterSyntaxFormatter.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/TypeIdentifierTests.cs diff --git a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs index fc3139975..33265c2d1 100644 --- a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs @@ -7,20 +7,16 @@ namespace Reqnroll.FeatureSourceGenerator; /// /// Provides a description of a .NET attribute instance. /// -/// The name of the attribute's type. -/// The namespace of the attribute's type. +/// The attribute's type. /// The positional arguments of the attribute. /// The named arguments of the attribute. public class AttributeDescriptor( - string typeName, - string ns, + TypeIdentifier type, ImmutableArray? positionalArguments = null, ImmutableDictionary? namedArguments = null) : IEquatable { - public string TypeName { get; } = typeName; - - public string Namespace { get; } = ns; + public TypeIdentifier Type { get; } = type; public ImmutableArray PositionalArguments { get; } = ThrowIfArgumentTypesNotValid( @@ -120,8 +116,7 @@ private int CalculateHashCode() { var hash = 47; - hash *= 13 + TypeName.GetHashCode(); - hash *= 13 + Namespace.GetHashCode(); + hash *= 13 + Type.GetHashCode(); var index = 0; foreach (var item in PositionalArguments) @@ -193,8 +188,7 @@ public virtual bool Equals(AttributeDescriptor? other) return false; } - return TypeName.Equals(other.TypeName) && - Namespace.Equals(other.Namespace) && + return Type.Equals(other.Type) && ArgumentsEqual(other); } @@ -322,7 +316,7 @@ public override string ToString() argumentList = string.Empty; } - return $"[{Namespace}.{TypeName}{argumentList}]"; + return $"[{Type}{argumentList}]"; } private static string ToLiteralString(object? value) @@ -361,7 +355,7 @@ private static string GetTypeIdentifier(Type type) public AttributeDescriptor WithPositionalArguments(params object?[] positionalArguments) { - return new AttributeDescriptor(TypeName, Namespace, positionalArguments.ToImmutableArray(), NamedArguments); + return new AttributeDescriptor(Type, positionalArguments.ToImmutableArray(), NamedArguments); } public AttributeDescriptor WithNamedArguments(object namedArguments) @@ -373,6 +367,6 @@ public AttributeDescriptor WithNamedArguments(object namedArguments) arguments.Add(propertyDescriptor.Name, propertyDescriptor.GetValue(namedArguments)); } - return new AttributeDescriptor(TypeName, Namespace, PositionalArguments, arguments.ToImmutableDictionary()); + return new AttributeDescriptor(Type, PositionalArguments, arguments.ToImmutableDictionary()); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpScenarioMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpScenarioMethod.cs new file mode 100644 index 000000000..059f43e75 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpScenarioMethod.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +/// +/// Represents a class which is a test fixture to execute the scenarios associated with a feature. +/// +public class CSharpTestFixtureClass +{ + public CSharpTestFixtureClass( + TypeIdentifier identifier, + ImmutableArray attributes = default, + ImmutableArray methods = default) + { + if (identifier.IsEmpty) + { + throw new ArgumentException("Value cannot be an empty identifier.", nameof(identifier)); + } + + Identifier = identifier; + + Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; + Methods = methods.IsDefault ? ImmutableArray.Empty : methods; + } + + public TypeIdentifier Identifier { get; } + public ImmutableArray Attributes { get; } + public ImmutableArray Methods { get; } + + public void WriteTo(CSharpSourceTextBuilder sourceBuilder) + { + sourceBuilder.Append("namespace ").Append(Identifier.Namespace.ToString()).AppendLine(); + sourceBuilder.BeginBlock("{"); + + if (!Attributes.IsEmpty) + { + foreach (var attribute in Attributes) + { + sourceBuilder.AppendAttributeBlock(attribute); + sourceBuilder.AppendLine(); + } + } + + sourceBuilder.Append("public class ").Append(Identifier.LocalName.ToString()); + sourceBuilder.BeginBlock("{"); + + WriteTestFixturePreambleTo(sourceBuilder); + + sourceBuilder.AppendLine(); + + WriteScenarioMethodsTo(sourceBuilder); + + sourceBuilder.EndBlock("}"); + sourceBuilder.EndBlock("}"); + } + + protected virtual void WriteTestFixturePreambleTo(CSharpSourceTextBuilder sourceBuilder) + { + throw new NotImplementedException(); + } + + protected virtual void WriteScenarioMethodsTo(CSharpSourceTextBuilder sourceBuilder) + { + var first = true; + foreach (var method in Methods) + { + if (!first) + { + sourceBuilder.AppendLine(); + } + + method.WriteTo(sourceBuilder); + + if (first) + { + first = false; + } + } + } +} + +public abstract class CSharpScenarioMethod(string name) +{ + public string Name { get; } = name; + + public void WriteTo(CSharpSourceTextBuilder sourceBuilder) + { + throw new NotImplementedException(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs index df9f57dca..a6ed28947 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Collections.Immutable; +using System.Security.Cryptography.X509Certificates; +using System.Text; namespace Reqnroll.FeatureSourceGenerator.CSharp; @@ -35,7 +37,7 @@ public CSharpSourceTextBuilder Append(string text) return this; } - public CSharpSourceTextBuilder AppendConstantList(IEnumerable values) + public CSharpSourceTextBuilder AppendConstantList(IEnumerable values) { var first = true; @@ -50,23 +52,42 @@ public CSharpSourceTextBuilder AppendConstantList(IEnumerable values) Append(", "); } - AppendConstant(value); + AppendLiteral(value); } return this; } - public CSharpSourceTextBuilder AppendConstant(object? value) + public CSharpSourceTextBuilder AppendLiteral(object? value) { return value switch { null => Append("null"), - string s => AppendConstant(s), + string s => AppendLiteral(s), + ImmutableArray array => AppendLiteralArray(array), + ImmutableArray array => AppendLiteralArray(array), _ => throw new NotSupportedException($"Values of type {value.GetType().FullName} cannot be encoded as a constant in C#.") }; } - private CSharpSourceTextBuilder AppendConstant(string? s) + private CSharpSourceTextBuilder AppendLiteralArray(ImmutableArray array) + { + if (!CSharpSyntax.TypeAliases.TryGetValue(typeof(T), out var typeName)) + { + typeName = $"global::{typeof(T).FullName}"; + } + + Append("new ").Append(typeName); + + if (array.Length == 0) + { + return Append("[0]"); + } + + return Append("[] { ").AppendConstantList(array).Append(" }"); + } + + private CSharpSourceTextBuilder AppendLiteral(string? s) { if (s == null) { @@ -215,4 +236,39 @@ public SourceText ToSourceText() { return SourceText.From(_buffer.ToString(), Encoding); } + + public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribute) + { + Append('['); + AppendTypeIdentifier(attribute.Type); + + if (attribute.NamedArguments.Count > 0 || attribute.PositionalArguments.Length > 0) + { + Append('('); + + AppendConstantList(attribute.PositionalArguments); + + var firstProperty = true; + foreach (var (name, value) in attribute.NamedArguments) + { + if (!firstProperty) + { + + } + + firstProperty = false; + } + + Append(')'); + } + + Append(']'); + + return this; + } + + public CSharpSourceTextBuilder AppendTypeIdentifier(TypeIdentifier identifier) + { + return this; + } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs index 5070355aa..28d9f4742 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs @@ -25,7 +25,13 @@ internal static class CSharpSyntax { typeof(void), "void" } }; - public static string CreateIdentifier(string s) + public static string CreateTypeIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: true); + + public static string CreateMethodIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: true); + + public static string CreateParameterIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: false); + + private static string CreateIdentifier(string s, bool capitalizeFirstWord) { var sb = new StringBuilder(); var newWord = true; @@ -52,7 +58,15 @@ public static string CreateIdentifier(string s) if (newWord) { - sb.Append(char.ToUpper(c)); + if (sb.Length == 0 && !capitalizeFirstWord) + { + sb.Append(c); + } + else + { + sb.Append(char.ToUpper(c)); + } + newWord = false; } else diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs index 26f8c1bf4..b6e853241 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -1,5 +1,6 @@ using Gherkin.Ast; using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp; @@ -43,7 +44,8 @@ internal SourceText GetSourceText() return SourceBuilder.ToSourceText(); } - protected virtual string GetClassName() => CSharpSyntax.CreateIdentifier(Document.Feature.Name + Document.Feature.Keyword); + protected virtual string GetClassName() => + CSharpSyntax.CreateTypeIdentifier(Document.Feature.Name + Document.Feature.Keyword); protected virtual string? GetBaseType() => null; @@ -105,7 +107,20 @@ protected virtual void AppendTestFixtureClass() { case Scenario scenario: SourceBuilder.AppendLine(); - AppendTestMethodForScenario(scenario); + AppendTestMethodForScenario(scenario, null); + break; + + case Rule rule: + foreach (var ruleChild in rule.Children) + { + switch (ruleChild) + { + case Scenario scenario: + SourceBuilder.AppendLine(); + AppendTestMethodForScenario(scenario, rule); + break; + } + } break; } } @@ -115,42 +130,42 @@ protected virtual void AppendTestFixtureClass() private void AppendAttribute(AttributeDescriptor attribute) { - SourceBuilder.Append("[global::").Append(attribute.Namespace).Append(".").Append(attribute.TypeName); + //SourceBuilder.Append("[global::").AppendTypeIdentifier(); - if (attribute.Arguments.Any() || attribute.PropertyValues.Any()) - { - var first = true; + //if (attribute.PositionalArguments.Any() || attribute.NamedArguments.Any()) + //{ + // var first = true; - SourceBuilder.Append("("); + // SourceBuilder.Append("("); - foreach (var argument in attribute.Arguments) - { - if (!first) - { - SourceBuilder.Append(", "); - } + // foreach (var argument in attribute.PositionalArguments) + // { + // if (!first) + // { + // SourceBuilder.Append(", "); + // } - first = false; + // first = false; - SourceBuilder.AppendConstant(argument); - } + // SourceBuilder.AppendLiteral(argument); + // } - foreach (var (propertyName, propertyValue) in attribute.PropertyValues) - { - if (!first) - { - SourceBuilder.Append(", "); - } + // foreach (var (name, argument) in attribute.NamedArguments) + // { + // if (!first) + // { + // SourceBuilder.Append(", "); + // } - first = false; + // first = false; - SourceBuilder.Append(propertyName).Append(" = ").AppendConstant(propertyValue); - } + // SourceBuilder.Append(name).Append(" = ").AppendLiteral(argument); + // } - SourceBuilder.Append(")"); - } + // SourceBuilder.Append(")"); + //} - SourceBuilder.AppendLine("]"); + //SourceBuilder.AppendLine("]"); } protected virtual IEnumerable GetTestFixtureAttributes() => []; @@ -178,9 +193,9 @@ protected virtual void AppendTestFixturePreamble() SourceBuilder .AppendLine("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") .BeginBlock() - .AppendLine("new global::System.Globalization.CultureInfo(").AppendConstant(feature.Language).AppendLine("), ") - .AppendConstant(Path.GetDirectoryName(FeatureInformation.FeatureSyntax.FilePath).Replace("\\", "\\\\")).AppendLine(", ") - .AppendConstant(feature.Name).AppendLine(", ") + .AppendLine("new global::System.Globalization.CultureInfo(").AppendLiteral(feature.Language).AppendLine("), ") + .AppendLiteral(Path.GetDirectoryName(FeatureInformation.FeatureSyntax.FilePath).Replace("\\", "\\\\")).AppendLine(", ") + .AppendLiteral(feature.Name).AppendLine(", ") .AppendLine("null, ") .AppendLine("global::Reqnroll.ProgrammingLanguage.CSharp, ") .AppendLine("FeatureTags);") @@ -212,7 +227,7 @@ protected virtual void AppendScenarioInitializeMethodBody() SourceBuilder.AppendLine("testRunner.OnScenarioInitialize(scenarioInfo);"); } - protected virtual void AppendTestMethodForScenario(Scenario scenario) + protected virtual void AppendTestMethodForScenario(Scenario scenario, Rule? rule) { var attributes = GetTestMethodAttributes(scenario); @@ -221,20 +236,73 @@ protected virtual void AppendTestMethodForScenario(Scenario scenario) AppendAttribute(attribute); } - SourceBuilder.Append("public async Task ").Append(CSharpSyntax.CreateIdentifier(scenario.Name)).AppendLine("()"); + var parameters = GetTestMethodParameters(scenario); + + SourceBuilder + .Append("public async Task ") + .Append(CSharpSyntax.CreateMethodIdentifier(scenario.Name)); + + if (parameters.Length > 0) + { + SourceBuilder + .AppendLine("(") + .BeginBlock(); + + var first = true; + + foreach (var parameter in parameters) + { + if (first) + { + first = false; + } + else + { + SourceBuilder.AppendLine(","); + } + + SourceBuilder.Append(parameter); + } + + SourceBuilder + .Append(")") + .EndBlock(); + } + else + { + SourceBuilder.AppendLine("()"); + } + SourceBuilder.BeginBlock("{"); - AppendTestMethodBodyForScenario(scenario); + AppendTestMethodBodyForScenario(scenario, rule); SourceBuilder.EndBlock("}"); } - protected virtual void AppendTestMethodBodyForScenario(Scenario scenario) + protected virtual ImmutableArray GetTestMethodParameters(Scenario scenario) + { + var parameters = new List(); + + var example = scenario.Examples.FirstOrDefault(); + + if (example != null) + { + parameters.AddRange( + example.TableHeader.Cells.Select(heading => $"string {CSharpSyntax.CreateParameterIdentifier(heading.Value)}")); + + parameters.Add("string[] exampleTags"); + } + + return parameters.ToImmutableArray(); + } + + protected virtual void AppendTestMethodBodyForScenario(Scenario scenario, Rule? rule) { AppendTestRunnerLookupForScenario(scenario); SourceBuilder.AppendLine(); - AppendScenarioInfo(scenario); + AppendScenarioInfo(scenario, rule); SourceBuilder.AppendLine(); SourceBuilder.AppendLine("try"); @@ -301,9 +369,9 @@ protected virtual void AppendScenarioStepInvocation(Step step, Scenario scenario _ => throw new NotSupportedException($"Steps of type \"{step.Keyword}\" are not supported.") // TODO: Add message from resx }) .Append("Async(") - .AppendConstant(step.Text) + .AppendLiteral(step.Text) .Append(", null, null, ") - .AppendConstant(step.Keyword) + .AppendLiteral(step.Keyword) .AppendLine(");"); if (IsLineMappingEnabled) @@ -312,22 +380,33 @@ protected virtual void AppendScenarioStepInvocation(Step step, Scenario scenario } } - protected virtual void AppendScenarioInfo(Scenario scenario) + protected virtual void AppendScenarioInfo(Scenario scenario, Rule? rule) { SourceBuilder.AppendLine("// start: calculate ScenarioInfo"); SourceBuilder - .Append("string[] tagsOfScenario = new string[] { ") + .Append("var tagsOfScenario = new string[] { ") .AppendConstantList(scenario.Tags.Select(tag => tag.Name.TrimStart('@'))) .AppendLine(" };"); SourceBuilder.AppendLine( "var argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); // needed for scenario outlines"); - // TODO: Add support for rules. - SourceBuilder.AppendLine("var inheritedTags = FeatureTags; // will be more complex if there are rules"); + if (rule == null) + { + SourceBuilder.AppendLine("var inheritedTags = FeatureTags;"); + } + else + { + SourceBuilder + .Append("var ruleTags = new string[] { ") + .AppendConstantList(rule.Tags.Select(tag => tag.Name.TrimStart('@'))) + .AppendLine(" };"); + + SourceBuilder.AppendLine("var inheritedTags = FeatureTags.Concat(ruleTags)"); + } SourceBuilder .Append("var scenarioInfo = new global::Reqnroll.ScenarioInfo(") - .AppendConstant(scenario.Name) + .AppendLiteral(scenario.Name) .AppendLine(", null, tagsOfScenario, argumentsOfScenario, inheritedTags);"); SourceBuilder.AppendLine("// end: calculate ScenarioInfo"); } diff --git a/Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs b/Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs index bb10339a7..5ff2987c0 100644 --- a/Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs +++ b/Reqnroll.FeatureSourceGenerator/EnumerableEqualityExtensions.cs @@ -46,10 +46,57 @@ public static int GetSetHashCode(this IEnumerable set) foreach (var item in set) { - hash += 13 + item?.GetHashCode() ?? 0; + hash *= 13 + item?.GetHashCode() ?? 0; } return hash; } } + + public static int GetSequenceHashCode(this IEnumerable sequence) => + sequence.GetSequenceHashCode(EqualityComparer.Default); + + public static int GetSequenceHashCode(this IEnumerable sequence, IEqualityComparer itemComparer) + { + unchecked + { + var hash = 23; + + var index = 0; + foreach (var item in sequence) + { + hash *= 13 + index++ + (item is null ? 0 : itemComparer.GetHashCode(item)); + } + + return hash; + } + } + + public static bool SetEqual(this IEnumerable first, IEnumerable second) + { + if (first is null) + { + return second is null; + } + + if (ReferenceEquals(first, second)) + { + return true; + } + + var items = first.ToList(); + + var count = 0; + + foreach (var item in second) + { + count++; + if (!items.Remove(item)) + { + return false; + } + } + + return items.Count == count; + } } diff --git a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs index 2223a5e22..9944a2a78 100644 --- a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs @@ -6,7 +6,12 @@ public interface ITestFrameworkHandler bool CanGenerateForCompilation(CompilationInformation compilationInformation); - SourceText GenerateTestFixture(FeatureInformation feature); + TestFixtureMethod GenerateTestFixtureMethod(ScenarioInformation scenarioInformation, CancellationToken cancellationToken); + + TestFixture GenerateTestFixture( + FeatureInformation featureInformation, + IEnumerable methods, + CancellationToken cancellationToken); bool IsTestFrameworkReferenced(CompilationInformation compilationInformation); } diff --git a/Reqnroll.FeatureSourceGenerator/IdentifierString.cs b/Reqnroll.FeatureSourceGenerator/IdentifierString.cs new file mode 100644 index 000000000..5ab72ad6a --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/IdentifierString.cs @@ -0,0 +1,123 @@ +using System.Globalization; + +namespace Reqnroll.FeatureSourceGenerator; + +public readonly struct IdentifierString : IEquatable, IEquatable +{ + private readonly string? _value; + + public IdentifierString(string? s) + { + if (string.IsNullOrEmpty(s)) + { + _value = null; + return; + } + + var atStartOfIdentifier = true; + + foreach (var c in s!) + { + if (atStartOfIdentifier) + { + if (!IsValidAsFirstCharacterInIdentifier(c)) + { + throw new ArgumentException( + $"'{c}' cannot appear as the first character in an identifier.", + nameof(s)); + } + + atStartOfIdentifier = false; + } + else + { + if (!IsValidInIdentifier(c)) + { + throw new ArgumentException( + $"'{c}' cannot appear in an identifier.", + nameof(s)); + } + } + } + + _value = s; + } + + public readonly bool IsEmpty => _value == null; + + internal static bool IsValidAsFirstCharacterInIdentifier(char c) + { + if (c == '_') + { + return true; + } + + var category = char.GetUnicodeCategory(c); + + return category == UnicodeCategory.UppercaseLetter + || category == UnicodeCategory.LowercaseLetter + || category == UnicodeCategory.TitlecaseLetter + || category == UnicodeCategory.ModifierLetter + || category == UnicodeCategory.OtherLetter; + } + + internal static bool IsValidInIdentifier(char c) + { + var category = char.GetUnicodeCategory(c); + + return category == UnicodeCategory.UppercaseLetter + || category == UnicodeCategory.LowercaseLetter + || category == UnicodeCategory.TitlecaseLetter + || category == UnicodeCategory.ModifierLetter + || category == UnicodeCategory.OtherLetter + || category == UnicodeCategory.LetterNumber + || category == UnicodeCategory.NonSpacingMark + || category == UnicodeCategory.SpacingCombiningMark + || category == UnicodeCategory.DecimalDigitNumber + || category == UnicodeCategory.ConnectorPunctuation + || category == UnicodeCategory.Format; + } + + public bool Equals(string? other) + { + if (string.IsNullOrEmpty(other)) + { + return IsEmpty; + } + + return string.Equals(_value, other, StringComparison.Ordinal); + } + + public bool Equals(IdentifierString other) => string.Equals(_value, other._value, StringComparison.Ordinal); + + public override string ToString() => _value ?? ""; + + public override bool Equals(object obj) + { + return obj switch + { + null => IsEmpty, + IdentifierString identifier => Equals(identifier), + string s => Equals(s), + _ => false + }; + } + + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public static bool Equals(IdentifierString identifier, string? s) => identifier.Equals(s); + + public static bool Equals(IdentifierString identifier1, IdentifierString identifier2) => identifier1.Equals(identifier2); + + public static bool operator ==(IdentifierString identifier, string? s) => Equals(identifier, s); + + public static bool operator !=(IdentifierString identifier, string? s) => !Equals(identifier, s); + + public static bool operator ==(IdentifierString identifier1, IdentifierString identifier2) => + Equals(identifier1, identifier2); + + public static bool operator !=(IdentifierString identifier1, IdentifierString identifier2) => + !Equals(identifier1, identifier2); + + public static implicit operator string(IdentifierString identifier) => identifier.ToString(); +} diff --git a/Reqnroll.FeatureSourceGenerator/ImmutableArrayEqualityComparer.cs b/Reqnroll.FeatureSourceGenerator/ImmutableArrayEqualityComparer.cs new file mode 100644 index 000000000..aa64040d4 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/ImmutableArrayEqualityComparer.cs @@ -0,0 +1,26 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +public class ImmutableArrayEqualityComparer(IEqualityComparer itemComparer) : IEqualityComparer> +{ + public ImmutableArrayEqualityComparer() : this(EqualityComparer.Default) + { + } + + public IEqualityComparer ItemComparer { get; } = itemComparer; + + public static ImmutableArrayEqualityComparer Default { get; } = new ImmutableArrayEqualityComparer(); + + public bool Equals(ImmutableArray x, ImmutableArray y) + { + if (x.Equals(y)) + { + return true; + } + + return x.SequenceEqual(y, ItemComparer); + } + + public int GetHashCode(ImmutableArray obj) => obj.GetSequenceHashCode(ItemComparer); +} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs index 23694255d..2bbe5dd5f 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs @@ -6,17 +6,13 @@ namespace Reqnroll.FeatureSourceGenerator.MSTest; internal class MSTestCSharpTestFixtureGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) { - const string MSTestNamespace = "Microsoft.VisualStudio.TestTools.UnitTesting"; + private static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); protected override IEnumerable GetTestFixtureAttributes() { return base.GetTestFixtureAttributes().Concat( [ - new AttributeDescriptor( - "TestClass", - MSTestNamespace, - ImmutableArray.Empty, - ImmutableArray>.Empty) + new AttributeDescriptor(new TypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))) ]); } @@ -70,20 +66,58 @@ protected override IEnumerable GetTestMethodAttributes(Scen { var attributes = new List { - new("TestMethod", MSTestNamespace), - new("Description", MSTestNamespace, ImmutableArray.Create(scenario.Name)), - new("TestProperty", MSTestNamespace, ImmutableArray.Create("FeatureTitle", Document.Feature.Name)) + new( + new TypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), + new( + new TypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + ImmutableArray.Create(scenario.Name)), + new( + new TypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + ImmutableArray.Create("FeatureTitle", Document.Feature.Name)) }; foreach (var tag in Document.Feature.Tags.Concat(scenario.Tags)) { attributes.Add( new AttributeDescriptor( - "TestCategory", - MSTestNamespace, + new TypeIdentifier(MSTestNamespace, new IdentifierString("TestCategory")), ImmutableArray.Create(tag.Name.TrimStart('@')))); } + foreach (var example in scenario.Examples) + { + var values = new object?[example.TableHeader.Cells.Count() + 1]; + + // Add tags as the last argument in the values passed to the data-row. + values[values.Length - 1] = example.Tags + .Select(tag => tag.Name.TrimStart('@')) + .ToImmutableArray(); + + foreach (var row in example.TableBody) + { + var i = 0; + foreach (var cell in row.Cells) + { + values[i++] = cell.Value; + } + + // DataRow's constructor is DataRow(object? data, params object?[] moreData) + // Because we often pass an array of strings as a second argument, we always wrap moreData + // in an explicit array to avoid the compiler mistaking our string array as the moreData value. + var first = values.First(); + var others = values.Skip(1).ToImmutableArray(); + + var positionalArguments = others.Length > 0 ? + ImmutableArray.Create(first, others) : + ImmutableArray.Create(first); + + attributes.Add( + new AttributeDescriptor( + new TypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + positionalArguments)); + } + } + return base.GetTestMethodAttributes(scenario).Concat(attributes); } diff --git a/Reqnroll.FeatureSourceGenerator/NamespaceString.cs b/Reqnroll.FeatureSourceGenerator/NamespaceString.cs new file mode 100644 index 000000000..596953662 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/NamespaceString.cs @@ -0,0 +1,103 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public readonly struct NamespaceString : IEquatable, IEquatable +{ + private readonly string? _value; + + public static readonly NamespaceString Empty = default; + + public NamespaceString(string s) + { + if (string.IsNullOrEmpty(s)) + { + _value = string.Empty; + return; + } + + var atStartOfIdentifier = true; + + foreach (var c in s) + { + if (atStartOfIdentifier) + { + if (!IdentifierString.IsValidAsFirstCharacterInIdentifier(c)) + { + throw new ArgumentException( + $"'{c}' cannot appear as the first character in an identifier.", + nameof(s)); + } + + atStartOfIdentifier = false; + } + else if (c == '.') + { + atStartOfIdentifier = true; + } + else + { + if (!IdentifierString.IsValidInIdentifier(c)) + { + throw new ArgumentException( + $"'{c}' cannot appear in an identifier.", + nameof(s)); + } + } + } + + if (atStartOfIdentifier) + { + throw new ArgumentException( + $"'.' cannot appear as the last character in the namespace.", + nameof(s)); + } + + _value = s; + } + + public readonly bool IsEmpty => string.IsNullOrEmpty(_value); + + public override string ToString() => _value ?? ""; + + public override int GetHashCode() => _value?.GetHashCode() ?? 0; + + public override bool Equals(object obj) + { + return obj switch + { + null => IsEmpty, + NamespaceString ns => Equals(ns), + string s => Equals(s), + _ => false + }; + } + + public bool Equals(string other) + { + if (string.IsNullOrEmpty(other)) + { + return IsEmpty; + } + + return _value!.Equals(other, StringComparison.Ordinal); + } + + public bool Equals(NamespaceString other) + { + if (other.IsEmpty) + { + return IsEmpty; + } + + return _value!.Equals(other._value, StringComparison.Ordinal); + } + + public static bool operator ==(NamespaceString namespaceA, NamespaceString namespaceB) => Equals(namespaceA, namespaceB); + + public static bool operator !=(NamespaceString namespaceA, NamespaceString namespaceB) => !Equals(namespaceA, namespaceB); + + public static bool operator ==(NamespaceString ns, string s) => Equals(ns, s); + + public static bool operator !=(NamespaceString ns, string s) => !Equals(ns, s); + + public static implicit operator string(NamespaceString ns) => ns.ToString(); +} diff --git a/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs b/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs new file mode 100644 index 000000000..989c5785c --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.FeatureSourceGenerator; +public class ParameterDescriptor: IEquatable +{ + public ParameterDescriptor(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Value cannot be null or an empty string.", nameof(name)); + } + + Name = name; + } + + public string Name { get; } + + public bool Equals(ParameterDescriptor? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other is null) + { + return false; + } + + return Name.Equals(other.Name); + } + + public override bool Equals(object obj) => Equals(obj as ParameterDescriptor); + + public override int GetHashCode() + { + return base.GetHashCode(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs b/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs new file mode 100644 index 000000000..48dd3881a --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +public record ScenarioInformation( + FeatureInformation Feature, + string Name, + ImmutableArray ImmutableArray, + ImmutableArray ScenarioSteps, + ImmutableArray ScenarioExampleSets, + string? RuleName, + ImmutableArray RuleTags); diff --git a/Reqnroll.FeatureSourceGenerator/TestFixture.cs b/Reqnroll.FeatureSourceGenerator/TestFixture.cs new file mode 100644 index 000000000..d11afd8f5 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixture.cs @@ -0,0 +1,21 @@ +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Represents a Reqnroll text fixture. +/// +/// The feature the test fixture provides a representation of. +public abstract class TestFixture(FeatureInformation feature) +{ + public FeatureInformation Feature { get; } = feature; + + public string HintName => Feature.FeatureHintName; + + /// + /// Renders the configured test fixture to source text. + /// + /// A representing the rendered test fixture. + public SourceText Render() + { + throw new NotImplementedException(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs new file mode 100644 index 000000000..b99d7cea5 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs @@ -0,0 +1,30 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +internal record TestFixtureComposition(FeatureInformation Feature, ImmutableArray Methods) +{ + public override int GetHashCode() + { + unchecked + { + var hash = 49151149; + + hash *= 983819 + Feature.GetHashCode(); + hash *= 983819 + Methods.GetSequenceHashCode(); + + return hash; + } + } + + public virtual bool Equals(TestFixtureComposition? other) + { + if (other is null) + { + return false; + } + + return Feature.Equals(other.Feature) && + (Methods.Equals(other.Methods) || Methods.SequenceEqual(other.Methods)); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs new file mode 100644 index 000000000..b01c8c72c --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs @@ -0,0 +1,5 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public class TestFixtureMethod +{ +} \ No newline at end of file diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 674fe4931..6de65ccdb 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -1,8 +1,15 @@ -using Reqnroll.FeatureSourceGenerator.Gherkin; +using Gherkin; +using Gherkin.Ast; +using Microsoft.CodeAnalysis.VisualBasic; +using Reqnroll.FeatureSourceGenerator.Gherkin; +using System.Collections; using System.Collections.Immutable; +using System.Runtime.CompilerServices; namespace Reqnroll.FeatureSourceGenerator; +using Location = Microsoft.CodeAnalysis.Location; + /// /// Defines the basis of a source-generator which processes Gherkin feature files into test fixtures. /// @@ -179,34 +186,223 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ]; }); - // Emit source files for each feature by invoking the generator. - context.RegisterSourceOutput(featureInformationOrErrors, static (context, featureOrError) => + // Filter features and errors. + var features = featureInformationOrErrors + .Where(result => result.IsSuccess) + .Select((result, _) => (FeatureInformation)result); + var errors = featureInformationOrErrors + .Where(result => !result.IsSuccess) + .Select((result, _) => (Diagnostic)result); + + // Generate scenario information from features. + var scenarios = features + .SelectMany(static (feature, cancellationToken) => + feature.FeatureSyntax.GetRoot().Feature.Children.SelectMany(child => + GetScenarioInformation(child, feature, cancellationToken))); + + // Generate scenario methods for each scenario. + var methods = scenarios + .Select(static (scenario, cancellationToken) => + (Method: scenario.Feature.TestFrameworkHandler.GenerateTestFixtureMethod(scenario, cancellationToken), + Scenario: scenario)); + + // Generate test fixtures for each feature. + var fixtures = methods.Collect() + .WithComparer(ImmutableArrayEqualityComparer<(TestFixtureMethod Method, ScenarioInformation Scenario)>.Default) + .SelectMany(static (methods, cancellationToken) => + methods + .GroupBy(item => item.Scenario.Feature, item => item.Method) + .Select(group => new TestFixtureComposition(group.Key, group.ToImmutableArray()))) + .Select(static (composition, cancellationToken) => + composition.Feature.TestFrameworkHandler.GenerateTestFixture( + composition.Feature, + composition.Methods, + cancellationToken)); + + // Emit errors. + context.RegisterSourceOutput(errors, static (context, error) => context.ReportDiagnostic(error)); + + // Emit parsing diagnostics. + context.RegisterSourceOutput(features, static (context, feature) => { - // If an error, report diagnostic. - if (!featureOrError.IsSuccess) + foreach (var diagnostic in feature.FeatureSyntax.GetDiagnostics()) { - var error = (Diagnostic)featureOrError; - context.ReportDiagnostic(error); - return; + context.ReportDiagnostic(diagnostic); } + }); - var feature = (FeatureInformation)featureOrError; + // Emit source files for fixtures. + context.RegisterSourceOutput( + fixtures, + static (context, fixture) => context.AddSource(fixture.HintName, fixture.Render())); + } - // Report any syntax errors in the parsing of the document. - foreach (var diagnostic in feature.FeatureSyntax.GetDiagnostics()) + private static IEnumerable GetScenarioInformation( + IHasLocation child, + FeatureInformation feature, + CancellationToken cancellationToken) + { + return child switch + { + Scenario scenario => [ GetScenarioInformation(scenario, feature, cancellationToken) ], + Rule rule => GetScenarioInformation(rule, feature, cancellationToken), + _ => [] + }; + } + + private static ScenarioInformation GetScenarioInformation( + Scenario scenario, + FeatureInformation feature, + CancellationToken cancellationToken) => + GetScenarioInformation(scenario, null, ImmutableArray.Empty, feature, cancellationToken); + + private static ScenarioInformation GetScenarioInformation( + Scenario scenario, + string? ruleName, + ImmutableArray ruleTags, + FeatureInformation feature, + CancellationToken cancellationToken) + { + var exampleSets = new List(); + + foreach (var example in scenario.Examples) + { + cancellationToken.ThrowIfCancellationRequested(); + + var examples = new ScenarioExampleSet( + example.TableHeader.Cells.Select(cell => cell.Value).ToImmutableArray(), + example.TableBody.Select(row => row.Cells.Select(cell => cell.Value).ToImmutableArray()).ToImmutableArray(), + example.Tags.Select(tag => tag.Name).ToImmutableArray()); + + exampleSets.Add(examples); + } + + var steps = new List(); + + foreach (var step in scenario.Steps) + { + cancellationToken.ThrowIfCancellationRequested(); + + var scenarioStep = new ScenarioStep( + step.KeywordType, + step.Keyword, + step.Text, + step.Location.Line); + + steps.Add(scenarioStep); + } + + return new ScenarioInformation( + feature, + scenario.Name, + scenario.Tags.Select(tag => tag.Name).ToImmutableArray(), + steps.ToImmutableArray(), + exampleSets.ToImmutableArray(), + ruleName, + ruleTags); + } + + private static IEnumerable GetScenarioInformation( + Rule rule, + FeatureInformation feature, + CancellationToken cancellationToken) + { + var tags = rule.Tags.Select(tag => tag.Name).ToImmutableArray(); + + foreach (var child in rule.Children) + { + cancellationToken.ThrowIfCancellationRequested(); + + switch (child) { - context.ReportDiagnostic(diagnostic); + case Scenario scenario: + yield return GetScenarioInformation(scenario, rule.Name, tags, feature, cancellationToken); + break; } + } + } - // Generate the test fixture source. - var source = feature.TestFrameworkHandler.GenerateTestFixture(feature); + protected abstract CompilationInformation GetCompilationInformation(Compilation compilation); +} + +public record ScenarioStep(StepKeywordType KeywordType, string Keyword, string Text, int LineNumber); - if (source != null) +public class ScenarioExampleSet : IEnumerable>, IEquatable +{ + public ScenarioExampleSet( + ImmutableArray headings, + ImmutableArray> values, + ImmutableArray tags) + { + foreach (var set in values) + { + if (set.Length != headings.Length) { - context.AddSource(feature.FeatureHintName, source); + throw new ArgumentException( + "Values must contain sets with the same number of values as the headings.", + nameof(values)); } - }); + } + + Headings = headings; + Values = values; + Tags = tags; } - protected abstract CompilationInformation GetCompilationInformation(Compilation compilation); + public ImmutableArray Headings { get; } + + public ImmutableArray> Values { get; } + + public ImmutableArray Tags { get; } + + public override bool Equals(object? obj) => Equals(obj as ScenarioExampleSet); + + public bool Equals(ScenarioExampleSet? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return (Headings.Equals(other.Headings) || Headings.SequenceEqual(other.Headings)) && + (Values.Equals(other.Values) || Values.SequenceEqual(other.Values, ImmutableArrayEqualityComparer.Default)) && + (Tags.Equals(other.Tags) || Tags.SequenceEqual(other.Tags)); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 43961407; + + hash *= 32360441 + Headings.GetSequenceHashCode(); + hash *= 32360441 + Values.GetSequenceHashCode(ImmutableArrayEqualityComparer.Default); + hash *= 32360441 + Tags.GetSequenceHashCode(); + + return hash; + } + } + + public IEnumerator> GetEnumerator() + { + foreach (var set in Values) + { + yield return GetAsRow(set); + } + } + + private IEnumerable<(string Name, string Value)> GetAsRow(ImmutableArray set) + { + for (var i = 0; i < Headings.Length; i++) + { + yield return (Name: Headings[i], Value: set[i]); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs new file mode 100644 index 000000000..e519fe676 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs @@ -0,0 +1,102 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public readonly struct TypeIdentifier : IEquatable, IEquatable +{ + public TypeIdentifier(IdentifierString localName) : this(NamespaceString.Empty, localName) + { + } + + public TypeIdentifier(NamespaceString ns, IdentifierString localName) + { + if (localName.IsEmpty && !ns.IsEmpty) + { + throw new ArgumentException( + "An empty local name cannot be combined with a non-empty namespace.", + nameof(localName)); + } + + LocalName = localName; + Namespace = ns; + } + + public readonly bool IsEmpty => LocalName.IsEmpty; + + public IdentifierString LocalName { get; } + + public NamespaceString Namespace { get; } + + public override string? ToString() => Namespace.IsEmpty ? LocalName.ToString() : $"{Namespace}.{LocalName}"; + + public static bool Equals(TypeIdentifier identifierA, TypeIdentifier identifierB) + { + return identifierA.Equals(identifierB); + } + + public static bool Equals(TypeIdentifier identifier, string? s) => identifier.Equals(s); + + public override bool Equals(object obj) + { + return obj switch + { + null => IsEmpty, + TypeIdentifier id => Equals(id), + string s => Equals(s), + _ => false + }; + } + + public bool Equals(TypeIdentifier other) + { + if (IsEmpty) + { + return other.IsEmpty; + } + + return Namespace.Equals(other.Namespace) && LocalName.Equals(other.LocalName); + } + + public bool Equals(string? other) + { + if (string.IsNullOrEmpty(other)) + { + return IsEmpty; + } + + if (Namespace.IsEmpty) + { + return LocalName.Equals(other); + } + + var ns = Namespace.ToString(); + var ln = LocalName.ToString(); + + if (ln.Length + ns.Length + 1 != other!.Length) + { + return false; + } + + var otherSpan = other.AsSpan(); + return otherSpan.Slice(0, ns.Length).Equals(ns.AsSpan(), StringComparison.Ordinal) && + otherSpan[ns.Length].Equals('.') && + otherSpan.Slice(ns.Length + 1).Equals(ln.AsSpan(), StringComparison.Ordinal); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = 1922063139; + hashCode *= -1521134295 + LocalName.GetHashCode(); + hashCode *= -1521134295 + Namespace.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(TypeIdentifier identifierA, TypeIdentifier identifierB) => Equals(identifierA, identifierB); + + public static bool operator !=(TypeIdentifier identifierA, TypeIdentifier identifierB) => !Equals(identifierA, identifierB); + + public static bool operator ==(TypeIdentifier identifier, string s) => Equals(identifier, s); + + public static bool operator !=(TypeIdentifier identifier, string s) => !Equals(identifier, s); +} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs index cb33937a3..e594a6554 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs @@ -4,13 +4,13 @@ namespace Reqnroll.FeatureSourceGenerator.XUnit; public class XUnitCSharpSyntaxGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) { - const string XUnitNamespace = "Xunit"; + private readonly NamespaceString XUnitNamespace = new("Xunit"); protected override IEnumerable GetTestMethodAttributes(Scenario scenario) { var attributes = new List { - new("Fact", XUnitNamespace) + new(new TypeIdentifier(XUnitNamespace, new IdentifierString("Fact"))) }; return base.GetTestMethodAttributes(scenario).Concat(attributes); diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs new file mode 100644 index 000000000..fea3862f1 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs @@ -0,0 +1,22 @@ +using FluentAssertions.Formatting; + +namespace Reqnroll.FeatureSourceGenerator; + +internal class AttributeDescriptorFormatter : IValueFormatter +{ + public bool CanHandle(object value) => value is AttributeDescriptor; + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + var descriptor = (AttributeDescriptor)value; + + if (context.UseLineBreaks) + { + formattedGraph.AddFragmentOnNewLine(descriptor.ToString()); + } + else + { + formattedGraph.AddFragment(descriptor.ToString()); + } + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs index 4d1b59f5f..cd4e967fd 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs @@ -7,14 +7,39 @@ public class AttributeDescriptorTests { public static IEnumerable StringRepresentationExamples { get; } = [ - [ new AttributeDescriptor("Foo", "Bar"), "[Bar.Foo]" ], - [ new AttributeDescriptor("Foo", "Bar", [ "Fizz" ]), "[Bar.Foo(\"Fizz\")]" ], - [ new AttributeDescriptor("Foo", "Bar", [ "Fizz", "Buzz" ]), "[Bar.Foo(\"Fizz\", \"Buzz\")]" ], - [ new AttributeDescriptor("Foo", "Bar", [ 1, 2 ]), "[Bar.Foo(1, 2)]" ], - [ new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create() ]), "[Bar.Foo(new string[] {})]" ], [ - new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create("potato", "pancakes") ]), - "[Bar.Foo(new string[] {\"potato\", \"pancakes\"})]" + new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))), + "[Foo.Bar]" + ], + [ + new AttributeDescriptor( + new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + [ "Fizz" ]), + "[Foo.Bar(\"Fizz\")]" + ], + [ + new AttributeDescriptor( + new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + [ "Fizz", "Buzz" ]), + "[Foo.Bar(\"Fizz\", \"Buzz\")]" + ], + [ + new AttributeDescriptor( + new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + [ 1, 2 ]), + "[Foo.Bar(1, 2)]" + ], + [ + new AttributeDescriptor( + new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + [ ImmutableArray.Create() ]), + "[Foo.Bar(new string[] {})]" + ], + [ + new AttributeDescriptor( + new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + [ ImmutableArray.Create("potato", "pancakes") ]), + "[Foo.Bar(new string[] {\"potato\", \"pancakes\"})]" ] ]; @@ -27,12 +52,12 @@ public void DescriptorsCanBeRepresntedAsStrings(AttributeDescriptor attribute, s public static IEnumerable DescriptorExamples { get; } = [ - [ () => new AttributeDescriptor("Foo", "Bar") ], - [ () => new AttributeDescriptor("Foo", "Bar", [ "Fizz" ]) ], - [ () => new AttributeDescriptor("Foo", "Bar", [ "Fizz", "Buzz" ]) ], - [ () => new AttributeDescriptor("Foo", "Bar", [ 1, 2 ]) ], - [ () => new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create() ]) ], - [ () => new AttributeDescriptor("Foo", "Bar", [ ImmutableArray.Create("potato") ]) ] + [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) ], + [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz" ]) ], + [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz", "Buzz" ]) ], + [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ 1, 2 ]) ], + [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create() ]) ], + [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create("potato") ]) ] ]; [Theory] @@ -64,7 +89,7 @@ public void DescriptorsAreEqualWhenTypeNamespaceAndArgumentsAreEquivalent(Func(T va { var argument = ImmutableArray.Create(value, value); - var attribute = new AttributeDescriptor("Foo", "Bar") + var attribute = new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) .WithPositionalArguments(argument) .WithNamedArguments(new { Property = argument }); @@ -139,7 +164,7 @@ public void DescriptorsCanBeCreatedWithImmutableArraysOfEnumsAsArguments(T va [InlineData(typeof(AttributeDescriptorTests))] public void DescriptorsCanBeCreatedWithTypesAsArguments(Type argument) { - var attribute = new AttributeDescriptor("Foo", "Bar") + var attribute = new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) .WithPositionalArguments(argument) .WithNamedArguments(new { Property = argument }); @@ -154,7 +179,7 @@ public void DescriptorsCannotBeCreatedWithArraysAsArguments() { var argument = Array.Empty(); - var attribute = new AttributeDescriptor("Foo", "Bar"); + var attribute = new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))); attribute .Invoking(attribute => attribute.WithPositionalArguments([ argument ])) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs index 0eb5336ab..5dc5ac29a 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs @@ -1,5 +1,15 @@ -using FluentAssertions.Execution; +using FluentAssertions.Equivalency; +using FluentAssertions.Execution; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using System.Linq.Expressions; +using System.Reflection; +using System.Xml.Linq; +using static System.Formats.Asn1.AsnWriter; namespace Reqnroll.FeatureSourceGenerator; public class CSharpMethodDeclarationAssertions(MethodDeclarationSyntax? subject) : @@ -120,4 +130,230 @@ public AndWhichConstraint HaveAttribute( return new AndWhichConstraint((TAssertions)this, match!); } + + public AndConstraint HaveAttribuesEquivalentTo( + AttributeDescriptor[] expectation, + string because = "", + params object[] becauseArgs) => + HaveAttribuesEquivalentTo((IEnumerable)expectation, because, becauseArgs); + + public AndConstraint HaveAttribuesEquivalentTo( + IEnumerable expectation, + string because = "", + params object[] becauseArgs) + { + using var scope = new AssertionScope(); + + scope.FormattingOptions.UseLineBreaks = true; + + var assertion = Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:the method} to have attributes equivalent to {0}", expectation) + .ForCondition(Subject is not null) + .FailWith("but {context:the method} is ."); + + var expected = expectation.ToList(); + var actual = Subject!.AttributeLists + .SelectMany(list => list.Attributes) + .Select(SyntaxInterpreter.GetAttributeDescriptor) + .ToList(); + + + var missing = expected.ToList(); + var extra = actual.ToList(); + + foreach (var item in expected) + { + if (extra.Remove(item)) + { + missing.Remove(item); + } + } + + if (missing.Count > 0) + { + if (extra.Count > 0) + { + assertion + .Then + .FailWith("but the method is missing attributes {0} and has extra attributes {1}", missing, extra); + } + else + { + assertion + .Then + .FailWith("but the method is missing attributes {0}", missing); + } + } + else if (extra.Count > 0) + { + assertion + .Then + .FailWith("but the method has extra attributes {0}", extra); + } + + return new AndConstraint((TAssertions)this); + } + + public AndConstraint HaveParametersEquivalentTo( + IEnumerable expectation, + string because = "", + params object[] becauseArgs) + { + using var scope = new AssertionScope(); + + scope.FormattingOptions.UseLineBreaks = true; + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:the method} to have parameters equivalent to {0} {reason}", expectation) + .ForCondition(Subject is not null) + .FailWith("but {context:the method} is ."); + + var expected = expectation.ToList(); + var actual = Subject!.ParameterList.Parameters; + + for (var i = 0; i < expected.Count; i++) + { + var expectedItem = expected[i]; + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:the method} to have parameter {0}: \"{1}\" {reason}", i+1, expectedItem) + .ForCondition(Subject.ParameterList.Parameters.Count != 0) + .FailWith("but {context:the method} has no parameters defined.", Subject.ParameterList.Parameters.Count) + .Then + .ForCondition(Subject.ParameterList.Parameters.Count > i) + .FailWith("but {context:the method} only has {0} parameters defined.", Subject.ParameterList.Parameters.Count) + .Then + .Given(() => Subject.ParameterList.Parameters[i]) + .ForCondition(actual => false)// actual.IsEquivalentTo(expectedItem, true)) + .FailWith("but found \"{0}\".", expectedItem); + } + + if (expected.Count < actual.Count) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:the method} to have parameters equivalent to {0}{reason}", expectation) + .FailWith("but {context:the method} has extra parameters {0}.", actual.Skip(expected.Count)); + } + + return new AndConstraint((TAssertions)this); + } +} + +internal static class SyntaxInterpreter +{ + public static AttributeDescriptor GetAttributeDescriptor(AttributeSyntax attribute) + { + var type = attribute.Name switch + { + QualifiedNameSyntax qname => new TypeIdentifier( + new NamespaceString( + qname.Left.ToString().StartsWith("global::") ? + qname.Left.ToString()[8..] : + qname.Left.ToString()), + new IdentifierString(qname.Right.ToString())), + _ => throw new NotImplementedException() + }; + + ImmutableArray positionalArguments; + ImmutableDictionary namedArguments; + + if (attribute.ArgumentList == null) + { + positionalArguments = []; + namedArguments = ImmutableDictionary.Empty; + } + else + { + positionalArguments = attribute.ArgumentList.Arguments + .Where(arg => arg.NameEquals == null) + .Select(arg => GetLiteralValue(arg.Expression)) + .ToImmutableArray(); + + namedArguments = attribute.ArgumentList.Arguments + .Where(arg => arg.NameEquals != null) + .ToImmutableDictionary(arg => arg.NameEquals!.Name.ToString(), arg => GetLiteralValue(arg.Expression)); + } + + return new AttributeDescriptor( + type, + positionalArguments, + namedArguments); + } + + public static object? GetLiteralValue(ExpressionSyntax expression) => expression switch + { + LiteralExpressionSyntax literal => literal.Token.Value, + ArrayCreationExpressionSyntax arrayCreation => GetLiteralValue(arrayCreation), + _ => throw new NotSupportedException( + $"Obtaining literal value of expressions of type \"{expression.GetType().Name}\" is not supported.") + }; + + private static object GetLiteralValue(ArrayCreationExpressionSyntax expression) + { + if (expression.Type.RankSpecifiers.Count > 1) + { + throw new NotSupportedException( + "Getting literal values of mutli-dimensional arrays is not supported."); + } + + var rank = expression.Type.RankSpecifiers[0]; + + if (rank.Sizes.Count > 1) + { + throw new NotSupportedException( + "Getting literal values of mutli-dimensional arrays is not supported."); + } + + Type itemType = expression.Type.ElementType switch + { + PredefinedTypeSyntax predefined => predefined switch + { + { Keyword.Text: "string" } => typeof(string), + { Keyword.Text: "object" } => typeof(object), + _ => throw new NotSupportedException($"Getting array literals of predefined type {predefined} is not supported.") + }, + _ => throw new NotSupportedException($"Getting array literals of type {expression.Type.ElementType} is not supported.") + }; + + var size = rank.Sizes[0]; + + int? arrayLength = size switch + { + OmittedArraySizeExpressionSyntax => null, + LiteralExpressionSyntax literal => literal.Kind() == SyntaxKind.NumericLiteralExpression ? + (int)GetLiteralValue(literal)! : + throw new InvalidOperationException(), + _ => throw new NotSupportedException() + }; + + return expression.Initializer is null ? + CreateImmutableArray(itemType, arrayLength!.Value) : + CreateImmutableArray(itemType, expression.Initializer.Expressions.Select(GetLiteralValue)); + } + + private static object CreateImmutableArray(Type itemType, IEnumerable values) + { + return typeof(SyntaxInterpreter) + .GetMethod(nameof(CreateImmutableArrayFromValues), BindingFlags.Static | BindingFlags.NonPublic)! + .MakeGenericMethod(itemType) + .Invoke(null, [values])!; + } + + private static ImmutableArray CreateImmutableArrayFromValues(IEnumerable values) => + values.Cast().ToImmutableArray(); + + private static object CreateImmutableArray(Type itemType, int size) + { + return typeof(SyntaxInterpreter) + .GetMethod(nameof(CreateImmutableArrayOfSize), BindingFlags.Static | BindingFlags.NonPublic)! + .MakeGenericMethod(itemType) + .Invoke(null, [size])!; + } + + private static ImmutableArray CreateImmutableArrayOfSize(int size) => + new T[size].ToImmutableArray(); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/IdentifierStringTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/IdentifierStringTests.cs new file mode 100644 index 000000000..29d0c16f4 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/IdentifierStringTests.cs @@ -0,0 +1,147 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public class IdentifierStringTests +{ + [Fact] + public void DefaultValue_IsEmpty() + { + var identifier = default(IdentifierString); + + identifier.IsEmpty.Should().BeTrue(); + identifier.ToString().Should().Be(""); + } + + [Theory] + [InlineData("Parser")] + [InlineData("__Parser")] + [InlineData("X509")] + public void Constructor_CreatesIdentifierStringFromValidValue(string name) + { + var identifier = new IdentifierString(name); + + identifier.IsEmpty.Should().BeFalse(); + identifier.ToString().Should().Be(name); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Constructor_CreatesEmptyIdentifierStringFromEmptyValue(string name) + { + var identifier = new IdentifierString(name); + + identifier.IsEmpty.Should().BeTrue(); + identifier.ToString().Should().Be(""); + } + + [Theory] + [InlineData(".FeatureSourceGenerator")] + [InlineData("FeatureSourceGenerator.")] + [InlineData("Reqnroll.FeatureSourceGenerator")] + [InlineData("1FeatureSourceGenerator")] + public void Constructor_ThrowsArgumentExceptionWhenUsingAnInvalidLocalNameValue(string name) + { + Func ctr = () => new IdentifierString(name); + + ctr.Should().Throw(); + } + + [Theory] + [InlineData("Parser", "Parser")] + [InlineData("_Parser", "_Parser")] + [InlineData("", "")] + [InlineData(null, null)] + [InlineData(null, "")] + public void EqualsIdentifierString_ReturnsTrueWhenValuesMatch(string id1, string id2) + { + new IdentifierString(id1).Equals(new IdentifierString(id2)).Should().BeTrue(); + } + + [Theory] + [InlineData("Parser", "parser")] + [InlineData("Parser", "_Parser")] + public void EqualsIdentifierString_ReturnsFalseWhenValuesDoNotMatch(string id1, string id2) + { + new IdentifierString(id1).Equals(new IdentifierString(id2)).Should().BeFalse(); + } + + [Theory] + [InlineData("Parser", "Parser", true)] + [InlineData("_Parser", "_Parser", true)] + [InlineData("_Parser", "_parser", false)] + [InlineData(null, "", true)] + [InlineData(null, null, true)] + [InlineData("", null, true)] + public void EqualsString_ReturnsCaseSensitiveEquivalence(string? id, string? identifier, bool expected) + { + new IdentifierString(id).Equals(identifier).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", "Parser")] + [InlineData("Internal", "Internal")] + [InlineData("_1XYZ", "_1XYZ")] + [InlineData("__Internal", "__Internal")] + [InlineData("", "")] + [InlineData(null, "")] + public void GetHashCode_ReturnsSameValueForEquivalentValues(string? id1, string? id2) + { + new IdentifierString(id1).GetHashCode().Should().Be(new IdentifierString(id2).GetHashCode()); + } + + [Theory] + [InlineData("Parser", "Parser", true)] + [InlineData("Parser", "parser", false)] + [InlineData("_Parser", "_Parser", true)] + [InlineData("_Parser", "_parser", false)] + [InlineData(null, "", true)] + [InlineData(null, null, true)] + [InlineData("", null, true)] + public void EqualityOperatorWithString_ReturnsEquivalenceWithCaseSensitivity(string? id, string? identifier, bool expected) + { + (new IdentifierString(id) == identifier).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", "Parser", true)] + [InlineData("_Parser" , "_Parser", true)] + [InlineData("_Parser","_parser", false)] + [InlineData("Parser", "parser", false)] + [InlineData("Parser", "_Parser", false)] + public void EqualityOperatorWithIdentifierString_ReturnsEquivalenceWithCaseSensitivity(string? id1, string? id2, bool expected) + { + (new IdentifierString(id1) == new IdentifierString(id2)).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", "Parser", false)] + [InlineData("Parser", "parser", true)] + [InlineData("_Parser", "_Parser", false)] + [InlineData("_Parser", "_parser", true)] + [InlineData(null, "", false)] + [InlineData(null, null, false)] + [InlineData("", null, false)] + public void InequalityOperatorWithString_ReturnsNonEquivalenceWithCaseSensitivity( + string? id, + string? identifier, + bool expected) + { + (new IdentifierString(id) != identifier).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", "Parser", false)] + [InlineData("Parser", "parser", true)] + [InlineData("_Parser", "_Parser", false)] + [InlineData("_Parser", "_parser", true)] + [InlineData(null, "", false)] + [InlineData(null, null, false)] + [InlineData("", null, false)] + public void InequalityOperatorWithIdentifierString_ReturnsNonEquivalenceWithCaseSensitivity( + string? id1, + string? id2, + bool expected) + { + (new IdentifierString(id1) != new IdentifierString(id2)).Should().Be(expected); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/Initializer.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/Initializer.cs new file mode 100644 index 000000000..f56fab7ee --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/Initializer.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +namespace Reqnroll.FeatureSourceGenerator; +internal static class Initializer +{ + [ModuleInitializer] + public static void SetDefaults() + { + FluentAssertions.Formatting.Formatter.AddFormatter(new AttributeDescriptorFormatter()); + FluentAssertions.Formatting.Formatter.AddFormatter(new ParameterSyntaxFormatter()); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs index 3e1e852ad..1af213d05 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs @@ -1,11 +1,18 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Reqnroll.FeatureSourceGenerator; using Reqnroll.FeatureSourceGenerator.CSharp; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Data.Common; +using Xunit.Abstractions; namespace Reqnroll.FeatureSourceGenerator; -public class MSTestFeatureSourceGeneratorTests +using static SyntaxFactory; + +public class MSTestFeatureSourceGeneratorTests(ITestOutputHelper output) { [Fact] public void GeneratorProducesMSTestOutputWhenWhenBuildPropertyConfiguredForMSTest() @@ -98,10 +105,148 @@ Then the result should be 120 var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() .Which.Should().BeAssignableTo().Subject!; + output.WriteLine($"Generated source:\n{generatedSyntaxTree}"); + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().HaveSingleAttribute("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass"); } + + [Fact] + public void GeneratorProducesMSTestDataRowsForScenarioExamples() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); + + const string featureText = + """ + #language: en + @featureTag1 + Feature: Sample + + Scenario Outline: Sample Scenario Outline + When happens + @example_tag + Examples: + | what | + | foo | + | bar | + Examples: Second example without tags - in this case the tag list is null. + | what | + | baz | + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { "build_property.ReqnrollTargetTestFramework", "MSTest" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Sample.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() + .Which.Should().BeAssignableTo().Subject!; + + output.WriteLine($"Generated source:\n{generatedSyntaxTree}"); + + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); + + generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") + .Which.Should().ContainSingleClassDeclaration("SampleFeature") + .Which.Should().ContainMethod("SampleScenarioOutline") + .Which.Should().HaveAttribuesEquivalentTo( + [ + MSTestSyntax.Attribute("TestMethod"), + MSTestSyntax.Attribute("Description", "Sample Scenario Outline"), + MSTestSyntax.Attribute("TestProperty", "FeatureTitle", "Sample"), + MSTestSyntax.Attribute("TestCategory", "featureTag1"), + MSTestSyntax.Attribute("DataRow", "foo", ImmutableArray.Create( ImmutableArray.Create("example_tag") )), + MSTestSyntax.Attribute("DataRow", "bar", ImmutableArray.Create( ImmutableArray.Create("example_tag") )), + MSTestSyntax.Attribute("DataRow", "baz", ImmutableArray.Create( ImmutableArray.Empty )) + ]); + //.And.HaveParametersEquivalentTo( + //[ + // Parameter(Identifier("what")).WithType(PredefinedType(Token(SyntaxKind.StringKeyword))), + // Parameter(Identifier("exampleTags")).WithType(ArrayType(PredefinedType(Token(SyntaxKind.StringKeyword)))) + //]); + } +} + +internal static class MSTestSyntax +{ + private static readonly NamespaceString Namespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); + + public static AttributeDescriptor Attribute(string type, params object?[] args) + { + return new AttributeDescriptor( + new TypeIdentifier(Namespace, new IdentifierString(type)), + [.. args]); + } + + private static ExpressionSyntax Argument(object? arg) + { + return arg switch + { + string s => SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(s)), + + string[] array => ArrayCreation(array), + object[] array => ArrayCreation(array), + + _ => throw new NotImplementedException() + }; + } + + private static ArrayCreationExpressionSyntax ArrayCreation(string[] array) => + ArrayCreation( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + array); + + private static ArrayCreationExpressionSyntax ArrayCreation(object[] array) => + ArrayCreation( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)), + array); + + private static ArrayCreationExpressionSyntax ArrayCreation(TypeSyntax type, T[] array) + { + var creation = SyntaxFactory.ArrayCreationExpression( + SyntaxFactory.ArrayType( + type, + SyntaxFactory.SingletonList( + SyntaxFactory.ArrayRankSpecifier( + SyntaxFactory.SingletonSeparatedList( + array.Length > 0 ? + SyntaxFactory.LiteralExpression( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal(0)) : + SyntaxFactory.OmittedArraySizeExpression()))))); + + if (array.Length > 0) + { + return creation + .WithInitializer( + SyntaxFactory.InitializerExpression( + SyntaxKind.ArrayInitializerExpression, + SyntaxFactory.SeparatedList( + array.Select(arg => Argument(arg))))); + } + else + { + return creation; + } + } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/NamespaceStringTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/NamespaceStringTests.cs new file mode 100644 index 000000000..9badbba30 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/NamespaceStringTests.cs @@ -0,0 +1,132 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public class NamespaceStringTests +{ + [Fact] + public void DefaultInstance_IsEmpty() + { + var ns = default(NamespaceString); + + ns.IsEmpty.Should().BeTrue(); + ns.ToString().Should().Be(""); + } + + [Theory] + [InlineData("Reqnoll")] + [InlineData("Reqnoll.FeatureSourceGenerator")] + [InlineData("__Internal")] + [InlineData("_1XYZ")] + [InlineData("Reqnroll.__Internal")] + public void Constructor_CreatesNamespaceValueFromValidString(string s) + { + var ns = new NamespaceString(s); + + ns.ToString().Should().Be(s); + ns.IsEmpty.Should().BeFalse(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Construtor_CreatesEmptyNamespaceValueFromEmptyString(string s) + { + var ns = new NamespaceString(s); + + ns.ToString().Should().Be(""); + ns.IsEmpty.Should().BeTrue(); + } + + [Theory] + [InlineData(".Reqnroll")] + [InlineData("Reqnroll.")] + [InlineData(".Reqnroll.FeatureSourceGenerator")] + [InlineData("Reqnroll.FeatureSourceGenerator.")] + [InlineData("Reqnroll.FeatureSourceGenerator.1")] + [InlineData("Reqnroll..FeatureSourceGenerator")] + public void Constructor_ThrowsArgumentExceptionWhenStringIsNotValidAsNamespace(string s) + { + Func ctr = () => new NamespaceString(s); + ctr.Should().Throw(); + } + + [Theory] + [InlineData("Reqnroll", "Reqnroll", true)] + [InlineData("Reqnroll", "reqnroll", false)] + [InlineData("Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator", true)] + [InlineData("__Internal", "__Internal", true)] + [InlineData("__Internal", "__internal", false)] + public void EqualsNamespaceString_ReturnsEquivalenceWithCaseSensitivity(string a, string b, bool expected) + { + new NamespaceString(a).Equals(new NamespaceString(b)).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Reqnroll", true)] + [InlineData("Reqnroll", "reqnroll", false)] + [InlineData("Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator", true)] + [InlineData("__Internal", "__Internal", true)] + [InlineData("__Internal", "__internal", false)] + [InlineData("__Internal", "1234", false)] + [InlineData("__Internal", ".Reqnroll", false)] + public void EqualsString_ReturnsEquivalenceWithCaseSensitivity(string a, string b, bool expected) + { + new NamespaceString(a).Equals(b).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnoll")] + [InlineData("Reqnoll.FeatureSourceGenerator")] + [InlineData("__Internal")] + [InlineData("_1XYZ")] + [InlineData("Reqnroll.__Internal")] + [InlineData("")] + [InlineData(null)] + public void GetHashCode_ReturnsSameValueForEquivalentValues(string s) + { + new NamespaceString(s).GetHashCode().Should().Be(new NamespaceString(s).GetHashCode()); + } + + [Theory] + [InlineData("Reqnroll", "Reqnroll", true)] + [InlineData("Reqnroll", "reqnroll", false)] + [InlineData("Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator", true)] + [InlineData("__Internal", "__Internal", true)] + [InlineData("__Internal", "__internal", false)] + public void EqualityOperatorWithString_ReturnsEquivalenceWithCaseSensitivity(string a, string b, bool expected) + { + (new NamespaceString(a) == new NamespaceString(b)).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Reqnroll", true)] + [InlineData("Reqnroll", "reqnroll", false)] + [InlineData("Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator", true)] + [InlineData("__Internal", "__Internal", true)] + [InlineData("__Internal", "__internal", false)] + public void EqualityOperatorWithNamespaceString_ReturnsEquivalenceWithCaseSensitivity(string a, string b, bool expected) + { + (new NamespaceString(a) == b).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Reqnroll", false)] + [InlineData("Reqnroll", "reqnroll", true)] + [InlineData("Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator", false)] + [InlineData("__Internal", "__Internal", false)] + [InlineData("__Internal", "__internal", true)] + public void InequalityOperatorWithString_ReturnsNonEquivalenceWithCaseSensitivity(string a, string b, bool expected) + { + (new NamespaceString(a) != new NamespaceString(b)).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Reqnroll", false)] + [InlineData("Reqnroll", "reqnroll", true)] + [InlineData("Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator", false)] + [InlineData("__Internal", "__Internal", false)] + [InlineData("__Internal", "__internal", true)] + public void InequalityOperatorWithNamespaceString_ReturnsNonEquivalenceWithCaseSensitivity(string a, string b, bool expected) + { + (new NamespaceString(a) != b).Should().Be(expected); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterSyntaxFormatter.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterSyntaxFormatter.cs new file mode 100644 index 000000000..ab07f6c01 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterSyntaxFormatter.cs @@ -0,0 +1,31 @@ +using FluentAssertions.Formatting; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Reqnroll.FeatureSourceGenerator; +internal class ParameterSyntaxFormatter : IValueFormatter +{ + public bool CanHandle(object value) => value is ParameterSyntax; + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + var syntax = (ParameterSyntax)value; + + var typeString = syntax.Type switch + { + ArrayTypeSyntax array => $"{array.ElementType}[]", + not null => syntax.Type.ToString(), + null => "" + }; + + var s = $"{typeString} {syntax.Identifier}"; + + if (context.UseLineBreaks) + { + formattedGraph.AddFragmentOnNewLine(s); + } + else + { + formattedGraph.AddFragment(s); + } + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/TypeIdentifierTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/TypeIdentifierTests.cs new file mode 100644 index 000000000..7a6bcaa6f --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/TypeIdentifierTests.cs @@ -0,0 +1,228 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public class TypeIdentifierTests +{ + [Fact] + public void DefaultValue_IsEmpty() + { + var identifier = default(TypeIdentifier); + + identifier.IsEmpty.Should().BeTrue(); + identifier.LocalName.IsEmpty.Should().BeTrue(); + identifier.Namespace.IsEmpty.Should().BeTrue(); + identifier.ToString().Should().Be(""); + } + + [Theory] + [InlineData("Parser")] + [InlineData("__Parser")] + [InlineData("X509")] + public void Constructor_CreatesTypeIdentifierFromValidName(string name) + { + var identifier = new TypeIdentifier(new IdentifierString(name)); + + identifier.IsEmpty.Should().BeFalse(); + identifier.LocalName.Should().Be(name); + identifier.Namespace.IsEmpty.Should().BeTrue(); + identifier.ToString().Should().Be(name); + } + + [Theory] + [InlineData("Reqnroll", "Parser")] + [InlineData("Reqnroll", "__Parser")] + [InlineData("Reqnroll", "X509")] + public void Constructor_CreatesTypeIdentifierFromValidNameAndNamespace(string ns, string name) + { + var nsx = new NamespaceString(ns); + + var identifier = new TypeIdentifier(nsx, new IdentifierString(name)); + + identifier.IsEmpty.Should().BeFalse(); + identifier.LocalName.Should().Be(name); + identifier.Namespace.Should().Be(nsx); + identifier.ToString().Should().Be($"{ns}.{name}"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Constructor_CreatesEmptyTypeIdentifierFromEmptyNameValue(string name) + { + var identifier = new TypeIdentifier(new IdentifierString(name)); + + identifier.IsEmpty.Should().BeTrue(); + identifier.LocalName.IsEmpty.Should().BeTrue(); + identifier.Namespace.IsEmpty.Should().BeTrue(); + identifier.ToString().Should().Be(""); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyNameWithANonEmptyNamespace(string name) + { + Func ctr = () => new TypeIdentifier(new NamespaceString("Reqnroll"), new IdentifierString(name)); + + ctr.Should().Throw(); + } + + [Theory] + [InlineData(".FeatureSourceGenerator")] + [InlineData("FeatureSourceGenerator.")] + [InlineData("Reqnroll.FeatureSourceGenerator")] + [InlineData("1FeatureSourceGenerator")] + public void Constructor_ThrowsArgumentExceptionWhenUsingAnInvalidLocalNameValue(string name) + { + Func ctr = () => new TypeIdentifier(new IdentifierString(name)); + + ctr.Should().Throw(); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser")] + [InlineData("Reqnroll", "_Parser", "Reqnroll", "_Parser")] + public void EqualsTypeIdentifier_ReturnsTrueWhenNamespacesAndLocalNameMatches( + string ns1, + string name1, + string ns2, + string name2) + { + var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + + typeId1.Equals(typeId2).Should().BeTrue(); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "System", "Parser")] + [InlineData("Reqnroll", "_Parser", "System", "_Parser")] + public void EqualsTypeIdentifier_ReturnsFalseWhenNamespaceDoesNotMatch( + string ns1, + string name1, + string ns2, + string name2) + { + var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + + typeId1.Equals(typeId2).Should().BeFalse(); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll", "parser")] + [InlineData("Reqnroll", "Parser", "Reqnroll", "_Parser")] + public void EqualsTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( + string ns1, + string name1, + string ns2, + string name2) + { + var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + + typeId1.Equals(typeId2).Should().BeFalse(); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll.Parser", true)] + [InlineData("Reqnroll", "_Parser", "Reqnroll._Parser", true)] + [InlineData(null, "_Parser", "_Parser", true)] + [InlineData(null, null, "", true)] + [InlineData(null, null, null, true)] + [InlineData("", "", null, true)] + public void EqualsString_ReturnsCaseSensitiveEquivalence(string ns, string name, string identifier, bool expected) + { + new TypeIdentifier(new NamespaceString(ns), new IdentifierString(name)).Equals(identifier).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser")] + [InlineData("Reqnroll", "Internal", "Reqnroll", "Internal")] + [InlineData(null, "_1XYZ", null, "_1XYZ")] + [InlineData("", "__Internal", "", "__Internal")] + [InlineData("", "", "", "")] + [InlineData(null, null, "", "")] + public void GetHashCode_ReturnsSameValueForEquivalentValues( + string ns1, + string name1, + string ns2, + string name2) + { + var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + + typeId1.GetHashCode().Should().Be(typeId2.GetHashCode()); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll.Parser", true)] + [InlineData("Reqnroll", "Parser", "Reqnroll.parser", false)] + [InlineData("Reqnroll", "_Parser", "Reqnroll._Parser", true)] + [InlineData(null, "_Parser", "_Parser", true)] + [InlineData(null, "_Parser", "_parser", false)] + [InlineData(null, null, "", true)] + [InlineData(null, null, null, true)] + [InlineData("", "", null, true)] + public void EqualityOperatorWithString_ReturnsEquivalenceWithCaseSensitivity(string ns, string name, string id, bool expected) + { + (new TypeIdentifier(new NamespaceString(ns), new IdentifierString(name)) == id).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser", true)] + [InlineData("Reqnroll", "_Parser", "Reqnroll", "_Parser", true)] + [InlineData("Reqnroll", "Parser", "System", "Parser", false)] + [InlineData("Reqnroll", "_Parser", "System", "_Parser", false)] + [InlineData("Reqnroll", "Parser", "Reqnroll", "parser", false)] + [InlineData("Reqnroll", "Parser", "Reqnroll", "_Parser", false)] + public void EqualityOperatorWithTypeIdentifier_ReturnsEquivalenceWithCaseSensitivity( + string ns1, + string name1, + string ns2, + string name2, + bool expected) + { + var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + + (typeId1 == typeId2).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll.Parser", false)] + [InlineData("Reqnroll", "Parser", "Reqnroll.parser", true)] + [InlineData("Reqnroll", "_Parser", "Reqnroll._Parser", false)] + [InlineData(null, "_Parser", "_Parser", false)] + [InlineData(null, "_Parser", "_parser", true)] + [InlineData(null, null, "", false)] + [InlineData(null, null, null, false)] + [InlineData("", "", null, false)] + public void InequalityOperatorWithString_ReturnsNonEquivalenceWithCaseSensitivity( + string ns, + string name, + string id, + bool expected) + { + (new TypeIdentifier(new NamespaceString(ns), new IdentifierString(name)) != id).Should().Be(expected); + } + + [Theory] + [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser", false)] + [InlineData("Reqnroll", "_Parser", "Reqnroll", "_Parser", false)] + [InlineData("Reqnroll", "Parser", "System", "Parser", true)] + [InlineData("Reqnroll", "_Parser", "System", "_Parser", true)] + [InlineData("Reqnroll", "Parser", "Reqnroll", "parser", true)] + [InlineData("Reqnroll", "Parser", "Reqnroll", "_Parser", true)] + public void InequalityOperatorWithTypeIdentifier_ReturnsNonEquivalenceWithCaseSensitivity( + string ns1, + string name1, + string ns2, + string name2, + bool expected) + { + var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + + (typeId1 != typeId2).Should().Be(expected); + } +} From ffa4241893ce4ca37ea5938eaf0b32158295823d Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Fri, 14 Jun 2024 23:33:06 +0100 Subject: [PATCH 19/48] Refactor towards model-based generation --- .../CSharp/CSharpSourceTextBuilder.cs | 4 +- ...rioMethod.cs => CSharpTestFixtureClass.cs} | 59 ++++++----- .../CSharp/CSharpTestMethod.cs | 69 +++++++++++++ .../FeatureInformation.cs | 3 +- .../ITestFixtureGenerator.cs | 29 ++++++ .../ITestFrameworkHandler.cs | 33 +++++-- .../MSTestCSharpTestFixtureGenerator.cs | 23 +++++ .../MSTest/MSTestHandler.cs | 26 ++--- .../NUnit/NUnitCSharpTestFixtureGenerator.cs | 20 ++++ .../NUnit/NUnitHandler.cs | 6 +- .../ParameterDescriptor.cs | 45 ++++++--- .../TestFixture.cs | 21 ---- .../TestFixtureClass.cs | 97 +++++++++++++++++++ .../TestFixtureComposition.cs | 6 +- .../TestFixtureMethod.cs | 5 - .../TestFixtureSourceGenerator.cs | 11 +-- Reqnroll.FeatureSourceGenerator/TestMethod.cs | 82 ++++++++++++++++ .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 20 ++++ .../XUnit/XUnitHandler.cs | 6 +- 19 files changed, 455 insertions(+), 110 deletions(-) rename Reqnroll.FeatureSourceGenerator/CSharp/{CSharpScenarioMethod.cs => CSharpTestFixtureClass.cs} (51%) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs create mode 100644 Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/TestFixture.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestMethod.cs create mode 100644 Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs index a6ed28947..774e84d23 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -240,7 +240,7 @@ public SourceText ToSourceText() public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribute) { Append('['); - AppendTypeIdentifier(attribute.Type); + AppendTypeReference(attribute.Type); if (attribute.NamedArguments.Count > 0 || attribute.PositionalArguments.Length > 0) { @@ -267,7 +267,7 @@ public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribut return this; } - public CSharpSourceTextBuilder AppendTypeIdentifier(TypeIdentifier identifier) + public CSharpSourceTextBuilder AppendTypeReference(TypeIdentifier identifier) { return this; } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpScenarioMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs similarity index 51% rename from Reqnroll.FeatureSourceGenerator/CSharp/CSharpScenarioMethod.cs rename to Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index 059f43e75..6df3d764c 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpScenarioMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -1,33 +1,42 @@ using System.Collections.Immutable; +using System.Text; namespace Reqnroll.FeatureSourceGenerator.CSharp; /// /// Represents a class which is a test fixture to execute the scenarios associated with a feature. /// -public class CSharpTestFixtureClass -{ +public class CSharpTestFixtureClass : TestFixtureClass +{ + private static readonly Encoding Encoding = new UTF8Encoding(false); + public CSharpTestFixtureClass( TypeIdentifier identifier, + string hintName, ImmutableArray attributes = default, - ImmutableArray methods = default) + ImmutableArray methods = default) : + base( + identifier, + hintName, + attributes) { - if (identifier.IsEmpty) - { - throw new ArgumentException("Value cannot be an empty identifier.", nameof(identifier)); - } + Methods = methods.IsDefault ? ImmutableArray.Empty : methods; + } - Identifier = identifier; + public ImmutableArray Methods { get; } - Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; - Methods = methods.IsDefault ? ImmutableArray.Empty : methods; - } + public override IEnumerable GetMethods() => Methods; + + public override SourceText Render() + { + var buffer = new CSharpSourceTextBuilder(); - public TypeIdentifier Identifier { get; } - public ImmutableArray Attributes { get; } - public ImmutableArray Methods { get; } + RenderTo(buffer); - public void WriteTo(CSharpSourceTextBuilder sourceBuilder) + return SourceText.From(buffer.ToString(), Encoding); + } + + public void RenderTo(CSharpSourceTextBuilder sourceBuilder) { sourceBuilder.Append("namespace ").Append(Identifier.Namespace.ToString()).AppendLine(); sourceBuilder.BeginBlock("{"); @@ -44,22 +53,22 @@ public void WriteTo(CSharpSourceTextBuilder sourceBuilder) sourceBuilder.Append("public class ").Append(Identifier.LocalName.ToString()); sourceBuilder.BeginBlock("{"); - WriteTestFixturePreambleTo(sourceBuilder); + RenderTestFixturePreambleTo(sourceBuilder); sourceBuilder.AppendLine(); - WriteScenarioMethodsTo(sourceBuilder); + RenderMethodsTo(sourceBuilder); sourceBuilder.EndBlock("}"); sourceBuilder.EndBlock("}"); } - protected virtual void WriteTestFixturePreambleTo(CSharpSourceTextBuilder sourceBuilder) + protected virtual void RenderTestFixturePreambleTo(CSharpSourceTextBuilder sourceBuilder) { throw new NotImplementedException(); } - protected virtual void WriteScenarioMethodsTo(CSharpSourceTextBuilder sourceBuilder) + protected virtual void RenderMethodsTo(CSharpSourceTextBuilder sourceBuilder) { var first = true; foreach (var method in Methods) @@ -69,7 +78,7 @@ protected virtual void WriteScenarioMethodsTo(CSharpSourceTextBuilder sourceBuil sourceBuilder.AppendLine(); } - method.WriteTo(sourceBuilder); + method.RenderTo(sourceBuilder); if (first) { @@ -78,13 +87,3 @@ protected virtual void WriteScenarioMethodsTo(CSharpSourceTextBuilder sourceBuil } } } - -public abstract class CSharpScenarioMethod(string name) -{ - public string Name { get; } = name; - - public void WriteTo(CSharpSourceTextBuilder sourceBuilder) - { - throw new NotImplementedException(); - } -} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs new file mode 100644 index 000000000..6d87c4054 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs @@ -0,0 +1,69 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +public abstract class CSharpTestMethod( + IdentifierString identifier, + ImmutableArray attributes = default, + ImmutableArray parameters = default) : + TestMethod(identifier, attributes, parameters), IEquatable +{ + public override bool Equals(object obj) => Equals(obj as CSharpTestMethod); + + public bool Equals(CSharpTestMethod? other) => base.Equals(other); + + public override int GetHashCode() => base.GetHashCode(); + + public void RenderTo(CSharpSourceTextBuilder sourceBuilder) + { + if (!Attributes.IsEmpty) + { + foreach (var attribute in Attributes) + { + sourceBuilder.AppendAttributeBlock(attribute); + sourceBuilder.AppendLine(); + } + } + + // Our test methods are always asynchronous and never return a value. + sourceBuilder.Append("public async Task ").Append(Identifier); + + if (!Parameters.IsEmpty) + { + sourceBuilder.BeginBlock("("); + + var first = true; + foreach (var parameter in Parameters) + { + if (!first) + { + sourceBuilder.AppendLine(","); + } + + sourceBuilder + .AppendTypeReference(parameter.Type) + .Append(' ') + .Append(parameter.Name); + + first = false; + } + + sourceBuilder.EndBlock(")"); + } + else + { + sourceBuilder.AppendLine("()"); + } + + sourceBuilder.BeginBlock("{"); + + RenderMethodBodyTo(sourceBuilder); + + sourceBuilder.EndBlock("}"); + } + + protected virtual void RenderMethodBodyTo(CSharpSourceTextBuilder sourceBuilder) + { + + } +} diff --git a/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs b/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs index 0dd8a8758..f8f2c8676 100644 --- a/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs +++ b/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs @@ -7,4 +7,5 @@ public record FeatureInformation( string FeatureHintName, string FeatureNamespace, CompilationInformation CompilationInformation, - ITestFrameworkHandler TestFrameworkHandler); + ITestFrameworkHandler TestFrameworkHandler, + ITestFixtureGenerator TestFixtureGenerator); diff --git a/Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs new file mode 100644 index 000000000..f39309945 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs @@ -0,0 +1,29 @@ +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Defines a component which generates test-fixture classes. +/// +public interface ITestFixtureGenerator +{ + /// + /// Generates a test method for a scenario. + /// + /// The scenario to create a test method for. + /// A token used to signal when generation should be canceled. + /// A representing the generated method. + TestMethod GenerateTestMethod( + ScenarioInformation scenario, + CancellationToken cancellationToken = default); + + /// + /// Generates a test fixture class for a feature, incorporating a set of generated methods. + /// + /// The feature to generate the test-fixture class for. + /// The collection of methods to incorporate into the fixture. + /// A token used to signal when generation should be canceled. + /// A represented the generated test-fixture class. + TestFixtureClass GenerateTestFixture( + FeatureInformation feature, + IEnumerable methods, + CancellationToken cancellationToken = default); +} diff --git a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs index 9944a2a78..3c8bebe6a 100644 --- a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs @@ -1,17 +1,34 @@ namespace Reqnroll.FeatureSourceGenerator; +/// +/// Defines a component that handles the aspects of generating code for a target test framework. +/// public interface ITestFrameworkHandler { + /// + /// Gets the name of the test framework this handler supports. + /// string FrameworkName { get; } - bool CanGenerateForCompilation(CompilationInformation compilationInformation); + /// + /// Gets a value indicating whether the handler can generate code for a compilation. + /// + /// The compilation to check for compatibilty. + /// true if the handler can generate for the specified compilation; oterwise false. + bool CanGenerateForCompilation(CompilationInformation compilation); - TestFixtureMethod GenerateTestFixtureMethod(ScenarioInformation scenarioInformation, CancellationToken cancellationToken); + /// + /// Gets a value indicating whether the test framework associated with the handler has been referenced by a compilation. + /// + /// The compilation to examine. + /// true if the associated test framework has been referenced by the compilation; + /// otherwise false. + bool IsTestFrameworkReferenced(CompilationInformation compilation); - TestFixture GenerateTestFixture( - FeatureInformation featureInformation, - IEnumerable methods, - CancellationToken cancellationToken); - - bool IsTestFrameworkReferenced(CompilationInformation compilationInformation); + /// + /// Gets a test-fixture generator for a compilation. + /// + /// The compilation to get a generator for. + /// A test-fixture generator for the specified compilation. + ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation); } diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..36fb86d0a --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs @@ -0,0 +1,23 @@ + +namespace Reqnroll.FeatureSourceGenerator.MSTest; + +/// +/// Performs generation of MSTest test fixtures in the C# language. +/// +internal class MSTestCSharpTestFixtureGenerator : ITestFixtureGenerator +{ + public TestFixtureClass GenerateTestFixture( + FeatureInformation feature, + IEnumerable methods, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TestMethod GenerateTestMethod( + ScenarioInformation scenario, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs index b6525a9b8..88ed44c02 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs @@ -1,4 +1,6 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Reqnroll.FeatureSourceGenerator.CSharp; +using System.Threading; namespace Reqnroll.FeatureSourceGenerator.MSTest; @@ -9,21 +11,21 @@ public class MSTestHandler : ITestFrameworkHandler { public string FrameworkName => "MSTest"; - public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => - compilationInformation is CSharpCompilationInformation; + public bool CanGenerateForCompilation(CompilationInformation compilation) => + compilation is CSharpCompilationInformation; - public SourceText GenerateTestFixture(FeatureInformation feature) + public bool IsTestFrameworkReferenced(CompilationInformation compilation) { - return feature.CompilationInformation switch - { - CSharpCompilationInformation => new MSTestCSharpTestFixtureGeneration(feature).GetSourceText(), - _ => throw new NotSupportedException(), - }; + return compilation.ReferencedAssemblies + .Any(assembly => assembly.Name == "Microsoft.VisualStudio.TestPlatform.TestFramework"); } - public bool IsTestFrameworkReferenced(CompilationInformation compilationInformation) + public ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation) { - return compilationInformation.ReferencedAssemblies - .Any(assembly => assembly.Name == "Microsoft.VisualStudio.TestPlatform.TestFramework"); + return compilation switch + { + CSharpCompilationInformation => new MSTestCSharpTestFixtureGenerator(), + _ => throw new NotSupportedException(), + }; } } diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..c7730019a --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs @@ -0,0 +1,20 @@ + +namespace Reqnroll.FeatureSourceGenerator.NUnit; + +internal class NUnitCSharpTestFixtureGenerator : ITestFixtureGenerator +{ + public TestFixtureClass GenerateTestFixture( + FeatureInformation feature, + IEnumerable methods, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TestMethod GenerateTestMethod( + ScenarioInformation scenario, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs index c166d9632..4f12b0f3c 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs @@ -12,11 +12,11 @@ public class NUnitHandler : ITestFrameworkHandler public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => compilationInformation is CSharpCompilationInformation; - public SourceText GenerateTestFixture(FeatureInformation feature) + public ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation) { - return feature.CompilationInformation switch + return compilation switch { - CSharpCompilationInformation => new NUnitCSharpSyntaxGeneration(feature).GetSourceText(), + CSharpCompilationInformation => new NUnitCSharpTestFixtureGenerator(), _ => throw new NotSupportedException(), }; } diff --git a/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs b/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs index 989c5785c..bcd5d94f4 100644 --- a/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs @@ -1,41 +1,54 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Reqnroll.FeatureSourceGenerator; -public class ParameterDescriptor: IEquatable +namespace Reqnroll.FeatureSourceGenerator; +public class ParameterDescriptor: IEquatable { - public ParameterDescriptor(string name) + public ParameterDescriptor(IdentifierString name, TypeIdentifier type) { - if (string.IsNullOrEmpty(name)) + if (name.IsEmpty) { - throw new ArgumentException("Value cannot be null or an empty string.", nameof(name)); + throw new ArgumentException("Value cannot be an empty identifier.", nameof(name)); + } + + if (type.IsEmpty) + { + throw new ArgumentException("Value cannot be an empty identifier.", nameof(type)); } Name = name; + Type = type; } - public string Name { get; } + public IdentifierString Name { get; } + + public TypeIdentifier Type { get; } public bool Equals(ParameterDescriptor? other) { - if (ReferenceEquals(this, other)) + if (other is null) { - return true; + return false; } - if (other is null) + if (ReferenceEquals(this, other)) { - return false; + return true; } - return Name.Equals(other.Name); + return Name.Equals(other.Name) && + Type.Equals(other.Type); } public override bool Equals(object obj) => Equals(obj as ParameterDescriptor); public override int GetHashCode() { - return base.GetHashCode(); + unchecked + { + var hash = 65362369; + + hash *= 45172373 + Name.GetHashCode(); + hash *= 45172373 + Type.GetHashCode(); + + return hash; + } } } diff --git a/Reqnroll.FeatureSourceGenerator/TestFixture.cs b/Reqnroll.FeatureSourceGenerator/TestFixture.cs deleted file mode 100644 index d11afd8f5..000000000 --- a/Reqnroll.FeatureSourceGenerator/TestFixture.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Reqnroll.FeatureSourceGenerator; - -/// -/// Represents a Reqnroll text fixture. -/// -/// The feature the test fixture provides a representation of. -public abstract class TestFixture(FeatureInformation feature) -{ - public FeatureInformation Feature { get; } = feature; - - public string HintName => Feature.FeatureHintName; - - /// - /// Renders the configured test fixture to source text. - /// - /// A representing the rendered test fixture. - public SourceText Render() - { - throw new NotImplementedException(); - } -} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs new file mode 100644 index 000000000..922b865bb --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs @@ -0,0 +1,97 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Represents a Reqnroll text fixture class. +/// +public abstract class TestFixtureClass : IEquatable +{ + /// + /// Initializes a new instance of the test fixture class. + /// + /// The namespace and name of the class. + /// The hint name used to identify the test fixture within the compilation. This is usually + /// a virtual path and virtual filename that makes sense within the context of a project. The value must be unique + /// within the compilation. + /// The attributes which are applied to the feature. + public TestFixtureClass( + TypeIdentifier identifier, + string hintName, + ImmutableArray attributes = default) + { + if (identifier.IsEmpty) + { + throw new ArgumentException("Value cannot be an empty identifier.", nameof(identifier)); + } + + if (string.IsNullOrEmpty(hintName)) + { + throw new ArgumentException("Value cannot be null or an empty string.", nameof(hintName)); + } + + Identifier = identifier; + HintName = hintName; + + Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; + } + + /// + /// Gets the identifier of the class. + /// + public TypeIdentifier Identifier { get; } + + /// + /// Gets the attributes which are applied to the fixture. + /// + public ImmutableArray Attributes { get; } + + /// + /// Gets the hint name associated with the test fixture. + /// + public string HintName { get; } + + /// + /// Gets the test methods which make up the fixture. + /// + public abstract IEnumerable GetMethods(); + + public override bool Equals(object obj) => Equals(obj as TestFixtureClass); + + public bool Equals(TestFixtureClass? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Identifier.Equals(other.Identifier) && + (Attributes.Equals(other.Attributes) || Attributes.SetEquals(other.Attributes)) && + HintName.Equals(other.HintName, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 83155477; + + hash *= 87057149 + Identifier.GetHashCode(); + hash *= 87057149 + Attributes.GetSetHashCode(); + hash *= 87057149 + StringComparer.Ordinal.GetHashCode(HintName); + + return hash; + } + } + + /// + /// Renders the configured test fixture to source text. + /// + /// A representing the rendered test fixture. + public abstract SourceText Render(); +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs index b99d7cea5..895c1115c 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs @@ -2,7 +2,7 @@ namespace Reqnroll.FeatureSourceGenerator; -internal record TestFixtureComposition(FeatureInformation Feature, ImmutableArray Methods) +internal record TestFixtureComposition(FeatureInformation Feature, ImmutableArray Methods) { public override int GetHashCode() { @@ -11,7 +11,7 @@ public override int GetHashCode() var hash = 49151149; hash *= 983819 + Feature.GetHashCode(); - hash *= 983819 + Methods.GetSequenceHashCode(); + hash *= 983819 + Methods.GetSetHashCode(); return hash; } @@ -25,6 +25,6 @@ public virtual bool Equals(TestFixtureComposition? other) } return Feature.Equals(other.Feature) && - (Methods.Equals(other.Methods) || Methods.SequenceEqual(other.Methods)); + (Methods.Equals(other.Methods) || Methods.SetEqual(other.Methods)); } } diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs deleted file mode 100644 index b01c8c72c..000000000 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureMethod.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Reqnroll.FeatureSourceGenerator; - -public class TestFixtureMethod -{ -} \ No newline at end of file diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 6de65ccdb..a4821cc56 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -1,10 +1,8 @@ using Gherkin; using Gherkin.Ast; -using Microsoft.CodeAnalysis.VisualBasic; using Reqnroll.FeatureSourceGenerator.Gherkin; using System.Collections; using System.Collections.Immutable; -using System.Runtime.CompilerServices; namespace Reqnroll.FeatureSourceGenerator; @@ -182,7 +180,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) FeatureHintName: featureHintName, FeatureNamespace: featureNamespace, CompilationInformation: compilationInfo, - TestFrameworkHandler: testFramework) + TestFrameworkHandler: testFramework, + TestFixtureGenerator: testFramework.GetTestFixtureGenerator(compilationInfo)) ]; }); @@ -203,18 +202,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Generate scenario methods for each scenario. var methods = scenarios .Select(static (scenario, cancellationToken) => - (Method: scenario.Feature.TestFrameworkHandler.GenerateTestFixtureMethod(scenario, cancellationToken), + (Method: scenario.Feature.TestFixtureGenerator.GenerateTestMethod(scenario, cancellationToken), Scenario: scenario)); // Generate test fixtures for each feature. var fixtures = methods.Collect() - .WithComparer(ImmutableArrayEqualityComparer<(TestFixtureMethod Method, ScenarioInformation Scenario)>.Default) + .WithComparer(ImmutableArrayEqualityComparer<(TestMethod Method, ScenarioInformation Scenario)>.Default) .SelectMany(static (methods, cancellationToken) => methods .GroupBy(item => item.Scenario.Feature, item => item.Method) .Select(group => new TestFixtureComposition(group.Key, group.ToImmutableArray()))) .Select(static (composition, cancellationToken) => - composition.Feature.TestFrameworkHandler.GenerateTestFixture( + composition.Feature.TestFixtureGenerator.GenerateTestFixture( composition.Feature, composition.Methods, cancellationToken)); diff --git a/Reqnroll.FeatureSourceGenerator/TestMethod.cs b/Reqnroll.FeatureSourceGenerator/TestMethod.cs new file mode 100644 index 000000000..0fafc5dcb --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestMethod.cs @@ -0,0 +1,82 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Represents a test method that executes a scenario. +/// +public abstract class TestMethod : IEquatable +{ + /// + /// Initializes a new instance of the class. + /// + /// The identifier of the method. + /// The attributes applied to the method. + /// The parameters defined by the method. + /// + /// is an empty identifier string. + /// + public TestMethod( + IdentifierString identifier, + ImmutableArray attributes = default, + ImmutableArray parameters = default) + { + if (identifier.IsEmpty) + { + throw new ArgumentException("Value cannot be an empty identifier.", nameof(identifier)); + } + + Identifier = identifier; + + Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; + Parameters = parameters.IsDefault ? ImmutableArray.Empty : parameters; + } + + /// + /// Gets the identifier of the test method. + /// + public IdentifierString Identifier { get; } + + /// + /// Gets the attributes applied to the method. + /// + public ImmutableArray Attributes { get; } + + /// + /// Gets the parameters which are defined by this method. + /// + public ImmutableArray Parameters { get; } + + public override bool Equals(object obj) => Equals(obj as TestMethod); + + public override int GetHashCode() + { + unchecked + { + var hash = 86434151; + + hash *= 83155477 + Identifier.GetHashCode(); + hash *= 83155477 + Attributes.GetSetHashCode(); + hash *= 83155477 + Parameters.GetSequenceHashCode(); + + return hash; + } + } + + public bool Equals(TestMethod? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Identifier.Equals(other.Identifier) && + (Attributes.Equals(other.Attributes) || Attributes.SetEquals(other.Attributes)) && + (Parameters.Equals(other.Parameters) || Parameters.SequenceEqual(other.Parameters)); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..b99550d75 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -0,0 +1,20 @@ + +namespace Reqnroll.FeatureSourceGenerator.XUnit; + +internal class XUnitCSharpTestFixtureGenerator : ITestFixtureGenerator +{ + public TestFixtureClass GenerateTestFixture( + FeatureInformation feature, + IEnumerable methods, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TestMethod GenerateTestMethod( + ScenarioInformation scenario, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs index 13f0b1ca1..4cdf6bd29 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -12,11 +12,11 @@ public class XUnitHandler : ITestFrameworkHandler public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => compilationInformation is CSharpCompilationInformation; - public SourceText GenerateTestFixture(FeatureInformation feature) + public ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation) { - return feature.CompilationInformation switch + return compilation switch { - CSharpCompilationInformation => new XUnitCSharpSyntaxGeneration(feature).GetSourceText(), + CSharpCompilationInformation => new XUnitCSharpTestFixtureGenerator(), _ => throw new NotSupportedException(), }; } From a280483156567d1425e1610683b96253ae5c3120 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Fri, 21 Jun 2024 20:56:32 +0100 Subject: [PATCH 20/48] Add scenarios for MSTest generation --- .../AttributeDescriptor.cs | 4 +- .../CSharp/CSharpSourceTextBuilder.cs | 2 +- .../CSharp/CSharpSyntax.cs | 8 +- .../CSharp/CSharpTestFixtureClass.cs | 2 +- .../CSharp/CSharpTestFixtureGeneration.cs | 5 +- .../CSharp/CSharpTestFixtureGenerator.cs | 62 ++++ .../CSharp/CSharpTestMethod.cs | 2 +- .../CommonTypes.cs | 6 + .../IdentifierString.cs | 2 + .../MSTestCSharpTestFixtureGeneration.cs | 12 +- .../MSTestCSharpTestFixtureGenerator.cs | 47 ++- .../MSTest/MSTestHandler.cs | 4 +- .../MSTest/MSTestSyntax.cs | 23 ++ .../NamedTypeIdentifier.cs | 180 ++++++++++ .../ParameterDescriptor.cs | 7 +- .../ScenarioInformation.cs | 110 ++++++- .../TestFixtureClass.cs | 10 +- .../TestFixtureSourceGenerator.cs | 10 +- .../TypeIdentifier.cs | 102 ------ .../XUnit/XUnitCSharpSyntaxGeneration.cs | 2 +- .../AssertionExtensions.cs | 8 + .../AttributeDescriptorTests.cs | 36 +- .../CSharpMethodDeclarationAssertions.cs | 2 +- .../MSTestCSharpTestFixtureGeneratorTests.cs | 155 +++++++++ .../MSTestFeatureSourceGenerationTests.cs} | 78 +---- ...erTests.cs => NamedTypeIdentifierTests.cs} | 90 ++--- .../ParameterDescriptorTests.cs | 67 ++++ .../TestMethodAssertions.cs | 309 ++++++++++++++++++ 28 files changed, 1020 insertions(+), 325 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CommonTypes.cs create mode 100644 Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs create mode 100644 Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs rename Tests/Reqnroll.FeatureSourceGeneratorTests/{MSTestFeatureSourceGeneratorTests.cs => MSTest/MSTestFeatureSourceGenerationTests.cs} (66%) rename Tests/Reqnroll.FeatureSourceGeneratorTests/{TypeIdentifierTests.cs => NamedTypeIdentifierTests.cs} (56%) create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterDescriptorTests.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs diff --git a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs index 33265c2d1..22c0dcf5c 100644 --- a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs @@ -11,12 +11,12 @@ namespace Reqnroll.FeatureSourceGenerator; /// The positional arguments of the attribute. /// The named arguments of the attribute. public class AttributeDescriptor( - TypeIdentifier type, + NamedTypeIdentifier type, ImmutableArray? positionalArguments = null, ImmutableDictionary? namedArguments = null) : IEquatable { - public TypeIdentifier Type { get; } = type; + public NamedTypeIdentifier Type { get; } = type; public ImmutableArray PositionalArguments { get; } = ThrowIfArgumentTypesNotValid( diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs index 774e84d23..ec3b54d4c 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -267,7 +267,7 @@ public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribut return this; } - public CSharpSourceTextBuilder AppendTypeReference(TypeIdentifier identifier) + public CSharpSourceTextBuilder AppendTypeReference(TypeIdentifier type) { return this; } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs index 28d9f4742..8de743130 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs @@ -25,13 +25,13 @@ internal static class CSharpSyntax { typeof(void), "void" } }; - public static string CreateTypeIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: true); + public static IdentifierString GenerateTypeIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: true); public static string CreateMethodIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: true); - public static string CreateParameterIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: false); + public static IdentifierString GenerateParameterIdentifier(string s) => CreateIdentifier(s, capitalizeFirstWord: false); - private static string CreateIdentifier(string s, bool capitalizeFirstWord) + private static IdentifierString CreateIdentifier(string s, bool capitalizeFirstWord) { var sb = new StringBuilder(); var newWord = true; @@ -75,7 +75,7 @@ private static string CreateIdentifier(string s, bool capitalizeFirstWord) } } - return sb.ToString(); + return new IdentifierString(sb.ToString()); } private static bool IsValidAsFirstCharacterInIdentifier(char c) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index 6df3d764c..c4bcf3dbf 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -11,7 +11,7 @@ public class CSharpTestFixtureClass : TestFixtureClass private static readonly Encoding Encoding = new UTF8Encoding(false); public CSharpTestFixtureClass( - TypeIdentifier identifier, + NamedTypeIdentifier identifier, string hintName, ImmutableArray attributes = default, ImmutableArray methods = default) : diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs index b6e853241..8f0c3d403 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs @@ -1,5 +1,4 @@ using Gherkin.Ast; -using Microsoft.CodeAnalysis.CSharp; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp; @@ -45,7 +44,7 @@ internal SourceText GetSourceText() } protected virtual string GetClassName() => - CSharpSyntax.CreateTypeIdentifier(Document.Feature.Name + Document.Feature.Keyword); + CSharpSyntax.GenerateTypeIdentifier(Document.Feature.Name + Document.Feature.Keyword); protected virtual string? GetBaseType() => null; @@ -289,7 +288,7 @@ protected virtual ImmutableArray GetTestMethodParameters(Scenario scenar if (example != null) { parameters.AddRange( - example.TableHeader.Cells.Select(heading => $"string {CSharpSyntax.CreateParameterIdentifier(heading.Value)}")); + example.TableHeader.Cells.Select(heading => $"string {CSharpSyntax.GenerateParameterIdentifier(heading.Value)}")); parameters.Add("string[] exampleTags"); } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..03813a3e5 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs @@ -0,0 +1,62 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +/// +/// Provides a base for generating CSharp test fixtures. +/// +public abstract class CSharpTestFixtureGenerator : ITestFixtureGenerator +{ + public TestFixtureClass GenerateTestFixture( + FeatureInformation feature, + IEnumerable methods, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public TestMethod GenerateTestMethod( + ScenarioInformation scenario, + CancellationToken cancellationToken = default) => GenerateCSharpTestMethod(scenario, cancellationToken); + + protected virtual CSharpTestMethod GenerateCSharpTestMethod( + ScenarioInformation scenario, + CancellationToken cancellationToken) + { + var identifier = CSharpSyntax.GenerateTypeIdentifier(scenario.Name); + + var attributes = GenerateTestMethodAttributes(scenario, cancellationToken); + var parameters = GenerateTestMethodParameters(scenario, cancellationToken); + + return new CSharpTestMethod(identifier, attributes, parameters); + } + + protected virtual ImmutableArray GenerateTestMethodParameters( + ScenarioInformation scenario, + CancellationToken cancellationToken) + { + // In the case the scenario defines no examples, we don't pass any paramters. + if (scenario.Examples.IsEmpty) + { + return ImmutableArray.Empty; + } + + // In the case there are examples, we expect to pass arguments to the method, + // the last one being any tags which apply to the example. + // We're assuming that every set defines the same columns, which means we'll only look at the first set. + var headings = scenario.Examples.First().Headings; + var parameters = headings + .Select(heading => new ParameterDescriptor(CSharpSyntax.GenerateParameterIdentifier(heading), CommonTypes.String)) + .ToList(); + + // Append the tags parameter. + parameters.Add( + new ParameterDescriptor(new IdentifierString("_tags"), new ArrayTypeIdentifier(CommonTypes.String))); + + return parameters.ToImmutableArray(); + } + + protected abstract ImmutableArray GenerateTestMethodAttributes( + ScenarioInformation scenario, + CancellationToken cancellationToken); +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs index 6d87c4054..27bf2cadb 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs @@ -2,7 +2,7 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; -public abstract class CSharpTestMethod( +public class CSharpTestMethod( IdentifierString identifier, ImmutableArray attributes = default, ImmutableArray parameters = default) : diff --git a/Reqnroll.FeatureSourceGenerator/CommonTypes.cs b/Reqnroll.FeatureSourceGenerator/CommonTypes.cs new file mode 100644 index 000000000..fad2addc3 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CommonTypes.cs @@ -0,0 +1,6 @@ +namespace Reqnroll.FeatureSourceGenerator; +internal static class CommonTypes +{ + public static readonly NamedTypeIdentifier String = + new(new NamespaceString("System"), new IdentifierString("String")); +} diff --git a/Reqnroll.FeatureSourceGenerator/IdentifierString.cs b/Reqnroll.FeatureSourceGenerator/IdentifierString.cs index 5ab72ad6a..7dcd61037 100644 --- a/Reqnroll.FeatureSourceGenerator/IdentifierString.cs +++ b/Reqnroll.FeatureSourceGenerator/IdentifierString.cs @@ -6,6 +6,8 @@ namespace Reqnroll.FeatureSourceGenerator; { private readonly string? _value; + public static readonly IdentifierString Empty = default; + public IdentifierString(string? s) { if (string.IsNullOrEmpty(s)) diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs index 2bbe5dd5f..921c409ee 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs @@ -12,7 +12,7 @@ protected override IEnumerable GetTestFixtureAttributes() { return base.GetTestFixtureAttributes().Concat( [ - new AttributeDescriptor(new TypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))) + new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))) ]); } @@ -67,12 +67,12 @@ protected override IEnumerable GetTestMethodAttributes(Scen var attributes = new List { new( - new TypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), new( - new TypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), ImmutableArray.Create(scenario.Name)), new( - new TypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), ImmutableArray.Create("FeatureTitle", Document.Feature.Name)) }; @@ -80,7 +80,7 @@ protected override IEnumerable GetTestMethodAttributes(Scen { attributes.Add( new AttributeDescriptor( - new TypeIdentifier(MSTestNamespace, new IdentifierString("TestCategory")), + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestCategory")), ImmutableArray.Create(tag.Name.TrimStart('@')))); } @@ -113,7 +113,7 @@ protected override IEnumerable GetTestMethodAttributes(Scen attributes.Add( new AttributeDescriptor( - new TypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), positionalArguments)); } } diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs index 36fb86d0a..0223567cd 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs @@ -1,23 +1,46 @@  +using Reqnroll.FeatureSourceGenerator.CSharp; +using System.Collections.Immutable; + namespace Reqnroll.FeatureSourceGenerator.MSTest; /// /// Performs generation of MSTest test fixtures in the C# language. /// -internal class MSTestCSharpTestFixtureGenerator : ITestFixtureGenerator +internal class MSTestCSharpTestFixtureGenerator : CSharpTestFixtureGenerator { - public TestFixtureClass GenerateTestFixture( - FeatureInformation feature, - IEnumerable methods, - CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public TestMethod GenerateTestMethod( + protected override ImmutableArray GenerateTestMethodAttributes( ScenarioInformation scenario, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { - throw new NotImplementedException(); + var featureName = scenario.Feature.FeatureSyntax.GetRoot().Feature.Name; + + var attributes = new List + { + MSTestSyntax.TestMethodAttribute(), + MSTestSyntax.DescriptionAttribute(scenario.Name), + MSTestSyntax.TestPropertyAttribute("FeatureTitle", featureName) + }; + + foreach (var set in scenario.Examples) + { + foreach (var example in set) + { + // DataRow's constructor is DataRow(object? data, params object?[] moreData) + // Because we often pass an array of strings as a second argument, we always wrap moreData + // in an explicit array to avoid the compiler mistaking our string array as the moreData value. + var values = example.Select(item => item.Value).ToList(); + var data = values.First(); + var moreData = values.Skip(1).ToList(); + + moreData.Add(set.Tags); + + var arguments = moreData.Count > 0 ? ImmutableArray.Create(data, moreData.ToImmutableArray()) : ImmutableArray.Create(data); + + attributes.Add(MSTestSyntax.DataRowAttribute(arguments)); + } + } + + return attributes.ToImmutableArray(); } } diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs index 88ed44c02..7efbcc180 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs @@ -1,6 +1,4 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Reqnroll.FeatureSourceGenerator.CSharp; -using System.Threading; +using Reqnroll.FeatureSourceGenerator.CSharp; namespace Reqnroll.FeatureSourceGenerator.MSTest; diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs new file mode 100644 index 000000000..29b2bb2a4 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.MSTest; +internal static class MSTestSyntax +{ + public static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); + + public static AttributeDescriptor TestMethodAttribute() => + new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))); + + public static AttributeDescriptor DescriptionAttribute(string description) => + new( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + ImmutableArray.Create(description)); + + public static AttributeDescriptor TestPropertyAttribute(string propertyName, object? propertyValue) => + new( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + namedArguments: new Dictionary { { propertyName, propertyValue } }.ToImmutableDictionary()); + + public static AttributeDescriptor DataRowAttribute(ImmutableArray values) => + new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), values); +} diff --git a/Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs new file mode 100644 index 000000000..8f946efdf --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs @@ -0,0 +1,180 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public abstract class TypeIdentifier(bool isNullable) : IEquatable +{ + public bool IsNullable { get; } = isNullable; + + public virtual bool Equals(TypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return IsNullable.Equals(other.IsNullable); + } + + public override bool Equals(object obj) => Equals(obj as TypeIdentifier); + + public override int GetHashCode() => IsNullable.GetHashCode(); + + public static bool Equals(TypeIdentifier? first, TypeIdentifier? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public static bool operator ==(TypeIdentifier? first, TypeIdentifier? second) => Equals(first, second); + + public static bool operator !=(TypeIdentifier? first, TypeIdentifier? second) => !Equals(first, second); +} + +public class ArrayTypeIdentifier(TypeIdentifier itemType, bool isNullable = false) : + TypeIdentifier(isNullable), IEquatable +{ + public TypeIdentifier ItemType { get; } = itemType; + + public bool Equals(ArrayTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && + ItemType.Equals(other.ItemType); + } + + public override bool Equals(object obj) => Equals(obj as ArrayTypeIdentifier); + + public override bool Equals(TypeIdentifier? other) => Equals(other as ArrayTypeIdentifier); + + public override int GetHashCode() + { + unchecked + { + var hash = base.GetHashCode(); + + hash *= ItemType.GetHashCode(); + + return hash; + } + } + + public override string ToString() => $"{ItemType}[]"; + + public static bool Equals(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public static bool operator ==(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) => Equals(first, second); + + public static bool operator !=(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) => !Equals(first, second); +} + +public class NamedTypeIdentifier : TypeIdentifier, IEquatable +{ + public NamedTypeIdentifier(IdentifierString localName) : this(NamespaceString.Empty, localName) + { + } + + public NamedTypeIdentifier(NamespaceString ns, IdentifierString localName, bool isNullable = false) : base(isNullable) + { + if (localName.IsEmpty && !ns.IsEmpty) + { + throw new ArgumentException( + "An empty local name cannot be combined with a non-empty namespace.", + nameof(localName)); + } + + LocalName = localName; + Namespace = ns; + } + + public IdentifierString LocalName { get; } + + public NamespaceString Namespace { get; } + + public override string? ToString() => Namespace.IsEmpty ? LocalName.ToString() : $"{Namespace}.{LocalName}"; + + public override bool Equals(object obj) => Equals(obj as NamedTypeIdentifier); + + public override bool Equals(TypeIdentifier? other) => Equals(other as NamedTypeIdentifier); + + public bool Equals(NamedTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && + Namespace.Equals(other.Namespace) && + LocalName.Equals(other.LocalName); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode *= 73812971 + LocalName.GetHashCode(); + hashCode *= 73812971 + Namespace.GetHashCode(); + return hashCode; + } + } + + public static bool Equals(NamedTypeIdentifier? first, NamedTypeIdentifier? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public static bool operator ==(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => Equals(first, second); + + public static bool operator !=(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => !Equals(first, second); +} diff --git a/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs b/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs index bcd5d94f4..95fba3e5f 100644 --- a/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs @@ -8,11 +8,6 @@ public ParameterDescriptor(IdentifierString name, TypeIdentifier type) throw new ArgumentException("Value cannot be an empty identifier.", nameof(name)); } - if (type.IsEmpty) - { - throw new ArgumentException("Value cannot be an empty identifier.", nameof(type)); - } - Name = name; Type = type; } @@ -51,4 +46,6 @@ public override int GetHashCode() return hash; } } + + public override string ToString() => $"{Type} {Name}"; } diff --git a/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs b/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs index 48dd3881a..e115909e4 100644 --- a/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs +++ b/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs @@ -2,11 +2,105 @@ namespace Reqnroll.FeatureSourceGenerator; -public record ScenarioInformation( - FeatureInformation Feature, - string Name, - ImmutableArray ImmutableArray, - ImmutableArray ScenarioSteps, - ImmutableArray ScenarioExampleSets, - string? RuleName, - ImmutableArray RuleTags); +public class ScenarioInformation( + FeatureInformation feature, + string name, + ImmutableArray tags, + ImmutableArray steps, + ImmutableArray examples = default, + RuleInformation? rule = null) : IEquatable +{ + public FeatureInformation Feature { get; } = feature; + + public string Name { get; } = string.IsNullOrEmpty(name) ? + throw new ArgumentException("Value cannot be null or an empty string", nameof(name)) : + name; + + public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; + + public ImmutableArray Steps { get; } = steps.IsDefault ? ImmutableArray.Empty : steps; + + public ImmutableArray Examples { get; } = examples.IsDefault ? ImmutableArray.Empty : examples; + + public RuleInformation? Rule { get; } = rule; + + public override bool Equals(object obj) => Equals(obj as ScenarioInformation); + + public bool Equals(ScenarioInformation? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Feature.Equals(other.Feature) && + Name.Equals(other.Name) && + Tags.SetEqual(other.Tags) && + Steps.SequenceEqual(other.Steps) && + Examples.SequenceEqual(other.Examples) && + RuleInformation.Equals(Rule, other.Rule); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override string ToString() => $"Name={Name}"; +} + +public class RuleInformation(string name, ImmutableArray tags) : IEquatable +{ + public string Name { get; } = string.IsNullOrEmpty(name) ? + throw new ArgumentException("Value cannot be null or an empty string", nameof(name)) : + name; + + public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; + + public bool Equals(RuleInformation? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Name.Equals(other.Name) && Tags.SetEqual(other.Tags); + } + + public static bool Equals(RuleInformation? first, RuleInformation? second) + { + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public override bool Equals(object obj) => Equals(obj as RuleInformation); + + public override int GetHashCode() + { + unchecked + { + var hash = 14353993; + + hash *= 50733161 + Name.GetHashCode(); + hash *= 50733161 + Tags.GetSetHashCode(); + + return hash; + } + } + + public override string ToString() => $"Name={Name}"; +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs index 922b865bb..bbe5b8fca 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs @@ -16,21 +16,17 @@ public abstract class TestFixtureClass : IEquatable /// within the compilation. /// The attributes which are applied to the feature. public TestFixtureClass( - TypeIdentifier identifier, + NamedTypeIdentifier identifier, string hintName, ImmutableArray attributes = default) { - if (identifier.IsEmpty) - { - throw new ArgumentException("Value cannot be an empty identifier.", nameof(identifier)); - } + Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier)); if (string.IsNullOrEmpty(hintName)) { throw new ArgumentException("Value cannot be null or an empty string.", nameof(hintName)); } - Identifier = identifier; HintName = hintName; Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; @@ -39,7 +35,7 @@ public TestFixtureClass( /// /// Gets the identifier of the class. /// - public TypeIdentifier Identifier { get; } + public NamedTypeIdentifier Identifier { get; } /// /// Gets the attributes which are applied to the fixture. diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index a4821cc56..67e90ceae 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -253,12 +253,11 @@ private static ScenarioInformation GetScenarioInformation( Scenario scenario, FeatureInformation feature, CancellationToken cancellationToken) => - GetScenarioInformation(scenario, null, ImmutableArray.Empty, feature, cancellationToken); + GetScenarioInformation(scenario, null, feature, cancellationToken); private static ScenarioInformation GetScenarioInformation( Scenario scenario, - string? ruleName, - ImmutableArray ruleTags, + RuleInformation? rule, FeatureInformation feature, CancellationToken cancellationToken) { @@ -297,8 +296,7 @@ private static ScenarioInformation GetScenarioInformation( scenario.Tags.Select(tag => tag.Name).ToImmutableArray(), steps.ToImmutableArray(), exampleSets.ToImmutableArray(), - ruleName, - ruleTags); + rule); } private static IEnumerable GetScenarioInformation( @@ -315,7 +313,7 @@ private static IEnumerable GetScenarioInformation( switch (child) { case Scenario scenario: - yield return GetScenarioInformation(scenario, rule.Name, tags, feature, cancellationToken); + yield return GetScenarioInformation(scenario, new RuleInformation(rule.Name, tags), feature, cancellationToken); break; } } diff --git a/Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs deleted file mode 100644 index e519fe676..000000000 --- a/Reqnroll.FeatureSourceGenerator/TypeIdentifier.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Reqnroll.FeatureSourceGenerator; - -public readonly struct TypeIdentifier : IEquatable, IEquatable -{ - public TypeIdentifier(IdentifierString localName) : this(NamespaceString.Empty, localName) - { - } - - public TypeIdentifier(NamespaceString ns, IdentifierString localName) - { - if (localName.IsEmpty && !ns.IsEmpty) - { - throw new ArgumentException( - "An empty local name cannot be combined with a non-empty namespace.", - nameof(localName)); - } - - LocalName = localName; - Namespace = ns; - } - - public readonly bool IsEmpty => LocalName.IsEmpty; - - public IdentifierString LocalName { get; } - - public NamespaceString Namespace { get; } - - public override string? ToString() => Namespace.IsEmpty ? LocalName.ToString() : $"{Namespace}.{LocalName}"; - - public static bool Equals(TypeIdentifier identifierA, TypeIdentifier identifierB) - { - return identifierA.Equals(identifierB); - } - - public static bool Equals(TypeIdentifier identifier, string? s) => identifier.Equals(s); - - public override bool Equals(object obj) - { - return obj switch - { - null => IsEmpty, - TypeIdentifier id => Equals(id), - string s => Equals(s), - _ => false - }; - } - - public bool Equals(TypeIdentifier other) - { - if (IsEmpty) - { - return other.IsEmpty; - } - - return Namespace.Equals(other.Namespace) && LocalName.Equals(other.LocalName); - } - - public bool Equals(string? other) - { - if (string.IsNullOrEmpty(other)) - { - return IsEmpty; - } - - if (Namespace.IsEmpty) - { - return LocalName.Equals(other); - } - - var ns = Namespace.ToString(); - var ln = LocalName.ToString(); - - if (ln.Length + ns.Length + 1 != other!.Length) - { - return false; - } - - var otherSpan = other.AsSpan(); - return otherSpan.Slice(0, ns.Length).Equals(ns.AsSpan(), StringComparison.Ordinal) && - otherSpan[ns.Length].Equals('.') && - otherSpan.Slice(ns.Length + 1).Equals(ln.AsSpan(), StringComparison.Ordinal); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = 1922063139; - hashCode *= -1521134295 + LocalName.GetHashCode(); - hashCode *= -1521134295 + Namespace.GetHashCode(); - return hashCode; - } - } - - public static bool operator ==(TypeIdentifier identifierA, TypeIdentifier identifierB) => Equals(identifierA, identifierB); - - public static bool operator !=(TypeIdentifier identifierA, TypeIdentifier identifierB) => !Equals(identifierA, identifierB); - - public static bool operator ==(TypeIdentifier identifier, string s) => Equals(identifier, s); - - public static bool operator !=(TypeIdentifier identifier, string s) => !Equals(identifier, s); -} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs index e594a6554..aa419ebb8 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs @@ -10,7 +10,7 @@ protected override IEnumerable GetTestMethodAttributes(Scen { var attributes = new List { - new(new TypeIdentifier(XUnitNamespace, new IdentifierString("Fact"))) + new(new NamedTypeIdentifier(XUnitNamespace, new IdentifierString("Fact"))) }; return base.GetTestMethodAttributes(scenario).Concat(attributes); diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs index 099724a30..61b8b8afa 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs @@ -36,4 +36,12 @@ public static CSharpNamespaceDeclarationAssertions Should(this NamespaceDeclarat [Pure] public static CSharpMethodDeclarationAssertions Should(this MethodDeclarationSyntax? actualValue) => new(actualValue); + + /// + /// Returns an object that can be used to assert the + /// current . + /// + [Pure] + public static TestMethodAssertions Should(this TestMethod? actualValue) => + new(actualValue); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs index cd4e967fd..adeb1a377 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs @@ -8,36 +8,36 @@ public class AttributeDescriptorTests public static IEnumerable StringRepresentationExamples { get; } = [ [ - new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))), + new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))), "[Foo.Bar]" ], [ new AttributeDescriptor( - new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz" ]), "[Foo.Bar(\"Fizz\")]" ], [ new AttributeDescriptor( - new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz", "Buzz" ]), "[Foo.Bar(\"Fizz\", \"Buzz\")]" ], [ new AttributeDescriptor( - new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ 1, 2 ]), "[Foo.Bar(1, 2)]" ], [ new AttributeDescriptor( - new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create() ]), "[Foo.Bar(new string[] {})]" ], [ new AttributeDescriptor( - new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create("potato", "pancakes") ]), "[Foo.Bar(new string[] {\"potato\", \"pancakes\"})]" ] @@ -52,12 +52,12 @@ public void DescriptorsCanBeRepresntedAsStrings(AttributeDescriptor attribute, s public static IEnumerable DescriptorExamples { get; } = [ - [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) ], - [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz" ]) ], - [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz", "Buzz" ]) ], - [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ 1, 2 ]) ], - [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create() ]) ], - [ () => new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create("potato") ]) ] + [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) ], + [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz" ]) ], + [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz", "Buzz" ]) ], + [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ 1, 2 ]) ], + [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create() ]) ], + [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create("potato") ]) ] ]; [Theory] @@ -89,7 +89,7 @@ public void DescriptorsAreEqualWhenTypeNamespaceAndArgumentsAreEquivalent(Func(T va { var argument = ImmutableArray.Create(value, value); - var attribute = new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) + var attribute = new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) .WithPositionalArguments(argument) .WithNamedArguments(new { Property = argument }); @@ -164,7 +164,7 @@ public void DescriptorsCanBeCreatedWithImmutableArraysOfEnumsAsArguments(T va [InlineData(typeof(AttributeDescriptorTests))] public void DescriptorsCanBeCreatedWithTypesAsArguments(Type argument) { - var attribute = new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) + var attribute = new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) .WithPositionalArguments(argument) .WithNamedArguments(new { Property = argument }); @@ -179,7 +179,7 @@ public void DescriptorsCannotBeCreatedWithArraysAsArguments() { var argument = Array.Empty(); - var attribute = new AttributeDescriptor(new TypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))); + var attribute = new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))); attribute .Invoking(attribute => attribute.WithPositionalArguments([ argument ])) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs index 5dc5ac29a..b5b52f264 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs @@ -249,7 +249,7 @@ public static AttributeDescriptor GetAttributeDescriptor(AttributeSyntax attribu { var type = attribute.Name switch { - QualifiedNameSyntax qname => new TypeIdentifier( + QualifiedNameSyntax qname => new NamedTypeIdentifier( new NamespaceString( qname.Left.ToString().StartsWith("global::") ? qname.Left.ToString()[8..] : diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs new file mode 100644 index 000000000..669043168 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -0,0 +1,155 @@ +using Gherkin; +using Microsoft.CodeAnalysis; +using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.Gherkin; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.MSTest; +public class MSTestCSharpTestFixtureGeneratorTests +{ + public MSTestCSharpTestFixtureGeneratorTests() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(AssemblyIdentity.FromAssemblyDefinition); + + Compilation = new CSharpCompilationInformation( + "Test.dll", + references.ToImmutableArray(), + Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11, + true); + + TestHandler = new MSTestHandler(); + Generator = TestHandler.GetTestFixtureGenerator(Compilation); + } + + private static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); + + protected CSharpCompilationInformation Compilation { get; } + + protected MSTestHandler TestHandler { get; } + + protected ITestFixtureGenerator Generator { get; } + + [Fact] + public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamples() + { + const string featureText = + """ + #language: en + @featureTag1 + Feature: Sample + + Scenario: Sample Scenario + When foo happens + """; + + var document = new Parser().Parse(new StringReader(featureText)); + var featureSyntax = new GherkinSyntaxTree(document, [], "Sample.feature"); + + var featureInfo = new FeatureInformation( + featureSyntax, + "Sample.feature", + "Reqnroll.Tests", + Compilation, + TestHandler, + Generator); + + var scenarioInfo = new ScenarioInformation( + featureInfo, + "Sample Scenario", + [], + [new ScenarioStep(StepKeywordType.Action, "When", "foo happens", 6)]); + + var method = Generator.GenerateTestMethod(scenarioInfo); + + method.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), + new AttributeDescriptor( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + ["Sample Scenario"]), + new AttributeDescriptor( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + namedArguments: new Dictionary{ { "FeatureTitle", "Sample" } }.ToImmutableDictionary()) + ]); + + method.Should().HaveNoParameters(); + } + + [Fact] + public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScenarioHasExamples() + { + const string featureText = + """ + #language: en + @featureTag1 + Feature: Sample + + Scenario Outline: Sample Scenario Outline + When happens + @example_tag + Examples: + | what | + | foo | + | bar | + Examples: Second example without tags - in this case the tag list is null. + | what | + | baz | + """; + + var document = new Parser().Parse(new StringReader(featureText)); + var featureSyntax = new GherkinSyntaxTree(document, [], "Sample.feature"); + + var featureInfo = new FeatureInformation( + featureSyntax, + "Sample.feature", + "Reqnroll.Tests", + Compilation, + TestHandler, + Generator); + + var exampleSet1 = new ScenarioExampleSet(["what"], [["foo"], ["bar"]], ["example_tag"]); + var exampleSet2 = new ScenarioExampleSet(["what"], [["baz"]], []); + + var scenarioInfo = new ScenarioInformation( + featureInfo, + "Sample Scenario Outline", + [], + [ new ScenarioStep(StepKeywordType.Action, "When", " happens", 6) ], + [ exampleSet1, exampleSet2 ]); + + var method = Generator.GenerateTestMethod(scenarioInfo); + + method.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), + new AttributeDescriptor( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + ["Sample Scenario Outline"]), + new AttributeDescriptor( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + namedArguments: new Dictionary{ { "FeatureTitle", "Sample" } }.ToImmutableDictionary()), + new AttributeDescriptor( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + ["foo", ImmutableArray.Create(ImmutableArray.Create("example_tag"))]), + new AttributeDescriptor( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + ["bar", ImmutableArray.Create(ImmutableArray.Create("example_tag"))]), + new AttributeDescriptor( + new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + ["baz", ImmutableArray.Create(ImmutableArray.Empty)]) + ]); + + method.Should().HaveParametersEquivalentTo( + [ + new ParameterDescriptor( + new IdentifierString("what"), + new NamedTypeIdentifier(new NamespaceString("System"), new IdentifierString("String"))), + + new ParameterDescriptor( + new IdentifierString("_tags"), + new ArrayTypeIdentifier(new NamedTypeIdentifier(new NamespaceString("System"), new IdentifierString("String")))) + ]); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs similarity index 66% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs index 1af213d05..668e58b29 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTestFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs @@ -4,15 +4,11 @@ using Reqnroll.FeatureSourceGenerator; using Reqnroll.FeatureSourceGenerator.CSharp; using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; -using System.Data.Common; using Xunit.Abstractions; namespace Reqnroll.FeatureSourceGenerator; -using static SyntaxFactory; - -public class MSTestFeatureSourceGeneratorTests(ITestOutputHelper output) +public class MSTestFeatureSourceGenerationTests(ITestOutputHelper output) { [Fact] public void GeneratorProducesMSTestOutputWhenWhenBuildPropertyConfiguredForMSTest() @@ -113,76 +109,6 @@ Then the result should be 120 .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().HaveSingleAttribute("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass"); } - - [Fact] - public void GeneratorProducesMSTestDataRowsForScenarioExamples() - { - var references = AppDomain.CurrentDomain.GetAssemblies() - .Where(asm => !asm.IsDynamic) - .Select(asm => MetadataReference.CreateFromFile(asm.Location)); - - var compilation = CSharpCompilation.Create("test", references: references); - - var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.MSTest]); - - const string featureText = - """ - #language: en - @featureTag1 - Feature: Sample - - Scenario Outline: Sample Scenario Outline - When happens - @example_tag - Examples: - | what | - | foo | - | bar | - Examples: Second example without tags - in this case the tag list is null. - | what | - | baz | - """; - - var optionsProvider = new FakeAnalyzerConfigOptionsProvider( - new InMemoryAnalyzerConfigOptions(new Dictionary - { - { "build_property.ReqnrollTargetTestFramework", "MSTest" } - })); - - var driver = CSharpGeneratorDriver - .Create(generator) - .AddAdditionalTexts([new FeatureFile("Sample.feature", featureText)]) - .WithUpdatedAnalyzerConfigOptions(optionsProvider) - .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); - - diagnostics.Should().BeEmpty(); - - var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() - .Which.Should().BeAssignableTo().Subject!; - - output.WriteLine($"Generated source:\n{generatedSyntaxTree}"); - - generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); - - generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") - .Which.Should().ContainSingleClassDeclaration("SampleFeature") - .Which.Should().ContainMethod("SampleScenarioOutline") - .Which.Should().HaveAttribuesEquivalentTo( - [ - MSTestSyntax.Attribute("TestMethod"), - MSTestSyntax.Attribute("Description", "Sample Scenario Outline"), - MSTestSyntax.Attribute("TestProperty", "FeatureTitle", "Sample"), - MSTestSyntax.Attribute("TestCategory", "featureTag1"), - MSTestSyntax.Attribute("DataRow", "foo", ImmutableArray.Create( ImmutableArray.Create("example_tag") )), - MSTestSyntax.Attribute("DataRow", "bar", ImmutableArray.Create( ImmutableArray.Create("example_tag") )), - MSTestSyntax.Attribute("DataRow", "baz", ImmutableArray.Create( ImmutableArray.Empty )) - ]); - //.And.HaveParametersEquivalentTo( - //[ - // Parameter(Identifier("what")).WithType(PredefinedType(Token(SyntaxKind.StringKeyword))), - // Parameter(Identifier("exampleTags")).WithType(ArrayType(PredefinedType(Token(SyntaxKind.StringKeyword)))) - //]); - } } internal static class MSTestSyntax @@ -192,7 +118,7 @@ internal static class MSTestSyntax public static AttributeDescriptor Attribute(string type, params object?[] args) { return new AttributeDescriptor( - new TypeIdentifier(Namespace, new IdentifierString(type)), + new NamedTypeIdentifier(Namespace, new IdentifierString(type)), [.. args]); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/TypeIdentifierTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/NamedTypeIdentifierTests.cs similarity index 56% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/TypeIdentifierTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/NamedTypeIdentifierTests.cs index 7a6bcaa6f..d31755f21 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/TypeIdentifierTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/NamedTypeIdentifierTests.cs @@ -1,27 +1,15 @@ namespace Reqnroll.FeatureSourceGenerator; -public class TypeIdentifierTests +public class NamedTypeIdentifierTests { - [Fact] - public void DefaultValue_IsEmpty() - { - var identifier = default(TypeIdentifier); - - identifier.IsEmpty.Should().BeTrue(); - identifier.LocalName.IsEmpty.Should().BeTrue(); - identifier.Namespace.IsEmpty.Should().BeTrue(); - identifier.ToString().Should().Be(""); - } - [Theory] [InlineData("Parser")] [InlineData("__Parser")] [InlineData("X509")] - public void Constructor_CreatesTypeIdentifierFromValidName(string name) + public void Constructor_CreatesNamedTypeIdentifierFromValidName(string name) { - var identifier = new TypeIdentifier(new IdentifierString(name)); + var identifier = new NamedTypeIdentifier(new IdentifierString(name)); - identifier.IsEmpty.Should().BeFalse(); identifier.LocalName.Should().Be(name); identifier.Namespace.IsEmpty.Should().BeTrue(); identifier.ToString().Should().Be(name); @@ -31,13 +19,12 @@ public void Constructor_CreatesTypeIdentifierFromValidName(string name) [InlineData("Reqnroll", "Parser")] [InlineData("Reqnroll", "__Parser")] [InlineData("Reqnroll", "X509")] - public void Constructor_CreatesTypeIdentifierFromValidNameAndNamespace(string ns, string name) + public void Constructor_CreatesNamedTypeIdentifierFromValidNameAndNamespace(string ns, string name) { var nsx = new NamespaceString(ns); - var identifier = new TypeIdentifier(nsx, new IdentifierString(name)); + var identifier = new NamedTypeIdentifier(nsx, new IdentifierString(name)); - identifier.IsEmpty.Should().BeFalse(); identifier.LocalName.Should().Be(name); identifier.Namespace.Should().Be(nsx); identifier.ToString().Should().Be($"{ns}.{name}"); @@ -48,9 +35,8 @@ public void Constructor_CreatesTypeIdentifierFromValidNameAndNamespace(string ns [InlineData(null)] public void Constructor_CreatesEmptyTypeIdentifierFromEmptyNameValue(string name) { - var identifier = new TypeIdentifier(new IdentifierString(name)); + var identifier = new NamedTypeIdentifier(new IdentifierString(name)); - identifier.IsEmpty.Should().BeTrue(); identifier.LocalName.IsEmpty.Should().BeTrue(); identifier.Namespace.IsEmpty.Should().BeTrue(); identifier.ToString().Should().Be(""); @@ -59,9 +45,9 @@ public void Constructor_CreatesEmptyTypeIdentifierFromEmptyNameValue(string name [Theory] [InlineData("")] [InlineData(null)] - public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyNameWithANonEmptyNamespace(string name) + public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyName(string name) { - Func ctr = () => new TypeIdentifier(new NamespaceString("Reqnroll"), new IdentifierString(name)); + Func ctr = () => new NamedTypeIdentifier(new NamespaceString("Reqnroll"), new IdentifierString(name)); ctr.Should().Throw(); } @@ -73,7 +59,7 @@ public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyNameWithANonEmpty [InlineData("1FeatureSourceGenerator")] public void Constructor_ThrowsArgumentExceptionWhenUsingAnInvalidLocalNameValue(string name) { - Func ctr = () => new TypeIdentifier(new IdentifierString(name)); + Func ctr = () => new NamedTypeIdentifier(new IdentifierString(name)); ctr.Should().Throw(); } @@ -87,8 +73,8 @@ public void EqualsTypeIdentifier_ReturnsTrueWhenNamespacesAndLocalNameMatches( string ns2, string name2) { - var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); typeId1.Equals(typeId2).Should().BeTrue(); } @@ -102,8 +88,8 @@ public void EqualsTypeIdentifier_ReturnsFalseWhenNamespaceDoesNotMatch( string ns2, string name2) { - var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); typeId1.Equals(typeId2).Should().BeFalse(); } @@ -117,8 +103,8 @@ public void EqualsTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( string ns2, string name2) { - var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); typeId1.Equals(typeId2).Should().BeFalse(); } @@ -132,7 +118,7 @@ public void EqualsTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( [InlineData("", "", null, true)] public void EqualsString_ReturnsCaseSensitiveEquivalence(string ns, string name, string identifier, bool expected) { - new TypeIdentifier(new NamespaceString(ns), new IdentifierString(name)).Equals(identifier).Should().Be(expected); + new NamedTypeIdentifier(new NamespaceString(ns), new IdentifierString(name)).Equals(identifier).Should().Be(expected); } [Theory] @@ -148,26 +134,12 @@ public void GetHashCode_ReturnsSameValueForEquivalentValues( string ns2, string name2) { - var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); typeId1.GetHashCode().Should().Be(typeId2.GetHashCode()); } - [Theory] - [InlineData("Reqnroll", "Parser", "Reqnroll.Parser", true)] - [InlineData("Reqnroll", "Parser", "Reqnroll.parser", false)] - [InlineData("Reqnroll", "_Parser", "Reqnroll._Parser", true)] - [InlineData(null, "_Parser", "_Parser", true)] - [InlineData(null, "_Parser", "_parser", false)] - [InlineData(null, null, "", true)] - [InlineData(null, null, null, true)] - [InlineData("", "", null, true)] - public void EqualityOperatorWithString_ReturnsEquivalenceWithCaseSensitivity(string ns, string name, string id, bool expected) - { - (new TypeIdentifier(new NamespaceString(ns), new IdentifierString(name)) == id).Should().Be(expected); - } - [Theory] [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser", true)] [InlineData("Reqnroll", "_Parser", "Reqnroll", "_Parser", true)] @@ -182,30 +154,12 @@ public void EqualityOperatorWithTypeIdentifier_ReturnsEquivalenceWithCaseSensiti string name2, bool expected) { - var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); (typeId1 == typeId2).Should().Be(expected); } - [Theory] - [InlineData("Reqnroll", "Parser", "Reqnroll.Parser", false)] - [InlineData("Reqnroll", "Parser", "Reqnroll.parser", true)] - [InlineData("Reqnroll", "_Parser", "Reqnroll._Parser", false)] - [InlineData(null, "_Parser", "_Parser", false)] - [InlineData(null, "_Parser", "_parser", true)] - [InlineData(null, null, "", false)] - [InlineData(null, null, null, false)] - [InlineData("", "", null, false)] - public void InequalityOperatorWithString_ReturnsNonEquivalenceWithCaseSensitivity( - string ns, - string name, - string id, - bool expected) - { - (new TypeIdentifier(new NamespaceString(ns), new IdentifierString(name)) != id).Should().Be(expected); - } - [Theory] [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser", false)] [InlineData("Reqnroll", "_Parser", "Reqnroll", "_Parser", false)] @@ -220,8 +174,8 @@ public void InequalityOperatorWithTypeIdentifier_ReturnsNonEquivalenceWithCaseSe string name2, bool expected) { - var typeId1 = new TypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new TypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); + var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); (typeId1 != typeId2).Should().Be(expected); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterDescriptorTests.cs new file mode 100644 index 000000000..669a0454b --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterDescriptorTests.cs @@ -0,0 +1,67 @@ +namespace Reqnroll.FeatureSourceGenerator; +public class ParameterDescriptorTests +{ + [Fact] + public void Constructor_ThrowsArgumentExceptionWhenNameIsEmpty() + { + Func ctr = () => + new ParameterDescriptor(IdentifierString.Empty, new NamedTypeIdentifier(new IdentifierString("string"))); + + ctr.Should().Throw(); + } + + [Fact] + public void Constructor_InitializesProperties() + { + var name = new IdentifierString("potato"); + var type = new NamedTypeIdentifier(new IdentifierString("string")); + + var descriptor = new ParameterDescriptor(name, type); + + descriptor.Name.Should().Be(name); + descriptor.Type.Should().Be(type); + } + + [Theory] + [InlineData("potato", "System", "String")] + [InlineData("foo", "System", "Int32")] + [InlineData("bar", "System", "Int64")] + [InlineData("fiz", "", "Buzz")] + public void GetHashCode_ReturnsSameValueForEquivalentValues(string name, string typeNamespace, string typeName) + { + var descriptorA = new ParameterDescriptor( + new IdentifierString(name), + new NamedTypeIdentifier(new NamespaceString(typeNamespace), new IdentifierString(typeName))); + + var descriptorB = new ParameterDescriptor( + new IdentifierString(name), + new NamedTypeIdentifier(new NamespaceString(typeNamespace), new IdentifierString(typeName))); + + descriptorA.GetHashCode().Should().Be(descriptorB.GetHashCode()); + } + + [Theory] + [InlineData("potato", "System", "String", "potato", "System", "String", true)] + [InlineData("potato", "System", "String", "potato", "", "String", false)] + [InlineData("potato", "System", "String", "foo", "System", "String", false)] + [InlineData("potato", "System", "String", "foo", "", "String", false)] + public void Equals_ReturnsEqualityBasedOnMatchingPropertyValues( + string nameA, + string typeNamespaceA, + string typeNameA, + string nameB, + string typeNamespaceB, + string typeNameB, + bool expected) + { + var descriptorA = new ParameterDescriptor( + new IdentifierString(nameA), + new NamedTypeIdentifier(new NamespaceString(typeNamespaceA), new IdentifierString(typeNameA))); + + var descriptorB = new ParameterDescriptor( + new IdentifierString(nameB), + new NamedTypeIdentifier(new NamespaceString(typeNamespaceB), new IdentifierString(typeNameB))); + + descriptorA.Equals(descriptorB).Should().Be(expected); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs new file mode 100644 index 000000000..86beeca7e --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs @@ -0,0 +1,309 @@ +using FluentAssertions.Execution; +using FluentAssertions.Primitives; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace Reqnroll.FeatureSourceGenerator; +public class TestMethodAssertions(TestMethod? subject) : + TestMethodAssertions(subject) +{ +} + +public class TestMethodAssertions(TestMethod? subject) : + ReferenceTypeAssertions(subject!) + where TAssertions : TestMethodAssertions +{ + protected override string Identifier => "method"; + + ///// + ///// Expects the class declaration have only a single attribute with a specific identifier. + ///// + ///// + ///// The declared type of the attribute. + ///// + ///// + ///// A formatted phrase as is supported by explaining why the assertion + ///// is needed. If the phrase does not start with the word because, it is prepended automatically. + ///// + ///// + ///// Zero or more objects to format using the placeholders in . + ///// + //public AndWhichConstraint HaveSingleAttribute( + // string type, + // string because = "", + // params object[] becauseArgs) + //{ + // var expectation = "Expected {context:method} to have a single attribute " + + // $"which is of type \"{type}\"{{reason}}"; + + // bool notNull = Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .ForCondition(Subject is not null) + // .FailWith(expectation + ", but found ."); + + // AttributeSyntax? match = default; + + // if (notNull) + // { + // var attributes = Subject!.Attributes; + + // switch (attributes.Length) + // { + // case 0: // Fail, Collection is empty + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but the class has no attributes."); + + // break; + // case 1: // Success Condition + // var single = attributes.Single(); + + // if (single.Name.ToString() != type) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but found the attribute \"{0}\".", single.Name); + // } + // else + // { + // match = single; + // } + + // break; + // default: // Fail, Collection contains more than a single item + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but found {0}.", attributes); + + // break; + // } + // } + + // return new AndWhichConstraint((TAssertions)this, match!); + //} + + //public AndWhichConstraint HaveAttribute( + // string type, + // string because = "", + // params object[] becauseArgs) + //{ + // var expectation = "Expected {context:method} to have an attribute " + + // $"of type \"{type}\"{{reason}}"; + + // bool notNull = Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .ForCondition(Subject is not null) + // .FailWith(expectation + ", but found ."); + + // AttributeSyntax? match = default; + + // if (notNull) + // { + // var attributes = Subject!.AttributeLists.SelectMany(list => list.Attributes).ToList(); + + // if (attributes.Count == 0) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but the method has no attributes."); + // } + // else + // { + // match = attributes.FirstOrDefault(attribute => attribute.Name.ToString() == type); + + // if (match == null) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but found {0}.", attributes); + // } + // } + // } + + // return new AndWhichConstraint((TAssertions)this, match!); + //} + + public AndConstraint HaveAttribuesEquivalentTo( + AttributeDescriptor[] expectation, + string because = "", + params object[] becauseArgs) => + HaveAttribuesEquivalentTo((IEnumerable)expectation, because, becauseArgs); + + public AndConstraint HaveAttribuesEquivalentTo( + IEnumerable expectation, + string because = "", + params object[] becauseArgs) + { + using var scope = new AssertionScope(); + + scope.FormattingOptions.UseLineBreaks = true; + + var assertion = Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:the method} to have attributes equivalent to {0}", expectation) + .ForCondition(Subject is not null) + .FailWith("but {context:the method} is ."); + + var expected = expectation.ToList(); + var actual = Subject!.Attributes; + + + var missing = expected.ToList(); + var extra = actual.ToList(); + + foreach (var item in expected) + { + if (extra.Remove(item)) + { + missing.Remove(item); + } + } + + if (missing.Count > 0) + { + if (extra.Count > 0) + { + assertion + .Then + .FailWith("but {context:the method} is missing attributes {0} and has extra attributes {1}", missing, extra); + } + else + { + assertion + .Then + .FailWith("but {context:the method} is missing attributes {0}", missing); + } + } + else if (extra.Count > 0) + { + assertion + .Then + .FailWith("but {context:the method} has extra attributes {0}", extra); + } + + return new AndConstraint((TAssertions)this); + } + + public AndConstraint HaveNoParameters( + string because = "", + params object[] becauseArgs) => + HaveParametersEquivalentTo([], because, becauseArgs); + + public AndConstraint HaveParametersEquivalentTo( + ParameterDescriptor[] expectation, + string because = "", + params object[] becauseArgs) => + HaveParametersEquivalentTo((IEnumerable)expectation, because, becauseArgs); + + public AndConstraint HaveParametersEquivalentTo( + IEnumerable expectation, + string because = "", + params object[] becauseArgs) + { + using var scope = new AssertionScope(); + + scope.FormattingOptions.UseLineBreaks = true; + + var setup = Execute.Assertion + .BecauseOf(because, becauseArgs); + + var expected = expectation.ToList(); + + var assertion = (expected.Count == 0 ? + setup.WithExpectation("Expected {context:the method} to have no parameters") : + setup.WithExpectation("Expected {context:the method} to have parameters equivalent to {0}", expected)) + .ForCondition(Subject is not null) + .FailWith("but {context:the method} is ."); + + var actual = Subject!.Parameters; + + if (expected.Count == 0 && actual.Length != 0) + { + assertion + .Then + .FailWith("but {context:the method} has parameters defined"); + } + else + { + for (var i = 0; i < expected.Count; i++) + { + var expectedItem = expected[i]; + var itemAssertion = Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected parameter {0} to be \"{1}\" ", i, expectedItem) + .ForCondition(actual.Length > i) + .FailWith(" but {context:the method} does not define a parameter {0}.", i); + + if (actual.Length >= i) + { + var actualItem = actual[i]; + + itemAssertion + .Then + .ForCondition(actualItem.Equals(expectedItem)) + .FailWith("but found \"{0}\".", actual[i]); + } + } + + if (actual.Length > expected.Count) + { + assertion + .Then + .FailWith("but {context:the method} has additional parameters {0}.", actual.Skip(expected.Count)); + } + } + + return new AndConstraint((TAssertions)this); + } + + //public AndConstraint HaveParametersEquivalentTo( + // IEnumerable expectation, + // string because = "", + // params object[] becauseArgs) + //{ + // using var scope = new AssertionScope(); + + // scope.FormattingOptions.UseLineBreaks = true; + + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .WithExpectation("Expected {context:the method} to have parameters equivalent to {0} {reason}", expectation) + // .ForCondition(Subject is not null) + // .FailWith("but {context:the method} is ."); + + // var expected = expectation.ToList(); + // var actual = Subject!.ParameterList.Parameters; + + // for (var i = 0; i < expected.Count; i++) + // { + // var expectedItem = expected[i]; + + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .WithExpectation("Expected {context:the method} to have parameter {0}: \"{1}\" {reason}", i+1, expectedItem) + // .ForCondition(Subject.ParameterList.Parameters.Count != 0) + // .FailWith("but {context:the method} has no parameters defined.", Subject.ParameterList.Parameters.Count) + // .Then + // .ForCondition(Subject.ParameterList.Parameters.Count > i) + // .FailWith("but {context:the method} only has {0} parameters defined.", Subject.ParameterList.Parameters.Count) + // .Then + // .Given(() => Subject.ParameterList.Parameters[i]) + // .ForCondition(actual => false)// actual.IsEquivalentTo(expectedItem, true)) + // .FailWith("but found \"{0}\".", expectedItem); + // } + + // if (expected.Count < actual.Count) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .WithExpectation("Expected {context:the method} to have parameters equivalent to {0}{reason}", expectation) + // .FailWith("but {context:the method} has extra parameters {0}.", actual.Skip(expected.Count)); + // } + + // return new AndConstraint((TAssertions)this); + //} +} From bb8face6e470a0d0613af0f30b70076c2db7c0dc Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 2 Jul 2024 01:28:54 +0100 Subject: [PATCH 21/48] Add MSTest support up-to 6/7 test cases --- .../CSharp/CSharpRenderingOptions.cs | 8 + .../CSharp/CSharpSourceTextBuilder.cs | 65 ++- .../CSharp/CSharpSyntax.cs | 11 +- .../CSharp/CSharpTestFixtureClass.cs | 154 +++++-- .../CSharp/CSharpTestFixtureGeneration.cs | 432 ------------------ .../CSharp/CSharpTestFixtureGenerator.cs | 135 ++++-- .../CSharpTestFixtureSourceGenerator.cs | 4 +- .../CSharp/CSharpTestMethod.cs | 176 ++++++- .../MSTest/MSTestCSharpTestFixtureClass.cs | 92 ++++ .../MSTestCSharpTestFixtureGenerator.cs | 77 ++++ .../CSharp/MSTest/MSTestCSharpTestMethod.cs | 27 ++ .../FeatureInformation.cs | 11 - .../GeneratorInformation.cs | 24 + .../Gherkin/GherkinDocumentComparer.cs | 6 +- .../Gherkin/GherkinSyntaxParser.cs | 54 +-- .../Gherkin/GherkinSyntaxTree.cs | 6 +- .../ITestFixtureGenerator.cs | 25 +- .../ITestFrameworkHandler.cs | 26 +- .../MSTestCSharpTestFixtureGeneration.cs | 132 ------ .../MSTestCSharpTestFixtureGenerator.cs | 46 -- .../MSTest/MSTestHandler.cs | 23 +- .../MSTest/MSTestSyntax.cs | 5 +- .../NUnit/NUnitCSharpSyntaxGeneration.cs | 7 - .../NUnit/NUnitCSharpTestFixtureGenerator.cs | 39 +- .../NUnit/NUnitHandler.cs | 17 +- .../NamedTypeIdentifier.cs | 180 -------- .../Reqnroll.FeatureSourceGenerator.csproj | 4 + .../SourceModel/ArrayTypeIdentifier.cs | 60 +++ .../{ => SourceModel}/AttributeDescriptor.cs | 10 +- .../{ => SourceModel}/CommonTypes.cs | 4 +- .../SourceModel/FeatureInformation.cs | 62 +++ .../SourceModel/IHasAttributes.cs | 14 + .../{ => SourceModel}/IdentifierString.cs | 6 +- .../SourceModel/NamedTypeIdentifier.cs | 80 ++++ .../{ => SourceModel}/NamespaceString.cs | 4 +- .../{ => SourceModel}/ParameterDescriptor.cs | 4 +- .../SourceModel/RuleInformation.cs | 54 +++ .../SourceModel/ScenarioExampleSet.cs | 84 ++++ .../{ => SourceModel}/ScenarioInformation.cs | 68 +-- .../SourceModel/ScenarioStep.cs | 5 + .../{ => SourceModel}/TestFixtureClass.cs | 19 +- .../{ => SourceModel}/TestMethod.cs | 27 +- .../SourceModel/TypeIdentifier.cs | 44 ++ .../TestFixtureComposition.cs | 12 +- .../TestFixtureGenerationContext.cs | 71 +++ .../TestFixtureSourceGenerator.cs | 298 +++++------- .../TestFrameworkInformation.cs | 23 - .../TestMethodGenerationContext.cs | 49 ++ .../XUnit/XUnitCSharpSyntaxGeneration.cs | 119 ++--- .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 42 +- .../XUnit/XUnitHandler.cs | 17 +- Reqnroll.sln | 13 + .../AssertionExtensions.cs | 13 +- .../AttributeAssertions.cs | 233 ++++++++++ .../AttributeDescriptorFormatter.cs | 1 + .../AttributeDescriptorTests.cs | 1 + .../CSharpMethodDeclarationAssertions.cs | 1 + .../MSTestCSharpTestFixtureGeneratorTests.cs | 128 +++--- .../MSTestFeatureSourceGenerationTests.cs | 4 +- .../IdentifierStringTests.cs | 14 +- .../NamedTypeIdentifierTests.cs | 16 +- .../{ => SourceModel}/NamespaceStringTests.cs | 4 +- .../ParameterDescriptorTests.cs | 6 +- .../TestMethodAssertions.cs | 248 +--------- 64 files changed, 2007 insertions(+), 1637 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingOptions.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/FeatureInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/GeneratorInformation.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/AttributeDescriptor.cs (98%) rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/CommonTypes.cs (50%) create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/FeatureInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/IHasAttributes.cs rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/IdentifierString.cs (96%) create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/NamespaceString.cs (96%) rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/ParameterDescriptor.cs (89%) create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/RuleInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioExampleSet.cs rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/ScenarioInformation.cs (51%) create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/TestFixtureClass.cs (78%) rename Reqnroll.FeatureSourceGenerator/{ => SourceModel}/TestMethod.cs (68%) create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureGenerationContext.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/TestMethodGenerationContext.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeAssertions.cs rename Tests/Reqnroll.FeatureSourceGeneratorTests/{ => SourceModel}/IdentifierStringTests.cs (93%) rename Tests/Reqnroll.FeatureSourceGeneratorTests/{ => SourceModel}/NamedTypeIdentifierTests.cs (91%) rename Tests/Reqnroll.FeatureSourceGeneratorTests/{ => SourceModel}/NamespaceStringTests.cs (98%) rename Tests/Reqnroll.FeatureSourceGeneratorTests/{ => SourceModel}/ParameterDescriptorTests.cs (94%) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingOptions.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingOptions.cs new file mode 100644 index 000000000..39bfcebd2 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingOptions.cs @@ -0,0 +1,8 @@ +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +/// +/// Defines the options which control the rendering of C# code. +/// +/// Whether to include #line directives to map generated tests to lines in source feature files. +/// Whether to use nullable reference types in generated code. +public record CSharpRenderingOptions(bool EnableLineMapping = true, bool UseNullableReferenceTypes = false); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs index ec3b54d4c..f3095b97c 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -using System.Security.Cryptography.X509Certificates; using System.Text; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator.CSharp; @@ -37,7 +37,7 @@ public CSharpSourceTextBuilder Append(string text) return this; } - public CSharpSourceTextBuilder AppendConstantList(IEnumerable values) + public CSharpSourceTextBuilder AppendLiteralList(IEnumerable values) { var first = true; @@ -66,11 +66,11 @@ public CSharpSourceTextBuilder AppendLiteral(object? value) string s => AppendLiteral(s), ImmutableArray array => AppendLiteralArray(array), ImmutableArray array => AppendLiteralArray(array), - _ => throw new NotSupportedException($"Values of type {value.GetType().FullName} cannot be encoded as a constant in C#.") + _ => throw new NotSupportedException($"Values of type {value.GetType().FullName} cannot be encoded as a literal value in C#.") }; } - private CSharpSourceTextBuilder AppendLiteralArray(ImmutableArray array) + public CSharpSourceTextBuilder AppendLiteralArray(ImmutableArray array) { if (!CSharpSyntax.TypeAliases.TryGetValue(typeof(T), out var typeName)) { @@ -84,17 +84,19 @@ private CSharpSourceTextBuilder AppendLiteralArray(ImmutableArray array) return Append("[0]"); } - return Append("[] { ").AppendConstantList(array).Append(" }"); + return Append("[] { ").AppendLiteralList(array).Append(" }"); } - private CSharpSourceTextBuilder AppendLiteral(string? s) + public CSharpSourceTextBuilder AppendLiteral(string? s) { if (s == null) { return Append("null"); } - _buffer.Append('"').Append(s).Append('"'); + var escapedValue = CSharpSyntax.FormatLiteral(s); + + Append(escapedValue); return this; } @@ -158,7 +160,6 @@ public CSharpSourceTextBuilder AppendLine(string text) /// public CSharpSourceTextBuilder BeginBlock() { - AppendLine(); _context = new CodeBlock(_context); return this; } @@ -167,7 +168,7 @@ public CSharpSourceTextBuilder BeginBlock() /// Appends the specified text and starts a new block. /// /// The text to append. - public CSharpSourceTextBuilder BeginBlock(string text) => Append(text).BeginBlock(); + public CSharpSourceTextBuilder BeginBlock(string text) => AppendLine(text).BeginBlock(); /// /// Ends the current block and begins a new line. @@ -246,17 +247,21 @@ public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribut { Append('('); - AppendConstantList(attribute.PositionalArguments); + AppendLiteralList(attribute.PositionalArguments); var firstProperty = true; foreach (var (name, value) in attribute.NamedArguments) { - if (!firstProperty) + if (firstProperty) { - + firstProperty = false; + } + else + { + Append(", "); } - firstProperty = false; + Append(name).Append(" = ").AppendLiteral(value); } Append(')'); @@ -269,6 +274,40 @@ public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribut public CSharpSourceTextBuilder AppendTypeReference(TypeIdentifier type) { + return type switch + { + NamedTypeIdentifier namedType => AppendTypeReference(namedType), + ArrayTypeIdentifier arrayType => AppendTypeReference(arrayType), + _ => throw new NotImplementedException() + }; + } + + public CSharpSourceTextBuilder AppendTypeReference(NamedTypeIdentifier type) + { + if (!type.Namespace.IsEmpty) + { + Append("global::").Append(type.Namespace).Append('.'); + } + + Append(type.LocalName); + + if (type.IsNullable) + { + Append('?'); + } + + return this; + } + + public CSharpSourceTextBuilder AppendTypeReference(ArrayTypeIdentifier type) + { + AppendTypeReference(type.ItemType).Append("[]"); + + if (type.IsNullable) + { + Append('?'); + } + return this; } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs index 8de743130..81e0d1e72 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSyntax.cs @@ -1,10 +1,14 @@ -using System.Globalization; +using Microsoft.CodeAnalysis.CSharp; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Globalization; using System.Text; namespace Reqnroll.FeatureSourceGenerator.CSharp; internal static class CSharpSyntax { + public static IdentifierString ExampleTagsParameterName { get; } = new IdentifierString("_exampleTags"); + public static readonly Dictionary TypeAliases = new() { { typeof(byte), "byte" }, @@ -110,4 +114,9 @@ private static bool IsValidInIdentifier(char c) || category == UnicodeCategory.ConnectorPunctuation || category == UnicodeCategory.Format; } + + internal static string FormatLiteral(string s) + { + return SymbolDisplay.FormatLiteral(s, true); + } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index c4bcf3dbf..d62ce0f26 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -1,84 +1,155 @@ using System.Collections.Immutable; using System.Text; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator.CSharp; /// /// Represents a class which is a test fixture to execute the scenarios associated with a feature. /// -public class CSharpTestFixtureClass : TestFixtureClass -{ +public class CSharpTestFixtureClass( + NamedTypeIdentifier identifier, + string hintName, + FeatureInformation feature, + ImmutableArray attributes = default, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) : TestFixtureClass( + identifier, + hintName, + feature, + attributes), IEquatable +{ private static readonly Encoding Encoding = new UTF8Encoding(false); - public CSharpTestFixtureClass( - NamedTypeIdentifier identifier, - string hintName, - ImmutableArray attributes = default, - ImmutableArray methods = default) : - base( - identifier, - hintName, - attributes) - { - Methods = methods.IsDefault ? ImmutableArray.Empty : methods; - } + public ImmutableArray Methods { get; } = + methods.IsDefault ? ImmutableArray.Empty : methods; - public ImmutableArray Methods { get; } + public CSharpRenderingOptions RenderingOptions { get; } = renderingOptions ?? new CSharpRenderingOptions(); + + public ImmutableArray NamespaceUsings { get; } = ImmutableArray.Create(new NamespaceString("System.Linq")); public override IEnumerable GetMethods() => Methods; - public override SourceText Render() + public override SourceText Render(CancellationToken cancellationToken = default) { var buffer = new CSharpSourceTextBuilder(); - RenderTo(buffer); + RenderTo(buffer, cancellationToken); return SourceText.From(buffer.ToString(), Encoding); } - public void RenderTo(CSharpSourceTextBuilder sourceBuilder) + public void RenderTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken = default) { - sourceBuilder.Append("namespace ").Append(Identifier.Namespace.ToString()).AppendLine(); + sourceBuilder.Append("namespace ").Append(Identifier.Namespace).AppendLine(); sourceBuilder.BeginBlock("{"); + if (!NamespaceUsings.IsEmpty) + { + foreach (var import in NamespaceUsings) + { + sourceBuilder.Append("using ").Append(import).AppendLine(";"); + } + + sourceBuilder.AppendLine(); + } + if (!Attributes.IsEmpty) { foreach (var attribute in Attributes) { + cancellationToken.ThrowIfCancellationRequested(); sourceBuilder.AppendAttributeBlock(attribute); sourceBuilder.AppendLine(); } } - sourceBuilder.Append("public class ").Append(Identifier.LocalName.ToString()); + sourceBuilder.Append("public partial class ").AppendLine(Identifier.LocalName); sourceBuilder.BeginBlock("{"); - RenderTestFixturePreambleTo(sourceBuilder); + if (RenderingOptions.EnableLineMapping && FeatureInformation.FilePath != null) + { + sourceBuilder.AppendDirective($"#line 1 \"{FeatureInformation.FilePath}\""); + sourceBuilder.AppendDirective("#line hidden"); + sourceBuilder.AppendLine(); + } + + RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + + sourceBuilder.EndBlock("}"); + sourceBuilder.EndBlock("}"); + } + + protected virtual void RenderTestFixtureContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + RenderFeatureInformationPropertiesTo(sourceBuilder); + + RenderScenarioInitializeMethodTo(sourceBuilder, cancellationToken); sourceBuilder.AppendLine(); - RenderMethodsTo(sourceBuilder); + RenderMethodsTo(sourceBuilder, cancellationToken); + } + + private void RenderFeatureInformationPropertiesTo(CSharpSourceTextBuilder sourceBuilder) + { + sourceBuilder + .Append("private static readonly string[] FeatureTags = new string[] { ") + .AppendLiteralList(FeatureInformation.Tags) + .AppendLine(" };"); + + sourceBuilder.AppendLine(); + + sourceBuilder + .AppendLine("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") + .BeginBlock() + .Append("new global::System.Globalization.CultureInfo(").AppendLiteral(FeatureInformation.Language).AppendLine("), ") + .AppendLiteral(Path.GetDirectoryName(FeatureInformation.FilePath)).AppendLine(", ") + .AppendLiteral(FeatureInformation.Name).AppendLine(", ") + .AppendLiteral(FeatureInformation.Description).AppendLine(", ") + .AppendLine("global::Reqnroll.ProgrammingLanguage.CSharp, ") + .AppendLine("FeatureTags);") + .EndBlock(); + } + + private void RenderScenarioInitializeMethodTo( + CSharpSourceTextBuilder sourceBuilder, + CancellationToken cancellationToken) + { + sourceBuilder.AppendLine( + "private global::System.Threading.Tasks.Task ScenarioInitialize(" + + "global::Reqnroll.ITestRunner testRunner, " + + "global::Reqnroll.ScenarioInfo scenarioInfo)"); + + sourceBuilder.BeginBlock("{"); + + RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + + sourceBuilder.AppendLine("return global::System.Threading.Tasks.Task.CompletedTask;"); - sourceBuilder.EndBlock("}"); sourceBuilder.EndBlock("}"); } - protected virtual void RenderTestFixturePreambleTo(CSharpSourceTextBuilder sourceBuilder) + protected virtual void RenderScenarioInitializeMethodBodyTo( + CSharpSourceTextBuilder sourceBuilder, + CancellationToken cancellationToken) { - throw new NotImplementedException(); + sourceBuilder.AppendLine("testRunner.OnScenarioInitialize(scenarioInfo);"); } - protected virtual void RenderMethodsTo(CSharpSourceTextBuilder sourceBuilder) + protected virtual void RenderMethodsTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) { var first = true; foreach (var method in Methods) { + cancellationToken.ThrowIfCancellationRequested(); + if (!first) { sourceBuilder.AppendLine(); } - method.RenderTo(sourceBuilder); + method.RenderTo(sourceBuilder, RenderingOptions); if (first) { @@ -86,4 +157,33 @@ protected virtual void RenderMethodsTo(CSharpSourceTextBuilder sourceBuilder) } } } + + public override bool Equals(object obj) => Equals(obj as CSharpTestFixtureClass); + + public override string ToString() => $"Identifier={Identifier}"; + + public bool Equals(CSharpTestFixtureClass? other) + { + if (!base.Equals(other)) + { + return false; + } + + return base.Equals(other) && + Methods.SequenceEqual(other.Methods) && + RenderingOptions.Equals(other.RenderingOptions); + } + + public override int GetHashCode() + { + unchecked + { + var hash = base.GetHashCode(); + + hash *= 83155477 + Methods.GetSequenceHashCode(); + hash *= 83155477 + RenderingOptions.GetHashCode(); + + return hash; + } + } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs deleted file mode 100644 index 8f0c3d403..000000000 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGeneration.cs +++ /dev/null @@ -1,432 +0,0 @@ -using Gherkin.Ast; -using System.Collections.Immutable; - -namespace Reqnroll.FeatureSourceGenerator.CSharp; - -/// -/// Represents a generation of a C# test fixture to allow the execution of a Gherkin feature. -/// -/// -/// This class provides the base for all Reqnroll's built-in test-framework implementations. It lays out the fundamental -/// common structure: -/// -/// A test class named after the feature title. -/// Every scenario/example and outline mapped to a method which passes the steps to the Reqnroll runtime. -/// -/// -public abstract class CSharpTestFixtureGeneration(FeatureInformation featureInfo) -{ - public FeatureInformation FeatureInformation { get; } = featureInfo; - - public CSharpCompilationInformation CompilationInformation { get; } = - (CSharpCompilationInformation)featureInfo.CompilationInformation; - - protected bool AreNullableReferenceTypesEnabled => CompilationInformation.HasNullableReferencesEnabled; - - private bool IsLineMappingEnabled { get; } = featureInfo.FeatureSyntax.FilePath != null; - - protected GherkinDocument Document { get; } = featureInfo.FeatureSyntax.GetRoot(); - - protected CSharpSourceTextBuilder SourceBuilder { get; } = new(); - - internal SourceText GetSourceText() - { - SourceBuilder.Reset(); - - SourceBuilder.Append("namespace ").Append(FeatureInformation.FeatureNamespace).AppendLine(); - SourceBuilder.BeginBlock("{"); - - AppendTestFixtureClass(); - - SourceBuilder.EndBlock("}"); - - return SourceBuilder.ToSourceText(); - } - - protected virtual string GetClassName() => - CSharpSyntax.GenerateTypeIdentifier(Document.Feature.Name + Document.Feature.Keyword); - - protected virtual string? GetBaseType() => null; - - protected virtual IEnumerable GetInterfaces() => []; - - protected virtual void AppendTestFixtureClass() - { - var attributes = GetTestFixtureAttributes(); - - foreach (var attribute in attributes) - { - AppendAttribute(attribute); - } - - var feature = Document.Feature; - var className = GetClassName(); - var baseType = GetBaseType(); - var interfaces = GetInterfaces().ToList(); - - SourceBuilder.Append("public class ").Append(className); - - if (baseType != null || interfaces.Count > 0) - { - var baseTypes = new List(); - - if (baseType != null) - { - baseTypes.Add(baseType); - } - - baseTypes.AddRange(interfaces); - - SourceBuilder.Append(" : "); - - var first = true; - - foreach (var value in baseTypes) - { - if (first) - { - first = false; - } - else - { - SourceBuilder.Append(", "); - } - - SourceBuilder.Append(value); - } - } - - SourceBuilder.BeginBlock("{"); - - AppendTestFixturePreamble(); - - foreach (var child in feature.Children) - { - switch (child) - { - case Scenario scenario: - SourceBuilder.AppendLine(); - AppendTestMethodForScenario(scenario, null); - break; - - case Rule rule: - foreach (var ruleChild in rule.Children) - { - switch (ruleChild) - { - case Scenario scenario: - SourceBuilder.AppendLine(); - AppendTestMethodForScenario(scenario, rule); - break; - } - } - break; - } - } - - SourceBuilder.EndBlock("}"); - } - - private void AppendAttribute(AttributeDescriptor attribute) - { - //SourceBuilder.Append("[global::").AppendTypeIdentifier(); - - //if (attribute.PositionalArguments.Any() || attribute.NamedArguments.Any()) - //{ - // var first = true; - - // SourceBuilder.Append("("); - - // foreach (var argument in attribute.PositionalArguments) - // { - // if (!first) - // { - // SourceBuilder.Append(", "); - // } - - // first = false; - - // SourceBuilder.AppendLiteral(argument); - // } - - // foreach (var (name, argument) in attribute.NamedArguments) - // { - // if (!first) - // { - // SourceBuilder.Append(", "); - // } - - // first = false; - - // SourceBuilder.Append(name).Append(" = ").AppendLiteral(argument); - // } - - // SourceBuilder.Append(")"); - //} - - //SourceBuilder.AppendLine("]"); - } - - protected virtual IEnumerable GetTestFixtureAttributes() => []; - - protected virtual void AppendTestFixturePreamble() - { - SourceBuilder.AppendLine("// start: shared service method & consts, NO STATE!"); - SourceBuilder.AppendLine(); - - if (IsLineMappingEnabled && FeatureInformation.FeatureSyntax.FilePath != null) - { - SourceBuilder.AppendDirective($"#line 1 \"{FeatureInformation.FeatureSyntax.FilePath}\""); - SourceBuilder.AppendDirective("#line hidden"); - SourceBuilder.AppendLine(); - } - - var feature = FeatureInformation.FeatureSyntax.GetRoot().Feature; - - SourceBuilder - .Append("private static readonly string[] FeatureTags = new string[] { ") - .AppendConstantList(feature.Tags.Select(tag => tag.Name.TrimStart('@'))) - .AppendLine(" };") - .AppendLine(); - - SourceBuilder - .AppendLine("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") - .BeginBlock() - .AppendLine("new global::System.Globalization.CultureInfo(").AppendLiteral(feature.Language).AppendLine("), ") - .AppendLiteral(Path.GetDirectoryName(FeatureInformation.FeatureSyntax.FilePath).Replace("\\", "\\\\")).AppendLine(", ") - .AppendLiteral(feature.Name).AppendLine(", ") - .AppendLine("null, ") - .AppendLine("global::Reqnroll.ProgrammingLanguage.CSharp, ") - .AppendLine("FeatureTags);") - .EndBlock() - .AppendLine(); - - AppendScenarioInitializeMethod(); - - SourceBuilder.AppendLine(); - SourceBuilder.AppendLine("// end: shared service method & consts, NO STATE!"); - } - - private void AppendScenarioInitializeMethod() - { - SourceBuilder.AppendLine( - "public async global::System.Threading.Tasks.Task ScenarioInitialize(" + - "global::Reqnroll.ITestRunner testRunner, " + - "global::Reqnroll.ScenarioInfo scenarioInfo)"); - - SourceBuilder.BeginBlock("{"); - - AppendScenarioInitializeMethodBody(); - - SourceBuilder.EndBlock("}"); - } - - protected virtual void AppendScenarioInitializeMethodBody() - { - SourceBuilder.AppendLine("testRunner.OnScenarioInitialize(scenarioInfo);"); - } - - protected virtual void AppendTestMethodForScenario(Scenario scenario, Rule? rule) - { - var attributes = GetTestMethodAttributes(scenario); - - foreach (var attribute in attributes) - { - AppendAttribute(attribute); - } - - var parameters = GetTestMethodParameters(scenario); - - SourceBuilder - .Append("public async Task ") - .Append(CSharpSyntax.CreateMethodIdentifier(scenario.Name)); - - if (parameters.Length > 0) - { - SourceBuilder - .AppendLine("(") - .BeginBlock(); - - var first = true; - - foreach (var parameter in parameters) - { - if (first) - { - first = false; - } - else - { - SourceBuilder.AppendLine(","); - } - - SourceBuilder.Append(parameter); - } - - SourceBuilder - .Append(")") - .EndBlock(); - } - else - { - SourceBuilder.AppendLine("()"); - } - - SourceBuilder.BeginBlock("{"); - - AppendTestMethodBodyForScenario(scenario, rule); - - SourceBuilder.EndBlock("}"); - } - - protected virtual ImmutableArray GetTestMethodParameters(Scenario scenario) - { - var parameters = new List(); - - var example = scenario.Examples.FirstOrDefault(); - - if (example != null) - { - parameters.AddRange( - example.TableHeader.Cells.Select(heading => $"string {CSharpSyntax.GenerateParameterIdentifier(heading.Value)}")); - - parameters.Add("string[] exampleTags"); - } - - return parameters.ToImmutableArray(); - } - - protected virtual void AppendTestMethodBodyForScenario(Scenario scenario, Rule? rule) - { - AppendTestRunnerLookupForScenario(scenario); - SourceBuilder.AppendLine(); - - AppendScenarioInfo(scenario, rule); - SourceBuilder.AppendLine(); - - SourceBuilder.AppendLine("try"); - SourceBuilder.BeginBlock("{"); - - if (IsLineMappingEnabled) - { - SourceBuilder.AppendDirective($"#line {scenario.Location.Line}"); - } - - SourceBuilder.AppendLine("await ScenarioInitialize(testRunner, scenarioInfo);"); - - if (IsLineMappingEnabled) - { - SourceBuilder.AppendDirective("#line hidden"); - } - - SourceBuilder.AppendLine(); - - SourceBuilder.AppendLine("if (global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags))"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("testRunner.SkipScenario();"); - SourceBuilder.EndBlock("}"); - SourceBuilder.AppendLine("else"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("await testRunner.OnScenarioStartAsync();"); - SourceBuilder.AppendLine(); - SourceBuilder.AppendLine("// start: invocation of scenario steps"); - - foreach (var step in scenario.Steps) - { - AppendScenarioStepInvocation(step, scenario); - } - - SourceBuilder.AppendLine("// end: invocation of scenario steps"); - SourceBuilder.EndBlock("}"); - SourceBuilder.AppendLine(); - SourceBuilder.AppendLine("// finishing the scenario"); - SourceBuilder.AppendLine("await testRunner.CollectScenarioErrorsAsync();"); - - SourceBuilder.EndBlock("}"); - SourceBuilder.AppendLine("finally"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("await testRunner.OnScenarioEndAsync();"); - SourceBuilder.EndBlock("}"); - } - - protected virtual void AppendScenarioStepInvocation(Step step, Scenario scenario) - { - if (IsLineMappingEnabled) - { - SourceBuilder.AppendDirective($"#line {step.Location.Line}"); - } - - SourceBuilder - .Append("await testRunner.") - .Append( - step.KeywordType switch - { - global::Gherkin.StepKeywordType.Context => "Given", - global::Gherkin.StepKeywordType.Action => "When", - global::Gherkin.StepKeywordType.Outcome => "Then", - global::Gherkin.StepKeywordType.Conjunction => "And", - _ => throw new NotSupportedException($"Steps of type \"{step.Keyword}\" are not supported.") // TODO: Add message from resx - }) - .Append("Async(") - .AppendLiteral(step.Text) - .Append(", null, null, ") - .AppendLiteral(step.Keyword) - .AppendLine(");"); - - if (IsLineMappingEnabled) - { - SourceBuilder.AppendDirective("#line hidden"); - } - } - - protected virtual void AppendScenarioInfo(Scenario scenario, Rule? rule) - { - SourceBuilder.AppendLine("// start: calculate ScenarioInfo"); - SourceBuilder - .Append("var tagsOfScenario = new string[] { ") - .AppendConstantList(scenario.Tags.Select(tag => tag.Name.TrimStart('@'))) - .AppendLine(" };"); - SourceBuilder.AppendLine( - "var argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); // needed for scenario outlines"); - - if (rule == null) - { - SourceBuilder.AppendLine("var inheritedTags = FeatureTags;"); - } - else - { - SourceBuilder - .Append("var ruleTags = new string[] { ") - .AppendConstantList(rule.Tags.Select(tag => tag.Name.TrimStart('@'))) - .AppendLine(" };"); - - SourceBuilder.AppendLine("var inheritedTags = FeatureTags.Concat(ruleTags)"); - } - - SourceBuilder - .Append("var scenarioInfo = new global::Reqnroll.ScenarioInfo(") - .AppendLiteral(scenario.Name) - .AppendLine(", null, tagsOfScenario, argumentsOfScenario, inheritedTags);"); - SourceBuilder.AppendLine("// end: calculate ScenarioInfo"); - } - - /// - /// Appends the code to provide the test runner instance for the scenario execution. - /// - /// The scenario to append code for. - /// - /// Implementations of this method must append code to achieve the following: - /// - /// Declare a local variable named testRunner of a type which can be assigned to type Reqnroll.TestRunner. - /// Assign to the testRunner variable the instance to use as the test runner for the scenario. - /// - /// - protected virtual void AppendTestRunnerLookupForScenario(Scenario scenario) - { - SourceBuilder.AppendLine("// getting test runner"); - SourceBuilder.AppendLine("var testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString();"); - SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); - } - - protected virtual IEnumerable GetTestMethodAttributes(Scenario scenario) => []; -} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs index 03813a3e5..629698c0c 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs @@ -1,40 +1,31 @@ using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator.CSharp; /// /// Provides a base for generating CSharp test fixtures. /// -public abstract class CSharpTestFixtureGenerator : ITestFixtureGenerator +/// The test framework handler the generator is associated with. +/// The type of test fixture class produced by the generator. +/// The type of test method produced by the generator. +public abstract class CSharpTestFixtureGenerator(ITestFrameworkHandler testFrameworkHandler) : + ITestFixtureGenerator + where TTestFixtureClass : CSharpTestFixtureClass + where TTestMethod : CSharpTestMethod { - public TestFixtureClass GenerateTestFixture( - FeatureInformation feature, - IEnumerable methods, - CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public TestMethod GenerateTestMethod( - ScenarioInformation scenario, - CancellationToken cancellationToken = default) => GenerateCSharpTestMethod(scenario, cancellationToken); + public ITestFrameworkHandler TestFrameworkHandler { get; } = testFrameworkHandler; - protected virtual CSharpTestMethod GenerateCSharpTestMethod( - ScenarioInformation scenario, - CancellationToken cancellationToken) - { - var identifier = CSharpSyntax.GenerateTypeIdentifier(scenario.Name); - - var attributes = GenerateTestMethodAttributes(scenario, cancellationToken); - var parameters = GenerateTestMethodParameters(scenario, cancellationToken); - - return new CSharpTestMethod(identifier, attributes, parameters); - } + protected abstract ImmutableArray GenerateTestFixtureClassAttributes( + TestFixtureGenerationContext context, + CancellationToken cancellationToken); protected virtual ImmutableArray GenerateTestMethodParameters( - ScenarioInformation scenario, + TestMethodGenerationContext context, CancellationToken cancellationToken) { + var scenario = context.ScenarioInformation; + // In the case the scenario defines no examples, we don't pass any paramters. if (scenario.Examples.IsEmpty) { @@ -49,14 +40,104 @@ protected virtual ImmutableArray GenerateTestMethodParamete .Select(heading => new ParameterDescriptor(CSharpSyntax.GenerateParameterIdentifier(heading), CommonTypes.String)) .ToList(); - // Append the tags parameter. + // Append the "example tags" parameter. parameters.Add( - new ParameterDescriptor(new IdentifierString("_tags"), new ArrayTypeIdentifier(CommonTypes.String))); + new ParameterDescriptor(CSharpSyntax.ExampleTagsParameterName, new ArrayTypeIdentifier(CommonTypes.String))); return parameters.ToImmutableArray(); } protected abstract ImmutableArray GenerateTestMethodAttributes( - ScenarioInformation scenario, + TestMethodGenerationContext context, CancellationToken cancellationToken); + + public TTestMethod GenerateTestMethod( + TestMethodGenerationContext context, + CancellationToken cancellationToken = default) + { + var scenario = context.ScenarioInformation; + var identifier = CSharpSyntax.GenerateTypeIdentifier(scenario.Name); + + var attributes = GenerateTestMethodAttributes(context, cancellationToken); + var parameters = GenerateTestMethodParameters(context, cancellationToken); + var scenarioParameters = GenerateScenarioParameters(context, cancellationToken); + + return CreateTestMethod(context, identifier, scenario, attributes, parameters, scenarioParameters); + } + + protected virtual ImmutableArray> GenerateScenarioParameters( + TestMethodGenerationContext context, + CancellationToken cancellationToken) + { + var scenario = context.ScenarioInformation; + + // In the case the scenario defines no examples, we don't pass any paramters. + if (scenario.Examples.IsEmpty) + { + return ImmutableArray>.Empty; + } + + var headings = scenario.Examples.First().Headings; + + return headings + .Select(CSharpSyntax.GenerateParameterIdentifier) + .Select(identifier => new KeyValuePair(identifier, identifier)) + .ToImmutableArray(); + } + + protected abstract TTestMethod CreateTestMethod( + TestMethodGenerationContext context, + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray attributes, + ImmutableArray parameters, + ImmutableArray> scenarioParameters); + + public TTestFixtureClass GenerateTestFixtureClass( + TestFixtureGenerationContext context, + IEnumerable methods, + CancellationToken cancellationToken = default) + { + var feature = context.FeatureInformation; + var featureTitle = feature.Name; + if (!featureTitle.EndsWith(" Feature", StringComparison.OrdinalIgnoreCase)) + { + featureTitle += " Feature"; + } + + var identifier = CSharpSyntax.GenerateTypeIdentifier(featureTitle); + + var attributes = GenerateTestFixtureClassAttributes(context, cancellationToken); + var namedIdentitifer = new NamedTypeIdentifier(context.TestFixtureNamespace, identifier); + + var generationOptions = new CSharpRenderingOptions( + UseNullableReferenceTypes: context.CompilationInformation.HasNullableReferencesEnabled); + + return CreateTestFixtureClass( + context, + namedIdentitifer, + feature, + attributes, + methods.ToImmutableArray(), + generationOptions); + } + + protected abstract TTestFixtureClass CreateTestFixtureClass( + TestFixtureGenerationContext context, + NamedTypeIdentifier identifier, + FeatureInformation feature, + ImmutableArray attributes, + ImmutableArray methods, + CSharpRenderingOptions renderingOptions); + + TestFixtureClass ITestFixtureGenerator.GenerateTestFixtureClass( + TestFixtureGenerationContext context, + IEnumerable methods, + CancellationToken cancellationToken) => GenerateTestFixtureClass(context, methods.Cast(), cancellationToken); + + TestMethod ITestFixtureGenerator.GenerateTestMethod( + TestMethodGenerationContext context, + CancellationToken cancellationToken) => GenerateTestMethod(context, cancellationToken); + + public virtual bool CanGenerateForCompilation(CSharpCompilationInformation compilation) => true; } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs index 9e0337104..41a7e4a34 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureSourceGenerator.cs @@ -7,7 +7,7 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; /// A generator of Reqnroll test fixtures for the C# language. /// [Generator(LanguageNames.CSharp)] -public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator +public class CSharpTestFixtureSourceGenerator : TestFixtureSourceGenerator { /// /// Initializes a new instance of the class. @@ -25,7 +25,7 @@ internal CSharpTestFixtureSourceGenerator(params ITestFrameworkHandler[] handler { } - protected override CompilationInformation GetCompilationInformation(Compilation compilation) + protected override CSharpCompilationInformation GetCompilationInformation(Compilation compilation) { var cSharpCompilation = (CSharpCompilation)compilation; diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs index 27bf2cadb..2ebd028f4 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs @@ -1,12 +1,15 @@ using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator.CSharp; public class CSharpTestMethod( IdentifierString identifier, + ScenarioInformation scenario, ImmutableArray attributes = default, - ImmutableArray parameters = default) : - TestMethod(identifier, attributes, parameters), IEquatable + ImmutableArray parameters = default, + ImmutableArray> scenarioParameters = default) : + TestMethod(identifier, scenario, attributes, parameters, scenarioParameters), IEquatable { public override bool Equals(object obj) => Equals(obj as CSharpTestMethod); @@ -14,12 +17,16 @@ public class CSharpTestMethod( public override int GetHashCode() => base.GetHashCode(); - public void RenderTo(CSharpSourceTextBuilder sourceBuilder) + public void RenderTo( + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken = default) { if (!Attributes.IsEmpty) { foreach (var attribute in Attributes) { + cancellationToken.ThrowIfCancellationRequested(); sourceBuilder.AppendAttributeBlock(attribute); sourceBuilder.AppendLine(); } @@ -35,6 +42,7 @@ public void RenderTo(CSharpSourceTextBuilder sourceBuilder) var first = true; foreach (var parameter in Parameters) { + cancellationToken.ThrowIfCancellationRequested(); if (!first) { sourceBuilder.AppendLine(","); @@ -57,13 +65,171 @@ public void RenderTo(CSharpSourceTextBuilder sourceBuilder) sourceBuilder.BeginBlock("{"); - RenderMethodBodyTo(sourceBuilder); + RenderMethodBodyTo(sourceBuilder, renderingOptions, cancellationToken); sourceBuilder.EndBlock("}"); } - protected virtual void RenderMethodBodyTo(CSharpSourceTextBuilder sourceBuilder) + protected virtual void RenderMethodBodyTo( + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); + RenderTestRunnerLookupTo(sourceBuilder, renderingOptions, cancellationToken); + sourceBuilder.AppendLine(); + + RenderScenarioInfoTo(sourceBuilder, renderingOptions, cancellationToken); + sourceBuilder.AppendLine(); + + sourceBuilder.AppendLine("try"); + sourceBuilder.BeginBlock("{"); + + if (renderingOptions.EnableLineMapping) + { + sourceBuilder.AppendDirective($"#line {Scenario.LineNumber}"); + } + + sourceBuilder.AppendLine("await ScenarioInitialize(testRunner, scenarioInfo);"); + + if (renderingOptions.EnableLineMapping) + { + sourceBuilder.AppendDirective("#line hidden"); + } + + sourceBuilder.AppendLine(); + + sourceBuilder.AppendLine("if (global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags))"); + sourceBuilder.BeginBlock("{"); + sourceBuilder.AppendLine("testRunner.SkipScenario();"); + sourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine("else"); + sourceBuilder.BeginBlock("{"); + sourceBuilder.AppendLine("await testRunner.OnScenarioStartAsync();"); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("// start: invocation of scenario steps"); + + foreach (var step in Scenario.Steps) + { + cancellationToken.ThrowIfCancellationRequested(); + RenderScenarioStepInvocationTo(step, sourceBuilder, renderingOptions, cancellationToken); + } + + sourceBuilder.AppendLine("// end: invocation of scenario steps"); + sourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("// finishing the scenario"); + sourceBuilder.AppendLine("await testRunner.CollectScenarioErrorsAsync();"); + + sourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine("finally"); + sourceBuilder.BeginBlock("{"); + sourceBuilder.AppendLine("await testRunner.OnScenarioEndAsync();"); + sourceBuilder.EndBlock("}"); + } + + protected virtual void RenderScenarioStepInvocationTo( + ScenarioStep step, + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken) + { + if (renderingOptions.EnableLineMapping) + { + sourceBuilder.AppendDirective($"#line {step.LineNumber}"); + } + + sourceBuilder + .Append("await testRunner.") + .Append( + step.KeywordType switch + { + global::Gherkin.StepKeywordType.Context => "Given", + global::Gherkin.StepKeywordType.Action => "When", + global::Gherkin.StepKeywordType.Outcome => "Then", + global::Gherkin.StepKeywordType.Conjunction => "And", + _ => throw new NotSupportedException() + }) + .Append("Async(") + .AppendLiteral(step.Text) + .Append(", null, null, ") + .AppendLiteral(step.Keyword) + .AppendLine(");"); + + if (renderingOptions.EnableLineMapping) + { + sourceBuilder.AppendDirective("#line hidden"); + } + } + + protected virtual void RenderScenarioInfoTo( + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken) + { + sourceBuilder.AppendLine("// start: calculate ScenarioInfo"); + sourceBuilder + .Append("var tagsOfScenario = new string[] { ") + .AppendLiteralList(Scenario.Tags) + .Append(" }"); + + // If a parameter has been defined for passing tags from the example, include it in the scenario's tags. + var exampleTagsParameter = Parameters.FirstOrDefault(parameter => parameter.Name == CSharpSyntax.ExampleTagsParameterName); + if (exampleTagsParameter != null) + { + sourceBuilder.Append(".Concat(").Append(exampleTagsParameter.Name).Append(").ToArray()"); + } + sourceBuilder.AppendLine(";"); + + sourceBuilder.AppendLine( + "var argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); // needed for scenario outlines"); + + foreach (var (name, value) in ParametersOfScenario) + { + sourceBuilder + .Append("argumentsOfScenario.Add(").AppendLiteral(name).Append(", ").Append(value).Append(");"); + } + + if (Scenario.Rule == null) + { + sourceBuilder.AppendLine("var inheritedTags = FeatureTags;"); + } + else + { + sourceBuilder + .Append("var ruleTags = new string[] { ") + .AppendLiteralList(Scenario.Rule.Tags) + .AppendLine(" };"); + + sourceBuilder.AppendLine("var inheritedTags = FeatureTags.Concat(ruleTags).ToArray();"); + } + + sourceBuilder + .Append("var scenarioInfo = new global::Reqnroll.ScenarioInfo(") + .AppendLiteral(Scenario.Name) + .AppendLine(", null, tagsOfScenario, argumentsOfScenario, inheritedTags);"); + sourceBuilder.AppendLine("// end: calculate ScenarioInfo"); + } + + /// + /// Renders the code to provide the test runner instance for the test method. + /// + /// The source builder to append the code to. + /// Options which control the rendering of the C# code. + /// A token used to signal when rendering should be canceled. + /// + /// Implementations of this method must append code to achieve the following: + /// + /// Declare a local variable named testRunner of a type which can be assigned to type Reqnroll.TestRunner. + /// Assign to the testRunner variable the instance to use as the test runner for the scenario. + /// + /// + protected virtual void RenderTestRunnerLookupTo( + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken) + { + sourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();"); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs new file mode 100644 index 000000000..5e535acbb --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs @@ -0,0 +1,92 @@ +using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; +public class MSTestCSharpTestFixtureClass( + NamedTypeIdentifier identifier, + string hintName, + FeatureInformation feature, + ImmutableArray attributes = default, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) : CSharpTestFixtureClass(identifier, hintName, feature, attributes, methods, renderingOptions) +{ + protected override void RenderTestFixtureContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + RenderTestRunnerFieldTo(sourceBuilder); + + sourceBuilder.AppendLine(); + + RenderTestContextPropertyTo(sourceBuilder); + + sourceBuilder.AppendLine(); + + RenderClassInitializeMethodTo(sourceBuilder, cancellationToken); + + sourceBuilder.AppendLine(); + + RenderClassCleanupMethodTo(sourceBuilder, cancellationToken); + + sourceBuilder.AppendLine(); + + base.RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + } + + private void RenderTestRunnerFieldTo(CSharpSourceTextBuilder sourceBuilder) + { + sourceBuilder.Append("private static global::Reqnroll.ITestRunner"); + if (RenderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append("?"); + } + sourceBuilder.AppendLine(" TestRunner;"); + } + + private void RenderTestContextPropertyTo(CSharpSourceTextBuilder sourceBuilder) + { + sourceBuilder.Append("public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext"); + if (RenderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append("?"); + } + sourceBuilder.AppendLine(" TestContext { get; set; }"); + } + + protected virtual void RenderClassInitializeMethodTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + sourceBuilder + .AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitialize]") + .AppendLine("public static Task IntializeFeatureAsync(global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)") + .BeginBlock("{") + .AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();") + .AppendLine("return TestRunner.OnFeatureStartAsync(FeatureInfo);") + .EndBlock("}"); + } + + protected virtual void RenderClassCleanupMethodTo( + CSharpSourceTextBuilder sourceBuilder, + CancellationToken cancellationToken) + { + sourceBuilder + .AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanup(" + + "Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupBehavior.EndOfClass)]") + .AppendLine("public static async Task TeardownFeatureAsync()") + .BeginBlock("{") + .Append("if (TestRunner == null)") + .BeginBlock("{") + .AppendLine("return;") + .EndBlock("}") + .AppendLine("await TestRunner.OnFeatureEndAsync();") + .AppendLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(TestRunner);") + .AppendLine("TestRunner = null;") + .EndBlock("}"); + } + + protected override void RenderScenarioInitializeMethodBodyTo( + CSharpSourceTextBuilder sourceBuilder, + CancellationToken cancellationToken) + { + base.RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + + sourceBuilder.AppendLine("testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(TestContext);"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..27fb37ae4 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs @@ -0,0 +1,77 @@ +using Reqnroll.FeatureSourceGenerator.MSTest; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; + +/// +/// Performs generation of MSTest test fixtures in the C# language. +/// +internal class MSTestCSharpTestFixtureGenerator(MSTestHandler frameworkHandler) : + CSharpTestFixtureGenerator(frameworkHandler) +{ + protected override MSTestCSharpTestFixtureClass CreateTestFixtureClass( + TestFixtureGenerationContext context, + NamedTypeIdentifier identifier, + FeatureInformation feature, + ImmutableArray attributes, + ImmutableArray methods, + CSharpRenderingOptions renderingOptions) + { + return new MSTestCSharpTestFixtureClass(identifier, context.FeatureHintName, feature, attributes, methods, renderingOptions); + } + + protected override CSharpTestMethod CreateTestMethod( + TestMethodGenerationContext context, + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray attributes, + ImmutableArray parameters, + ImmutableArray> scenarioParameters) + { + return new MSTestCSharpTestMethod(identifier, scenario, attributes, parameters, scenarioParameters); + } + + protected override ImmutableArray GenerateTestFixtureClassAttributes( + TestFixtureGenerationContext context, + CancellationToken cancellationToken) + { + return ImmutableArray.Create(MSTestSyntax.TestClassAttribute()); + } + + protected override ImmutableArray GenerateTestMethodAttributes( + TestMethodGenerationContext context, + CancellationToken cancellationToken) + { + var scenario = context.ScenarioInformation; + var feature = context.FeatureInformation; + + var attributes = new List + { + MSTestSyntax.TestMethodAttribute(), + MSTestSyntax.DescriptionAttribute(scenario.Name), + MSTestSyntax.TestPropertyAttribute("FeatureTitle", feature.Name) + }; + + foreach (var set in scenario.Examples) + { + foreach (var example in set) + { + // DataRow's constructor is DataRow(object? data, params object?[] moreData) + // Because we often pass an array of strings as a second argument, we always wrap moreData + // in an explicit array to avoid the compiler mistaking our string array as the moreData value. + var values = example.Select(item => item.Value).ToList(); + var data = values.First(); + var moreData = values.Skip(1).ToList(); + + moreData.Add(set.Tags); + + var arguments = moreData.Count > 0 ? ImmutableArray.Create(data, moreData.ToImmutableArray()) : ImmutableArray.Create(data); + + attributes.Add(MSTestSyntax.DataRowAttribute(arguments)); + } + } + + return attributes.ToImmutableArray(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs new file mode 100644 index 000000000..525ed3a5c --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs @@ -0,0 +1,27 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; +public class MSTestCSharpTestMethod( + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray attributes = default, + ImmutableArray parameters = default, + ImmutableArray> scenarioParameters = default) : + CSharpTestMethod(identifier, scenario, attributes, parameters, scenarioParameters) +{ + protected override void RenderTestRunnerLookupTo( + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken) + { + // For MSTest we use a test-runner assigned to the class. + sourceBuilder + .AppendLine("global::Reqnroll.ITestRunner testRunner;") + .AppendLine("if (TestRunner == null)") + .BeginBlock("{") + .AppendLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") + .EndBlock("}") + .AppendLine("testRunner = TestRunner;"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs b/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs deleted file mode 100644 index f8f2c8676..000000000 --- a/Reqnroll.FeatureSourceGenerator/FeatureInformation.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Reqnroll.FeatureSourceGenerator.Gherkin; - -namespace Reqnroll.FeatureSourceGenerator; - -public record FeatureInformation( - GherkinSyntaxTree FeatureSyntax, - string FeatureHintName, - string FeatureNamespace, - CompilationInformation CompilationInformation, - ITestFrameworkHandler TestFrameworkHandler, - ITestFixtureGenerator TestFixtureGenerator); diff --git a/Reqnroll.FeatureSourceGenerator/GeneratorInformation.cs b/Reqnroll.FeatureSourceGenerator/GeneratorInformation.cs new file mode 100644 index 000000000..3a717b33f --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/GeneratorInformation.cs @@ -0,0 +1,24 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +internal record GeneratorInformation( + ImmutableArray> CompatibleGenerators, + ImmutableArray> UseableGenerators, + ITestFixtureGenerator? DefaultGenerator) + where TCompilationInformation : CompilationInformation +{ + public override int GetHashCode() + { + unchecked + { + var hash = 47; + + hash *= 13 + CompatibleGenerators.GetSetHashCode(); + hash *= 13 + UseableGenerators.GetSetHashCode(); + hash *= 13 + DefaultGenerator?.GetHashCode() ?? 0; + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs index 2badad9f7..132f81db5 100644 --- a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinDocumentComparer.cs @@ -2,16 +2,16 @@ namespace Reqnroll.FeatureSourceGenerator.Gherkin; -internal class GherkinDocumentComparer : IEqualityComparer +internal class GherkinDocumentComparer : IEqualityComparer { public static GherkinDocumentComparer Default { get; } = new GherkinDocumentComparer(); - public bool Equals(GherkinDocument x, GherkinDocument y) + public bool Equals(GherkinDocument? x, GherkinDocument? y) { return false; } - public int GetHashCode(GherkinDocument obj) + public int GetHashCode(GherkinDocument? obj) { if (obj == null) { diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs index 2a0ac6679..5e6c76c1f 100644 --- a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs @@ -6,7 +6,7 @@ namespace Reqnroll.FeatureSourceGenerator.Gherkin; using Location = global::Gherkin.Ast.Location; -public class GherkinSyntaxParser +internal static class GherkinSyntaxParser { public static readonly DiagnosticDescriptor SyntaxError = new( id: DiagnosticIds.SyntaxError, @@ -16,37 +16,37 @@ public class GherkinSyntaxParser DiagnosticSeverity.Error, true); - public GherkinSyntaxTree Parse(SourceText text, string path, CancellationToken cancellationToken = default) - { - var parser = new Parser { StopAtFirstError = false }; - - GherkinDocument? document = null; - ImmutableArray? diagnostics = null; - - cancellationToken.ThrowIfCancellationRequested(); - - try - { - // CONSIDER: Using a parser that doesn't throw exceptions for syntax errors. - document = parser.Parse(new SourceTokenScanner(text)); - } - catch (CompositeParserException ex) - { - diagnostics = ex.Errors.Select(error => CreateGherkinDiagnostic(error, text, path)).ToImmutableArray(); - } - - return new GherkinSyntaxTree( - document ?? new GherkinDocument(null, []), - diagnostics ?? ImmutableArray.Empty, - path); - } + //public GherkinSyntaxTree Parse(SourceText text, string path, CancellationToken cancellationToken = default) + //{ + // var parser = new Parser { StopAtFirstError = false }; + + // GherkinDocument? document = null; + // ImmutableArray? diagnostics = null; + + // cancellationToken.ThrowIfCancellationRequested(); + + // try + // { + // // CONSIDER: Using a parser that doesn't throw exceptions for syntax errors. + // document = parser.Parse(new SourceTokenScanner(text)); + // } + // catch (CompositeParserException ex) + // { + // diagnostics = ex.Errors.Select(error => CreateGherkinDiagnostic(error, text, path)).ToImmutableArray(); + // } + + // return new GherkinSyntaxTree( + // document ?? new GherkinDocument(null, []), + // diagnostics ?? ImmutableArray.Empty, + // path); + //} - private Diagnostic CreateGherkinDiagnostic(ParserException exception, SourceText text, string path) + public static Diagnostic CreateGherkinDiagnostic(ParserException exception, SourceText text, string path) { return Diagnostic.Create(SyntaxError, CreateLocation(exception.Location, text, path), exception.Message); } - private Microsoft.CodeAnalysis.Location CreateLocation(Location location, SourceText text, string path) + private static Microsoft.CodeAnalysis.Location CreateLocation(Location location, SourceText text, string path) { var start = text.Lines[location.Line].Start + location.Column; diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs index 818e5ef27..b2f57ac29 100644 --- a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxTree.cs @@ -4,10 +4,10 @@ namespace Reqnroll.FeatureSourceGenerator.Gherkin; public class GherkinSyntaxTree : IEquatable { - private readonly GherkinDocument _root; + private readonly GherkinDocument? _root; private readonly ImmutableArray _diagnostics; - internal GherkinSyntaxTree(GherkinDocument root, ImmutableArray diagnostics, string? path) + internal GherkinSyntaxTree(GherkinDocument? root, ImmutableArray diagnostics, string? path) { _root = root; _diagnostics = diagnostics; @@ -53,6 +53,6 @@ public override int GetHashCode() public ImmutableArray GetDiagnostics() => _diagnostics; - public GherkinDocument GetRoot() => _root; + public GherkinDocument? GetRoot() => _root; } diff --git a/Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs index f39309945..393c4bc00 100644 --- a/Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/ITestFixtureGenerator.cs @@ -1,29 +1,38 @@ -namespace Reqnroll.FeatureSourceGenerator; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator; /// /// Defines a component which generates test-fixture classes. /// -public interface ITestFixtureGenerator +/// The type of compilation information handled by the generator. +public interface ITestFixtureGenerator + where TCompilationInformation : CompilationInformation { + /// + /// Gets the test framework handler this generator is associated with. + /// + ITestFrameworkHandler TestFrameworkHandler { get; } + /// /// Generates a test method for a scenario. /// - /// The scenario to create a test method for. + /// The method-generation context. /// A token used to signal when generation should be canceled. - /// A representing the generated method. + /// A representing the generated method. TestMethod GenerateTestMethod( - ScenarioInformation scenario, + TestMethodGenerationContext context, CancellationToken cancellationToken = default); /// /// Generates a test fixture class for a feature, incorporating a set of generated methods. /// - /// The feature to generate the test-fixture class for. + /// The feature-generation context. /// The collection of methods to incorporate into the fixture. /// A token used to signal when generation should be canceled. /// A represented the generated test-fixture class. - TestFixtureClass GenerateTestFixture( - FeatureInformation feature, + TestFixtureClass GenerateTestFixtureClass( + TestFixtureGenerationContext context, IEnumerable methods, CancellationToken cancellationToken = default); } diff --git a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs index 3c8bebe6a..649afce44 100644 --- a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs @@ -1,34 +1,28 @@ namespace Reqnroll.FeatureSourceGenerator; /// -/// Defines a component that handles the aspects of generating code for a target test framework. +/// Defines a component that handles the support for a test framework. /// public interface ITestFrameworkHandler { /// - /// Gets the name of the test framework this handler supports. + /// Gets the name of the test framework associated with the handler. /// - string FrameworkName { get; } + string TestFrameworkName { get; } /// - /// Gets a value indicating whether the handler can generate code for a compilation. - /// - /// The compilation to check for compatibilty. - /// true if the handler can generate for the specified compilation; oterwise false. - bool CanGenerateForCompilation(CompilationInformation compilation); - - /// - /// Gets a value indicating whether the test framework associated with the handler has been referenced by a compilation. + /// Gets a value indicating whether the test framework associated with the handler is referenced by a compilation. /// /// The compilation to examine. - /// true if the associated test framework has been referenced by the compilation; + /// true if the test framework is referenced by the compilation; /// otherwise false. bool IsTestFrameworkReferenced(CompilationInformation compilation); /// - /// Gets a test-fixture generator for a compilation. + /// Gets a test-fixture generator for the test framework. /// - /// The compilation to get a generator for. - /// A test-fixture generator for the specified compilation. - ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation); + /// The type of compilation to obtain a generator for. + /// A test-fixture generator if one can be produced for the compilation type; otherwise null. + ITestFixtureGenerator? GetTestFixtureGenerator() + where TCompilationInformation : CompilationInformation; } diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs deleted file mode 100644 index 921c409ee..000000000 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGeneration.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Gherkin.Ast; -using Reqnroll.FeatureSourceGenerator.CSharp; -using System.Collections.Immutable; - -namespace Reqnroll.FeatureSourceGenerator.MSTest; - -internal class MSTestCSharpTestFixtureGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) -{ - private static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); - - protected override IEnumerable GetTestFixtureAttributes() - { - return base.GetTestFixtureAttributes().Concat( - [ - new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))) - ]); - } - - protected override void AppendTestFixturePreamble() - { - SourceBuilder.AppendLine("// start: MSTest Specific part"); - SourceBuilder.AppendLine(); - - SourceBuilder.Append("public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext"); - if (AreNullableReferenceTypesEnabled) - { - SourceBuilder.Append("?"); - } - SourceBuilder.AppendLine(" TestContext { get; set; }"); - SourceBuilder.AppendLine(); - - AppendClassInitializeMethod(); - SourceBuilder.AppendLine(); - - AppendClassCleanupMethod(); - SourceBuilder.AppendLine(); - - SourceBuilder.AppendLine("// end: MSTest Specific part"); - - base.AppendTestFixturePreamble(); - } - - protected virtual void AppendClassInitializeMethod() - { - SourceBuilder.AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitialize]"); - SourceBuilder.AppendLine("public static Task IntializeFeatureAsync(global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("var testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString();"); - SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); - SourceBuilder.AppendLine("return testRunner.OnFeatureStartAsync(FeatureInfo);"); - SourceBuilder.EndBlock("}"); - } - - protected virtual void AppendClassCleanupMethod() - { - SourceBuilder.AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanup]"); - SourceBuilder.AppendLine("public static Task TeardownFeatureAsync()"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("var testWorkerId = global::System.Threading.Thread.CurrentThread.ManagedThreadId.ToString();"); - SourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); - SourceBuilder.AppendLine("return testRunner.OnFeatureEndAsync();"); - SourceBuilder.EndBlock("}"); - } - - protected override IEnumerable GetTestMethodAttributes(Scenario scenario) - { - var attributes = new List - { - new( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), - new( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), - ImmutableArray.Create(scenario.Name)), - new( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), - ImmutableArray.Create("FeatureTitle", Document.Feature.Name)) - }; - - foreach (var tag in Document.Feature.Tags.Concat(scenario.Tags)) - { - attributes.Add( - new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestCategory")), - ImmutableArray.Create(tag.Name.TrimStart('@')))); - } - - foreach (var example in scenario.Examples) - { - var values = new object?[example.TableHeader.Cells.Count() + 1]; - - // Add tags as the last argument in the values passed to the data-row. - values[values.Length - 1] = example.Tags - .Select(tag => tag.Name.TrimStart('@')) - .ToImmutableArray(); - - foreach (var row in example.TableBody) - { - var i = 0; - foreach (var cell in row.Cells) - { - values[i++] = cell.Value; - } - - // DataRow's constructor is DataRow(object? data, params object?[] moreData) - // Because we often pass an array of strings as a second argument, we always wrap moreData - // in an explicit array to avoid the compiler mistaking our string array as the moreData value. - var first = values.First(); - var others = values.Skip(1).ToImmutableArray(); - - var positionalArguments = others.Length > 0 ? - ImmutableArray.Create(first, others) : - ImmutableArray.Create(first); - - attributes.Add( - new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), - positionalArguments)); - } - } - - return base.GetTestMethodAttributes(scenario).Concat(attributes); - } - - protected override void AppendScenarioInitializeMethodBody() - { - base.AppendScenarioInitializeMethodBody(); - - SourceBuilder.AppendLine(); - SourceBuilder.AppendLine("// MsTest specific customization:"); - SourceBuilder.AppendLine("testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(TestContext);"); - } -} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs deleted file mode 100644 index 0223567cd..000000000 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestCSharpTestFixtureGenerator.cs +++ /dev/null @@ -1,46 +0,0 @@ - -using Reqnroll.FeatureSourceGenerator.CSharp; -using System.Collections.Immutable; - -namespace Reqnroll.FeatureSourceGenerator.MSTest; - -/// -/// Performs generation of MSTest test fixtures in the C# language. -/// -internal class MSTestCSharpTestFixtureGenerator : CSharpTestFixtureGenerator -{ - protected override ImmutableArray GenerateTestMethodAttributes( - ScenarioInformation scenario, - CancellationToken cancellationToken) - { - var featureName = scenario.Feature.FeatureSyntax.GetRoot().Feature.Name; - - var attributes = new List - { - MSTestSyntax.TestMethodAttribute(), - MSTestSyntax.DescriptionAttribute(scenario.Name), - MSTestSyntax.TestPropertyAttribute("FeatureTitle", featureName) - }; - - foreach (var set in scenario.Examples) - { - foreach (var example in set) - { - // DataRow's constructor is DataRow(object? data, params object?[] moreData) - // Because we often pass an array of strings as a second argument, we always wrap moreData - // in an explicit array to avoid the compiler mistaking our string array as the moreData value. - var values = example.Select(item => item.Value).ToList(); - var data = values.First(); - var moreData = values.Skip(1).ToList(); - - moreData.Add(set.Tags); - - var arguments = moreData.Count > 0 ? ImmutableArray.Create(data, moreData.ToImmutableArray()) : ImmutableArray.Create(data); - - attributes.Add(MSTestSyntax.DataRowAttribute(arguments)); - } - } - - return attributes.ToImmutableArray(); - } -} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs index 7efbcc180..060027f24 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs @@ -1,16 +1,15 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp.MSTest; namespace Reqnroll.FeatureSourceGenerator.MSTest; /// -/// The handler for MSTest. +/// The MSTest framework handler. /// public class MSTestHandler : ITestFrameworkHandler { - public string FrameworkName => "MSTest"; - - public bool CanGenerateForCompilation(CompilationInformation compilation) => - compilation is CSharpCompilationInformation; + public string TestFrameworkName => "MSTest"; public bool IsTestFrameworkReferenced(CompilationInformation compilation) { @@ -18,12 +17,14 @@ public bool IsTestFrameworkReferenced(CompilationInformation compilation) .Any(assembly => assembly.Name == "Microsoft.VisualStudio.TestPlatform.TestFramework"); } - public ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation) + public ITestFixtureGenerator? GetTestFixtureGenerator() + where TCompilationInformation : CompilationInformation { - return compilation switch + if (typeof(TCompilationInformation).IsAssignableFrom(typeof(CSharpCompilationInformation))) { - CSharpCompilationInformation => new MSTestCSharpTestFixtureGenerator(), - _ => throw new NotSupportedException(), - }; + return (ITestFixtureGenerator)new MSTestCSharpTestFixtureGenerator(this); + } + + return null; } } diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs index 29b2bb2a4..506905aa7 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs @@ -1,9 +1,12 @@ using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator.MSTest; internal static class MSTestSyntax { public static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); + public static AttributeDescriptor TestClassAttribute() => + new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))); public static AttributeDescriptor TestMethodAttribute() => new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))); @@ -16,7 +19,7 @@ public static AttributeDescriptor DescriptionAttribute(string description) => public static AttributeDescriptor TestPropertyAttribute(string propertyName, object? propertyValue) => new( new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), - namedArguments: new Dictionary { { propertyName, propertyValue } }.ToImmutableDictionary()); + positionalArguments: ImmutableArray.Create(propertyName, propertyValue)); public static AttributeDescriptor DataRowAttribute(ImmutableArray values) => new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), values); diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs deleted file mode 100644 index afe43fd2f..000000000 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpSyntaxGeneration.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; - -namespace Reqnroll.FeatureSourceGenerator.NUnit; - -public class NUnitCSharpSyntaxGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) -{ -} diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs index c7730019a..c82205fd5 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs @@ -1,19 +1,44 @@ - +using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + namespace Reqnroll.FeatureSourceGenerator.NUnit; -internal class NUnitCSharpTestFixtureGenerator : ITestFixtureGenerator +internal class NUnitCSharpTestFixtureGenerator(NUnitHandler testFrameworkHandler) : + CSharpTestFixtureGenerator(testFrameworkHandler) { - public TestFixtureClass GenerateTestFixture( + protected override CSharpTestFixtureClass CreateTestFixtureClass( + TestFixtureGenerationContext context, + NamedTypeIdentifier identifier, FeatureInformation feature, - IEnumerable methods, - CancellationToken cancellationToken = default) + ImmutableArray attributes, + ImmutableArray methods, + CSharpRenderingOptions renderingOptions) { throw new NotImplementedException(); } - public TestMethod GenerateTestMethod( + protected override CSharpTestMethod CreateTestMethod( + TestMethodGenerationContext context, + IdentifierString identifier, ScenarioInformation scenario, - CancellationToken cancellationToken = default) + ImmutableArray attributes, + ImmutableArray parameters, + ImmutableArray> scenarioParameters) + { + throw new NotImplementedException(); + } + + protected override ImmutableArray GenerateTestFixtureClassAttributes( + TestFixtureGenerationContext context, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected override ImmutableArray GenerateTestMethodAttributes( + TestMethodGenerationContext context, + CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs index 4f12b0f3c..919becdbb 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs @@ -7,18 +7,17 @@ namespace Reqnroll.FeatureSourceGenerator.NUnit; /// public class NUnitHandler : ITestFrameworkHandler { - public string FrameworkName => "NUnit"; + public string TestFrameworkName => "NUnit"; - public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => - compilationInformation is CSharpCompilationInformation; - - public ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation) + public ITestFixtureGenerator? GetTestFixtureGenerator() + where TCompilationInformation : CompilationInformation { - return compilation switch + if (typeof(TCompilationInformation).IsAssignableFrom(typeof(CSharpCompilationInformation))) { - CSharpCompilationInformation => new NUnitCSharpTestFixtureGenerator(), - _ => throw new NotSupportedException(), - }; + return (ITestFixtureGenerator)new NUnitCSharpTestFixtureGenerator(this); + } + + return null; } public bool IsTestFrameworkReferenced(CompilationInformation compilationInformation) diff --git a/Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs deleted file mode 100644 index 8f946efdf..000000000 --- a/Reqnroll.FeatureSourceGenerator/NamedTypeIdentifier.cs +++ /dev/null @@ -1,180 +0,0 @@ -namespace Reqnroll.FeatureSourceGenerator; - -public abstract class TypeIdentifier(bool isNullable) : IEquatable -{ - public bool IsNullable { get; } = isNullable; - - public virtual bool Equals(TypeIdentifier? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return IsNullable.Equals(other.IsNullable); - } - - public override bool Equals(object obj) => Equals(obj as TypeIdentifier); - - public override int GetHashCode() => IsNullable.GetHashCode(); - - public static bool Equals(TypeIdentifier? first, TypeIdentifier? second) - { - if (ReferenceEquals(first, second)) - { - return true; - } - - if (first is null) - { - return false; - } - - return first.Equals(second); - } - - public static bool operator ==(TypeIdentifier? first, TypeIdentifier? second) => Equals(first, second); - - public static bool operator !=(TypeIdentifier? first, TypeIdentifier? second) => !Equals(first, second); -} - -public class ArrayTypeIdentifier(TypeIdentifier itemType, bool isNullable = false) : - TypeIdentifier(isNullable), IEquatable -{ - public TypeIdentifier ItemType { get; } = itemType; - - public bool Equals(ArrayTypeIdentifier? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return base.Equals(other) && - ItemType.Equals(other.ItemType); - } - - public override bool Equals(object obj) => Equals(obj as ArrayTypeIdentifier); - - public override bool Equals(TypeIdentifier? other) => Equals(other as ArrayTypeIdentifier); - - public override int GetHashCode() - { - unchecked - { - var hash = base.GetHashCode(); - - hash *= ItemType.GetHashCode(); - - return hash; - } - } - - public override string ToString() => $"{ItemType}[]"; - - public static bool Equals(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) - { - if (ReferenceEquals(first, second)) - { - return true; - } - - if (first is null) - { - return false; - } - - return first.Equals(second); - } - - public static bool operator ==(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) => Equals(first, second); - - public static bool operator !=(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) => !Equals(first, second); -} - -public class NamedTypeIdentifier : TypeIdentifier, IEquatable -{ - public NamedTypeIdentifier(IdentifierString localName) : this(NamespaceString.Empty, localName) - { - } - - public NamedTypeIdentifier(NamespaceString ns, IdentifierString localName, bool isNullable = false) : base(isNullable) - { - if (localName.IsEmpty && !ns.IsEmpty) - { - throw new ArgumentException( - "An empty local name cannot be combined with a non-empty namespace.", - nameof(localName)); - } - - LocalName = localName; - Namespace = ns; - } - - public IdentifierString LocalName { get; } - - public NamespaceString Namespace { get; } - - public override string? ToString() => Namespace.IsEmpty ? LocalName.ToString() : $"{Namespace}.{LocalName}"; - - public override bool Equals(object obj) => Equals(obj as NamedTypeIdentifier); - - public override bool Equals(TypeIdentifier? other) => Equals(other as NamedTypeIdentifier); - - public bool Equals(NamedTypeIdentifier? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return base.Equals(other) && - Namespace.Equals(other.Namespace) && - LocalName.Equals(other.LocalName); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode *= 73812971 + LocalName.GetHashCode(); - hashCode *= 73812971 + Namespace.GetHashCode(); - return hashCode; - } - } - - public static bool Equals(NamedTypeIdentifier? first, NamedTypeIdentifier? second) - { - if (ReferenceEquals(first, second)) - { - return true; - } - - if (first is null) - { - return false; - } - - return first.Equals(second); - } - - public static bool operator ==(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => Equals(first, second); - - public static bool operator !=(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => !Equals(first, second); -} diff --git a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj index 1c146af78..339f72676 100644 --- a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj +++ b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj @@ -59,6 +59,10 @@ + + + + diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs new file mode 100644 index 000000000..a2c87e814 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs @@ -0,0 +1,60 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public class ArrayTypeIdentifier(TypeIdentifier itemType, bool isNullable = false) : + TypeIdentifier(isNullable), IEquatable +{ + public TypeIdentifier ItemType { get; } = itemType; + + public bool Equals(ArrayTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && + ItemType.Equals(other.ItemType); + } + + public override bool Equals(object obj) => Equals(obj as ArrayTypeIdentifier); + + public override bool Equals(TypeIdentifier? other) => Equals(other as ArrayTypeIdentifier); + + public override int GetHashCode() + { + unchecked + { + var hash = base.GetHashCode(); + + hash *= ItemType.GetHashCode(); + + return hash; + } + } + + public override string ToString() => $"{ItemType}[]"; + + public static bool Equals(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public static bool operator ==(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) => Equals(first, second); + + public static bool operator !=(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) => !Equals(first, second); +} diff --git a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs similarity index 98% rename from Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs index 22c0dcf5c..3fd83c489 100644 --- a/Reqnroll.FeatureSourceGenerator/AttributeDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs @@ -2,7 +2,7 @@ using System.Collections.Immutable; using System.ComponentModel; -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; /// /// Provides a description of a .NET attribute instance. @@ -18,7 +18,7 @@ public class AttributeDescriptor( { public NamedTypeIdentifier Type { get; } = type; - public ImmutableArray PositionalArguments { get; } = + public ImmutableArray PositionalArguments { get; } = ThrowIfArgumentTypesNotValid( positionalArguments.GetValueOrDefault(ImmutableArray.Empty), nameof(positionalArguments)); @@ -77,7 +77,7 @@ private static void ThrowIfArgumentTypeNotValid(object? value, string paramName) if (IsImmutableArrayType(valueType)) { var array = (IEnumerable)value; - foreach(var item in array) + foreach (var item in array) { ThrowIfArgumentTypeNotValid(item, paramName); } @@ -111,7 +111,7 @@ public override int GetHashCode() } private int CalculateHashCode() - { + { unchecked { var hash = 47; @@ -124,7 +124,7 @@ private int CalculateHashCode() hash *= 13 + index++ + GetItemHash(item); } - foreach(var (name, value) in NamedArguments) + foreach (var (name, value) in NamedArguments) { hash *= 13 + name.GetHashCode() + GetItemHash(value); } diff --git a/Reqnroll.FeatureSourceGenerator/CommonTypes.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/CommonTypes.cs similarity index 50% rename from Reqnroll.FeatureSourceGenerator/CommonTypes.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/CommonTypes.cs index fad2addc3..23b07359e 100644 --- a/Reqnroll.FeatureSourceGenerator/CommonTypes.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/CommonTypes.cs @@ -1,6 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; internal static class CommonTypes { - public static readonly NamedTypeIdentifier String = + public static readonly NamedTypeIdentifier String = new(new NamespaceString("System"), new IdentifierString("String")); } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/FeatureInformation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/FeatureInformation.cs new file mode 100644 index 000000000..8912041ef --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/FeatureInformation.cs @@ -0,0 +1,62 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class FeatureInformation( + string name, + string? description, + string language, + ImmutableArray tags = default, + string? filePath = default) : IEquatable +{ + public string Name { get; } = + string.IsNullOrEmpty(name) ? + throw new ArgumentException("Value cannot be null or an empty string.", nameof(name)) : + name; + + public string? Description { get; } = description; + + public string Language { get; } = + string.IsNullOrEmpty(language) ? + throw new ArgumentException("Value cannot be null or an empty string.", nameof(language)) : + language; + + public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; + + public string? FilePath { get; } = filePath; + + public override bool Equals(object obj) => Equals(obj as FeatureInformation); + + public bool Equals(FeatureInformation? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Name.Equals(other.Name) && + Language.Equals(other.Language) && + Tags.SetEquals(other.Tags) && + string.Equals(FilePath, other.FilePath, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 97533941; + + hash *= 37820603 + Name.GetHashCode(); + hash *= 37820603 + Language.GetHashCode(); + hash *= 37820603 + Tags.GetSetHashCode(); + hash *= 37820603 + FilePath == null ? 0 : StringComparer.Ordinal.GetHashCode(FilePath); + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/IHasAttributes.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/IHasAttributes.cs new file mode 100644 index 000000000..0bf9ec7c0 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/IHasAttributes.cs @@ -0,0 +1,14 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +/// +/// Defines a source element which has attributes. +/// +public interface IHasAttributes +{ + /// + /// Gets the attributes of the source element. + /// + ImmutableArray Attributes { get; } +} diff --git a/Reqnroll.FeatureSourceGenerator/IdentifierString.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/IdentifierString.cs similarity index 96% rename from Reqnroll.FeatureSourceGenerator/IdentifierString.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/IdentifierString.cs index 7dcd61037..b231386be 100644 --- a/Reqnroll.FeatureSourceGenerator/IdentifierString.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/IdentifierString.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; public readonly struct IdentifierString : IEquatable, IEquatable { @@ -115,10 +115,10 @@ public override bool Equals(object obj) public static bool operator !=(IdentifierString identifier, string? s) => !Equals(identifier, s); - public static bool operator ==(IdentifierString identifier1, IdentifierString identifier2) => + public static bool operator ==(IdentifierString identifier1, IdentifierString identifier2) => Equals(identifier1, identifier2); - public static bool operator !=(IdentifierString identifier1, IdentifierString identifier2) => + public static bool operator !=(IdentifierString identifier1, IdentifierString identifier2) => !Equals(identifier1, identifier2); public static implicit operator string(IdentifierString identifier) => identifier.ToString(); diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs new file mode 100644 index 000000000..8defffb56 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs @@ -0,0 +1,80 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator; + +public class NamedTypeIdentifier : TypeIdentifier, IEquatable +{ + public NamedTypeIdentifier(IdentifierString localName) : this(NamespaceString.Empty, localName) + { + } + + public NamedTypeIdentifier(NamespaceString ns, IdentifierString localName, bool isNullable = false) : base(isNullable) + { + if (localName.IsEmpty && !ns.IsEmpty) + { + throw new ArgumentException( + "An empty local name cannot be combined with a non-empty namespace.", + nameof(localName)); + } + + LocalName = localName; + Namespace = ns; + } + + public IdentifierString LocalName { get; } + + public NamespaceString Namespace { get; } + + public override string? ToString() => Namespace.IsEmpty ? LocalName.ToString() : $"{Namespace}.{LocalName}"; + + public override bool Equals(object obj) => Equals(obj as NamedTypeIdentifier); + + public override bool Equals(TypeIdentifier? other) => Equals(other as NamedTypeIdentifier); + + public bool Equals(NamedTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && + Namespace.Equals(other.Namespace) && + LocalName.Equals(other.LocalName); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode *= 73812971 + LocalName.GetHashCode(); + hashCode *= 73812971 + Namespace.GetHashCode(); + return hashCode; + } + } + + public static bool Equals(NamedTypeIdentifier? first, NamedTypeIdentifier? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public static bool operator ==(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => Equals(first, second); + + public static bool operator !=(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => !Equals(first, second); +} diff --git a/Reqnroll.FeatureSourceGenerator/NamespaceString.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/NamespaceString.cs similarity index 96% rename from Reqnroll.FeatureSourceGenerator/NamespaceString.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/NamespaceString.cs index 596953662..04661e0cd 100644 --- a/Reqnroll.FeatureSourceGenerator/NamespaceString.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/NamespaceString.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator; public readonly struct NamespaceString : IEquatable, IEquatable { diff --git a/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ParameterDescriptor.cs similarity index 89% rename from Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/ParameterDescriptor.cs index 95fba3e5f..86105b1d3 100644 --- a/Reqnroll.FeatureSourceGenerator/ParameterDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ParameterDescriptor.cs @@ -1,5 +1,5 @@ -namespace Reqnroll.FeatureSourceGenerator; -public class ParameterDescriptor: IEquatable +namespace Reqnroll.FeatureSourceGenerator.SourceModel; +public class ParameterDescriptor : IEquatable { public ParameterDescriptor(IdentifierString name, TypeIdentifier type) { diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/RuleInformation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/RuleInformation.cs new file mode 100644 index 000000000..360f9bf76 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/RuleInformation.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class RuleInformation(string name, ImmutableArray tags) : IEquatable +{ + public string Name { get; } = string.IsNullOrEmpty(name) ? + throw new ArgumentException("Value cannot be null or an empty string", nameof(name)) : + name; + + public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; + + public bool Equals(RuleInformation? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Name.Equals(other.Name) && Tags.SetEqual(other.Tags); + } + + public static bool Equals(RuleInformation? first, RuleInformation? second) + { + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public override bool Equals(object obj) => Equals(obj as RuleInformation); + + public override int GetHashCode() + { + unchecked + { + var hash = 14353993; + + hash *= 50733161 + Name.GetHashCode(); + hash *= 50733161 + Tags.GetSetHashCode(); + + return hash; + } + } + + public override string ToString() => $"Name={Name}"; +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioExampleSet.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioExampleSet.cs new file mode 100644 index 000000000..72ee943be --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioExampleSet.cs @@ -0,0 +1,84 @@ +using System.Collections; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class ScenarioExampleSet : IEnumerable>, IEquatable +{ + public ScenarioExampleSet( + ImmutableArray headings, + ImmutableArray> values, + ImmutableArray tags) + { + foreach (var set in values) + { + if (set.Length != headings.Length) + { + throw new ArgumentException( + "Values must contain sets with the same number of values as the headings.", + nameof(values)); + } + } + + Headings = headings; + Values = values; + Tags = tags; + } + + public ImmutableArray Headings { get; } + + public ImmutableArray> Values { get; } + + public ImmutableArray Tags { get; } + + public override bool Equals(object? obj) => Equals(obj as ScenarioExampleSet); + + public bool Equals(ScenarioExampleSet? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return (Headings.Equals(other.Headings) || Headings.SequenceEqual(other.Headings)) && + (Values.Equals(other.Values) || Values.SequenceEqual(other.Values, ImmutableArrayEqualityComparer.Default)) && + (Tags.Equals(other.Tags) || Tags.SequenceEqual(other.Tags)); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 43961407; + + hash *= 32360441 + Headings.GetSequenceHashCode(); + hash *= 32360441 + Values.GetSequenceHashCode(ImmutableArrayEqualityComparer.Default); + hash *= 32360441 + Tags.GetSequenceHashCode(); + + return hash; + } + } + + public IEnumerator> GetEnumerator() + { + foreach (var set in Values) + { + yield return GetAsRow(set); + } + } + + private IEnumerable<(string Name, string Value)> GetAsRow(ImmutableArray set) + { + for (var i = 0; i < Headings.Length; i++) + { + yield return (Name: Headings[i], Value: set[i]); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs similarity index 51% rename from Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs index e115909e4..a0d2bd4c2 100644 --- a/Reqnroll.FeatureSourceGenerator/ScenarioInformation.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs @@ -1,21 +1,20 @@ using System.Collections.Immutable; -namespace Reqnroll.FeatureSourceGenerator; - +namespace Reqnroll.FeatureSourceGenerator.SourceModel; public class ScenarioInformation( - FeatureInformation feature, string name, + int lineNumber, ImmutableArray tags, ImmutableArray steps, ImmutableArray examples = default, RuleInformation? rule = null) : IEquatable { - public FeatureInformation Feature { get; } = feature; - public string Name { get; } = string.IsNullOrEmpty(name) ? throw new ArgumentException("Value cannot be null or an empty string", nameof(name)) : name; + public int LineNumber { get; } = lineNumber; + public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; public ImmutableArray Steps { get; } = steps.IsDefault ? ImmutableArray.Empty : steps; @@ -38,65 +37,28 @@ public bool Equals(ScenarioInformation? other) return true; } - return Feature.Equals(other.Feature) && - Name.Equals(other.Name) && + return Name.Equals(other.Name) && Tags.SetEqual(other.Tags) && Steps.SequenceEqual(other.Steps) && Examples.SequenceEqual(other.Examples) && RuleInformation.Equals(Rule, other.Rule); } - public override int GetHashCode() - { - return base.GetHashCode(); - } - - public override string ToString() => $"Name={Name}"; -} - -public class RuleInformation(string name, ImmutableArray tags) : IEquatable -{ - public string Name { get; } = string.IsNullOrEmpty(name) ? - throw new ArgumentException("Value cannot be null or an empty string", nameof(name)) : - name; - - public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; - - public bool Equals(RuleInformation? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return Name.Equals(other.Name) && Tags.SetEqual(other.Tags); - } - - public static bool Equals(RuleInformation? first, RuleInformation? second) - { - if (first is null) - { - return false; - } - - return first.Equals(second); - } - - public override bool Equals(object obj) => Equals(obj as RuleInformation); - public override int GetHashCode() { unchecked { - var hash = 14353993; + var hash = 42261493; + + hash *= 95921717 + Name.GetHashCode(); + hash *= 95921717 + Tags.GetSetHashCode(); + hash *= 95921717 + Steps.GetSequenceHashCode(); + hash *= 95921717 + Examples.GetSequenceHashCode(); - hash *= 50733161 + Name.GetHashCode(); - hash *= 50733161 + Tags.GetSetHashCode(); + if (Rule != null) + { + hash *= 95921717 + Rule.GetHashCode(); + } return hash; } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs new file mode 100644 index 000000000..9f78b8a76 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs @@ -0,0 +1,5 @@ +using Gherkin; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public record ScenarioStep(StepKeywordType KeywordType, string Keyword, string Text, int LineNumber); diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs similarity index 78% rename from Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs index bbe5b8fca..851d73db0 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs @@ -1,11 +1,11 @@ using System.Collections.Immutable; -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; /// /// Represents a Reqnroll text fixture class. /// -public abstract class TestFixtureClass : IEquatable +public abstract class TestFixtureClass : IEquatable, IHasAttributes { /// /// Initializes a new instance of the test fixture class. @@ -14,10 +14,12 @@ public abstract class TestFixtureClass : IEquatable /// The hint name used to identify the test fixture within the compilation. This is usually /// a virtual path and virtual filename that makes sense within the context of a project. The value must be unique /// within the compilation. + /// The feature information that will be included in the test fixture. /// The attributes which are applied to the feature. public TestFixtureClass( NamedTypeIdentifier identifier, string hintName, + FeatureInformation featureInformation, ImmutableArray attributes = default) { Identifier = identifier ?? throw new ArgumentNullException(nameof(identifier)); @@ -28,6 +30,7 @@ public TestFixtureClass( } HintName = hintName; + FeatureInformation = featureInformation; Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; } @@ -47,6 +50,11 @@ public TestFixtureClass( /// public string HintName { get; } + /// + /// Gets the feature information that will be included in the test fixture. + /// + public FeatureInformation FeatureInformation { get; } + /// /// Gets the test methods which make up the fixture. /// @@ -68,7 +76,8 @@ public bool Equals(TestFixtureClass? other) return Identifier.Equals(other.Identifier) && (Attributes.Equals(other.Attributes) || Attributes.SetEquals(other.Attributes)) && - HintName.Equals(other.HintName, StringComparison.Ordinal); + HintName.Equals(other.HintName, StringComparison.Ordinal) && + FeatureInformation.Equals(other.FeatureInformation); } public override int GetHashCode() @@ -80,6 +89,7 @@ public override int GetHashCode() hash *= 87057149 + Identifier.GetHashCode(); hash *= 87057149 + Attributes.GetSetHashCode(); hash *= 87057149 + StringComparer.Ordinal.GetHashCode(HintName); + hash *= 87057149 + FeatureInformation.GetHashCode(); return hash; } @@ -88,6 +98,7 @@ public override int GetHashCode() /// /// Renders the configured test fixture to source text. /// + /// A token used to signal when rendering should be canceled. /// A representing the rendered test fixture. - public abstract SourceText Render(); + public abstract SourceText Render(CancellationToken cancellationToken = default); } diff --git a/Reqnroll.FeatureSourceGenerator/TestMethod.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs similarity index 68% rename from Reqnroll.FeatureSourceGenerator/TestMethod.cs rename to Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs index 0fafc5dcb..29f4b6515 100644 --- a/Reqnroll.FeatureSourceGenerator/TestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs @@ -1,25 +1,30 @@ using System.Collections.Immutable; -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; /// /// Represents a test method that executes a scenario. /// -public abstract class TestMethod : IEquatable +public abstract class TestMethod : IEquatable, IHasAttributes { /// /// Initializes a new instance of the class. /// /// The identifier of the method. + /// The scenario associated with the method. /// The attributes applied to the method. /// The parameters defined by the method. + /// A map of scenario parameters to the identifiers that will be used to supply + /// a value for each parameter. /// /// is an empty identifier string. /// public TestMethod( IdentifierString identifier, + ScenarioInformation scenario, ImmutableArray attributes = default, - ImmutableArray parameters = default) + ImmutableArray parameters = default, + ImmutableArray> scenarioParameters = default) { if (identifier.IsEmpty) { @@ -27,9 +32,10 @@ public TestMethod( } Identifier = identifier; - + Scenario = scenario; Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; Parameters = parameters.IsDefault ? ImmutableArray.Empty : parameters; + ParametersOfScenario = scenarioParameters.IsDefault ? ImmutableArray>.Empty : scenarioParameters; } /// @@ -47,6 +53,16 @@ public TestMethod( /// public ImmutableArray Parameters { get; } + /// + /// Gets the arguments of the scenario, as they map to parameters. + /// + public ImmutableArray> ParametersOfScenario { get; } + + /// + /// Gets information about the scenario associated with the test method. + /// + public ScenarioInformation Scenario { get; } + public override bool Equals(object obj) => Equals(obj as TestMethod); public override int GetHashCode() @@ -77,6 +93,7 @@ public bool Equals(TestMethod? other) return Identifier.Equals(other.Identifier) && (Attributes.Equals(other.Attributes) || Attributes.SetEquals(other.Attributes)) && - (Parameters.Equals(other.Parameters) || Parameters.SequenceEqual(other.Parameters)); + (Parameters.Equals(other.Parameters) || Parameters.SequenceEqual(other.Parameters)) && + ParametersOfScenario.Equals(other.ParametersOfScenario); } } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs new file mode 100644 index 000000000..d94112d58 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs @@ -0,0 +1,44 @@ +namespace Reqnroll.FeatureSourceGenerator; + +public abstract class TypeIdentifier(bool isNullable) : IEquatable +{ + public bool IsNullable { get; } = isNullable; + + public virtual bool Equals(TypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return IsNullable.Equals(other.IsNullable); + } + + public override bool Equals(object obj) => Equals(obj as TypeIdentifier); + + public override int GetHashCode() => IsNullable.GetHashCode(); + + public static bool Equals(TypeIdentifier? first, TypeIdentifier? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + + if (first is null) + { + return false; + } + + return first.Equals(second); + } + + public static bool operator ==(TypeIdentifier? first, TypeIdentifier? second) => Equals(first, second); + + public static bool operator !=(TypeIdentifier? first, TypeIdentifier? second) => !Equals(first, second); +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs index 895c1115c..b03866a38 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureComposition.cs @@ -1,8 +1,12 @@ using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator; -internal record TestFixtureComposition(FeatureInformation Feature, ImmutableArray Methods) +internal record TestFixtureComposition( + TestFixtureGenerationContext Context, + ImmutableArray Methods) + where TCompilationInformation : CompilationInformation { public override int GetHashCode() { @@ -10,21 +14,21 @@ public override int GetHashCode() { var hash = 49151149; - hash *= 983819 + Feature.GetHashCode(); + hash *= 983819 + Context.GetHashCode(); hash *= 983819 + Methods.GetSetHashCode(); return hash; } } - public virtual bool Equals(TestFixtureComposition? other) + public virtual bool Equals(TestFixtureComposition? other) { if (other is null) { return false; } - return Feature.Equals(other.Feature) && + return Context.Equals(other.Context) && (Methods.Equals(other.Methods) || Methods.SetEqual(other.Methods)); } } diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureGenerationContext.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureGenerationContext.cs new file mode 100644 index 000000000..0b91e1bc8 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureGenerationContext.cs @@ -0,0 +1,71 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator; + +public class TestFixtureGenerationContext( + FeatureInformation featureInformation, + ImmutableArray scenarioInformations, + string featureHintName, + NamespaceString testFixtureNamespace, + TCompilationInformation compilationInformation, + ITestFixtureGenerator testFixtureGenerator) + where TCompilationInformation : CompilationInformation +{ + public FeatureInformation FeatureInformation { get; } = featureInformation; + + public ImmutableArray ScenarioInformations { get; } = + scenarioInformations.IsDefault ? + ImmutableArray.Empty : + scenarioInformations; + + public string FeatureHintName { get; } = featureHintName; + + public NamespaceString TestFixtureNamespace { get; } = testFixtureNamespace; + + public TCompilationInformation CompilationInformation { get; } = compilationInformation; + + public ITestFrameworkHandler TestFrameworkHandler => TestFixtureGenerator.TestFrameworkHandler; + + public ITestFixtureGenerator TestFixtureGenerator { get; } = testFixtureGenerator; + + public bool Equals(TestFixtureGenerationContext? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(other, this)) + { + return true; + } + + return FeatureInformation.Equals(other.FeatureInformation) && + ScenarioInformations.SequenceEqual(other.ScenarioInformations) && + FeatureHintName.Equals(other.FeatureHintName, StringComparison.Ordinal) && + TestFixtureNamespace.Equals(other.TestFixtureNamespace) && + CompilationInformation.Equals(other.CompilationInformation) && + TestFixtureGenerator.Equals(other.TestFixtureGenerator); + } + + public override bool Equals(object obj) => Equals(obj as TestFixtureGenerationContext); + + public override int GetHashCode() + { + unchecked + { + var hash = 37970701; + + hash *= 99584197 + FeatureInformation.GetHashCode(); + hash *= 99584197 + ScenarioInformations.GetSequenceHashCode(); + hash *= 99584197 + FeatureHintName.GetHashCode(); + hash *= 99584197 + TestFixtureNamespace.GetHashCode(); + hash *= 99584197 + CompilationInformation.GetHashCode(); + hash *= 99584197 + TestFixtureGenerator.GetHashCode(); + + return hash; + } + } +} + diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 67e90ceae..a93305763 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -1,7 +1,8 @@ using Gherkin; using Gherkin.Ast; +using Microsoft.CodeAnalysis; using Reqnroll.FeatureSourceGenerator.Gherkin; -using System.Collections; +using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator; @@ -11,8 +12,9 @@ namespace Reqnroll.FeatureSourceGenerator; /// /// Defines the basis of a source-generator which processes Gherkin feature files into test fixtures. /// -public abstract class TestFixtureSourceGenerator( +public abstract class TestFixtureSourceGenerator( ImmutableArray testFrameworkHandlers) : IIncrementalGenerator + where TCompilationInformation : CompilationInformation { public static readonly DiagnosticDescriptor NoTestFrameworkFound = new( id: DiagnosticIds.NoTestFrameworkFound, @@ -50,15 +52,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var compilationInformation = context.CompilationProvider .Select((compilation, _) => GetCompilationInformation(compilation)); - // Find compatible test frameworks and choose a default based on referenced assemblies. - var testFrameworkInformation = compilationInformation + // Find compatible generator and choose a default based on referenced assemblies. + var generatorInformation = compilationInformation .Select((compilationInfo, cancellationToken) => { - var compatibleHandlers = _testFrameworkHandlers - .Where(handler => handler.CanGenerateForCompilation(compilationInfo)) + var compatibleGenerators = _testFrameworkHandlers + .Select(handler => handler.GetTestFixtureGenerator()) + .Where(generator => generator != null) + .Select(generator => generator!) .ToImmutableArray(); - if (!compatibleHandlers.Any()) + if (!compatibleGenerators.Any()) { // This condition should only be possible if Roslyn is compiling a language we have produced a generator for // without also including a compatible test framework handler; it should never occur in practice. @@ -66,30 +70,30 @@ public void Initialize(IncrementalGeneratorInitializationContext context) $"No test framework handlers are available which can generate code for the current compilation."); } - var availableHandlers = compatibleHandlers - .Where(handler => handler.IsTestFrameworkReferenced(compilationInfo)) + var useableGenerators = compatibleGenerators + .Where(generator => generator.TestFrameworkHandler.IsTestFrameworkReferenced(compilationInfo)) .ToImmutableArray(); - var defaultHandlers = availableHandlers.FirstOrDefault(); + var defaultGenerator = useableGenerators.FirstOrDefault(); - return new TestFrameworkInformation( - compatibleHandlers, - availableHandlers, - defaultHandlers); + return new GeneratorInformation( + compatibleGenerators, + useableGenerators, + defaultGenerator); }); // Obtain the options which will influence the generation of features and combine with all other information // to produce parsed syntax ready for translation into test fixtures. - var featureInformationOrErrors = featureFiles + var testFixtureGenerationContextsOrErrors = featureFiles .Combine(context.AnalyzerConfigOptionsProvider) .Combine(compilationInformation) - .Combine(testFrameworkInformation) - .SelectMany(static IEnumerable> (input, cancellationToken) => + .Combine(generatorInformation) + .SelectMany(static IEnumerable>> (input, cancellationToken) => { - var (((featureFile, optionsProvider), compilationInfo), testFrameworkInformation) = input; + var (((featureFile, optionsProvider), compilationInfo), generatorInformation) = input; + + var options = optionsProvider.GetOptions(featureFile); - var options = optionsProvider.GetOptions(featureFile); - var source = featureFile.GetText(cancellationToken); // If there is no source text, we can skip this file completely. @@ -98,52 +102,56 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return []; } - // Select the test framework from the following sources: + // Select the generator from the following sources: // 1. The reqnroll.target_test_framework value from .editorconfig // 2. The ReqnrollTargetTestFramework from the build system properties (MSBuild project files or command-line argument) // 3. The assemblies referenced by the compilation indicating the presence of a test framework. - ITestFrameworkHandler? testFramework; + ITestFixtureGenerator? generator; if (options.TryGetValue("reqnroll.target_test_framework", out var targetTestFrameworkIdentifier) || options.TryGetValue("build_property.ReqnrollTargetTestFramework", out targetTestFrameworkIdentifier)) { // Select the target framework from the option specified. - testFramework = testFrameworkInformation.CompatibleHandlers - .SingleOrDefault(handler => - string.Equals(handler.FrameworkName, targetTestFrameworkIdentifier, StringComparison.OrdinalIgnoreCase)); + generator = generatorInformation.CompatibleGenerators + .SingleOrDefault(generator => string.Equals( + generator.TestFrameworkHandler.TestFrameworkName, + targetTestFrameworkIdentifier, + StringComparison.OrdinalIgnoreCase)); - if (testFramework == null) + if (generator == null) { // The properties specified a test framework which is not recognised or not supported for this language. - var frameworkNames = testFrameworkInformation.CompatibleHandlers.Select(framework => framework.FrameworkName); + var frameworkNames = generatorInformation.CompatibleGenerators + .Select(generator => generator.TestFrameworkHandler.TestFrameworkName); var frameworks = string.Join(", ", frameworkNames); - return - [ + return + [ Diagnostic.Create( TestFrameworkNotSupported, Location.None, targetTestFrameworkIdentifier, - frameworks) + frameworks) ]; } } - else if (testFrameworkInformation.DefaultHandler != null) + else if (generatorInformation.DefaultGenerator != null) { // Use the default handler. - testFramework = testFrameworkInformation.DefaultHandler; + generator = generatorInformation.DefaultGenerator; } else { // Report that no suitable target test framework could be determined. - var frameworkNames = testFrameworkInformation.CompatibleHandlers.Select(framework => framework.FrameworkName); + var frameworkNames = generatorInformation.CompatibleGenerators + .Select(generator => generator.TestFrameworkHandler.TestFrameworkName); var frameworks = string.Join(", ", frameworkNames); - return - [ + return + [ Diagnostic.Create( NoTestFrameworkFound, Location.None, - frameworks) + frameworks) ]; } @@ -154,111 +162,127 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } var featureHintName = Path.GetFileNameWithoutExtension(featureFile.Path); - var featureNamespace = rootNamespace; + var testFixtureNamespace = rootNamespace; if (options.TryGetValue("build_metadata.AdditionalFiles.RelativeDir", out var relativeDir)) { - var featureNamespaceParts = relativeDir + var testFixtureNamespaceParts = relativeDir .Replace(Path.DirectorySeparatorChar, '.') .Replace(Path.AltDirectorySeparatorChar, '.') .Split('.') .Select(part => DotNetSyntax.CreateIdentifier(part)); - featureNamespace = string.Join(".", featureNamespaceParts); + testFixtureNamespace = string.Join(".", testFixtureNamespaceParts); featureHintName = relativeDir + featureHintName; } // Parse the feature file and output the result. - var parser = new GherkinSyntaxParser(); + var parser = new Parser { StopAtFirstError = false }; + + cancellationToken.ThrowIfCancellationRequested(); + + GherkinDocument document; - var featureSyntax = parser.Parse(source, featureFile.Path, cancellationToken); + try + { + // CONSIDER: Using a parser that doesn't throw exceptions for syntax errors. + document = parser.Parse(new SourceTokenScanner(source)); + } + catch (CompositeParserException ex) + { + var diagnostics = ex.Errors + .Select(error => GherkinSyntaxParser.CreateGherkinDiagnostic(error, source, featureFile.Path)); + + return [.. diagnostics]; + } + + var feature = document.Feature; + + var featureInformation = new FeatureInformation( + feature.Name, + feature.Description, + feature.Language, + feature.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(), + featureFile.Path); + + var scenarioInformations = feature.Children + .SelectMany(child => CreateScenarioInformations(child, cancellationToken)) + .ToImmutableArray(); return [ - new FeatureInformation( - FeatureSyntax: featureSyntax, - FeatureHintName: featureHintName, - FeatureNamespace: featureNamespace, - CompilationInformation: compilationInfo, - TestFrameworkHandler: testFramework, - TestFixtureGenerator: testFramework.GetTestFixtureGenerator(compilationInfo)) + new TestFixtureGenerationContext( + featureInformation, + scenarioInformations, + featureHintName, + new NamespaceString(testFixtureNamespace), + compilationInfo, + generator) ]; }); + - // Filter features and errors. - var features = featureInformationOrErrors + // Filter contexts and errors. + var testFixtureGenerationContexts = testFixtureGenerationContextsOrErrors .Where(result => result.IsSuccess) - .Select((result, _) => (FeatureInformation)result); - var errors = featureInformationOrErrors + .Select((result, _) => (TestFixtureGenerationContext)result); + var errors = testFixtureGenerationContextsOrErrors .Where(result => !result.IsSuccess) .Select((result, _) => (Diagnostic)result); - // Generate scenario information from features. - var scenarios = features - .SelectMany(static (feature, cancellationToken) => - feature.FeatureSyntax.GetRoot().Feature.Children.SelectMany(child => - GetScenarioInformation(child, feature, cancellationToken))); + // Set up test method generation contexts from feature generation contexts. + var testMethodGenerationContexts = testFixtureGenerationContexts + .SelectMany(static (context, cancellationToken) => + context.ScenarioInformations.Select(scenario => + new TestMethodGenerationContext(scenario, context))); - // Generate scenario methods for each scenario. - var methods = scenarios - .Select(static (scenario, cancellationToken) => - (Method: scenario.Feature.TestFixtureGenerator.GenerateTestMethod(scenario, cancellationToken), - Scenario: scenario)); + // Generate test methods for each scenario. + var methods = testMethodGenerationContexts + .Select(static (context, cancellationToken) => + (Method: context.TestFixtureGenerator.GenerateTestMethod(context, cancellationToken), + Context: context)); // Generate test fixtures for each feature. var fixtures = methods.Collect() - .WithComparer(ImmutableArrayEqualityComparer<(TestMethod Method, ScenarioInformation Scenario)>.Default) + .WithComparer(ImmutableArrayEqualityComparer<(TestMethod Method, TestMethodGenerationContext Context)>.Default) .SelectMany(static (methods, cancellationToken) => methods - .GroupBy(item => item.Scenario.Feature, item => item.Method) - .Select(group => new TestFixtureComposition(group.Key, group.ToImmutableArray()))) + .GroupBy(item => item.Context.TestFixtureGenerationContext, item => item.Method) + .Select(group => new TestFixtureComposition(group.Key, group.ToImmutableArray()))) .Select(static (composition, cancellationToken) => - composition.Feature.TestFixtureGenerator.GenerateTestFixture( - composition.Feature, + composition.Context.TestFixtureGenerator.GenerateTestFixtureClass( + composition.Context, composition.Methods, cancellationToken)); // Emit errors. context.RegisterSourceOutput(errors, static (context, error) => context.ReportDiagnostic(error)); - // Emit parsing diagnostics. - context.RegisterSourceOutput(features, static (context, feature) => - { - foreach (var diagnostic in feature.FeatureSyntax.GetDiagnostics()) - { - context.ReportDiagnostic(diagnostic); - } - }); - // Emit source files for fixtures. context.RegisterSourceOutput( fixtures, - static (context, fixture) => context.AddSource(fixture.HintName, fixture.Render())); + static (context, fixture) => context.AddSource(fixture.HintName, fixture.Render(context.CancellationToken))); } - private static IEnumerable GetScenarioInformation( + private static IEnumerable CreateScenarioInformations( IHasLocation child, - FeatureInformation feature, CancellationToken cancellationToken) { return child switch { - Scenario scenario => [ GetScenarioInformation(scenario, feature, cancellationToken) ], - Rule rule => GetScenarioInformation(rule, feature, cancellationToken), + Scenario scenario => [CreateScenarioInformation(scenario, cancellationToken)], + Rule rule => CreateScenarioInformations(rule, cancellationToken), _ => [] }; } - private static ScenarioInformation GetScenarioInformation( + private static ScenarioInformation CreateScenarioInformation( Scenario scenario, - FeatureInformation feature, - CancellationToken cancellationToken) => - GetScenarioInformation(scenario, null, feature, cancellationToken); + CancellationToken cancellationToken) => CreateScenarioInformation(scenario, null, cancellationToken); - private static ScenarioInformation GetScenarioInformation( + private static ScenarioInformation CreateScenarioInformation( Scenario scenario, RuleInformation? rule, - FeatureInformation feature, CancellationToken cancellationToken) { var exampleSets = new List(); @@ -270,7 +294,7 @@ private static ScenarioInformation GetScenarioInformation( var examples = new ScenarioExampleSet( example.TableHeader.Cells.Select(cell => cell.Value).ToImmutableArray(), example.TableBody.Select(row => row.Cells.Select(cell => cell.Value).ToImmutableArray()).ToImmutableArray(), - example.Tags.Select(tag => tag.Name).ToImmutableArray()); + example.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray()); exampleSets.Add(examples); } @@ -291,20 +315,19 @@ private static ScenarioInformation GetScenarioInformation( } return new ScenarioInformation( - feature, scenario.Name, - scenario.Tags.Select(tag => tag.Name).ToImmutableArray(), + scenario.Location.Line, + scenario.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(), steps.ToImmutableArray(), exampleSets.ToImmutableArray(), rule); } - private static IEnumerable GetScenarioInformation( + private static IEnumerable CreateScenarioInformations( Rule rule, - FeatureInformation feature, CancellationToken cancellationToken) { - var tags = rule.Tags.Select(tag => tag.Name).ToImmutableArray(); + var tags = rule.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(); foreach (var child in rule.Children) { @@ -313,93 +336,14 @@ private static IEnumerable GetScenarioInformation( switch (child) { case Scenario scenario: - yield return GetScenarioInformation(scenario, new RuleInformation(rule.Name, tags), feature, cancellationToken); + yield return CreateScenarioInformation( + scenario, + new RuleInformation(rule.Name, tags), + cancellationToken); break; } } } - protected abstract CompilationInformation GetCompilationInformation(Compilation compilation); -} - -public record ScenarioStep(StepKeywordType KeywordType, string Keyword, string Text, int LineNumber); - -public class ScenarioExampleSet : IEnumerable>, IEquatable -{ - public ScenarioExampleSet( - ImmutableArray headings, - ImmutableArray> values, - ImmutableArray tags) - { - foreach (var set in values) - { - if (set.Length != headings.Length) - { - throw new ArgumentException( - "Values must contain sets with the same number of values as the headings.", - nameof(values)); - } - } - - Headings = headings; - Values = values; - Tags = tags; - } - - public ImmutableArray Headings { get; } - - public ImmutableArray> Values { get; } - - public ImmutableArray Tags { get; } - - public override bool Equals(object? obj) => Equals(obj as ScenarioExampleSet); - - public bool Equals(ScenarioExampleSet? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return (Headings.Equals(other.Headings) || Headings.SequenceEqual(other.Headings)) && - (Values.Equals(other.Values) || Values.SequenceEqual(other.Values, ImmutableArrayEqualityComparer.Default)) && - (Tags.Equals(other.Tags) || Tags.SequenceEqual(other.Tags)); - } - - public override int GetHashCode() - { - unchecked - { - var hash = 43961407; - - hash *= 32360441 + Headings.GetSequenceHashCode(); - hash *= 32360441 + Values.GetSequenceHashCode(ImmutableArrayEqualityComparer.Default); - hash *= 32360441 + Tags.GetSequenceHashCode(); - - return hash; - } - } - - public IEnumerator> GetEnumerator() - { - foreach (var set in Values) - { - yield return GetAsRow(set); - } - } - - private IEnumerable<(string Name, string Value)> GetAsRow(ImmutableArray set) - { - for (var i = 0; i < Headings.Length; i++) - { - yield return (Name: Headings[i], Value: set[i]); - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + protected abstract TCompilationInformation GetCompilationInformation(Compilation compilation); } diff --git a/Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs b/Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs deleted file mode 100644 index 5c49ee248..000000000 --- a/Reqnroll.FeatureSourceGenerator/TestFrameworkInformation.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Immutable; - -namespace Reqnroll.FeatureSourceGenerator; - -internal record TestFrameworkInformation( - ImmutableArray CompatibleHandlers, - ImmutableArray ReferencedHandlers, - ITestFrameworkHandler? DefaultHandler) -{ - public override int GetHashCode() - { - unchecked - { - var hash = 47; - - hash *= 13 + CompatibleHandlers.GetSetHashCode(); - hash *= 13 + ReferencedHandlers.GetSetHashCode(); - hash *= 13 + DefaultHandler?.GetHashCode() ?? 0; - - return hash; - } - } -} diff --git a/Reqnroll.FeatureSourceGenerator/TestMethodGenerationContext.cs b/Reqnroll.FeatureSourceGenerator/TestMethodGenerationContext.cs new file mode 100644 index 000000000..207c26dad --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestMethodGenerationContext.cs @@ -0,0 +1,49 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator; + +public class TestMethodGenerationContext( + ScenarioInformation scenarioInformation, + TestFixtureGenerationContext testFixtureGenerationContext) : + IEquatable?> + where TCompilationInformation : CompilationInformation +{ + public TestFixtureGenerationContext TestFixtureGenerationContext { get; } = testFixtureGenerationContext; + + public ScenarioInformation ScenarioInformation { get; } = scenarioInformation; + + public ITestFixtureGenerator TestFixtureGenerator => TestFixtureGenerationContext.TestFixtureGenerator; + + public FeatureInformation FeatureInformation => TestFixtureGenerationContext.FeatureInformation; + + public override bool Equals(object obj) => Equals(obj as TestMethodGenerationContext); + + public bool Equals(TestMethodGenerationContext? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return TestFixtureGenerationContext.Equals(other.TestFixtureGenerationContext) && + ScenarioInformation.Equals(other.ScenarioInformation); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 14353993; + + hash *= 50733161 + TestFixtureGenerationContext.GetHashCode(); + hash *= 50733161 + ScenarioInformation.GetHashCode(); + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs index aa419ebb8..15b5ca2bb 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs @@ -1,73 +1,74 @@ -using Gherkin.Ast; -using Reqnroll.FeatureSourceGenerator.CSharp; +//using Gherkin.Ast; +//using Reqnroll.FeatureSourceGenerator.CSharp; +//using Reqnroll.FeatureSourceGenerator.SourceModel; -namespace Reqnroll.FeatureSourceGenerator.XUnit; -public class XUnitCSharpSyntaxGeneration(FeatureInformation featureInfo) : CSharpTestFixtureGeneration(featureInfo) -{ - private readonly NamespaceString XUnitNamespace = new("Xunit"); +//namespace Reqnroll.FeatureSourceGenerator.XUnit; +//public class XUnitCSharpSyntaxGeneration(TestFixtureGenerationContext featureInfo) : CSharpTestFixtureGeneration(featureInfo) +//{ +// private readonly NamespaceString XUnitNamespace = new("Xunit"); - protected override IEnumerable GetTestMethodAttributes(Scenario scenario) - { - var attributes = new List - { - new(new NamedTypeIdentifier(XUnitNamespace, new IdentifierString("Fact"))) - }; +// protected override IEnumerable GetTestMethodAttributes(Scenario scenario) +// { +// var attributes = new List +// { +// new(new NamedTypeIdentifier(XUnitNamespace, new IdentifierString("Fact"))) +// }; - return base.GetTestMethodAttributes(scenario).Concat(attributes); - } +// return base.GetTestMethodAttributes(scenario).Concat(attributes); +// } - protected override IEnumerable GetInterfaces() => - base.GetInterfaces().Concat([ $"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>" ]); +// protected override IEnumerable GetInterfaces() => +// base.GetInterfaces().Concat([ $"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>" ]); - protected override void AppendTestFixturePreamble() - { - AppendLifetimeClass(); +// protected override void AppendTestFixturePreamble() +// { +// AppendLifetimeClass(); - AppendConstructor(); +// AppendConstructor(); - base.AppendTestFixturePreamble(); - } +// base.AppendTestFixturePreamble(); +// } - protected virtual void AppendConstructor() - { - // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. - SourceBuilder.AppendLine($"public {GetClassName()}(Lifetime lifetime)"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("Lifetime = lifetime;"); - SourceBuilder.EndBlock("}"); - } +// protected virtual void AppendConstructor() +// { +// // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. +// SourceBuilder.AppendLine($"public {GetClassName()}(Lifetime lifetime)"); +// SourceBuilder.BeginBlock("{"); +// SourceBuilder.AppendLine("Lifetime = lifetime;"); +// SourceBuilder.EndBlock("}"); +// } - protected virtual void AppendLifetimeClass() - { - // This class represents the feature lifetime in the xUnit framework. - SourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); - SourceBuilder.BeginBlock("{"); +// protected virtual void AppendLifetimeClass() +// { +// // This class represents the feature lifetime in the xUnit framework. +// SourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); +// SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); +// SourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); - SourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); - SourceBuilder.BeginBlock("{"); - // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. - SourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); - SourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); - SourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); - SourceBuilder.EndBlock("}"); +// SourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); +// SourceBuilder.BeginBlock("{"); +// // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. +// SourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); +// SourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); +// SourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); +// SourceBuilder.EndBlock("}"); - SourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); - SourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); - SourceBuilder.BeginBlock("TestRunner = null;"); - SourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); - SourceBuilder.EndBlock("}"); +// SourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); +// SourceBuilder.BeginBlock("{"); +// SourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); +// SourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); +// SourceBuilder.BeginBlock("TestRunner = null;"); +// SourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); +// SourceBuilder.EndBlock("}"); - SourceBuilder.EndBlock("}"); - SourceBuilder.AppendLine(); - } +// SourceBuilder.EndBlock("}"); +// SourceBuilder.AppendLine(); +// } - protected override void AppendTestRunnerLookupForScenario(Scenario scenario) - { - // For xUnit test runners are scoped to the whole feature execution lifetime - SourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner;"); - } -} +// protected override void AppendTestRunnerLookupForScenario(Scenario scenario) +// { +// // For xUnit test runners are scoped to the whole feature execution lifetime +// SourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner;"); +// } +//} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs index b99550d75..d057b384b 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -1,19 +1,45 @@  +using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + namespace Reqnroll.FeatureSourceGenerator.XUnit; -internal class XUnitCSharpTestFixtureGenerator : ITestFixtureGenerator +internal class XUnitCSharpTestFixtureGenerator(XUnitHandler testFrameworkHandler) : + CSharpTestFixtureGenerator(testFrameworkHandler) { - public TestFixtureClass GenerateTestFixture( - FeatureInformation feature, - IEnumerable methods, - CancellationToken cancellationToken = default) + protected override CSharpTestFixtureClass CreateTestFixtureClass( + TestFixtureGenerationContext context, + NamedTypeIdentifier identifier, + FeatureInformation feature, + ImmutableArray attributes, + ImmutableArray methods, + CSharpRenderingOptions renderingOptions) + { + throw new NotImplementedException(); + } + + protected override CSharpTestMethod CreateTestMethod( + TestMethodGenerationContext context, + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray attributes, + ImmutableArray parameters, + ImmutableArray> scenarioParameters) + { + throw new NotImplementedException(); + } + + protected override ImmutableArray GenerateTestFixtureClassAttributes( + TestFixtureGenerationContext context, + CancellationToken cancellationToken) { throw new NotImplementedException(); } - public TestMethod GenerateTestMethod( - ScenarioInformation scenario, - CancellationToken cancellationToken = default) + protected override ImmutableArray GenerateTestMethodAttributes( + TestMethodGenerationContext context, + CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs index 4cdf6bd29..85eadffb8 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -7,18 +7,17 @@ namespace Reqnroll.FeatureSourceGenerator.XUnit; /// public class XUnitHandler : ITestFrameworkHandler { - public string FrameworkName => "xUnit"; + public string TestFrameworkName => "xUnit"; - public bool CanGenerateForCompilation(CompilationInformation compilationInformation) => - compilationInformation is CSharpCompilationInformation; - - public ITestFixtureGenerator GetTestFixtureGenerator(CompilationInformation compilation) + public ITestFixtureGenerator? GetTestFixtureGenerator() + where TCompilationInformation : CompilationInformation { - return compilation switch + if (typeof(TCompilationInformation).IsAssignableFrom(typeof(CSharpCompilationInformation))) { - CSharpCompilationInformation => new XUnitCSharpTestFixtureGenerator(), - _ => throw new NotSupportedException(), - }; + return (ITestFixtureGenerator)new XUnitCSharpTestFixtureGenerator(this); + } + + return null; } public bool IsTestFrameworkReferenced(CompilationInformation compilationInformation) diff --git a/Reqnroll.sln b/Reqnroll.sln index 521df415a..f51690ed6 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -116,6 +116,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{ reqnroll.ico = reqnroll.ico EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnroll.FeatureSourceGeneratorTests", "Tests\Reqnroll.FeatureSourceGeneratorTests\Reqnroll.FeatureSourceGeneratorTests.csproj", "{A470519A-EC44-40A9-9D1D-9667C1049F9D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Reqnroll.FeatureSourceGenerator", "Reqnroll.FeatureSourceGenerator\Reqnroll.FeatureSourceGenerator.csproj", "{33DBD7B5-00CB-4463-9204-1E3B73E5B39D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -242,6 +246,14 @@ Global {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CBEB31D-5F98-460F-B193-578AEEA78CF9}.Release|Any CPU.Build.0 = Release|Any CPU + {A470519A-EC44-40A9-9D1D-9667C1049F9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A470519A-EC44-40A9-9D1D-9667C1049F9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A470519A-EC44-40A9-9D1D-9667C1049F9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A470519A-EC44-40A9-9D1D-9667C1049F9D}.Release|Any CPU.Build.0 = Release|Any CPU + {33DBD7B5-00CB-4463-9204-1E3B73E5B39D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33DBD7B5-00CB-4463-9204-1E3B73E5B39D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33DBD7B5-00CB-4463-9204-1E3B73E5B39D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33DBD7B5-00CB-4463-9204-1E3B73E5B39D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -280,6 +292,7 @@ Global {C658B37D-FD36-4868-9070-4EB452FAE526} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} {6CBEB31D-5F98-460F-B193-578AEEA78CF9} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} {53475D78-4500-4399-9539-0D5403C13C7A} = {577A0375-1436-446C-802B-3C75C8CEF94F} + {A470519A-EC44-40A9-9D1D-9667C1049F9D} = {0359B7D7-7E29-48E9-8DF9-7D1FACFA5CFA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs index 61b8b8afa..8af999d13 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AssertionExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Diagnostics.Contracts; namespace Reqnroll.FeatureSourceGenerator; @@ -38,8 +39,16 @@ public static CSharpMethodDeclarationAssertions Should(this MethodDeclarationSyn new(actualValue); /// - /// Returns an object that can be used to assert the - /// current . + /// Returns an object that can be used to assert the + /// subject with attributes. + /// + [Pure] + public static AttributeAssertions Should(this IHasAttributes? actualValue) => + new(actualValue); + + /// + /// Returns a object that can be used to assert the + /// subject with attributes. /// [Pure] public static TestMethodAssertions Should(this TestMethod? actualValue) => diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeAssertions.cs new file mode 100644 index 000000000..d57146e5b --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeAssertions.cs @@ -0,0 +1,233 @@ +using FluentAssertions.Execution; +using FluentAssertions.Primitives; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator; +public class AttributeAssertions(IHasAttributes? subject) : + AttributeAssertions(subject) +{ +} + +public class AttributeAssertions(TSubject subject) : + ReferenceTypeAssertions(subject) + where TSubject : IHasAttributes? + where TAssertions : AttributeAssertions +{ + protected override string Identifier => "subject"; + + ///// + ///// Expects the class declaration have only a single attribute with a specific identifier. + ///// + ///// + ///// The declared type of the attribute. + ///// + ///// + ///// A formatted phrase as is supported by explaining why the assertion + ///// is needed. If the phrase does not start with the word because, it is prepended automatically. + ///// + ///// + ///// Zero or more objects to format using the placeholders in . + ///// + //public AndWhichConstraint HaveSingleAttribute( + // string type, + // string because = "", + // params object[] becauseArgs) + //{ + // var expectation = "Expected {context:subject} to have a single attribute " + + // $"which is of type \"{type}\"{{reason}}"; + + // bool notNull = Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .ForCondition(Subject is not null) + // .FailWith(expectation + ", but found ."); + + // AttributeSyntax? match = default; + + // if (notNull) + // { + // var attributes = Subject!.Attributes; + + // switch (attributes.Length) + // { + // case 0: // Fail, Collection is empty + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but the class has no attributes."); + + // break; + // case 1: // Success Condition + // var single = attributes.Single(); + + // if (single.Name.ToString() != type) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but found the attribute \"{0}\".", single.Name); + // } + // else + // { + // match = single; + // } + + // break; + // default: // Fail, Collection contains more than a single item + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but found {0}.", attributes); + + // break; + // } + // } + + // return new AndWhichConstraint((TAssertions)this, match!); + //} + + //public AndWhichConstraint HaveAttribute( + // string type, + // string because = "", + // params object[] becauseArgs) + //{ + // var expectation = "Expected {context:subject} to have an attribute " + + // $"of type \"{type}\"{{reason}}"; + + // bool notNull = Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .ForCondition(Subject is not null) + // .FailWith(expectation + ", but found ."); + + // AttributeSyntax? match = default; + + // if (notNull) + // { + // var attributes = Subject!.AttributeLists.SelectMany(list => list.Attributes).ToList(); + + // if (attributes.Count == 0) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but the subject has no attributes."); + // } + // else + // { + // match = attributes.FirstOrDefault(attribute => attribute.Name.ToString() == type); + + // if (match == null) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .FailWith(expectation + ", but found {0}.", attributes); + // } + // } + // } + + // return new AndWhichConstraint((TAssertions)this, match!); + //} + + public AndConstraint HaveAttribuesEquivalentTo( + AttributeDescriptor[] expectation, + string because = "", + params object[] becauseArgs) => + HaveAttribuesEquivalentTo((IEnumerable)expectation, because, becauseArgs); + + public AndConstraint HaveAttribuesEquivalentTo( + IEnumerable expectation, + string because = "", + params object[] becauseArgs) + { + using var scope = new AssertionScope(); + + scope.FormattingOptions.UseLineBreaks = true; + + var assertion = Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:the subject} to have attributes equivalent to {0}", expectation) + .ForCondition(Subject is not null) + .FailWith("but {context:the subject} is ."); + + var expected = expectation.ToList(); + var actual = Subject!.Attributes; + + + var missing = expected.ToList(); + var extra = actual.ToList(); + + foreach (var item in expected) + { + if (extra.Remove(item)) + { + missing.Remove(item); + } + } + + if (missing.Count > 0) + { + if (extra.Count > 0) + { + assertion + .Then + .FailWith("but {context:the subject} is missing attributes {0} and has extra attributes {1}", missing, extra); + } + else + { + assertion + .Then + .FailWith("but {context:the subject} is missing attributes {0}", missing); + } + } + else if (extra.Count > 0) + { + assertion + .Then + .FailWith("but {context:the subject} has extra attributes {0}", extra); + } + + return new AndConstraint((TAssertions)this); + } + + //public AndConstraint HaveParametersEquivalentTo( + // IEnumerable expectation, + // string because = "", + // params object[] becauseArgs) + //{ + // using var scope = new AssertionScope(); + + // scope.FormattingOptions.UseLineBreaks = true; + + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .WithExpectation("Expected {context:the subject} to have parameters equivalent to {0} {reason}", expectation) + // .ForCondition(Subject is not null) + // .FailWith("but {context:the subject} is ."); + + // var expected = expectation.ToList(); + // var actual = Subject!.ParameterList.Parameters; + + // for (var i = 0; i < expected.Count; i++) + // { + // var expectedItem = expected[i]; + + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .WithExpectation("Expected {context:the subject} to have parameter {0}: \"{1}\" {reason}", i+1, expectedItem) + // .ForCondition(Subject.ParameterList.Parameters.Count != 0) + // .FailWith("but {context:the subject} has no parameters defined.", Subject.ParameterList.Parameters.Count) + // .Then + // .ForCondition(Subject.ParameterList.Parameters.Count > i) + // .FailWith("but {context:the subject} only has {0} parameters defined.", Subject.ParameterList.Parameters.Count) + // .Then + // .Given(() => Subject.ParameterList.Parameters[i]) + // .ForCondition(actual => false)// actual.IsEquivalentTo(expectedItem, true)) + // .FailWith("but found \"{0}\".", expectedItem); + // } + + // if (expected.Count < actual.Count) + // { + // Execute.Assertion + // .BecauseOf(because, becauseArgs) + // .WithExpectation("Expected {context:the subject} to have parameters equivalent to {0}{reason}", expectation) + // .FailWith("but {context:the subject} has extra parameters {0}.", actual.Skip(expected.Count)); + // } + + // return new AndConstraint((TAssertions)this); + //} +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs index fea3862f1..871cbd352 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorFormatter.cs @@ -1,4 +1,5 @@ using FluentAssertions.Formatting; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator; diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs index adeb1a377..6e09a12c9 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs @@ -1,4 +1,5 @@ using FluentAssertions.Execution; +using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator; diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs index b5b52f264..be7e35097 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Drawing; diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs index 669043168..75273aee9 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -1,7 +1,7 @@ using Gherkin; using Microsoft.CodeAnalysis; using Reqnroll.FeatureSourceGenerator.CSharp; -using Reqnroll.FeatureSourceGenerator.Gherkin; +using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.MSTest; @@ -20,7 +20,7 @@ public MSTestCSharpTestFixtureGeneratorTests() true); TestHandler = new MSTestHandler(); - Generator = TestHandler.GetTestFixtureGenerator(Compilation); + Generator = TestHandler.GetTestFixtureGenerator()!; } private static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); @@ -29,39 +29,69 @@ public MSTestCSharpTestFixtureGeneratorTests() protected MSTestHandler TestHandler { get; } - protected ITestFixtureGenerator Generator { get; } + protected ITestFixtureGenerator Generator { get; } [Fact] - public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamples() + public void GenerateTestFixture_CreatesClassForFeatureWithMsTestAttributes() { - const string featureText = - """ - #language: en - @featureTag1 - Feature: Sample - - Scenario: Sample Scenario - When foo happens - """; + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); - var document = new Parser().Parse(new StringReader(featureText)); - var featureSyntax = new GherkinSyntaxTree(document, [], "Sample.feature"); + var scenarioInfo = new ScenarioInformation( + "Sample Scenario", + 22, + [], + [new ScenarioStep(StepKeywordType.Action, "When", "foo happens", 6)]); - var featureInfo = new FeatureInformation( - featureSyntax, + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [ scenarioInfo ], "Sample.feature", - "Reqnroll.Tests", + new NamespaceString("Reqnroll.Tests"), Compilation, - TestHandler, Generator); + var testFixture = Generator.GenerateTestFixtureClass(testFixtureGenerationContext, []); + + testFixture.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))), + ]); + } + + [Fact] + public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamples() + { + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + var scenarioInfo = new ScenarioInformation( - featureInfo, "Sample Scenario", + 22, [], [new ScenarioStep(StepKeywordType.Action, "When", "foo happens", 6)]); - var method = Generator.GenerateTestMethod(scenarioInfo); + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [ scenarioInfo ], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testMethodGenerationContext = new TestMethodGenerationContext( + scenarioInfo, + testFixtureGenerationContext); + + var method = Generator.GenerateTestMethod(testMethodGenerationContext); method.Should().HaveAttribuesEquivalentTo( [ @@ -71,55 +101,45 @@ When foo happens ["Sample Scenario"]), new AttributeDescriptor( new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), - namedArguments: new Dictionary{ { "FeatureTitle", "Sample" } }.ToImmutableDictionary()) + positionalArguments: ["FeatureTitle", "Sample"]) ]); method.Should().HaveNoParameters(); } - [Fact] + [Fact] public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScenarioHasExamples() { - const string featureText = - """ - #language: en - @featureTag1 - Feature: Sample - - Scenario Outline: Sample Scenario Outline - When happens - @example_tag - Examples: - | what | - | foo | - | bar | - Examples: Second example without tags - in this case the tag list is null. - | what | - | baz | - """; - - var document = new Parser().Parse(new StringReader(featureText)); - var featureSyntax = new GherkinSyntaxTree(document, [], "Sample.feature"); - - var featureInfo = new FeatureInformation( - featureSyntax, - "Sample.feature", - "Reqnroll.Tests", - Compilation, - TestHandler, - Generator); - var exampleSet1 = new ScenarioExampleSet(["what"], [["foo"], ["bar"]], ["example_tag"]); var exampleSet2 = new ScenarioExampleSet(["what"], [["baz"]], []); var scenarioInfo = new ScenarioInformation( - featureInfo, "Sample Scenario Outline", + 22, [], [ new ScenarioStep(StepKeywordType.Action, "When", " happens", 6) ], [ exampleSet1, exampleSet2 ]); - var method = Generator.GenerateTestMethod(scenarioInfo); + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [scenarioInfo], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testMethodGenerationContext = new TestMethodGenerationContext( + scenarioInfo, + testFixtureGenerationContext); + + var method = Generator.GenerateTestMethod(testMethodGenerationContext); method.Should().HaveAttribuesEquivalentTo( [ diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs index 668e58b29..59e4f7a43 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs @@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Reqnroll.FeatureSourceGenerator; using Reqnroll.FeatureSourceGenerator.CSharp; -using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.SourceModel; using Xunit.Abstractions; namespace Reqnroll.FeatureSourceGenerator; @@ -28,7 +28,7 @@ public void GeneratorProducesMSTestOutputWhenWhenBuildPropertyConfiguredForMSTes Feature: Calculator @mytag - Scenario: Add two numbers + Scenario: Add two "simple" numbers Given the first number is 50 And the second number is 70 When the two numbers are added diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/IdentifierStringTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/IdentifierStringTests.cs similarity index 93% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/IdentifierStringTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/IdentifierStringTests.cs index 29d0c16f4..66ef70a33 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/IdentifierStringTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/IdentifierStringTests.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator; +using Reqnroll.FeatureSourceGenerator; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; public class IdentifierStringTests { @@ -103,11 +105,11 @@ public void EqualityOperatorWithString_ReturnsEquivalenceWithCaseSensitivity(str } [Theory] - [InlineData("Parser", "Parser", true)] - [InlineData("_Parser" , "_Parser", true)] - [InlineData("_Parser","_parser", false)] - [InlineData("Parser", "parser", false)] - [InlineData("Parser", "_Parser", false)] + [InlineData("Parser", "Parser", true)] + [InlineData("_Parser", "_Parser", true)] + [InlineData("_Parser", "_parser", false)] + [InlineData("Parser", "parser", false)] + [InlineData("Parser", "_Parser", false)] public void EqualityOperatorWithIdentifierString_ReturnsEquivalenceWithCaseSensitivity(string? id1, string? id2, bool expected) { (new IdentifierString(id1) == new IdentifierString(id2)).Should().Be(expected); diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/NamedTypeIdentifierTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamedTypeIdentifierTests.cs similarity index 91% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/NamedTypeIdentifierTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamedTypeIdentifierTests.cs index d31755f21..6cfd31ad7 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/NamedTypeIdentifierTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamedTypeIdentifierTests.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator; +using Reqnroll.FeatureSourceGenerator; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; public class NamedTypeIdentifierTests { @@ -109,18 +111,6 @@ public void EqualsTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( typeId1.Equals(typeId2).Should().BeFalse(); } - [Theory] - [InlineData("Reqnroll", "Parser", "Reqnroll.Parser", true)] - [InlineData("Reqnroll", "_Parser", "Reqnroll._Parser", true)] - [InlineData(null, "_Parser", "_Parser", true)] - [InlineData(null, null, "", true)] - [InlineData(null, null, null, true)] - [InlineData("", "", null, true)] - public void EqualsString_ReturnsCaseSensitiveEquivalence(string ns, string name, string identifier, bool expected) - { - new NamedTypeIdentifier(new NamespaceString(ns), new IdentifierString(name)).Equals(identifier).Should().Be(expected); - } - [Theory] [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser")] [InlineData("Reqnroll", "Internal", "Reqnroll", "Internal")] diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/NamespaceStringTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamespaceStringTests.cs similarity index 98% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/NamespaceStringTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamespaceStringTests.cs index 9badbba30..e08592f9a 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/NamespaceStringTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamespaceStringTests.cs @@ -1,4 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator; +using Reqnroll.FeatureSourceGenerator; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; public class NamespaceStringTests { diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ParameterDescriptorTests.cs similarity index 94% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterDescriptorTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ParameterDescriptorTests.cs index 669a0454b..6f51fd5ab 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/ParameterDescriptorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ParameterDescriptorTests.cs @@ -1,10 +1,12 @@ -namespace Reqnroll.FeatureSourceGenerator; +using Reqnroll.FeatureSourceGenerator; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; public class ParameterDescriptorTests { [Fact] public void Constructor_ThrowsArgumentExceptionWhenNameIsEmpty() { - Func ctr = () => + Func ctr = () => new ParameterDescriptor(IdentifierString.Empty, new NamedTypeIdentifier(new IdentifierString("string"))); ctr.Should().Throw(); diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs index 86beeca7e..bb1471487 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/TestMethodAssertions.cs @@ -1,193 +1,20 @@ using FluentAssertions.Execution; -using FluentAssertions.Primitives; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; -using System.Reflection; +using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator; + public class TestMethodAssertions(TestMethod? subject) : - TestMethodAssertions(subject) + TestMethodAssertions(subject) { } -public class TestMethodAssertions(TestMethod? subject) : - ReferenceTypeAssertions(subject!) - where TAssertions : TestMethodAssertions +public class TestMethodAssertions(TestMethod? subject) : + AttributeAssertions(subject) + where TSubject: TestMethod? + where TAssertions : TestMethodAssertions { protected override string Identifier => "method"; - ///// - ///// Expects the class declaration have only a single attribute with a specific identifier. - ///// - ///// - ///// The declared type of the attribute. - ///// - ///// - ///// A formatted phrase as is supported by explaining why the assertion - ///// is needed. If the phrase does not start with the word because, it is prepended automatically. - ///// - ///// - ///// Zero or more objects to format using the placeholders in . - ///// - //public AndWhichConstraint HaveSingleAttribute( - // string type, - // string because = "", - // params object[] becauseArgs) - //{ - // var expectation = "Expected {context:method} to have a single attribute " + - // $"which is of type \"{type}\"{{reason}}"; - - // bool notNull = Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .ForCondition(Subject is not null) - // .FailWith(expectation + ", but found ."); - - // AttributeSyntax? match = default; - - // if (notNull) - // { - // var attributes = Subject!.Attributes; - - // switch (attributes.Length) - // { - // case 0: // Fail, Collection is empty - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .FailWith(expectation + ", but the class has no attributes."); - - // break; - // case 1: // Success Condition - // var single = attributes.Single(); - - // if (single.Name.ToString() != type) - // { - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .FailWith(expectation + ", but found the attribute \"{0}\".", single.Name); - // } - // else - // { - // match = single; - // } - - // break; - // default: // Fail, Collection contains more than a single item - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .FailWith(expectation + ", but found {0}.", attributes); - - // break; - // } - // } - - // return new AndWhichConstraint((TAssertions)this, match!); - //} - - //public AndWhichConstraint HaveAttribute( - // string type, - // string because = "", - // params object[] becauseArgs) - //{ - // var expectation = "Expected {context:method} to have an attribute " + - // $"of type \"{type}\"{{reason}}"; - - // bool notNull = Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .ForCondition(Subject is not null) - // .FailWith(expectation + ", but found ."); - - // AttributeSyntax? match = default; - - // if (notNull) - // { - // var attributes = Subject!.AttributeLists.SelectMany(list => list.Attributes).ToList(); - - // if (attributes.Count == 0) - // { - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .FailWith(expectation + ", but the method has no attributes."); - // } - // else - // { - // match = attributes.FirstOrDefault(attribute => attribute.Name.ToString() == type); - - // if (match == null) - // { - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .FailWith(expectation + ", but found {0}.", attributes); - // } - // } - // } - - // return new AndWhichConstraint((TAssertions)this, match!); - //} - - public AndConstraint HaveAttribuesEquivalentTo( - AttributeDescriptor[] expectation, - string because = "", - params object[] becauseArgs) => - HaveAttribuesEquivalentTo((IEnumerable)expectation, because, becauseArgs); - - public AndConstraint HaveAttribuesEquivalentTo( - IEnumerable expectation, - string because = "", - params object[] becauseArgs) - { - using var scope = new AssertionScope(); - - scope.FormattingOptions.UseLineBreaks = true; - - var assertion = Execute.Assertion - .BecauseOf(because, becauseArgs) - .WithExpectation("Expected {context:the method} to have attributes equivalent to {0}", expectation) - .ForCondition(Subject is not null) - .FailWith("but {context:the method} is ."); - - var expected = expectation.ToList(); - var actual = Subject!.Attributes; - - - var missing = expected.ToList(); - var extra = actual.ToList(); - - foreach (var item in expected) - { - if (extra.Remove(item)) - { - missing.Remove(item); - } - } - - if (missing.Count > 0) - { - if (extra.Count > 0) - { - assertion - .Then - .FailWith("but {context:the method} is missing attributes {0} and has extra attributes {1}", missing, extra); - } - else - { - assertion - .Then - .FailWith("but {context:the method} is missing attributes {0}", missing); - } - } - else if (extra.Count > 0) - { - assertion - .Then - .FailWith("but {context:the method} has extra attributes {0}", extra); - } - - return new AndConstraint((TAssertions)this); - } - public AndConstraint HaveNoParameters( string because = "", params object[] becauseArgs) => @@ -196,7 +23,7 @@ public AndConstraint HaveNoParameters( public AndConstraint HaveParametersEquivalentTo( ParameterDescriptor[] expectation, string because = "", - params object[] becauseArgs) => + params object[] becauseArgs) => HaveParametersEquivalentTo((IEnumerable)expectation, because, becauseArgs); public AndConstraint HaveParametersEquivalentTo( @@ -214,10 +41,10 @@ public AndConstraint HaveParametersEquivalentTo( var expected = expectation.ToList(); var assertion = (expected.Count == 0 ? - setup.WithExpectation("Expected {context:the method} to have no parameters") : - setup.WithExpectation("Expected {context:the method} to have parameters equivalent to {0}", expected)) + setup.WithExpectation("Expected {context:the subject} to have no parameters") : + setup.WithExpectation("Expected {context:the subject} to have parameters equivalent to {0}", expected)) .ForCondition(Subject is not null) - .FailWith("but {context:the method} is ."); + .FailWith("but {context:the subject} is ."); var actual = Subject!.Parameters; @@ -225,7 +52,7 @@ public AndConstraint HaveParametersEquivalentTo( { assertion .Then - .FailWith("but {context:the method} has parameters defined"); + .FailWith("but {context:the subject} has parameters defined"); } else { @@ -236,7 +63,7 @@ public AndConstraint HaveParametersEquivalentTo( .BecauseOf(because, becauseArgs) .WithExpectation("Expected parameter {0} to be \"{1}\" ", i, expectedItem) .ForCondition(actual.Length > i) - .FailWith(" but {context:the method} does not define a parameter {0}.", i); + .FailWith(" but {context:the subject} does not define a parameter {0}.", i); if (actual.Length >= i) { @@ -253,57 +80,10 @@ public AndConstraint HaveParametersEquivalentTo( { assertion .Then - .FailWith("but {context:the method} has additional parameters {0}.", actual.Skip(expected.Count)); + .FailWith("but {context:the subject} has additional parameters {0}.", actual.Skip(expected.Count)); } } return new AndConstraint((TAssertions)this); } - - //public AndConstraint HaveParametersEquivalentTo( - // IEnumerable expectation, - // string because = "", - // params object[] becauseArgs) - //{ - // using var scope = new AssertionScope(); - - // scope.FormattingOptions.UseLineBreaks = true; - - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .WithExpectation("Expected {context:the method} to have parameters equivalent to {0} {reason}", expectation) - // .ForCondition(Subject is not null) - // .FailWith("but {context:the method} is ."); - - // var expected = expectation.ToList(); - // var actual = Subject!.ParameterList.Parameters; - - // for (var i = 0; i < expected.Count; i++) - // { - // var expectedItem = expected[i]; - - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .WithExpectation("Expected {context:the method} to have parameter {0}: \"{1}\" {reason}", i+1, expectedItem) - // .ForCondition(Subject.ParameterList.Parameters.Count != 0) - // .FailWith("but {context:the method} has no parameters defined.", Subject.ParameterList.Parameters.Count) - // .Then - // .ForCondition(Subject.ParameterList.Parameters.Count > i) - // .FailWith("but {context:the method} only has {0} parameters defined.", Subject.ParameterList.Parameters.Count) - // .Then - // .Given(() => Subject.ParameterList.Parameters[i]) - // .ForCondition(actual => false)// actual.IsEquivalentTo(expectedItem, true)) - // .FailWith("but found \"{0}\".", expectedItem); - // } - - // if (expected.Count < actual.Count) - // { - // Execute.Assertion - // .BecauseOf(because, becauseArgs) - // .WithExpectation("Expected {context:the method} to have parameters equivalent to {0}{reason}", expectation) - // .FailWith("but {context:the method} has extra parameters {0}.", actual.Skip(expected.Count)); - // } - - // return new AndConstraint((TAssertions)this); - //} } From d7db69486c5da8a3f7f180c25edb1af2d617273b Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Wed, 3 Jul 2024 00:52:07 +0100 Subject: [PATCH 22/48] Add StepInvocation to model for testing --- .../CSharp/CSharpTestFixtureClass.cs | 46 ++++++---- .../CSharp/CSharpTestFixtureGenerator.cs | 84 ++++++++++++++----- .../CSharp/CSharpTestMethod.cs | 64 +++++++++----- .../MSTest/MSTestCSharpTestFixtureClass.cs | 27 ++++-- .../MSTestCSharpTestFixtureGenerator.cs | 14 +--- .../CSharp/MSTest/MSTestCSharpTestMethod.cs | 23 +++-- .../CSharp/TestFixtureDescriptor.cs | 15 ++++ .../NUnit/NUnitCSharpTestFixtureGenerator.cs | 10 +-- .../SourceModel/ScenarioStep.cs | 6 +- .../SourceModel/StepInvocation.cs | 57 +++++++++++++ .../SourceModel/StepType.cs | 27 ++++++ .../SourceModel/TestFixtureClass.cs | 14 +++- .../SourceModel/TestMethod.cs | 39 ++++++++- .../SourceModel/TestMethodDescriptor.cs | 21 +++++ .../TestFixtureSourceGenerator.cs | 9 +- .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 13 +-- .../MSTestCSharpTestFixtureGeneratorTests.cs | 20 +++-- 17 files changed, 378 insertions(+), 111 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/StepType.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/TestMethodDescriptor.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index d62ce0f26..d7b487ce9 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -7,26 +7,42 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; /// /// Represents a class which is a test fixture to execute the scenarios associated with a feature. /// -public class CSharpTestFixtureClass( - NamedTypeIdentifier identifier, - string hintName, - FeatureInformation feature, - ImmutableArray attributes = default, - ImmutableArray methods = default, - CSharpRenderingOptions? renderingOptions = null) : TestFixtureClass( - identifier, - hintName, - feature, - attributes), IEquatable +public class CSharpTestFixtureClass : TestFixtureClass, IEquatable { + public CSharpTestFixtureClass( + NamedTypeIdentifier identifier, + string hintName, + FeatureInformation feature, + ImmutableArray attributes = default, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) + : base( + identifier, + hintName, + feature, + attributes) + { + Methods = methods.IsDefault ? ImmutableArray.Empty : methods; + RenderingOptions = renderingOptions ?? new CSharpRenderingOptions(); + } + + public CSharpTestFixtureClass( + TestFixtureDescriptor descriptor, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) : base(descriptor) + { + Methods = methods.IsDefault ? ImmutableArray.Empty : methods; + RenderingOptions = renderingOptions ?? new CSharpRenderingOptions(); + } + private static readonly Encoding Encoding = new UTF8Encoding(false); - public ImmutableArray Methods { get; } = - methods.IsDefault ? ImmutableArray.Empty : methods; + public ImmutableArray Methods { get; } - public CSharpRenderingOptions RenderingOptions { get; } = renderingOptions ?? new CSharpRenderingOptions(); + public CSharpRenderingOptions RenderingOptions { get; } - public ImmutableArray NamespaceUsings { get; } = ImmutableArray.Create(new NamespaceString("System.Linq")); + public virtual ImmutableArray NamespaceUsings { get; } = + ImmutableArray.Create(new NamespaceString("System.Linq")); public override IEnumerable GetMethods() => Methods; diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs index 629698c0c..9335e60ee 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs @@ -54,15 +54,62 @@ protected abstract ImmutableArray GenerateTestMethodAttribu public TTestMethod GenerateTestMethod( TestMethodGenerationContext context, CancellationToken cancellationToken = default) + { + var descriptor = new TestMethodDescriptor + { + Scenario = context.ScenarioInformation, + Identifier = CSharpSyntax.GenerateTypeIdentifier(context.ScenarioInformation.Name), + StepInvocations = GenerateStepInvocations(context, cancellationToken), + Attributes = GenerateTestMethodAttributes(context, cancellationToken), + Parameters = GenerateTestMethodParameters(context, cancellationToken), + ScenarioParameters = GenerateScenarioParameters(context, cancellationToken) + }; + + return CreateTestMethod(context, descriptor); + } + + protected virtual ImmutableArray GenerateStepInvocations( + TestMethodGenerationContext context, + CancellationToken cancellationToken) { var scenario = context.ScenarioInformation; - var identifier = CSharpSyntax.GenerateTypeIdentifier(scenario.Name); - var attributes = GenerateTestMethodAttributes(context, cancellationToken); - var parameters = GenerateTestMethodParameters(context, cancellationToken); - var scenarioParameters = GenerateScenarioParameters(context, cancellationToken); + // In the case the scenario defines no examples, we don't pass any paramters to steps. + if (scenario.Examples.IsEmpty) + { + return scenario.Steps + .Select(step => new StepInvocation(step.StepType, step.LineNumber, step.Keyword, step.Text)) + .ToImmutableArray(); + } - return CreateTestMethod(context, identifier, scenario, attributes, parameters, scenarioParameters); + var headings = scenario.Examples.First().Headings; + var argumentMap = headings.ToDictionary(heading => heading, CSharpSyntax.GenerateParameterIdentifier); + + var invocations = new List(); + + // Translate the steps into invocations with arguments where required. + foreach (var step in scenario.Steps) + { + // Look for any example placeholders in the step text to be converted to a format string with arguments. + var arguments = new List(); + var text = step.Text; + + foreach (var (heading, parameter) in argumentMap) + { + var placeholder = $"<{heading}>"; + + if (text.Contains(placeholder)) + { + var index = arguments.Count; + text = text.Replace(placeholder, $"{{{index}}}"); + arguments.Add(parameter); + } + } + + invocations.Add(new StepInvocation(step.StepType, step.LineNumber, step.Keyword, text, arguments.ToImmutableArray())); + } + + return invocations.ToImmutableArray(); } protected virtual ImmutableArray> GenerateScenarioParameters( @@ -86,12 +133,8 @@ protected virtual ImmutableArray> Generat } protected abstract TTestMethod CreateTestMethod( - TestMethodGenerationContext context, - IdentifierString identifier, - ScenarioInformation scenario, - ImmutableArray attributes, - ImmutableArray parameters, - ImmutableArray> scenarioParameters); + TestMethodGenerationContext context, + TestMethodDescriptor descriptor); public TTestFixtureClass GenerateTestFixtureClass( TestFixtureGenerationContext context, @@ -106,27 +149,28 @@ public TTestFixtureClass GenerateTestFixtureClass( } var identifier = CSharpSyntax.GenerateTypeIdentifier(featureTitle); - - var attributes = GenerateTestFixtureClassAttributes(context, cancellationToken); - var namedIdentitifer = new NamedTypeIdentifier(context.TestFixtureNamespace, identifier); + + var descriptor = new TestFixtureDescriptor + { + Identifier = new NamedTypeIdentifier(context.TestFixtureNamespace, identifier), + Feature = feature, + Attributes = GenerateTestFixtureClassAttributes(context, cancellationToken), + HintName = context.FeatureHintName + }; var generationOptions = new CSharpRenderingOptions( UseNullableReferenceTypes: context.CompilationInformation.HasNullableReferencesEnabled); return CreateTestFixtureClass( context, - namedIdentitifer, - feature, - attributes, + descriptor, methods.ToImmutableArray(), generationOptions); } protected abstract TTestFixtureClass CreateTestFixtureClass( TestFixtureGenerationContext context, - NamedTypeIdentifier identifier, - FeatureInformation feature, - ImmutableArray attributes, + TestFixtureDescriptor descriptor, ImmutableArray methods, CSharpRenderingOptions renderingOptions); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs index 2ebd028f4..67c937b9a 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs @@ -1,16 +1,25 @@ -using System.Collections.Immutable; -using Reqnroll.FeatureSourceGenerator.SourceModel; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp; -public class CSharpTestMethod( +public class CSharpTestMethod : TestMethod, IEquatable +{ + public CSharpTestMethod( IdentifierString identifier, ScenarioInformation scenario, + ImmutableArray stepInvocations, ImmutableArray attributes = default, ImmutableArray parameters = default, - ImmutableArray> scenarioParameters = default) : - TestMethod(identifier, scenario, attributes, parameters, scenarioParameters), IEquatable -{ + ImmutableArray> scenarioParameters = default) + : base(identifier, scenario, stepInvocations, attributes, parameters, scenarioParameters) + { + } + + public CSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor) + { + } + public override bool Equals(object obj) => Equals(obj as CSharpTestMethod); public bool Equals(CSharpTestMethod? other) => base.Equals(other); @@ -110,10 +119,10 @@ protected virtual void RenderMethodBodyTo( sourceBuilder.AppendLine(); sourceBuilder.AppendLine("// start: invocation of scenario steps"); - foreach (var step in Scenario.Steps) + foreach (var invocation in StepInvocations) { cancellationToken.ThrowIfCancellationRequested(); - RenderScenarioStepInvocationTo(step, sourceBuilder, renderingOptions, cancellationToken); + RenderScenarioStepInvocationTo(invocation, sourceBuilder, renderingOptions, cancellationToken); } sourceBuilder.AppendLine("// end: invocation of scenario steps"); @@ -130,31 +139,48 @@ protected virtual void RenderMethodBodyTo( } protected virtual void RenderScenarioStepInvocationTo( - ScenarioStep step, + StepInvocation invocation, CSharpSourceTextBuilder sourceBuilder, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { if (renderingOptions.EnableLineMapping) { - sourceBuilder.AppendDirective($"#line {step.LineNumber}"); + sourceBuilder.AppendDirective($"#line {invocation.SourceLineNumber}"); } sourceBuilder .Append("await testRunner.") .Append( - step.KeywordType switch + invocation.Type switch { - global::Gherkin.StepKeywordType.Context => "Given", - global::Gherkin.StepKeywordType.Action => "When", - global::Gherkin.StepKeywordType.Outcome => "Then", - global::Gherkin.StepKeywordType.Conjunction => "And", - _ => throw new NotSupportedException() + StepType.Context => "Given", + StepType.Action => "When", + StepType.Outcome => "Then", + StepType.Conjunction => "And", + _ => throw new NotSupportedException() }) - .Append("Async(") - .AppendLiteral(step.Text) + .Append("Async("); + + if (invocation.Arguments.IsEmpty) + { + sourceBuilder.AppendLiteral(invocation.Text); + } + else + { + sourceBuilder.Append("string.Format(").AppendLiteral(invocation.Text); + + foreach (var argument in invocation.Arguments) + { + sourceBuilder.Append(", ").Append(argument); + } + + sourceBuilder.Append(")"); + } + + sourceBuilder .Append(", null, null, ") - .AppendLiteral(step.Keyword) + .AppendLiteral(invocation.Keyword) .AppendLine(");"); if (renderingOptions.EnableLineMapping) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs index 5e535acbb..8714c79a1 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs @@ -2,14 +2,27 @@ using Reqnroll.FeatureSourceGenerator.SourceModel; namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; -public class MSTestCSharpTestFixtureClass( - NamedTypeIdentifier identifier, - string hintName, - FeatureInformation feature, - ImmutableArray attributes = default, - ImmutableArray methods = default, - CSharpRenderingOptions? renderingOptions = null) : CSharpTestFixtureClass(identifier, hintName, feature, attributes, methods, renderingOptions) +public class MSTestCSharpTestFixtureClass : CSharpTestFixtureClass { + public MSTestCSharpTestFixtureClass( + NamedTypeIdentifier identifier, + string hintName, + FeatureInformation feature, + ImmutableArray attributes = default, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) + : base(identifier, hintName, feature, attributes, methods, renderingOptions) + { + } + + public MSTestCSharpTestFixtureClass( + TestFixtureDescriptor descriptor, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) + : base(descriptor, methods, renderingOptions) + { + } + protected override void RenderTestFixtureContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) { RenderTestRunnerFieldTo(sourceBuilder); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs index 27fb37ae4..44ef40d97 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs @@ -12,24 +12,18 @@ internal class MSTestCSharpTestFixtureGenerator(MSTestHandler frameworkHandler) { protected override MSTestCSharpTestFixtureClass CreateTestFixtureClass( TestFixtureGenerationContext context, - NamedTypeIdentifier identifier, - FeatureInformation feature, - ImmutableArray attributes, + TestFixtureDescriptor descriptor, ImmutableArray methods, CSharpRenderingOptions renderingOptions) { - return new MSTestCSharpTestFixtureClass(identifier, context.FeatureHintName, feature, attributes, methods, renderingOptions); + return new MSTestCSharpTestFixtureClass(descriptor, methods, renderingOptions); } protected override CSharpTestMethod CreateTestMethod( TestMethodGenerationContext context, - IdentifierString identifier, - ScenarioInformation scenario, - ImmutableArray attributes, - ImmutableArray parameters, - ImmutableArray> scenarioParameters) + TestMethodDescriptor descriptor) { - return new MSTestCSharpTestMethod(identifier, scenario, attributes, parameters, scenarioParameters); + return new MSTestCSharpTestMethod(descriptor); } protected override ImmutableArray GenerateTestFixtureClassAttributes( diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs index 525ed3a5c..96a46687a 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs @@ -2,14 +2,23 @@ using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; -public class MSTestCSharpTestMethod( - IdentifierString identifier, - ScenarioInformation scenario, - ImmutableArray attributes = default, - ImmutableArray parameters = default, - ImmutableArray> scenarioParameters = default) : - CSharpTestMethod(identifier, scenario, attributes, parameters, scenarioParameters) +public class MSTestCSharpTestMethod : CSharpTestMethod { + public MSTestCSharpTestMethod( + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray stepInvocations, + ImmutableArray attributes = default, + ImmutableArray parameters = default, + ImmutableArray> scenarioParameters = default) + : base(identifier, scenario, stepInvocations, attributes, parameters, scenarioParameters) + { + } + + public MSTestCSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor) + { + } + protected override void RenderTestRunnerLookupTo( CSharpSourceTextBuilder sourceBuilder, CSharpRenderingOptions renderingOptions, diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs b/Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs new file mode 100644 index 000000000..b45e8b425 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs @@ -0,0 +1,15 @@ +using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +public class TestFixtureDescriptor +{ + public NamedTypeIdentifier? Identifier { get; set; } + + public string? HintName { get; set; } + + public FeatureInformation? Feature { get; set; } + + public ImmutableArray Attributes { get; set; } +} diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs index c82205fd5..8186f22a9 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs @@ -9,9 +9,7 @@ internal class NUnitCSharpTestFixtureGenerator(NUnitHandler testFrameworkHandler { protected override CSharpTestFixtureClass CreateTestFixtureClass( TestFixtureGenerationContext context, - NamedTypeIdentifier identifier, - FeatureInformation feature, - ImmutableArray attributes, + TestFixtureDescriptor descriptor, ImmutableArray methods, CSharpRenderingOptions renderingOptions) { @@ -20,11 +18,7 @@ protected override CSharpTestFixtureClass CreateTestFixtureClass( protected override CSharpTestMethod CreateTestMethod( TestMethodGenerationContext context, - IdentifierString identifier, - ScenarioInformation scenario, - ImmutableArray attributes, - ImmutableArray parameters, - ImmutableArray> scenarioParameters) + TestMethodDescriptor descriptor) { throw new NotImplementedException(); } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs index 9f78b8a76..d4f812a89 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs @@ -1,5 +1,3 @@ -using Gherkin; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; -namespace Reqnroll.FeatureSourceGenerator.SourceModel; - -public record ScenarioStep(StepKeywordType KeywordType, string Keyword, string Text, int LineNumber); +public record ScenarioStep(StepType StepType, string Keyword, string Text, int LineNumber); diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs new file mode 100644 index 000000000..f7dc5c7df --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs @@ -0,0 +1,57 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; +public class StepInvocation( + StepType type, + int sourceLineNumber, + string keyword, + string text, + ImmutableArray arguments = default) +{ + public StepType Type { get; } = type; + + public int SourceLineNumber { get; } = sourceLineNumber; + + public string Keyword { get; } = keyword; + + public string Text { get; } = text; + + public ImmutableArray Arguments { get; } = + arguments.IsDefault ? ImmutableArray.Empty : arguments; + + + public virtual bool Equals(StepInvocation? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Type.Equals(other.Type) && + SourceLineNumber.Equals(other.SourceLineNumber) && + Keyword.Equals(other.Keyword) && + Text.Equals(other.Text) && + (Arguments.Equals(other.Arguments) || Arguments.SequenceEqual(other.Arguments)); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 92321497; + + hash *= 47886541 + Type.GetHashCode(); + hash *= 47886541 + SourceLineNumber.GetHashCode(); + hash *= 47886541 + Keyword.GetHashCode(); + hash *= 47886541 + Text.GetHashCode(); + hash *= 47886541 + Arguments.GetSequenceHashCode(); + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/StepType.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/StepType.cs new file mode 100644 index 000000000..ca82a6c6d --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/StepType.cs @@ -0,0 +1,27 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +/// +/// Specifies the type of a scenario step. +/// +public enum StepType +{ + /// + /// The step sets up the context of the scenario. Associated with the "Given" keyword. + /// + Context, + + /// + /// The step performs an action in the scenario. Associated with the "When" keyword. + /// + Action, + + /// + /// The step performs an assertion on the result of the scenario. Associated with the "Then" keyword. + /// + Outcome, + + /// + /// The step is a continuation of the previous step type. Associted with the "And" keyword. + /// + Conjunction +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs index 851d73db0..2854d0d86 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using Reqnroll.FeatureSourceGenerator.CSharp; +using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.SourceModel; @@ -16,7 +17,7 @@ public abstract class TestFixtureClass : IEquatable, IHasAttr /// within the compilation. /// The feature information that will be included in the test fixture. /// The attributes which are applied to the feature. - public TestFixtureClass( + protected TestFixtureClass( NamedTypeIdentifier identifier, string hintName, FeatureInformation featureInformation, @@ -35,6 +36,15 @@ public TestFixtureClass( Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; } + protected TestFixtureClass(TestFixtureDescriptor descriptor) : + this( + descriptor.Identifier ?? throw new ArgumentException($"{nameof(descriptor.Identifier)} cannot be null.", nameof(descriptor)), + descriptor.HintName ?? throw new ArgumentException($"{nameof(descriptor.HintName)} cannot be null.", nameof(descriptor)), + descriptor.Feature ?? throw new ArgumentException($"{nameof(descriptor.Feature)} cannot be null.", nameof(descriptor)), + descriptor.Attributes) + { + } + /// /// Gets the identifier of the class. /// diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs index 29f4b6515..871cb60c2 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs @@ -12,6 +12,7 @@ public abstract class TestMethod : IEquatable, IHasAttributes /// /// The identifier of the method. /// The scenario associated with the method. + /// The step invocations performed by the method. /// The attributes applied to the method. /// The parameters defined by the method. /// A map of scenario parameters to the identifiers that will be used to supply @@ -19,9 +20,10 @@ public abstract class TestMethod : IEquatable, IHasAttributes /// /// is an empty identifier string. /// - public TestMethod( + protected TestMethod( IdentifierString identifier, ScenarioInformation scenario, + ImmutableArray stepInvocations, ImmutableArray attributes = default, ImmutableArray parameters = default, ImmutableArray> scenarioParameters = default) @@ -33,16 +35,42 @@ public TestMethod( Identifier = identifier; Scenario = scenario; + StepInvocations = stepInvocations.IsDefault ? ImmutableArray.Empty : stepInvocations; Attributes = attributes.IsDefault ? ImmutableArray.Empty : attributes; Parameters = parameters.IsDefault ? ImmutableArray.Empty : parameters; ParametersOfScenario = scenarioParameters.IsDefault ? ImmutableArray>.Empty : scenarioParameters; } + /// + /// Initializes a new inastance of the class from a descriptor. + /// + /// A description of the method to create. + /// + /// The is incomplete. + /// + protected TestMethod(TestMethodDescriptor descriptor) + : this( + descriptor.Identifier, + descriptor.Scenario ?? throw new ArgumentException( + $"{nameof(descriptor.Scenario)} property cannot be null.", + nameof(descriptor)), + descriptor.StepInvocations, + descriptor.Attributes, + descriptor.Parameters, + descriptor.ScenarioParameters) + { + } + /// /// Gets the identifier of the test method. /// public IdentifierString Identifier { get; } + /// + /// Gets the step invocations the method performs. + /// + public ImmutableArray StepInvocations { get; } + /// /// Gets the attributes applied to the method. /// @@ -70,10 +98,13 @@ public override int GetHashCode() unchecked { var hash = 86434151; - + hash *= 83155477 + Identifier.GetHashCode(); + hash *= 83155477 + Scenario.GetHashCode(); + hash *= 83155477 + StepInvocations.GetSequenceHashCode(); hash *= 83155477 + Attributes.GetSetHashCode(); hash *= 83155477 + Parameters.GetSequenceHashCode(); + hash *= 83155477 + ParametersOfScenario.GetSequenceHashCode(); return hash; } @@ -92,8 +123,10 @@ public bool Equals(TestMethod? other) } return Identifier.Equals(other.Identifier) && + Scenario.Equals(other.Scenario) && + (StepInvocations.Equals(other.StepInvocations) || StepInvocations.SequenceEqual(other.StepInvocations)) && (Attributes.Equals(other.Attributes) || Attributes.SetEquals(other.Attributes)) && (Parameters.Equals(other.Parameters) || Parameters.SequenceEqual(other.Parameters)) && - ParametersOfScenario.Equals(other.ParametersOfScenario); + (ParametersOfScenario.Equals(other.ParametersOfScenario) || ParametersOfScenario.SequenceEqual(other.ParametersOfScenario)); } } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethodDescriptor.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethodDescriptor.cs new file mode 100644 index 000000000..e4f0101ae --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethodDescriptor.cs @@ -0,0 +1,21 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +/// +/// A mutable description of a test method, used to help construct test method instances. +/// +public class TestMethodDescriptor +{ + public IdentifierString Identifier { get; set; } + + public ScenarioInformation? Scenario { get; set; } + + public ImmutableArray StepInvocations { get; set; } + + public ImmutableArray Attributes { get; set; } + + public ImmutableArray Parameters { get; set; } + + public ImmutableArray> ScenarioParameters { get; set; } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index a93305763..485353b9c 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -306,7 +306,14 @@ private static ScenarioInformation CreateScenarioInformation( cancellationToken.ThrowIfCancellationRequested(); var scenarioStep = new ScenarioStep( - step.KeywordType, + step.KeywordType switch + { + StepKeywordType.Context => StepType.Context, + StepKeywordType.Action => StepType.Action, + StepKeywordType.Outcome => StepType.Outcome, + StepKeywordType.Conjunction => StepType.Conjunction, + _ => throw new NotSupportedException() + }, step.Keyword, step.Text, step.Location.Line); diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs index d057b384b..c29079556 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -1,5 +1,4 @@ - -using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; @@ -10,9 +9,7 @@ internal class XUnitCSharpTestFixtureGenerator(XUnitHandler testFrameworkHandler { protected override CSharpTestFixtureClass CreateTestFixtureClass( TestFixtureGenerationContext context, - NamedTypeIdentifier identifier, - FeatureInformation feature, - ImmutableArray attributes, + TestFixtureDescriptor descriptor, ImmutableArray methods, CSharpRenderingOptions renderingOptions) { @@ -21,11 +18,7 @@ protected override CSharpTestFixtureClass CreateTestFixtureClass( protected override CSharpTestMethod CreateTestMethod( TestMethodGenerationContext context, - IdentifierString identifier, - ScenarioInformation scenario, - ImmutableArray attributes, - ImmutableArray parameters, - ImmutableArray> scenarioParameters) + TestMethodDescriptor descriptor) { throw new NotImplementedException(); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs index 75273aee9..bbaddbe63 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -45,7 +45,7 @@ public void GenerateTestFixture_CreatesClassForFeatureWithMsTestAttributes() "Sample Scenario", 22, [], - [new ScenarioStep(StepKeywordType.Action, "When", "foo happens", 6)]); + [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -77,7 +77,7 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp "Sample Scenario", 22, [], - [new ScenarioStep(StepKeywordType.Action, "When", "foo happens", 6)]); + [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -105,6 +105,11 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp ]); method.Should().HaveNoParameters(); + + method.StepInvocations.Should().BeEquivalentTo( + [ + new StepInvocation(StepType.Action, 6, "When", "foo happens") + ]); } [Fact] @@ -117,7 +122,7 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen "Sample Scenario Outline", 22, [], - [ new ScenarioStep(StepKeywordType.Action, "When", " happens", 6) ], + [ new ScenarioStep(StepType.Action, "When", " happens", 6) ], [ exampleSet1, exampleSet2 ]); var featureInfo = new FeatureInformation( @@ -149,7 +154,7 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen ["Sample Scenario Outline"]), new AttributeDescriptor( new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), - namedArguments: new Dictionary{ { "FeatureTitle", "Sample" } }.ToImmutableDictionary()), + positionalArguments: ["FeatureTitle", "Sample"]), new AttributeDescriptor( new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), ["foo", ImmutableArray.Create(ImmutableArray.Create("example_tag"))]), @@ -168,8 +173,13 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen new NamedTypeIdentifier(new NamespaceString("System"), new IdentifierString("String"))), new ParameterDescriptor( - new IdentifierString("_tags"), + new IdentifierString("_exampleTags"), new ArrayTypeIdentifier(new NamedTypeIdentifier(new NamespaceString("System"), new IdentifierString("String")))) ]); + + method.StepInvocations.Should().BeEquivalentTo( + [ + new StepInvocation(StepType.Action, 6, "When", "{0} happens", [new IdentifierString("what")]) + ]); } } From c3fa1b0e893f21e96b4a92e692657d3e529fc4e8 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Wed, 3 Jul 2024 22:42:52 +0100 Subject: [PATCH 23/48] Add omission of ignored examples by default with option to enable --- .../TestFixtureSourceGenerator.cs | 38 +++- .../CSharpTestFixtureSourceGeneratorTests.cs | 198 ++++++++++++++++++ .../MSTestCSharpTestFixtureGeneratorTests.cs | 3 +- ...eqnroll.FeatureSourceGeneratorTests.csproj | 1 + .../XUnitFeatureSourceGeneratorTests.cs | 7 +- 5 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs rename Tests/Reqnroll.FeatureSourceGeneratorTests/{ => XUnit}/XUnitFeatureSourceGeneratorTests.cs (95%) diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 485353b9c..6386ca5c0 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -196,6 +196,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return [.. diagnostics]; } + // Determine whether we should include ignored examples in our sample sets. + var emitIgnoredExamples = false; + if (options.TryGetValue("reqnroll.emit_ignored_examples", out var emitIgnoredExamplesValue) || + options.TryGetValue("build_property.ReqnrollEmitIgnoredExamples", out emitIgnoredExamplesValue)) + { + bool.TryParse(emitIgnoredExamplesValue, out emitIgnoredExamples); + } + var feature = document.Feature; var featureInformation = new FeatureInformation( @@ -206,18 +214,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) featureFile.Path); var scenarioInformations = feature.Children - .SelectMany(child => CreateScenarioInformations(child, cancellationToken)) + .SelectMany(child => CreateScenarioInformations(child, emitIgnoredExamples, cancellationToken)) .ToImmutableArray(); return [ new TestFixtureGenerationContext( - featureInformation, - scenarioInformations, - featureHintName, - new NamespaceString(testFixtureNamespace), - compilationInfo, - generator) + featureInformation, + scenarioInformations, + featureHintName, + new NamespaceString(testFixtureNamespace), + compilationInfo, + generator) ]; }); @@ -266,23 +274,26 @@ public void Initialize(IncrementalGeneratorInitializationContext context) private static IEnumerable CreateScenarioInformations( IHasLocation child, + bool emitIgnoredExamples, CancellationToken cancellationToken) { return child switch { - Scenario scenario => [CreateScenarioInformation(scenario, cancellationToken)], - Rule rule => CreateScenarioInformations(rule, cancellationToken), + Scenario scenario => [CreateScenarioInformation(scenario, emitIgnoredExamples, cancellationToken)], + Rule rule => CreateScenarioInformations(rule, emitIgnoredExamples, cancellationToken), _ => [] }; } private static ScenarioInformation CreateScenarioInformation( Scenario scenario, - CancellationToken cancellationToken) => CreateScenarioInformation(scenario, null, cancellationToken); + bool emitIgnoredExamples, + CancellationToken cancellationToken) => CreateScenarioInformation(scenario, null, emitIgnoredExamples, cancellationToken); private static ScenarioInformation CreateScenarioInformation( Scenario scenario, RuleInformation? rule, + bool emitIgnoredExamples, CancellationToken cancellationToken) { var exampleSets = new List(); @@ -296,6 +307,11 @@ private static ScenarioInformation CreateScenarioInformation( example.TableBody.Select(row => row.Cells.Select(cell => cell.Value).ToImmutableArray()).ToImmutableArray(), example.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray()); + if (!emitIgnoredExamples && examples.Tags.Contains("ignore", StringComparer.OrdinalIgnoreCase)) + { + continue; + } + exampleSets.Add(examples); } @@ -332,6 +348,7 @@ private static ScenarioInformation CreateScenarioInformation( private static IEnumerable CreateScenarioInformations( Rule rule, + bool emitIgnoredExamples, CancellationToken cancellationToken) { var tags = rule.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(); @@ -346,6 +363,7 @@ private static IEnumerable CreateScenarioInformations( yield return CreateScenarioInformation( scenario, new RuleInformation(rule.Name, tags), + emitIgnoredExamples, cancellationToken); break; } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs new file mode 100644 index 000000000..fc348b99a --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs @@ -0,0 +1,198 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Moq; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; +public class CSharpTestFixtureSourceGeneratorTests +{ + private readonly Mock _mockHandler = new(); + + private readonly Mock> _mockGenerator = new(); + + public CSharpTestFixtureSourceGeneratorTests() + { + _mockHandler + .Setup(handler => handler.IsTestFrameworkReferenced(It.IsAny())) + .Returns(true); + + _mockHandler.SetupGet(handler => handler.TestFrameworkName).Returns("Mock"); + + _mockHandler + .Setup(handler => handler.GetTestFixtureGenerator()) + .Returns(_mockGenerator.Object); + + + _mockGenerator.SetupGet(generator => generator.TestFrameworkHandler).Returns(_mockHandler.Object); + + //_mockGenerator + // .Setup(generator => generator.GenerateTestMethod( + // It.IsAny>(), + // It.IsAny())) + // .Returns(new CSharpTestMethod()) + + _mockGenerator + .Setup(generator => generator.GenerateTestFixtureClass( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .Returns(new CSharpTestFixtureClass( + new NamedTypeIdentifier(new IdentifierString("Mock")), + "Mock.feature", + new FeatureInformation("Mock", null, "en"))); + } + + [Theory] + [InlineData("reqnroll.emit_ignored_examples")] + [InlineData("build_property.ReqnrollEmitIgnoredExamples")] + public void GeneratorEmitsIgnoredScenariosWhenOptionEnabled(string setting) + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([_mockHandler.Object]); + + const string featureText = + """ + Feature: Sample Feature + + Scenario Outline: SO + When the step + Examples: + | result | + | passes | + | fails | + | is pending | + | is undefined | + @ignore + Examples: + | result | + | ignored | + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { setting, "true" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generate = _mockGenerator.Invocations + .Single(inv => inv.Method.Name == nameof(ITestFixtureGenerator.GenerateTestMethod)); + + var context = (TestMethodGenerationContext)generate.Arguments[0]; + + context.ScenarioInformation.Examples.Should().HaveCount(2); + } + + [Theory] + [InlineData("reqnroll.emit_ignored_examples")] + [InlineData("build_property.ReqnrollEmitIgnoredExamples")] + public void GeneratorOmitsIgnoredScenariosWhenOptionDisabled(string setting) + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([_mockHandler.Object]); + + const string featureText = + """ + Feature: Sample Feature + + Scenario Outline: SO + When the step + Examples: + | result | + | passes | + | fails | + | is pending | + | is undefined | + @ignore + Examples: + | result | + | ignored | + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { setting, "false" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generate = _mockGenerator.Invocations + .Single(inv => inv.Method.Name == nameof(ITestFixtureGenerator.GenerateTestMethod)); + + var context = (TestMethodGenerationContext)generate.Arguments[0]; + + context.ScenarioInformation.Examples.Should().HaveCount(1); + } + + [Fact] + public void GeneratorOmitsIgnoredScenariosByDefault() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([_mockHandler.Object]); + + const string featureText = + """ + Feature: Sample Feature + + Scenario Outline: SO + When the step + Examples: + | result | + | passes | + | fails | + | is pending | + | is undefined | + @ignore + Examples: + | result | + | ignored | + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions([])); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + diagnostics.Should().BeEmpty(); + + var generate = _mockGenerator.Invocations + .Single(inv => inv.Method.Name == nameof(ITestFixtureGenerator.GenerateTestMethod)); + + var context = (TestMethodGenerationContext)generate.Arguments[0]; + + context.ScenarioInformation.Examples.Should().HaveCount(1); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs index bbaddbe63..226c61ca5 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -1,5 +1,4 @@ -using Gherkin; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj index 6e04d4425..5ae2185d5 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/Reqnroll.FeatureSourceGeneratorTests.csproj @@ -18,6 +18,7 @@ + diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs similarity index 95% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs index edb6a7c0c..67269c374 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnitFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs @@ -1,12 +1,13 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Reqnroll.FeatureSourceGenerator; using Reqnroll.FeatureSourceGenerator.CSharp; -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.XUnit; public class XUnitFeatureSourceGeneratorTests { - [Fact] + [Fact(Skip = "XUnit not yet implemented.")] public void GeneratorProducesXUnitOutputWhenWhenBuildPropertyConfiguredForXUnit() { var references = AppDomain.CurrentDomain.GetAssemblies() @@ -56,7 +57,7 @@ Then the result should be 120 .Which.Should().HaveAttribute("global::Xunit.Fact"); } - [Fact] + [Fact(Skip = "XUnit not yet implemented.")] public void GeneratorProducesXUnitOutputWhenWhenEditorConfigConfiguredForXUnit() { var references = AppDomain.CurrentDomain.GetAssemblies() From 100ce8ab1043f34fc0727e1b91aec7d1ea1ce1a4 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Wed, 3 Jul 2024 23:40:31 +0100 Subject: [PATCH 24/48] Add readme placeholder --- Reqnroll.FeatureSourceGenerator/README.md | 1 + .../Reqnroll.FeatureSourceGenerator.csproj | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/README.md diff --git a/Reqnroll.FeatureSourceGenerator/README.md b/Reqnroll.FeatureSourceGenerator/README.md new file mode 100644 index 000000000..17d853686 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/README.md @@ -0,0 +1 @@ +# Roslyn Feature Source Generator diff --git a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj index 339f72676..476f16064 100644 --- a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj +++ b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj @@ -17,6 +17,7 @@ false true $(NoWarn);NU5128 + README.md @@ -57,10 +58,7 @@ - - - - + From d97266ef9685411e8343b02b2d88d4ae66ab312f Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Thu, 4 Jul 2024 20:56:17 +0100 Subject: [PATCH 25/48] Disable symbol packing for Feature Source Generator --- .../Reqnroll.FeatureSourceGenerator.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj index 476f16064..8c2056c65 100644 --- a/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj +++ b/Reqnroll.FeatureSourceGenerator/Reqnroll.FeatureSourceGenerator.csproj @@ -15,6 +15,7 @@ true false + false true $(NoWarn);NU5128 README.md From d73ad56e9eccf9b42c983f3e62a9cf2216d5416f Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Mon, 8 Jul 2024 20:12:39 +0100 Subject: [PATCH 26/48] Add syntax for expressing more type identifiers --- .../CSharp/CSharpSourceTextBuilder.cs | 40 ++++- .../CSharp/CSharpTestFixtureClass.cs | 5 +- .../CSharp/CSharpTestFixtureGenerator.cs | 23 ++- .../MSTest/MSTestCSharpTestFixtureClass.cs | 2 +- .../CSharp/TestFixtureDescriptor.cs | 4 +- .../XUnit/XUnitCSharpTestFixtureClass.cs | 74 +++++++++ .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 46 ++++++ .../CSharp/XUnit/XUnitCSharpTestMethod.cs | 30 ++++ .../MSTest/MSTestSyntax.cs | 10 +- .../SourceModel/ArrayTypeIdentifier.cs | 29 ++-- .../SourceModel/AttributeDescriptor.cs | 4 +- .../SourceModel/CommonTypes.cs | 4 +- .../SourceModel/GenericTypeIdentifier.cs | 89 +++++++++++ .../SourceModel/LocalTypeIdentifier.cs | 6 + .../SourceModel/NamedTypeIdentifier.cs | 80 ---------- .../SourceModel/NamespaceString.cs | 15 ++ .../SourceModel/NestedTypeIdentifier.cs | 42 +++++ .../SourceModel/QualifiedTypeIdentifier.cs | 66 ++++++++ .../SourceModel/SimpleTypeIdentifier.cs | 70 +++++++++ .../SourceModel/TestFixtureClass.cs | 15 +- .../SourceModel/TypeIdentifier.cs | 44 +----- .../XUnit/XUnitCSharpSyntaxGeneration.cs | 74 --------- .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 39 ----- .../XUnit/XUnitHandler.cs | 5 +- .../AttributeDescriptorTests.cs | 36 ++--- .../CSharpTestFixtureSourceGeneratorTests.cs | 2 +- .../CSharpMethodDeclarationAssertions.cs | 4 +- .../MSTestCSharpTestFixtureGeneratorTests.cs | 24 +-- .../MSTestFeatureSourceGenerationTests.cs | 2 +- .../SourceModel/ArrayTypeIdentifierTests.cs | 99 ++++++++++++ .../SourceModel/GenericTypeIdentifierTests.cs | 148 ++++++++++++++++++ .../SourceModel/ParameterDescriptorTests.cs | 15 +- ...sts.cs => QualifiedTypeIdentifierTests.cs} | 87 ++++------ .../SourceModel/SimpleTypeIdentifierTests.cs | 104 ++++++++++++ .../XUnit/XUnitFeatureSourceGeneratorTests.cs | 4 +- 35 files changed, 967 insertions(+), 374 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/GenericTypeIdentifier.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/LocalTypeIdentifier.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/NestedTypeIdentifier.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/QualifiedTypeIdentifier.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/SimpleTypeIdentifier.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ArrayTypeIdentifierTests.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/GenericTypeIdentifierTests.cs rename Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/{NamedTypeIdentifierTests.cs => QualifiedTypeIdentifierTests.cs} (51%) create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/SimpleTypeIdentifierTests.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs index f3095b97c..e26a70119 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -276,20 +276,41 @@ public CSharpSourceTextBuilder AppendTypeReference(TypeIdentifier type) { return type switch { - NamedTypeIdentifier namedType => AppendTypeReference(namedType), + SimpleTypeIdentifier simpleType => AppendTypeReference(simpleType), + GenericTypeIdentifier genericType => AppendTypeReference(genericType), + QualifiedTypeIdentifier qualifiedType => AppendTypeReference(qualifiedType), ArrayTypeIdentifier arrayType => AppendTypeReference(arrayType), _ => throw new NotImplementedException() }; } - public CSharpSourceTextBuilder AppendTypeReference(NamedTypeIdentifier type) + public CSharpSourceTextBuilder AppendTypeReference(SimpleTypeIdentifier type) { - if (!type.Namespace.IsEmpty) + Append(type.Name); + + if (type.IsNullable) { - Append("global::").Append(type.Namespace).Append('.'); + Append('?'); } - Append(type.LocalName); + return this; + } + + public CSharpSourceTextBuilder AppendTypeReference(GenericTypeIdentifier type) + { + Append(type.Name); + + Append('<'); + + AppendTypeReference(type.TypeArguments[0]); + + for (var i = 1; i < type.TypeArguments.Length; i++) + { + Append(','); + AppendTypeReference(type.TypeArguments[i]); + } + + Append('>'); if (type.IsNullable) { @@ -299,6 +320,15 @@ public CSharpSourceTextBuilder AppendTypeReference(NamedTypeIdentifier type) return this; } + public CSharpSourceTextBuilder AppendTypeReference(QualifiedTypeIdentifier type) + { + Append("global::").Append(type.Namespace).Append('.'); + + AppendTypeReference(type.LocalType); + + return this; + } + public CSharpSourceTextBuilder AppendTypeReference(ArrayTypeIdentifier type) { AppendTypeReference(type.ItemType).Append("[]"); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index d7b487ce9..b9035408e 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -10,7 +10,7 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; public class CSharpTestFixtureClass : TestFixtureClass, IEquatable { public CSharpTestFixtureClass( - NamedTypeIdentifier identifier, + QualifiedTypeIdentifier identifier, string hintName, FeatureInformation feature, ImmutableArray attributes = default, @@ -57,6 +57,7 @@ public override SourceText Render(CancellationToken cancellationToken = default) public void RenderTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken = default) { + sourceBuilder.Append("namespace ").Append(Identifier.Namespace).AppendLine(); sourceBuilder.BeginBlock("{"); @@ -80,7 +81,7 @@ public void RenderTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken ca } } - sourceBuilder.Append("public partial class ").AppendLine(Identifier.LocalName); + sourceBuilder.Append("public partial class ").AppendTypeReference(Identifier.LocalType).AppendLine(); sourceBuilder.BeginBlock("{"); if (RenderingOptions.EnableLineMapping && FeatureInformation.FilePath != null) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs index 9335e60ee..cc12c7e83 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs @@ -16,9 +16,12 @@ public abstract class CSharpTestFixtureGenerator { public ITestFrameworkHandler TestFrameworkHandler { get; } = testFrameworkHandler; - protected abstract ImmutableArray GenerateTestFixtureClassAttributes( + protected virtual ImmutableArray GenerateTestFixtureClassAttributes( TestFixtureGenerationContext context, - CancellationToken cancellationToken); + CancellationToken cancellationToken) + { + return ImmutableArray.Empty; + } protected virtual ImmutableArray GenerateTestMethodParameters( TestMethodGenerationContext context, @@ -148,12 +151,14 @@ public TTestFixtureClass GenerateTestFixtureClass( featureTitle += " Feature"; } - var identifier = CSharpSyntax.GenerateTypeIdentifier(featureTitle); - + var className = CSharpSyntax.GenerateTypeIdentifier(featureTitle); + var qualifiedClassName = context.TestFixtureNamespace + new SimpleTypeIdentifier(className); + var descriptor = new TestFixtureDescriptor { - Identifier = new NamedTypeIdentifier(context.TestFixtureNamespace, identifier), + Identifier = qualifiedClassName, Feature = feature, + Interfaces = GenerateTestFixtureInterfaces(context, qualifiedClassName, cancellationToken), Attributes = GenerateTestFixtureClassAttributes(context, cancellationToken), HintName = context.FeatureHintName }; @@ -168,6 +173,14 @@ public TTestFixtureClass GenerateTestFixtureClass( generationOptions); } + protected virtual ImmutableArray GenerateTestFixtureInterfaces( + TestFixtureGenerationContext context, + QualifiedTypeIdentifier qualifiedClassName, + CancellationToken cancellationToken) + { + return ImmutableArray.Empty; + } + protected abstract TTestFixtureClass CreateTestFixtureClass( TestFixtureGenerationContext context, TestFixtureDescriptor descriptor, diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs index 8714c79a1..c7283d906 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs @@ -5,7 +5,7 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; public class MSTestCSharpTestFixtureClass : CSharpTestFixtureClass { public MSTestCSharpTestFixtureClass( - NamedTypeIdentifier identifier, + QualifiedTypeIdentifier identifier, string hintName, FeatureInformation feature, ImmutableArray attributes = default, diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs b/Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs index b45e8b425..d95f80f6f 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/TestFixtureDescriptor.cs @@ -5,11 +5,13 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; public class TestFixtureDescriptor { - public NamedTypeIdentifier? Identifier { get; set; } + public QualifiedTypeIdentifier? Identifier { get; set; } public string? HintName { get; set; } public FeatureInformation? Feature { get; set; } + public ImmutableArray Interfaces { get; set; } + public ImmutableArray Attributes { get; set; } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs new file mode 100644 index 000000000..45d8fa34d --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs @@ -0,0 +1,74 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.XUnit; +public class XUnitCSharpTestFixtureClass : CSharpTestFixtureClass +{ + public XUnitCSharpTestFixtureClass( + QualifiedTypeIdentifier identifier, + string hintName, + FeatureInformation feature, + ImmutableArray attributes = default, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) + : base(identifier, hintName, feature, attributes, methods, renderingOptions) + { + } + + public XUnitCSharpTestFixtureClass( + TestFixtureDescriptor descriptor, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) + : base(descriptor, methods, renderingOptions) + { + } + + //protected override IEnumerable GetInterfaces() => + // base.GetInterfaces().Concat([$"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>"]); + + //protected override void AppendTestFixturePreamble() + //{ + // AppendLifetimeClass(); + + // AppendConstructor(); + + // base.AppendTestFixturePreamble(); + //} + + //protected virtual void AppendConstructor() + //{ + // // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. + // SourceBuilder.AppendLine($"public {GetClassName()}(Lifetime lifetime)"); + // SourceBuilder.BeginBlock("{"); + // SourceBuilder.AppendLine("Lifetime = lifetime;"); + // SourceBuilder.EndBlock("}"); + //} + + //protected virtual void AppendLifetimeClass() + //{ + // // This class represents the feature lifetime in the xUnit framework. + // SourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); + // SourceBuilder.BeginBlock("{"); + + // SourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); + + // SourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); + // SourceBuilder.BeginBlock("{"); + // // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. + // SourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); + // SourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); + // SourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); + // SourceBuilder.EndBlock("}"); + + // SourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); + // SourceBuilder.BeginBlock("{"); + // SourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); + // SourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); + // SourceBuilder.BeginBlock("TestRunner = null;"); + // SourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); + // SourceBuilder.EndBlock("}"); + + // SourceBuilder.EndBlock("}"); + // SourceBuilder.AppendLine(); + //} +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..79c2ebe82 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -0,0 +1,46 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using Reqnroll.FeatureSourceGenerator.XUnit; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.XUnit; + +internal class XUnitCSharpTestFixtureGenerator(XUnitHandler testFrameworkHandler) : + CSharpTestFixtureGenerator(testFrameworkHandler) +{ + protected override XUnitCSharpTestFixtureClass CreateTestFixtureClass( + TestFixtureGenerationContext context, + TestFixtureDescriptor descriptor, + ImmutableArray methods, + CSharpRenderingOptions renderingOptions) + { + return new XUnitCSharpTestFixtureClass( + descriptor, + methods.Cast().ToImmutableArray(), + renderingOptions); + } + + protected override XUnitCSharpTestMethod CreateTestMethod( + TestMethodGenerationContext context, + TestMethodDescriptor descriptor) + { + return new XUnitCSharpTestMethod(descriptor); + } + + protected override ImmutableArray GenerateTestMethodAttributes( + TestMethodGenerationContext context, + CancellationToken cancellationToken) + { + return ImmutableArray.Create( + new AttributeDescriptor( + XUnitHandler.XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Fact")))); + } + + protected override ImmutableArray GenerateTestFixtureInterfaces( + TestFixtureGenerationContext context, + QualifiedTypeIdentifier qualifiedClassName, + CancellationToken cancellationToken) + { + return ImmutableArray.Create( + XUnitHandler.XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("IClassFixture"))); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs new file mode 100644 index 000000000..9a12a55b3 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs @@ -0,0 +1,30 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.XUnit; +internal class XUnitCSharpTestMethod : CSharpTestMethod +{ + public XUnitCSharpTestMethod( + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray stepInvocations, + ImmutableArray attributes = default, + ImmutableArray parameters = default, + ImmutableArray> scenarioParameters = default) + : base(identifier, scenario, stepInvocations, attributes, parameters, scenarioParameters) + { + } + + public XUnitCSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor) + { + } + + protected override void RenderTestRunnerLookupTo( + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken) + { + // For xUnit test runners are scoped to the whole feature execution lifetime + sourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner;"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs index 506905aa7..264818521 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestSyntax.cs @@ -6,21 +6,21 @@ internal static class MSTestSyntax { public static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); public static AttributeDescriptor TestClassAttribute() => - new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))); + new(MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestClass"))); public static AttributeDescriptor TestMethodAttribute() => - new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))); + new(MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestMethod"))); public static AttributeDescriptor DescriptionAttribute(string description) => new( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), ImmutableArray.Create(description)); public static AttributeDescriptor TestPropertyAttribute(string propertyName, object? propertyValue) => new( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestProperty")), positionalArguments: ImmutableArray.Create(propertyName, propertyValue)); public static AttributeDescriptor DataRowAttribute(ImmutableArray values) => - new(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), values); + new(MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("DataRow")), values); } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs index a2c87e814..0c5fb0a18 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ArrayTypeIdentifier.cs @@ -1,10 +1,12 @@ -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; public class ArrayTypeIdentifier(TypeIdentifier itemType, bool isNullable = false) : - TypeIdentifier(isNullable), IEquatable + TypeIdentifier, IEquatable { public TypeIdentifier ItemType { get; } = itemType; + public override bool IsNullable { get; } = isNullable; + public bool Equals(ArrayTypeIdentifier? other) { if (other is null) @@ -17,27 +19,36 @@ public bool Equals(ArrayTypeIdentifier? other) return true; } - return base.Equals(other) && - ItemType.Equals(other.ItemType); + return ItemType.Equals(other.ItemType) && + IsNullable.Equals(other.IsNullable); } public override bool Equals(object obj) => Equals(obj as ArrayTypeIdentifier); - public override bool Equals(TypeIdentifier? other) => Equals(other as ArrayTypeIdentifier); - public override int GetHashCode() { unchecked { - var hash = base.GetHashCode(); + var hash = 36571313; - hash *= ItemType.GetHashCode(); + hash *= 82795997 + ItemType.GetHashCode(); + hash *= 82795997 + IsNullable.GetHashCode(); return hash; } } - public override string ToString() => $"{ItemType}[]"; + public override string ToString() + { + var str = $"{ItemType}[]"; + + if (IsNullable) + { + str += '?'; + } + + return str; + } public static bool Equals(ArrayTypeIdentifier? first, ArrayTypeIdentifier? second) { diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs index 3fd83c489..961403e32 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs @@ -11,12 +11,12 @@ namespace Reqnroll.FeatureSourceGenerator.SourceModel; /// The positional arguments of the attribute. /// The named arguments of the attribute. public class AttributeDescriptor( - NamedTypeIdentifier type, + QualifiedTypeIdentifier type, ImmutableArray? positionalArguments = null, ImmutableDictionary? namedArguments = null) : IEquatable { - public NamedTypeIdentifier Type { get; } = type; + public QualifiedTypeIdentifier Type { get; } = type; public ImmutableArray PositionalArguments { get; } = ThrowIfArgumentTypesNotValid( diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/CommonTypes.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/CommonTypes.cs index 23b07359e..4c378c1a2 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/CommonTypes.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/CommonTypes.cs @@ -1,6 +1,6 @@ namespace Reqnroll.FeatureSourceGenerator.SourceModel; internal static class CommonTypes { - public static readonly NamedTypeIdentifier String = - new(new NamespaceString("System"), new IdentifierString("String")); + public static readonly QualifiedTypeIdentifier String = + new(new NamespaceString("System"), new SimpleTypeIdentifier(new IdentifierString("String"))); } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/GenericTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/GenericTypeIdentifier.cs new file mode 100644 index 000000000..e45390797 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/GenericTypeIdentifier.cs @@ -0,0 +1,89 @@ +using System.Collections.Immutable; +using System.Text; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class GenericTypeIdentifier( + IdentifierString name, ImmutableArray + typeArguments, + bool isNullable = false) : LocalTypeIdentifier(isNullable), IEquatable +{ + public IdentifierString Name { get; } = + name.IsEmpty ? throw new ArgumentException("Value cannot be an empty identifier.", nameof(name)) : name; + + public ImmutableArray TypeArguments { get; } = + typeArguments.IsDefaultOrEmpty ? + throw new ArgumentException("Value cannot be an empty array.", nameof(typeArguments)) : + typeArguments; + + public override bool Equals(object obj) => Equals(obj as GenericTypeIdentifier); + + public bool Equals(GenericTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Name.Equals(other.Name) && + IsNullable.Equals(other.IsNullable) && + TypeArguments.SequenceEqual(other.TypeArguments); + } + + public static bool Equals(GenericTypeIdentifier? typeA, GenericTypeIdentifier? typeB) + { + if (ReferenceEquals(typeA, typeB)) + { + return true; + } + + if (typeA is null) + { + return false; + } + + return typeA.Equals(typeB); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 42650617; + + hash *= 57433771 + Name.GetHashCode(); + hash *= 57433771 + IsNullable.GetHashCode(); + hash *= 57433771 + TypeArguments.GetSequenceHashCode(); + + return hash; + } + } + + public override string ToString() + { + var sb = new StringBuilder(Name); + + sb.Append('<'); + + sb.Append(TypeArguments[0].ToString()); + + for (var i = 1; i < TypeArguments.Length; i++) + { + sb.Append(','); + sb.Append(TypeArguments[i].ToString()); + } + + sb.Append('>'); + + return sb.ToString(); + } + + public static bool operator ==(GenericTypeIdentifier? typeA, GenericTypeIdentifier? typeB) => Equals(typeA, typeB); + + public static bool operator !=(GenericTypeIdentifier? typeA, GenericTypeIdentifier? typeB) => !Equals(typeA, typeB); +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/LocalTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/LocalTypeIdentifier.cs new file mode 100644 index 000000000..f8a3e0165 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/LocalTypeIdentifier.cs @@ -0,0 +1,6 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public abstract class LocalTypeIdentifier(bool isNullable = false) : TypeIdentifier +{ + public override bool IsNullable { get; } = isNullable; +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs deleted file mode 100644 index 8defffb56..000000000 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/NamedTypeIdentifier.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Reqnroll.FeatureSourceGenerator.SourceModel; - -namespace Reqnroll.FeatureSourceGenerator; - -public class NamedTypeIdentifier : TypeIdentifier, IEquatable -{ - public NamedTypeIdentifier(IdentifierString localName) : this(NamespaceString.Empty, localName) - { - } - - public NamedTypeIdentifier(NamespaceString ns, IdentifierString localName, bool isNullable = false) : base(isNullable) - { - if (localName.IsEmpty && !ns.IsEmpty) - { - throw new ArgumentException( - "An empty local name cannot be combined with a non-empty namespace.", - nameof(localName)); - } - - LocalName = localName; - Namespace = ns; - } - - public IdentifierString LocalName { get; } - - public NamespaceString Namespace { get; } - - public override string? ToString() => Namespace.IsEmpty ? LocalName.ToString() : $"{Namespace}.{LocalName}"; - - public override bool Equals(object obj) => Equals(obj as NamedTypeIdentifier); - - public override bool Equals(TypeIdentifier? other) => Equals(other as NamedTypeIdentifier); - - public bool Equals(NamedTypeIdentifier? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return base.Equals(other) && - Namespace.Equals(other.Namespace) && - LocalName.Equals(other.LocalName); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode *= 73812971 + LocalName.GetHashCode(); - hashCode *= 73812971 + Namespace.GetHashCode(); - return hashCode; - } - } - - public static bool Equals(NamedTypeIdentifier? first, NamedTypeIdentifier? second) - { - if (ReferenceEquals(first, second)) - { - return true; - } - - if (first is null) - { - return false; - } - - return first.Equals(second); - } - - public static bool operator ==(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => Equals(first, second); - - public static bool operator !=(NamedTypeIdentifier? first, NamedTypeIdentifier? second) => !Equals(first, second); -} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/NamespaceString.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/NamespaceString.cs index 04661e0cd..3e3165868 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/NamespaceString.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/NamespaceString.cs @@ -101,5 +101,20 @@ public bool Equals(NamespaceString other) public static bool operator !=(NamespaceString ns, string s) => !Equals(ns, s); + public static QualifiedTypeIdentifier operator +(NamespaceString ns, LocalTypeIdentifier localType) + { + if (ns.IsEmpty) + { + throw new ArgumentException("Cannot qualify a type with an empty namespace.", nameof(ns)); + } + + if (localType is null) + { + throw new ArgumentNullException(nameof(localType)); + } + + return new QualifiedTypeIdentifier(ns, localType); + } + public static implicit operator string(NamespaceString ns) => ns.ToString(); } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/NestedTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/NestedTypeIdentifier.cs new file mode 100644 index 000000000..3cd531f14 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/NestedTypeIdentifier.cs @@ -0,0 +1,42 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class NestedTypeIdentifier(TypeIdentifier encapsulatingType, LocalTypeIdentifier localType) : + TypeIdentifier, IEquatable +{ + public TypeIdentifier EncapsulatingType { get; } = encapsulatingType; + + public LocalTypeIdentifier LocalType { get; } = localType; + + public override bool IsNullable => LocalType.IsNullable; + + public bool Equals(NestedTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return EncapsulatingType.Equals(other.EncapsulatingType) && + LocalType.Equals(other.LocalType); + } + + public override bool Equals(object obj) => Equals(obj as NestedTypeIdentifier); + + public override int GetHashCode() + { + unchecked + { + var hash = 69067723; + + hash *= 81073471 + EncapsulatingType.GetHashCode(); + hash *= 81073471 + LocalType.GetHashCode(); + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/QualifiedTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/QualifiedTypeIdentifier.cs new file mode 100644 index 000000000..2a955a7af --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/QualifiedTypeIdentifier.cs @@ -0,0 +1,66 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class QualifiedTypeIdentifier(NamespaceString ns, LocalTypeIdentifier localType) : + TypeIdentifier, IEquatable +{ + public NamespaceString Namespace { get; } = + ns.IsEmpty ? throw new ArgumentException("Value cannot be an empty namespace.", nameof(ns)) : ns; + + public LocalTypeIdentifier LocalType { get; } = localType; + + public override bool IsNullable => LocalType.IsNullable; + + public bool Equals(QualifiedTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Namespace.Equals(other.Namespace) && + LocalType.Equals(other.LocalType) && + IsNullable.Equals(other.IsNullable); + } + + public override bool Equals(object obj) => Equals(obj as QualifiedTypeIdentifier); + + public static bool Equals(QualifiedTypeIdentifier? typeA, QualifiedTypeIdentifier? typeB) + { + if (ReferenceEquals(typeA, typeB)) + { + return true; + } + + if (typeA is null) + { + return false; + } + + return typeA.Equals(typeB); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 20462011; + + hash *= 50825449 + Namespace.GetHashCode(); + hash *= 50825449 + LocalType.GetHashCode(); + hash *= 50825449 + IsNullable.GetHashCode(); + + return hash; + } + } + + public override string ToString() => $"{Namespace}.{LocalType}"; + + public static bool operator ==(QualifiedTypeIdentifier? typeA, QualifiedTypeIdentifier? typeB) => Equals(typeA, typeB); + + public static bool operator !=(QualifiedTypeIdentifier? typeA, QualifiedTypeIdentifier? typeB) => !Equals(typeA, typeB); +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/SimpleTypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/SimpleTypeIdentifier.cs new file mode 100644 index 000000000..09be568f1 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/SimpleTypeIdentifier.cs @@ -0,0 +1,70 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class SimpleTypeIdentifier(IdentifierString name, bool isNullable = false) : + LocalTypeIdentifier(isNullable), IEquatable +{ + public IdentifierString Name { get; } = + name.IsEmpty ? throw new ArgumentException("Value cannot be an empty identifier.", nameof(name)) : name; + + public override bool Equals(object obj) => Equals(obj as SimpleTypeIdentifier); + + public bool Equals(SimpleTypeIdentifier? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Name.Equals(other.Name) && + IsNullable.Equals(other.IsNullable); + } + + public static bool Equals(SimpleTypeIdentifier? typeA, SimpleTypeIdentifier? typeB) + { + if (ReferenceEquals(typeA, typeB)) + { + return true; + } + + if (typeA is null) + { + return false; + } + + return typeA.Equals(typeB); + } + + public override int GetHashCode() + { + unchecked + { + var hash = 62786819; + + hash *= 41806399 + Name.GetHashCode(); + hash *= 41806399 + IsNullable.GetHashCode(); + + return hash; + } + } + + public override string ToString() + { + if (IsNullable) + { + return Name + '?'; + } + else + { + return Name; + } + } + + public static bool operator ==(SimpleTypeIdentifier? typeA, SimpleTypeIdentifier? typeB) => Equals(typeA, typeB); + + public static bool operator !=(SimpleTypeIdentifier? typeA, SimpleTypeIdentifier? typeB) => !Equals(typeA, typeB); +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs index 2854d0d86..ae000deb5 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs @@ -16,9 +16,9 @@ public abstract class TestFixtureClass : IEquatable, IHasAttr /// a virtual path and virtual filename that makes sense within the context of a project. The value must be unique /// within the compilation. /// The feature information that will be included in the test fixture. - /// The attributes which are applied to the feature. + /// The attributes which are applied to the class. protected TestFixtureClass( - NamedTypeIdentifier identifier, + QualifiedTypeIdentifier identifier, string hintName, FeatureInformation featureInformation, ImmutableArray attributes = default) @@ -48,15 +48,20 @@ protected TestFixtureClass(TestFixtureDescriptor descriptor) : /// /// Gets the identifier of the class. /// - public NamedTypeIdentifier Identifier { get; } + public QualifiedTypeIdentifier Identifier { get; } /// - /// Gets the attributes which are applied to the fixture. + /// Gets the interfaces which are implemented by the class. + /// + public virtual ImmutableArray Interfaces => ImmutableArray.Empty; + + /// + /// Gets the attributes which are applied to the class. /// public ImmutableArray Attributes { get; } /// - /// Gets the hint name associated with the test fixture. + /// Gets the hint name associated with the test class. /// public string HintName { get; } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs index d94112d58..42027d299 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TypeIdentifier.cs @@ -1,44 +1,6 @@ -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.SourceModel; -public abstract class TypeIdentifier(bool isNullable) : IEquatable +public abstract class TypeIdentifier { - public bool IsNullable { get; } = isNullable; - - public virtual bool Equals(TypeIdentifier? other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return IsNullable.Equals(other.IsNullable); - } - - public override bool Equals(object obj) => Equals(obj as TypeIdentifier); - - public override int GetHashCode() => IsNullable.GetHashCode(); - - public static bool Equals(TypeIdentifier? first, TypeIdentifier? second) - { - if (ReferenceEquals(first, second)) - { - return true; - } - - if (first is null) - { - return false; - } - - return first.Equals(second); - } - - public static bool operator ==(TypeIdentifier? first, TypeIdentifier? second) => Equals(first, second); - - public static bool operator !=(TypeIdentifier? first, TypeIdentifier? second) => !Equals(first, second); + public abstract bool IsNullable { get; } } diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs deleted file mode 100644 index 15b5ca2bb..000000000 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpSyntaxGeneration.cs +++ /dev/null @@ -1,74 +0,0 @@ -//using Gherkin.Ast; -//using Reqnroll.FeatureSourceGenerator.CSharp; -//using Reqnroll.FeatureSourceGenerator.SourceModel; - -//namespace Reqnroll.FeatureSourceGenerator.XUnit; -//public class XUnitCSharpSyntaxGeneration(TestFixtureGenerationContext featureInfo) : CSharpTestFixtureGeneration(featureInfo) -//{ -// private readonly NamespaceString XUnitNamespace = new("Xunit"); - -// protected override IEnumerable GetTestMethodAttributes(Scenario scenario) -// { -// var attributes = new List -// { -// new(new NamedTypeIdentifier(XUnitNamespace, new IdentifierString("Fact"))) -// }; - -// return base.GetTestMethodAttributes(scenario).Concat(attributes); -// } - -// protected override IEnumerable GetInterfaces() => -// base.GetInterfaces().Concat([ $"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>" ]); - -// protected override void AppendTestFixturePreamble() -// { -// AppendLifetimeClass(); - -// AppendConstructor(); - -// base.AppendTestFixturePreamble(); -// } - -// protected virtual void AppendConstructor() -// { -// // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. -// SourceBuilder.AppendLine($"public {GetClassName()}(Lifetime lifetime)"); -// SourceBuilder.BeginBlock("{"); -// SourceBuilder.AppendLine("Lifetime = lifetime;"); -// SourceBuilder.EndBlock("}"); -// } - -// protected virtual void AppendLifetimeClass() -// { -// // This class represents the feature lifetime in the xUnit framework. -// SourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); -// SourceBuilder.BeginBlock("{"); - -// SourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); - -// SourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); -// SourceBuilder.BeginBlock("{"); -// // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. -// SourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); -// SourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); -// SourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); -// SourceBuilder.EndBlock("}"); - -// SourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); -// SourceBuilder.BeginBlock("{"); -// SourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); -// SourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); -// SourceBuilder.BeginBlock("TestRunner = null;"); -// SourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); -// SourceBuilder.EndBlock("}"); - -// SourceBuilder.EndBlock("}"); -// SourceBuilder.AppendLine(); -// } - -// protected override void AppendTestRunnerLookupForScenario(Scenario scenario) -// { -// // For xUnit test runners are scoped to the whole feature execution lifetime -// SourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner;"); -// } -//} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs deleted file mode 100644 index c29079556..000000000 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitCSharpTestFixtureGenerator.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; -using Reqnroll.FeatureSourceGenerator.SourceModel; -using System.Collections.Immutable; - -namespace Reqnroll.FeatureSourceGenerator.XUnit; - -internal class XUnitCSharpTestFixtureGenerator(XUnitHandler testFrameworkHandler) : - CSharpTestFixtureGenerator(testFrameworkHandler) -{ - protected override CSharpTestFixtureClass CreateTestFixtureClass( - TestFixtureGenerationContext context, - TestFixtureDescriptor descriptor, - ImmutableArray methods, - CSharpRenderingOptions renderingOptions) - { - throw new NotImplementedException(); - } - - protected override CSharpTestMethod CreateTestMethod( - TestMethodGenerationContext context, - TestMethodDescriptor descriptor) - { - throw new NotImplementedException(); - } - - protected override ImmutableArray GenerateTestFixtureClassAttributes( - TestFixtureGenerationContext context, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override ImmutableArray GenerateTestMethodAttributes( - TestMethodGenerationContext context, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } -} diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs index 85eadffb8..47d511a6d 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -1,4 +1,5 @@ using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp.XUnit; namespace Reqnroll.FeatureSourceGenerator.XUnit; @@ -7,9 +8,11 @@ namespace Reqnroll.FeatureSourceGenerator.XUnit; /// public class XUnitHandler : ITestFrameworkHandler { + internal static readonly NamespaceString XUnitNamespace = new("Xunit"); + public string TestFrameworkName => "xUnit"; - public ITestFixtureGenerator? GetTestFixtureGenerator() + public ITestFixtureGenerator? GetTestFixtureGenerator() where TCompilationInformation : CompilationInformation { if (typeof(TCompilationInformation).IsAssignableFrom(typeof(CSharpCompilationInformation))) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs index 6e09a12c9..a22a0f696 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs @@ -9,36 +9,36 @@ public class AttributeDescriptorTests public static IEnumerable StringRepresentationExamples { get; } = [ [ - new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))), + new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar"))), "[Foo.Bar]" ], [ new AttributeDescriptor( - new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ "Fizz" ]), "[Foo.Bar(\"Fizz\")]" ], [ new AttributeDescriptor( - new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ "Fizz", "Buzz" ]), "[Foo.Bar(\"Fizz\", \"Buzz\")]" ], [ new AttributeDescriptor( - new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ 1, 2 ]), "[Foo.Bar(1, 2)]" ], [ new AttributeDescriptor( - new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ ImmutableArray.Create() ]), "[Foo.Bar(new string[] {})]" ], [ new AttributeDescriptor( - new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), + new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ ImmutableArray.Create("potato", "pancakes") ]), "[Foo.Bar(new string[] {\"potato\", \"pancakes\"})]" ] @@ -53,12 +53,12 @@ public void DescriptorsCanBeRepresntedAsStrings(AttributeDescriptor attribute, s public static IEnumerable DescriptorExamples { get; } = [ - [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) ], - [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz" ]) ], - [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ "Fizz", "Buzz" ]) ], - [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ 1, 2 ]) ], - [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create() ]) ], - [ () => new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar")), [ ImmutableArray.Create("potato") ]) ] + [ () => new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar"))) ], + [ () => new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ "Fizz" ]) ], + [ () => new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ "Fizz", "Buzz" ]) ], + [ () => new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ 1, 2 ]) ], + [ () => new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ ImmutableArray.Create() ]) ], + [ () => new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar")), [ ImmutableArray.Create("potato") ]) ] ]; [Theory] @@ -90,7 +90,7 @@ public void DescriptorsAreEqualWhenTypeNamespaceAndArgumentsAreEquivalent(Func(T va { var argument = ImmutableArray.Create(value, value); - var attribute = new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) + var attribute = new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar"))) .WithPositionalArguments(argument) .WithNamedArguments(new { Property = argument }); @@ -165,7 +165,7 @@ public void DescriptorsCanBeCreatedWithImmutableArraysOfEnumsAsArguments(T va [InlineData(typeof(AttributeDescriptorTests))] public void DescriptorsCanBeCreatedWithTypesAsArguments(Type argument) { - var attribute = new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))) + var attribute = new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar"))) .WithPositionalArguments(argument) .WithNamedArguments(new { Property = argument }); @@ -180,7 +180,7 @@ public void DescriptorsCannotBeCreatedWithArraysAsArguments() { var argument = Array.Empty(); - var attribute = new AttributeDescriptor(new NamedTypeIdentifier(new NamespaceString("Foo"), new IdentifierString("Bar"))); + var attribute = new AttributeDescriptor(new NamespaceString("Foo") + new SimpleTypeIdentifier(new IdentifierString("Bar"))); attribute .Invoking(attribute => attribute.WithPositionalArguments([ argument ])) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs index fc348b99a..19cb38769 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs @@ -37,7 +37,7 @@ public CSharpTestFixtureSourceGeneratorTests() It.IsAny>(), It.IsAny())) .Returns(new CSharpTestFixtureClass( - new NamedTypeIdentifier(new IdentifierString("Mock")), + new NamespaceString("Test") + new SimpleTypeIdentifier(new IdentifierString("Mock")), "Mock.feature", new FeatureInformation("Mock", null, "en"))); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs index be7e35097..198777a1b 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs @@ -250,12 +250,12 @@ public static AttributeDescriptor GetAttributeDescriptor(AttributeSyntax attribu { var type = attribute.Name switch { - QualifiedNameSyntax qname => new NamedTypeIdentifier( + QualifiedNameSyntax qname => new QualifiedTypeIdentifier( new NamespaceString( qname.Left.ToString().StartsWith("global::") ? qname.Left.ToString()[8..] : qname.Left.ToString()), - new IdentifierString(qname.Right.ToString())), + new SimpleTypeIdentifier(new IdentifierString(qname.Right.ToString()))), _ => throw new NotImplementedException() }; diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs index 226c61ca5..be91453ed 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -58,7 +58,7 @@ public void GenerateTestFixture_CreatesClassForFeatureWithMsTestAttributes() testFixture.Should().HaveAttribuesEquivalentTo( [ - new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestClass"))), + new AttributeDescriptor(MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestClass"))), ]); } @@ -94,12 +94,12 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp method.Should().HaveAttribuesEquivalentTo( [ - new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), + new AttributeDescriptor(MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestMethod"))), new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), ["Sample Scenario"]), new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestProperty")), positionalArguments: ["FeatureTitle", "Sample"]) ]); @@ -147,21 +147,21 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen method.Should().HaveAttribuesEquivalentTo( [ - new AttributeDescriptor(new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestMethod"))), + new AttributeDescriptor(MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestMethod"))), new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("Description")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), ["Sample Scenario Outline"]), new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("TestProperty")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("TestProperty")), positionalArguments: ["FeatureTitle", "Sample"]), new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("DataRow")), ["foo", ImmutableArray.Create(ImmutableArray.Create("example_tag"))]), new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("DataRow")), ["bar", ImmutableArray.Create(ImmutableArray.Create("example_tag"))]), new AttributeDescriptor( - new NamedTypeIdentifier(MSTestNamespace, new IdentifierString("DataRow")), + MSTestNamespace + new SimpleTypeIdentifier(new IdentifierString("DataRow")), ["baz", ImmutableArray.Create(ImmutableArray.Empty)]) ]); @@ -169,11 +169,11 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen [ new ParameterDescriptor( new IdentifierString("what"), - new NamedTypeIdentifier(new NamespaceString("System"), new IdentifierString("String"))), + new NamespaceString("System") + new SimpleTypeIdentifier(new IdentifierString("String"))), new ParameterDescriptor( new IdentifierString("_exampleTags"), - new ArrayTypeIdentifier(new NamedTypeIdentifier(new NamespaceString("System"), new IdentifierString("String")))) + new ArrayTypeIdentifier(new NamespaceString("System") + new SimpleTypeIdentifier(new IdentifierString("String")))) ]); method.StepInvocations.Should().BeEquivalentTo( diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs index 59e4f7a43..919c58994 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs @@ -118,7 +118,7 @@ internal static class MSTestSyntax public static AttributeDescriptor Attribute(string type, params object?[] args) { return new AttributeDescriptor( - new NamedTypeIdentifier(Namespace, new IdentifierString(type)), + Namespace + new SimpleTypeIdentifier(new IdentifierString(type)), [.. args]); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ArrayTypeIdentifierTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ArrayTypeIdentifierTests.cs new file mode 100644 index 000000000..2dd77a68c --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ArrayTypeIdentifierTests.cs @@ -0,0 +1,99 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class ArrayTypeIdentifierTests +{ + [Theory] + [InlineData("Parser", true, false)] + [InlineData("__Parser", false, false)] + [InlineData("X509", true, true)] + public void Constructor_CreatesArrayTypeFromType(string localName, bool itemIsNullable, bool arrayIsNullable) + { + var itemType = new SimpleTypeIdentifier(new IdentifierString(localName), itemIsNullable); + + var identifier = new ArrayTypeIdentifier(itemType, arrayIsNullable); + + identifier.ItemType.Should().Be(itemType); + identifier.IsNullable.Should().Be(arrayIsNullable); + } + + [Theory] + [InlineData("Parser", true, "Parser", true, true)] + [InlineData("_Parser", false, "_Parser", false, true)] + [InlineData("Parser", false, "Parser", true, false)] + [InlineData("Parser", true, "_Parser", false, false)] + public void Equals_ReturnsTrueWhenItemTypeAndNullabilityMatches( + string nameA, + bool arrayAIsNullable, + string nameB, + bool arrayBIsNullable, + bool expected) + { + var typeIdA = new SimpleTypeIdentifier(new IdentifierString(nameA)); + var typeIdB = new SimpleTypeIdentifier(new IdentifierString(nameB)); + + var arrayTypeA = new ArrayTypeIdentifier(typeIdA, arrayAIsNullable); + var arrayTypeB = new ArrayTypeIdentifier(typeIdB, arrayBIsNullable); + + arrayTypeA.Equals(arrayTypeB).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", true, "Parser", true)] + [InlineData("_Parser", false, "_Parser", false)] + public void GetHashCode_ReturnsSameValueForEquivalentValues( + string nameA, + bool arrayAIsNullable, + string nameB, + bool arrayBIsNullable) + { + var typeIdA = new SimpleTypeIdentifier(new IdentifierString(nameA)); + var typeIdB = new SimpleTypeIdentifier(new IdentifierString(nameB)); + + var arrayTypeA = new ArrayTypeIdentifier(typeIdA, arrayAIsNullable); + var arrayTypeB = new ArrayTypeIdentifier(typeIdB, arrayBIsNullable); + + arrayTypeA.GetHashCode().Should().Be(arrayTypeB.GetHashCode()); + } + + [Theory] + [InlineData("Parser", true, "Parser", true, true)] + [InlineData("_Parser", false, "_Parser", false, true)] + [InlineData("Parser", false, "Parser", true, false)] + [InlineData("Parser", true, "_Parser", false, false)] + public void EqualityOperatorWithArrayTypeIdentifier_ReturnsEquivalenceBasedOnItemTypeAndNullability( + string nameA, + bool arrayAIsNullable, + string nameB, + bool arrayBIsNullable, + bool expected) + { + var typeIdA = new SimpleTypeIdentifier(new IdentifierString(nameA)); + var typeIdB = new SimpleTypeIdentifier(new IdentifierString(nameB)); + + var arrayTypeA = new ArrayTypeIdentifier(typeIdA, arrayAIsNullable); + var arrayTypeB = new ArrayTypeIdentifier(typeIdB, arrayBIsNullable); + + (arrayTypeA == arrayTypeB).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", true, "Parser", true, false)] + [InlineData("_Parser", false, "_Parser", false, false)] + [InlineData("Parser", false, "Parser", true, true)] + [InlineData("Parser", true, "_Parser", false, true)] + public void InequalityOperatorWithArrayTypeIdentifier_ReturnsNonEquivalenceBasedOnItemTypeAndNullability( + string nameA, + bool arrayAIsNullable, + string nameB, + bool arrayBIsNullable, + bool expected) + { + var typeIdA = new SimpleTypeIdentifier(new IdentifierString(nameA)); + var typeIdB = new SimpleTypeIdentifier(new IdentifierString(nameB)); + + var arrayTypeA = new ArrayTypeIdentifier(typeIdA, arrayAIsNullable); + var arrayTypeB = new ArrayTypeIdentifier(typeIdB, arrayBIsNullable); + + (arrayTypeA != arrayTypeB).Should().Be(expected); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/GenericTypeIdentifierTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/GenericTypeIdentifierTests.cs new file mode 100644 index 000000000..13ea06a2e --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/GenericTypeIdentifierTests.cs @@ -0,0 +1,148 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class GenericTypeIdentifierTests +{ + [Theory] + [InlineData("Parser")] + [InlineData("__Parser")] + [InlineData("X509")] + public void Constructor_CreatesGenericTypeIdentifierFromValidName(string name) + { + var nameIdentifier = new IdentifierString(name); + + var identifier = new GenericTypeIdentifier(nameIdentifier, [ new SimpleTypeIdentifier(new IdentifierString("string")) ]); + + identifier.Name.Should().Be(nameIdentifier); + identifier.IsNullable.Should().BeFalse(); + identifier.TypeArguments.Should().BeEquivalentTo([ new SimpleTypeIdentifier(new IdentifierString("string")) ]); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyName(string name) + { + Func ctr = () => new GenericTypeIdentifier( + new IdentifierString(name), + [new SimpleTypeIdentifier(new IdentifierString("string"))]); + + ctr.Should().Throw(); + } + + [Fact] + public void Constructor_ThrowsArgumentExceptionWhenNoTypeArgumentsAreSpecified() + { + Func ctr = () => new GenericTypeIdentifier(new IdentifierString("Parser"), []); + + ctr.Should().Throw(); + } + + [Theory] + [InlineData("Parser", "Parser")] + [InlineData("_Parser", "_Parser")] + public void EqualsGenericTypeIdentifier_ReturnsTrueWhenNameAndGenericTypeArgumentsMatch( + string name1, + string name2) + { + var typeId1 = new GenericTypeIdentifier(new IdentifierString(name1), [new SimpleTypeIdentifier(new IdentifierString("string"))]); + var typeId2 = new GenericTypeIdentifier(new IdentifierString(name2), [new SimpleTypeIdentifier(new IdentifierString("string"))]); + + typeId1.Equals(typeId2).Should().BeTrue(); + } + + [Theory] + [InlineData("Parser", "parser")] + [InlineData("Parser", "_Parser")] + public void EqualsGenericTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( + string name1, + string name2) + { + var typeId1 = new GenericTypeIdentifier( + new IdentifierString(name1), + [new SimpleTypeIdentifier(new IdentifierString("string"))]); + + var typeId2 = new GenericTypeIdentifier( + new IdentifierString(name2), + [new SimpleTypeIdentifier(new IdentifierString("string"))]); + + typeId1.Equals(typeId2).Should().BeFalse(); + } + + [Fact] + public void EqualsGenericTypeIdentifier_ReturnsFalseTypeArgumentsDoNotMatch() + { + var typeId1 = new GenericTypeIdentifier( + new IdentifierString("List"), + [new SimpleTypeIdentifier(new IdentifierString("string"))]); + + var typeId2 = new GenericTypeIdentifier( + new IdentifierString("List"), + [new SimpleTypeIdentifier(new IdentifierString("int"))]); + + typeId1.Equals(typeId2).Should().BeFalse(); + } + + [Theory] + [InlineData("Parser", "Parser")] + [InlineData("Internal", "Internal")] + [InlineData("_1XYZ", "_1XYZ")] + [InlineData("__Internal", "__Internal")] + public void GetHashCode_ReturnsSameValueForEquivalentValues( + string name1, + string name2) + { + var typeId1 = new GenericTypeIdentifier( + new IdentifierString(name1), + [new SimpleTypeIdentifier(new IdentifierString("int"))]); + + var typeId2 = new GenericTypeIdentifier( + new IdentifierString(name2), + [new SimpleTypeIdentifier(new IdentifierString("int"))]); + + typeId1.GetHashCode().Should().Be(typeId2.GetHashCode()); + } + + [Theory] + [InlineData("Parser", "Parser", true)] + [InlineData("_Parser", "_Parser", true)] + [InlineData("_Parser", "_parser", false)] + [InlineData("Parser", "parser", false)] + [InlineData("Parser", "_Parser", false)] + public void EqualityOperatorWithTypeIdentifier_ReturnsEquivalence( + string name1, + string name2, + bool expected) + { + var typeId1 = new GenericTypeIdentifier( + new IdentifierString(name1), + [new SimpleTypeIdentifier(new IdentifierString("int"))]); + + var typeId2 = new GenericTypeIdentifier( + new IdentifierString(name2), + [new SimpleTypeIdentifier(new IdentifierString("int"))]); + + (typeId1 == typeId2).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", "Parser", false)] + [InlineData("_Parser", "_Parser", false)] + [InlineData("_Parser", "_parser", true)] + [InlineData("Parser", "parser", true)] + [InlineData("Parser", "_Parser", true)] + public void InequalityOperatorWithTypeIdentifier_ReturnsNonEquivalence( + string name1, + string name2, + bool expected) + { + var typeId1 = new GenericTypeIdentifier( + new IdentifierString(name1), + [new SimpleTypeIdentifier(new IdentifierString("int"))]); + + var typeId2 = new GenericTypeIdentifier( + new IdentifierString(name2), + [new SimpleTypeIdentifier(new IdentifierString("int"))]); + + (typeId1 != typeId2).Should().Be(expected); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ParameterDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ParameterDescriptorTests.cs index 6f51fd5ab..c36edfbd2 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ParameterDescriptorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/ParameterDescriptorTests.cs @@ -7,7 +7,7 @@ public class ParameterDescriptorTests public void Constructor_ThrowsArgumentExceptionWhenNameIsEmpty() { Func ctr = () => - new ParameterDescriptor(IdentifierString.Empty, new NamedTypeIdentifier(new IdentifierString("string"))); + new ParameterDescriptor(IdentifierString.Empty, new SimpleTypeIdentifier(new IdentifierString("string"))); ctr.Should().Throw(); } @@ -16,7 +16,7 @@ public void Constructor_ThrowsArgumentExceptionWhenNameIsEmpty() public void Constructor_InitializesProperties() { var name = new IdentifierString("potato"); - var type = new NamedTypeIdentifier(new IdentifierString("string")); + var type = new SimpleTypeIdentifier(new IdentifierString("string")); var descriptor = new ParameterDescriptor(name, type); @@ -28,25 +28,22 @@ public void Constructor_InitializesProperties() [InlineData("potato", "System", "String")] [InlineData("foo", "System", "Int32")] [InlineData("bar", "System", "Int64")] - [InlineData("fiz", "", "Buzz")] public void GetHashCode_ReturnsSameValueForEquivalentValues(string name, string typeNamespace, string typeName) { var descriptorA = new ParameterDescriptor( new IdentifierString(name), - new NamedTypeIdentifier(new NamespaceString(typeNamespace), new IdentifierString(typeName))); + new NamespaceString(typeNamespace) + new SimpleTypeIdentifier(new IdentifierString(typeName))); var descriptorB = new ParameterDescriptor( new IdentifierString(name), - new NamedTypeIdentifier(new NamespaceString(typeNamespace), new IdentifierString(typeName))); + new NamespaceString(typeNamespace) + new SimpleTypeIdentifier(new IdentifierString(typeName))); descriptorA.GetHashCode().Should().Be(descriptorB.GetHashCode()); } [Theory] [InlineData("potato", "System", "String", "potato", "System", "String", true)] - [InlineData("potato", "System", "String", "potato", "", "String", false)] [InlineData("potato", "System", "String", "foo", "System", "String", false)] - [InlineData("potato", "System", "String", "foo", "", "String", false)] public void Equals_ReturnsEqualityBasedOnMatchingPropertyValues( string nameA, string typeNamespaceA, @@ -58,11 +55,11 @@ public void Equals_ReturnsEqualityBasedOnMatchingPropertyValues( { var descriptorA = new ParameterDescriptor( new IdentifierString(nameA), - new NamedTypeIdentifier(new NamespaceString(typeNamespaceA), new IdentifierString(typeNameA))); + new NamespaceString(typeNamespaceA) + new SimpleTypeIdentifier(new IdentifierString(typeNameA))); var descriptorB = new ParameterDescriptor( new IdentifierString(nameB), - new NamedTypeIdentifier(new NamespaceString(typeNamespaceB), new IdentifierString(typeNameB))); + new NamespaceString(typeNamespaceB) + new SimpleTypeIdentifier(new IdentifierString(typeNameB))); descriptorA.Equals(descriptorB).Should().Be(expected); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamedTypeIdentifierTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/QualifiedTypeIdentifierTests.cs similarity index 51% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamedTypeIdentifierTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/QualifiedTypeIdentifierTests.cs index 6cfd31ad7..dd292ec9e 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/NamedTypeIdentifierTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/QualifiedTypeIdentifierTests.cs @@ -2,66 +2,43 @@ namespace Reqnroll.FeatureSourceGenerator.SourceModel; -public class NamedTypeIdentifierTests +public class QualifiedTypeIdentifierTests { - [Theory] - [InlineData("Parser")] - [InlineData("__Parser")] - [InlineData("X509")] - public void Constructor_CreatesNamedTypeIdentifierFromValidName(string name) - { - var identifier = new NamedTypeIdentifier(new IdentifierString(name)); - - identifier.LocalName.Should().Be(name); - identifier.Namespace.IsEmpty.Should().BeTrue(); - identifier.ToString().Should().Be(name); - } - [Theory] [InlineData("Reqnroll", "Parser")] [InlineData("Reqnroll", "__Parser")] [InlineData("Reqnroll", "X509")] - public void Constructor_CreatesNamedTypeIdentifierFromValidNameAndNamespace(string ns, string name) + public void Constructor_CreatesQualifiedTypeIdentifierFromValidNameAndNamespace(string ns, string name) { var nsx = new NamespaceString(ns); + var localType = new SimpleTypeIdentifier(new IdentifierString(name)); - var identifier = new NamedTypeIdentifier(nsx, new IdentifierString(name)); + var identifier = new QualifiedTypeIdentifier(nsx, localType); - identifier.LocalName.Should().Be(name); + identifier.LocalType.Should().Be(localType); identifier.Namespace.Should().Be(nsx); - identifier.ToString().Should().Be($"{ns}.{name}"); } [Theory] - [InlineData("")] - [InlineData(null)] - public void Constructor_CreatesEmptyTypeIdentifierFromEmptyNameValue(string name) + [InlineData("Reqnroll", "Parser", "Reqnroll.Parser")] + [InlineData("Reqnroll", "__Parser", "Reqnroll.__Parser")] + [InlineData("Reqnroll", "X509", "Reqnroll.X509")] + public void ToString_ReturnsNamespaceAndLocalTypeSeparatedByADot(string ns, string name, string expected) { - var identifier = new NamedTypeIdentifier(new IdentifierString(name)); + var nsx = new NamespaceString(ns); + var localType = new SimpleTypeIdentifier(new IdentifierString(name)); + var identifier = new QualifiedTypeIdentifier(nsx, localType); - identifier.LocalName.IsEmpty.Should().BeTrue(); - identifier.Namespace.IsEmpty.Should().BeTrue(); - identifier.ToString().Should().Be(""); + identifier.ToString().Should().Be(expected); } [Theory] [InlineData("")] [InlineData(null)] - public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyName(string name) - { - Func ctr = () => new NamedTypeIdentifier(new NamespaceString("Reqnroll"), new IdentifierString(name)); - - ctr.Should().Throw(); - } - - [Theory] - [InlineData(".FeatureSourceGenerator")] - [InlineData("FeatureSourceGenerator.")] - [InlineData("Reqnroll.FeatureSourceGenerator")] - [InlineData("1FeatureSourceGenerator")] - public void Constructor_ThrowsArgumentExceptionWhenUsingAnInvalidLocalNameValue(string name) + public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyNamespace(string ns) { - Func ctr = () => new NamedTypeIdentifier(new IdentifierString(name)); + var localType = new SimpleTypeIdentifier(new IdentifierString("Parser")); + Func ctr = () => new QualifiedTypeIdentifier(new NamespaceString(ns), localType); ctr.Should().Throw(); } @@ -69,14 +46,14 @@ public void Constructor_ThrowsArgumentExceptionWhenUsingAnInvalidLocalNameValue( [Theory] [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser")] [InlineData("Reqnroll", "_Parser", "Reqnroll", "_Parser")] - public void EqualsTypeIdentifier_ReturnsTrueWhenNamespacesAndLocalNameMatches( + public void EqualsTypeIdentifier_ReturnsTrueWhenNamespacesAndLocalTypeMatches( string ns1, string name1, string ns2, string name2) { - var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new QualifiedTypeIdentifier(new NamespaceString(ns1), new SimpleTypeIdentifier(new IdentifierString(name1))); + var typeId2 = new QualifiedTypeIdentifier(new NamespaceString(ns2), new SimpleTypeIdentifier(new IdentifierString(name2))); typeId1.Equals(typeId2).Should().BeTrue(); } @@ -90,8 +67,8 @@ public void EqualsTypeIdentifier_ReturnsFalseWhenNamespaceDoesNotMatch( string ns2, string name2) { - var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new QualifiedTypeIdentifier(new NamespaceString(ns1), new SimpleTypeIdentifier(new IdentifierString(name1))); + var typeId2 = new QualifiedTypeIdentifier(new NamespaceString(ns2), new SimpleTypeIdentifier(new IdentifierString(name2))); typeId1.Equals(typeId2).Should().BeFalse(); } @@ -99,14 +76,14 @@ public void EqualsTypeIdentifier_ReturnsFalseWhenNamespaceDoesNotMatch( [Theory] [InlineData("Reqnroll", "Parser", "Reqnroll", "parser")] [InlineData("Reqnroll", "Parser", "Reqnroll", "_Parser")] - public void EqualsTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( + public void EqualsTypeIdentifier_ReturnsFalseWhenLocalTypeDoesNotMatch( string ns1, string name1, string ns2, string name2) { - var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new QualifiedTypeIdentifier(new NamespaceString(ns1), new SimpleTypeIdentifier(new IdentifierString(name1))); + var typeId2 = new QualifiedTypeIdentifier(new NamespaceString(ns2), new SimpleTypeIdentifier(new IdentifierString(name2))); typeId1.Equals(typeId2).Should().BeFalse(); } @@ -114,18 +91,14 @@ public void EqualsTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( [Theory] [InlineData("Reqnroll", "Parser", "Reqnroll", "Parser")] [InlineData("Reqnroll", "Internal", "Reqnroll", "Internal")] - [InlineData(null, "_1XYZ", null, "_1XYZ")] - [InlineData("", "__Internal", "", "__Internal")] - [InlineData("", "", "", "")] - [InlineData(null, null, "", "")] public void GetHashCode_ReturnsSameValueForEquivalentValues( string ns1, string name1, string ns2, string name2) { - var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new QualifiedTypeIdentifier(new NamespaceString(ns1), new SimpleTypeIdentifier(new IdentifierString(name1))); + var typeId2 = new QualifiedTypeIdentifier(new NamespaceString(ns2), new SimpleTypeIdentifier(new IdentifierString(name2))); typeId1.GetHashCode().Should().Be(typeId2.GetHashCode()); } @@ -144,8 +117,8 @@ public void EqualityOperatorWithTypeIdentifier_ReturnsEquivalenceWithCaseSensiti string name2, bool expected) { - var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new QualifiedTypeIdentifier(new NamespaceString(ns1), new SimpleTypeIdentifier(new IdentifierString(name1))); + var typeId2 = new QualifiedTypeIdentifier(new NamespaceString(ns2), new SimpleTypeIdentifier(new IdentifierString(name2))); (typeId1 == typeId2).Should().Be(expected); } @@ -164,8 +137,8 @@ public void InequalityOperatorWithTypeIdentifier_ReturnsNonEquivalenceWithCaseSe string name2, bool expected) { - var typeId1 = new NamedTypeIdentifier(new NamespaceString(ns1), new IdentifierString(name1)); - var typeId2 = new NamedTypeIdentifier(new NamespaceString(ns2), new IdentifierString(name2)); + var typeId1 = new QualifiedTypeIdentifier(new NamespaceString(ns1), new SimpleTypeIdentifier(new IdentifierString(name1))); + var typeId2 = new QualifiedTypeIdentifier(new NamespaceString(ns2), new SimpleTypeIdentifier(new IdentifierString(name2))); (typeId1 != typeId2).Should().Be(expected); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/SimpleTypeIdentifierTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/SimpleTypeIdentifierTests.cs new file mode 100644 index 000000000..7e7fffac0 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/SourceModel/SimpleTypeIdentifierTests.cs @@ -0,0 +1,104 @@ +using Reqnroll.FeatureSourceGenerator; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class SimpleTypeIdentifierTests +{ + [Theory] + [InlineData("Parser")] + [InlineData("__Parser")] + [InlineData("X509")] + public void Constructor_CreatesSimpleTypeIdentifierFromValidName(string name) + { + var nameIdentifier = new IdentifierString(name); + + var identifier = new SimpleTypeIdentifier(nameIdentifier); + + identifier.Name.Should().Be(nameIdentifier); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Constructor_ThrowsArgumentExceptionWhenUsingAnEmptyName(string name) + { + Func ctr = () => new SimpleTypeIdentifier(new IdentifierString(name)); + + ctr.Should().Throw(); + } + + [Theory] + [InlineData("Parser", "Parser")] + [InlineData("_Parser", "_Parser")] + public void EqualsTypeIdentifier_ReturnsTrueWhenLocalNamesMatche( + string name1, + string name2) + { + var typeId1 = new SimpleTypeIdentifier(new IdentifierString(name1)); + var typeId2 = new SimpleTypeIdentifier(new IdentifierString(name2)); + + typeId1.Equals(typeId2).Should().BeTrue(); + } + + [Theory] + [InlineData("Parser", "parser")] + [InlineData("Parser", "_Parser")] + public void EqualsTypeIdentifier_ReturnsFalseWhenLocalNameDoesNotMatch( + string name1, + string name2) + { + var typeId1 = new SimpleTypeIdentifier(new IdentifierString(name1)); + var typeId2 = new SimpleTypeIdentifier(new IdentifierString(name2)); + + typeId1.Equals(typeId2).Should().BeFalse(); + } + + [Theory] + [InlineData("Parser", "Parser")] + [InlineData("Internal", "Internal")] + [InlineData("_1XYZ", "_1XYZ")] + [InlineData("__Internal", "__Internal")] + public void GetHashCode_ReturnsSameValueForEquivalentValues( + string name1, + string name2) + { + var typeId1 = new SimpleTypeIdentifier(new IdentifierString(name1)); + var typeId2 = new SimpleTypeIdentifier(new IdentifierString(name2)); + + typeId1.GetHashCode().Should().Be(typeId2.GetHashCode()); + } + + [Theory] + [InlineData("Parser", "Parser", true)] + [InlineData("_Parser", "_Parser", true)] + [InlineData("_Parser", "_parser", false)] + [InlineData("Parser", "parser", false)] + [InlineData("Parser", "_Parser", false)] + public void EqualityOperatorWithTypeIdentifier_ReturnsEquivalenceWithCaseSensitivity( + string name1, + string name2, + bool expected) + { + var typeId1 = new SimpleTypeIdentifier(new IdentifierString(name1)); + var typeId2 = new SimpleTypeIdentifier(new IdentifierString(name2)); + + (typeId1 == typeId2).Should().Be(expected); + } + + [Theory] + [InlineData("Parser", "Parser", false)] + [InlineData("_Parser", "_Parser", false)] + [InlineData("_Parser", "_parser", true)] + [InlineData("Parser", "parser", true)] + [InlineData("Parser", "_Parser", true)] + public void InequalityOperatorWithTypeIdentifier_ReturnsNonEquivalenceWithCaseSensitivity( + string name1, + string name2, + bool expected) + { + var typeId1 = new SimpleTypeIdentifier(new IdentifierString(name1)); + var typeId2 = new SimpleTypeIdentifier(new IdentifierString(name2)); + + (typeId1 != typeId2).Should().Be(expected); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs index 67269c374..db21104f9 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs @@ -7,7 +7,7 @@ namespace Reqnroll.FeatureSourceGenerator.XUnit; public class XUnitFeatureSourceGeneratorTests { - [Fact(Skip = "XUnit not yet implemented.")] + [Fact] public void GeneratorProducesXUnitOutputWhenWhenBuildPropertyConfiguredForXUnit() { var references = AppDomain.CurrentDomain.GetAssemblies() @@ -57,7 +57,7 @@ Then the result should be 120 .Which.Should().HaveAttribute("global::Xunit.Fact"); } - [Fact(Skip = "XUnit not yet implemented.")] + [Fact] public void GeneratorProducesXUnitOutputWhenWhenEditorConfigConfiguredForXUnit() { var references = AppDomain.CurrentDomain.GetAssemblies() From cc53938cbf60b615b75ecd073b894c927a1e2af7 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 9 Jul 2024 16:50:50 +0100 Subject: [PATCH 27/48] Add test for XUnit IClassFixture declaration --- .../XUnit/XUnitCSharpTestFixtureClass.cs | 5 ++ .../XUnit/XUnitSyntax.cs | 18 ++++++++ .../CSharpTestFixtureGeneratorTestBase.cs | 27 +++++++++++ .../MSTestCSharpTestFixtureGeneratorTests.cs | 27 +---------- .../XUnitCSharpTestFixtureGeneratorTests.cs | 46 +++++++++++++++++++ 5 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs index 45d8fa34d..09341d1f5 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs @@ -1,4 +1,5 @@ using Reqnroll.FeatureSourceGenerator.SourceModel; +using Reqnroll.FeatureSourceGenerator.XUnit; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp.XUnit; @@ -13,6 +14,7 @@ public XUnitCSharpTestFixtureClass( CSharpRenderingOptions? renderingOptions = null) : base(identifier, hintName, feature, attributes, methods, renderingOptions) { + Interfaces = ImmutableArray.Create(XUnitSyntax.LifetimeInterfaceType(Identifier)); } public XUnitCSharpTestFixtureClass( @@ -21,8 +23,11 @@ public XUnitCSharpTestFixtureClass( CSharpRenderingOptions? renderingOptions = null) : base(descriptor, methods, renderingOptions) { + Interfaces = ImmutableArray.Create(XUnitSyntax.LifetimeInterfaceType(Identifier)); } + public override ImmutableArray Interfaces { get; } + //protected override IEnumerable GetInterfaces() => // base.GetInterfaces().Concat([$"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>"]); diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs new file mode 100644 index 000000000..284b649c0 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs @@ -0,0 +1,18 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.XUnit; +internal static class XUnitSyntax +{ + public static readonly NamespaceString XUnitNamespace = new("Xunit"); + + internal static QualifiedTypeIdentifier LifetimeInterfaceType(QualifiedTypeIdentifier testFixtureType) + { + return XUnitNamespace + new GenericTypeIdentifier( + new IdentifierString("IClassFixture"), + ImmutableArray.Create( + new NestedTypeIdentifier( + testFixtureType.LocalType, + new SimpleTypeIdentifier(new IdentifierString("Lifecycle"))))); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs new file mode 100644 index 000000000..b291656cb --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; +using Reqnroll.FeatureSourceGenerator.MSTest; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; +public abstract class CSharpTestFixtureGeneratorTestBase where THandler : ITestFrameworkHandler +{ + public CSharpTestFixtureGeneratorTestBase(THandler handler) + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(AssemblyIdentity.FromAssemblyDefinition); + + Compilation = new CSharpCompilationInformation( + "Test.dll", + references.ToImmutableArray(), + Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11, + true); + + Generator = handler.GetTestFixtureGenerator() ?? + throw new InvalidOperationException($"Handler for {handler.TestFrameworkName} does not support C# generation."); + } + + protected CSharpCompilationInformation Compilation { get; } + + protected ITestFixtureGenerator Generator { get; } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs index be91453ed..6e2537a98 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -1,35 +1,12 @@ -using Microsoft.CodeAnalysis; -using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.MSTest; -public class MSTestCSharpTestFixtureGeneratorTests +public class MSTestCSharpTestFixtureGeneratorTests() : CSharpTestFixtureGeneratorTestBase(new MSTestHandler()) { - public MSTestCSharpTestFixtureGeneratorTests() - { - var references = AppDomain.CurrentDomain.GetAssemblies() - .Where(asm => !asm.IsDynamic) - .Select(AssemblyIdentity.FromAssemblyDefinition); - - Compilation = new CSharpCompilationInformation( - "Test.dll", - references.ToImmutableArray(), - Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11, - true); - - TestHandler = new MSTestHandler(); - Generator = TestHandler.GetTestFixtureGenerator()!; - } - private static readonly NamespaceString MSTestNamespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); - protected CSharpCompilationInformation Compilation { get; } - - protected MSTestHandler TestHandler { get; } - - protected ITestFixtureGenerator Generator { get; } - [Fact] public void GenerateTestFixture_CreatesClassForFeatureWithMsTestAttributes() { diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs new file mode 100644 index 000000000..1d7445c20 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs @@ -0,0 +1,46 @@ +using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator.XUnit; +public class XUnitCSharpTestFixtureGeneratorTests() : CSharpTestFixtureGeneratorTestBase(new XUnitHandler()) +{ + private static readonly NamespaceString XUnitNamespace = new("Xunit"); + + [Fact] + public void GenerateTestFixture_CreatesClassForFeatureWithXUnitLifecycleInterface() + { + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + + var scenarioInfo = new ScenarioInformation( + "Sample Scenario", + 22, + [], + [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [scenarioInfo], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testFixture = Generator.GenerateTestFixtureClass(testFixtureGenerationContext, []); + + testFixture.Interfaces.Should().BeEquivalentTo( + [ + XUnitNamespace + new GenericTypeIdentifier( + new IdentifierString("IClassFixture"), + [ + new NestedTypeIdentifier( + new SimpleTypeIdentifier(new IdentifierString("SampleFeature")), + new SimpleTypeIdentifier(new IdentifierString("Lifecycle"))) + ]) + ]); + } +} From b1c057a63766998574d827c8a64ebfdd2499a8ca Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 9 Jul 2024 17:10:17 +0100 Subject: [PATCH 28/48] Add XUnit lifetime implementation --- .../XUnit/XUnitCSharpTestFixtureClass.cs | 91 +++++++++++-------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs index 09341d1f5..e8a677c82 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs @@ -28,52 +28,65 @@ public XUnitCSharpTestFixtureClass( public override ImmutableArray Interfaces { get; } - //protected override IEnumerable GetInterfaces() => - // base.GetInterfaces().Concat([$"global::Xunit.IClassFixture<{GetClassName()}.Lifetime>"]); + protected override void RenderTestFixtureContentTo( + CSharpSourceTextBuilder sourceBuilder, + CancellationToken cancellationToken) + { + RenderLifetimeClassTo(sourceBuilder, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); - //protected override void AppendTestFixturePreamble() - //{ - // AppendLifetimeClass(); + RenderConstructorTo(sourceBuilder, cancellationToken); - // AppendConstructor(); + cancellationToken.ThrowIfCancellationRequested(); + + base.RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + } - // base.AppendTestFixturePreamble(); - //} + protected virtual void RenderConstructorTo( + CSharpSourceTextBuilder SourceBuilder, + CancellationToken cancellationToken) + { + var className = Identifier.LocalType switch + { + SimpleTypeIdentifier simple => simple.Name, + GenericTypeIdentifier generic => generic.Name, + _ => throw new NotImplementedException( + $"Writing constructor for {Identifier.GetType().Name} values is not implemented.") + }; - //protected virtual void AppendConstructor() - //{ - // // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. - // SourceBuilder.AppendLine($"public {GetClassName()}(Lifetime lifetime)"); - // SourceBuilder.BeginBlock("{"); - // SourceBuilder.AppendLine("Lifetime = lifetime;"); - // SourceBuilder.EndBlock("}"); - //} + // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. + SourceBuilder.Append("public ").Append(className).AppendLine("(Lifetime lifetime)"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.AppendLine("Lifetime = lifetime;"); + SourceBuilder.EndBlock("}"); + } - //protected virtual void AppendLifetimeClass() - //{ - // // This class represents the feature lifetime in the xUnit framework. - // SourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); - // SourceBuilder.BeginBlock("{"); + protected virtual void RenderLifetimeClassTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + // This class represents the feature lifetime in the xUnit framework. + sourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); + sourceBuilder.BeginBlock("{"); - // SourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); + sourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); - // SourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); - // SourceBuilder.BeginBlock("{"); - // // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. - // SourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); - // SourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); - // SourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); - // SourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); + sourceBuilder.BeginBlock("{"); + // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. + sourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); + sourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); + sourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); + sourceBuilder.EndBlock("}"); - // SourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); - // SourceBuilder.BeginBlock("{"); - // SourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); - // SourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); - // SourceBuilder.BeginBlock("TestRunner = null;"); - // SourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); - // SourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); + sourceBuilder.BeginBlock("{"); + sourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); + sourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); + sourceBuilder.BeginBlock("TestRunner = null;"); + sourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); + sourceBuilder.EndBlock("}"); - // SourceBuilder.EndBlock("}"); - // SourceBuilder.AppendLine(); - //} + sourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine(); + } } From cf30365a8eae909c55da41630c1470b6b6520cb0 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 9 Jul 2024 21:52:45 +0100 Subject: [PATCH 29/48] Add test for xunit method attributes --- .../XUnitCSharpTestFixtureGeneratorTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs index 1d7445c20..0c91ebc48 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs @@ -43,4 +43,50 @@ public void GenerateTestFixture_CreatesClassForFeatureWithXUnitLifecycleInterfac ]) ]); } + + [Fact] + public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamples() + { + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + + var scenarioInfo = new ScenarioInformation( + "Sample Scenario", + 22, + [], + [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [scenarioInfo], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testMethodGenerationContext = new TestMethodGenerationContext( + scenarioInfo, + testFixtureGenerationContext); + + var method = Generator.GenerateTestMethod(testMethodGenerationContext); + + method.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor(XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Fact"))), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), + ["Sample Scenario"]), + ]); + + method.Should().HaveNoParameters(); + + method.StepInvocations.Should().BeEquivalentTo( + [ + new StepInvocation(StepType.Action, 6, "When", "foo happens") + ]); + } } From 5201675f780456e8136e6c800699b6bfe1d734cb Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 9 Jul 2024 22:29:25 +0100 Subject: [PATCH 30/48] Add xunit attributes to generation --- .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 14 +++----------- .../SourceModel/AttributeDescriptor.cs | 18 +++++++++--------- .../XUnit/XUnitSyntax.cs | 19 +++++++++++++++++++ .../AttributeDescriptorTests.cs | 10 +++++----- .../CSharpMethodDeclarationAssertions.cs | 8 +++++--- .../XUnitCSharpTestFixtureGeneratorTests.cs | 14 +++++++++++--- 6 files changed, 52 insertions(+), 31 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs index 79c2ebe82..e25fabf80 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -31,16 +31,8 @@ protected override ImmutableArray GenerateTestMethodAttribu CancellationToken cancellationToken) { return ImmutableArray.Create( - new AttributeDescriptor( - XUnitHandler.XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Fact")))); - } - - protected override ImmutableArray GenerateTestFixtureInterfaces( - TestFixtureGenerationContext context, - QualifiedTypeIdentifier qualifiedClassName, - CancellationToken cancellationToken) - { - return ImmutableArray.Create( - XUnitHandler.XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("IClassFixture"))); + XUnitSyntax.SkippableFactAttribute(context.ScenarioInformation.Name), + XUnitSyntax.TraitAttribute("FeatureTitle", context.FeatureInformation.Name), + XUnitSyntax.TraitAttribute("Description", context.ScenarioInformation.Name)); } } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs index 961403e32..8a41679e1 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/AttributeDescriptor.cs @@ -13,7 +13,7 @@ namespace Reqnroll.FeatureSourceGenerator.SourceModel; public class AttributeDescriptor( QualifiedTypeIdentifier type, ImmutableArray? positionalArguments = null, - ImmutableDictionary? namedArguments = null) + ImmutableDictionary? namedArguments = null) : IEquatable { public QualifiedTypeIdentifier Type { get; } = type; @@ -23,8 +23,8 @@ public class AttributeDescriptor( positionalArguments.GetValueOrDefault(ImmutableArray.Empty), nameof(positionalArguments)); - public ImmutableDictionary NamedArguments { get; } = - ThrowIfArgumentTypesNotValid(namedArguments ?? ImmutableDictionary.Empty, nameof(namedArguments)); + public ImmutableDictionary NamedArguments { get; } = + ThrowIfArgumentTypesNotValid(namedArguments ?? ImmutableDictionary.Empty, nameof(namedArguments)); private int? _hashCode; @@ -91,8 +91,8 @@ private static void ThrowIfArgumentTypeNotValid(object? value, string paramName) } } - private static ImmutableDictionary ThrowIfArgumentTypesNotValid( - ImmutableDictionary namedArguments, + private static ImmutableDictionary ThrowIfArgumentTypesNotValid( + ImmutableDictionary namedArguments, string paramName) { foreach (var item in namedArguments.Values) @@ -199,8 +199,8 @@ private bool ArgumentsEqual(AttributeDescriptor other) } private static bool ArgumentDictionaryEqual( - ImmutableDictionary first, - ImmutableDictionary second) + ImmutableDictionary first, + ImmutableDictionary second) { if (ReferenceEquals(first, second)) { @@ -360,11 +360,11 @@ public AttributeDescriptor WithPositionalArguments(params object?[] positionalAr public AttributeDescriptor WithNamedArguments(object namedArguments) { - var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase); + var arguments = new Dictionary(); foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(namedArguments)) { - arguments.Add(propertyDescriptor.Name, propertyDescriptor.GetValue(namedArguments)); + arguments.Add(new IdentifierString(propertyDescriptor.Name), propertyDescriptor.GetValue(namedArguments)); } return new AttributeDescriptor(Type, PositionalArguments, arguments.ToImmutableDictionary()); diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs index 284b649c0..275ec77aa 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs @@ -15,4 +15,23 @@ internal static QualifiedTypeIdentifier LifetimeInterfaceType(QualifiedTypeIdent testFixtureType.LocalType, new SimpleTypeIdentifier(new IdentifierString("Lifecycle"))))); } + + internal static AttributeDescriptor SkippableFactAttribute(string displayName) + { + return new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("SkippableFact")), + namedArguments: ImmutableDictionary.CreateRange( + [ + new KeyValuePair( + new IdentifierString("DisplayName"), + displayName) + ])); + } + + internal static AttributeDescriptor TraitAttribute(string name, string value) + { + return new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Trait")), + ImmutableArray.Create(name, value)); + } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs index a22a0f696..28439bd11 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/AttributeDescriptorTests.cs @@ -97,7 +97,7 @@ public void DescriptorsCanBeCreatedWithSomeBuiltInTypesAsArguments(object? argum using var assertions = new AssertionScope(); attribute.PositionalArguments[0].Should().Be(argument); - attribute.NamedArguments["Property"].Should().Be(argument); + attribute.NamedArguments[new IdentifierString("Property")].Should().Be(argument); } [Theory] @@ -112,7 +112,7 @@ public void DescriptorsCanBeCreatedWithEnumsAsArguments(object? argument) using var assertions = new AssertionScope(); attribute.PositionalArguments[0].Should().Be(argument); - attribute.NamedArguments["Property"].Should().Be(argument); + attribute.NamedArguments[new IdentifierString("Property")].Should().Be(argument); } [Theory] @@ -140,7 +140,7 @@ public void DescriptorsCanBeCreatedWithImmutableArraysOfSomeBuiltInTypesAsArgume using var assertions = new AssertionScope(); attribute.PositionalArguments[0].Should().Be(argument); - attribute.NamedArguments["Property"].Should().Be(argument); + attribute.NamedArguments[new IdentifierString("Property")].Should().Be(argument); } [Theory] @@ -157,7 +157,7 @@ public void DescriptorsCanBeCreatedWithImmutableArraysOfEnumsAsArguments(T va using var assertions = new AssertionScope(); attribute.PositionalArguments[0].Should().Be(argument); - attribute.NamedArguments["Property"].Should().Be(argument); + attribute.NamedArguments[new IdentifierString("Property")].Should().Be(argument); } [Theory] @@ -172,7 +172,7 @@ public void DescriptorsCanBeCreatedWithTypesAsArguments(Type argument) using var assertions = new AssertionScope(); attribute.PositionalArguments[0].Should().Be(argument); - attribute.NamedArguments["Property"].Should().Be(argument); + attribute.NamedArguments[new IdentifierString("Property")].Should().Be(argument); } [Fact] diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs index 198777a1b..2e0d7d790 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharpMethodDeclarationAssertions.cs @@ -260,12 +260,12 @@ public static AttributeDescriptor GetAttributeDescriptor(AttributeSyntax attribu }; ImmutableArray positionalArguments; - ImmutableDictionary namedArguments; + ImmutableDictionary namedArguments; if (attribute.ArgumentList == null) { positionalArguments = []; - namedArguments = ImmutableDictionary.Empty; + namedArguments = ImmutableDictionary.Empty; } else { @@ -276,7 +276,9 @@ public static AttributeDescriptor GetAttributeDescriptor(AttributeSyntax attribu namedArguments = attribute.ArgumentList.Arguments .Where(arg => arg.NameEquals != null) - .ToImmutableDictionary(arg => arg.NameEquals!.Name.ToString(), arg => GetLiteralValue(arg.Expression)); + .ToImmutableDictionary( + arg => new IdentifierString(arg.NameEquals!.Name.ToString()), + arg => GetLiteralValue(arg.Expression)); } return new AttributeDescriptor( diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs index 0c91ebc48..0c5c9f85e 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs @@ -1,5 +1,6 @@ using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.XUnit; public class XUnitCSharpTestFixtureGeneratorTests() : CSharpTestFixtureGeneratorTestBase(new XUnitHandler()) @@ -76,10 +77,17 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp method.Should().HaveAttribuesEquivalentTo( [ - new AttributeDescriptor(XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Fact"))), new AttributeDescriptor( - XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), - ["Sample Scenario"]), + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("SkippableFact")), + namedArguments: new Dictionary{ + { new IdentifierString("DisplayName"), "Sample Scenario" } + }.ToImmutableDictionary()), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Trait")), + ["FeatureTitle", "Sample"]), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Trait")), + ["Description", "Sample Scenario"]) ]); method.Should().HaveNoParameters(); From 50b6432f4802d6a5743024ae15767df97329e1e8 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 9 Jul 2024 22:55:12 +0100 Subject: [PATCH 31/48] Add rendering of C# test fixture class interfaces --- .../CSharp/CSharpTestFixtureClass.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index b9035408e..388d2228d 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -81,7 +81,20 @@ public void RenderTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken ca } } - sourceBuilder.Append("public partial class ").AppendTypeReference(Identifier.LocalType).AppendLine(); + sourceBuilder.Append("public partial class ").AppendTypeReference(Identifier.LocalType); + + if (!Interfaces.IsEmpty) + { + sourceBuilder.Append(" :").AppendTypeReference(Interfaces[0]); + + for (var i = 1; i < Interfaces.Length; i++) + { + sourceBuilder.Append(" ,").AppendTypeReference(Interfaces[i]); + } + } + + sourceBuilder.AppendLine(); + sourceBuilder.BeginBlock("{"); if (RenderingOptions.EnableLineMapping && FeatureInformation.FilePath != null) From e92851f6af778f1002f457e44a010d10a1ae9fe9 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Wed, 10 Jul 2024 20:22:25 +0100 Subject: [PATCH 32/48] Add XUnit implementation --- .../CSharp/CSharpSourceTextBuilder.cs | 21 ++++- .../CSharp/MSTest/MSTestCSharpTestMethod.cs | 7 +- .../XUnit/XUnitCSharpTestFixtureClass.cs | 74 +++++++++++++---- .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 39 ++++++++- .../CSharp/XUnit/XUnitCSharpTestMethod.cs | 15 +++- .../XUnit/XUnitSyntax.cs | 26 +++++- ...s => MSTestFeatureSourceGeneratorTests.cs} | 2 +- .../XUnitCSharpTestFixtureGeneratorTests.cs | 80 ++++++++++++++++++- .../XUnit/XUnitFeatureSourceGeneratorTests.cs | 11 ++- 9 files changed, 240 insertions(+), 35 deletions(-) rename Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/{MSTestFeatureSourceGenerationTests.cs => MSTestFeatureSourceGeneratorTests.cs} (98%) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs index e26a70119..9d3b9ca87 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -276,14 +276,29 @@ public CSharpSourceTextBuilder AppendTypeReference(TypeIdentifier type) { return type switch { - SimpleTypeIdentifier simpleType => AppendTypeReference(simpleType), - GenericTypeIdentifier genericType => AppendTypeReference(genericType), + LocalTypeIdentifier localType => AppendTypeReference(localType), QualifiedTypeIdentifier qualifiedType => AppendTypeReference(qualifiedType), ArrayTypeIdentifier arrayType => AppendTypeReference(arrayType), - _ => throw new NotImplementedException() + NestedTypeIdentifier nestedType => AppendTypeReference(nestedType), + _ => throw new NotImplementedException($"Appending references of type {type.GetType().Name} is not implemented.") }; } + public CSharpSourceTextBuilder AppendTypeReference(LocalTypeIdentifier type) + { + return type switch + { + SimpleTypeIdentifier simpleType => AppendTypeReference(simpleType), + GenericTypeIdentifier genericType => AppendTypeReference(genericType), + _ => throw new NotImplementedException($"Appending references of type {type.GetType().Name} is not implemented.") + }; + } + + public CSharpSourceTextBuilder AppendTypeReference(NestedTypeIdentifier type) + { + return AppendTypeReference(type.EncapsulatingType).Append('.').AppendTypeReference(type.LocalType); + } + public CSharpSourceTextBuilder AppendTypeReference(SimpleTypeIdentifier type) { Append(type.Name); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs index 96a46687a..7d540b7d4 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs @@ -26,11 +26,10 @@ protected override void RenderTestRunnerLookupTo( { // For MSTest we use a test-runner assigned to the class. sourceBuilder - .AppendLine("global::Reqnroll.ITestRunner testRunner;") - .AppendLine("if (TestRunner == null)") + .AppendLine("global::Reqnroll.ITestRunner testRunner = TestRunner;") + .AppendLine("if (testRunner == null)") .BeginBlock("{") .AppendLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") - .EndBlock("}") - .AppendLine("testRunner = TestRunner;"); + .EndBlock("}"); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs index e8a677c82..a1a41490c 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs @@ -14,7 +14,8 @@ public XUnitCSharpTestFixtureClass( CSharpRenderingOptions? renderingOptions = null) : base(identifier, hintName, feature, attributes, methods, renderingOptions) { - Interfaces = ImmutableArray.Create(XUnitSyntax.LifetimeInterfaceType(Identifier)); + Interfaces = ImmutableArray.Create( + XUnitSyntax.LifetimeInterfaceType(Identifier)); } public XUnitCSharpTestFixtureClass( @@ -23,7 +24,8 @@ public XUnitCSharpTestFixtureClass( CSharpRenderingOptions? renderingOptions = null) : base(descriptor, methods, renderingOptions) { - Interfaces = ImmutableArray.Create(XUnitSyntax.LifetimeInterfaceType(Identifier)); + Interfaces = ImmutableArray.Create( + XUnitSyntax.LifetimeInterfaceType(Identifier)); } public override ImmutableArray Interfaces { get; } @@ -36,10 +38,18 @@ protected override void RenderTestFixtureContentTo( cancellationToken.ThrowIfCancellationRequested(); + sourceBuilder.AppendLine("private readonly FeatureLifetime _lifetime;"); + sourceBuilder.AppendLine(); + + sourceBuilder.AppendLine("private readonly global::Xunit.Abstractions.ITestOutputHelper _testOutputHelper;"); + sourceBuilder.AppendLine(); + RenderConstructorTo(sourceBuilder, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); + sourceBuilder.AppendLine(); + base.RenderTestFixtureContentTo(sourceBuilder, cancellationToken); } @@ -56,37 +66,69 @@ protected virtual void RenderConstructorTo( }; // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. - SourceBuilder.Append("public ").Append(className).AppendLine("(Lifetime lifetime)"); + // Output helper is included to by registered in the container. + SourceBuilder.Append("public ").Append(className).AppendLine("(FeatureLifetime lifetime, " + + "global::Xunit.Abstractions.ITestOutputHelper testOutputHelper)"); SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("Lifetime = lifetime;"); + SourceBuilder.AppendLine("_lifetime = lifetime;"); + SourceBuilder.AppendLine("_testOutputHelper = testOutputHelper;"); SourceBuilder.EndBlock("}"); } protected virtual void RenderLifetimeClassTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) { // This class represents the feature lifetime in the xUnit framework. - sourceBuilder.AppendLine("public class Lifetime : global::Xunit.IAsyncLifetime"); + sourceBuilder.AppendLine("public class FeatureLifetime : global::Xunit.IAsyncLifetime"); sourceBuilder.BeginBlock("{"); + RenderLifetimeClassContentTo(sourceBuilder, cancellationToken); + sourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine(); + } - sourceBuilder.AppendLine("public global::Reqnroll.TestRunner TestRunner { get; private set; }"); + private void RenderLifetimeClassContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + sourceBuilder.Append("public global::Reqnroll.ITestRunner"); + + if (RenderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append('?'); + } + + sourceBuilder.AppendLine(" TestRunner { get; private set; }"); + + sourceBuilder.AppendLine(); sourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); sourceBuilder.BeginBlock("{"); - // Our XUnit infrastructure uses a custom mechanism for identifying worker IDs. - sourceBuilder.AppendLine("var testWorkerId = global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.GetWorkerId();"); - sourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly(null, testWorkerId);"); - sourceBuilder.AppendLine("return TestRunner.OnFeatureStartAsync(featureInfo);"); + sourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();"); + sourceBuilder.Append("return TestRunner.OnFeatureStartAsync(").AppendTypeReference(Identifier).Append(".FeatureInfo").AppendLine(");"); sourceBuilder.EndBlock("}"); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); sourceBuilder.BeginBlock("{"); - sourceBuilder.BeginBlock("var testWorkerId = testRunner.TestWorkerId;"); - sourceBuilder.BeginBlock("await testRunner.OnFeatureEndAsync();"); - sourceBuilder.BeginBlock("TestRunner = null;"); - sourceBuilder.BeginBlock("global::Reqnroll.xUnit.ReqnrollPlugin.XUnitParallelWorkerTracker.Instance.ReleaseWorker(testWorkerId);"); - sourceBuilder.EndBlock("}"); + sourceBuilder.Append("await TestRunner"); + if (RenderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append('!'); + } + + sourceBuilder.AppendLine(".OnFeatureEndAsync();"); + sourceBuilder.AppendLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(TestRunner);"); + sourceBuilder.AppendLine("TestRunner = null;"); sourceBuilder.EndBlock("}"); - sourceBuilder.AppendLine(); + } + + protected override void RenderScenarioInitializeMethodBodyTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + base.RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + + sourceBuilder.AppendLine( + "testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs" + + "(_testOutputHelper);"); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs index e25fabf80..2336282ad 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -30,9 +30,40 @@ protected override ImmutableArray GenerateTestMethodAttribu TestMethodGenerationContext context, CancellationToken cancellationToken) { - return ImmutableArray.Create( - XUnitSyntax.SkippableFactAttribute(context.ScenarioInformation.Name), - XUnitSyntax.TraitAttribute("FeatureTitle", context.FeatureInformation.Name), - XUnitSyntax.TraitAttribute("Description", context.ScenarioInformation.Name)); + var scenario = context.ScenarioInformation; + var feature = context.FeatureInformation; + + var attributes = new List(); + + if (scenario.Examples.IsEmpty) + { + attributes.Add(XUnitSyntax.SkippableFactAttribute(context.ScenarioInformation.Name)); + } + else + { + attributes.Add(XUnitSyntax.SkippableTheoryAttribute(context.ScenarioInformation.Name)); + } + + attributes.Add(XUnitSyntax.TraitAttribute("FeatureTitle", context.FeatureInformation.Name)); + attributes.Add(XUnitSyntax.TraitAttribute("Description", context.ScenarioInformation.Name)); + + foreach (var tag in scenario.Tags) + { + attributes.Add(XUnitSyntax.TraitAttribute("Category", tag)); + } + + foreach (var set in scenario.Examples) + { + foreach (var example in set) + { + var values = example.Select(example => (object?)example.Value).ToList(); + + values.Add(set.Tags); + + attributes.Add(XUnitSyntax.InlineDataAttribute(values.ToImmutableArray())); + } + } + + return attributes.ToImmutableArray(); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs index 9a12a55b3..920adc8c8 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs @@ -25,6 +25,19 @@ protected override void RenderTestRunnerLookupTo( CancellationToken cancellationToken) { // For xUnit test runners are scoped to the whole feature execution lifetime - sourceBuilder.AppendLine("var testRunner = Lifecycle.TestRunner;"); + sourceBuilder.Append("global::Reqnroll.ITestRunner"); + + if (renderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append('?'); + } + + sourceBuilder.AppendLine(" testRunner = _lifetime.TestRunner;"); + + sourceBuilder + .AppendLine("if (testRunner == null)") + .BeginBlock("{") + .AppendLine("throw new global::System.InvalidOperationException(\"The test fixture lifecycle has not been initialized.\");") + .EndBlock("}"); } } diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs index 275ec77aa..9ab9a8c58 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitSyntax.cs @@ -6,6 +6,18 @@ internal static class XUnitSyntax { public static readonly NamespaceString XUnitNamespace = new("Xunit"); + internal static QualifiedTypeIdentifier AsyncLifetimeType() + { + return XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("IAsyncLifetime")); + } + + internal static AttributeDescriptor InlineDataAttribute(ImmutableArray values) + { + return new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("InlineData")), + values); + } + internal static QualifiedTypeIdentifier LifetimeInterfaceType(QualifiedTypeIdentifier testFixtureType) { return XUnitNamespace + new GenericTypeIdentifier( @@ -13,7 +25,7 @@ internal static QualifiedTypeIdentifier LifetimeInterfaceType(QualifiedTypeIdent ImmutableArray.Create( new NestedTypeIdentifier( testFixtureType.LocalType, - new SimpleTypeIdentifier(new IdentifierString("Lifecycle"))))); + new SimpleTypeIdentifier(new IdentifierString("FeatureLifetime"))))); } internal static AttributeDescriptor SkippableFactAttribute(string displayName) @@ -28,6 +40,18 @@ internal static AttributeDescriptor SkippableFactAttribute(string displayName) ])); } + internal static AttributeDescriptor SkippableTheoryAttribute(string displayName) + { + return new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("SkippableTheory")), + namedArguments: ImmutableDictionary.CreateRange( + [ + new KeyValuePair( + new IdentifierString("DisplayName"), + displayName) + ])); + } + internal static AttributeDescriptor TraitAttribute(string name, string value) { return new AttributeDescriptor( diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs similarity index 98% rename from Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs rename to Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs index 919c58994..d26a2dd27 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGenerationTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs @@ -8,7 +8,7 @@ namespace Reqnroll.FeatureSourceGenerator; -public class MSTestFeatureSourceGenerationTests(ITestOutputHelper output) +public class MSTestFeatureSourceGeneratorTests(ITestOutputHelper output) { [Fact] public void GeneratorProducesMSTestOutputWhenWhenBuildPropertyConfiguredForMSTest() diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs index 0c5c9f85e..b892b08fa 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs @@ -1,6 +1,7 @@ using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; +using Xunit.Abstractions; namespace Reqnroll.FeatureSourceGenerator.XUnit; public class XUnitCSharpTestFixtureGeneratorTests() : CSharpTestFixtureGeneratorTestBase(new XUnitHandler()) @@ -8,7 +9,7 @@ public class XUnitCSharpTestFixtureGeneratorTests() : CSharpTestFixtureGenerator private static readonly NamespaceString XUnitNamespace = new("Xunit"); [Fact] - public void GenerateTestFixture_CreatesClassForFeatureWithXUnitLifecycleInterface() + public void GenerateTestFixture_CreatesClassForFeatureWithXUnitLifetimeInterface() { var featureInfo = new FeatureInformation( "Sample", @@ -40,7 +41,7 @@ public void GenerateTestFixture_CreatesClassForFeatureWithXUnitLifecycleInterfac [ new NestedTypeIdentifier( new SimpleTypeIdentifier(new IdentifierString("SampleFeature")), - new SimpleTypeIdentifier(new IdentifierString("Lifecycle"))) + new SimpleTypeIdentifier(new IdentifierString("FeatureLifetime"))) ]) ]); } @@ -97,4 +98,79 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp new StepInvocation(StepType.Action, 6, "When", "foo happens") ]); } + + [Fact] + public void GenerateTestMethod_CreatesMethodWithInlineDataAttributesWhenScenarioHasExamples() + { + var exampleSet1 = new ScenarioExampleSet(["what"], [["foo"], ["bar"]], ["example_tag"]); + var exampleSet2 = new ScenarioExampleSet(["what"], [["baz"]], []); + + var scenarioInfo = new ScenarioInformation( + "Sample Scenario Outline", + 22, + [], + [new ScenarioStep(StepType.Action, "When", " happens", 6)], + [exampleSet1, exampleSet2]); + + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [scenarioInfo], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testMethodGenerationContext = new TestMethodGenerationContext( + scenarioInfo, + testFixtureGenerationContext); + + var method = Generator.GenerateTestMethod(testMethodGenerationContext); + + method.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("SkippableTheory")), + namedArguments: new Dictionary{ + { new IdentifierString("DisplayName"), "Sample Scenario Outline" } + }.ToImmutableDictionary()), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Trait")), + ["FeatureTitle", "Sample"]), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Trait")), + ["Description", "Sample Scenario Outline"]), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("InlineData")), + ["foo", ImmutableArray.Create("example_tag")]), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("InlineData")), + ["bar", ImmutableArray.Create("example_tag")]), + new AttributeDescriptor( + XUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("InlineData")), + ["baz", ImmutableArray.Empty]) + ]); + + method.Should().HaveParametersEquivalentTo( + [ + new ParameterDescriptor( + new IdentifierString("what"), + new NamespaceString("System") + new SimpleTypeIdentifier(new IdentifierString("String"))), + + new ParameterDescriptor( + new IdentifierString("_exampleTags"), + new ArrayTypeIdentifier(new NamespaceString("System") + new SimpleTypeIdentifier(new IdentifierString("String")))) + ]); + + method.StepInvocations.Should().BeEquivalentTo( + [ + new StepInvocation(StepType.Action, 6, "When", "{0} happens", [new IdentifierString("what")]) + ]); + } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs index db21104f9..906666bd0 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitFeatureSourceGeneratorTests.cs @@ -2,10 +2,11 @@ using Microsoft.CodeAnalysis.CSharp; using Reqnroll.FeatureSourceGenerator; using Reqnroll.FeatureSourceGenerator.CSharp; +using Xunit.Abstractions; namespace Reqnroll.FeatureSourceGenerator.XUnit; -public class XUnitFeatureSourceGeneratorTests +public class XUnitFeatureSourceGeneratorTests(ITestOutputHelper output) { [Fact] public void GeneratorProducesXUnitOutputWhenWhenBuildPropertyConfiguredForXUnit() @@ -49,12 +50,14 @@ Then the result should be 120 var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() .Which.Should().BeAssignableTo().Subject!; + output.WriteLine($"Generated source:\n{generatedSyntaxTree}"); + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().ContainMethod("AddTwoNumbers") - .Which.Should().HaveAttribute("global::Xunit.Fact"); + .Which.Should().HaveAttribute("global::Xunit.SkippableFact"); } [Fact] @@ -99,11 +102,13 @@ Then the result should be 120 var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() .Which.Should().BeAssignableTo().Subject!; + output.WriteLine($"Generated source:\n{generatedSyntaxTree}"); + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") .Which.Should().ContainMethod("AddTwoNumbers") - .Which.Should().HaveAttribute("global::Xunit.Fact"); + .Which.Should().HaveAttribute("global::Xunit.SkippableFact"); } } From acece943660e0be96ce96518e3941f4cfe6b203f Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Fri, 12 Jul 2024 18:31:05 +0100 Subject: [PATCH 33/48] Added NUnit generation with default emission of skipped scenarios --- .../build/Reqnroll.NUnit.props | 1 + .../CSharp/CSharpSourceTextBuilder.cs | 5 + .../MSTest/MSTestCSharpTestFixtureClass.cs | 8 +- .../MSTestCSharpTestFixtureGenerator.cs | 8 +- .../NUnit/NUnitCSharpTestFixtureClass.cs | 91 ++++++++++ .../NUnit/NUnitCSharpTestFixtureGenerator.cs | 77 +++++++++ .../CSharp/NUnit/NUnitCSharpTestMethod.cs | 43 +++++ .../XUnit/XUnitCSharpTestFixtureClass.cs | 8 +- .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 7 +- .../CSharp/XUnit/XUnitCSharpTestMethod.cs | 2 +- .../NUnit/NUnitCSharpTestFixtureGenerator.cs | 39 ----- .../NUnit/NUnitHandler.cs | 1 + .../NUnit/NUnitSyntax.cs | 58 +++++++ .../TestFixtureSourceGenerator.cs | 3 + .../Reqnroll.FeatureSourceGenerator.props | 5 + .../MSTestFeatureSourceGeneratorTests.cs | 70 +------- .../NUnitCSharpTestFixtureGeneratorTests.cs | 158 ++++++++++++++++++ .../NUnit/NUnitFeatureSourceGeneratorTests.cs | 112 +++++++++++++ .../XUnitCSharpTestFixtureGeneratorTests.cs | 1 - 19 files changed, 572 insertions(+), 125 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs create mode 100644 Reqnroll.FeatureSourceGenerator/NUnit/NUnitSyntax.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs create mode 100644 Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitFeatureSourceGeneratorTests.cs diff --git a/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props b/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props index a3d0bf7cc..0f3cacd31 100644 --- a/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props +++ b/Plugins/Reqnroll.NUnit.Generator.ReqnrollPlugin/build/Reqnroll.NUnit.props @@ -2,6 +2,7 @@ NUnit + true diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs index 9d3b9ca87..0e1482549 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs @@ -254,6 +254,11 @@ public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribut { if (firstProperty) { + if (!attribute.PositionalArguments.IsEmpty) + { + Append(", "); + } + firstProperty = false; } else diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs index c7283d906..2846089f2 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs @@ -9,17 +9,17 @@ public MSTestCSharpTestFixtureClass( string hintName, FeatureInformation feature, ImmutableArray attributes = default, - ImmutableArray methods = default, + ImmutableArray methods = default, CSharpRenderingOptions? renderingOptions = null) - : base(identifier, hintName, feature, attributes, methods, renderingOptions) + : base(identifier, hintName, feature, attributes, methods.CastArray(), renderingOptions) { } public MSTestCSharpTestFixtureClass( TestFixtureDescriptor descriptor, - ImmutableArray methods = default, + ImmutableArray methods = default, CSharpRenderingOptions? renderingOptions = null) - : base(descriptor, methods, renderingOptions) + : base(descriptor, methods.CastArray(), renderingOptions) { } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs index 44ef40d97..7c0531a29 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs @@ -8,18 +8,18 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; /// Performs generation of MSTest test fixtures in the C# language. /// internal class MSTestCSharpTestFixtureGenerator(MSTestHandler frameworkHandler) : - CSharpTestFixtureGenerator(frameworkHandler) + CSharpTestFixtureGenerator(frameworkHandler) { protected override MSTestCSharpTestFixtureClass CreateTestFixtureClass( TestFixtureGenerationContext context, TestFixtureDescriptor descriptor, - ImmutableArray methods, + ImmutableArray methods, CSharpRenderingOptions renderingOptions) { return new MSTestCSharpTestFixtureClass(descriptor, methods, renderingOptions); } - protected override CSharpTestMethod CreateTestMethod( + protected override MSTestCSharpTestMethod CreateTestMethod( TestMethodGenerationContext context, TestMethodDescriptor descriptor) { @@ -51,6 +51,8 @@ protected override ImmutableArray GenerateTestMethodAttribu { foreach (var example in set) { + cancellationToken.ThrowIfCancellationRequested(); + // DataRow's constructor is DataRow(object? data, params object?[] moreData) // Because we often pass an array of strings as a second argument, we always wrap moreData // in an explicit array to avoid the compiler mistaking our string array as the moreData value. diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs new file mode 100644 index 000000000..0ae40d915 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs @@ -0,0 +1,91 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.NUnit; +public class NUnitCSharpTestFixtureClass : CSharpTestFixtureClass +{ + public NUnitCSharpTestFixtureClass( + QualifiedTypeIdentifier identifier, + string hintName, + FeatureInformation feature, + ImmutableArray attributes = default, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) + : base(identifier, hintName, feature, attributes, methods.CastArray(), renderingOptions) + { + } + + public NUnitCSharpTestFixtureClass( + TestFixtureDescriptor descriptor, + ImmutableArray methods = default, + CSharpRenderingOptions? renderingOptions = null) + : base(descriptor, methods.CastArray(), renderingOptions) + { + } + + protected override void RenderTestFixtureContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + sourceBuilder.Append("private global::Reqnroll.ITestRunner"); + + if (RenderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append('?'); + } + + sourceBuilder.AppendLine(" _testRunner;"); + + sourceBuilder.AppendLine(); + + RenderFeatureSetupMethodTo(sourceBuilder); + + sourceBuilder.AppendLine(); + + RenderFeatureTearDownMethodTo(sourceBuilder); + + sourceBuilder.AppendLine(); + + base.RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + } + + private void RenderFeatureSetupMethodTo(CSharpSourceTextBuilder sourceBuilder) + { + sourceBuilder + .AppendLine("[global::NUnit.Framework.OneTimeSetUp]") + .AppendLine("public virtual global::System.Threading.Tasks.Task FeatureSetupAsync()") + .BeginBlock("{") + .AppendLine("_testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();") + .AppendLine("return _testRunner.OnFeatureStartAsync(FeatureInfo);") + .EndBlock("}"); + } + + private void RenderFeatureTearDownMethodTo(CSharpSourceTextBuilder sourceBuilder) + { + sourceBuilder + .AppendLine("[global::NUnit.Framework.OneTimeTearDown]") + .AppendLine("public async virtual global::System.Threading.Tasks.Task FeatureTearDownAsync()") + .BeginBlock("{"); + + sourceBuilder.Append("await _testRunner"); + + if (RenderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append('!'); + } + + sourceBuilder.AppendLine(".OnFeatureEndAsync();"); + + sourceBuilder + .AppendLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(_testRunner);") + .AppendLine("_testRunner = null;") + .EndBlock("}"); + } + + protected override void RenderScenarioInitializeMethodBodyTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + { + base.RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + + sourceBuilder.AppendLine( + "testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(" + + "global::NUnit.Framework.TestContext.CurrentContext);"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..ba9eece96 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs @@ -0,0 +1,77 @@ +using Reqnroll.FeatureSourceGenerator.NUnit; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.NUnit; +internal class NUnitCSharpTestFixtureGenerator(NUnitHandler frameworkHandler) : + CSharpTestFixtureGenerator(frameworkHandler) +{ + protected override NUnitCSharpTestFixtureClass CreateTestFixtureClass( + TestFixtureGenerationContext context, + TestFixtureDescriptor descriptor, + ImmutableArray methods, + CSharpRenderingOptions renderingOptions) + { + return new NUnitCSharpTestFixtureClass(descriptor, methods, renderingOptions); + } + + protected override NUnitCSharpTestMethod CreateTestMethod( + TestMethodGenerationContext context, + TestMethodDescriptor descriptor) + { + return new NUnitCSharpTestMethod(descriptor); + } + + protected override ImmutableArray GenerateTestFixtureClassAttributes( + TestFixtureGenerationContext context, + CancellationToken cancellationToken) + { + return ImmutableArray.Create(NUnitSyntax.DescriptionAttribute(context.FeatureInformation.Name)); + } + + protected override ImmutableArray GenerateTestMethodAttributes( + TestMethodGenerationContext context, + CancellationToken cancellationToken) + { + var scenario = context.ScenarioInformation; + var feature = context.FeatureInformation; + + var attributes = new List(); + + if (scenario.Examples.IsEmpty) + { + attributes.Add(NUnitSyntax.TestAttribute()); + } + + attributes.Add(NUnitSyntax.DescriptionAttribute(context.ScenarioInformation.Name)); + + foreach (var tag in scenario.Tags) + { + attributes.Add(NUnitSyntax.CategoryAttribute(tag)); + } + + if (scenario.Tags.Contains("ignore")) + { + attributes.Add(NUnitSyntax.IgnoreAttribute("Ignored scenario")); + } + + foreach (var set in scenario.Examples) + { + foreach (var example in set) + { + cancellationToken.ThrowIfCancellationRequested(); + + var values = example.Select(example => (object?)example.Value).ToList(); + + values.Add(set.Tags); + + string? category = set.Tags.IsEmpty ? null : string.Join(",", set.Tags); + string? ignore = set.Tags.Contains("ignore", StringComparer.OrdinalIgnoreCase) ? "Ignored by @ignore tag" : null; + + attributes.Add(NUnitSyntax.TestCaseAttribute(values.ToImmutableArray(), category, ignore)); + } + } + + return attributes.ToImmutableArray(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs new file mode 100644 index 000000000..03b4516e9 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs @@ -0,0 +1,43 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.NUnit; +public class NUnitCSharpTestMethod : CSharpTestMethod +{ + public NUnitCSharpTestMethod( + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray stepInvocations, + ImmutableArray attributes = default, + ImmutableArray parameters = default, + ImmutableArray> scenarioParameters = default) + : base(identifier, scenario, stepInvocations, attributes, parameters, scenarioParameters) + { + } + + public NUnitCSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor) + { + } + + protected override void RenderTestRunnerLookupTo( + CSharpSourceTextBuilder sourceBuilder, + CSharpRenderingOptions renderingOptions, + CancellationToken cancellationToken) + { + // For NUnit we use a test-runner assigned to the class. + sourceBuilder.Append("global::Reqnroll.ITestRunner"); + + if (renderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append('?'); + } + + sourceBuilder.AppendLine(" testRunner = _testRunner;"); + + sourceBuilder + .AppendLine("if (testRunner == null)") + .BeginBlock("{") + .AppendLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") + .EndBlock("}"); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs index a1a41490c..747c0a591 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs @@ -10,9 +10,9 @@ public XUnitCSharpTestFixtureClass( string hintName, FeatureInformation feature, ImmutableArray attributes = default, - ImmutableArray methods = default, + ImmutableArray methods = default, CSharpRenderingOptions? renderingOptions = null) - : base(identifier, hintName, feature, attributes, methods, renderingOptions) + : base(identifier, hintName, feature, attributes, methods.CastArray(), renderingOptions) { Interfaces = ImmutableArray.Create( XUnitSyntax.LifetimeInterfaceType(Identifier)); @@ -20,9 +20,9 @@ public XUnitCSharpTestFixtureClass( public XUnitCSharpTestFixtureClass( TestFixtureDescriptor descriptor, - ImmutableArray methods = default, + ImmutableArray methods = default, CSharpRenderingOptions? renderingOptions = null) - : base(descriptor, methods, renderingOptions) + : base(descriptor, methods.CastArray(), renderingOptions) { Interfaces = ImmutableArray.Create( XUnitSyntax.LifetimeInterfaceType(Identifier)); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs index 2336282ad..764fb2308 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -13,10 +13,7 @@ protected override XUnitCSharpTestFixtureClass CreateTestFixtureClass( ImmutableArray methods, CSharpRenderingOptions renderingOptions) { - return new XUnitCSharpTestFixtureClass( - descriptor, - methods.Cast().ToImmutableArray(), - renderingOptions); + return new XUnitCSharpTestFixtureClass(descriptor, methods, renderingOptions); } protected override XUnitCSharpTestMethod CreateTestMethod( @@ -54,6 +51,8 @@ protected override ImmutableArray GenerateTestMethodAttribu foreach (var set in scenario.Examples) { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var example in set) { var values = example.Select(example => (object?)example.Value).ToList(); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs index 920adc8c8..93987d9e5 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs @@ -2,7 +2,7 @@ using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp.XUnit; -internal class XUnitCSharpTestMethod : CSharpTestMethod +public class XUnitCSharpTestMethod : CSharpTestMethod { public XUnitCSharpTestMethod( IdentifierString identifier, diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs deleted file mode 100644 index 8186f22a9..000000000 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitCSharpTestFixtureGenerator.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; -using Reqnroll.FeatureSourceGenerator.SourceModel; -using System.Collections.Immutable; - -namespace Reqnroll.FeatureSourceGenerator.NUnit; - -internal class NUnitCSharpTestFixtureGenerator(NUnitHandler testFrameworkHandler) : - CSharpTestFixtureGenerator(testFrameworkHandler) -{ - protected override CSharpTestFixtureClass CreateTestFixtureClass( - TestFixtureGenerationContext context, - TestFixtureDescriptor descriptor, - ImmutableArray methods, - CSharpRenderingOptions renderingOptions) - { - throw new NotImplementedException(); - } - - protected override CSharpTestMethod CreateTestMethod( - TestMethodGenerationContext context, - TestMethodDescriptor descriptor) - { - throw new NotImplementedException(); - } - - protected override ImmutableArray GenerateTestFixtureClassAttributes( - TestFixtureGenerationContext context, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override ImmutableArray GenerateTestMethodAttributes( - TestMethodGenerationContext context, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } -} diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs index 919becdbb..827ead031 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs @@ -1,4 +1,5 @@ using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp.NUnit; namespace Reqnroll.FeatureSourceGenerator.NUnit; diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitSyntax.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitSyntax.cs new file mode 100644 index 000000000..d6c4cd6b8 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitSyntax.cs @@ -0,0 +1,58 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.NUnit; +internal static class NUnitSyntax +{ + public static readonly NamespaceString NUnitNamespace = new("NUnit.Framework"); + + internal static AttributeDescriptor CategoryAttribute(string tag) + { + return new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Category")), + ImmutableArray.Create(tag)); + } + + internal static AttributeDescriptor DescriptionAttribute(string description) + { + return new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), + ImmutableArray.Create(description)); + } + + internal static AttributeDescriptor IgnoreAttribute(string reason) + { + return new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Ignore")), + ImmutableArray.Create(reason)); + } + + internal static AttributeDescriptor TestAttribute() + { + return new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Test"))); + } + + internal static AttributeDescriptor TestCaseAttribute( + ImmutableArray values, + string? category = null, + string? ignoreReason = null) + { + var namedArguments = new Dictionary(); + + if (category != null) + { + namedArguments.Add(new IdentifierString("Category"), category); + } + + if (ignoreReason != null) + { + namedArguments.Add(new IdentifierString("IgnoreReason"), ignoreReason); + } + + return new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("TestCase")), + values, + namedArguments.ToImmutableDictionary()); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 6386ca5c0..2ef1b7c5a 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -4,6 +4,7 @@ using Reqnroll.FeatureSourceGenerator.Gherkin; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; +using System.Diagnostics; namespace Reqnroll.FeatureSourceGenerator; @@ -196,6 +197,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return [.. diagnostics]; } + Debugger.Launch(); + // Determine whether we should include ignored examples in our sample sets. var emitIgnoredExamples = false; if (options.TryGetValue("reqnroll.emit_ignored_examples", out var emitIgnoredExamplesValue) || diff --git a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props index b73a6d13e..fb6589323 100644 --- a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props +++ b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props @@ -6,4 +6,9 @@ + + + + + diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs index d26a2dd27..8f148da11 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestFeatureSourceGeneratorTests.cs @@ -1,12 +1,10 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Reqnroll.FeatureSourceGenerator; using Reqnroll.FeatureSourceGenerator.CSharp; -using Reqnroll.FeatureSourceGenerator.SourceModel; using Xunit.Abstractions; -namespace Reqnroll.FeatureSourceGenerator; +namespace Reqnroll.FeatureSourceGenerator.MSTest; public class MSTestFeatureSourceGeneratorTests(ITestOutputHelper output) { @@ -110,69 +108,3 @@ Then the result should be 120 .Which.Should().HaveSingleAttribute("global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass"); } } - -internal static class MSTestSyntax -{ - private static readonly NamespaceString Namespace = new("Microsoft.VisualStudio.TestTools.UnitTesting"); - - public static AttributeDescriptor Attribute(string type, params object?[] args) - { - return new AttributeDescriptor( - Namespace + new SimpleTypeIdentifier(new IdentifierString(type)), - [.. args]); - } - - private static ExpressionSyntax Argument(object? arg) - { - return arg switch - { - string s => SyntaxFactory.LiteralExpression( - SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(s)), - - string[] array => ArrayCreation(array), - object[] array => ArrayCreation(array), - - _ => throw new NotImplementedException() - }; - } - - private static ArrayCreationExpressionSyntax ArrayCreation(string[] array) => - ArrayCreation( - SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), - array); - - private static ArrayCreationExpressionSyntax ArrayCreation(object[] array) => - ArrayCreation( - SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)), - array); - - private static ArrayCreationExpressionSyntax ArrayCreation(TypeSyntax type, T[] array) - { - var creation = SyntaxFactory.ArrayCreationExpression( - SyntaxFactory.ArrayType( - type, - SyntaxFactory.SingletonList( - SyntaxFactory.ArrayRankSpecifier( - SyntaxFactory.SingletonSeparatedList( - array.Length > 0 ? - SyntaxFactory.LiteralExpression( - SyntaxKind.NumericLiteralExpression, - SyntaxFactory.Literal(0)) : - SyntaxFactory.OmittedArraySizeExpression()))))); - - if (array.Length > 0) - { - return creation - .WithInitializer( - SyntaxFactory.InitializerExpression( - SyntaxKind.ArrayInitializerExpression, - SyntaxFactory.SeparatedList( - array.Select(arg => Argument(arg))))); - } - else - { - return creation; - } - } -} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs new file mode 100644 index 000000000..9f942a98b --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs @@ -0,0 +1,158 @@ +using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.NUnit; +public class NUnitCSharpTestFixtureGeneratorTests() : CSharpTestFixtureGeneratorTestBase(new NUnitHandler()) +{ + private static readonly NamespaceString NUnitNamespace = new("NUnit.Framework"); + + [Fact] + public void GenerateTestFixture_CreatesClassForFeatureWithNUnitAttributes() + { + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + + var scenarioInfo = new ScenarioInformation( + "Sample Scenario", + 22, + [], + [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [ scenarioInfo ], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testFixture = Generator.GenerateTestFixtureClass(testFixtureGenerationContext, []); + + testFixture.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), + ["Sample"]), + ]); + } + + [Fact] + public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamples() + { + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + + var scenarioInfo = new ScenarioInformation( + "Sample Scenario", + 22, + [], + [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [ scenarioInfo ], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testMethodGenerationContext = new TestMethodGenerationContext( + scenarioInfo, + testFixtureGenerationContext); + + var method = Generator.GenerateTestMethod(testMethodGenerationContext); + + method.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor(NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Test"))), + new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), + ["Sample Scenario"]) + ]); + + method.Should().HaveNoParameters(); + + method.StepInvocations.Should().BeEquivalentTo( + [ + new StepInvocation(StepType.Action, 6, "When", "foo happens") + ]); + } + + [Fact] + public void GenerateTestMethod_CreatesMethodWithTestCaseAttributesWhenScenarioHasExamples() + { + var exampleSet1 = new ScenarioExampleSet(["what"], [["foo"], ["bar"]], ["example_tag"]); + var exampleSet2 = new ScenarioExampleSet(["what"], [["baz"]], []); + + var scenarioInfo = new ScenarioInformation( + "Sample Scenario Outline", + 22, + [], + [new ScenarioStep(StepType.Action, "When", " happens", 6)], + [exampleSet1, exampleSet2]); + + var featureInfo = new FeatureInformation( + "Sample", + null, + "en", + ["featureTag1"], + null); + + var testFixtureGenerationContext = new TestFixtureGenerationContext( + featureInfo, + [scenarioInfo], + "Sample.feature", + new NamespaceString("Reqnroll.Tests"), + Compilation, + Generator); + + var testMethodGenerationContext = new TestMethodGenerationContext( + scenarioInfo, + testFixtureGenerationContext); + + var method = Generator.GenerateTestMethod(testMethodGenerationContext); + + method.Should().HaveAttribuesEquivalentTo( + [ + new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("Description")), + ["Sample Scenario Outline"]), + new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("TestCase")), + ["foo", ImmutableArray.Create("example_tag")], + new Dictionary{{ new IdentifierString("Category"), "example_tag" } }.ToImmutableDictionary()), + new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("TestCase")), + ["bar", ImmutableArray.Create("example_tag")], + new Dictionary{{ new IdentifierString("Category"), "example_tag" } }.ToImmutableDictionary()), + new AttributeDescriptor( + NUnitNamespace + new SimpleTypeIdentifier(new IdentifierString("TestCase")), + ["baz", ImmutableArray.Empty]) + ]); + + method.Should().HaveParametersEquivalentTo( + [ + new ParameterDescriptor( + new IdentifierString("what"), + new NamespaceString("System") + new SimpleTypeIdentifier(new IdentifierString("String"))), + + new ParameterDescriptor( + new IdentifierString("_exampleTags"), + new ArrayTypeIdentifier(new NamespaceString("System") + new SimpleTypeIdentifier(new IdentifierString("String")))) + ]); + + method.StepInvocations.Should().BeEquivalentTo( + [ + new StepInvocation(StepType.Action, 6, "When", "{0} happens", [new IdentifierString("what")]) + ]); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitFeatureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitFeatureSourceGeneratorTests.cs new file mode 100644 index 000000000..14a9c4d30 --- /dev/null +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitFeatureSourceGeneratorTests.cs @@ -0,0 +1,112 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Reqnroll.FeatureSourceGenerator; +using Reqnroll.FeatureSourceGenerator.CSharp; +using Xunit.Abstractions; + +namespace Reqnroll.FeatureSourceGenerator.NUnit; + +public class NUnitFeatureSourceGeneratorTests(ITestOutputHelper output) +{ + [Fact] + public void GeneratorProducesNUnitOutputWhenWhenBuildPropertyConfiguredForNUnit() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.NUnit]); + + const string featureText = + """ + #language: en + @featureTag1 + Feature: Calculator + + @mytag + Scenario: Add two "simple" numbers + Given the first number is 50 + And the second number is 70 + When the two numbers are added + Then the result should be 120 + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { "build_property.ReqnrollTargetTestFramework", "NUnit" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() + .Which.Should().BeAssignableTo().Subject!; + + output.WriteLine($"Generated source:\n{generatedSyntaxTree}"); + + diagnostics.Should().BeEmpty(); + + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); + + generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") + .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") + .Which.Should().HaveSingleAttribute("global::NUnit.Framework.Description"); + } + + [Fact] + public void GeneratorProducesNUnitOutputWhenWhenEditorConfigConfiguredForNUnit() + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(asm => !asm.IsDynamic) + .Select(asm => MetadataReference.CreateFromFile(asm.Location)); + + var compilation = CSharpCompilation.Create("test", references: references); + + var generator = new CSharpTestFixtureSourceGenerator([BuiltInTestFrameworkHandlers.NUnit]); + + const string featureText = + """ + #language: en + @featureTag1 + Feature: Calculator + + @mytag + Scenario: Add two numbers + Given the first number is 50 + And the second number is 70 + When the two numbers are added + Then the result should be 120 + """; + + var optionsProvider = new FakeAnalyzerConfigOptionsProvider( + new InMemoryAnalyzerConfigOptions(new Dictionary + { + { "reqnroll.target_test_framework", "NUnit" } + })); + + var driver = CSharpGeneratorDriver + .Create(generator) + .AddAdditionalTexts([new FeatureFile("Calculator.feature", featureText)]) + .WithUpdatedAnalyzerConfigOptions(optionsProvider) + .RunGeneratorsAndUpdateCompilation(compilation, out var generatedCompilation, out var diagnostics); + + var generatedSyntaxTree = generatedCompilation.SyntaxTrees.Should().ContainSingle() + .Which.Should().BeAssignableTo().Subject!; + + output.WriteLine($"Generated source:\n{generatedSyntaxTree}"); + + diagnostics.Should().BeEmpty(); + + generatedSyntaxTree.GetDiagnostics().Should().BeEmpty(); + + generatedSyntaxTree.GetRoot().Should().ContainSingleNamespaceDeclaration("test") + .Which.Should().ContainSingleClassDeclaration("CalculatorFeature") + .Which.Should().HaveSingleAttribute("global::NUnit.Framework.Description"); + } +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs index b892b08fa..956049927 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs @@ -1,7 +1,6 @@ using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; -using Xunit.Abstractions; namespace Reqnroll.FeatureSourceGenerator.XUnit; public class XUnitCSharpTestFixtureGeneratorTests() : CSharpTestFixtureGeneratorTestBase(new XUnitHandler()) From e117f1600ae49ccd083d590997847357d8048fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1sp=C3=A1r=20Nagy?= Date: Sat, 27 Jul 2024 18:44:42 +0200 Subject: [PATCH 34/48] fix small typos and simple code things --- .../CSharp/CSharpTestFixtureGenerator.cs | 6 +++--- .../CSharp/MSTest/MSTestCSharpTestFixtureClass.cs | 2 +- .../NUnit/NUnitCSharpTestFixtureGenerator.cs | 2 +- .../CSharp/XUnit/XUnitCSharpTestFixtureClass.cs | 14 +++++++------- .../XUnit/XUnitCSharpTestFixtureGenerator.cs | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs index cc12c7e83..9b41593b5 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs @@ -29,7 +29,7 @@ protected virtual ImmutableArray GenerateTestMethodParamete { var scenario = context.ScenarioInformation; - // In the case the scenario defines no examples, we don't pass any paramters. + // In the case the scenario defines no examples, we don't pass any parameters. if (scenario.Examples.IsEmpty) { return ImmutableArray.Empty; @@ -77,7 +77,7 @@ protected virtual ImmutableArray GenerateStepInvocations( { var scenario = context.ScenarioInformation; - // In the case the scenario defines no examples, we don't pass any paramters to steps. + // In the case the scenario defines no examples, we don't pass any parameters to steps. if (scenario.Examples.IsEmpty) { return scenario.Steps @@ -121,7 +121,7 @@ protected virtual ImmutableArray> Generat { var scenario = context.ScenarioInformation; - // In the case the scenario defines no examples, we don't pass any paramters. + // In the case the scenario defines no examples, we don't pass any parameters. if (scenario.Examples.IsEmpty) { return ImmutableArray>.Empty; diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs index 2846089f2..2245bb324 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs @@ -68,7 +68,7 @@ protected virtual void RenderClassInitializeMethodTo(CSharpSourceTextBuilder sou { sourceBuilder .AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitialize]") - .AppendLine("public static Task IntializeFeatureAsync(global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)") + .AppendLine("public static Task InitializeFeatureAsync(global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)") .BeginBlock("{") .AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();") .AppendLine("return TestRunner.OnFeatureStartAsync(FeatureInfo);") diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs index ba9eece96..12a5d16e0 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureGenerator.cs @@ -61,7 +61,7 @@ protected override ImmutableArray GenerateTestMethodAttribu { cancellationToken.ThrowIfCancellationRequested(); - var values = example.Select(example => (object?)example.Value).ToList(); + var values = example.Select(exampleValue => (object?)exampleValue.Value).ToList(); values.Add(set.Tags); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs index 747c0a591..866db4d84 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs @@ -54,7 +54,7 @@ protected override void RenderTestFixtureContentTo( } protected virtual void RenderConstructorTo( - CSharpSourceTextBuilder SourceBuilder, + CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) { var className = Identifier.LocalType switch @@ -65,14 +65,14 @@ protected virtual void RenderConstructorTo( $"Writing constructor for {Identifier.GetType().Name} values is not implemented.") }; - // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. + // Lifetime class is initialized once per feature, then passed to the constructor of each test class instance. // Output helper is included to by registered in the container. - SourceBuilder.Append("public ").Append(className).AppendLine("(FeatureLifetime lifetime, " + + sourceBuilder.Append("public ").Append(className).AppendLine("(FeatureLifetime lifetime, " + "global::Xunit.Abstractions.ITestOutputHelper testOutputHelper)"); - SourceBuilder.BeginBlock("{"); - SourceBuilder.AppendLine("_lifetime = lifetime;"); - SourceBuilder.AppendLine("_testOutputHelper = testOutputHelper;"); - SourceBuilder.EndBlock("}"); + sourceBuilder.BeginBlock("{"); + sourceBuilder.AppendLine("_lifetime = lifetime;"); + sourceBuilder.AppendLine("_testOutputHelper = testOutputHelper;"); + sourceBuilder.EndBlock("}"); } protected virtual void RenderLifetimeClassTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs index 764fb2308..515e7fb4a 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureGenerator.cs @@ -55,7 +55,7 @@ protected override ImmutableArray GenerateTestMethodAttribu foreach (var example in set) { - var values = example.Select(example => (object?)example.Value).ToList(); + var values = example.Select(exampleValue => (object?)exampleValue.Value).ToList(); values.Add(set.Tags); From 0ca11812a5054c452cc7f42d532fc87458981812 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Thu, 1 Aug 2024 01:11:02 +0100 Subject: [PATCH 35/48] Handle empty test-framework selector --- .../TestFixtureSourceGenerator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 2ef1b7c5a..e9a6363ee 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -108,8 +108,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // 2. The ReqnrollTargetTestFramework from the build system properties (MSBuild project files or command-line argument) // 3. The assemblies referenced by the compilation indicating the presence of a test framework. ITestFixtureGenerator? generator; - if (options.TryGetValue("reqnroll.target_test_framework", out var targetTestFrameworkIdentifier) + if ((options.TryGetValue("reqnroll.target_test_framework", out var targetTestFrameworkIdentifier) || options.TryGetValue("build_property.ReqnrollTargetTestFramework", out targetTestFrameworkIdentifier)) + && !string.IsNullOrEmpty(targetTestFrameworkIdentifier)) { // Select the target framework from the option specified. generator = generatorInformation.CompatibleGenerators @@ -197,8 +198,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return [.. diagnostics]; } - Debugger.Launch(); - // Determine whether we should include ignored examples in our sample sets. var emitIgnoredExamples = false; if (options.TryGetValue("reqnroll.emit_ignored_examples", out var emitIgnoredExamplesValue) || From 24931b1314f78b2d73198f23f0a05c6bf6abf5dd Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Thu, 1 Aug 2024 01:14:06 +0100 Subject: [PATCH 36/48] Fix nullability of MSTest test runner variable --- .../CSharp/MSTest/MSTestCSharpTestMethod.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs index 7d540b7d4..1e43b6d2c 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs @@ -26,7 +26,15 @@ protected override void RenderTestRunnerLookupTo( { // For MSTest we use a test-runner assigned to the class. sourceBuilder - .AppendLine("global::Reqnroll.ITestRunner testRunner = TestRunner;") + .Append("global::Reqnroll.ITestRunner"); + + if (renderingOptions.UseNullableReferenceTypes) + { + sourceBuilder.Append('?'); + } + + sourceBuilder + .AppendLine(" testRunner = TestRunner;") .AppendLine("if (testRunner == null)") .BeginBlock("{") .AppendLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") From 1a0dbfee18c791b71ae407872977b0677a0ba7d5 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Thu, 1 Aug 2024 02:11:19 +0100 Subject: [PATCH 37/48] Fix test-method equality comparisons --- .../CSharp/CSharpTestFixtureClass.cs | 2 +- .../SourceModel/TestFixtureClass.cs | 4 ++-- Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs | 8 ++++++-- .../TestFixtureSourceGenerator.cs | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index 388d2228d..9722b445c 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -200,7 +200,7 @@ public bool Equals(CSharpTestFixtureClass? other) } return base.Equals(other) && - Methods.SequenceEqual(other.Methods) && + Methods.SequenceEqual(other!.Methods) && RenderingOptions.Equals(other.RenderingOptions); } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs index ae000deb5..8b79855e2 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs @@ -6,7 +6,7 @@ namespace Reqnroll.FeatureSourceGenerator.SourceModel; /// /// Represents a Reqnroll text fixture class. /// -public abstract class TestFixtureClass : IEquatable, IHasAttributes +public abstract class TestFixtureClass : IHasAttributes { /// /// Initializes a new instance of the test fixture class. @@ -77,7 +77,7 @@ protected TestFixtureClass(TestFixtureDescriptor descriptor) : public override bool Equals(object obj) => Equals(obj as TestFixtureClass); - public bool Equals(TestFixtureClass? other) + protected bool Equals(TestFixtureClass? other) { if (other is null) { diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs index 871cb60c2..f0ed2a634 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs @@ -98,6 +98,8 @@ public override int GetHashCode() unchecked { var hash = 86434151; + + hash *= 83155477 + GetType().GetHashCode(); hash *= 83155477 + Identifier.GetHashCode(); hash *= 83155477 + Scenario.GetHashCode(); @@ -110,7 +112,7 @@ public override int GetHashCode() } } - public bool Equals(TestMethod? other) + public virtual bool Equals(TestMethod? other) { if (other is null) { @@ -122,7 +124,9 @@ public bool Equals(TestMethod? other) return true; } - return Identifier.Equals(other.Identifier) && + return + GetType().Equals(other.GetType()) && + Identifier.Equals(other.Identifier) && Scenario.Equals(other.Scenario) && (StepInvocations.Equals(other.StepInvocations) || StepInvocations.SequenceEqual(other.StepInvocations)) && (Attributes.Equals(other.Attributes) || Attributes.SetEquals(other.Attributes)) && diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index e9a6363ee..f4bcb9759 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -91,6 +91,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Combine(generatorInformation) .SelectMany(static IEnumerable>> (input, cancellationToken) => { + Debugger.Launch(); + var (((featureFile, optionsProvider), compilationInfo), generatorInformation) = input; var options = optionsProvider.GetOptions(featureFile); From da7ebc003b8ec322b2aad779fb8829379425c763 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 4 Aug 2024 15:06:52 +0100 Subject: [PATCH 38/48] Add column-markers to source-line mapping --- .../CSharp/CSharpRenderingContext.cs | 13 + .../CSharp/CSharpSourceTextBuilder.cs | 363 ---------------- .../CSharp/CSharpSourceTextWriter.cs | 410 ++++++++++++++++++ ...x => CSharpSourceTextWriterResources.resx} | 0 .../CSharp/CSharpTestFixtureClass.cs | 105 +++-- .../CSharp/CSharpTestFixtureGenerator.cs | 4 +- .../CSharp/CSharpTestMethod.cs | 198 +++++---- .../MSTest/MSTestCSharpTestFixtureClass.cs | 72 +-- .../CSharp/MSTest/MSTestCSharpTestMethod.cs | 16 +- .../NUnit/NUnitCSharpTestFixtureClass.cs | 58 +-- .../CSharp/NUnit/NUnitCSharpTestMethod.cs | 14 +- .../XUnit/XUnitCSharpTestFixtureClass.cs | 88 ++-- .../CSharp/XUnit/XUnitCSharpTestMethod.cs | 14 +- .../SourceModel/ScenarioInformation.cs | 5 +- .../SourceModel/ScenarioStep.cs | 2 +- .../SourceModel/StepInvocation.cs | 15 +- .../SourceModel/TestFixtureClass.cs | 5 + .../SourceModel/TestMethod.cs | 6 +- .../TestFixtureSourceGenerator.cs | 32 +- .../TestFixtureSourceRenderingContext.cs | 23 + .../MSTestCSharpTestFixtureGeneratorTests.cs | 47 +- .../NUnitCSharpTestFixtureGeneratorTests.cs | 47 +- .../XUnitCSharpTestFixtureGeneratorTests.cs | 47 +- 23 files changed, 894 insertions(+), 690 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingContext.cs delete mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs rename Reqnroll.FeatureSourceGenerator/CSharp/{CSharpSourceTextBuilderResources.resx => CSharpSourceTextWriterResources.resx} (100%) create mode 100644 Reqnroll.FeatureSourceGenerator/TestFixtureSourceRenderingContext.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingContext.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingContext.cs new file mode 100644 index 000000000..8fac2ff21 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpRenderingContext.cs @@ -0,0 +1,13 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; +public class CSharpRenderingContext( + FeatureInformation feature, + string outputHintName, + CSharpSourceTextWriter writer, + CSharpRenderingOptions renderingOptions) : TestFixtureSourceRenderingContext(feature, outputHintName) +{ + public CSharpSourceTextWriter Writer { get; } = writer; + + public CSharpRenderingOptions RenderingOptions { get; } = renderingOptions; +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs deleted file mode 100644 index 0e1482549..000000000 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilder.cs +++ /dev/null @@ -1,363 +0,0 @@ -using System.Collections.Immutable; -using System.Text; -using Reqnroll.FeatureSourceGenerator.SourceModel; - -namespace Reqnroll.FeatureSourceGenerator.CSharp; - -public class CSharpSourceTextBuilder -{ - private static readonly UTF8Encoding Encoding = new(false); - - private readonly StringBuilder _buffer = new(); - - private bool _isFreshLine = true; - - private const string Indent = " "; - - private CodeBlock _context = new(); - - /// - /// Gets the depth of the current block. - /// - public int Depth => _context.Depth; - - public CSharpSourceTextBuilder Append(char c) - { - AppendIndentIfIsFreshLine(); - - _buffer.Append(c); - return this; - } - - public CSharpSourceTextBuilder Append(string text) - { - AppendIndentIfIsFreshLine(); - - _buffer.Append(text); - return this; - } - - public CSharpSourceTextBuilder AppendLiteralList(IEnumerable values) - { - var first = true; - - foreach (var value in values) - { - if (first) - { - first = false; - } - else - { - Append(", "); - } - - AppendLiteral(value); - } - - return this; - } - - public CSharpSourceTextBuilder AppendLiteral(object? value) - { - return value switch - { - null => Append("null"), - string s => AppendLiteral(s), - ImmutableArray array => AppendLiteralArray(array), - ImmutableArray array => AppendLiteralArray(array), - _ => throw new NotSupportedException($"Values of type {value.GetType().FullName} cannot be encoded as a literal value in C#.") - }; - } - - public CSharpSourceTextBuilder AppendLiteralArray(ImmutableArray array) - { - if (!CSharpSyntax.TypeAliases.TryGetValue(typeof(T), out var typeName)) - { - typeName = $"global::{typeof(T).FullName}"; - } - - Append("new ").Append(typeName); - - if (array.Length == 0) - { - return Append("[0]"); - } - - return Append("[] { ").AppendLiteralList(array).Append(" }"); - } - - public CSharpSourceTextBuilder AppendLiteral(string? s) - { - if (s == null) - { - return Append("null"); - } - - var escapedValue = CSharpSyntax.FormatLiteral(s); - - Append(escapedValue); - return this; - } - - private void AppendIndentIfIsFreshLine() - { - if (_isFreshLine) - { - AppendIndentToDepth(); - - _isFreshLine = false; - } - } - - private void AppendIndentToDepth() - { - for (var i = 0; i < Depth; i++) - { - _buffer.Append(Indent); - } - } - - internal void Reset() => _buffer.Clear(); - - public CSharpSourceTextBuilder AppendDirective(string directive) - { - if (!_isFreshLine) - { - throw new InvalidOperationException(CSharpSourceTextBuilderResources.CannotAppendDirectiveUnlessAtStartOfLine); - } - - _buffer.Append(directive); - - _buffer.AppendLine(); - - _isFreshLine = true; - return this; - } - - public CSharpSourceTextBuilder AppendLine() - { - AppendIndentIfIsFreshLine(); - - _buffer.AppendLine(); - - _isFreshLine = true; - return this; - } - - public CSharpSourceTextBuilder AppendLine(string text) - { - AppendIndentIfIsFreshLine(); - - _buffer.AppendLine(text); - - _isFreshLine = true; - return this; - } - - /// - /// Starts a new code block. - /// - public CSharpSourceTextBuilder BeginBlock() - { - _context = new CodeBlock(_context); - return this; - } - - /// - /// Appends the specified text and starts a new block. - /// - /// The text to append. - public CSharpSourceTextBuilder BeginBlock(string text) => AppendLine(text).BeginBlock(); - - /// - /// Ends the current block and begins a new line. - /// - /// - /// The builder is not currently in a block (the is zero.) - /// - public CSharpSourceTextBuilder EndBlock() - { - if (_context.Parent == null) - { - throw new InvalidOperationException(CSharpSourceTextBuilderResources.NotInCodeBlock); - } - - _context = _context.Parent; - return AppendLine(); - } - - /// - /// Ends the current block, appends the specified text and begins a new line. - /// - /// The text to append. - /// - /// The builder is not currently in a block (the is zero.) - /// - public CSharpSourceTextBuilder EndBlock(string text) - { - if (_context.Parent == null) - { - throw new InvalidOperationException(CSharpSourceTextBuilderResources.NotInCodeBlock); - } - - _context = _context.Parent; - return AppendLine(text); - } - - /// - /// Gets the value of this instance as a string. - /// - /// A string containing all text appended to the builder. - public override string ToString() => _buffer.ToString(); - - private class CodeBlock - { - public CodeBlock() - { - Depth = 0; - } - - public CodeBlock(CodeBlock parent) - { - Parent = parent; - Depth = parent.Depth + 1; - } - - public int Depth { get; } - - public CodeBlock? Parent { get; } - - public bool InSection { get; set; } - - public bool HasSection { get; set; } - } - - public SourceText ToSourceText() - { - return SourceText.From(_buffer.ToString(), Encoding); - } - - public CSharpSourceTextBuilder AppendAttributeBlock(AttributeDescriptor attribute) - { - Append('['); - AppendTypeReference(attribute.Type); - - if (attribute.NamedArguments.Count > 0 || attribute.PositionalArguments.Length > 0) - { - Append('('); - - AppendLiteralList(attribute.PositionalArguments); - - var firstProperty = true; - foreach (var (name, value) in attribute.NamedArguments) - { - if (firstProperty) - { - if (!attribute.PositionalArguments.IsEmpty) - { - Append(", "); - } - - firstProperty = false; - } - else - { - Append(", "); - } - - Append(name).Append(" = ").AppendLiteral(value); - } - - Append(')'); - } - - Append(']'); - - return this; - } - - public CSharpSourceTextBuilder AppendTypeReference(TypeIdentifier type) - { - return type switch - { - LocalTypeIdentifier localType => AppendTypeReference(localType), - QualifiedTypeIdentifier qualifiedType => AppendTypeReference(qualifiedType), - ArrayTypeIdentifier arrayType => AppendTypeReference(arrayType), - NestedTypeIdentifier nestedType => AppendTypeReference(nestedType), - _ => throw new NotImplementedException($"Appending references of type {type.GetType().Name} is not implemented.") - }; - } - - public CSharpSourceTextBuilder AppendTypeReference(LocalTypeIdentifier type) - { - return type switch - { - SimpleTypeIdentifier simpleType => AppendTypeReference(simpleType), - GenericTypeIdentifier genericType => AppendTypeReference(genericType), - _ => throw new NotImplementedException($"Appending references of type {type.GetType().Name} is not implemented.") - }; - } - - public CSharpSourceTextBuilder AppendTypeReference(NestedTypeIdentifier type) - { - return AppendTypeReference(type.EncapsulatingType).Append('.').AppendTypeReference(type.LocalType); - } - - public CSharpSourceTextBuilder AppendTypeReference(SimpleTypeIdentifier type) - { - Append(type.Name); - - if (type.IsNullable) - { - Append('?'); - } - - return this; - } - - public CSharpSourceTextBuilder AppendTypeReference(GenericTypeIdentifier type) - { - Append(type.Name); - - Append('<'); - - AppendTypeReference(type.TypeArguments[0]); - - for (var i = 1; i < type.TypeArguments.Length; i++) - { - Append(','); - AppendTypeReference(type.TypeArguments[i]); - } - - Append('>'); - - if (type.IsNullable) - { - Append('?'); - } - - return this; - } - - public CSharpSourceTextBuilder AppendTypeReference(QualifiedTypeIdentifier type) - { - Append("global::").Append(type.Namespace).Append('.'); - - AppendTypeReference(type.LocalType); - - return this; - } - - public CSharpSourceTextBuilder AppendTypeReference(ArrayTypeIdentifier type) - { - AppendTypeReference(type.ItemType).Append("[]"); - - if (type.IsNullable) - { - Append('?'); - } - - return this; - } -} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs new file mode 100644 index 000000000..b7065f119 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs @@ -0,0 +1,410 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text; +using Reqnroll.FeatureSourceGenerator.SourceModel; + +namespace Reqnroll.FeatureSourceGenerator.CSharp; + +public class CSharpSourceTextWriter +{ + class Buffer + { + private readonly StringBuilder _sb = new(); + + public int Line { get; private set; } + + public int Character { get; private set; } + + public void Append(char c) + { + Debug.Assert(!Environment.NewLine.Contains(c), "Use WriteLine to append a line."); + _sb.Append(c); + Character++; + } + + public void Append(string s) + { + Debug.Assert(!s.Contains(Environment.NewLine), "Use WriteLine to append a line."); + _sb.Append(s); + Character++; + } + + public void AppendLine() + { + _sb.AppendLine(); + Character = 0; + Line++; + } + + public void AppendLine(char c) + { + _sb.Append(c); + _sb.AppendLine(); + Character = 0; + Line++; + } + + public void AppendLine(string s) + { + _sb.AppendLine(s); + Character = 0; + Line++; + } + + public override string ToString() => _sb.ToString(); + } + + class CodeBlock + { + public CodeBlock() + { + Depth = 0; + } + + public CodeBlock(CodeBlock parent) + { + Parent = parent; + Depth = parent.Depth + 1; + } + + public int Depth { get; } + + public CodeBlock? Parent { get; } + + public bool InSection { get; set; } + + public bool HasSection { get; set; } + } + + private static readonly UTF8Encoding Encoding = new(false); + + private readonly Buffer _buffer = new(); + + private const string Indent = " "; + + private CodeBlock _context = new(); + + /// + /// Gets the depth of the current block. + /// + public int Depth => _context.Depth; + + /// + /// Gets the position of the writer. + /// + public LinePosition Position => new(_buffer.Line, _buffer.Character); + + /// + /// Gets the number of characters which will be appended to the next new line in the current block. + /// + public int NewLineOffset => Indent.Length * Depth; + + public CSharpSourceTextWriter Write(char c) + { + WriteIndentIfIsFreshLine(); + + _buffer.Append(c); + return this; + } + + public CSharpSourceTextWriter Write(string text) + { + WriteIndentIfIsFreshLine(); + + _buffer.Append(text); + return this; + } + + public CSharpSourceTextWriter WriteLiteralList(IEnumerable values) + { + var first = true; + + foreach (var value in values) + { + if (first) + { + first = false; + } + else + { + Write(", "); + } + + WriterLiteral(value); + } + + return this; + } + + public CSharpSourceTextWriter WriterLiteral(object? value) + { + return value switch + { + null => Write("null"), + string s => WriteLiteral(s), + ImmutableArray array => WriteLiteralArray(array), + ImmutableArray array => WriteLiteralArray(array), + _ => throw new NotSupportedException($"Values of type {value.GetType().FullName} cannot be encoded as a literal value in C#.") + }; + } + + public CSharpSourceTextWriter WriteLiteralArray(ImmutableArray array) + { + if (!CSharpSyntax.TypeAliases.TryGetValue(typeof(T), out var typeName)) + { + typeName = $"global::{typeof(T).FullName}"; + } + + Write("new ").Write(typeName); + + if (array.Length == 0) + { + return Write("[0]"); + } + + return Write("[] { ").WriteLiteralList(array).Write(" }"); + } + + public CSharpSourceTextWriter WriteLiteral(string? s) + { + if (s == null) + { + return Write("null"); + } + + var escapedValue = CSharpSyntax.FormatLiteral(s); + + Write(escapedValue); + return this; + } + + private void WriteIndentIfIsFreshLine() + { + if (_buffer.Character == 0) + { + for (var i = 0; i < Depth; i++) + { + _buffer.Append(Indent); + } + } + } + + public CSharpSourceTextWriter WriteLineDirective(LinePosition start, LinePosition end, int offset, string filePath) + { + return WriteDirective($"#line ({start.Line}, {start.Character}) - ({end.Line}, {end.Character}) {offset} \"{filePath}\""); + } + + public CSharpSourceTextWriter WriteDirective(string directive) + { + if (_buffer.Character != 0) + { + throw new InvalidOperationException(CSharpSourceTextWriterResources.CannotAppendDirectiveUnlessAtStartOfLine); + } + + _buffer.AppendLine(directive); + + return this; + } + + public CSharpSourceTextWriter WriteLine() + { + WriteIndentIfIsFreshLine(); + + _buffer.AppendLine(); + + return this; + } + + public CSharpSourceTextWriter WriteLine(string text) + { + WriteIndentIfIsFreshLine(); + + _buffer.AppendLine(text); + + return this; + } + + /// + /// Starts a new code block. + /// + public CSharpSourceTextWriter BeginBlock() + { + _context = new CodeBlock(_context); + return this; + } + + /// + /// Appends the specified text and starts a new block. + /// + /// The text to append. + public CSharpSourceTextWriter BeginBlock(string text) => WriteLine(text).BeginBlock(); + + /// + /// Ends the current block and begins a new line. + /// + /// + /// The builder is not currently in a block (the is zero.) + /// + public CSharpSourceTextWriter EndBlock() + { + if (_context.Parent == null) + { + throw new InvalidOperationException(CSharpSourceTextWriterResources.NotInCodeBlock); + } + + _context = _context.Parent; + return WriteLine(); + } + + /// + /// Ends the current block, appends the specified text and begins a new line. + /// + /// The text to append. + /// + /// The builder is not currently in a block (the is zero.) + /// + public CSharpSourceTextWriter EndBlock(string text) + { + if (_context.Parent == null) + { + throw new InvalidOperationException(CSharpSourceTextWriterResources.NotInCodeBlock); + } + + _context = _context.Parent; + return WriteLine(text); + } + + /// + /// Gets the value of this instance as a string. + /// + /// A string containing all text appended to the builder. + public override string ToString() => _buffer.ToString(); + + public SourceText ToSourceText() + { + return SourceText.From(_buffer.ToString(), Encoding); + } + + public CSharpSourceTextWriter WriteAttributeBlock(AttributeDescriptor attribute) + { + Write('['); + WriteTypeReference(attribute.Type); + + if (attribute.NamedArguments.Count > 0 || attribute.PositionalArguments.Length > 0) + { + Write('('); + + WriteLiteralList(attribute.PositionalArguments); + + var firstProperty = true; + foreach (var (name, value) in attribute.NamedArguments) + { + if (firstProperty) + { + if (!attribute.PositionalArguments.IsEmpty) + { + Write(", "); + } + + firstProperty = false; + } + else + { + Write(", "); + } + + Write(name).Write(" = ").WriterLiteral(value); + } + + Write(')'); + } + + Write(']'); + + return this; + } + + public CSharpSourceTextWriter WriteTypeReference(TypeIdentifier type) + { + return type switch + { + LocalTypeIdentifier localType => WriteTypeReference(localType), + QualifiedTypeIdentifier qualifiedType => WriteTypeReference(qualifiedType), + ArrayTypeIdentifier arrayType => WriteTypeReference(arrayType), + NestedTypeIdentifier nestedType => WriteTypeReference(nestedType), + _ => throw new NotImplementedException($"Appending references of type {type.GetType().Name} is not implemented.") + }; + } + + public CSharpSourceTextWriter WriteTypeReference(LocalTypeIdentifier type) + { + return type switch + { + SimpleTypeIdentifier simpleType => WriteTypeReference(simpleType), + GenericTypeIdentifier genericType => WriteTypeReference(genericType), + _ => throw new NotImplementedException($"Appending references of type {type.GetType().Name} is not implemented.") + }; + } + + public CSharpSourceTextWriter WriteTypeReference(NestedTypeIdentifier type) + { + return WriteTypeReference(type.EncapsulatingType).Write('.').WriteTypeReference(type.LocalType); + } + + public CSharpSourceTextWriter WriteTypeReference(SimpleTypeIdentifier type) + { + Write(type.Name); + + if (type.IsNullable) + { + Write('?'); + } + + return this; + } + + public CSharpSourceTextWriter WriteTypeReference(GenericTypeIdentifier type) + { + Write(type.Name); + + Write('<'); + + WriteTypeReference(type.TypeArguments[0]); + + for (var i = 1; i < type.TypeArguments.Length; i++) + { + Write(','); + WriteTypeReference(type.TypeArguments[i]); + } + + Write('>'); + + if (type.IsNullable) + { + Write('?'); + } + + return this; + } + + public CSharpSourceTextWriter WriteTypeReference(QualifiedTypeIdentifier type) + { + Write("global::").Write(type.Namespace).Write('.'); + + WriteTypeReference(type.LocalType); + + return this; + } + + public CSharpSourceTextWriter WriteTypeReference(ArrayTypeIdentifier type) + { + WriteTypeReference(type.ItemType).Write("[]"); + + if (type.IsNullable) + { + Write('?'); + } + + return this; + } +} diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilderResources.resx b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriterResources.resx similarity index 100% rename from Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextBuilderResources.resx rename to Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriterResources.resx diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs index 9722b445c..e28d98b49 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureClass.cs @@ -48,27 +48,27 @@ public CSharpTestFixtureClass( public override SourceText Render(CancellationToken cancellationToken = default) { - var buffer = new CSharpSourceTextBuilder(); + var buffer = new CSharpSourceTextWriter(); RenderTo(buffer, cancellationToken); return SourceText.From(buffer.ToString(), Encoding); } - public void RenderTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken = default) + public void RenderTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken = default) { - sourceBuilder.Append("namespace ").Append(Identifier.Namespace).AppendLine(); - sourceBuilder.BeginBlock("{"); + writer.Write("namespace ").Write(Identifier.Namespace).WriteLine(); + writer.BeginBlock("{"); if (!NamespaceUsings.IsEmpty) { foreach (var import in NamespaceUsings) { - sourceBuilder.Append("using ").Append(import).AppendLine(";"); + writer.Write("using ").Write(import).WriteLine(";"); } - sourceBuilder.AppendLine(); + writer.WriteLine(); } if (!Attributes.IsEmpty) @@ -76,98 +76,97 @@ public void RenderTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken ca foreach (var attribute in Attributes) { cancellationToken.ThrowIfCancellationRequested(); - sourceBuilder.AppendAttributeBlock(attribute); - sourceBuilder.AppendLine(); + writer.WriteAttributeBlock(attribute); + writer.WriteLine(); } } - sourceBuilder.Append("public partial class ").AppendTypeReference(Identifier.LocalType); + if (RenderingOptions.EnableLineMapping && FeatureInformation.FilePath != null) + { + writer.WriteDirective("#line hidden"); + writer.WriteLine(); + } + + writer.Write("public partial class ").WriteTypeReference(Identifier.LocalType); if (!Interfaces.IsEmpty) { - sourceBuilder.Append(" :").AppendTypeReference(Interfaces[0]); + writer.Write(" :").WriteTypeReference(Interfaces[0]); for (var i = 1; i < Interfaces.Length; i++) { - sourceBuilder.Append(" ,").AppendTypeReference(Interfaces[i]); + writer.Write(" ,").WriteTypeReference(Interfaces[i]); } } - sourceBuilder.AppendLine(); + writer.WriteLine(); - sourceBuilder.BeginBlock("{"); - - if (RenderingOptions.EnableLineMapping && FeatureInformation.FilePath != null) - { - sourceBuilder.AppendDirective($"#line 1 \"{FeatureInformation.FilePath}\""); - sourceBuilder.AppendDirective("#line hidden"); - sourceBuilder.AppendLine(); - } + writer.BeginBlock("{"); - RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + RenderTestFixtureContentTo(writer, cancellationToken); - sourceBuilder.EndBlock("}"); - sourceBuilder.EndBlock("}"); + writer.EndBlock("}"); + writer.EndBlock("}"); } - protected virtual void RenderTestFixtureContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected virtual void RenderTestFixtureContentTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - RenderFeatureInformationPropertiesTo(sourceBuilder); + RenderFeatureInformationPropertiesTo(writer); - RenderScenarioInitializeMethodTo(sourceBuilder, cancellationToken); + RenderScenarioInitializeMethodTo(writer, cancellationToken); - sourceBuilder.AppendLine(); + writer.WriteLine(); - RenderMethodsTo(sourceBuilder, cancellationToken); + RenderMethodsTo(writer, cancellationToken); } - private void RenderFeatureInformationPropertiesTo(CSharpSourceTextBuilder sourceBuilder) + private void RenderFeatureInformationPropertiesTo(CSharpSourceTextWriter writer) { - sourceBuilder - .Append("private static readonly string[] FeatureTags = new string[] { ") - .AppendLiteralList(FeatureInformation.Tags) - .AppendLine(" };"); + writer + .Write("private static readonly string[] FeatureTags = new string[] { ") + .WriteLiteralList(FeatureInformation.Tags) + .WriteLine(" };"); - sourceBuilder.AppendLine(); + writer.WriteLine(); - sourceBuilder - .AppendLine("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") + writer + .WriteLine("private static readonly global::Reqnroll.FeatureInfo FeatureInfo = new global::Reqnroll.FeatureInfo(") .BeginBlock() - .Append("new global::System.Globalization.CultureInfo(").AppendLiteral(FeatureInformation.Language).AppendLine("), ") - .AppendLiteral(Path.GetDirectoryName(FeatureInformation.FilePath)).AppendLine(", ") - .AppendLiteral(FeatureInformation.Name).AppendLine(", ") - .AppendLiteral(FeatureInformation.Description).AppendLine(", ") - .AppendLine("global::Reqnroll.ProgrammingLanguage.CSharp, ") - .AppendLine("FeatureTags);") + .Write("new global::System.Globalization.CultureInfo(").WriteLiteral(FeatureInformation.Language).WriteLine("), ") + .WriteLiteral(Path.GetDirectoryName(FeatureInformation.FilePath)).WriteLine(", ") + .WriteLiteral(FeatureInformation.Name).WriteLine(", ") + .WriteLiteral(FeatureInformation.Description).WriteLine(", ") + .WriteLine("global::Reqnroll.ProgrammingLanguage.CSharp, ") + .WriteLine("FeatureTags);") .EndBlock(); } private void RenderScenarioInitializeMethodTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - sourceBuilder.AppendLine( + writer.WriteLine( "private global::System.Threading.Tasks.Task ScenarioInitialize(" + "global::Reqnroll.ITestRunner testRunner, " + "global::Reqnroll.ScenarioInfo scenarioInfo)"); - sourceBuilder.BeginBlock("{"); + writer.BeginBlock("{"); - RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + RenderScenarioInitializeMethodBodyTo(writer, cancellationToken); - sourceBuilder.AppendLine("return global::System.Threading.Tasks.Task.CompletedTask;"); + writer.WriteLine("return global::System.Threading.Tasks.Task.CompletedTask;"); - sourceBuilder.EndBlock("}"); + writer.EndBlock("}"); } protected virtual void RenderScenarioInitializeMethodBodyTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - sourceBuilder.AppendLine("testRunner.OnScenarioInitialize(scenarioInfo);"); + writer.WriteLine("testRunner.OnScenarioInitialize(scenarioInfo);"); } - protected virtual void RenderMethodsTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected virtual void RenderMethodsTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { var first = true; foreach (var method in Methods) @@ -176,10 +175,10 @@ protected virtual void RenderMethodsTo(CSharpSourceTextBuilder sourceBuilder, Ca if (!first) { - sourceBuilder.AppendLine(); + writer.WriteLine(); } - method.RenderTo(sourceBuilder, RenderingOptions); + method.RenderTo(writer, RenderingOptions); if (first) { diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs index 9b41593b5..9b9cacdba 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs @@ -81,7 +81,7 @@ protected virtual ImmutableArray GenerateStepInvocations( if (scenario.Examples.IsEmpty) { return scenario.Steps - .Select(step => new StepInvocation(step.StepType, step.LineNumber, step.Keyword, step.Text)) + .Select(step => new StepInvocation(step.StepType, step.Position, step.Keyword, step.Text)) .ToImmutableArray(); } @@ -109,7 +109,7 @@ protected virtual ImmutableArray GenerateStepInvocations( } } - invocations.Add(new StepInvocation(step.StepType, step.LineNumber, step.Keyword, text, arguments.ToImmutableArray())); + invocations.Add(new StepInvocation(step.StepType, step.Position, step.Keyword, text, arguments.ToImmutableArray())); } return invocations.ToImmutableArray(); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs index 67c937b9a..15828677d 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs @@ -6,12 +6,12 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp; public class CSharpTestMethod : TestMethod, IEquatable { public CSharpTestMethod( - IdentifierString identifier, - ScenarioInformation scenario, - ImmutableArray stepInvocations, - ImmutableArray attributes = default, - ImmutableArray parameters = default, - ImmutableArray> scenarioParameters = default) + IdentifierString identifier, + ScenarioInformation scenario, + ImmutableArray stepInvocations, + ImmutableArray attributes = default, + ImmutableArray parameters = default, + ImmutableArray> scenarioParameters = default) : base(identifier, scenario, stepInvocations, attributes, parameters, scenarioParameters) { } @@ -27,7 +27,7 @@ public CSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor) public override int GetHashCode() => base.GetHashCode(); public void RenderTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken = default) { @@ -36,17 +36,17 @@ public void RenderTo( foreach (var attribute in Attributes) { cancellationToken.ThrowIfCancellationRequested(); - sourceBuilder.AppendAttributeBlock(attribute); - sourceBuilder.AppendLine(); + writer.WriteAttributeBlock(attribute); + writer.WriteLine(); } } // Our test methods are always asynchronous and never return a value. - sourceBuilder.Append("public async Task ").Append(Identifier); + writer.Write("public async Task ").Write(Identifier); if (!Parameters.IsEmpty) { - sourceBuilder.BeginBlock("("); + writer.BeginBlock("("); var first = true; foreach (var parameter in Parameters) @@ -54,104 +54,114 @@ public void RenderTo( cancellationToken.ThrowIfCancellationRequested(); if (!first) { - sourceBuilder.AppendLine(","); + writer.WriteLine(","); } - sourceBuilder - .AppendTypeReference(parameter.Type) - .Append(' ') - .Append(parameter.Name); + writer + .WriteTypeReference(parameter.Type) + .Write(' ') + .Write(parameter.Name); first = false; } - sourceBuilder.EndBlock(")"); + writer.EndBlock(")"); } else { - sourceBuilder.AppendLine("()"); + writer.WriteLine("()"); } - sourceBuilder.BeginBlock("{"); + writer.BeginBlock("{"); - RenderMethodBodyTo(sourceBuilder, renderingOptions, cancellationToken); + RenderMethodBodyTo(writer, renderingOptions, cancellationToken); - sourceBuilder.EndBlock("}"); + writer.EndBlock("}"); } protected virtual void RenderMethodBodyTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - RenderTestRunnerLookupTo(sourceBuilder, renderingOptions, cancellationToken); - sourceBuilder.AppendLine(); + RenderTestRunnerLookupTo(writer, renderingOptions, cancellationToken); + writer.WriteLine(); - RenderScenarioInfoTo(sourceBuilder, renderingOptions, cancellationToken); - sourceBuilder.AppendLine(); + RenderScenarioInfoTo(writer, renderingOptions, cancellationToken); + writer.WriteLine(); - sourceBuilder.AppendLine("try"); - sourceBuilder.BeginBlock("{"); + writer.WriteLine("try"); + writer.BeginBlock("{"); if (renderingOptions.EnableLineMapping) { - sourceBuilder.AppendDirective($"#line {Scenario.LineNumber}"); + writer.WriteLineDirective( + Scenario.KeywordAndNamePosition.StartLinePosition, + Scenario.KeywordAndNamePosition.EndLinePosition, + writer.NewLineOffset, + Scenario.KeywordAndNamePosition.Path); } - sourceBuilder.AppendLine("await ScenarioInitialize(testRunner, scenarioInfo);"); + writer.WriteLine("await ScenarioInitialize(testRunner, scenarioInfo);"); if (renderingOptions.EnableLineMapping) { - sourceBuilder.AppendDirective("#line hidden"); + writer.WriteDirective("#line hidden"); } - sourceBuilder.AppendLine(); + writer.WriteLine(); - sourceBuilder.AppendLine("if (global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags))"); - sourceBuilder.BeginBlock("{"); - sourceBuilder.AppendLine("testRunner.SkipScenario();"); - sourceBuilder.EndBlock("}"); - sourceBuilder.AppendLine("else"); - sourceBuilder.BeginBlock("{"); - sourceBuilder.AppendLine("await testRunner.OnScenarioStartAsync();"); - sourceBuilder.AppendLine(); - sourceBuilder.AppendLine("// start: invocation of scenario steps"); + writer.WriteLine("if (global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags))"); + writer.BeginBlock("{"); + writer.WriteLine("testRunner.SkipScenario();"); + writer.EndBlock("}"); + writer.WriteLine("else"); + writer.BeginBlock("{"); + writer.WriteLine("await testRunner.OnScenarioStartAsync();"); + writer.WriteLine(); + writer.WriteLine("// start: invocation of scenario steps"); foreach (var invocation in StepInvocations) { cancellationToken.ThrowIfCancellationRequested(); - RenderScenarioStepInvocationTo(invocation, sourceBuilder, renderingOptions, cancellationToken); + RenderScenarioStepInvocationTo(invocation, writer, renderingOptions, cancellationToken); } - sourceBuilder.AppendLine("// end: invocation of scenario steps"); - sourceBuilder.EndBlock("}"); - sourceBuilder.AppendLine(); - sourceBuilder.AppendLine("// finishing the scenario"); - sourceBuilder.AppendLine("await testRunner.CollectScenarioErrorsAsync();"); - - sourceBuilder.EndBlock("}"); - sourceBuilder.AppendLine("finally"); - sourceBuilder.BeginBlock("{"); - sourceBuilder.AppendLine("await testRunner.OnScenarioEndAsync();"); - sourceBuilder.EndBlock("}"); + writer.WriteLine("// end: invocation of scenario steps"); + writer.EndBlock("}"); + writer.WriteLine(); + writer.WriteLine("// finishing the scenario"); + writer.WriteLine("await testRunner.CollectScenarioErrorsAsync();"); + + writer.EndBlock("}"); + writer.WriteLine("finally"); + writer.BeginBlock("{"); + writer.WriteLine("await testRunner.OnScenarioEndAsync();"); + writer.EndBlock("}"); } protected virtual void RenderScenarioStepInvocationTo( StepInvocation invocation, - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { - if (renderingOptions.EnableLineMapping) + var position = invocation.Position; + + if (position.Path != null && renderingOptions.EnableLineMapping) { - sourceBuilder.AppendDirective($"#line {invocation.SourceLineNumber}"); + writer.WriteLineDirective( + position.StartLinePosition, + position.EndLinePosition, + writer.NewLineOffset, + position.Path); } - sourceBuilder - .Append("await testRunner.") - .Append( + writer + .Write("await testRunner.") + .Write( invocation.Type switch { StepType.Context => "Given", @@ -160,88 +170,88 @@ protected virtual void RenderScenarioStepInvocationTo( StepType.Conjunction => "And", _ => throw new NotSupportedException() }) - .Append("Async("); + .Write("Async("); if (invocation.Arguments.IsEmpty) { - sourceBuilder.AppendLiteral(invocation.Text); + writer.WriteLiteral(invocation.Text); } else { - sourceBuilder.Append("string.Format(").AppendLiteral(invocation.Text); + writer.Write("string.Format(").WriteLiteral(invocation.Text); foreach (var argument in invocation.Arguments) { - sourceBuilder.Append(", ").Append(argument); + writer.Write(", ").Write(argument); } - sourceBuilder.Append(")"); + writer.Write(")"); } - sourceBuilder - .Append(", null, null, ") - .AppendLiteral(invocation.Keyword) - .AppendLine(");"); + writer + .Write(", null, null, ") + .WriteLiteral(invocation.Keyword) + .WriteLine(");"); if (renderingOptions.EnableLineMapping) { - sourceBuilder.AppendDirective("#line hidden"); + writer.WriteDirective("#line hidden"); } } protected virtual void RenderScenarioInfoTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { - sourceBuilder.AppendLine("// start: calculate ScenarioInfo"); - sourceBuilder - .Append("var tagsOfScenario = new string[] { ") - .AppendLiteralList(Scenario.Tags) - .Append(" }"); + writer.WriteLine("// start: calculate ScenarioInfo"); + writer + .Write("var tagsOfScenario = new string[] { ") + .WriteLiteralList(Scenario.Tags) + .Write(" }"); // If a parameter has been defined for passing tags from the example, include it in the scenario's tags. var exampleTagsParameter = Parameters.FirstOrDefault(parameter => parameter.Name == CSharpSyntax.ExampleTagsParameterName); if (exampleTagsParameter != null) { - sourceBuilder.Append(".Concat(").Append(exampleTagsParameter.Name).Append(").ToArray()"); + writer.Write(".Concat(").Write(exampleTagsParameter.Name).Write(").ToArray()"); } - sourceBuilder.AppendLine(";"); + writer.WriteLine(";"); - sourceBuilder.AppendLine( + writer.WriteLine( "var argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); // needed for scenario outlines"); foreach (var (name, value) in ParametersOfScenario) { - sourceBuilder - .Append("argumentsOfScenario.Add(").AppendLiteral(name).Append(", ").Append(value).Append(");"); + writer + .Write("argumentsOfScenario.Add(").WriteLiteral(name).Write(", ").Write(value).Write(");"); } if (Scenario.Rule == null) { - sourceBuilder.AppendLine("var inheritedTags = FeatureTags;"); + writer.WriteLine("var inheritedTags = FeatureTags;"); } else { - sourceBuilder - .Append("var ruleTags = new string[] { ") - .AppendLiteralList(Scenario.Rule.Tags) - .AppendLine(" };"); + writer + .Write("var ruleTags = new string[] { ") + .WriteLiteralList(Scenario.Rule.Tags) + .WriteLine(" };"); - sourceBuilder.AppendLine("var inheritedTags = FeatureTags.Concat(ruleTags).ToArray();"); + writer.WriteLine("var inheritedTags = FeatureTags.Concat(ruleTags).ToArray();"); } - sourceBuilder - .Append("var scenarioInfo = new global::Reqnroll.ScenarioInfo(") - .AppendLiteral(Scenario.Name) - .AppendLine(", null, tagsOfScenario, argumentsOfScenario, inheritedTags);"); - sourceBuilder.AppendLine("// end: calculate ScenarioInfo"); + writer + .Write("var scenarioInfo = new global::Reqnroll.ScenarioInfo(") + .WriteLiteral(Scenario.Name) + .WriteLine(", null, tagsOfScenario, argumentsOfScenario, inheritedTags);"); + writer.WriteLine("// end: calculate ScenarioInfo"); } /// /// Renders the code to provide the test runner instance for the test method. /// - /// The source builder to append the code to. + /// The source builder to append the code to. /// Options which control the rendering of the C# code. /// A token used to signal when rendering should be canceled. /// @@ -252,10 +262,10 @@ protected virtual void RenderScenarioInfoTo( /// /// protected virtual void RenderTestRunnerLookupTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { - sourceBuilder.AppendLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();"); + writer.WriteLine("var testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();"); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs index 2245bb324..4d5bed339 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureClass.cs @@ -23,83 +23,83 @@ public MSTestCSharpTestFixtureClass( { } - protected override void RenderTestFixtureContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected override void RenderTestFixtureContentTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - RenderTestRunnerFieldTo(sourceBuilder); + RenderTestRunnerFieldTo(writer); - sourceBuilder.AppendLine(); + writer.WriteLine(); - RenderTestContextPropertyTo(sourceBuilder); + RenderTestContextPropertyTo(writer); - sourceBuilder.AppendLine(); + writer.WriteLine(); - RenderClassInitializeMethodTo(sourceBuilder, cancellationToken); + RenderClassInitializeMethodTo(writer, cancellationToken); - sourceBuilder.AppendLine(); + writer.WriteLine(); - RenderClassCleanupMethodTo(sourceBuilder, cancellationToken); + RenderClassCleanupMethodTo(writer, cancellationToken); - sourceBuilder.AppendLine(); + writer.WriteLine(); - base.RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + base.RenderTestFixtureContentTo(writer, cancellationToken); } - private void RenderTestRunnerFieldTo(CSharpSourceTextBuilder sourceBuilder) + private void RenderTestRunnerFieldTo(CSharpSourceTextWriter writer) { - sourceBuilder.Append("private static global::Reqnroll.ITestRunner"); + writer.Write("private static global::Reqnroll.ITestRunner"); if (RenderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append("?"); + writer.Write("?"); } - sourceBuilder.AppendLine(" TestRunner;"); + writer.WriteLine(" TestRunner;"); } - private void RenderTestContextPropertyTo(CSharpSourceTextBuilder sourceBuilder) + private void RenderTestContextPropertyTo(CSharpSourceTextWriter writer) { - sourceBuilder.Append("public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext"); + writer.Write("public global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext"); if (RenderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append("?"); + writer.Write("?"); } - sourceBuilder.AppendLine(" TestContext { get; set; }"); + writer.WriteLine(" TestContext { get; set; }"); } - protected virtual void RenderClassInitializeMethodTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected virtual void RenderClassInitializeMethodTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - sourceBuilder - .AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitialize]") - .AppendLine("public static Task InitializeFeatureAsync(global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)") + writer + .WriteLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitialize]") + .WriteLine("public static Task InitializeFeatureAsync(global::Microsoft.VisualStudio.TestTools.UnitTesting.TestContext testContext)") .BeginBlock("{") - .AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();") - .AppendLine("return TestRunner.OnFeatureStartAsync(FeatureInfo);") + .WriteLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();") + .WriteLine("return TestRunner.OnFeatureStartAsync(FeatureInfo);") .EndBlock("}"); } protected virtual void RenderClassCleanupMethodTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - sourceBuilder - .AppendLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanup(" + + writer + .WriteLine("[global::Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanup(" + "Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupBehavior.EndOfClass)]") - .AppendLine("public static async Task TeardownFeatureAsync()") + .WriteLine("public static async Task TeardownFeatureAsync()") .BeginBlock("{") - .Append("if (TestRunner == null)") + .Write("if (TestRunner == null)") .BeginBlock("{") - .AppendLine("return;") + .WriteLine("return;") .EndBlock("}") - .AppendLine("await TestRunner.OnFeatureEndAsync();") - .AppendLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(TestRunner);") - .AppendLine("TestRunner = null;") + .WriteLine("await TestRunner.OnFeatureEndAsync();") + .WriteLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(TestRunner);") + .WriteLine("TestRunner = null;") .EndBlock("}"); } protected override void RenderScenarioInitializeMethodBodyTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - base.RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + base.RenderScenarioInitializeMethodBodyTo(writer, cancellationToken); - sourceBuilder.AppendLine("testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(TestContext);"); + writer.WriteLine("testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(TestContext);"); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs index 1e43b6d2c..11f690c65 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestMethod.cs @@ -20,24 +20,24 @@ public MSTestCSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor } protected override void RenderTestRunnerLookupTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { // For MSTest we use a test-runner assigned to the class. - sourceBuilder - .Append("global::Reqnroll.ITestRunner"); + writer + .Write("global::Reqnroll.ITestRunner"); if (renderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append('?'); + writer.Write('?'); } - sourceBuilder - .AppendLine(" testRunner = TestRunner;") - .AppendLine("if (testRunner == null)") + writer + .WriteLine(" testRunner = TestRunner;") + .WriteLine("if (testRunner == null)") .BeginBlock("{") - .AppendLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") + .WriteLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") .EndBlock("}"); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs index 0ae40d915..9e8b5b039 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestFixtureClass.cs @@ -23,68 +23,68 @@ public NUnitCSharpTestFixtureClass( { } - protected override void RenderTestFixtureContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected override void RenderTestFixtureContentTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - sourceBuilder.Append("private global::Reqnroll.ITestRunner"); + writer.Write("private global::Reqnroll.ITestRunner"); if (RenderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append('?'); + writer.Write('?'); } - sourceBuilder.AppendLine(" _testRunner;"); + writer.WriteLine(" _testRunner;"); - sourceBuilder.AppendLine(); + writer.WriteLine(); - RenderFeatureSetupMethodTo(sourceBuilder); + RenderFeatureSetupMethodTo(writer); - sourceBuilder.AppendLine(); + writer.WriteLine(); - RenderFeatureTearDownMethodTo(sourceBuilder); + RenderFeatureTearDownMethodTo(writer); - sourceBuilder.AppendLine(); + writer.WriteLine(); - base.RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + base.RenderTestFixtureContentTo(writer, cancellationToken); } - private void RenderFeatureSetupMethodTo(CSharpSourceTextBuilder sourceBuilder) + private void RenderFeatureSetupMethodTo(CSharpSourceTextWriter writer) { - sourceBuilder - .AppendLine("[global::NUnit.Framework.OneTimeSetUp]") - .AppendLine("public virtual global::System.Threading.Tasks.Task FeatureSetupAsync()") + writer + .WriteLine("[global::NUnit.Framework.OneTimeSetUp]") + .WriteLine("public virtual global::System.Threading.Tasks.Task FeatureSetupAsync()") .BeginBlock("{") - .AppendLine("_testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();") - .AppendLine("return _testRunner.OnFeatureStartAsync(FeatureInfo);") + .WriteLine("_testRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();") + .WriteLine("return _testRunner.OnFeatureStartAsync(FeatureInfo);") .EndBlock("}"); } - private void RenderFeatureTearDownMethodTo(CSharpSourceTextBuilder sourceBuilder) + private void RenderFeatureTearDownMethodTo(CSharpSourceTextWriter writer) { - sourceBuilder - .AppendLine("[global::NUnit.Framework.OneTimeTearDown]") - .AppendLine("public async virtual global::System.Threading.Tasks.Task FeatureTearDownAsync()") + writer + .WriteLine("[global::NUnit.Framework.OneTimeTearDown]") + .WriteLine("public async virtual global::System.Threading.Tasks.Task FeatureTearDownAsync()") .BeginBlock("{"); - sourceBuilder.Append("await _testRunner"); + writer.Write("await _testRunner"); if (RenderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append('!'); + writer.Write('!'); } - sourceBuilder.AppendLine(".OnFeatureEndAsync();"); + writer.WriteLine(".OnFeatureEndAsync();"); - sourceBuilder - .AppendLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(_testRunner);") - .AppendLine("_testRunner = null;") + writer + .WriteLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(_testRunner);") + .WriteLine("_testRunner = null;") .EndBlock("}"); } - protected override void RenderScenarioInitializeMethodBodyTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected override void RenderScenarioInitializeMethodBodyTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - base.RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + base.RenderScenarioInitializeMethodBodyTo(writer, cancellationToken); - sourceBuilder.AppendLine( + writer.WriteLine( "testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(" + "global::NUnit.Framework.TestContext.CurrentContext);"); } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs index 03b4516e9..b0e407fd6 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/NUnit/NUnitCSharpTestMethod.cs @@ -20,24 +20,24 @@ public NUnitCSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor) } protected override void RenderTestRunnerLookupTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { // For NUnit we use a test-runner assigned to the class. - sourceBuilder.Append("global::Reqnroll.ITestRunner"); + writer.Write("global::Reqnroll.ITestRunner"); if (renderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append('?'); + writer.Write('?'); } - sourceBuilder.AppendLine(" testRunner = _testRunner;"); + writer.WriteLine(" testRunner = _testRunner;"); - sourceBuilder - .AppendLine("if (testRunner == null)") + writer + .WriteLine("if (testRunner == null)") .BeginBlock("{") - .AppendLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") + .WriteLine("throw new global::System.InvalidOperationException(\"TestRunner has not been assigned to the test fixture.\");") .EndBlock("}"); } } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs index 866db4d84..724e6f7a6 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestFixtureClass.cs @@ -31,30 +31,30 @@ public XUnitCSharpTestFixtureClass( public override ImmutableArray Interfaces { get; } protected override void RenderTestFixtureContentTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - RenderLifetimeClassTo(sourceBuilder, cancellationToken); + RenderLifetimeClassTo(writer, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - sourceBuilder.AppendLine("private readonly FeatureLifetime _lifetime;"); - sourceBuilder.AppendLine(); + writer.WriteLine("private readonly FeatureLifetime _lifetime;"); + writer.WriteLine(); - sourceBuilder.AppendLine("private readonly global::Xunit.Abstractions.ITestOutputHelper _testOutputHelper;"); - sourceBuilder.AppendLine(); + writer.WriteLine("private readonly global::Xunit.Abstractions.ITestOutputHelper _testOutputHelper;"); + writer.WriteLine(); - RenderConstructorTo(sourceBuilder, cancellationToken); + RenderConstructorTo(writer, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - sourceBuilder.AppendLine(); + writer.WriteLine(); - base.RenderTestFixtureContentTo(sourceBuilder, cancellationToken); + base.RenderTestFixtureContentTo(writer, cancellationToken); } protected virtual void RenderConstructorTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter SourceBuilder, CancellationToken cancellationToken) { var className = Identifier.LocalType switch @@ -65,69 +65,69 @@ protected virtual void RenderConstructorTo( $"Writing constructor for {Identifier.GetType().Name} values is not implemented.") }; - // Lifetime class is initialized once per feature, then passed to the constructor of each test class instance. + // Lifetime class is initialzed once per feature, then passed to the constructor of each test class instance. // Output helper is included to by registered in the container. - sourceBuilder.Append("public ").Append(className).AppendLine("(FeatureLifetime lifetime, " + + SourceBuilder.Write("public ").Write(className).WriteLine("(FeatureLifetime lifetime, " + "global::Xunit.Abstractions.ITestOutputHelper testOutputHelper)"); - sourceBuilder.BeginBlock("{"); - sourceBuilder.AppendLine("_lifetime = lifetime;"); - sourceBuilder.AppendLine("_testOutputHelper = testOutputHelper;"); - sourceBuilder.EndBlock("}"); + SourceBuilder.BeginBlock("{"); + SourceBuilder.WriteLine("_lifetime = lifetime;"); + SourceBuilder.WriteLine("_testOutputHelper = testOutputHelper;"); + SourceBuilder.EndBlock("}"); } - protected virtual void RenderLifetimeClassTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected virtual void RenderLifetimeClassTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { // This class represents the feature lifetime in the xUnit framework. - sourceBuilder.AppendLine("public class FeatureLifetime : global::Xunit.IAsyncLifetime"); - sourceBuilder.BeginBlock("{"); - RenderLifetimeClassContentTo(sourceBuilder, cancellationToken); - sourceBuilder.EndBlock("}"); - sourceBuilder.AppendLine(); + writer.WriteLine("public class FeatureLifetime : global::Xunit.IAsyncLifetime"); + writer.BeginBlock("{"); + RenderLifetimeClassContentTo(writer, cancellationToken); + writer.EndBlock("}"); + writer.WriteLine(); } - private void RenderLifetimeClassContentTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + private void RenderLifetimeClassContentTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - sourceBuilder.Append("public global::Reqnroll.ITestRunner"); + writer.Write("public global::Reqnroll.ITestRunner"); if (RenderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append('?'); + writer.Write('?'); } - sourceBuilder.AppendLine(" TestRunner { get; private set; }"); + writer.WriteLine(" TestRunner { get; private set; }"); - sourceBuilder.AppendLine(); + writer.WriteLine(); - sourceBuilder.AppendLine("public global::System.Threading.Tasks.Task InitializeAsync()"); - sourceBuilder.BeginBlock("{"); - sourceBuilder.AppendLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();"); - sourceBuilder.Append("return TestRunner.OnFeatureStartAsync(").AppendTypeReference(Identifier).Append(".FeatureInfo").AppendLine(");"); - sourceBuilder.EndBlock("}"); + writer.WriteLine("public global::System.Threading.Tasks.Task InitializeAsync()"); + writer.BeginBlock("{"); + writer.WriteLine("TestRunner = global::Reqnroll.TestRunnerManager.GetTestRunnerForAssembly();"); + writer.Write("return TestRunner.OnFeatureStartAsync(").WriteTypeReference(Identifier).Write(".FeatureInfo").WriteLine(");"); + writer.EndBlock("}"); - sourceBuilder.AppendLine(); + writer.WriteLine(); - sourceBuilder.AppendLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); - sourceBuilder.BeginBlock("{"); - sourceBuilder.Append("await TestRunner"); + writer.WriteLine("public async global::System.Threading.Tasks.Task DisposeAsync()"); + writer.BeginBlock("{"); + writer.Write("await TestRunner"); if (RenderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append('!'); + writer.Write('!'); } - sourceBuilder.AppendLine(".OnFeatureEndAsync();"); - sourceBuilder.AppendLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(TestRunner);"); - sourceBuilder.AppendLine("TestRunner = null;"); - sourceBuilder.EndBlock("}"); + writer.WriteLine(".OnFeatureEndAsync();"); + writer.WriteLine("global::Reqnroll.TestRunnerManager.ReleaseTestRunner(TestRunner);"); + writer.WriteLine("TestRunner = null;"); + writer.EndBlock("}"); } - protected override void RenderScenarioInitializeMethodBodyTo(CSharpSourceTextBuilder sourceBuilder, CancellationToken cancellationToken) + protected override void RenderScenarioInitializeMethodBodyTo(CSharpSourceTextWriter writer, CancellationToken cancellationToken) { - base.RenderScenarioInitializeMethodBodyTo(sourceBuilder, cancellationToken); + base.RenderScenarioInitializeMethodBodyTo(writer, cancellationToken); - sourceBuilder.AppendLine( + writer.WriteLine( "testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs" + "(_testOutputHelper);"); } diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs index 93987d9e5..22901bfcd 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/XUnit/XUnitCSharpTestMethod.cs @@ -20,24 +20,24 @@ public XUnitCSharpTestMethod(TestMethodDescriptor descriptor) : base(descriptor) } protected override void RenderTestRunnerLookupTo( - CSharpSourceTextBuilder sourceBuilder, + CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { // For xUnit test runners are scoped to the whole feature execution lifetime - sourceBuilder.Append("global::Reqnroll.ITestRunner"); + writer.Write("global::Reqnroll.ITestRunner"); if (renderingOptions.UseNullableReferenceTypes) { - sourceBuilder.Append('?'); + writer.Write('?'); } - sourceBuilder.AppendLine(" testRunner = _lifetime.TestRunner;"); + writer.WriteLine(" testRunner = _lifetime.TestRunner;"); - sourceBuilder - .AppendLine("if (testRunner == null)") + writer + .WriteLine("if (testRunner == null)") .BeginBlock("{") - .AppendLine("throw new global::System.InvalidOperationException(\"The test fixture lifecycle has not been initialized.\");") + .WriteLine("throw new global::System.InvalidOperationException(\"The test fixture lifecycle has not been initialized.\");") .EndBlock("}"); } } diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs index a0d2bd4c2..9758961f6 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs @@ -1,9 +1,10 @@ using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.SourceModel; + public class ScenarioInformation( string name, - int lineNumber, + FileLinePositionSpan keywordAndNamePosition, ImmutableArray tags, ImmutableArray steps, ImmutableArray examples = default, @@ -13,7 +14,7 @@ public class ScenarioInformation( throw new ArgumentException("Value cannot be null or an empty string", nameof(name)) : name; - public int LineNumber { get; } = lineNumber; + public FileLinePositionSpan KeywordAndNamePosition { get; } = keywordAndNamePosition; public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs index d4f812a89..3714b085c 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs @@ -1,3 +1,3 @@ namespace Reqnroll.FeatureSourceGenerator.SourceModel; -public record ScenarioStep(StepType StepType, string Keyword, string Text, int LineNumber); +public record ScenarioStep(StepType StepType, string Keyword, string Text, FileLinePositionSpan Position); diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs index f7dc5c7df..16e812f41 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs @@ -1,16 +1,16 @@ using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.SourceModel; -public class StepInvocation( +public sealed class StepInvocation( StepType type, - int sourceLineNumber, + FileLinePositionSpan position, string keyword, string text, - ImmutableArray arguments = default) + ImmutableArray arguments = default) : IEquatable { public StepType Type { get; } = type; - public int SourceLineNumber { get; } = sourceLineNumber; + public FileLinePositionSpan Position { get; } = position; public string Keyword { get; } = keyword; @@ -19,8 +19,7 @@ public class StepInvocation( public ImmutableArray Arguments { get; } = arguments.IsDefault ? ImmutableArray.Empty : arguments; - - public virtual bool Equals(StepInvocation? other) + public bool Equals(StepInvocation? other) { if (other is null) { @@ -33,7 +32,7 @@ public virtual bool Equals(StepInvocation? other) } return Type.Equals(other.Type) && - SourceLineNumber.Equals(other.SourceLineNumber) && + Position.Equals(other.Position) && Keyword.Equals(other.Keyword) && Text.Equals(other.Text) && (Arguments.Equals(other.Arguments) || Arguments.SequenceEqual(other.Arguments)); @@ -46,7 +45,7 @@ public override int GetHashCode() var hash = 92321497; hash *= 47886541 + Type.GetHashCode(); - hash *= 47886541 + SourceLineNumber.GetHashCode(); + hash *= 47886541 + Position.GetHashCode(); hash *= 47886541 + Keyword.GetHashCode(); hash *= 47886541 + Text.GetHashCode(); hash *= 47886541 + Arguments.GetSequenceHashCode(); diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs index 8b79855e2..82433cfeb 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestFixtureClass.cs @@ -89,6 +89,11 @@ protected bool Equals(TestFixtureClass? other) return true; } + if (GetType() != other.GetType()) + { + return false; + } + return Identifier.Equals(other.Identifier) && (Attributes.Equals(other.Attributes) || Attributes.SetEquals(other.Attributes)) && HintName.Equals(other.HintName, StringComparison.Ordinal) && diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs index f0ed2a634..b5bd132a5 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/TestMethod.cs @@ -124,8 +124,12 @@ public virtual bool Equals(TestMethod? other) return true; } + if (GetType() != other.GetType()) + { + return false; + } + return - GetType().Equals(other.GetType()) && Identifier.Equals(other.Identifier) && Scenario.Equals(other.Scenario) && (StepInvocations.Equals(other.StepInvocations) || StepInvocations.SequenceEqual(other.StepInvocations)) && diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index f4bcb9759..8d154b15b 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -91,8 +91,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Combine(generatorInformation) .SelectMany(static IEnumerable>> (input, cancellationToken) => { - Debugger.Launch(); - var (((featureFile, optionsProvider), compilationInfo), generatorInformation) = input; var options = optionsProvider.GetOptions(featureFile); @@ -190,6 +188,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) try { // CONSIDER: Using a parser that doesn't throw exceptions for syntax errors. + // CONSIDER: Using a parser that uses Roslyn text data-types. + // CONSIDER: Using a parser that supports incremental parsing. document = parser.Parse(new SourceTokenScanner(source)); } catch (CompositeParserException ex) @@ -218,7 +218,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) featureFile.Path); var scenarioInformations = feature.Children - .SelectMany(child => CreateScenarioInformations(child, emitIgnoredExamples, cancellationToken)) + .SelectMany(child => CreateScenarioInformations(featureFile.Path, child, emitIgnoredExamples, cancellationToken)) .ToImmutableArray(); return @@ -277,24 +277,27 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } private static IEnumerable CreateScenarioInformations( + string sourceFilePath, IHasLocation child, bool emitIgnoredExamples, CancellationToken cancellationToken) { return child switch { - Scenario scenario => [CreateScenarioInformation(scenario, emitIgnoredExamples, cancellationToken)], - Rule rule => CreateScenarioInformations(rule, emitIgnoredExamples, cancellationToken), + Scenario scenario => [CreateScenarioInformation(sourceFilePath, scenario, emitIgnoredExamples, cancellationToken)], + Rule rule => CreateScenarioInformations(sourceFilePath, rule, emitIgnoredExamples, cancellationToken), _ => [] }; } private static ScenarioInformation CreateScenarioInformation( + string sourceFilePath, Scenario scenario, bool emitIgnoredExamples, - CancellationToken cancellationToken) => CreateScenarioInformation(scenario, null, emitIgnoredExamples, cancellationToken); + CancellationToken cancellationToken) => CreateScenarioInformation(sourceFilePath, scenario, null, emitIgnoredExamples, cancellationToken); private static ScenarioInformation CreateScenarioInformation( + string sourceFilePath, Scenario scenario, RuleInformation? rule, bool emitIgnoredExamples, @@ -325,6 +328,11 @@ private static ScenarioInformation CreateScenarioInformation( { cancellationToken.ThrowIfCancellationRequested(); + var startPosition = new LinePosition(step.Location.Line, step.Location.Column); + // We assume as single character gap between keyword and step text; could be more. + var endPosition = new LinePosition(startPosition.Line, startPosition.Character + step.Keyword.Length + step.Text.Length + 1); + var position = new FileLinePositionSpan(sourceFilePath, new LinePositionSpan(startPosition, endPosition)); + var scenarioStep = new ScenarioStep( step.KeywordType switch { @@ -336,14 +344,20 @@ private static ScenarioInformation CreateScenarioInformation( }, step.Keyword, step.Text, - step.Location.Line); + position); steps.Add(scenarioStep); } + var keywordAndNameStartPosition = new LinePosition(scenario.Location.Line, scenario.Location.Column); + // We assume as single character gap between keyword and scenario name; could be more. + var keywordAndNameEndPosition = new LinePosition( + scenario.Location.Line, + scenario.Location.Column + scenario.Keyword.Length + scenario.Name.Length + 1); + return new ScenarioInformation( scenario.Name, - scenario.Location.Line, + new FileLinePositionSpan(sourceFilePath, keywordAndNameStartPosition, keywordAndNameEndPosition), scenario.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(), steps.ToImmutableArray(), exampleSets.ToImmutableArray(), @@ -351,6 +365,7 @@ private static ScenarioInformation CreateScenarioInformation( } private static IEnumerable CreateScenarioInformations( + string sourceFilePath, Rule rule, bool emitIgnoredExamples, CancellationToken cancellationToken) @@ -365,6 +380,7 @@ private static IEnumerable CreateScenarioInformations( { case Scenario scenario: yield return CreateScenarioInformation( + sourceFilePath, scenario, new RuleInformation(rule.Name, tags), emitIgnoredExamples, diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceRenderingContext.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceRenderingContext.cs new file mode 100644 index 000000000..d5726fb15 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceRenderingContext.cs @@ -0,0 +1,23 @@ +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Diagnostics; + +namespace Reqnroll.FeatureSourceGenerator; + +/// +/// Provides information about the rendering of a test-fixture's source. +/// +/// The feature that is the primary source of the test-fixture. +/// The hint name of the test-fixture source being rendered. +[DebuggerDisplay("OutputHintName={OutputHintName}")] +public class TestFixtureSourceRenderingContext(FeatureInformation feature, string outputHintName) +{ + /// + /// Gets the hint name of the test-fixture source being rendered. + /// + public string OutputHintName { get; } = outputHintName; + + /// + /// Gets the feature that is the primary source of the test-fixture. + /// + public FeatureInformation Feature { get; } = feature; +} diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs index 6e2537a98..1de8130f3 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -1,4 +1,6 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; +using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; @@ -19,9 +21,15 @@ public void GenerateTestFixture_CreatesClassForFeatureWithMsTestAttributes() var scenarioInfo = new ScenarioInformation( "Sample Scenario", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + [ + new ScenarioStep( + StepType.Action, + "When", + "foo happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -51,9 +59,15 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp var scenarioInfo = new ScenarioInformation( "Sample Scenario", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + [ + new ScenarioStep( + StepType.Action, + "When", + "foo happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -84,7 +98,11 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp method.StepInvocations.Should().BeEquivalentTo( [ - new StepInvocation(StepType.Action, 6, "When", "foo happens") + new StepInvocation( + StepType.Action, + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20)), + "When", + "foo happens") ]); } @@ -96,9 +114,15 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen var scenarioInfo = new ScenarioInformation( "Sample Scenario Outline", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [ new ScenarioStep(StepType.Action, "When", " happens", 6) ], + [ + new ScenarioStep( + StepType.Action, + "When", + " happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ], [ exampleSet1, exampleSet2 ]); var featureInfo = new FeatureInformation( @@ -155,7 +179,12 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen method.StepInvocations.Should().BeEquivalentTo( [ - new StepInvocation(StepType.Action, 6, "When", "{0} happens", [new IdentifierString("what")]) + new StepInvocation( + StepType.Action, + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20)), + "When", + "{0} happens", + [new IdentifierString("what")]) ]); } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs index 9f942a98b..a748cbd10 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs @@ -1,4 +1,6 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; @@ -19,9 +21,15 @@ public void GenerateTestFixture_CreatesClassForFeatureWithNUnitAttributes() var scenarioInfo = new ScenarioInformation( "Sample Scenario", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + [ + new ScenarioStep( + StepType.Action, + "When", + "foo happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -53,9 +61,15 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp var scenarioInfo = new ScenarioInformation( "Sample Scenario", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + [ + new ScenarioStep( + StepType.Action, + "When", + "foo happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -83,7 +97,11 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp method.StepInvocations.Should().BeEquivalentTo( [ - new StepInvocation(StepType.Action, 6, "When", "foo happens") + new StepInvocation( + StepType.Action, + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20)), + "When", + "foo happens") ]); } @@ -95,9 +113,15 @@ public void GenerateTestMethod_CreatesMethodWithTestCaseAttributesWhenScenarioHa var scenarioInfo = new ScenarioInformation( "Sample Scenario Outline", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", " happens", 6)], + [ + new ScenarioStep( + StepType.Action, + "When", + " happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ], [exampleSet1, exampleSet2]); var featureInfo = new FeatureInformation( @@ -152,7 +176,12 @@ [new ScenarioStep(StepType.Action, "When", " happens", 6)], method.StepInvocations.Should().BeEquivalentTo( [ - new StepInvocation(StepType.Action, 6, "When", "{0} happens", [new IdentifierString("what")]) + new StepInvocation( + StepType.Action, + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20)), + "When", + "{0} happens", + [new IdentifierString("what")]) ]); } } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs index 956049927..cbcfa2691 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs @@ -1,4 +1,6 @@ -using Reqnroll.FeatureSourceGenerator.CSharp; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; @@ -19,9 +21,15 @@ public void GenerateTestFixture_CreatesClassForFeatureWithXUnitLifetimeInterface var scenarioInfo = new ScenarioInformation( "Sample Scenario", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + [ + new ScenarioStep( + StepType.Action, + "When", + "foo happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -57,9 +65,15 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp var scenarioInfo = new ScenarioInformation( "Sample Scenario", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", "foo happens", 6)]); + [ + new ScenarioStep( + StepType.Action, + "When", + "foo happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ]); var testFixtureGenerationContext = new TestFixtureGenerationContext( featureInfo, @@ -94,7 +108,11 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp method.StepInvocations.Should().BeEquivalentTo( [ - new StepInvocation(StepType.Action, 6, "When", "foo happens") + new StepInvocation( + StepType.Action, + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20)), + "When", + "foo happens") ]); } @@ -106,9 +124,15 @@ public void GenerateTestMethod_CreatesMethodWithInlineDataAttributesWhenScenario var scenarioInfo = new ScenarioInformation( "Sample Scenario Outline", - 22, + new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], - [new ScenarioStep(StepType.Action, "When", " happens", 6)], + [ + new ScenarioStep( + StepType.Action, + "When", + " happens", + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20))) + ], [exampleSet1, exampleSet2]); var featureInfo = new FeatureInformation( @@ -169,7 +193,12 @@ [new ScenarioStep(StepType.Action, "When", " happens", 6)], method.StepInvocations.Should().BeEquivalentTo( [ - new StepInvocation(StepType.Action, 6, "When", "{0} happens", [new IdentifierString("what")]) + new StepInvocation( + StepType.Action, + new FileLinePositionSpan("Sample.feature", new LinePosition(4, 4), new LinePosition(4, 20)), + "When", + "{0} happens", + [new IdentifierString("what")]) ]); } } From 24ed8a8a5d9f25ce79253cdbc7ec48144f745c08 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Mon, 5 Aug 2024 21:41:34 +0100 Subject: [PATCH 39/48] Add support for background steps at feature and rule level --- .../SourceModel/ScenarioInformation.cs | 4 +- .../SourceModel/ScenarioStep.cs | 3 - .../SourceModel/Step.cs | 3 + .../TestFixtureSourceGenerator.cs | 159 ++++++++++++------ .../MSTestCSharpTestFixtureGeneratorTests.cs | 6 +- .../NUnitCSharpTestFixtureGeneratorTests.cs | 6 +- .../XUnitCSharpTestFixtureGeneratorTests.cs | 6 +- 7 files changed, 117 insertions(+), 70 deletions(-) delete mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs index 9758961f6..826cb5e5a 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioInformation.cs @@ -6,7 +6,7 @@ public class ScenarioInformation( string name, FileLinePositionSpan keywordAndNamePosition, ImmutableArray tags, - ImmutableArray steps, + ImmutableArray steps, ImmutableArray examples = default, RuleInformation? rule = null) : IEquatable { @@ -18,7 +18,7 @@ public class ScenarioInformation( public ImmutableArray Tags { get; } = tags.IsDefault ? ImmutableArray.Empty : tags; - public ImmutableArray Steps { get; } = steps.IsDefault ? ImmutableArray.Empty : steps; + public ImmutableArray Steps { get; } = steps.IsDefault ? ImmutableArray.Empty : steps; public ImmutableArray Examples { get; } = examples.IsDefault ? ImmutableArray.Empty : examples; diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs deleted file mode 100644 index 3714b085c..000000000 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/ScenarioStep.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Reqnroll.FeatureSourceGenerator.SourceModel; - -public record ScenarioStep(StepType StepType, string Keyword, string Text, FileLinePositionSpan Position); diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs new file mode 100644 index 000000000..ed54f90da --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs @@ -0,0 +1,3 @@ +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public record Step(StepType StepType, string Keyword, string Text, FileLinePositionSpan Position); diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 8d154b15b..21e79ccd9 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -1,10 +1,8 @@ using Gherkin; using Gherkin.Ast; -using Microsoft.CodeAnalysis; using Reqnroll.FeatureSourceGenerator.Gherkin; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; -using System.Diagnostics; namespace Reqnroll.FeatureSourceGenerator; @@ -217,15 +215,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) feature.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(), featureFile.Path); - var scenarioInformations = feature.Children - .SelectMany(child => CreateScenarioInformations(featureFile.Path, child, emitIgnoredExamples, cancellationToken)) - .ToImmutableArray(); + var scenarioInformations = CreateScenarioInformations(featureFile.Path, feature, emitIgnoredExamples, cancellationToken); return [ new TestFixtureGenerationContext( featureInformation, - scenarioInformations, + scenarioInformations.ToImmutableArray(), featureHintName, new NamespaceString(testFixtureNamespace), compilationInfo, @@ -278,27 +274,96 @@ public void Initialize(IncrementalGeneratorInitializationContext context) private static IEnumerable CreateScenarioInformations( string sourceFilePath, - IHasLocation child, + Feature feature, + bool emitIgnoredExamples, + CancellationToken cancellationToken) + { + var children = feature.Children.ToList(); + + var scenarios = new List(); + var backgroundSteps = new List(); + + if (children.FirstOrDefault() is Background background) + { + PopulateSteps(backgroundSteps, sourceFilePath, background, cancellationToken); + } + + foreach (var child in children) + { + cancellationToken.ThrowIfCancellationRequested(); + + switch (child) + { + case Scenario scenario: + scenarios.Add( + CreateScenarioInformation(sourceFilePath, scenario, backgroundSteps, emitIgnoredExamples, cancellationToken)); + break; + + case Rule rule: + scenarios.AddRange( + CreateScenarioInformations(sourceFilePath, rule, backgroundSteps, emitIgnoredExamples, cancellationToken)); + break; + } + } + + return scenarios; + } + + private static IEnumerable CreateScenarioInformations( + string sourceFilePath, + Rule rule, + IReadOnlyList backgroundSteps, bool emitIgnoredExamples, CancellationToken cancellationToken) { - return child switch + var tags = rule.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(); + + var children = rule.Children.ToList(); + + var scenarios = new List(); + var combinedBackgroundSteps = backgroundSteps.ToList(); + + if (children.FirstOrDefault() is Background background) { - Scenario scenario => [CreateScenarioInformation(sourceFilePath, scenario, emitIgnoredExamples, cancellationToken)], - Rule rule => CreateScenarioInformations(sourceFilePath, rule, emitIgnoredExamples, cancellationToken), - _ => [] - }; + PopulateSteps(combinedBackgroundSteps, sourceFilePath, background, cancellationToken); + } + + foreach (var child in rule.Children) + { + cancellationToken.ThrowIfCancellationRequested(); + + switch (child) + { + case Scenario scenario: + yield return CreateScenarioInformation( + sourceFilePath, + scenario, + combinedBackgroundSteps, + new RuleInformation(rule.Name, tags), + emitIgnoredExamples, + cancellationToken); + break; + } + } } private static ScenarioInformation CreateScenarioInformation( string sourceFilePath, Scenario scenario, + IReadOnlyList backgroundSteps, bool emitIgnoredExamples, - CancellationToken cancellationToken) => CreateScenarioInformation(sourceFilePath, scenario, null, emitIgnoredExamples, cancellationToken); + CancellationToken cancellationToken) => CreateScenarioInformation( + sourceFilePath, + scenario, + backgroundSteps, + null, + emitIgnoredExamples, + cancellationToken); private static ScenarioInformation CreateScenarioInformation( string sourceFilePath, Scenario scenario, + IReadOnlyList backgroundSteps, RuleInformation? rule, bool emitIgnoredExamples, CancellationToken cancellationToken) @@ -322,9 +387,31 @@ private static ScenarioInformation CreateScenarioInformation( exampleSets.Add(examples); } - var steps = new List(); + var steps = backgroundSteps.ToList(); + PopulateSteps(steps, sourceFilePath, scenario, cancellationToken); + + var keywordAndNameStartPosition = new LinePosition(scenario.Location.Line, scenario.Location.Column); + // We assume as single character gap between keyword and scenario name; could be more. + var keywordAndNameEndPosition = new LinePosition( + scenario.Location.Line, + scenario.Location.Column + scenario.Keyword.Length + scenario.Name.Length + 1); + + return new ScenarioInformation( + scenario.Name, + new FileLinePositionSpan(sourceFilePath, keywordAndNameStartPosition, keywordAndNameEndPosition), + scenario.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(), + steps.ToImmutableArray(), + exampleSets.ToImmutableArray(), + rule); + } - foreach (var step in scenario.Steps) + private static void PopulateSteps( + List steps, + string sourceFilePath, + StepsContainer container, + CancellationToken cancellationToken) + { + foreach (var step in container.Steps) { cancellationToken.ThrowIfCancellationRequested(); @@ -333,7 +420,7 @@ private static ScenarioInformation CreateScenarioInformation( var endPosition = new LinePosition(startPosition.Line, startPosition.Character + step.Keyword.Length + step.Text.Length + 1); var position = new FileLinePositionSpan(sourceFilePath, new LinePositionSpan(startPosition, endPosition)); - var scenarioStep = new ScenarioStep( + var scenarioStep = new SourceModel.Step( step.KeywordType switch { StepKeywordType.Context => StepType.Context, @@ -348,46 +435,6 @@ private static ScenarioInformation CreateScenarioInformation( steps.Add(scenarioStep); } - - var keywordAndNameStartPosition = new LinePosition(scenario.Location.Line, scenario.Location.Column); - // We assume as single character gap between keyword and scenario name; could be more. - var keywordAndNameEndPosition = new LinePosition( - scenario.Location.Line, - scenario.Location.Column + scenario.Keyword.Length + scenario.Name.Length + 1); - - return new ScenarioInformation( - scenario.Name, - new FileLinePositionSpan(sourceFilePath, keywordAndNameStartPosition, keywordAndNameEndPosition), - scenario.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(), - steps.ToImmutableArray(), - exampleSets.ToImmutableArray(), - rule); - } - - private static IEnumerable CreateScenarioInformations( - string sourceFilePath, - Rule rule, - bool emitIgnoredExamples, - CancellationToken cancellationToken) - { - var tags = rule.Tags.Select(tag => tag.Name.TrimStart('@')).ToImmutableArray(); - - foreach (var child in rule.Children) - { - cancellationToken.ThrowIfCancellationRequested(); - - switch (child) - { - case Scenario scenario: - yield return CreateScenarioInformation( - sourceFilePath, - scenario, - new RuleInformation(rule.Name, tags), - emitIgnoredExamples, - cancellationToken); - break; - } - } } protected abstract TCompilationInformation GetCompilationInformation(Compilation compilation); diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs index 1de8130f3..9c35b4834 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/MSTest/MSTestCSharpTestFixtureGeneratorTests.cs @@ -24,7 +24,7 @@ public void GenerateTestFixture_CreatesClassForFeatureWithMsTestAttributes() new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", "foo happens", @@ -62,7 +62,7 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", "foo happens", @@ -117,7 +117,7 @@ public void GenerateTestMethod_CreatesMethodWithMSTestDataRowsAttributesWhenScen new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", " happens", diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs index a748cbd10..4a1431ffc 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/NUnit/NUnitCSharpTestFixtureGeneratorTests.cs @@ -24,7 +24,7 @@ public void GenerateTestFixture_CreatesClassForFeatureWithNUnitAttributes() new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", "foo happens", @@ -64,7 +64,7 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", "foo happens", @@ -116,7 +116,7 @@ public void GenerateTestMethod_CreatesMethodWithTestCaseAttributesWhenScenarioHa new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", " happens", diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs index cbcfa2691..5862ad1d3 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/XUnit/XUnitCSharpTestFixtureGeneratorTests.cs @@ -24,7 +24,7 @@ public void GenerateTestFixture_CreatesClassForFeatureWithXUnitLifetimeInterface new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", "foo happens", @@ -68,7 +68,7 @@ public void GenerateTestMethod_CreatesParameterlessMethodForScenarioWithoutExamp new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", "foo happens", @@ -127,7 +127,7 @@ public void GenerateTestMethod_CreatesMethodWithInlineDataAttributesWhenScenario new FileLinePositionSpan("Sample.feature", new LinePosition(3, 0), new LinePosition(3, 24)), [], [ - new ScenarioStep( + new Step( StepType.Action, "When", " happens", From 36f97057682455154b163322d016f17ed0fb506a Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Tue, 6 Aug 2024 20:26:12 +0100 Subject: [PATCH 40/48] Fix location mapping --- .../Gherkin/GherkinSyntaxParser.cs | 2 +- .../TestFixtureSourceGenerator.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs index 5e6c76c1f..d96351c2f 100644 --- a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs @@ -48,7 +48,7 @@ public static Diagnostic CreateGherkinDiagnostic(ParserException exception, Sour private static Microsoft.CodeAnalysis.Location CreateLocation(Location location, SourceText text, string path) { - var start = text.Lines[location.Line].Start + location.Column; + var start = text.Lines[location.Line - 1].Start + location.Column; return Microsoft.CodeAnalysis.Location.Create( path, diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 21e79ccd9..cfe79371b 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -3,6 +3,7 @@ using Reqnroll.FeatureSourceGenerator.Gherkin; using Reqnroll.FeatureSourceGenerator.SourceModel; using System.Collections.Immutable; +using System.Diagnostics; namespace Reqnroll.FeatureSourceGenerator; @@ -93,6 +94,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var options = optionsProvider.GetOptions(featureFile); + // Launch a debugger if configured. + if (options.TryGetValue("reqnroll_feature_source_generator.launch_debugger", out var launchDebuggerValue) && + bool.TryParse(launchDebuggerValue, out var launchDebugger) && + launchDebugger) + { + Debugger.Launch(); + } + var source = featureFile.GetText(cancellationToken); // If there is no source text, we can skip this file completely. From c8fa22fc3c227000fbe5cf5fde46592db26b2300 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Wed, 7 Aug 2024 22:33:12 +0100 Subject: [PATCH 41/48] Fix off-by-one line/column mapping --- .../CSharp/CSharpSourceTextWriter.cs | 5 ++++- .../Gherkin/GherkinSyntaxExtensions.cs | 10 ++++++++++ .../Gherkin/GherkinSyntaxParser.cs | 13 ++++++++----- .../TestFixtureSourceGenerator.cs | 5 +++-- 4 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxExtensions.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs index b7065f119..2d1560d09 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpSourceTextWriter.cs @@ -191,7 +191,10 @@ private void WriteIndentIfIsFreshLine() public CSharpSourceTextWriter WriteLineDirective(LinePosition start, LinePosition end, int offset, string filePath) { - return WriteDirective($"#line ({start.Line}, {start.Character}) - ({end.Line}, {end.Character}) {offset} \"{filePath}\""); + // Roslyn uses 0-based indexing for line number and character offset values. + // #line directives use 1-based indexing for line number of character offset values. + return WriteDirective( + $"#line ({start.Line + 1}, {start.Character + 1}) - ({end.Line + 1}, {end.Character + 1}) {offset} \"{filePath}\""); } public CSharpSourceTextWriter WriteDirective(string directive) diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxExtensions.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxExtensions.cs new file mode 100644 index 000000000..e5f0dd020 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxExtensions.cs @@ -0,0 +1,10 @@ +namespace Reqnroll.FeatureSourceGenerator.Gherkin; +internal static class GherkinSyntaxExtensions +{ + public static LinePosition ToLinePosition(this global::Gherkin.Ast.Location location) + { + // Roslyn uses 0-based indexes for line and character-offset numbers. + // The Gherkin parser uses 1-based indexes for line and column numbers. + return new LinePosition(location.Line - 1, location.Column - 1); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs index d96351c2f..20008a951 100644 --- a/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs +++ b/Reqnroll.FeatureSourceGenerator/Gherkin/GherkinSyntaxParser.cs @@ -48,13 +48,16 @@ public static Diagnostic CreateGherkinDiagnostic(ParserException exception, Sour private static Microsoft.CodeAnalysis.Location CreateLocation(Location location, SourceText text, string path) { - var start = text.Lines[location.Line - 1].Start + location.Column; + // Roslyn uses 0-based indexes for line and character-offset numbers. + // The Gherkin parser uses 1-based indexes for line and column numbers. + var positionStart = location.ToLinePosition(); + var line = text.Lines[positionStart.Line]; + var lineLength = line.Span.Length; + var positionEnd = new LinePosition(positionStart.Line, lineLength); return Microsoft.CodeAnalysis.Location.Create( path, - new TextSpan(start, 0), - new LinePositionSpan( - new LinePosition(location.Line, location.Column), - new LinePosition(location.Line, location.Column))); + new TextSpan(line.Span.Start + positionStart.Character, lineLength - positionStart.Character), + new LinePositionSpan(positionStart, positionEnd)); ; } } diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index cfe79371b..40895df93 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -197,6 +197,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // CONSIDER: Using a parser that doesn't throw exceptions for syntax errors. // CONSIDER: Using a parser that uses Roslyn text data-types. // CONSIDER: Using a parser that supports incremental parsing. + // CONSIDER: Using a parser that provides human-readable syntax errors. document = parser.Parse(new SourceTokenScanner(source)); } catch (CompositeParserException ex) @@ -399,7 +400,7 @@ private static ScenarioInformation CreateScenarioInformation( var steps = backgroundSteps.ToList(); PopulateSteps(steps, sourceFilePath, scenario, cancellationToken); - var keywordAndNameStartPosition = new LinePosition(scenario.Location.Line, scenario.Location.Column); + var keywordAndNameStartPosition = scenario.Location.ToLinePosition(); // We assume as single character gap between keyword and scenario name; could be more. var keywordAndNameEndPosition = new LinePosition( scenario.Location.Line, @@ -424,7 +425,7 @@ private static void PopulateSteps( { cancellationToken.ThrowIfCancellationRequested(); - var startPosition = new LinePosition(step.Location.Line, step.Location.Column); + var startPosition = step.Location.ToLinePosition(); // We assume as single character gap between keyword and step text; could be more. var endPosition = new LinePosition(startPosition.Line, startPosition.Character + step.Keyword.Length + step.Text.Length + 1); var position = new FileLinePositionSpan(sourceFilePath, new LinePositionSpan(startPosition, endPosition)); From 7ae843c018def73333c792d87020619de5864a82 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sat, 10 Aug 2024 23:51:31 +0100 Subject: [PATCH 42/48] Fix handling of feature files in folders Includes generation of namespaces based on folder structure --- .../AnalyzerConfigOptionsExtensions.cs | 77 +++++++++++++++++++ .../TestFixtureSourceGenerator.cs | 71 +++++++++++------ .../Reqnroll.FeatureSourceGenerator.props | 6 +- .../Reqnroll.FeatureSourceGenerator.targets | 8 +- 4 files changed, 134 insertions(+), 28 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs diff --git a/Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs b/Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs new file mode 100644 index 000000000..7bf4cbd83 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs @@ -0,0 +1,77 @@ +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Reqnroll.FeatureSourceGenerator; + +internal static class AnalyzerConfigOptionsExtensions +{ + public static string? GetStringValue(this AnalyzerConfigOptions options, string name) + { + options.TryGetValue(name, out var value); + return value; + } + + public static string? GetStringValue(this AnalyzerConfigOptions options, string name1, string name2) + { + if (options.TryGetValue(name1, out var value) || + options.TryGetValue(name2, out value)) + { + return value; + } + + return null; + } + + public static string? GetStringValue(this AnalyzerConfigOptions options, string name1, string name2, string name3) + { + if (options.TryGetValue(name1, out var value) || + options.TryGetValue(name2, out value) || + options.TryGetValue(name3, out value)) + { + return value; + } + + return null; + } + + public static bool? GetBooleanValue(this AnalyzerConfigOptions options, string name) + { + if (options.TryGetValue(name, out var value)) + { + if (bool.TryParse(value, out bool result)) + { + return result; + } + } + + return null; + } + + public static bool? GetBooleanValue(this AnalyzerConfigOptions options, string name1, string name2) + { + if (options.TryGetValue(name1, out var value) || + options.TryGetValue(name2, out value)) + { + if (bool.TryParse(value, out bool result)) + { + return result; + } + } + + return null; + } + + public static bool? GetBooleanValue(this AnalyzerConfigOptions options, string name1, string name2, string name3) + { + if (options.TryGetValue(name1, out var value) || + options.TryGetValue(name2, out value) || + options.TryGetValue(name3, out value)) + { + if (bool.TryParse(value, out bool result)) + { + return result; + } + } + + return null; + } +} diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 40895df93..7e2686f18 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -57,9 +57,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select((compilationInfo, cancellationToken) => { var compatibleGenerators = _testFrameworkHandlers - .Select(handler => handler.GetTestFixtureGenerator()) + .Select(handler => handler.GetTestFixtureGenerator()!) .Where(generator => generator != null) - .Select(generator => generator!) .ToImmutableArray(); if (!compatibleGenerators.Any()) @@ -95,9 +94,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var options = optionsProvider.GetOptions(featureFile); // Launch a debugger if configured. - if (options.TryGetValue("reqnroll_feature_source_generator.launch_debugger", out var launchDebuggerValue) && - bool.TryParse(launchDebuggerValue, out var launchDebugger) && - launchDebugger) + if (options.GetBooleanValue( + "reqnroll_feature_source_generator.launch_debugger", + "build_property.ReqnrollDebugGenerator") ?? false) { Debugger.Launch(); } @@ -111,13 +110,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } // Select the generator from the following sources: - // 1. The reqnroll.target_test_framework value from .editorconfig + // 1. The reqnroll_feature_source_generator.target_test_framework value from .editorconfig // 2. The ReqnrollTargetTestFramework from the build system properties (MSBuild project files or command-line argument) // 3. The assemblies referenced by the compilation indicating the presence of a test framework. ITestFixtureGenerator? generator; - if ((options.TryGetValue("reqnroll.target_test_framework", out var targetTestFrameworkIdentifier) - || options.TryGetValue("build_property.ReqnrollTargetTestFramework", out targetTestFrameworkIdentifier)) - && !string.IsNullOrEmpty(targetTestFrameworkIdentifier)) + var targetTestFrameworkIdentifier = options.GetStringValue( + "reqnroll_feature_source_generator.target_test_framework", + "build_property.ReqnrollTargetTestFramework"); + if(!string.IsNullOrEmpty(targetTestFrameworkIdentifier)) { // Select the target framework from the option specified. generator = generatorInformation.CompatibleGenerators @@ -164,21 +164,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ]; } - // Determine the namespace of the feature from the project. - if (!optionsProvider.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace)) - { - rootNamespace = compilationInfo.AssemblyName ?? "ReqnrollFeatures"; - } + // Determine the hint path and namespace for the generated test fixtured based on the project and the relative file path. + var rootNamespace = optionsProvider.GlobalOptions.GetStringValue("build_property.RootNamespace") ?? + compilationInfo.AssemblyName ?? + "ReqnrollFeatures"; var featureHintName = Path.GetFileNameWithoutExtension(featureFile.Path); var testFixtureNamespace = rootNamespace; - if (options.TryGetValue("build_metadata.AdditionalFiles.RelativeDir", out var relativeDir)) + var relativeDir = options.GetStringValue("build_metadata.AdditionalFiles.RelativeDir"); + if (!string.IsNullOrEmpty(relativeDir)) { - var testFixtureNamespaceParts = relativeDir + var testFixtureNamespaceParts = relativeDir! .Replace(Path.DirectorySeparatorChar, '.') .Replace(Path.AltDirectorySeparatorChar, '.') - .Split('.') - .Select(part => DotNetSyntax.CreateIdentifier(part)); + .Split(['.'], StringSplitOptions.RemoveEmptyEntries) + .Select(part => DotNetSyntax.CreateIdentifier(part)) + .ToList(); + + testFixtureNamespaceParts.Insert(0, testFixtureNamespace); testFixtureNamespace = string.Join(".", testFixtureNamespaceParts); @@ -202,6 +205,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } catch (CompositeParserException ex) { + // We can't report errors for specific files using the built-in diagnostic system + // https://github.com/dotnet/roslyn/issues/49531 + // Instead we will report the error by writing it as output. + // Use the diagnostic to convey the feature hint name we'll use to write the error. var diagnostics = ex.Errors .Select(error => GherkinSyntaxParser.CreateGherkinDiagnostic(error, source, featureFile.Path)); @@ -209,12 +216,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } // Determine whether we should include ignored examples in our sample sets. - var emitIgnoredExamples = false; - if (options.TryGetValue("reqnroll.emit_ignored_examples", out var emitIgnoredExamplesValue) || - options.TryGetValue("build_property.ReqnrollEmitIgnoredExamples", out emitIgnoredExamplesValue)) - { - bool.TryParse(emitIgnoredExamplesValue, out emitIgnoredExamples); - } + var emitIgnoredExamples = options.GetBooleanValue( + "reqnroll_feature_source_generator.emit_ignored_examples", + "build_metadata.AdditionalFiles.EmitIgnoredExamples", + "build_property.ReqnrollEmitIgnoredExamples") ?? false; var feature = document.Feature; @@ -274,7 +279,23 @@ public void Initialize(IncrementalGeneratorInitializationContext context) cancellationToken)); // Emit errors. - context.RegisterSourceOutput(errors, static (context, error) => context.ReportDiagnostic(error)); + context.RegisterSourceOutput( + errors, + static (context, error) => + { + // We can't report errors for specific files using the built-in diagnostic system + // https://github.com/dotnet/roslyn/issues/49531 + // Instead we will report the error by writing it as output. + + //if (error.Location == Location.None) + //{ + context.ReportDiagnostic(error); + //} + //else + //{ + // context.AddSource(error.Location.GetLineSpan().Path, SourceText.From("")) + //} + }); // Emit source files for fixtures. context.RegisterSourceOutput( diff --git a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props index fb6589323..e27e6754e 100644 --- a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props +++ b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.props @@ -4,11 +4,15 @@ - + + + + + diff --git a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets index d5be3d539..914772a0b 100644 --- a/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets +++ b/Reqnroll.FeatureSourceGenerator/build/Reqnroll.FeatureSourceGenerator.targets @@ -1,7 +1,11 @@ - + + - + From 6520617fcad92f0084bd59d190c2e572f2a925f4 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 11 Aug 2024 10:13:56 +0100 Subject: [PATCH 43/48] Add "empty-string-is-null" convention to options --- .../AnalyzerConfigOptionsExtensions.cs | 51 +++++-------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs b/Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs index 7bf4cbd83..cc98490ce 100644 --- a/Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs +++ b/Reqnroll.FeatureSourceGenerator/AnalyzerConfigOptionsExtensions.cs @@ -6,41 +6,34 @@ internal static class AnalyzerConfigOptionsExtensions { public static string? GetStringValue(this AnalyzerConfigOptions options, string name) { - options.TryGetValue(name, out var value); - return value; - } - - public static string? GetStringValue(this AnalyzerConfigOptions options, string name1, string name2) - { - if (options.TryGetValue(name1, out var value) || - options.TryGetValue(name2, out value)) + if (options.TryGetValue(name, out var value)) { - return value; + return string.IsNullOrEmpty(value) ? null : value; } return null; } - public static string? GetStringValue(this AnalyzerConfigOptions options, string name1, string name2, string name3) + public static string? GetStringValue(this AnalyzerConfigOptions options, string name1, string name2) { - if (options.TryGetValue(name1, out var value) || - options.TryGetValue(name2, out value) || - options.TryGetValue(name3, out value)) - { - return value; - } + return GetStringValue(options, name1) ?? GetStringValue(options, name2); + } - return null; + public static string? GetStringValue(this AnalyzerConfigOptions options, string name1, string name2, string name3) + { + return GetStringValue(options, name1) ?? GetStringValue(options, name2) ?? GetStringValue(options, name3); } public static bool? GetBooleanValue(this AnalyzerConfigOptions options, string name) { - if (options.TryGetValue(name, out var value)) + if (options.TryGetValue(name, out var value) && !string.IsNullOrEmpty(value)) { if (bool.TryParse(value, out bool result)) { return result; } + + return false; } return null; @@ -48,30 +41,12 @@ internal static class AnalyzerConfigOptionsExtensions public static bool? GetBooleanValue(this AnalyzerConfigOptions options, string name1, string name2) { - if (options.TryGetValue(name1, out var value) || - options.TryGetValue(name2, out value)) - { - if (bool.TryParse(value, out bool result)) - { - return result; - } - } - - return null; + return GetBooleanValue(options, name1) ?? GetBooleanValue(options, name2); } public static bool? GetBooleanValue(this AnalyzerConfigOptions options, string name1, string name2, string name3) { - if (options.TryGetValue(name1, out var value) || - options.TryGetValue(name2, out value) || - options.TryGetValue(name3, out value)) - { - if (bool.TryParse(value, out bool result)) - { - return result; - } - } - return null; + return GetBooleanValue(options, name1) ?? GetBooleanValue(options, name2) ?? GetBooleanValue(options, name3); } } From 40be218d57c9f0c3b655fcc2917d121a24c858e5 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sat, 24 Aug 2024 21:35:21 +0100 Subject: [PATCH 44/48] Add test for table arguments being passed to steps --- .../Generation/GenerationTestBase.cs | 67 +++++++++++++++++++ .../TRXParser.cs | 37 +++++++--- .../TestExecutionResult.cs | 1 + 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs index e90eaae0d..6ce7e4fde 100644 --- a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs +++ b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs @@ -2,6 +2,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using FluentAssertions; using Reqnroll.TestProjectGenerator; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Collections.Generic; namespace Reqnroll.SystemTests.Generation; @@ -364,5 +367,69 @@ public async Task WhenScenarioStepIsCalled() #endregion + #region Test tables arguments are processed + + [TestMethod] + public void Table_arguments_are_passed_to_steps() + { + AddScenario( + """ + Scenario: Using tables with steps + When this table is processed + | Example | + | A | + | B | + | C | + """); + + AddBindingClass( + """ + namespace TableArguments.StepDefinitions + { + [Binding] + public class TableArgumentSteps + { + [When("this table is processed")] + public async Task WhenThisTableIsProcessed(DataTable table) + { + var tableData = new + { + headings = table.Header.ToList(), + rows = table.Rows.Select(row => row.Select(kvp => kvp.Value)).ToList() + }; + + Log.LogCustom("argument", $"table = {System.Text.Json.JsonSerializer.Serialize(tableData)}"); + } + } + } + """); + + ExecuteTests(); + + var arguments = _bindingDriver.GetActualLogLines("argument").ToList(); + + arguments.Should().NotBeEmpty(); + + arguments[0].Should().StartWith("-> argument: table = "); + var tableSource = arguments[0]; + var tableJson = tableSource[tableSource.IndexOf('{')..(tableSource.LastIndexOf('}')+1)]; + var tableData = JsonSerializer.Deserialize(tableJson); + + var actualHeadings = tableData + .GetProperty("headings") + .EnumerateArray() + .Select(item => item.ToString()); + + var actualRows = tableData + .GetProperty("rows") + .EnumerateArray() + .Select(item => item.EnumerateArray().Select(data => data.ToString()).ToList()); + + actualHeadings.Should().BeEquivalentTo(["Example"]); + actualRows.Should().BeEquivalentTo(new List> { new() { "A" }, new() { "B" }, new() { "C" } }); + } + + #endregion + //TODO: test parallel execution (details TBD) - maybe this should be in a separate test class } diff --git a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TRXParser.cs b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TRXParser.cs index 02d2f2fe1..7e7fd7c13 100644 --- a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TRXParser.cs +++ b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TRXParser.cs @@ -154,7 +154,7 @@ private List GetTestResultsInternal(XElement testRunResultsElement, TestName = testNameAttribute.Value, Outcome = outcomeAttribute.Value, StdOut = stdOutElement?.Value, - Steps = steps, + Steps = steps.ToList(), ErrorMessage = errorMessage?.Value, InnerResults = GetTestResultsInternal(innerResultsElement, xmlns) }; @@ -162,11 +162,11 @@ private List GetTestResultsInternal(XElement testRunResultsElement, return testResults.ToList(); } - private static List ParseTestOutput(XElement stdOutElement) + private static IEnumerable ParseTestOutput(XElement stdOutElement) { if (stdOutElement == null) { - return null; + yield break; } var stdOutText = stdOutElement.Value; @@ -175,13 +175,32 @@ private static List ParseTestOutput(XElement stdOutElement) var primaryOutput = logLines.TakeWhile(line => line != "TestContext Messages:"); - var steps = primaryOutput - .Where(line => line.StartsWith("Given ") || - line.StartsWith("When ") || - line.StartsWith("Then ") || - line.StartsWith("And ")); + TestStepResult step = null; - return steps.Select(step => new TestStepResult { Step = step }).ToList(); + foreach (var line in primaryOutput) + { + if (line.StartsWith("Given ") || + line.StartsWith("When ") || + line.StartsWith("Then ") || + line.StartsWith("And ")) + { + if (step != null) + { + yield return step; + } + + step = new TestStepResult { Step = line }; + } + else + { + step?.Output.Add(line); + } + } + + if (step != null) + { + yield return step; + } } private int GetNUnitIgnoredCount(TestExecutionResult testExecutionResult) diff --git a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TestExecutionResult.cs b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TestExecutionResult.cs index e8e8522f0..1c9680b29 100644 --- a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TestExecutionResult.cs +++ b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/TestExecutionResult.cs @@ -46,5 +46,6 @@ public class TestStepResult public string Step { get; set; } public string Error { get; set; } public string Result { get; set; } + public List Output { get; } = []; } } \ No newline at end of file From 610a7d1e8fe36398635999ae5eddec6c8b96b507 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 25 Aug 2024 17:33:33 +0100 Subject: [PATCH 45/48] Tighten up background handling assertions --- .../Generation/GenerationTestBase.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs index 6ce7e4fde..c145706cb 100644 --- a/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs +++ b/Tests/Reqnroll.SystemTests/Generation/GenerationTestBase.cs @@ -348,18 +348,18 @@ public async Task WhenScenarioStepIsCalled() var results = _vsTestExecutionDriver.LastTestExecutionResult.LeafTestResults; results.Should().ContainSingle(tr => tr.TestName == "Background Scenario Steps" || tr.TestName == "BackgroundScenarioSteps") - .Which.Steps.Should().BeEquivalentTo( + .Which.Steps.Select(result => result.Step).Should().BeEquivalentTo( [ - new TestStepResult { Step = "Given background step 1 is called" }, - new TestStepResult { Step = "When scenario step is called" } + "Given background step 1 is called", + "When scenario step is called" ]); results.Should().ContainSingle(tr => tr.TestName == "Rule Background Scenario Steps" || tr.TestName == "RuleBackgroundScenarioSteps") - .Which.Steps.Should().BeEquivalentTo( + .Which.Steps.Select(result => result.Step).Should().BeEquivalentTo( [ - new TestStepResult { Step = "Given background step 1 is called" }, - new TestStepResult { Step = "Given background step 2 is called" }, - new TestStepResult { Step = "When scenario step is called" } + "Given background step 1 is called", + "Given background step 2 is called", + "When scenario step is called" ]); ShouldAllScenariosPass(); From 90453e15fd07cffdc5869c26d976a04e7dadaee5 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 25 Aug 2024 20:44:22 +0100 Subject: [PATCH 46/48] Add generation support for MSTest 3 --- ...s => MSTest2CSharpTestFixtureGenerator.cs} | 4 +- .../MSTest3CSharpTestFixtureGenerator.cs | 64 +++++++++++++++++++ .../ITestFrameworkHandler.cs | 3 +- .../MSTest/MSTestHandler.cs | 17 +++-- .../NUnit/NUnitHandler.cs | 3 +- .../TestFixtureSourceGenerator.cs | 2 +- .../XUnit/XUnitHandler.cs | 3 +- 7 files changed, 86 insertions(+), 10 deletions(-) rename Reqnroll.FeatureSourceGenerator/CSharp/MSTest/{MSTestCSharpTestFixtureGenerator.cs => MSTest2CSharpTestFixtureGenerator.cs} (94%) create mode 100644 Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTest3CSharpTestFixtureGenerator.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTest2CSharpTestFixtureGenerator.cs similarity index 94% rename from Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs rename to Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTest2CSharpTestFixtureGenerator.cs index 7c0531a29..3acf8b2e0 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTestCSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTest2CSharpTestFixtureGenerator.cs @@ -5,9 +5,9 @@ namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; /// -/// Performs generation of MSTest test fixtures in the C# language. +/// Performs generation of MSTest 2.x test fixtures in the C# language. /// -internal class MSTestCSharpTestFixtureGenerator(MSTestHandler frameworkHandler) : +internal class MSTest2CSharpTestFixtureGenerator(MSTestHandler frameworkHandler) : CSharpTestFixtureGenerator(frameworkHandler) { protected override MSTestCSharpTestFixtureClass CreateTestFixtureClass( diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTest3CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTest3CSharpTestFixtureGenerator.cs new file mode 100644 index 000000000..75f0462eb --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/CSharp/MSTest/MSTest3CSharpTestFixtureGenerator.cs @@ -0,0 +1,64 @@ +using Reqnroll.FeatureSourceGenerator.MSTest; +using Reqnroll.FeatureSourceGenerator.SourceModel; +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.CSharp.MSTest; + +/// +/// Performs generation of MSTest 3.x test fixtures in the C# language. +/// +internal class MSTest3CSharpTestFixtureGenerator(MSTestHandler frameworkHandler) : + CSharpTestFixtureGenerator(frameworkHandler) +{ + protected override MSTestCSharpTestFixtureClass CreateTestFixtureClass( + TestFixtureGenerationContext context, + TestFixtureDescriptor descriptor, + ImmutableArray methods, + CSharpRenderingOptions renderingOptions) + { + return new MSTestCSharpTestFixtureClass(descriptor, methods, renderingOptions); + } + + protected override MSTestCSharpTestMethod CreateTestMethod( + TestMethodGenerationContext context, + TestMethodDescriptor descriptor) + { + return new MSTestCSharpTestMethod(descriptor); + } + + protected override ImmutableArray GenerateTestFixtureClassAttributes( + TestFixtureGenerationContext context, + CancellationToken cancellationToken) + { + return ImmutableArray.Create(MSTestSyntax.TestClassAttribute()); + } + + protected override ImmutableArray GenerateTestMethodAttributes( + TestMethodGenerationContext context, + CancellationToken cancellationToken) + { + var scenario = context.ScenarioInformation; + var feature = context.FeatureInformation; + + var attributes = new List + { + MSTestSyntax.TestMethodAttribute(), + MSTestSyntax.DescriptionAttribute(scenario.Name), + MSTestSyntax.TestPropertyAttribute("FeatureTitle", feature.Name) + }; + + foreach (var set in scenario.Examples) + { + foreach (var example in set) + { + cancellationToken.ThrowIfCancellationRequested(); + + var arguments = example.Select(item => item.Value).ToImmutableArray(); + + attributes.Add(MSTestSyntax.DataRowAttribute(arguments)); + } + } + + return attributes.ToImmutableArray(); + } +} diff --git a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs index 649afce44..3a863e8fb 100644 --- a/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/ITestFrameworkHandler.cs @@ -22,7 +22,8 @@ public interface ITestFrameworkHandler /// Gets a test-fixture generator for the test framework. /// /// The type of compilation to obtain a generator for. + /// The compilation to examine. /// A test-fixture generator if one can be produced for the compilation type; otherwise null. - ITestFixtureGenerator? GetTestFixtureGenerator() + ITestFixtureGenerator? GetTestFixtureGenerator(TCompilationInformation compilation) where TCompilationInformation : CompilationInformation; } diff --git a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs index 060027f24..6c0cf7138 100644 --- a/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/MSTest/MSTestHandler.cs @@ -1,5 +1,4 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Reqnroll.FeatureSourceGenerator.CSharp; +using Reqnroll.FeatureSourceGenerator.CSharp; using Reqnroll.FeatureSourceGenerator.CSharp.MSTest; namespace Reqnroll.FeatureSourceGenerator.MSTest; @@ -17,12 +16,22 @@ public bool IsTestFrameworkReferenced(CompilationInformation compilation) .Any(assembly => assembly.Name == "Microsoft.VisualStudio.TestPlatform.TestFramework"); } - public ITestFixtureGenerator? GetTestFixtureGenerator() + public ITestFixtureGenerator? GetTestFixtureGenerator( + TCompilationInformation compilation) where TCompilationInformation : CompilationInformation { if (typeof(TCompilationInformation).IsAssignableFrom(typeof(CSharpCompilationInformation))) { - return (ITestFixtureGenerator)new MSTestCSharpTestFixtureGenerator(this); + var version = compilation.ReferencedAssemblies + .Where(assembly => assembly.Name == "Microsoft.VisualStudio.TestPlatform.TestFramework") + .Max(assembly => assembly.Version); + + if (version >= new Version(3, 0)) + { + return (ITestFixtureGenerator)new MSTest3CSharpTestFixtureGenerator(this); + } + + return (ITestFixtureGenerator)new MSTest2CSharpTestFixtureGenerator(this); } return null; diff --git a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs index 827ead031..5db9b7e75 100644 --- a/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/NUnit/NUnitHandler.cs @@ -10,7 +10,8 @@ public class NUnitHandler : ITestFrameworkHandler { public string TestFrameworkName => "NUnit"; - public ITestFixtureGenerator? GetTestFixtureGenerator() + public ITestFixtureGenerator? GetTestFixtureGenerator( + TCompilationInformation compilation) where TCompilationInformation : CompilationInformation { if (typeof(TCompilationInformation).IsAssignableFrom(typeof(CSharpCompilationInformation))) diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 7e2686f18..34c6c3d13 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -57,7 +57,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select((compilationInfo, cancellationToken) => { var compatibleGenerators = _testFrameworkHandlers - .Select(handler => handler.GetTestFixtureGenerator()!) + .Select(handler => handler.GetTestFixtureGenerator(compilationInfo)!) .Where(generator => generator != null) .ToImmutableArray(); diff --git a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs index 47d511a6d..892e87b0d 100644 --- a/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs +++ b/Reqnroll.FeatureSourceGenerator/XUnit/XUnitHandler.cs @@ -12,7 +12,8 @@ public class XUnitHandler : ITestFrameworkHandler public string TestFrameworkName => "xUnit"; - public ITestFixtureGenerator? GetTestFixtureGenerator() + public ITestFixtureGenerator? GetTestFixtureGenerator( + TCompilationInformation compilation) where TCompilationInformation : CompilationInformation { if (typeof(TCompilationInformation).IsAssignableFrom(typeof(CSharpCompilationInformation))) From 312b7933e544eecb5131d62a56e6631d2c398e95 Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Sun, 25 Aug 2024 23:35:12 +0100 Subject: [PATCH 47/48] Added support for DataTable and DocString arguments --- .../CSharp/CSharpTestFixtureGenerator.cs | 18 +++- .../CSharp/CSharpTestMethod.cs | 85 ++++++++++++++++++- .../SourceModel/DataTable.cs | 60 +++++++++++++ .../SourceModel/Step.cs | 8 +- .../SourceModel/StepInvocation.cs | 23 ++++- .../TestFixtureSourceGenerator.cs | 23 ++++- 6 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 Reqnroll.FeatureSourceGenerator/SourceModel/DataTable.cs diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs index 9b9cacdba..dbc1d5b3f 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestFixtureGenerator.cs @@ -81,7 +81,13 @@ protected virtual ImmutableArray GenerateStepInvocations( if (scenario.Examples.IsEmpty) { return scenario.Steps - .Select(step => new StepInvocation(step.StepType, step.Position, step.Keyword, step.Text)) + .Select(step => new StepInvocation( + step.StepType, + step.Position, + step.Keyword, + step.Text, + dataTableArgument: step.DataTableArgument, + docStringArgument: step.DocStringArgument)) .ToImmutableArray(); } @@ -109,7 +115,15 @@ protected virtual ImmutableArray GenerateStepInvocations( } } - invocations.Add(new StepInvocation(step.StepType, step.Position, step.Keyword, text, arguments.ToImmutableArray())); + invocations.Add( + new StepInvocation( + step.StepType, + step.Position, + step.Keyword, + text, + arguments.ToImmutableArray(), + step.DataTableArgument, + step.DocStringArgument)); } return invocations.ToImmutableArray(); diff --git a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs index 15828677d..e9b146fd2 100644 --- a/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs +++ b/Reqnroll.FeatureSourceGenerator/CSharp/CSharpTestMethod.cs @@ -123,10 +123,11 @@ protected virtual void RenderMethodBodyTo( writer.WriteLine(); writer.WriteLine("// start: invocation of scenario steps"); - foreach (var invocation in StepInvocations) + for (var i = 0; i < StepInvocations.Length; i++) { cancellationToken.ThrowIfCancellationRequested(); - RenderScenarioStepInvocationTo(invocation, writer, renderingOptions, cancellationToken); + var invocation = StepInvocations[i]; + RenderScenarioStepInvocationTo(invocation, i, writer, renderingOptions, cancellationToken); } writer.WriteLine("// end: invocation of scenario steps"); @@ -144,10 +145,86 @@ protected virtual void RenderMethodBodyTo( protected virtual void RenderScenarioStepInvocationTo( StepInvocation invocation, + int index, CSharpSourceTextWriter writer, CSharpRenderingOptions renderingOptions, CancellationToken cancellationToken) { + var tableArgName = invocation.DataTableArgument is null ? "null" : $"step{index}DataTableArg"; + var docStringArgName = invocation.DocStringArgument is null ? "null" : $"step{index}DocStringArg"; + + if (invocation.DataTableArgument is not null) + { + writer.WriteLine($"var {tableArgName} = new global::Reqnroll.DataTable("); + writer.BeginBlock(); + var firstHeading = true; + foreach (var heading in invocation.DataTableArgument.Headings) + { + if (firstHeading) + { + firstHeading = false; + } + else + { + writer.WriteLine(","); + } + + writer.WriteLiteral(heading); + } + writer.WriteLine(");"); + writer.EndBlock(); + + foreach (var row in invocation.DataTableArgument.Rows) + { + writer.Write(tableArgName).WriteLine(".AddRow("); + writer.BeginBlock(); + var firstColumn = true; + foreach (var cell in row) + { + if (firstColumn) + { + firstColumn = false; + } + else + { + writer.WriteLine(","); + } + + writer.WriteLiteral(cell); + } + writer.WriteLine(");"); + writer.EndBlock(); + } + } + + if (invocation.DocStringArgument is not null) + { + writer.WriteLine($"var {tableArgName} = "); + writer.BeginBlock(); + + var firstLine = true; + + foreach (var line in invocation.DocStringArgument.Split()) + { + if (firstLine) + { + firstLine = false; + } + else + { + writer.WriteLine(" +"); + } + + writer.Write('"'); + writer.WriteLiteral(line); + writer.Write('"'); + } + + writer.WriteLine(";"); + + writer.EndBlock(); + } + var position = invocation.Position; if (position.Path != null && renderingOptions.EnableLineMapping) @@ -189,7 +266,9 @@ protected virtual void RenderScenarioStepInvocationTo( } writer - .Write(", null, null, ") + .Write(", null, ") + .Write(tableArgName) + .Write(", ") .WriteLiteral(invocation.Keyword) .WriteLine(");"); diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/DataTable.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/DataTable.cs new file mode 100644 index 000000000..bb0cc9907 --- /dev/null +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/DataTable.cs @@ -0,0 +1,60 @@ +using System.Collections.Immutable; + +namespace Reqnroll.FeatureSourceGenerator.SourceModel; + +public class DataTable(ImmutableArray headings, ImmutableArray> rows) : IEquatable +{ + public ImmutableArray Headings { get; } = headings.IsDefault ? ImmutableArray.Empty : headings; + + public ImmutableArray> Rows { get; } = rows.IsDefault ? ImmutableArray>.Empty : rows; + + public bool Equals(DataTable? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (!Headings.SequenceEqual(other.Headings) || Rows.Length != other.Rows.Length) + { + return false; + } + + for (var i = 0; i < Rows.Length; i++) + { + var row = Rows[i]; + var otherRow = other.Rows[i]; + + if (!row.SequenceEqual(otherRow)) + { + return false; + } + } + + return true; + } + + public override bool Equals(object obj) => Equals(obj as DataTable); + + public override int GetHashCode() + { + unchecked + { + var hash = 25979651; + + hash *= 33473171 + Rows.GetSequenceHashCode(); + + foreach (var row in Rows) + { + hash *= 33473171 + row.GetSequenceHashCode(); + } + + return hash; + } + } +} diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs index ed54f90da..3cdac6875 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/Step.cs @@ -1,3 +1,9 @@ namespace Reqnroll.FeatureSourceGenerator.SourceModel; -public record Step(StepType StepType, string Keyword, string Text, FileLinePositionSpan Position); +public record Step( + StepType StepType, + string Keyword, + string Text, + FileLinePositionSpan Position, + DataTable? DataTableArgument = default, + string? DocStringArgument = default); diff --git a/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs b/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs index 16e812f41..83c2f6ed0 100644 --- a/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs +++ b/Reqnroll.FeatureSourceGenerator/SourceModel/StepInvocation.cs @@ -6,7 +6,9 @@ public sealed class StepInvocation( FileLinePositionSpan position, string keyword, string text, - ImmutableArray arguments = default) : IEquatable + ImmutableArray arguments = default, + DataTable? dataTableArgument = default, + string? docStringArgument = default) : IEquatable { public StepType Type { get; } = type; @@ -19,6 +21,10 @@ public sealed class StepInvocation( public ImmutableArray Arguments { get; } = arguments.IsDefault ? ImmutableArray.Empty : arguments; + public DataTable? DataTableArgument { get; } = dataTableArgument; + + public string? DocStringArgument { get; } = docStringArgument; + public bool Equals(StepInvocation? other) { if (other is null) @@ -35,7 +41,10 @@ public bool Equals(StepInvocation? other) Position.Equals(other.Position) && Keyword.Equals(other.Keyword) && Text.Equals(other.Text) && - (Arguments.Equals(other.Arguments) || Arguments.SequenceEqual(other.Arguments)); + (Arguments.Equals(other.Arguments) || Arguments.SequenceEqual(other.Arguments)) && + (DataTableArgument is null && other.DataTableArgument is null || + DataTableArgument is not null && DataTableArgument.Equals(other.DataTableArgument)) && + string.Equals(DocStringArgument, other.DocStringArgument); } public override int GetHashCode() @@ -50,6 +59,16 @@ public override int GetHashCode() hash *= 47886541 + Text.GetHashCode(); hash *= 47886541 + Arguments.GetSequenceHashCode(); + if (DataTableArgument != null) + { + hash *= 47886541 + DataTableArgument.GetHashCode(); + } + + if (DocStringArgument != null) + { + hash *= 47886541 + DocStringArgument.GetHashCode(); + } + return hash; } } diff --git a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs index 34c6c3d13..da7bbf300 100644 --- a/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs +++ b/Reqnroll.FeatureSourceGenerator/TestFixtureSourceGenerator.cs @@ -451,6 +451,25 @@ private static void PopulateSteps( var endPosition = new LinePosition(startPosition.Line, startPosition.Character + step.Keyword.Length + step.Text.Length + 1); var position = new FileLinePositionSpan(sourceFilePath, new LinePositionSpan(startPosition, endPosition)); + string? docStringArgument = null; + SourceModel.DataTable? dataTableArgument = null; + + if (step.Argument is DocString docString) + { + docStringArgument = docString.Content; + } + else if (step.Argument is global::Gherkin.Ast.DataTable dataTable) + { + var headings = dataTable.Rows.First().Cells + .Select(cell => cell.Value) + .ToImmutableArray(); + var rows = dataTable.Rows.Skip(1) + .Select(row => row.Cells.Select(cell => cell.Value).ToImmutableArray()) + .ToImmutableArray(); + + dataTableArgument = new SourceModel.DataTable(headings, rows); + } + var scenarioStep = new SourceModel.Step( step.KeywordType switch { @@ -462,7 +481,9 @@ private static void PopulateSteps( }, step.Keyword, step.Text, - position); + position, + dataTableArgument, + docStringArgument); steps.Add(scenarioStep); } From 498a7e6d24e2677c0bf8a50de624bee78852e8cc Mon Sep 17 00:00:00 2001 From: Paul Turner Date: Mon, 26 Aug 2024 20:10:10 +0100 Subject: [PATCH 48/48] Fix tests broken by adding compilation argument --- .../CSharp/CSharpTestFixtureGeneratorTestBase.cs | 3 +-- .../CSharp/CSharpTestFixtureSourceGeneratorTests.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs index b291656cb..05f1d9c47 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureGeneratorTestBase.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis; -using Reqnroll.FeatureSourceGenerator.MSTest; using System.Collections.Immutable; namespace Reqnroll.FeatureSourceGenerator.CSharp; @@ -17,7 +16,7 @@ public CSharpTestFixtureGeneratorTestBase(THandler handler) Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11, true); - Generator = handler.GetTestFixtureGenerator() ?? + Generator = handler.GetTestFixtureGenerator(Compilation) ?? throw new InvalidOperationException($"Handler for {handler.TestFrameworkName} does not support C# generation."); } diff --git a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs index 19cb38769..f24c742c6 100644 --- a/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs +++ b/Tests/Reqnroll.FeatureSourceGeneratorTests/CSharp/CSharpTestFixtureSourceGeneratorTests.cs @@ -19,7 +19,7 @@ public CSharpTestFixtureSourceGeneratorTests() _mockHandler.SetupGet(handler => handler.TestFrameworkName).Returns("Mock"); _mockHandler - .Setup(handler => handler.GetTestFixtureGenerator()) + .Setup(handler => handler.GetTestFixtureGenerator(It.IsAny())) .Returns(_mockGenerator.Object);