diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 579bea94a..d4e10a402 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -252,11 +252,15 @@ jobs: - name: Install Test Report Dependencies run: | dotnet add ./Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj package GitHubActionsTestLogger + dotnet add ./Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj package GitHubActionsTestLogger - name: Build run: dotnet build --no-restore ${{ needs.build.outputs.build_params }} - name: System Tests shell: pwsh run: dotnet test ./Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj --logger "trx;LogFileName=${{ github.workspace }}/TestResults/systemtests-windows-results.trx" ${{ needs.build.outputs.test_params }} + - name: CucumberMessages Tests + shell: pwsh + run: dotnet test ./Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj --logger "trx;LogFileName=${{ github.workspace }}/TestResults/cucumbermessagestests-windows-results.trx" ${{ needs.build.outputs.test_params }} - name: Upload Test Result TRX Files uses: actions/upload-artifact@v4 if: always() @@ -281,11 +285,15 @@ jobs: - name: Install Test Report Dependencies run: | dotnet add ./Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj package GitHubActionsTestLogger + dotnet add ./Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj package GitHubActionsTestLogger - name: Build run: dotnet build --no-restore ${{ needs.build.outputs.build_params }} - name: System Tests shell: pwsh run: dotnet test ./Tests/Reqnroll.SystemTests/Reqnroll.SystemTests.csproj --filter "TestCategory!=MsBuild&TestCategory!=Net481" --logger "trx;LogFileName=${{ github.workspace }}/TestResults/systemtests-linux-results.trx" ${{ needs.build.outputs.test_params }} + - name: CucumberMessages Tests + shell: pwsh + run: dotnet test ./Tests/CucumberMessages.CompatibilityTests/CucumberMessages.Tests.csproj --logger "trx;LogFileName=${{ github.workspace }}/TestResults/cucumbermessagestests-windows-results.trx" ${{ needs.build.outputs.test_params }} - name: Upload Test Result TRX Files uses: actions/upload-artifact@v4 if: always() diff --git a/.gitignore b/.gitignore index 2845d4764..bc330c143 100644 --- a/.gitignore +++ b/.gitignore @@ -386,3 +386,4 @@ docs/_build/* nCrunchTemp*.csproj *.received.* +/Tests/CucumberMessages.CompatibilityTests/Samples/CucumberMessages/* diff --git a/Reqnroll.Generator/DefaultDependencyProvider.cs b/Reqnroll.Generator/DefaultDependencyProvider.cs index 2c0ba5121..dbea0632c 100644 --- a/Reqnroll.Generator/DefaultDependencyProvider.cs +++ b/Reqnroll.Generator/DefaultDependencyProvider.cs @@ -1,5 +1,7 @@ using Reqnroll.BoDi; using Reqnroll.Configuration; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.EnvironmentAccess; using Reqnroll.Generator.Configuration; using Reqnroll.Generator.Generation; using Reqnroll.Generator.Interfaces; @@ -45,6 +47,9 @@ public virtual void RegisterDefaults(ObjectContainer container) container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); + container.RegisterTypeAs(); container.RegisterTypeAs(); diff --git a/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs b/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs new file mode 100644 index 000000000..c89f894da --- /dev/null +++ b/Reqnroll.Generator/Generation/CucumberGherkinDocumentExpressionGenerator.cs @@ -0,0 +1,425 @@ +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Io.Cucumber.Messages.Types; +using Reqnroll.Generator.CodeDom; +using System; +using System.Linq; +using System.CodeDom; +using System.Collections.Generic; + +namespace Reqnroll.Generator.Generation +{ + + /// + /// This class is responsible for generating a CodeDom expression that represents code that will recreate the given Cucumber Gherkin Document. + /// + internal class CucumberGherkinDocumentExpressionGenerator : CucumberMessage_TraversalVisitorBase + { + Io.Cucumber.Messages.Types.GherkinDocument _gherkinDocument; + CodeExpression _gherkinDocumentExpression; + + CodeExpression _feature; + List _CommentsList; + CodeExpression _location; + List _TagsList; + List _FeatureChildrenList; + CodeExpression _background; + CodeExpression _scenario; + CodeExpression _rule; + List _RuleChildrenList; + List _StepsList; + List _ExamplesList; + CodeExpression _dataTable; + CodeExpression _DocString; + List _TableRowsList; + List _TableCellsList; + + private static readonly string GENERICLIST = typeof(List<>).FullName; + + public CucumberGherkinDocumentExpressionGenerator() + { + } + private void Reset() + { + _feature = null; + _CommentsList = new List(); + _location = null; + _TagsList = new List(); + _FeatureChildrenList = new List(); + _background = null; + _scenario = null; + _rule = null; + _RuleChildrenList = new List(); + _StepsList = new List(); + _ExamplesList = new List(); + _dataTable = null; + _DocString = null; + _TableRowsList = new List(); + _TableCellsList = new List(); + + } + + public CodeExpression GenerateGherkinDocumentExpression(GherkinDocument gherkinDocument) + { + Reset(); + + _gherkinDocument = gherkinDocument; + _CommentsList = new List(); + + Visit(gherkinDocument); + + var commentTypeRef = new CodeTypeReference(typeof(Comment), CodeTypeReferenceOptions.GlobalReference); + var commentsListExpr = new CodeTypeReference(GENERICLIST, commentTypeRef); + var initializer = new CodeArrayCreateExpression(commentTypeRef, _CommentsList.ToArray()); + + _gherkinDocumentExpression = new CodeObjectCreateExpression(new CodeTypeReference(typeof(GherkinDocument), CodeTypeReferenceOptions.GlobalReference), + new CodePrimitiveExpression(_gherkinDocument.Uri), + _feature, + new CodeObjectCreateExpression(commentsListExpr, initializer)); + + return _gherkinDocumentExpression; + } + + + public override void Visit(Feature feature) + { + var location = _location; + var featureChildren = _FeatureChildrenList; + _FeatureChildrenList = new List(); + var tags = _TagsList; + _TagsList = new List(); + + base.Visit(feature); + + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); + + var fcCodeDomTypeRef = new CodeTypeReference(typeof(FeatureChild), CodeTypeReferenceOptions.GlobalReference); + var FClistExpr = new CodeTypeReference(GENERICLIST, fcCodeDomTypeRef); + var initializer = new CodeArrayCreateExpression(fcCodeDomTypeRef, _FeatureChildrenList.ToArray()); + + _feature = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Feature), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(feature.Language), + new CodePrimitiveExpression(feature.Keyword), + new CodePrimitiveExpression(feature.Name), + new CodePrimitiveExpression(feature.Description), + new CodeObjectCreateExpression(FClistExpr, initializer)); + + _location = location; + _FeatureChildrenList = featureChildren; + _TagsList = tags; + } + + public override void Visit(Comment comment) + { + var location = _location; + + base.Visit(comment); + + _CommentsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Comment), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodePrimitiveExpression(comment.Text))); + + _location = location; + } + + public override void Visit(Tag tag) + { + var location = _location; + + base.Visit(tag); + + _TagsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodePrimitiveExpression(tag.Name), + new CodePrimitiveExpression(tag.Id))); + + _location = location; + + } + + public override void Visit(Location location) + { + base.Visit(location); + var columnExprTypeExpr = new CodeTypeReference(typeof(Nullable<>)); + columnExprTypeExpr.TypeArguments.Add(typeof(long)); + + _location = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Location), CodeTypeReferenceOptions.GlobalReference), + new CodePrimitiveExpression(location.Line), + location.Column == null ? new CodeObjectCreateExpression(columnExprTypeExpr) :new CodeObjectCreateExpression(columnExprTypeExpr, new CodePrimitiveExpression(location.Column))); + + } + + public override void Visit(FeatureChild featureChild) + { + var rule = _rule; + var scenario = _scenario; + var background = _background; + + _rule = null; + _scenario = null; + _background = null; + + base.Visit(featureChild); + + _FeatureChildrenList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(FeatureChild), CodeTypeReferenceOptions.GlobalReference), + _rule ?? new CodePrimitiveExpression(null), + _background ?? new CodePrimitiveExpression(null), + _scenario ?? new CodePrimitiveExpression(null))); + + _rule = rule; + _scenario = scenario; + _background = background; + } + + public override void Visit(Io.Cucumber.Messages.Types.Rule rule) + { + var location = _location; + var ruleChildren = _RuleChildrenList; + _RuleChildrenList = new List(); + var tags = _TagsList; + _TagsList = new List(); + + base.Visit(rule); + + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); + + var ruleChildCodeDomTypeRef = new CodeTypeReference(typeof(RuleChild), CodeTypeReferenceOptions.GlobalReference); + var ruleChildrenListExpr = new CodeTypeReference(GENERICLIST, ruleChildCodeDomTypeRef); + var ruleChildrenInitializer = new CodeArrayCreateExpression(ruleChildCodeDomTypeRef, _RuleChildrenList.ToArray()); + + _rule = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.Rule), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(rule.Keyword), + new CodePrimitiveExpression(rule.Name), + new CodePrimitiveExpression(rule.Description), + new CodeObjectCreateExpression(ruleChildrenListExpr, ruleChildrenInitializer), + new CodePrimitiveExpression(rule.Id)); + + _location = location; + _RuleChildrenList = ruleChildren; + _TagsList = tags; + } + + public override void Visit(RuleChild ruleChild) + { + var background = _background; + var scenario = _scenario; + + _background = null; + _scenario = null; + + base.Visit(ruleChild); + + _RuleChildrenList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(RuleChild), CodeTypeReferenceOptions.GlobalReference), + _background ?? new CodePrimitiveExpression(null), + _scenario ?? new CodePrimitiveExpression(null))); + + _background = background; + _scenario = scenario; + } + + public override void Visit(Scenario scenario) + { + var location = _location; + var tags = _TagsList; + var steps = _StepsList; + var examples = _ExamplesList; + _TagsList = new List(); + _StepsList = new List(); + _ExamplesList = new List(); + + base.Visit(scenario); + + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); + + var stepCodeDomTypeRef = new CodeTypeReference(typeof(Step), CodeTypeReferenceOptions.GlobalReference); + var stepsListExpr = new CodeTypeReference(GENERICLIST, stepCodeDomTypeRef); + var stepsinitializer = new CodeArrayCreateExpression(stepCodeDomTypeRef, _StepsList.ToArray()); + + var examplesCodeDomTypeRef = new CodeTypeReference(typeof(Examples), CodeTypeReferenceOptions.GlobalReference); + var examplesListExpr = new CodeTypeReference(GENERICLIST, examplesCodeDomTypeRef); + var examplesinitializer = new CodeArrayCreateExpression(examplesCodeDomTypeRef, _ExamplesList.ToArray()); + + _scenario = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Scenario), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(scenario.Keyword), + new CodePrimitiveExpression(scenario.Name), + new CodePrimitiveExpression(scenario.Description), + new CodeObjectCreateExpression(stepsListExpr, stepsinitializer), + new CodeObjectCreateExpression(examplesListExpr, examplesinitializer), + new CodePrimitiveExpression(scenario.Id)); + + _location = location; + _TagsList = tags; + _StepsList = steps; + _ExamplesList = examples; + } + + public override void Visit(Examples examples) + { + var location = _location; + var tags = _TagsList; + var table = _TableRowsList; + _TagsList = new List(); + _TableRowsList = new List(); + + // When visting Examples, all TableRow intances that get visited (both TableHeaderRow and TableBodyRows) will get added to the _TableRowsList. + // Therefore, when we create the Examples create expression, we'll pull the Header out of the _TableRowsList as the first item + // and the Body out of the _TableRowsList as the rest of the items. + + base.Visit(examples); + + var tagCodeDomTypeRef = new CodeTypeReference(typeof(Tag), CodeTypeReferenceOptions.GlobalReference); + var tagsListExpr = new CodeTypeReference(GENERICLIST, tagCodeDomTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagCodeDomTypeRef, _TagsList.ToArray()); + var tableHeaderRow = _TableRowsList.First(); + + var tableRowCodeDomTypeRef = new CodeTypeReference(typeof(TableRow), CodeTypeReferenceOptions.GlobalReference); + var tableBodyListExpr = new CodeTypeReference(GENERICLIST, tableRowCodeDomTypeRef); + var tableBodyInitializer = new CodeArrayCreateExpression(tableRowCodeDomTypeRef, _TableRowsList.Skip(1).ToArray()); + + _ExamplesList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Examples), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodeObjectCreateExpression(tagsListExpr, tagsinitializer), + new CodePrimitiveExpression(examples.Keyword), + new CodePrimitiveExpression(examples.Name), + new CodePrimitiveExpression(examples.Description), + tableHeaderRow, + new CodeObjectCreateExpression(tableBodyListExpr, tableBodyInitializer), + new CodePrimitiveExpression(examples.Id))); + + _location = location; + _TagsList = tags; + _TableRowsList = table; + } + + public override void Visit(Background background) + { + var location = _location; + var steps = _StepsList; + _StepsList = new List(); + + base.Visit(background); + + + var stepCodeDomTypeRef = new CodeTypeReference(typeof(Step), CodeTypeReferenceOptions.GlobalReference); + var stepListExpr = new CodeTypeReference(GENERICLIST, stepCodeDomTypeRef); + var initializer = new CodeArrayCreateExpression(stepCodeDomTypeRef, _StepsList.ToArray()); + + _background = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Background), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodePrimitiveExpression(background.Keyword), + new CodePrimitiveExpression(background.Name), + new CodePrimitiveExpression(background.Description), + new CodeObjectCreateExpression(stepListExpr, initializer), + new CodePrimitiveExpression(background.Id)); + + _location = location; + _StepsList = steps; + } + + public override void Visit(Step step) + { + var location = _location; + var docString = _DocString; + var dataTable = _dataTable; + + _DocString = null; + _dataTable = null; + + base.Visit(step); + + _StepsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(Step), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodePrimitiveExpression(step.Keyword), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(new CodeTypeReference(typeof(StepKeywordType), CodeTypeReferenceOptions.GlobalReference)), step.KeywordType.ToString()), + new CodePrimitiveExpression(step.Text), + _DocString ?? new CodePrimitiveExpression(null), + _dataTable ?? new CodePrimitiveExpression(null), + new CodePrimitiveExpression(step.Id))); + + _location = location; + _DocString = docString; + _dataTable = dataTable; + } + + public override void Visit(DocString docString) + { + var location = _location; + + base.Visit(docString); + + _DocString = new CodeObjectCreateExpression(new CodeTypeReference(typeof(DocString), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodePrimitiveExpression(docString.MediaType), + new CodePrimitiveExpression(docString.Content), + new CodePrimitiveExpression(docString.Delimiter)); + + _location = location; + } + + public override void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) + { + var location = _location; + var rows = _TableRowsList; + _TableRowsList = new List(); + + base.Visit(dataTable); + + var tableRowCodeDomTypeRef = new CodeTypeReference(typeof(TableRow), CodeTypeReferenceOptions.GlobalReference); + var listExpr = new CodeTypeReference(GENERICLIST, tableRowCodeDomTypeRef); + var initializer = new CodeArrayCreateExpression(tableRowCodeDomTypeRef, _TableRowsList.ToArray()); + + _dataTable = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.DataTable), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodeObjectCreateExpression(listExpr, initializer)); + + _location = location; + _TableRowsList = rows; + } + + public override void Visit(TableRow row) + { + var location = _location; + var cells = _TableCellsList; + _TableCellsList = new List(); + + base.Visit(row); + + var tableCellCodeDomTypeRef = new CodeTypeReference(typeof(TableCell), CodeTypeReferenceOptions.GlobalReference); + var CellListExpr = new CodeTypeReference(GENERICLIST, tableCellCodeDomTypeRef); + + var initializer = new CodeArrayCreateExpression(tableCellCodeDomTypeRef, _TableCellsList.ToArray()); + + _TableRowsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(TableRow), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodeObjectCreateExpression(CellListExpr, initializer), + new CodePrimitiveExpression(row.Id))); + + _location = location; + _TableCellsList = cells; + } + + public override void Visit(TableCell cell) + { + var location = _location; + + base.Visit(cell); + + _TableCellsList.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(TableCell), CodeTypeReferenceOptions.GlobalReference), + _location, + new CodePrimitiveExpression(cell.Value))); + + _location = location; + } + } +} diff --git a/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs b/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs new file mode 100644 index 000000000..477d6bc7c --- /dev/null +++ b/Reqnroll.Generator/Generation/CucumberPicklesExpressionGenerator.cs @@ -0,0 +1,186 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Generator.CodeDom; +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.Generator.Generation +{ + /// + /// Generates a CodeDom expression to create a list of Cucumber Pickles + /// + internal class CucumberPicklesExpressionGenerator : CucumberMessage_TraversalVisitorBase + { + private List _PickleList; + private List _PickleSteps; + private List _PickleTags; + CodeExpression _PickleStepArgument; + CodeExpression _PickleDocString; + CodeExpression _PickleTable; + private List _TableRows; + private List _PickleCells; + + private static readonly string GENERICLIST = typeof(List<>).FullName; + + + public CucumberPicklesExpressionGenerator() + { + } + + private void Reset() + { + _PickleList = new List(); + _PickleSteps = new List(); + _PickleTags = new List(); + _PickleStepArgument = null; + _PickleDocString = null; + _PickleTable = null; + } + + public CodeExpression GeneratePicklesExpression(IEnumerable pickles) + { + Reset(); + foreach (var pickle in pickles) + { + Visit(pickle); + } + + var pickleCodeTypeRef = new CodeTypeReference(typeof(Pickle), CodeTypeReferenceOptions.GlobalReference); + var commentsListExpr = new CodeTypeReference(GENERICLIST, pickleCodeTypeRef); + var initializer = new CodeArrayCreateExpression(pickleCodeTypeRef, _PickleList.ToArray()); + + return new CodeObjectCreateExpression(commentsListExpr, initializer); + } + + public override void Visit(Pickle pickle) + { + var steps = _PickleSteps; + _PickleSteps = new List(); + + var tags = _PickleTags; + _PickleTags = new List(); + + base.Visit(pickle); + + var pStepTypeRef = new CodeTypeReference(typeof(PickleStep), CodeTypeReferenceOptions.GlobalReference); + var stepsExpr = new CodeTypeReference(GENERICLIST, pStepTypeRef); + var stepsinitializer = new CodeArrayCreateExpression(pStepTypeRef, _PickleSteps.ToArray()); + + var tagsTypeRef = new CodeTypeReference(typeof(PickleTag), CodeTypeReferenceOptions.GlobalReference); + var tagsExpr = new CodeTypeReference(GENERICLIST, tagsTypeRef); + var tagsinitializer = new CodeArrayCreateExpression(tagsTypeRef, _PickleTags.ToArray()); + + var astIdsExpr = new CodeTypeReference(typeof(List)); + var astIdsInitializer = new CodeArrayCreateExpression(typeof(string), pickle.AstNodeIds.Select(s => new CodePrimitiveExpression(s)).ToArray()); + + _PickleList.Add(new CodeObjectCreateExpression( + new CodeTypeReference(typeof(Pickle), CodeTypeReferenceOptions.GlobalReference), + new CodePrimitiveExpression(pickle.Id), + new CodePrimitiveExpression(pickle.Uri), + new CodePrimitiveExpression(pickle.Name), + new CodePrimitiveExpression(pickle.Language), + new CodeObjectCreateExpression(stepsExpr, stepsinitializer), + new CodeObjectCreateExpression(tagsExpr, tagsinitializer), + new CodeObjectCreateExpression(astIdsExpr, astIdsInitializer) + )); + + _PickleSteps = steps; + _PickleTags = tags; + } + + public override void Visit(PickleStep step) + { + var arg = _PickleStepArgument; + _PickleStepArgument = null; + + base.Visit(step); + + var astIdsExpr = new CodeTypeReference(typeof(List)); + var astIdsInitializer = new CodeArrayCreateExpression(typeof(string), step.AstNodeIds.Select(s => new CodePrimitiveExpression(s)).ToArray()); + + _PickleSteps.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleStep), CodeTypeReferenceOptions.GlobalReference), + _PickleStepArgument ?? new CodePrimitiveExpression(null), + new CodeObjectCreateExpression(astIdsExpr, astIdsInitializer), + new CodePrimitiveExpression(step.Id), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(new CodeTypeReference(typeof(PickleStepType), CodeTypeReferenceOptions.GlobalReference)), step.Type.ToString()), + new CodePrimitiveExpression(step.Text))); + + _PickleStepArgument = arg; + } + + public override void Visit(PickleDocString docString) + { + _PickleDocString = new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleDocString), CodeTypeReferenceOptions.GlobalReference), + new CodePrimitiveExpression(docString.MediaType), + new CodePrimitiveExpression(docString.Content)); + } + + public override void Visit(PickleStepArgument argument) + { + var docString = _PickleDocString; + var table = _PickleTable; + + _PickleDocString = null; + _PickleTable = null; + + base.Visit(argument); + + _PickleStepArgument = new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleStepArgument), CodeTypeReferenceOptions.GlobalReference), + _PickleDocString ?? new CodePrimitiveExpression(null), + _PickleTable ?? new CodePrimitiveExpression(null)); + + _PickleDocString = docString; + _PickleTable = table; + } + + public override void Visit(PickleTable pickleTable) + { + var rows = _TableRows; + _TableRows = new List(); + + base.Visit(pickleTable); + + var pickleTableRowTypeRef = new CodeTypeReference(typeof(PickleTableRow), CodeTypeReferenceOptions.GlobalReference); + var rowsExpr = new CodeTypeReference(GENERICLIST, pickleTableRowTypeRef); + var rowsInitializer = new CodeArrayCreateExpression(pickleTableRowTypeRef, _TableRows.ToArray()); + + _PickleTable = new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTable), CodeTypeReferenceOptions.GlobalReference), + new CodeObjectCreateExpression(rowsExpr, rowsInitializer)); + + _TableRows = rows; + } + + public override void Visit(PickleTableRow row) + { + var cells = _PickleCells; + _PickleCells = new List(); + + base.Visit(row); + + var pickleTableCellTypeRef = new CodeTypeReference(typeof(PickleTableCell), CodeTypeReferenceOptions.GlobalReference); + var cellsExpr = new CodeTypeReference(GENERICLIST, pickleTableCellTypeRef); + var cellsInitializer = new CodeArrayCreateExpression(pickleTableCellTypeRef, _PickleCells.ToArray()); + + _TableRows.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTableRow), CodeTypeReferenceOptions.GlobalReference), + new CodeObjectCreateExpression(cellsExpr, cellsInitializer))); + + _PickleCells = cells; + } + + public override void Visit(PickleTableCell cell) + { + _PickleCells.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTableCell), CodeTypeReferenceOptions.GlobalReference), + new CodePrimitiveExpression(cell.Value))); + } + + public override void Visit(PickleTag tag) + { + _PickleTags.Add(new CodeObjectCreateExpression(new CodeTypeReference(typeof(PickleTag), CodeTypeReferenceOptions.GlobalReference), + new CodePrimitiveExpression(tag.Name), + new CodePrimitiveExpression(tag.AstNodeId))); + } + } +} diff --git a/Reqnroll.Generator/Generation/GeneratorConstants.cs b/Reqnroll.Generator/Generation/GeneratorConstants.cs index 859c3a111..daf6d2853 100644 --- a/Reqnroll.Generator/Generation/GeneratorConstants.cs +++ b/Reqnroll.Generator/Generation/GeneratorConstants.cs @@ -19,5 +19,7 @@ public class GeneratorConstants public const string SCENARIO_TAGS_VARIABLE_NAME = "tagsOfScenario"; public const string SCENARIO_ARGUMENTS_VARIABLE_NAME = "argumentsOfScenario"; public const string FEATURE_TAGS_VARIABLE_NAME = "featureTags"; + public const string PICKLEINDEX_PARAMETER_NAME = "__pickleIndex"; + public const string PICKLEINDEX_VARIABLE_NAME = "m_pickleIndex"; } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index 658c9b387..3e656aa69 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -48,7 +48,7 @@ public void SetupFeatureBackground(TestClassGenerationContext generationContext) GenerateStep(generationContext, statements, step, null); } backgroundMethod.Statements.AddRange(statements.ToArray()); - + } #region Rule Background Support @@ -92,7 +92,7 @@ public void GenerateStep(TestClassGenerationContext generationContext, List {new CodePrimitiveExpression(formatText)}; + var formatArguments = new List {new CodePrimitiveExpression(formatText) }; formatArguments.AddRange(arguments.Select(id => new CodeVariableReferenceExpression(id))); return new CodeMethodInvokeExpression( @@ -233,5 +233,15 @@ private CodeExpression GetSubstitutedString(string text, ParameterSubstitution p "Format", formatArguments.ToArray()); } + public void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, int pickleIndex) + { + // string m_pickleId = pickleJar.CurrentPickleId; or + // string m_pickleId = @pickleId; + var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), GeneratorConstants.PICKLEINDEX_VARIABLE_NAME, + pickleIdIncludedInParameters ? + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_PARAMETER_NAME) : + new CodePrimitiveExpression(pickleIndex.ToString())); + testMethod.Statements.Add(pickleIdVariable); + } } } \ No newline at end of file diff --git a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs index 97af90d9a..b1136ee3d 100644 --- a/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs @@ -1,14 +1,20 @@ using System; using System.CodeDom; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; +using Gherkin.CucumberMessages; using Reqnroll.Configuration; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Generator.CodeDom; using Reqnroll.Generator.UnitTestConverter; using Reqnroll.Generator.UnitTestProvider; using Reqnroll.Parser; +using Reqnroll.Parser.CucmberMessageSupport; using Reqnroll.Tracing; namespace Reqnroll.Generator.Generation @@ -23,12 +29,16 @@ public class UnitTestFeatureGenerator : IFeatureGenerator private readonly IUnitTestGeneratorProvider _testGeneratorProvider; private readonly UnitTestMethodGenerator _unitTestMethodGenerator; private readonly LinePragmaHandler _linePragmaHandler; + private readonly ICucumberMessagesConfiguration _cucumberConfiguration; + private CodeMemberMethod _cucumberMessagesInitializeMethod; public UnitTestFeatureGenerator( IUnitTestGeneratorProvider testGeneratorProvider, CodeDomHelper codeDomHelper, ReqnrollConfiguration reqnrollConfiguration, - IDecoratorRegistry decoratorRegistry) + IDecoratorRegistry decoratorRegistry, + // Adding a dependency on the Cucumber configuration subsystem. Eventually remove this as Cucumber Config is folded into overall Reqnroll Config. + ICucumberMessagesConfiguration cucumberConfiguration) { _testGeneratorProvider = testGeneratorProvider; _codeDomHelper = codeDomHelper; @@ -37,11 +47,12 @@ public UnitTestFeatureGenerator( _linePragmaHandler = new LinePragmaHandler(_reqnrollConfiguration, _codeDomHelper); _scenarioPartHelper = new ScenarioPartHelper(_reqnrollConfiguration, _codeDomHelper); _unitTestMethodGenerator = new UnitTestMethodGenerator(testGeneratorProvider, decoratorRegistry, _codeDomHelper, _scenarioPartHelper, _reqnrollConfiguration); + _cucumberConfiguration = cucumberConfiguration; } public string TestClassNameFormat { get; set; } = "{0}Feature"; - public CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace) + public CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace,out IEnumerable generationWarnings) { var codeNamespace = CreateNamespace(targetNamespace); var feature = document.ReqnrollFeature; @@ -65,6 +76,8 @@ public CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string t //before returning the generated code, call the provider's method in case the generated code needs to be customized _testGeneratorProvider.FinalizeTestClass(generationContext); + + generationWarnings = generationContext.GenerationWarnings; return codeNamespace; } @@ -118,7 +131,7 @@ private void SetupScenarioCleanupMethod(TestClassGenerationContext generationCon var scenarioCleanupMethod = generationContext.ScenarioCleanupMethod; scenarioCleanupMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final; - scenarioCleanupMethod.Name = GeneratorConstants.SCENARIO_CLEANUP_NAME; + scenarioCleanupMethod.Name = GeneratorConstants.SCENARIO_CLEANUP_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(scenarioCleanupMethod); @@ -152,6 +165,7 @@ private void SetupTestClass(TestClassGenerationContext generationContext) } DeclareFeatureTagsField(generationContext); + DeclareFeatureMessagesFactoryMembers(generationContext); DeclareFeatureInfoMember(generationContext); } @@ -184,11 +198,110 @@ private void DeclareFeatureInfoMember(TestClassGenerationContext generationConte new CodeFieldReferenceExpression( new CodeTypeReferenceExpression(new CodeTypeReference(typeof(ProgrammingLanguage), CodeTypeReferenceOptions.GlobalReference)), _codeDomHelper.TargetLanguage.ToString()), - new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME)); + new CodeFieldReferenceExpression(null, GeneratorConstants.FEATURE_TAGS_VARIABLE_NAME), + new CodeMethodInvokeExpression(null, _cucumberMessagesInitializeMethod.Name)); generationContext.TestClass.Members.Add(featureInfoField); } + private void DeclareFeatureMessagesFactoryMembers(TestClassGenerationContext generationContext) + { + // Generation of Cucumber Messages relies on access to the parsed AST. + CodeObjectCreateExpression sourceExpression; + CodeExpression gherkinDocumentExpression; + CodeExpression picklesExpression; + CodeDelegateCreateExpression sourceFunc; + CodeDelegateCreateExpression gherkinDocumentFunc; + CodeDelegateCreateExpression picklesFunc; + + string sourceFileLocation; + sourceFileLocation = Path.Combine(generationContext.Document.DocumentLocation.FeatureFolderPath, generationContext.Document.DocumentLocation.SourceFilePath); + + // Adding three static methods to the test class: one each as Factory methods for source, gherkinDocument, and pickles Messages + // Bodies of these methods are added later inside the try/catch block + sourceFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func), CodeTypeReferenceOptions.GlobalReference), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "SourceFunc"); + var sourceFactoryMethod = new CodeMemberMethod(); + sourceFactoryMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + sourceFactoryMethod.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.Source), CodeTypeReferenceOptions.GlobalReference); + sourceFactoryMethod.Name = sourceFunc.MethodName; + generationContext.TestClass.Members.Add(sourceFactoryMethod); + + gherkinDocumentFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func), CodeTypeReferenceOptions.GlobalReference), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "GherkinDocumentFunc"); + var gherkinDocumentFactoryMethod = new CodeMemberMethod(); + gherkinDocumentFactoryMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + gherkinDocumentFactoryMethod.ReturnType = new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.GherkinDocument), CodeTypeReferenceOptions.GlobalReference); + gherkinDocumentFactoryMethod.Name = gherkinDocumentFunc.MethodName; + generationContext.TestClass.Members.Add(gherkinDocumentFactoryMethod); + + picklesFunc = new CodeDelegateCreateExpression(new CodeTypeReference(typeof(Func>), CodeTypeReferenceOptions.GlobalReference), new CodeTypeReferenceExpression(generationContext.TestClass.Name), "PicklesFunc"); + var picklesFactoryMethod = new CodeMemberMethod(); + picklesFactoryMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + picklesFactoryMethod.ReturnType = new CodeTypeReference(typeof(System.Collections.Generic.IEnumerable), CodeTypeReferenceOptions.GlobalReference); + picklesFactoryMethod.Name = picklesFunc.MethodName; + generationContext.TestClass.Members.Add(picklesFactoryMethod); + + // Create a new method that will be added to the test class. + // It will be called to provide the FeatureCucumberMessages property value of the FeatureInfo object when that object is constructed + var CucumberMessagesInitializeMethod = new CodeMemberMethod(); + CucumberMessagesInitializeMethod.Attributes = MemberAttributes.Private | MemberAttributes.Static; + CucumberMessagesInitializeMethod.Name = "InitializeCucumberMessages"; + CucumberMessagesInitializeMethod.ReturnType = new CodeTypeReference(typeof(FeatureLevelCucumberMessages), CodeTypeReferenceOptions.GlobalReference); + generationContext.TestClass.Members.Add(CucumberMessagesInitializeMethod); + _cucumberMessagesInitializeMethod = CucumberMessagesInitializeMethod; + + // Create a FeatureLevelCucumberMessages object and add it to featureInfo + var featureLevelCucumberMessagesExpression = new CodeObjectCreateExpression(new CodeTypeReference(typeof(FeatureLevelCucumberMessages), CodeTypeReferenceOptions.GlobalReference), + sourceFunc, + gherkinDocumentFunc, + picklesFunc, + new CodePrimitiveExpression(sourceFileLocation)); + + CucumberMessagesInitializeMethod.Statements.Add( + new CodeMethodReturnStatement( + featureLevelCucumberMessagesExpression)); + + try + { + var messageConverter = new CucumberMessagesConverter(new GuidIdGenerator()); + var featureSource = Reqnroll.CucumberMessages.PayloadProcessing.Cucumber.CucumberMessageTransformer.ToSource(messageConverter.ConvertToCucumberMessagesSource(generationContext.Document)); + var featureGherkinDocument = messageConverter.ConvertToCucumberMessagesGherkinDocument(generationContext.Document); + var featurePickles = messageConverter.ConvertToCucumberMessagesPickles(featureGherkinDocument); + var featureGherkinDocumentMessage = CucumberMessages.PayloadProcessing.Cucumber.CucumberMessageTransformer.ToGherkinDocument(featureGherkinDocument); + var featurePickleMessages = CucumberMessages.PayloadProcessing.Cucumber.CucumberMessageTransformer.ToPickles(featurePickles); + + // generate a CodeDom expression to create the Source object from the featureSourceMessage + sourceExpression = new CodeObjectCreateExpression(new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.Source), CodeTypeReferenceOptions.GlobalReference), + new CodePrimitiveExpression(featureSource.Uri), + new CodePrimitiveExpression(featureSource.Data), + new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(new CodeTypeReference(typeof(Io.Cucumber.Messages.Types.SourceMediaType), CodeTypeReferenceOptions.GlobalReference)), featureSource.MediaType.ToString())); + + // generate a CodeDom expression to create the GherkinDocument object from the featureGherkinDocumentMessage + var gherkinDocumentExpressionGenerator = new CucumberGherkinDocumentExpressionGenerator(); + gherkinDocumentExpression = gherkinDocumentExpressionGenerator.GenerateGherkinDocumentExpression(featureGherkinDocumentMessage); + + // generate a CodeDom expression to create the Pickles object from the featurePickleMessages + var pickleExpressionGenerator = new CucumberPicklesExpressionGenerator(); + picklesExpression = pickleExpressionGenerator.GeneratePicklesExpression(featurePickleMessages); + + // wrap these expressions in Func + + sourceFactoryMethod.Statements.Add(new CodeMethodReturnStatement(sourceExpression)); + + gherkinDocumentFactoryMethod.Statements.Add(new CodeMethodReturnStatement(gherkinDocumentExpression)); + + picklesFactoryMethod.Statements.Add(new CodeMethodReturnStatement(picklesExpression)); + + } + catch (Exception e) + { + generationContext.GenerationWarnings.Add($"WARNING: Failed to process Cucumber Pickles. Support for generating Cucumber Messages will be disabled. Exception: {e.Message}"); + // Should any error occur during pickling or serialization of Cucumber Messages, we will abort and not add the Cucumber Messages to the featureInfo. + // This effectively turns OFF the Cucumber Messages support for this feature. + + return; + } + } + private void SetupTestClassInitializeMethod(TestClassGenerationContext generationContext) { var testClassInitializeMethod = generationContext.TestClassInitializeMethod; @@ -197,7 +310,7 @@ private void SetupTestClassInitializeMethod(TestClassGenerationContext generatio testClassInitializeMethod.Name = GeneratorConstants.TESTCLASS_INITIALIZE_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testClassInitializeMethod); - + _testGeneratorProvider.SetTestClassInitializeMethod(generationContext); } @@ -209,7 +322,7 @@ private void SetupTestClassCleanupMethod(TestClassGenerationContext generationCo testClassCleanupMethod.Name = GeneratorConstants.TESTCLASS_CLEANUP_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testClassCleanupMethod); - + _testGeneratorProvider.SetTestClassCleanupMethod(generationContext); } @@ -221,7 +334,7 @@ private void SetupTestInitializeMethod(TestClassGenerationContext generationCont testInitializeMethod.Name = GeneratorConstants.TEST_INITIALIZE_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testInitializeMethod); - + _testGeneratorProvider.SetTestInitializeMethod(generationContext); // Obtain the test runner for executing a single test @@ -232,7 +345,7 @@ private void SetupTestInitializeMethod(TestClassGenerationContext generationCont var getTestRunnerExpression = new CodeMethodInvokeExpression( new CodeTypeReferenceExpression(new CodeTypeReference(typeof(TestRunnerManager), CodeTypeReferenceOptions.GlobalReference)), nameof(TestRunnerManager.GetTestRunnerForAssembly), - _codeDomHelper.CreateOptionalArgumentExpression("featureHint", + _codeDomHelper.CreateOptionalArgumentExpression("featureHint", new CodeVariableReferenceExpression(GeneratorConstants.FEATUREINFO_FIELD))); testInitializeMethod.Statements.Add( @@ -305,11 +418,11 @@ private void SetupTestCleanupMethod(TestClassGenerationContext generationContext testCleanupMethod.Name = GeneratorConstants.TEST_CLEANUP_NAME; _codeDomHelper.MarkCodeMemberMethodAsAsync(testCleanupMethod); - + _testGeneratorProvider.SetTestCleanupMethod(generationContext); var testRunnerField = _scenarioPartHelper.GetTestRunnerExpression(); - + //await testRunner.OnScenarioEndAsync(); var expression = new CodeMethodInvokeExpression( testRunnerField, @@ -352,7 +465,7 @@ private void SetupScenarioStartMethod(TestClassGenerationContext generationConte scenarioStartMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final; scenarioStartMethod.Name = GeneratorConstants.SCENARIO_START_NAME; - + _codeDomHelper.MarkCodeMemberMethodAsAsync(scenarioStartMethod); //await testRunner.OnScenarioStartAsync(); diff --git a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs index 3e71a178c..a36dcdf10 100644 --- a/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs +++ b/Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs @@ -23,6 +23,11 @@ public class UnitTestMethodGenerator private readonly ReqnrollConfiguration _reqnrollConfiguration; private readonly IUnitTestGeneratorProvider _unitTestGeneratorProvider; + // When generating test methods, the pickle index tells the runtime which Pickle this test method/case corresponds to. + // The index is used during Cucumber Message generation to look up the PickleID and include it in the emitted Cucumber Messages. + // As test methods are generated, the pickle index is incremented. + private int _pickleIndex = 0; + public UnitTestMethodGenerator(IUnitTestGeneratorProvider unitTestGeneratorProvider, IDecoratorRegistry decoratorRegistry, CodeDomHelper codeDomHelper, ScenarioPartHelper scenarioPartHelper, ReqnrollConfiguration reqnrollConfiguration) { _unitTestGeneratorProvider = unitTestGeneratorProvider; @@ -39,13 +44,14 @@ IEnumerable GetScenarioDefinitionsOfRule(IEnume .Where(child => child is not Background) .Select(sd => new ScenarioDefinitionInFeatureFile(sd, feature, rule)); - return + return GetScenarioDefinitionsOfRule(feature.Children, null) .Concat(feature.Children.OfType().SelectMany(rule => GetScenarioDefinitionsOfRule(rule.Children, rule))); } public void CreateUnitTests(ReqnrollFeature feature, TestClassGenerationContext generationContext) { + _pickleIndex = 0; foreach (var scenarioDefinition in GetScenarioDefinitions(feature)) { CreateUnitTest(generationContext, scenarioDefinition); @@ -66,6 +72,7 @@ private void CreateUnitTest(TestClassGenerationContext generationContext, Scenar else { GenerateTest(generationContext, scenarioDefinitionInFeatureFile); + _pickleIndex++; } } @@ -87,7 +94,7 @@ private void GenerateScenarioOutlineTest(TestClassGenerationContext generationCo GenerateScenarioOutlineExamplesAsIndividualMethods(scenarioOutline, generationContext, scenarioOutlineTestMethod, paramToIdentifier); } - GenerateTestBody(generationContext, scenarioDefinitionInFeatureFile, scenarioOutlineTestMethod, exampleTagsParam, paramToIdentifier); + GenerateTestBody(generationContext, scenarioDefinitionInFeatureFile, scenarioOutlineTestMethod, exampleTagsParam, paramToIdentifier, true); } private void GenerateTest(TestClassGenerationContext generationContext, ScenarioDefinitionInFeatureFile scenarioDefinitionInFeatureFile) @@ -129,7 +136,12 @@ private void GenerateTestBody( TestClassGenerationContext generationContext, ScenarioDefinitionInFeatureFile scenarioDefinitionInFeatureFile, CodeMemberMethod testMethod, - CodeExpression additionalTagsExpression = null, ParameterSubstitution paramToIdentifier = null) + CodeExpression additionalTagsExpression = null, + ParameterSubstitution paramToIdentifier = null, + + // This flag indicates whether the pickleIndex will be given to the method as an argument (coming from a RowTest) (if true) + // or should be generated as a local variable (if false). + bool pickleIdIncludedInParameters = false) { var scenarioDefinition = scenarioDefinitionInFeatureFile.ScenarioDefinition; var feature = scenarioDefinitionInFeatureFile.Feature; @@ -190,6 +202,11 @@ private void GenerateTestBody( AddVariableForArguments(testMethod, paramToIdentifier); + // Cucumber Messages support uses a new variables: pickleIndex + // The pickleIndex tells the runtime which Pickle this test corresponds to. + // When Backgrounds and Rule Backgrounds are used, we don't know ahead of time how many Steps there are in the Pickle. + AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, _pickleIndex); + testMethod.Statements.Add( new CodeVariableDeclarationStatement(new CodeTypeReference(typeof(ScenarioInfo), CodeTypeReferenceOptions.GlobalReference), "scenarioInfo", new CodeObjectCreateExpression(new CodeTypeReference(typeof(ScenarioInfo), CodeTypeReferenceOptions.GlobalReference), @@ -197,7 +214,8 @@ private void GenerateTestBody( new CodePrimitiveExpression(scenarioDefinition.Description), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME), new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME), - inheritedTagsExpression))); + inheritedTagsExpression, + new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_VARIABLE_NAME)))); GenerateScenarioInitializeCall(generationContext, scenarioDefinition, testMethod); @@ -206,6 +224,11 @@ private void GenerateTestBody( GenerateScenarioCleanupMethodCall(generationContext, testMethod); } + internal void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, int pickleIndex) + { + _scenarioPartHelper.AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, pickleIndex); + } + private void AddVariableForTags(CodeMemberMethod testMethod, CodeExpression tagsExpression) { var tagVariable = new CodeVariableDeclarationStatement(typeof(string[]), GeneratorConstants.SCENARIO_TAGS_VARIABLE_NAME, tagsExpression); @@ -261,7 +284,6 @@ internal void GenerateTestMethodBody(TestClassGenerationContext generationContex var backgroundMethodCallExpression = new CodeMethodInvokeExpression( new CodeThisReferenceExpression(), generationContext.FeatureBackgroundMethod.Name); - _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(backgroundMethodCallExpression); statementsWhenScenarioIsExecuted.Add(new CodeExpressionStatement(backgroundMethodCallExpression)); } @@ -322,7 +344,7 @@ internal void GenerateScenarioCleanupMethodCall(TestClassGenerationContext gener testMethod.Statements.Add(expression); } - + private CodeMethodInvokeExpression CreateTestRunnerSkipScenarioCall() { return new CodeMethodInvokeExpression( @@ -362,7 +384,8 @@ private void GenerateScenarioOutlineExamplesAsIndividualMethods( foreach (var example in exampleSet.TableBody.Select((r, i) => new { Row = r, Index = i })) { var variantName = useFirstColumnAsName ? example.Row.Cells.First().Value : string.Format("Variant {0}", example.Index); - GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, exampleSet.Tags.ToArray(), variantName); + GenerateScenarioOutlineTestVariant(generationContext, scenarioOutline, scenarioOutlineTestMethod, paramToIdentifier, exampleSet.Name ?? "", exampleSetIdentifier, example.Row, _pickleIndex, exampleSet.Tags.ToArray(), variantName); + _pickleIndex++; } exampleSetIndex++; @@ -377,8 +400,11 @@ private void GenerateScenarioOutlineExamplesAsRowTests(TestClassGenerationContex { foreach (var row in examples.TableBody) { - var arguments = row.Cells.Select(c => c.Value); + var arguments = row.Cells.Select(c => c.Value).Concat(new[] { _pickleIndex.ToString() }); + _unitTestGeneratorProvider.SetRow(generationContext, scenatioOutlineTestMethod, arguments, GetNonIgnoreTags(examples.Tags), HasIgnoreTag(examples.Tags)); + + _pickleIndex++; } } } @@ -434,12 +460,12 @@ private CodeMemberMethod CreateScenarioOutlineTestMethod(TestClassGenerationCont testMethod.Name = string.Format(GeneratorConstants.TEST_NAME_FORMAT, scenarioOutline.Name.ToIdentifier()); _codeDomHelper.MarkCodeMemberMethodAsAsync(testMethod); - + foreach (var pair in paramToIdentifier) { testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), pair.Value)); } - + testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), GeneratorConstants.PICKLEINDEX_PARAMETER_NAME)); testMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string[]), GeneratorConstants.SCENARIO_OUTLINE_EXAMPLE_TAGS_PARAMETER)); return testMethod; } @@ -452,16 +478,17 @@ private void GenerateScenarioOutlineTestVariant( string exampleSetTitle, string exampleSetIdentifier, Gherkin.Ast.TableRow row, + int pickleIndex, Tag[] exampleSetTags, string variantName) { var testMethod = CreateTestMethod(generationContext, scenarioOutline, exampleSetTags, variantName, exampleSetIdentifier); - + //call test implementation with the params var argumentExpressions = row.Cells.Select(paramCell => new CodePrimitiveExpression(paramCell.Value)).Cast().ToList(); - + argumentExpressions.Add(new CodePrimitiveExpression(pickleIndex.ToString())); argumentExpressions.Add(_scenarioPartHelper.GetStringArrayExpression(exampleSetTags)); - + var statements = new List(); using (new SourceLineScope(_reqnrollConfiguration, _codeDomHelper, statements, generationContext.Document.SourceFilePath, scenarioOutline.Location)) @@ -475,7 +502,7 @@ private void GenerateScenarioOutlineTestVariant( statements.Add(new CodeExpressionStatement(callTestMethodExpression)); } - + testMethod.Statements.AddRange(statements.ToArray()); //_linePragmaHandler.AddLineDirectiveHidden(testMethod.Statements); diff --git a/Reqnroll.Generator/Interfaces/TestGeneratorResult.cs b/Reqnroll.Generator/Interfaces/TestGeneratorResult.cs index 2ace9841e..898ebbe0b 100644 --- a/Reqnroll.Generator/Interfaces/TestGeneratorResult.cs +++ b/Reqnroll.Generator/Interfaces/TestGeneratorResult.cs @@ -26,6 +26,10 @@ public class TestGeneratorResult public bool Success { get { return Errors == null || !Errors.Any(); } } + /// + /// Warning messages from code generation, if any. + /// + public IEnumerable Warnings { get; private set; } public TestGeneratorResult(params TestGenerationError[] errors) : this((IEnumerable)errors) { @@ -37,12 +41,16 @@ public TestGeneratorResult(IEnumerable errors) if (errors.Count() == 0) throw new ArgumentException("no errors provided", "errors"); Errors = errors.ToArray(); + Warnings = new List(); } - public TestGeneratorResult(string generatedTestCode, bool isUpToDate) + public TestGeneratorResult(string generatedTestCode, bool isUpToDate, IEnumerable warnings) { IsUpToDate = isUpToDate; GeneratedTestCode = generatedTestCode; + Warnings = new List(); + if (warnings != null) + Warnings = warnings.ToList(); } } } \ No newline at end of file diff --git a/Reqnroll.Generator/TestClassGenerationContext.cs b/Reqnroll.Generator/TestClassGenerationContext.cs index 0ce2a42d3..8a323b810 100644 --- a/Reqnroll.Generator/TestClassGenerationContext.cs +++ b/Reqnroll.Generator/TestClassGenerationContext.cs @@ -28,6 +28,8 @@ public class TestClassGenerationContext public IDictionary CustomData { get; private set; } + public ICollection GenerationWarnings { get; private set; } + public TestClassGenerationContext( IUnitTestGeneratorProvider unitTestGeneratorProvider, ReqnrollDocument document, @@ -60,6 +62,7 @@ public TestClassGenerationContext( GenerateRowTests = generateRowTests; CustomData = new Dictionary(); + GenerationWarnings = new List(); } } } \ No newline at end of file diff --git a/Reqnroll.Generator/TestGenerator.cs b/Reqnroll.Generator/TestGenerator.cs index 1cf033d21..ed7ad27dd 100644 --- a/Reqnroll.Generator/TestGenerator.cs +++ b/Reqnroll.Generator/TestGenerator.cs @@ -1,6 +1,8 @@ using System; using System.CodeDom; using System.CodeDom.Compiler; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -61,18 +63,18 @@ protected override TestGeneratorResult GenerateTestFileWithExceptions(FeatureFil { preliminaryUpToDateCheckResult = testUpToDateChecker.IsUpToDatePreliminary(featureFileInput, generatedTestFullPath, settings.UpToDateCheckingMethod); if (preliminaryUpToDateCheckResult == true) - return new TestGeneratorResult(null, true); + return new TestGeneratorResult(null, true, null); } - string generatedTestCode = GetGeneratedTestCode(featureFileInput); + string generatedTestCode = GetGeneratedTestCode(featureFileInput, out IEnumerable generatedWarnings); if(string.IsNullOrEmpty(generatedTestCode)) - return new TestGeneratorResult(null, true); + return new TestGeneratorResult(null, true, generatedWarnings); if (settings.CheckUpToDate && preliminaryUpToDateCheckResult != false) { var isUpToDate = testUpToDateChecker.IsUpToDate(featureFileInput, generatedTestFullPath, generatedTestCode, settings.UpToDateCheckingMethod); if (isUpToDate) - return new TestGeneratorResult(null, true); + return new TestGeneratorResult(null, true, generatedWarnings); } if (settings.WriteResultToFile) @@ -80,16 +82,17 @@ protected override TestGeneratorResult GenerateTestFileWithExceptions(FeatureFil File.WriteAllText(generatedTestFullPath, generatedTestCode, Encoding.UTF8); } - return new TestGeneratorResult(generatedTestCode, false); + return new TestGeneratorResult(generatedTestCode, false, generatedWarnings); } - protected string GetGeneratedTestCode(FeatureFileInput featureFileInput) + protected string GetGeneratedTestCode(FeatureFileInput featureFileInput, out IEnumerable generationWarnings) { + generationWarnings = new List(); using (var outputWriter = new IndentProcessingWriter(new StringWriter())) { var codeProvider = codeDomHelper.CreateCodeDomProvider(); - var codeNamespace = GenerateTestFileCode(featureFileInput); - + var codeNamespace = GenerateTestFileCode(featureFileInput, out IEnumerable generatedWarnings); + generationWarnings = generatedWarnings; if (codeNamespace == null) return ""; var options = new CodeGeneratorOptions @@ -138,8 +141,9 @@ private string FixVBNetAsyncMethodDeclarations(string generatedTestCode) return result; } - private CodeNamespace GenerateTestFileCode(FeatureFileInput featureFileInput) + private CodeNamespace GenerateTestFileCode(FeatureFileInput featureFileInput, out IEnumerable generationWarnings) { + generationWarnings = new List(); string targetNamespace = GetTargetNamespace(featureFileInput) ?? "Reqnroll.GeneratedTests"; var parser = gherkinParserFactory.Create(reqnrollConfiguration.FeatureLanguage); @@ -153,7 +157,8 @@ private CodeNamespace GenerateTestFileCode(FeatureFileInput featureFileInput) var featureGenerator = featureGeneratorRegistry.CreateGenerator(reqnrollDocument); - var codeNamespace = featureGenerator.GenerateUnitTestFixture(reqnrollDocument, null, targetNamespace); + var codeNamespace = featureGenerator.GenerateUnitTestFixture(reqnrollDocument, null, targetNamespace, out var generatedWarnings); + generationWarnings = generatedWarnings; return codeNamespace; } diff --git a/Reqnroll.Generator/UnitTestConverter/IFeatureGenerator.cs b/Reqnroll.Generator/UnitTestConverter/IFeatureGenerator.cs index e99d03716..0b39db1f9 100644 --- a/Reqnroll.Generator/UnitTestConverter/IFeatureGenerator.cs +++ b/Reqnroll.Generator/UnitTestConverter/IFeatureGenerator.cs @@ -1,10 +1,12 @@ using System.CodeDom; +using System.Collections.Generic; +using System.Transactions; using Reqnroll.Parser; namespace Reqnroll.Generator.UnitTestConverter { public interface IFeatureGenerator { - CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace); + CodeNamespace GenerateUnitTestFixture(ReqnrollDocument document, string testClassName, string targetNamespace, out IEnumerable warnings); } } \ No newline at end of file diff --git a/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs b/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs new file mode 100644 index 000000000..3f7b36372 --- /dev/null +++ b/Reqnroll.Parser/CucumberMessageSupport/CucumberMessagesConverter.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.IO; +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.Parser.CucmberMessageSupport +{ + /// + /// Utility class that converts Reqnroll AST types in to CucumberMessages.Types types. + /// It uses two classes from the Gherking project: AstMessagesConverter and PickleCompiler + /// + /// Once the Gherkin project implementation directly emits CucumberMessages (eliminating the use of the Gherkin.CucumberMessages.Types namespace), this class can be removed + /// + public class CucumberMessagesConverter : ICucumberMessagesConverters + { + private IIdGenerator _idGenerator; + + public CucumberMessagesConverter(IIdGenerator idGenerator) + { + _idGenerator = idGenerator; + } + + // This method transforms an AST ReqnrollDocument into a CucumberMessages.Types.GherkinDocument + // Before doing so, it patches any missing Location elements in the AST. These might be missing because our ExternalData Plugin does not emit Location elements for the Example table rows it generates + // + public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument) + { + var NullLocationPatcher = new PatchMissingLocationElementsTransformation(); + var gherkinDocumentWithLocation = NullLocationPatcher.TransformDocument(gherkinDocument); + var converter = new AstMessagesConverter(_idGenerator); + var location = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)); + return converter.ConvertGherkinDocumentToEventArgs(gherkinDocumentWithLocation, location); + } + + public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument) + { + if (File.Exists(gherkinDocument.SourceFilePath)) + { + var sourceText = File.ReadAllText(gherkinDocument.SourceFilePath); + return new Source + { + Uri = Path.Combine(gherkinDocument.DocumentLocation.FeatureFolderPath, Path.GetFileName(gherkinDocument.SourceFilePath)), + Data = sourceText, + MediaType = "text/x.cucumber.gherkin+plain" + }; + } + else + { + return new Source + { + Uri = "Unknown", + Data = $"Source Document: {gherkinDocument.SourceFilePath} not found.", + MediaType = "text/x.cucumber.gherkin+plain" + }; + + } + } + + public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument) + { + var pickleCompiler = new Gherkin.CucumberMessages.Pickles.PickleCompiler(_idGenerator); + return pickleCompiler.Compile(gherkinDocument); + } + } +} diff --git a/Reqnroll.Parser/CucumberMessageSupport/GherkinDocumentVisitor.cs b/Reqnroll.Parser/CucumberMessageSupport/GherkinDocumentVisitor.cs new file mode 100644 index 000000000..77bff6f48 --- /dev/null +++ b/Reqnroll.Parser/CucumberMessageSupport/GherkinDocumentVisitor.cs @@ -0,0 +1,143 @@ +using System; +using Gherkin.Ast; + +namespace Reqnroll.Parser.CucmberMessageSupport +{ + abstract class GherkinDocumentVisitor + { + protected virtual void AcceptDocument(ReqnrollDocument document) + { + OnDocumentVisiting(document); + if (document.Feature != null) + { + AcceptFeature(document.Feature); + } + OnDocumentVisited(document); + } + + protected virtual void AcceptFeature(Feature feature) + { + OnFeatureVisiting(feature); + foreach (var featureChild in feature.Children) + { + if (featureChild is Rule rule) AcceptRule(rule); + else if (featureChild is Background background) AcceptBackground(background); + else if (featureChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + else if (featureChild is Scenario scenario) AcceptScenario(scenario); + } + OnFeatureVisited(feature); + } + + protected virtual void AcceptStep(Step step) + { + OnStepVisited(step); + } + + protected virtual void AcceptScenario(Scenario scenario) + { + OnScenarioVisiting(scenario); + foreach (var step in scenario.Steps) + { + AcceptStep(step); + } + OnScenarioVisited(scenario); + } + + protected virtual void AcceptScenarioOutline(ScenarioOutline scenarioOutline) + { + OnScenarioOutlineVisiting(scenarioOutline); + foreach (var step in scenarioOutline.Steps) + { + AcceptStep(step); + } + OnScenarioOutlineVisited(scenarioOutline); + } + + protected virtual void AcceptBackground(Background background) + { + OnBackgroundVisiting(background); + foreach (var step in background.Steps) + { + AcceptStep(step); + } + OnBackgroundVisited(background); + } + + protected virtual void AcceptRule(Rule rule) + { + OnRuleVisiting(rule); + foreach (var ruleChild in rule.Children) + { + if (ruleChild is Background background) AcceptBackground(background); + else if (ruleChild is ScenarioOutline scenarioOutline) AcceptScenarioOutline(scenarioOutline); + else if (ruleChild is Scenario scenario) AcceptScenario(scenario); + } + OnRuleVisited(rule); + } + + protected virtual void OnDocumentVisiting(ReqnrollDocument document) + { + //nop + } + + protected virtual void OnDocumentVisited(ReqnrollDocument document) + { + //nop + } + + protected virtual void OnFeatureVisiting(Feature feature) + { + //nop + } + + protected virtual void OnFeatureVisited(Feature feature) + { + //nop + } + + protected virtual void OnBackgroundVisiting(Background background) + { + //nop + } + + protected virtual void OnBackgroundVisited(Background background) + { + //nop + } + + protected virtual void OnRuleVisiting(Rule rule) + { + //nop + } + + protected virtual void OnRuleVisited(Rule rule) + { + //nop + } + + protected virtual void OnScenarioOutlineVisiting(ScenarioOutline scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioOutlineVisited(ScenarioOutline scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioVisiting(Scenario scenario) + { + //nop + } + + protected virtual void OnScenarioVisited(Scenario scenario) + { + //nop + } + + protected virtual void OnStepVisited(Step step) + { + //nop + } + } +} diff --git a/Reqnroll.Parser/CucumberMessageSupport/ICucumberMessagesConverters.cs b/Reqnroll.Parser/CucumberMessageSupport/ICucumberMessagesConverters.cs new file mode 100644 index 000000000..aaf58d0ca --- /dev/null +++ b/Reqnroll.Parser/CucumberMessageSupport/ICucumberMessagesConverters.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.Parser.CucmberMessageSupport +{ + public interface ICucumberMessagesConverters + { + public GherkinDocument ConvertToCucumberMessagesGherkinDocument(ReqnrollDocument gherkinDocument); + public Source ConvertToCucumberMessagesSource(ReqnrollDocument gherkinDocument); + public IEnumerable ConvertToCucumberMessagesPickles(GherkinDocument gherkinDocument); + } +} diff --git a/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs b/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs new file mode 100644 index 000000000..647b2f358 --- /dev/null +++ b/Reqnroll.Parser/CucumberMessageSupport/PatchMissingLocationElementsTransformation.cs @@ -0,0 +1,75 @@ +using Gherkin.Ast; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.Parser.CucmberMessageSupport +{ + /// + /// This class is used to patch missing location elements for features, scenarios and scenario outlines. + /// It is based upon the AST visitor implementation found in the External Data plugin. It may be worth finding a way to generalize the visitor base classes but this is sufficient for now. + /// + internal class PatchMissingLocationElementsTransformation : ScenarioTransformationVisitor + { + protected override void OnFeatureVisited(Feature feature) + { + var patchedFeatureLocation = PatchLocation(feature.Location); + var patchedFeature = new Feature( + feature.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), + patchedFeatureLocation, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + feature.Children.ToArray()); + base.OnFeatureVisited(patchedFeature); + + } + protected override Scenario GetTransformedScenario(Scenario scenario) + { + return new Scenario( + scenario.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), + PatchLocation(scenario.Location), + scenario.Keyword, + scenario.Name, + scenario.Description, + scenario.Steps.Select(s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), + scenario.Examples.ToArray()); + } + + protected override Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline) + { + if (scenarioOutline.Examples == null || !scenarioOutline.Examples.Any()) + return null; + + var exampleTables = scenarioOutline.Examples; + List transformedExamples = new List(); + + transformedExamples.AddRange(exampleTables.Select(e => PatchExamplesLocations(e))); + return new ScenarioOutline( + scenarioOutline.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), + PatchLocation(scenarioOutline.Location), + scenarioOutline.Keyword, + scenarioOutline.Name, + scenarioOutline.Description, + scenarioOutline.Steps.Select(s => new Step(PatchLocation(s.Location), s.Keyword, s.KeywordType, s.Text, s.Argument)).ToArray(), + transformedExamples.ToArray()); + } + + private Examples PatchExamplesLocations(Examples e) + { + var headerCells = e.TableHeader.Cells; + var tableHeader = new TableRow(PatchLocation(e.TableHeader.Location), headerCells.Select(hc => new TableCell(PatchLocation(hc.Location), hc.Value)).ToArray()); + var rows = e.TableBody.Select(r => new TableRow(PatchLocation(r.Location), r.Cells.Select(c => new TableCell(PatchLocation(c.Location), c.Value)).ToArray())).ToArray(); + return new Examples(e.Tags.Select(t => new Tag(PatchLocation(t.Location), t.Name)).ToArray(), PatchLocation(e.Location), e.Keyword, e.Name, e.Description, tableHeader, rows); + } + + private static Location PatchLocation(Location l) + { + return l ?? new Location(0, 0); + } + + + } +} diff --git a/Reqnroll.Parser/CucumberMessageSupport/ScenarioTransformationVisitor.cs b/Reqnroll.Parser/CucumberMessageSupport/ScenarioTransformationVisitor.cs new file mode 100644 index 000000000..85f9dd691 --- /dev/null +++ b/Reqnroll.Parser/CucumberMessageSupport/ScenarioTransformationVisitor.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gherkin.Ast; + +namespace Reqnroll.Parser.CucmberMessageSupport +{ + abstract class ScenarioTransformationVisitor : GherkinDocumentVisitor + { + protected ReqnrollDocument _sourceDocument; + private ReqnrollDocument _transformedDocument; + private ReqnrollFeature _transformedFeature; + private bool _hasTransformedScenarioInFeature = false; + private bool _hasTransformedScenarioInCurrentRule = false; + private readonly List _featureChildren = new(); + private readonly List _ruleChildren = new(); + private List _currentChildren; + + public ReqnrollDocument TransformDocument(ReqnrollDocument document) + { + Reset(); + AcceptDocument(document); + return _transformedDocument ?? document; + } + + private void Reset() + { + _sourceDocument = null; + _transformedDocument = null; + _transformedFeature = null; + _featureChildren.Clear(); + _ruleChildren.Clear(); + _hasTransformedScenarioInFeature = false; + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _featureChildren; + } + + protected abstract Scenario GetTransformedScenarioOutline(ScenarioOutline scenarioOutline); + protected abstract Scenario GetTransformedScenario(Scenario scenario); + + protected override void OnScenarioOutlineVisited(ScenarioOutline scenarioOutline) + { + var transformedScenarioOutline = GetTransformedScenarioOutline(scenarioOutline); + OnScenarioVisitedInternal(scenarioOutline, transformedScenarioOutline); + } + + protected override void OnScenarioVisited(Scenario scenario) + { + var transformedScenario = GetTransformedScenario(scenario); + OnScenarioVisitedInternal(scenario, transformedScenario); + } + + private void OnScenarioVisitedInternal(Scenario scenario, Scenario transformedScenario) + { + if (transformedScenario == null) + { + _currentChildren.Add(scenario); + return; + } + + _hasTransformedScenarioInFeature = true; + _hasTransformedScenarioInCurrentRule = true; + _currentChildren.Add(transformedScenario); + } + + protected override void OnBackgroundVisited(Background background) + { + _currentChildren.Add(background); + } + + protected override void OnRuleVisiting(Rule rule) + { + _ruleChildren.Clear(); + _hasTransformedScenarioInCurrentRule = false; + _currentChildren = _ruleChildren; + } + + protected override void OnRuleVisited(Rule rule) + { + _currentChildren = _featureChildren; + if (_hasTransformedScenarioInCurrentRule) + { + var transformedRule = new Rule( + rule.Tags?.ToArray() ?? Array.Empty(), + rule.Location, + rule.Keyword, + rule.Name, + rule.Description, + _ruleChildren.ToArray()); + _featureChildren.Add(transformedRule); + } + else + { + _featureChildren.Add(rule); + } + } + + protected override void OnFeatureVisited(Feature feature) + { + if (_hasTransformedScenarioInFeature) + _transformedFeature = new ReqnrollFeature( + feature.Tags?.ToArray() ?? Array.Empty(), + feature.Location, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + _featureChildren.ToArray()); + } + + protected override void OnDocumentVisiting(ReqnrollDocument document) + { + _sourceDocument = document; + } + + protected override void OnDocumentVisited(ReqnrollDocument document) + { + if (_transformedFeature != null) + _transformedDocument = new ReqnrollDocument(_transformedFeature, document.Comments.ToArray(), document.DocumentLocation); + } + } +} diff --git a/Reqnroll.Tools.MsBuild.Generation/FeatureFileCodeBehindGenerator.cs b/Reqnroll.Tools.MsBuild.Generation/FeatureFileCodeBehindGenerator.cs index 30ef6b72c..d64ee1c08 100644 --- a/Reqnroll.Tools.MsBuild.Generation/FeatureFileCodeBehindGenerator.cs +++ b/Reqnroll.Tools.MsBuild.Generation/FeatureFileCodeBehindGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.Build.Utilities; using Reqnroll.Utils; @@ -55,6 +56,14 @@ public IEnumerable GenerateFilesForProject( continue; } + if (generatorResult.Warnings != null) + { + foreach (var warning in generatorResult.Warnings) + { + Log.LogWarning(warning); + } + } + string targetFilePath = _filePathGenerator.GenerateFilePath( projectFolder, outputPath, diff --git a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs index fafbf4afa..5ea8ba55d 100644 --- a/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs +++ b/Reqnroll.Tools.MsBuild.Generation/GenerateFeatureFileCodeBehindTaskContainerBuilder.cs @@ -40,7 +40,7 @@ public IObjectContainer BuildRootContainer( objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); - + objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); objectContainer.RegisterTypeAs(); diff --git a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec index 0cbe70051..d97724d22 100644 --- a/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec +++ b/Reqnroll.Tools.MsBuild.Generation/Reqnroll.Tools.MsBuild.Generation.nuspec @@ -27,7 +27,7 @@ - + diff --git a/Reqnroll.Tools.MsBuild.Generation/TestFileGeneratorResult.cs b/Reqnroll.Tools.MsBuild.Generation/TestFileGeneratorResult.cs index 21db741c6..dec847463 100644 --- a/Reqnroll.Tools.MsBuild.Generation/TestFileGeneratorResult.cs +++ b/Reqnroll.Tools.MsBuild.Generation/TestFileGeneratorResult.cs @@ -19,6 +19,7 @@ public TestFileGeneratorResult(TestGeneratorResult generatorResult, string fileN Errors = generatorResult.Errors; IsUpToDate = generatorResult.IsUpToDate; GeneratedTestCode = generatorResult.GeneratedTestCode; + Warnings = generatorResult.Warnings; } /// @@ -38,6 +39,7 @@ public TestFileGeneratorResult(TestGeneratorResult generatorResult, string fileN public bool Success => Errors == null || !Errors.Any(); + public IEnumerable Warnings { get; } public string Filename { get; } } } \ No newline at end of file diff --git a/Reqnroll.sln b/Reqnroll.sln index ce536c9e5..272aa42c7 100644 --- a/Reqnroll.sln +++ b/Reqnroll.sln @@ -121,6 +121,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reqnroll.Assist.Dynamic.Spe EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assist.Dynamic", "Assist.Dynamic", "{D93DD6F6-7AA0-402C-845C-A7FDF722D0A2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberMessages.Tests", "Tests\CucumberMessages.CompatibilityTests\CucumberMessages.Tests.csproj", "{5072F73C-8CDD-4B44-B3F8-4212F65C3708}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -251,6 +253,10 @@ Global {4FF6D163-9E9B-4F5F-8742-B6D902EDC358}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FF6D163-9E9B-4F5F-8742-B6D902EDC358}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FF6D163-9E9B-4F5F-8742-B6D902EDC358}.Release|Any CPU.Build.0 = Release|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5072F73C-8CDD-4B44-B3F8-4212F65C3708}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -291,6 +297,7 @@ Global {1F189DA5-C5E9-4168-B65B-8BE22BB05912} = {D93DD6F6-7AA0-402C-845C-A7FDF722D0A2} {4FF6D163-9E9B-4F5F-8742-B6D902EDC358} = {D93DD6F6-7AA0-402C-845C-A7FDF722D0A2} {D93DD6F6-7AA0-402C-845C-A7FDF722D0A2} = {8BE0FE31-6A52-452E-BE71-B8C64A3ED402} + {5072F73C-8CDD-4B44-B3F8-4212F65C3708} = {A10B5CD6-38EC-4D7E-9D1C-2EBA8017E437} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A4D0636F-0160-4FA5-81A3-9784C7E3B3A4} diff --git a/Reqnroll/Analytics/AnalyticsEventProvider.cs b/Reqnroll/Analytics/AnalyticsEventProvider.cs index 90a446c3f..6c1f8dc07 100644 --- a/Reqnroll/Analytics/AnalyticsEventProvider.cs +++ b/Reqnroll/Analytics/AnalyticsEventProvider.cs @@ -13,13 +13,13 @@ namespace Reqnroll.Analytics public class AnalyticsEventProvider : IAnalyticsEventProvider { private readonly IUserUniqueIdStore _userUniqueIdStore; - private readonly IEnvironmentWrapper _environmentWrapper; + private readonly IEnvironmentInfoProvider _environmentInfoProvider; private readonly string _unitTestProvider; - public AnalyticsEventProvider(IUserUniqueIdStore userUniqueIdStore, UnitTestProviderConfiguration unitTestProviderConfiguration, IEnvironmentWrapper environmentWrapper) + public AnalyticsEventProvider(IUserUniqueIdStore userUniqueIdStore, UnitTestProviderConfiguration unitTestProviderConfiguration, IEnvironmentInfoProvider environmentInfoProvider) { _userUniqueIdStore = userUniqueIdStore; - _environmentWrapper = environmentWrapper; + _environmentInfoProvider = environmentInfoProvider; _unitTestProvider = unitTestProviderConfiguration.UnitTestProvider; } @@ -27,11 +27,11 @@ public ReqnrollProjectCompilingEvent CreateProjectCompilingEvent(string msbuildV { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = GetReqnrollVersion(); - string buildServerName = GetBuildServerName(); - bool isDockerContainer = IsRunningInDockerContainer(); + string reqnrollVersion = _environmentInfoProvider.GetReqnrollVersion(); + string buildServerName = _environmentInfoProvider.GetBuildServerName(); + bool isDockerContainer = _environmentInfoProvider.IsRunningInDockerContainer(); string hashedAssemblyName = ToSha256(assemblyName); - string platform = GetOSPlatform(); + string platform = _environmentInfoProvider.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var compiledEvent = new ReqnrollProjectCompilingEvent( @@ -56,13 +56,13 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly { string userId = _userUniqueIdStore.GetUserId(); string unitTestProvider = _unitTestProvider; - string reqnrollVersion = GetReqnrollVersion(); - string targetFramework = GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; - bool isDockerContainer = IsRunningInDockerContainer(); - string buildServerName = GetBuildServerName(); + string reqnrollVersion = _environmentInfoProvider.GetReqnrollVersion(); + string targetFramework = _environmentInfoProvider.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + bool isDockerContainer = _environmentInfoProvider.IsRunningInDockerContainer(); + string buildServerName = _environmentInfoProvider.GetBuildServerName(); string hashedAssemblyName = ToSha256(testAssemblyName); - string platform = GetOSPlatform(); + string platform = _environmentInfoProvider.GetOSPlatform(); string platformDescription = RuntimeInformation.OSDescription; var runningEvent = new ReqnrollProjectRunningEvent( @@ -80,70 +80,6 @@ public ReqnrollProjectRunningEvent CreateProjectRunningEvent(string testAssembly return runningEvent; } - private string GetOSPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "Windows"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "Linux"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "OSX"; - } - - throw new InvalidOperationException("Platform cannot be identified"); - } - - private readonly Dictionary buildServerTypes - = new Dictionary { - { "TF_BUILD","Azure Pipelines"}, - { "TEAMCITY_VERSION","TeamCity"}, - { "JENKINS_HOME","Jenkins"}, - { "GITHUB_ACTIONS","GitHub Actions"}, - { "GITLAB_CI","GitLab CI/CD"}, - { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, - { "TRAVIS","Travis CI"}, - { "APPVEYOR","AppVeyor"}, - { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, - { "bamboo_agentId", "Atlassian Bamboo" }, - { "CIRCLECI", "CircleCI" }, - { "GO_PIPELINE_NAME", "GoCD" }, - { "BUDDY", "Buddy" }, - { "NEVERCODE", "Nevercode" }, - { "SEMAPHORE", "SEMAPHORE" }, - { "BROWSERSTACK_USERNAME", "BrowserStack" }, - { "CF_BUILD_ID", "Codefresh" }, - { "TentacleVersion", "Octopus Deploy" }, - - { "CI_NAME", "CodeShip" } - }; - - private string GetBuildServerName() - { - foreach (var buildServerType in buildServerTypes) - { - var envVariable = _environmentWrapper.GetEnvironmentVariable(buildServerType.Key); - if (envVariable is ISuccess) - return buildServerType.Value; - } - return null; - } - - private bool IsRunningInDockerContainer() - { - return _environmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; - } - - private string GetReqnrollVersion() - { - return VersionInfo.AssemblyInformationalVersion; - } private string ToSha256(string inputString) { @@ -163,17 +99,5 @@ private string ToSha256(string inputString) return stringBuilder.ToString(); } - private string GetNetCoreVersion() - { - var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; - var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); - int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); - if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) - { - return assemblyPath[netCoreAppIndex + 1]; - } - - return null; - } } } diff --git a/Reqnroll/Bindings/BindingMatch.cs b/Reqnroll/Bindings/BindingMatch.cs index a0c2958c5..e387eefd9 100644 --- a/Reqnroll/Bindings/BindingMatch.cs +++ b/Reqnroll/Bindings/BindingMatch.cs @@ -1,3 +1,6 @@ +using Reqnroll.Infrastructure; +using System.Linq; + namespace Reqnroll.Bindings { public class BindingMatch @@ -13,10 +16,10 @@ public class BindingMatch public int ScopeMatches { get; private set; } public bool IsScoped { get { return ScopeMatches > 0; } } - public object[] Arguments { get; private set; } + public MatchArgument[] Arguments { get; private set; } public StepContext StepContext { get; private set; } - public BindingMatch(IStepDefinitionBinding stepBinding, int scopeMatches, object[] arguments, StepContext stepContext) + public BindingMatch(IStepDefinitionBinding stepBinding, int scopeMatches, MatchArgument[] arguments, StepContext stepContext) { StepBinding = stepBinding; ScopeMatches = scopeMatches; diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigFile_ConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/ConfigFile_ConfigurationSource.cs new file mode 100644 index 000000000..b53dca3ed --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ConfigFile_ConfigurationSource.cs @@ -0,0 +1,53 @@ +using Reqnroll.CommonModels; +using Reqnroll.Configuration; +using Reqnroll.EnvironmentAccess; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +namespace Reqnroll.CucumberMessages.Configuration +{ + public class ConfigFile_ConfigurationSource : IConfigurationSource + { + private IReqnrollJsonLocator _configFileLocator; + + public ConfigFile_ConfigurationSource(IReqnrollJsonLocator configurationFileLocator) + { + _configFileLocator = configurationFileLocator; + } + + public ConfigurationDTO GetConfiguration() + { + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + var fileName = _configFileLocator.GetReqnrollJsonFilePath(); + + ConfigurationDTO configurationDTO = new(); + CucumberMessagesConfiguration section = null; + + if (File.Exists(fileName)) + { + var jsonFileContent = File.ReadAllText(fileName); + using JsonDocument reqnrollConfigDoc = JsonDocument.Parse(jsonFileContent, new JsonDocumentOptions() + { + CommentHandling = JsonCommentHandling.Skip + }); + if (reqnrollConfigDoc.RootElement.TryGetProperty("cucumberMessagesConfiguration", out JsonElement CMC)) + { + section = JsonSerializer.Deserialize(CMC.GetRawText(), jsonOptions); + } + } + if (section != null) + { + configurationDTO.Enabled = section.Enabled; + configurationDTO.OutputFilePath = section.OutputFilePath; + return configurationDTO; + } + return null; + } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs new file mode 100644 index 000000000..5a5fe6a67 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ConfigurationDTO.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + /// + /// These classes holds configuration information from a configuration source. + /// These are JSON serialized and correspond to the json schema in CucumberMessages-config-schema.json + /// + public class ConfigurationDTO : ICucumberMessagesConfiguration + { + + public bool Enabled { get; set; } + public string OutputFilePath { get; set; } + public ConfigurationDTO() : this(false) { } + public ConfigurationDTO(bool enabled) + { + Enabled = enabled; + } + } +} + diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs new file mode 100644 index 000000000..c11a82b90 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfiguration.cs @@ -0,0 +1,122 @@ +using Reqnroll.BoDi; +using Reqnroll.CommonModels; +using Reqnroll.Configuration; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace Reqnroll.CucumberMessages.Configuration +{ + /// + /// This class is responsible for determining the configuration of the Cucumber Messages subsystem. + /// It is wired into the object container as a singleton and is a dependency of the PubSub classes. + /// + /// When any consumer of this class asks for one of the properties of ICucumberConfiguration, + /// the class will resolve the configuration (only once). + /// + /// A default configuration is provided (by DefaultConfigurationSource). + /// It is supplemented by one or more profiles from the configuration file. (ConfigFile_ConfigurationSource) + /// Then Environmment Variable Overrides are applied. + /// + public class CucumberConfiguration : ICucumberMessagesConfiguration + { + public static ICucumberMessagesConfiguration Current { get; private set; } + public bool Enabled => _enablementOverrideFlag && _resolvedConfiguration.Value.Enabled; + public string OutputFilePath => _resolvedConfiguration.Value.OutputFilePath; + + private readonly IObjectContainer _objectContainer; + private Lazy _traceListenerLazy; + private IEnvironmentWrapper _environmentWrapper; + private IReqnrollJsonLocator _reqnrollJsonLocator; + private Lazy _resolvedConfiguration; + private bool _enablementOverrideFlag = true; + + public CucumberConfiguration(IObjectContainer objectContainer, IEnvironmentWrapper environmentWrapper, IReqnrollJsonLocator configurationFileLocator) + { + _objectContainer = objectContainer; + _traceListenerLazy = new Lazy(() => _objectContainer.Resolve()); + _environmentWrapper = environmentWrapper; + _reqnrollJsonLocator = configurationFileLocator; + _resolvedConfiguration = new Lazy(ResolveConfiguration); + Current = this; + } + + #region Override API + public void SetEnabled(bool value) + { + _enablementOverrideFlag = value; + } + #endregion + + + private ResolvedConfiguration ResolveConfiguration() + { + var config = ApplyHierarchicalConfiguration(); + var resolved = ApplyEnvironmentOverrides(config); + + // a final sanity check, the filename cannot be empty + if (string.IsNullOrEmpty(resolved.OutputFilePath)) + { + resolved.OutputFilePath = "./reqnroll_report.ndjson"; + } + EnsureOutputDirectory(resolved); + return resolved; + } + private ConfigurationDTO ApplyHierarchicalConfiguration() + { + var defaultConfigurationProvider = new DefaultConfigurationSource(_environmentWrapper); + var fileBasedConfigurationProvider = new ConfigFile_ConfigurationSource(_reqnrollJsonLocator); + + ConfigurationDTO defaultConfig = defaultConfigurationProvider.GetConfiguration(); + ConfigurationDTO fileBasedConfig = fileBasedConfigurationProvider.GetConfiguration(); + defaultConfig = MergeConfigs(defaultConfig, fileBasedConfig); + return defaultConfig; + } + + private ResolvedConfiguration ApplyEnvironmentOverrides(ConfigurationDTO config) + { + var filePathValue = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILEPATH_ENVIRONMENT_VARIABLE); + + var result = new ResolvedConfiguration() + { + Enabled = config.Enabled, + OutputFilePath = config.OutputFilePath + }; + + if (filePathValue is Success) + result.OutputFilePath = ((Success)filePathValue).Result; + + + var enabledResult = _environmentWrapper.GetEnvironmentVariable(CucumberConfigurationConstants.REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE); + result.Enabled = enabledResult is Success ? Convert.ToBoolean(((Success)enabledResult).Result) : result.Enabled; + + return result; + } + + private ConfigurationDTO MergeConfigs(ConfigurationDTO rootConfig, ConfigurationDTO overridingConfig) + { + if (overridingConfig != null) + { + rootConfig.Enabled = overridingConfig.Enabled; + rootConfig.OutputFilePath = overridingConfig.OutputFilePath ?? rootConfig.OutputFilePath; + } + + return rootConfig; + } + + private void EnsureOutputDirectory(ResolvedConfiguration config) + { + + if (!Directory.Exists(Path.GetDirectoryName(config.OutputFilePath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(config.OutputFilePath)); + } + } + + } +} + diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs new file mode 100644 index 000000000..a81febab9 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberConfigurationConstants.cs @@ -0,0 +1,8 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public static class CucumberConfigurationConstants + { + public const string REQNROLL_CUCUMBER_MESSAGES_OUTPUT_FILEPATH_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__OUTPUT_FILEPATH"; + public const string REQNROLL_CUCUMBER_MESSAGES_ENABLE_ENVIRONMENT_VARIABLE = "REQNROLL__CUCUMBER_MESSAGES__ENABLED"; + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json new file mode 100644 index 000000000..0aba57661 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberMessages-config-schema.json @@ -0,0 +1,11 @@ +{ + "description": "This class holds configuration information from a configuration source for Reqnroll Cucumber Message Generation.\n", + "properties": { + "Enabled": { + "type": "boolean" + }, + "OutputFilePath": { + "type": "string" + } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/CucumberMessagesConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/CucumberMessagesConfiguration.cs new file mode 100644 index 000000000..1c8070618 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/CucumberMessagesConfiguration.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + public class CucumberMessagesConfiguration + { + public bool Enabled { get; set; } + public string OutputFilePath { get; set; } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs new file mode 100644 index 000000000..c71e25895 --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/DefaultConfigurationSource.cs @@ -0,0 +1,31 @@ +using Reqnroll.EnvironmentAccess; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + + /// + /// Defaults are: + /// - FileOutputEnabled = false + /// + internal class DefaultConfigurationSource : IConfigurationSource + { + private readonly IEnvironmentWrapper _environmentWrapper; + + public DefaultConfigurationSource(IEnvironmentWrapper environmentWrapper) + { + _environmentWrapper = environmentWrapper; + } + public ConfigurationDTO GetConfiguration() + { + var res = new ConfigurationDTO(); + string defaultOutputFileName = "./reqnroll_report.ndjson"; + + res.Enabled = false; + res.OutputFilePath = defaultOutputFileName; + return res; + } + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs b/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs new file mode 100644 index 000000000..2993802cb --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/IConfigurationSource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.Configuration +{ + internal interface IConfigurationSource + { + ConfigurationDTO GetConfiguration(); + } +} diff --git a/Reqnroll/CucumberMessages/Configuration/ICucumberMessagesConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ICucumberMessagesConfiguration.cs new file mode 100644 index 000000000..2bc17ec3e --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ICucumberMessagesConfiguration.cs @@ -0,0 +1,8 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public interface ICucumberMessagesConfiguration + { + bool Enabled { get; } + string OutputFilePath { get; } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs new file mode 100644 index 000000000..246bd506c --- /dev/null +++ b/Reqnroll/CucumberMessages/Configuration/ResolvedConfiguration.cs @@ -0,0 +1,9 @@ +namespace Reqnroll.CucumberMessages.Configuration +{ + public class ResolvedConfiguration + { + public bool Enabled { get; set; } + public string OutputFilePath { get; set; } + } +} + diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/AttachmentAddedEventWrapper.cs b/Reqnroll/CucumberMessages/ExecutionTracking/AttachmentAddedEventWrapper.cs new file mode 100644 index 000000000..555d28552 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/AttachmentAddedEventWrapper.cs @@ -0,0 +1,29 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Events; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + // This class acts as an addendum to AttachmentAddedEvent and provides the ability to convey which Pickle, TestCase, and TestStep were responsible for the Attachment being added. + internal class AttachmentAddedEventWrapper : IGenerateMessage + { + internal AttachmentAddedEventWrapper(AttachmentAddedEvent attachmentAddedEvent, string testRunStartedId, string testCaseStartedId, string testCaseStepId) + { + AttachmentAddedEvent = attachmentAddedEvent; + TestRunStartedId = testRunStartedId; + TestCaseStartedId = testCaseStartedId; + TestCaseStepId = testCaseStepId; + } + + internal AttachmentAddedEvent AttachmentAddedEvent { get; } + internal string TestCaseStartedId { get; } + internal string TestCaseStepId { get; } + internal string TestRunStartedId { get; } + + public IEnumerable GenerateFrom(ExecutionEvent executionEvent) + { + return [Envelope.Create(CucumberMessageFactory.ToAttachment(this))]; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/AttachmentTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/AttachmentTracker.cs new file mode 100644 index 000000000..e86de97f8 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/AttachmentTracker.cs @@ -0,0 +1,41 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + internal class AttachmentTracker + { + + private List _attacheds = new(); + + internal void RecordAttachment(AttachmentAddedEvent attachmentEvent) + { + _attacheds.Add(attachmentEvent); + } + + internal void RecordOutput(OutputAddedEvent outputAddedEvent) + { + _attacheds.Add(outputAddedEvent); + } + + internal ExecutionEvent FindMatchingAttachment(Attachment a) + { + if (a.FileName == null) // ... meaning this is an Output attachment, not a file attachment + { + var o = _attacheds.OfType().Where(e => e.Text == a.Body).First(); + _attacheds.Remove(o); + return o; + } + else + { + var r = _attacheds.OfType().Where(e => a.FileName == Path.GetFileName(e.FilePath)).First(); + _attacheds.Remove(r); + return r; + } + } + } +} diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs new file mode 100644 index 000000000..70ccea288 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/FeatureTracker.cs @@ -0,0 +1,254 @@ +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.CucumberMessages.RuntimeSupport; +using Reqnroll.Events; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// FeatureTracker is responsible for tracking the execution of a Feature. + /// There will be one instance of this class per gherkin Feature. + /// + public class FeatureTracker + { + // Static Messages are those generated during code generation (Source, GherkinDocument & Pickles) + // and the StepTransformations, StepDefinitions and Hook messages which are global to the entire Solution. + internal IEnumerable StaticMessages => _staticMessages.Value; + private Lazy> _staticMessages; + + // ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. + public IIdGenerator IDGenerator { get; set; } + public string TestRunStartedId { get; } + + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + // This dictionary is shared across all Features (via the Publisher) + // The same is true of the StepTransformations and StepDefinitionBindings used for Undefined Parameter Types + internal ConcurrentDictionary StepDefinitionsByPattern = new(); + + // This dictionary maps from (string) PickkleID to the TestCase tracker + private ConcurrentDictionary testCaseTrackersById = new(); + + public string FeatureName { get; } + public bool Enabled { get; } + + // This dictionary maps from (string) PickleIDIndex to (string) PickleID + private Dictionary PickleIds { get; } = new(); + private PickleJar PickleJar { get; set; } + + public bool FeatureExecutionSuccess { get; private set; } + + // This constructor is used by the Publisher when it sees a Feature (by name) for the first time + public FeatureTracker(FeatureStartedEvent featureStartedEvent, string testRunStartedId, IIdGenerator idGenerator, ConcurrentDictionary stepDefinitionPatterns) + { + TestRunStartedId = testRunStartedId; + StepDefinitionsByPattern = stepDefinitionPatterns; + IDGenerator = idGenerator; + FeatureName = featureStartedEvent.FeatureContext.FeatureInfo.Title; + var featureHasCucumberMessages = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages != null; + Enabled = featureHasCucumberMessages && featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles != null ? true : false; + ProcessEvent(featureStartedEvent); + } + + // At the completion of Feature execution, this is called to generate all non-static Messages + // Iterating through all Scenario trackers, generating all messages. + public IEnumerable RuntimeGeneratedMessages + { + get + { + return testCaseTrackersById.Values.OrderBy(tc => tc.TestCaseStartedTimeStamp).SelectMany(scenario => scenario.RuntimeGeneratedMessages); + } + } + + internal void ProcessEvent(FeatureStartedEvent featureStartedEvent) + { + if (!Enabled) return; + // This has side-effects needed for proper execution of subsequent events; eg, the Ids of the static messages get generated and then subsequent events generate Ids that follow + _staticMessages = new Lazy>(() => GenerateStaticMessages(featureStartedEvent)); + } + + // This method is used to generate the static messages (Source, GherkinDocument & Pickles) and the StepTransformations, StepDefinitions and Hook messages which are global to the entire Solution + // This should be called only once per Feature. As such, it relies on the use of a lock section within the Publisher to ensure that only a single instance of the FeatureTracker is created per Feature + private IEnumerable GenerateStaticMessages(FeatureStartedEvent featureStartedEvent) + { + var gd = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.GherkinDocument(); + + var pickles = featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Pickles().ToList(); + + for (int i = 0; i < pickles.Count; i++) + { + PickleIds.Add(i.ToString(), pickles[i].Id); + } + + PickleJar = new PickleJar(pickles); + + yield return Envelope.Create(featureStartedEvent.FeatureContext.FeatureInfo.FeatureCucumberMessages.Source()); + + yield return Envelope.Create(gd); + foreach (var pickle in pickles) + { + yield return Envelope.Create(pickle); + } + + } + + // When the FeatureFinished event fires, we calculate the Feature-level Execution Status + // If Scenarios are running in parallel, this event will fire multiple times (once per each instance of the test class). + // Running this method multiple times is harmless. The FeatureExecutionSuccess property is only consumed upon the TestRunComplete event (ie, only once). + public void ProcessEvent(FeatureFinishedEvent featureFinishedEvent) + { + var testCases = testCaseTrackersById.Values.ToList(); + + // Calculate the Feature-level Execution Status + FeatureExecutionSuccess = testCases.All(tc => tc.Finished) switch + { + true => testCases.All(tc => tc.ScenarioExecutionStatus == ScenarioExecutionStatus.OK), + _ => false + }; + } + + public void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + var pickleIndex = scenarioStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + + // The following validations and ANE throws are in place to help identify threading bugs when Scenarios are run in parallel. + // TODO: consider removing these or placing them within #IFDEBUG + + if (String.IsNullOrEmpty(pickleIndex)) + { + // Should never happen + if (scenarioStartedEvent.ScenarioContext == null) + throw new ArgumentNullException("ScenarioContext", "ScenarioContext is not properly initialized for Cucumber Messages."); + if (scenarioStartedEvent.ScenarioContext.ScenarioInfo == null) + throw new ArgumentNullException("ScenarioInfo", "ScenarioContext/ScenarioInfo is not properly initialized for Cucumber Messages."); + throw new ArgumentNullException("PickleIdIndex", "ScenarioContext/ScenarioInfo does not have a properly initialized PickleIdIndex."); + } + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (PickleJar == null) + throw new ArgumentNullException("PickleJar", "PickleJar is not properly initialized for Cucumber Messages."); + // Fetch the PickleStepSequence for this Pickle and give to the ScenarioInfo + var pickleStepSequence = PickleJar.PickleStepSequenceFor(pickleIndex); + scenarioStartedEvent.ScenarioContext.ScenarioInfo.PickleStepSequence = pickleStepSequence; ; + + TestCaseTracker tct; + if (testCaseTrackersById.TryGetValue(pickleId, out tct)) + { + // This represents a re-execution of the TestCase. + tct.ProcessEvent((ExecutionEvent)scenarioStartedEvent); + } + else // This is the first time this TestCase (aka, pickle) is getting executed. New up a TestCaseTracker for it. + { + tct = new TestCaseTracker(this, pickleId); + tct.ProcessEvent((ExecutionEvent)scenarioStartedEvent); + testCaseTrackersById.TryAdd(pickleId, tct); + } + } + } + + public void ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + var pickleIndex = scenarioFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent((ExecutionEvent)scenarioFinishedEvent); + } + } + } + + public void ProcessEvent(StepStartedEvent stepStartedEvent) + { + var pickleIndex = stepStartedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent((ExecutionEvent)stepStartedEvent); + } + } + } + + public void ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var pickleIndex = stepFinishedEvent.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + + if (String.IsNullOrEmpty(pickleIndex)) return; + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent((ExecutionEvent)stepFinishedEvent); + } + } + } + + public void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + var pickleIndex = hookBindingStartedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + tccmt.ProcessEvent((ExecutionEvent)hookBindingStartedEvent); + } + } + + public void ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + var pickleIndex = hookBindingFinishedEvent.ContextManager?.ScenarioContext?.ScenarioInfo?.PickleIdIndex; + + if (String.IsNullOrEmpty(pickleIndex)) return; + + if (PickleIds.TryGetValue(pickleIndex, out var pickleId)) + { + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + tccmt.ProcessEvent((ExecutionEvent)hookBindingFinishedEvent); + } + } + + public void ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var pickleId = attachmentAddedEvent.FeatureInfo?.CucumberMessages_PickleId; + + if (String.IsNullOrEmpty(pickleId)) return; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent((ExecutionEvent)attachmentAddedEvent); + } + } + + public void ProcessEvent(OutputAddedEvent outputAddedEvent) + { + var pickleId = outputAddedEvent.FeatureInfo?.CucumberMessages_PickleId; + + if (String.IsNullOrEmpty(pickleId)) return; + + if (testCaseTrackersById.TryGetValue(pickleId, out var tccmt)) + { + tccmt.ProcessEvent((ExecutionEvent)outputAddedEvent); + } + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepDefinition.cs b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepDefinition.cs new file mode 100644 index 000000000..2eb3b774c --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepDefinition.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + internal class HookStepDefinition : TestStepDefinition + { + internal string HookId { get; } + + internal HookStepDefinition(string testStepDefinitionId, string hookId, TestCaseDefinition parentTestCaseDefinition) : base(testStepDefinitionId, hookId, parentTestCaseDefinition) + { + HookId = hookId; + } + } +} diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs new file mode 100644 index 000000000..67e42bc49 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/HookStepTracker.cs @@ -0,0 +1,60 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Events; +using System.Collections.Generic; +using System.Linq; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// This class is used to track execution of Hook Steps + /// + /// + internal class HookStepTracker : StepExecutionTrackerBase, IGenerateMessage + { + internal string HookBindingSignature { get; private set; } + internal HookBindingFinishedEvent HookBindingFinishedEvent { get; private set; } + internal HookStepTracker(TestCaseTracker tracker, TestCaseExecutionRecord testCaseExecutionRecord) : base(tracker, testCaseExecutionRecord) + { + } + + internal void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + StepStarted = hookBindingStartedEvent.Timestamp; + if (ParentTestCase.AttemptCount == 0) + { + HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingStartedEvent.HookBinding); + var hookId = ParentTestCase.StepDefinitionsByPattern[HookBindingSignature]; + var testStepId = ParentTestCase.IDGenerator.GetNewId(); + Definition = new HookStepDefinition(testStepId, hookId, ParentTestCase.TestCaseDefinition); + ParentTestCase.TestCaseDefinition.StepDefinitions.Add(Definition); + } + else + { + HookBindingSignature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingStartedEvent.HookBinding); + var hookId = ParentTestCase.StepDefinitionsByPattern[HookBindingSignature]; + Definition = ParentTestCase.TestCaseDefinition.StepDefinitions.OfType().Where(hd => hd.HookId == hookId).First(); + } + } + + internal void ProcessEvent(HookBindingFinishedEvent hookFinishedEvent) + { + StepFinished = hookFinishedEvent.Timestamp; + HookBindingFinishedEvent = hookFinishedEvent; + Exception = hookFinishedEvent.HookException; + Status = Exception == null ? ScenarioExecutionStatus.OK : ScenarioExecutionStatus.TestError; + } + + public IEnumerable GenerateFrom(ExecutionEvent executionEvent) + { + return executionEvent switch + { + HookBindingStartedEvent started => [Envelope.Create(CucumberMessageFactory.ToTestStepStarted(this))], + HookBindingFinishedEvent finished => [Envelope.Create(CucumberMessageFactory.ToTestStepFinished(this))], + _ => Enumerable.Empty() + }; + } + } + + +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/IGenerateMessage.cs b/Reqnroll/CucumberMessages/ExecutionTracking/IGenerateMessage.cs new file mode 100644 index 000000000..263916c99 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/IGenerateMessage.cs @@ -0,0 +1,16 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// This interface signifies that the implementer can generate message(s) based upon an ExecutionEvent + /// + public interface IGenerateMessage + { + public IEnumerable GenerateFrom(ExecutionEvent executionEvent); + } +} diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/OutputAddedEventWrapper.cs b/Reqnroll/CucumberMessages/ExecutionTracking/OutputAddedEventWrapper.cs new file mode 100644 index 000000000..7e0ebe7c6 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/OutputAddedEventWrapper.cs @@ -0,0 +1,28 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Events; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + internal class OutputAddedEventWrapper : IGenerateMessage + { + internal OutputAddedEvent OutputAddedEvent { get; } + internal string TestRunStartedId { get; } + internal string TestCaseStepId { get; } + internal string TestCaseStartedId { get; } + + internal OutputAddedEventWrapper(OutputAddedEvent outputAddedEvent, string testRunStartedId, string testCaseStartedId, string testCaseStepId) + { + OutputAddedEvent = outputAddedEvent; + TestRunStartedId = testRunStartedId; + TestCaseStartedId = testCaseStartedId; + TestCaseStepId = testCaseStepId; + } + + public IEnumerable GenerateFrom(ExecutionEvent executionEvent) + { + return [Envelope.Create(CucumberMessageFactory.ToAttachment(this))]; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs new file mode 100644 index 000000000..e066dc2bc --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/StepExecutionTrackerBase.cs @@ -0,0 +1,30 @@ +using System; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// Base class for tracking execution of steps (StepDefinitionBinding Methods and Hooks) + /// + internal class StepExecutionTrackerBase + { + internal string TestCaseStartedID => ParentExecutionRecord.TestCaseStartedId; + internal ScenarioExecutionStatus Status { get; set; } + + internal DateTime StepStarted { get; set; } + internal DateTime StepFinished { get; set; } + internal TimeSpan Duration { get => StepFinished - StepStarted; } + internal Exception Exception { get; set; } + + internal TestStepDefinition Definition { get; set; } + + internal TestCaseTracker ParentTestCase { get; } + + internal TestCaseExecutionRecord ParentExecutionRecord { get; } + + internal StepExecutionTrackerBase(TestCaseTracker parentScenario, TestCaseExecutionRecord parentExecutionRecord) + { + ParentTestCase = parentScenario; + ParentExecutionRecord = parentExecutionRecord; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseDefinition.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseDefinition.cs new file mode 100644 index 000000000..480d8e363 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseDefinition.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// Data class that holds information about an executed TestCase that is discovered the first time the Scenario/TestCase is executed + /// + internal class TestCaseDefinition + { + internal string TestCaseId { get; } + internal string PickleId { get; } + internal TestCaseTracker Tracker { get; } + internal List StepDefinitions { get; } + + internal TestCaseDefinition(string testCaseID, string pickleId, TestCaseTracker owner) + { + TestCaseId = testCaseID; + PickleId = pickleId; + Tracker = owner; + StepDefinitions = new(); + } + + internal string FindStepDefIDByStepPattern(string canonicalizedStepPattern) + { + return Tracker.StepDefinitionsByPattern[canonicalizedStepPattern]; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseExecutionRecord.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseExecutionRecord.cs new file mode 100644 index 000000000..b4b6e42ae --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseExecutionRecord.cs @@ -0,0 +1,101 @@ +using Reqnroll.Events; +using System; +using System.Linq; +using System.Collections.Generic; +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// Data class that holds information about a single execution of a TestCase. + /// There will be mulitple of these for a given TestCase if it is Retried. + /// + internal class TestCaseExecutionRecord : IGenerateMessage + { + internal int AttemptId { get; } + internal bool WillBeRetried { get; set; } + + // The ID of this particular execution of this Test Case + internal string TestCaseStartedId { get; } + + internal DateTime TestCaseStartedTimeStamp { get; private set; } + internal DateTime TestCaseFinishedTimeStamp { get; private set; } + internal ScenarioExecutionStatus ScenarioExecutionStatus { get; private set; } + internal TestCaseTracker TestCaseTracker { get; } + + internal List StepExecutionTrackers { get; } + internal StepExecutionTrackerBase CurrentStep { get { return StepExecutionTrackers.Last(); } } + + + + internal TestCaseExecutionRecord(int attemptId, string testCaseStartedId, TestCaseTracker testCaseTracker) + { + AttemptId = attemptId; + TestCaseStartedId = testCaseStartedId; + WillBeRetried = false; + StepExecutionTrackers = new(); + TestCaseTracker = testCaseTracker; + } + + internal IEnumerable RuntimeMessages + { + get + { + while (_events.Count > 0) + { + var (generator, execEvent) = _events.Dequeue(); + foreach (var e in generator.GenerateFrom(execEvent)) + { + yield return e; + } + } + } + } + + internal void StoreMessageGenerator(IGenerateMessage generator, ExecutionEvent executionEvent) + { + _events.Enqueue((generator, executionEvent)); + } + + // This queue holds ExecutionEvents that will be processed in stage 2 + private Queue<(IGenerateMessage, ExecutionEvent)> _events = new(); + + + internal void RecordStart(ScenarioStartedEvent e) + { + TestCaseStartedTimeStamp = e.Timestamp; + StoreMessageGenerator(this, e); + } + + internal void RecordFinish(ScenarioFinishedEvent e) + { + TestCaseFinishedTimeStamp = e.Timestamp; + ScenarioExecutionStatus = e.ScenarioContext.ScenarioExecutionStatus; + StoreMessageGenerator(this, e); + } + + public IEnumerable GenerateFrom(ExecutionEvent executionEvent) + { + switch (executionEvent) + { + case ScenarioStartedEvent scenarioStartedEvent: + // On the first execution of this TestCase we emit a TestCase Message. + // On subsequent retries we do not. + if (AttemptId == 0) + { + var testCase = CucumberMessageFactory.ToTestCase(TestCaseTracker.TestCaseDefinition); + yield return Envelope.Create(testCase); + } + var testCaseStarted = CucumberMessageFactory.ToTestCaseStarted(this, TestCaseTracker.TestCaseId); + yield return Envelope.Create(testCaseStarted); + break; + case ScenarioFinishedEvent scenarioFinishedEvent: + yield return Envelope.Create(CucumberMessageFactory.ToTestCaseFinished(this)); + break; + default: + break; + } + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs new file mode 100644 index 000000000..0c3cd50ab --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestCaseTracker.cs @@ -0,0 +1,214 @@ +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.CucumberMessages.RuntimeSupport; +using Reqnroll.Events; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + + /// + /// This class is used to track the execution of Test Cases + /// There will be one instance of this class per gherkin Pickle/TestCase. + /// It will track info from both Feature-level and Scenario-level Execution Events for a single Test Case + /// Individual executions will be recorded as a TestExecutionRecord. + /// + internal class TestCaseTracker + { + internal TestCaseTracker(FeatureTracker featureTracker, string pickleId) + { + TestRunStartedId = featureTracker.TestRunStartedId; + PickleId = pickleId; + FeatureName = featureTracker.FeatureName; + Enabled = featureTracker.Enabled; + IDGenerator = featureTracker.IDGenerator; + StepDefinitionsByPattern = featureTracker.StepDefinitionsByPattern; + AttemptCount = -1; + TestCaseStartedTimeStamp = DateTime.Now; + } + + // Feature FeatureName and Pickle ID make up a unique identifier for tracking execution of Test Cases + internal string FeatureName { get; } + internal string TestRunStartedId { get; } + internal string PickleId { get; } = string.Empty; + internal string TestCaseId { get; private set; } + internal int AttemptCount { get; private set; } + public object TestCaseStartedTimeStamp { get; } + internal bool Enabled { get; } //This will be false if the feature could not be pickled + internal bool Finished { get; private set; } + internal ScenarioExecutionStatus ScenarioExecutionStatus { get { return ExecutionHistory.Last().ScenarioExecutionStatus; } } + + + // ID Generator to use when generating IDs for TestCase messages and beyond + // If gherkin feature was generated using integer IDs, then we will use an integer ID generator seeded with the last known integer ID + // otherwise we'll use a GUID ID generator. We can't know ahead of time which type of ID generator to use, therefore this is not set by the constructor. + internal IIdGenerator IDGenerator { get; set; } + + internal TestCaseDefinition TestCaseDefinition { get; private set; } + private List ExecutionHistory = new(); + private TestCaseExecutionRecord Current_Execution; + private void SetExecutionRecordAsCurrentlyExecuting(TestCaseExecutionRecord executionRecord) + { + Current_Execution = executionRecord; + if (!ExecutionHistory.Contains(executionRecord)) + ExecutionHistory.Add(executionRecord); + } + + // Returns all of the Cucumber Messages that result from execution of the Test Case (eg, TestCase, TestCaseStarted, TestCaseFinished, TestStepStarted/Finished) + internal IEnumerable RuntimeGeneratedMessages + { + get + { + // ask each execution record for its messages + var tempListOfMessages = new List(); + foreach (var testCaseExecution in ExecutionHistory) + { + var aRunsWorth = testCaseExecution.RuntimeMessages; + if (tempListOfMessages.Count > 0) + { + // there has been a previous run of this scenario that was retried + // We will create a copy of the last TestRunFinished message, but with the 'willBeRetried' flag set to true + // and substitute the copy into the list + var lastRunTestCaseFinished = tempListOfMessages.Last(); + var lastRunTCMarkedAsToBeRetried = FixupWillBeRetried(lastRunTestCaseFinished); + tempListOfMessages.Remove(lastRunTestCaseFinished); + tempListOfMessages.Add(lastRunTCMarkedAsToBeRetried); + } + tempListOfMessages.AddRange(aRunsWorth); + } + return tempListOfMessages; + } + } + + private Envelope FixupWillBeRetried(Envelope lastRunTestCaseFinished) + { + TestCaseFinished prior = lastRunTestCaseFinished.Content() as TestCaseFinished; + return Envelope.Create(new TestCaseFinished(prior.TestCaseStartedId, prior.Timestamp, true)); + } + + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + internal ConcurrentDictionary StepDefinitionsByPattern; + + internal void ProcessEvent(ExecutionEvent anEvent) + { + if (!Enabled) return; + switch (anEvent) + { + case ScenarioStartedEvent scenarioStartedEvent: + ProcessEvent(scenarioStartedEvent); + break; + case ScenarioFinishedEvent scenarioFinishedEvent: + ProcessEvent(scenarioFinishedEvent); + break; + case StepStartedEvent stepStartedEvent: + ProcessEvent(stepStartedEvent); + break; + case StepFinishedEvent stepFinishedEvent: + ProcessEvent(stepFinishedEvent); + break; + case HookBindingStartedEvent hookBindingStartedEvent: + ProcessEvent(hookBindingStartedEvent); + break; + case HookBindingFinishedEvent hookBindingFinishedEvent: + ProcessEvent(hookBindingFinishedEvent); + break; + case AttachmentAddedEvent attachmentAddedEvent: + ProcessEvent(attachmentAddedEvent); + break; + case OutputAddedEvent outputAddedEvent: + ProcessEvent(outputAddedEvent); + break; + default: + throw new NotImplementedException($"Event type {anEvent.GetType().Name} is not supported."); + } + } + private void ProcessEvent(ScenarioStartedEvent scenarioStartedEvent) + { + AttemptCount++; + scenarioStartedEvent.FeatureContext.FeatureInfo.CucumberMessages_PickleId = PickleId; + // on the first time this Scenario is executed, create a TestCaseDefinition + if (AttemptCount == 0) + { + TestCaseId = IDGenerator.GetNewId(); + TestCaseDefinition = new TestCaseDefinition(TestCaseId, PickleId, this); + } + else + { + // Reset tracking + Finished = false; + Current_Execution = null; + } + var testCaseExec = new TestCaseExecutionRecord(AttemptCount, IDGenerator.GetNewId(), this); + SetExecutionRecordAsCurrentlyExecuting(testCaseExec); + testCaseExec.RecordStart(scenarioStartedEvent); + } + + + private void ProcessEvent(ScenarioFinishedEvent scenarioFinishedEvent) + { + Finished = true; + Current_Execution.RecordFinish(scenarioFinishedEvent); + } + + private void ProcessEvent(StepStartedEvent stepStartedEvent) + { + var stepState = new TestStepTracker(this, Current_Execution); + + stepState.ProcessEvent(stepStartedEvent); + Current_Execution.StepExecutionTrackers.Add(stepState); + Current_Execution.StoreMessageGenerator(stepState, stepStartedEvent); + } + private void ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + var stepState = Current_Execution.CurrentStep as TestStepTracker; + + stepState.ProcessEvent(stepFinishedEvent); + Current_Execution.StoreMessageGenerator(stepState, stepFinishedEvent); + } + + private void ProcessEvent(HookBindingStartedEvent hookBindingStartedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps; Before/AfterTestRun hooks were processed earlier by the Publisher + if (hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.AfterTestRun || hookBindingStartedEvent.HookBinding.HookType == Bindings.HookType.BeforeTestRun) + return; + var hookStepStateTracker = new HookStepTracker(this, Current_Execution); + hookStepStateTracker.ProcessEvent(hookBindingStartedEvent); + Current_Execution.StepExecutionTrackers.Add(hookStepStateTracker); + Current_Execution.StoreMessageGenerator(hookStepStateTracker, hookBindingStartedEvent); + } + + private void ProcessEvent(HookBindingFinishedEvent hookBindingFinishedEvent) + { + // At this point we only care about hooks that wrap scenarios or steps; TestRunHooks were processed earlier by the Publisher + if (hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.AfterTestRun || hookBindingFinishedEvent.HookBinding.HookType == Bindings.HookType.BeforeTestRun) + return; + var step = Current_Execution.CurrentStep as HookStepTracker; + step.ProcessEvent(hookBindingFinishedEvent); + Current_Execution.StoreMessageGenerator(step, hookBindingFinishedEvent); + } + private void ProcessEvent(AttachmentAddedEvent attachmentAddedEvent) + { + var attachmentExecutionEventWrapper = new AttachmentAddedEventWrapper( + attachmentAddedEvent, + Current_Execution.TestCaseTracker.TestRunStartedId, + Current_Execution.TestCaseStartedId, + Current_Execution.CurrentStep.Definition.TestStepId); + Current_Execution.StoreMessageGenerator(attachmentExecutionEventWrapper, attachmentAddedEvent); + } + private void ProcessEvent(OutputAddedEvent outputAddedEvent) + { + var outputExecutionEventWrapper = new OutputAddedEventWrapper( + outputAddedEvent, + Current_Execution.TestCaseTracker.TestRunStartedId, + Current_Execution.TestCaseStartedId, + Current_Execution.CurrentStep.Definition.TestStepId); + + Current_Execution.StoreMessageGenerator(outputExecutionEventWrapper, outputAddedEvent); + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestRunHookTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestRunHookTracker.cs new file mode 100644 index 000000000..81eee93f5 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestRunHookTracker.cs @@ -0,0 +1,43 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Events; +using System; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// Captures information about TestRun Hooks (Before/After TestRun and Before/After Feature) + /// + internal class TestRunHookTracker : IGenerateMessage + { + internal TestRunHookTracker(string id, string hookDefinitionId, DateTime timestamp, string testRunID) + { + TestRunHookId = id; + TestRunHook_HookId = hookDefinitionId; + TestRunID = testRunID; + TimeStamp = timestamp; + } + + internal string TestRunHookId { get; } + internal string TestRunHook_HookId { get; } + internal string TestRunID { get; } + internal DateTime TimeStamp { get; set; } + internal TimeSpan Duration { get; set; } + internal System.Exception Exception { get; set; } + internal ScenarioExecutionStatus Status + { + get { return Exception == null ? ScenarioExecutionStatus.OK : ScenarioExecutionStatus.TestError; } + } + + public IEnumerable GenerateFrom(ExecutionEvent executionEvent) + { + return executionEvent switch + { + HookBindingStartedEvent started => new List() { Envelope.Create(CucumberMessageFactory.ToTestRunHookStarted(this)) }, + HookBindingFinishedEvent finished => new List() { Envelope.Create(CucumberMessageFactory.ToTestRunHookFinished(this)) }, + _ => throw new ArgumentOutOfRangeException(nameof(executionEvent), executionEvent, null), + }; + } + } +} diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepDefinition.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepDefinition.cs new file mode 100644 index 000000000..b01881a9a --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepDefinition.cs @@ -0,0 +1,92 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Events; +using System.Collections.Generic; +using System.Linq; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + public class TestStepArgument + { + public string Value; + public int? StartOffset; + public string Type; + } + + + /// + /// Data class that captures information about a TestStep that is being executed for the first time. + /// One of these is created per step, regardless of how many times the Test Case is retried. + /// + internal class TestStepDefinition + { + // The Id of the Step within the TestCase + internal string TestStepId { get; } + + // The Id of the PickleStep + internal string PickleStepID { get; } + internal TestCaseDefinition ParentTestCaseDefinition { get; } + + // The Step Definition(s) that match this step of the Test Case. None for no match, 1 for a successful match, 2 or more for Ambiguous match. + internal List StepDefinitionIds { get; private set; } + // Indicates whether the step was successfully bound to a Step Definition. + internal bool Bound { get; private set; } + + // The method name and signature of the bound method + private string CanonicalizedStepPattern { get; set; } + + // List of method signatures that cause an ambiguous situation to arise + private IEnumerable AmbiguousStepDefinitions { get; set; } + internal bool Ambiguous { get { return AmbiguousStepDefinitions != null && AmbiguousStepDefinitions.Count() > 0; } } + private IStepDefinitionBinding StepDefinitionBinding; + + internal List StepArguments { get; private set; } + + + internal TestStepDefinition(string testStepDefinitionId, string pickleStepId, TestCaseDefinition parentTestCaseDefinition) + { + TestStepId = testStepDefinitionId; + PickleStepID = pickleStepId; + ParentTestCaseDefinition = parentTestCaseDefinition; + } + + // Once the StepFinished event fires, we can finally capture which step binding was used and the arguments sent as parameters to the binding method + internal void PopulateStepDefinitionFromExecutionResult(StepFinishedEvent stepFinishedEvent) + { + var bindingMatch = stepFinishedEvent.StepContext?.StepInfo?.BindingMatch; + Bound = !(bindingMatch == null || bindingMatch == BindingMatch.NonMatching); + + StepDefinitionBinding = Bound ? bindingMatch.StepBinding : null; + CanonicalizedStepPattern = Bound ? CucumberMessageFactory.CanonicalizeStepDefinitionPattern(StepDefinitionBinding) : ""; + var StepDefinitionId = Bound ? ParentTestCaseDefinition.FindStepDefIDByStepPattern(CanonicalizedStepPattern) : null; + + var Status = stepFinishedEvent.StepContext.Status; + + if (Status == ScenarioExecutionStatus.TestError && stepFinishedEvent.ScenarioContext.TestError != null) + { + var Exception = stepFinishedEvent.ScenarioContext.TestError; + if (Exception is AmbiguousBindingException) + { + AmbiguousStepDefinitions = new List(((AmbiguousBindingException)Exception).Matches.Select(m => + ParentTestCaseDefinition.FindStepDefIDByStepPattern(CucumberMessageFactory.CanonicalizeStepDefinitionPattern(m.StepBinding)))); + } + } + + StepDefinitionIds = Ambiguous ? AmbiguousStepDefinitions.ToList() : StepDefinitionId != null ? [StepDefinitionId] : []; + + var IsInputDataTableOrDocString = stepFinishedEvent.StepContext.StepInfo.Table != null || stepFinishedEvent.StepContext.StepInfo.MultilineText != null; + var argumentValues = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.Value.ToString()).ToList() : new List(); + var argumentStartOffsets = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.Arguments.Select(arg => arg.StartOffset).ToList() : new List(); + var argumentTypes = Bound ? stepFinishedEvent.StepContext.StepInfo.BindingMatch.StepBinding.Method.Parameters.Select(p => p.Type.Name).ToList() : new List(); + StepArguments = new(); + if (Bound && !IsInputDataTableOrDocString) + { + for (int i = 0; i < argumentValues.Count; i++) + { + StepArguments.Add(new TestStepArgument { Value = argumentValues[i], StartOffset = argumentStartOffsets[i], Type = argumentTypes[i] }); + } + } + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs new file mode 100644 index 000000000..08d78f053 --- /dev/null +++ b/Reqnroll/CucumberMessages/ExecutionTracking/TestStepTracker.cs @@ -0,0 +1,62 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.Assist; +using Reqnroll.Bindings; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Reqnroll.CucumberMessages.ExecutionTracking +{ + /// + /// This class is used to track the execution of Test StepDefinitionBinding Methods + /// + internal class TestStepTracker : StepExecutionTrackerBase, IGenerateMessage + { + internal TestStepTracker(TestCaseTracker parentTracker, TestCaseExecutionRecord parentExecutionRecord) : base(parentTracker, parentExecutionRecord) + { + } + + public IEnumerable GenerateFrom(ExecutionEvent executionEvent) + { + return executionEvent switch + { + StepStartedEvent started => [Envelope.Create(CucumberMessageFactory.ToTestStepStarted(this))], + StepFinishedEvent finished => [Envelope.Create(CucumberMessageFactory.ToTestStepFinished(this))], + _ => Enumerable.Empty() + }; + } + + internal void ProcessEvent(StepStartedEvent stepStartedEvent) + { + StepStarted = stepStartedEvent.Timestamp; + + // if this is the first time to execute this step for this test, generate the properties needed to Generate the TestStep Message (stored in a TestStepDefinition) + if (ParentTestCase.AttemptCount == 0) + { + var testStepId = ParentTestCase.IDGenerator.GetNewId(); + var pickleStepID = stepStartedEvent.StepContext.StepInfo.PickleStepId; + Definition = new(testStepId, pickleStepID, ParentTestCase.TestCaseDefinition); + ParentTestCase.TestCaseDefinition.StepDefinitions.Add(Definition); + } + else + { + // On retries of the TestCase, find the Definition previously created. + Definition = ParentTestCase.TestCaseDefinition.StepDefinitions.OfType().Where(sd => sd.PickleStepID == stepStartedEvent.StepContext.StepInfo.PickleStepId).First(); + } + } + + internal void ProcessEvent(StepFinishedEvent stepFinishedEvent) + { + if (ParentTestCase.AttemptCount == 0) + Definition.PopulateStepDefinitionFromExecutionResult(stepFinishedEvent); + Exception = stepFinishedEvent.ScenarioContext.TestError; + StepFinished = stepFinishedEvent.Timestamp; + + Status = stepFinishedEvent.StepContext.Status; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs new file mode 100644 index 000000000..512a63c49 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessagEnumConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber +{ + /// + /// Gherkin Cucumber Message enums use attributes to provide a Text value to represent the enum value. + /// This class is used to convert the enum to and from a string during serialization with System.Text.Json + /// + /// /Gherkin Message Enum Type + internal class CucumberMessageEnumConverter : JsonConverter where T : struct, Enum + { + private readonly Dictionary _enumToString = new(); + private readonly Dictionary _stringToEnum = new(); + + protected internal CucumberMessageEnumConverter() + { + var type = typeof(T); + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var value = (T)field.GetValue(null)!; + var attribute = field.GetCustomAttribute(); + var name = attribute?.Description ?? field.Name; + _enumToString[value] = name; + _stringToEnum[name] = value; + } + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var stringValue = reader.GetString(); + return _stringToEnum.TryGetValue(stringValue!, out var enumValue) ? enumValue : default; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(_enumToString.TryGetValue(value, out var stringValue) ? stringValue : value.ToString()); + } + } + +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageExtensions.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageExtensions.cs new file mode 100644 index 000000000..5912b5279 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageExtensions.cs @@ -0,0 +1,124 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CommonModels; +using System; +using System.Collections.Generic; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber +{ + /// + /// Convenience methods to help work with Message Envelopes + /// + public static class CucumberMessageExtensions + { + public static List MessagesWithIds = new(){ typeof(Background), + typeof(Examples), + typeof(Hook), + typeof(ParameterType), + typeof(Pickle), + typeof(PickleStep), + typeof(Rule), + typeof(Scenario), + typeof(Step), + typeof(StepDefinition), + typeof(TableRow), + typeof(Tag), + typeof(TestCase), + typeof(TestCaseStarted), + typeof(TestStep), + typeof(TestRunHookStarted) + }; + + + public static bool HasId(this object element) + { + return MessagesWithIds.Contains(element.GetType()); + } + + public static string Id(this object message) + { + return message switch + { + Background bgd => bgd.Id, + Examples ex => ex.Id, + Hook hook => hook.Id, + ParameterType pt => pt.Id, + Pickle p => p.Id, + PickleStep ps => ps.Id, + Rule r => r.Id, + Scenario sc => sc.Id, + Step st => st.Id, + StepDefinition sd => sd.Id, + TableRow tr => tr.Id, + Tag tag => tag.Id, + TestCase tc => tc.Id, + TestCaseStarted tcs => tcs.Id, + TestStep ts => ts.Id, + TestRunHookStarted trhs => trhs.Id, + _ => throw new ArgumentException($"Message of type: {message.GetType()} has no ID") + }; + } + + public static List EnvelopeContentTypes = new() + { + typeof(Attachment), + typeof(GherkinDocument), + typeof(Hook), + typeof(Meta), + typeof(ParameterType), + typeof(ParseError), + typeof(Pickle), + typeof(Source), + typeof(StepDefinition), + typeof(TestCase), + typeof(TestCaseFinished), + typeof(TestCaseStarted), + typeof(TestRunFinished), + typeof(TestRunStarted), + typeof(TestStepFinished), + typeof(TestStepStarted), + typeof(UndefinedParameterType), + typeof(TestRunHookStarted), + typeof(TestRunHookFinished) + }; + + public static object Content(this Envelope envelope) + { + object result = null; + if (envelope.Attachment != null) { result = envelope.Attachment; } + else if (envelope.GherkinDocument != null) { result = envelope.GherkinDocument; } + else if (envelope.Hook != null) { result = envelope.Hook; } + else if (envelope.Meta != null) { result = envelope.Meta; } + else if (envelope.ParameterType != null) { result = envelope.ParameterType; } + else if (envelope.ParseError != null) { result = envelope.ParseError; } + else if (envelope.Pickle != null) { result = envelope.Pickle; } + else if (envelope.Source != null) { result = envelope.Source; } + else if (envelope.StepDefinition != null) { result = envelope.StepDefinition; } + else if (envelope.TestCase != null) { result = envelope.TestCase; } + else if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished; } + else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted; } + else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished; } + else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted; } + else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished; } + else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted; } + else if (envelope.UndefinedParameterType != null) { result = envelope.UndefinedParameterType; } + else if (envelope.TestRunHookStarted != null) { result = envelope.TestRunHookStarted; } + else if (envelope.TestRunHookFinished != null) { result = envelope.TestRunHookFinished; } + return result; + } + + public static Timestamp Timestamp(this Envelope envelope) + { + Timestamp result = null; + if (envelope.TestCaseFinished != null) { result = envelope.TestCaseFinished.Timestamp; } + else if (envelope.TestCaseStarted != null) { result = envelope.TestCaseStarted.Timestamp; } + else if (envelope.TestRunFinished != null) { result = envelope.TestRunFinished.Timestamp; } + else if (envelope.TestRunStarted != null) { result = envelope.TestRunStarted.Timestamp; } + else if (envelope.TestStepFinished != null) { result = envelope.TestStepFinished.Timestamp; } + else if (envelope.TestStepStarted != null) { result = envelope.TestStepStarted.Timestamp; } + else if (envelope.TestRunHookStarted != null) { result = envelope.TestRunHookStarted.Timestamp; } + else if (envelope.TestRunHookFinished != null) { result = envelope.TestRunHookFinished.Timestamp; } + else throw new ArgumentException($"Envelope of type: {envelope.Content().GetType()} does not contain a timestamp"); + return result; + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs new file mode 100644 index 000000000..06908a2e0 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageFactory.cs @@ -0,0 +1,433 @@ +using Cucumber.Messages; +using Gherkin.CucumberMessages; +using Io.Cucumber.Messages.Types; +using Reqnroll.Bindings; +using Reqnroll.BoDi; +using Reqnroll.CommonModels; +using Reqnroll.CucumberMessages.ExecutionTracking; +using Reqnroll.EnvironmentAccess; +using Reqnroll.Events; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber +{ + /// + /// This class provides functions to convert execution level detail into Cucumber message elements + /// + /// These are typically called after execution is completed for a Feature. + /// + internal class CucumberMessageFactory + { + internal static TestRunStarted ToTestRunStarted(DateTime timestamp, string id) + { + return new TestRunStarted(Converters.ToTimestamp(timestamp.ToUniversalTime()), id); + } + + internal static TestRunFinished ToTestRunFinished(bool testRunStatus, DateTime timestamp, string testRunStartedId) + { + return new TestRunFinished(null, testRunStatus, Converters.ToTimestamp(timestamp.ToUniversalTime()), null, testRunStartedId); + } + + internal static TestRunHookStarted ToTestRunHookStarted(TestRunHookTracker hookTracker) + { + return new TestRunHookStarted(hookTracker.TestRunHookId, hookTracker.TestRunID, hookTracker.TestRunHook_HookId, Converters.ToTimestamp(hookTracker.TimeStamp.ToUniversalTime())); + } + + internal static TestRunHookFinished ToTestRunHookFinished(TestRunHookTracker hookTracker) + { + return new TestRunHookFinished(hookTracker.TestRunHookId, ToTestStepResult(hookTracker), Converters.ToTimestamp(hookTracker.TimeStamp.ToUniversalTime())); + } + + internal static TestCase ToTestCase(TestCaseDefinition testCaseDefinition) + { + var testSteps = new List(); + + foreach (var stepDefinition in testCaseDefinition.StepDefinitions) + { + switch (stepDefinition) + { + case HookStepDefinition _: + var hookTestStep = ToHookTestStep(stepDefinition as HookStepDefinition); + testSteps.Add(hookTestStep); + break; + case TestStepDefinition _: + var testStep = ToPickleTestStep(stepDefinition); + testSteps.Add(testStep); + break; + default: + throw new NotImplementedException(); + } + } + var testCase = new TestCase + ( + testCaseDefinition.TestCaseId, + testCaseDefinition.PickleId, + testSteps, + testCaseDefinition.Tracker.TestRunStartedId + ); + return testCase; + } + internal static TestCaseStarted ToTestCaseStarted(TestCaseExecutionRecord testCaseExecution, string testCaseId) + { + return new TestCaseStarted( + testCaseExecution.AttemptId, + testCaseExecution.TestCaseStartedId, + testCaseId, + null, + Converters.ToTimestamp(testCaseExecution.TestCaseStartedTimeStamp.ToUniversalTime())); + } + internal static TestCaseFinished ToTestCaseFinished(TestCaseExecutionRecord testCaseExecution) + { + return new TestCaseFinished( + testCaseExecution.TestCaseStartedId, + Converters.ToTimestamp(testCaseExecution.TestCaseFinishedTimeStamp.ToUniversalTime()), + false); + } + internal static StepDefinition ToStepDefinition(IStepDefinitionBinding binding, IIdGenerator idGenerator) + { + StepDefinitionPattern stepDefinitionPattern = ToStepDefinitionPattern(binding); + SourceReference sourceRef = ToSourceRef(binding); + + var result = new StepDefinition + ( + idGenerator.GetNewId(), + stepDefinitionPattern, + sourceRef + ); + return result; + } + + internal static StepDefinitionPattern ToStepDefinitionPattern(IStepDefinitionBinding binding) + { + var bindingSourceText = binding.SourceExpression; + var expressionType = binding.ExpressionType; + var stepDefinitionPatternType = expressionType switch + { + StepDefinitionExpressionTypes.CucumberExpression => StepDefinitionPatternType.CUCUMBER_EXPRESSION, + _ => StepDefinitionPatternType.REGULAR_EXPRESSION + }; + var stepDefinitionPattern = new StepDefinitionPattern(bindingSourceText, stepDefinitionPatternType); + return stepDefinitionPattern; + } + internal static UndefinedParameterType ToUndefinedParameterType(string expression, string paramName, IIdGenerator iDGenerator) + { + return new UndefinedParameterType(expression, paramName); + } + + internal static ParameterType ToParameterType(IStepArgumentTransformationBinding stepTransform, IIdGenerator iDGenerator) + { + var regex = stepTransform.Regex; + var regexPattern = regex == null ? null : regex.ToString(); + var name = stepTransform.Name ?? stepTransform.Method.ReturnType.Name; + var result = new ParameterType + ( + name, + [regexPattern], + false, + false, + iDGenerator.GetNewId(), + ToSourceRef(stepTransform) + ); + return result; + } + + private static SourceReference ToSourceRef(IBinding binding) + { + var methodName = binding.Method.Name; + var className = binding.Method.Type.AssemblyName + "." + binding.Method.Type.FullName; + var paramTypes = binding.Method.Parameters.Select(x => x.Type.Name).ToList(); + var methodDescription = new JavaMethod(className, methodName, paramTypes); + var sourceRef = SourceReference.Create(methodDescription); + return sourceRef; + } + + internal static TestStep ToPickleTestStep(TestStepDefinition stepDef) + { + bool bound = stepDef.Bound; + bool ambiguous = stepDef.Ambiguous; + + var args = stepDef.StepArguments + .Select(arg => ToStepMatchArgument(arg)) + .ToList(); + + var result = new TestStep( + null, + stepDef.TestStepId, + stepDef.PickleStepID, + stepDef.StepDefinitionIds, + bound ? new List { new StepMatchArgumentsList(args) } : new List() + ); + + return result; + } + + internal static StepMatchArgument ToStepMatchArgument(TestStepArgument argument) + { + return new StepMatchArgument( + new Group( + [], + argument.StartOffset, + argument.Value + ), + NormalizePrimitiveTypeNamesToCucumberTypeNames(argument.Type)); + } + internal static TestStepStarted ToTestStepStarted(TestStepTracker stepState) + { + return new TestStepStarted( + stepState.TestCaseStartedID, + stepState.Definition.TestStepId, + Converters.ToTimestamp(stepState.StepStarted.ToUniversalTime())); + } + + internal static TestStepFinished ToTestStepFinished(TestStepTracker stepState) + { + return new TestStepFinished( + stepState.TestCaseStartedID, + stepState.Definition.TestStepId, + ToTestStepResult(stepState), + Converters.ToTimestamp(stepState.StepFinished.ToUniversalTime())); + } + + internal static Hook ToHook(IHookBinding hookBinding, IIdGenerator iDGenerator) + { + SourceReference sourceRef = ToSourceRef(hookBinding); + + var result = new Hook + ( + iDGenerator.GetNewId(), + null, + sourceRef, + hookBinding.IsScoped ? "@{hookBinding.BindingScope.Tag}" : null, + ToHookType(hookBinding) + ); + return result; + } + + internal static Io.Cucumber.Messages.Types.HookType ToHookType(IHookBinding hookBinding) + { + return hookBinding.HookType switch + { + Bindings.HookType.BeforeTestRun => Io.Cucumber.Messages.Types.HookType.BEFORE_TEST_RUN, + Bindings.HookType.AfterTestRun => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_RUN, + Bindings.HookType.BeforeFeature => Io.Cucumber.Messages.Types.HookType.BEFORE_TEST_RUN, + Bindings.HookType.AfterFeature => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_RUN, + Bindings.HookType.BeforeScenario => Io.Cucumber.Messages.Types.HookType.BEFORE_TEST_CASE, + Bindings.HookType.AfterScenario => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_CASE, + Bindings.HookType.BeforeStep => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_STEP, + Bindings.HookType.AfterStep => Io.Cucumber.Messages.Types.HookType.AFTER_TEST_STEP, + + // Note: The following isn't strictly correct, but about all that can be done given that Cucumber doesn't support any other types of Hooks + _ => Io.Cucumber.Messages.Types.HookType.BEFORE_TEST_RUN + }; + } + + internal static TestStep ToHookTestStep(HookStepDefinition hookStepDefinition) + { + var hookId = hookStepDefinition.HookId; + + return new TestStep( + hookId, + hookStepDefinition.TestStepId, + null, + null, + null); + } + internal static TestStepStarted ToTestStepStarted(HookStepTracker hookStepProcessor) + { + return new TestStepStarted(hookStepProcessor.TestCaseStartedID, + hookStepProcessor.Definition.TestStepId, + Converters.ToTimestamp(hookStepProcessor.StepStarted.ToUniversalTime())); + } + + internal static TestStepFinished ToTestStepFinished(HookStepTracker hookStepProcessor) + { + return new TestStepFinished(hookStepProcessor.TestCaseStartedID, + hookStepProcessor.Definition.TestStepId, + ToTestStepResult(hookStepProcessor), Converters.ToTimestamp(hookStepProcessor.StepFinished.ToUniversalTime())); + } + + internal static Attachment ToAttachment(AttachmentAddedEventWrapper tracker) + { + var attEvent = tracker.AttachmentAddedEvent; + return new Attachment( + Base64EncodeFile(attEvent.FilePath), + AttachmentContentEncoding.BASE64, + Path.GetFileName(attEvent.FilePath), + FileExtensionToMIMETypeMap.GetMimeType(Path.GetExtension(attEvent.FilePath)), + null, + tracker.TestCaseStartedId, + tracker.TestCaseStepId, + null, + tracker.TestRunStartedId); + } + internal static Attachment ToAttachment(OutputAddedEventWrapper tracker) + { + var outputAddedEvent = tracker.OutputAddedEvent; + return new Attachment( + outputAddedEvent.Text, + AttachmentContentEncoding.IDENTITY, + null, + "text/x.cucumber.log+plain", + null, + tracker.TestCaseStartedId, + tracker.TestCaseStepId, + null, + tracker.TestRunStartedId); + } + + private static TestStepResult ToTestStepResult(StepExecutionTrackerBase stepState) + { + return new TestStepResult( + Converters.ToDuration(stepState.Duration), + "", + ToTestStepResultStatus(stepState.Status), + ToException(stepState.Exception) + ); + } + + private static TestStepResult ToTestStepResult(TestRunHookTracker hookTracker) + { + return new TestStepResult( + Converters.ToDuration(hookTracker.Duration), + "", + ToTestStepResultStatus(hookTracker.Status), + ToException(hookTracker.Exception)); + } + + private static Io.Cucumber.Messages.Types.Exception ToException(System.Exception exception) + { + if (exception == null) return null; + + return new Io.Cucumber.Messages.Types.Exception( + exception.GetType().Name, + exception.Message, + exception.StackTrace + ); + } + + private static TestStepResultStatus ToTestStepResultStatus(ScenarioExecutionStatus status) + { + return status switch + { + ScenarioExecutionStatus.OK => TestStepResultStatus.PASSED, + ScenarioExecutionStatus.BindingError => TestStepResultStatus.AMBIGUOUS, + ScenarioExecutionStatus.TestError => TestStepResultStatus.FAILED, + ScenarioExecutionStatus.Skipped => TestStepResultStatus.SKIPPED, + ScenarioExecutionStatus.UndefinedStep => TestStepResultStatus.UNDEFINED, + ScenarioExecutionStatus.StepDefinitionPending => TestStepResultStatus.PENDING, + _ => TestStepResultStatus.UNKNOWN + }; + } + + internal static Envelope ToMeta(IObjectContainer container) + { + var environmentInfoProvider = container.Resolve(); + var environmentWrapper = container.Resolve(); + + var implementation = new Product("Reqnroll", environmentInfoProvider.GetReqnrollVersion()); + string targetFramework = environmentInfoProvider.GetNetCoreVersion() ?? RuntimeInformation.FrameworkDescription; + + var runTime = new Product("dotNet", targetFramework); + var os = new Product(environmentInfoProvider.GetOSPlatform(), RuntimeInformation.OSDescription); + + var cpu = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm => new Product("arm", null), + Architecture.Arm64 => new Product("arm64", null), + Architecture.X86 => new Product("x86", null), + Architecture.X64 => new Product("x64", null), + _ => new Product(null, null), + }; + + var ci_name = environmentInfoProvider.GetBuildServerName(); + + var ci = ToCi(ci_name, environmentInfoProvider, environmentWrapper); + + return Envelope.Create(new Meta( + ProtocolVersion.Version.Split('+')[0], + implementation, + runTime, + os, + cpu, + ci)); + } + + private static Ci ToCi(string ci_name, IEnvironmentInfoProvider environmentInfoProvider, IEnvironmentWrapper environmentWrapper) + { + //TODO: Find a way to abstract how various CI systems convey links to builds and build numbers. + // Until then, these will be hard coded as null + if (string.IsNullOrEmpty(ci_name)) return null; + + var git = ToGit(environmentWrapper); + + return new Ci(ci_name, null, null, git); + } + + private static Git ToGit(IEnvironmentWrapper environmentWrapper) + { + Git git; + var git_url = environmentWrapper.GetEnvironmentVariable("GIT_URL"); + var git_branch = environmentWrapper.GetEnvironmentVariable("GIT_BRANCH"); + var git_commit = environmentWrapper.GetEnvironmentVariable("GIT_COMMIT"); + var git_tag = environmentWrapper.GetEnvironmentVariable("GIT_TAG"); + if (git_url is not ISuccess) git = null; + else + git = new Git + ( + (git_url as ISuccess).Result, + git_branch is ISuccess ? (git_branch as ISuccess).Result : null, + git_commit is ISuccess ? (git_commit as ISuccess).Result : null, + git_tag is ISuccess ? (git_tag as ISuccess).Result : null + ); + return git; + } + + #region utility methods + internal static string CanonicalizeStepDefinitionPattern(IStepDefinitionBinding stepDefinition) + { + string signature = GenerateSignature(stepDefinition); + + return $"{stepDefinition.Method.Type.AssemblyName}.{stepDefinition.Method.Type.FullName}.{stepDefinition.Method.Name}({signature})"; + } + + internal static string CanonicalizeHookBinding(IHookBinding hookBinding) + { + string signature = GenerateSignature(hookBinding); + return $"{hookBinding.Method.Type.AssemblyName}.{hookBinding.Method.Type.FullName}.{hookBinding.Method.Name}({signature})"; + } + + private static string GenerateSignature(IBinding stepDefinition) + { + return stepDefinition.Method != null ? string.Join(",", stepDefinition.Method.Parameters.Select(p => p.Type.Name)) : ""; + } + private static string Base64EncodeFile(string filePath) + { + byte[] fileBytes = File.ReadAllBytes(filePath); + return Convert.ToBase64String(fileBytes); + } + + private static string NormalizePrimitiveTypeNamesToCucumberTypeNames(string name) + { + return name switch + { + "Int16" => "short", + "Int32" => "int", + "Int64" => "long", + "Single" => "float", + "Double" => "double", + "Byte" => "byte", + "String" => "string", + "Boolean" => "bool", + "Decimal" => "decimal", + "BigInteger" => "biginteger", + _ => name + }; + } + #endregion + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs new file mode 100644 index 000000000..edb9703bc --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageTransformer.cs @@ -0,0 +1,347 @@ +using Gherkin.CucumberMessages; +using Gherkin.CucumberMessages.Types; +using Io.Cucumber.Messages.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber +{ + /// + /// The purpose of this class is to transform Cucumber messages from the Gherkin.CucumberMessages.Types namespace to the Io.Cucumber.Messages.Types namespace + /// + /// TODO: Once the Gherkin project is updated to directly consume and produce Cucumber messages, this class can be removed + /// + public class CucumberMessageTransformer + { + public static Io.Cucumber.Messages.Types.Source ToSource(global::Gherkin.CucumberMessages.Types.Source gherkinSource) + { + var result = new Io.Cucumber.Messages.Types.Source + ( + gherkinSource.Uri, + gherkinSource.Data, + gherkinSource.MediaType == "text/x.cucumber.gherkin+plain" ? SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN : SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN + ); + return result; + } + + public static Io.Cucumber.Messages.Types.GherkinDocument ToGherkinDocument(global::Gherkin.CucumberMessages.Types.GherkinDocument gherkinDoc) + { + var result = new Io.Cucumber.Messages.Types.GherkinDocument + ( + gherkinDoc.Uri, + ToFeature(gherkinDoc.Feature), + ToComments(gherkinDoc.Comments) + ); + return result; + } + + private static Io.Cucumber.Messages.Types.Feature ToFeature(global::Gherkin.CucumberMessages.Types.Feature feature) + { + if (feature == null) + { + return null; + } + + var children = feature.Children.Select(ToFeatureChild).ToList(); + var tags = feature.Tags.Select(ToTag).ToList(); + + return new Io.Cucumber.Messages.Types.Feature( + ToLocation(feature.Location), + tags, + feature.Language, + feature.Keyword, + feature.Name, + feature.Description, + children) + ; + } + + private static Io.Cucumber.Messages.Types.Location ToLocation(global::Gherkin.CucumberMessages.Types.Location location) + { + if (location == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Location(location.Line, location.Column); + } + + + private static Io.Cucumber.Messages.Types.Tag ToTag(global::Gherkin.CucumberMessages.Types.Tag tag) + { + if (tag == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Tag(ToLocation(tag.Location), tag.Name, tag.Id); + } + + private static Io.Cucumber.Messages.Types.FeatureChild ToFeatureChild(global::Gherkin.CucumberMessages.Types.FeatureChild child) + { + if (child == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.FeatureChild + ( + ToRule(child.Rule), + ToBackground(child.Background), + ToScenario(child.Scenario) + ); + } + + private static Io.Cucumber.Messages.Types.Scenario ToScenario(global::Gherkin.CucumberMessages.Types.Scenario scenario) + { + if (scenario == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Scenario + ( + ToLocation(scenario.Location), + scenario.Tags.Select(ToTag).ToList(), + scenario.Keyword, + scenario.Name, + scenario.Description, + scenario.Steps.Select(ToStep).ToList(), + scenario.Examples.Select(ToExamples).ToList(), + scenario.Id + ); + } + + private static Io.Cucumber.Messages.Types.Examples ToExamples(global::Gherkin.CucumberMessages.Types.Examples examples) + { + if (examples == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Examples( + ToLocation(examples.Location), + examples.Tags.Select(ToTag).ToList(), + examples.Keyword, + examples.Name, + examples.Description, + ToTableRow(examples.TableHeader), + examples.TableBody.Select(ToTableRow).ToList(), + examples.Id + ); + } + private static Io.Cucumber.Messages.Types.TableCell ToTableCell(global::Gherkin.CucumberMessages.Types.TableCell cell) + { + return new Io.Cucumber.Messages.Types.TableCell( + ToLocation(cell.Location), + cell.Value + ); + } + + private static Io.Cucumber.Messages.Types.TableRow ToTableRow(global::Gherkin.CucumberMessages.Types.TableRow row) + { + return new Io.Cucumber.Messages.Types.TableRow( + ToLocation(row.Location), + row.Cells.Select(ToTableCell).ToList(), + row.Id + ); + } + private static Io.Cucumber.Messages.Types.Step ToStep(global::Gherkin.CucumberMessages.Types.Step step) + { + if (step == null) + { + return null; + } + + return new Io.Cucumber.Messages.Types.Step( + ToLocation(step.Location), + step.Keyword, + ToKeyWordType(step.KeywordType), + step.Text, + step.DocString == null ? null : ToDocString(step.DocString), + step.DataTable == null ? null : ToDataTable(step.DataTable), + step.Id + ); + } + + private static Io.Cucumber.Messages.Types.Background ToBackground(global::Gherkin.CucumberMessages.Types.Background background) + { + if (background == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Background( + ToLocation(background.Location), + background.Keyword, + background.Name, + background.Description, + background.Steps.Select(ToStep).ToList(), + background.Id + ); + } + + private static Io.Cucumber.Messages.Types.Rule ToRule(global::Gherkin.CucumberMessages.Types.Rule rule) + { + if (rule == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.Rule( + ToLocation(rule.Location), + rule.Tags.Select(ToTag).ToList(), + rule.Keyword, + rule.Name, + rule.Description, + rule.Children.Select(ToRuleChild).ToList(), + rule.Id + ); + } + + private static Io.Cucumber.Messages.Types.RuleChild ToRuleChild(global::Gherkin.CucumberMessages.Types.RuleChild child) + { + return new Io.Cucumber.Messages.Types.RuleChild( + ToBackground(child.Background), + ToScenario(child.Scenario) + ); + } + + private static List ToComments(IReadOnlyCollection comments) + { + return comments.Select(ToComment).ToList(); + } + + private static Io.Cucumber.Messages.Types.Comment ToComment(global::Gherkin.CucumberMessages.Types.Comment comment) + { + return new Io.Cucumber.Messages.Types.Comment( + ToLocation(comment.Location), + comment.Text + ); + } + private static StepKeywordType ToKeyWordType(global::Gherkin.StepKeywordType keywordType) + { + return keywordType switch + { + //case Gherkin.StepKeywordType.Unspecified: + // return Io.Cucumber.Messages.Types.StepKeywordType.UNKNOWN; + global::Gherkin.StepKeywordType.Context => StepKeywordType.CONTEXT, + global::Gherkin.StepKeywordType.Conjunction => StepKeywordType.CONJUNCTION, + global::Gherkin.StepKeywordType.Action => StepKeywordType.ACTION, + global::Gherkin.StepKeywordType.Outcome => StepKeywordType.OUTCOME, + global::Gherkin.StepKeywordType.Unknown => StepKeywordType.UNKNOWN, + _ => throw new ArgumentException($"Invalid keyword type: {keywordType}"), + }; + } + + private static Io.Cucumber.Messages.Types.DocString ToDocString(global::Gherkin.CucumberMessages.Types.DocString docString) + { + return new Io.Cucumber.Messages.Types.DocString( + ToLocation(docString.Location), + docString.MediaType, + docString.Content, + docString.Delimiter + ); + } + + private static Io.Cucumber.Messages.Types.DataTable ToDataTable(global::Gherkin.CucumberMessages.Types.DataTable dataTable) + { + return new Io.Cucumber.Messages.Types.DataTable( + ToLocation(dataTable.Location), + dataTable.Rows.Select(ToTableRow).ToList() + ); + } + + public static List ToPickles(IEnumerable pickles) + { + return pickles.Select(ToPickle).ToList(); + } + + private static Io.Cucumber.Messages.Types.Pickle ToPickle(global::Gherkin.CucumberMessages.Types.Pickle pickle) + { + return new Io.Cucumber.Messages.Types.Pickle( + pickle.Id, + pickle.Uri, + pickle.Name, + pickle.Language, + pickle.Steps.Select(ToPickleStep).ToList(), + pickle.Tags.Select(ToPickleTag).ToList(), + pickle.AstNodeIds.ToList() + ); + } + private static Io.Cucumber.Messages.Types.PickleTag ToPickleTag(global::Gherkin.CucumberMessages.Types.PickleTag pickleTag) + { + return new Io.Cucumber.Messages.Types.PickleTag( + pickleTag.Name, + pickleTag.AstNodeId + ); + } + private static Io.Cucumber.Messages.Types.PickleStep ToPickleStep(global::Gherkin.CucumberMessages.Types.PickleStep pickleStep) + { + return new Io.Cucumber.Messages.Types.PickleStep( + ToPickleStepArgument(pickleStep.Argument), + pickleStep.AstNodeIds.ToList(), + pickleStep.Id, + ToPickleStepType(pickleStep.Type), + pickleStep.Text + ); + } + private static Io.Cucumber.Messages.Types.PickleStepArgument ToPickleStepArgument(global::Gherkin.CucumberMessages.Types.PickleStepArgument pickleStepArgument) + { + if (pickleStepArgument == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleStepArgument( + ToPickleDocString(pickleStepArgument.DocString), + ToPickleTable(pickleStepArgument.DataTable) + ); + } + + private static PickleStepType ToPickleStepType(global::Gherkin.StepKeywordType pickleStepType) + { + return pickleStepType switch + { + global::Gherkin.StepKeywordType.Unknown => PickleStepType.UNKNOWN, + global::Gherkin.StepKeywordType.Action => PickleStepType.ACTION, + global::Gherkin.StepKeywordType.Outcome => PickleStepType.OUTCOME, + global::Gherkin.StepKeywordType.Context => PickleStepType.CONTEXT, + _ => throw new ArgumentException($"Invalid pickle step type: {pickleStepType}") + }; + } + private static Io.Cucumber.Messages.Types.PickleDocString ToPickleDocString(global::Gherkin.CucumberMessages.Types.PickleDocString pickleDocString) + { + if (pickleDocString == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleDocString( + pickleDocString.MediaType, + pickleDocString.Content + ); + } + + private static Io.Cucumber.Messages.Types.PickleTable ToPickleTable(global::Gherkin.CucumberMessages.Types.PickleTable pickleTable) + { + if (pickleTable == null) + { + return null; + } + return new Io.Cucumber.Messages.Types.PickleTable( + pickleTable.Rows.Select(ToPickleTableRow).ToList() + ); + } + + private static Io.Cucumber.Messages.Types.PickleTableRow ToPickleTableRow(global::Gherkin.CucumberMessages.Types.PickleTableRow pickleTableRow) + { + return new Io.Cucumber.Messages.Types.PickleTableRow( + pickleTableRow.Cells.Select(ToPickleTableCell).ToList() + ); + } + + private static Io.Cucumber.Messages.Types.PickleTableCell ToPickleTableCell(global::Gherkin.CucumberMessages.Types.PickleTableCell pickleTableCell) + { + return new Io.Cucumber.Messages.Types.PickleTableCell( + pickleTableCell.Value + ); + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageVisitor.cs new file mode 100644 index 000000000..5e42feec5 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessageVisitor.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; + +public class CucumberMessageVisitor +{ + public static void Accept(ICucumberMessageVisitor visitor, object message) + { + switch (message) + { + // Existing cases + case Envelope envelope: + visitor.Visit(envelope); + break; + case Attachment attachment: + visitor.Visit(attachment); + break; + case GherkinDocument gherkinDocument: + visitor.Visit(gherkinDocument); + break; + case Feature feature: + visitor.Visit(feature); + break; + case FeatureChild featureChild: + visitor.Visit(featureChild); + break; + case Rule rule: + visitor.Visit(rule); + break; + case RuleChild ruleChild: + visitor.Visit(ruleChild); + break; + case Background background: + visitor.Visit(background); + break; + case Scenario scenario: + visitor.Visit(scenario); + break; + case Examples examples: + visitor.Visit(examples); + break; + case Step step: + visitor.Visit(step); + break; + case TableRow tableRow: + visitor.Visit(tableRow); + break; + case TableCell tableCell: + visitor.Visit(tableCell); + break; + case Tag tag: + visitor.Visit(tag); + break; + case Pickle pickle: + visitor.Visit(pickle); + break; + case PickleStep pickleStep: + visitor.Visit(pickleStep); + break; + case PickleStepArgument pickleStepArgument: + visitor.Visit(pickleStepArgument); + break; + case PickleTable pickleTable: + visitor.Visit(pickleTable); + break; + case PickleTableRow pickleTableRow: + visitor.Visit(pickleTableRow); + break; + case PickleTableCell pickleTableCell: + visitor.Visit(pickleTableCell); + break; + case PickleTag pickleTag: + visitor.Visit(pickleTag); + break; + case TestCase testCase: + visitor.Visit(testCase); + break; + case TestCaseStarted testCaseStarted: + visitor.Visit(testCaseStarted); + break; + case TestCaseFinished testCaseFinished: + visitor.Visit(testCaseFinished); + break; + case TestStep testStep: + visitor.Visit(testStep); + break; + case TestStepStarted testStepStarted: + visitor.Visit(testStepStarted); + break; + case TestStepFinished testStepFinished: + visitor.Visit(testStepFinished); + break; + case TestStepResult testStepResult: + visitor.Visit(testStepResult); + break; + case Hook hook: + visitor.Visit(hook); + break; + case StepDefinition stepDefinition: + visitor.Visit(stepDefinition); + break; + case ParameterType parameterType: + visitor.Visit(parameterType); + break; + case UndefinedParameterType undefinedParameterType: + visitor.Visit(undefinedParameterType); + break; + case SourceReference sourceReference: + visitor.Visit(sourceReference); + break; + case Duration duration: + visitor.Visit(duration); + break; + case Timestamp timestamp: + visitor.Visit(timestamp); + break; + case Io.Cucumber.Messages.Types.Exception exception: + visitor.Visit(exception); + break; + case Meta meta: + visitor.Visit(meta); + break; + case Product product: + visitor.Visit(product); + break; + case Ci ci: + visitor.Visit(ci); + break; + case Git git: + visitor.Visit(git); + break; + case Source source: + visitor.Visit(source); + break; + case Comment comment: + visitor.Visit(comment); + break; + case Io.Cucumber.Messages.Types.DataTable dataTable: + visitor.Visit(dataTable); + break; + case DocString docString: + visitor.Visit(docString); + break; + case Group group: + visitor.Visit(group); + break; + case JavaMethod javaMethod: + visitor.Visit(javaMethod); + break; + case JavaStackTraceElement javaStackTraceElement: + visitor.Visit(javaStackTraceElement); + break; + case Location location: + visitor.Visit(location); + break; + case ParseError parseError: + visitor.Visit(parseError); + break; + case PickleDocString pickleDocString: + visitor.Visit(pickleDocString); + break; + case StepDefinitionPattern stepDefinitionPattern: + visitor.Visit(stepDefinitionPattern); + break; + case StepMatchArgument stepMatchArgument: + visitor.Visit(stepMatchArgument); + break; + case StepMatchArgumentsList stepMatchArgumentsList: + visitor.Visit(stepMatchArgumentsList); + break; + case TestRunStarted testRunStarted: + visitor.Visit(testRunStarted); + break; + case TestRunFinished testRunFinished: + visitor.Visit(testRunFinished); + break; + case TestRunHookStarted testRunHookStarted: + visitor.Visit(testRunHookStarted); + break; + case TestRunHookFinished testRunHookFinished: + visitor.Visit(testRunHookFinished); + break; + + default: + throw new ArgumentException($"Unsupported message type:{message.GetType().Name}", nameof(message)); + } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs new file mode 100644 index 000000000..630ccca3f --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/CucumberMessage_TraversalVisitorBase.cs @@ -0,0 +1,856 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber +{ + /// + /// Base implementation of a visitor pattern over the Cucumber Message types + /// + /// (consumer of this, for now, is within the Test class for Messages; but provided here for future use) + /// + public abstract class CucumberMessage_TraversalVisitorBase : ICucumberMessageVisitor + { + public void Accept(object message) + { + if (message != null) CucumberMessageVisitor.Accept(this, message); + } + + public virtual void Visit(Envelope envelope) + { + OnVisiting(envelope); + Accept(envelope.Content()); + OnVisited(envelope); + } + + public virtual void Visit(Attachment attachment) + { + OnVisiting(attachment); + OnVisited(attachment); + } + + public virtual void Visit(GherkinDocument gherkinDocument) + { + OnVisiting(gherkinDocument); + + if (gherkinDocument.Comments != null) + { + foreach (var comment in gherkinDocument.Comments) + { + Accept(comment); + } + } + if (gherkinDocument.Feature != null) + Accept(gherkinDocument.Feature); + + OnVisited(gherkinDocument); + } + + public virtual void Visit(Feature feature) + { + OnVisiting(feature); + Accept(feature.Location); + foreach (var tag in feature.Tags ?? new List()) + { + Accept(tag); + } + foreach (var featureChild in feature.Children ?? new List()) + { + Accept(featureChild); + } + OnVisited(feature); + } + + public virtual void Visit(FeatureChild featureChild) + { + OnVisiting(featureChild); + if (featureChild.Rule != null) + Accept(featureChild.Rule); + else if (featureChild.Background != null) + Accept(featureChild.Background); + else if (featureChild.Scenario != null) + Accept(featureChild.Scenario); + OnVisited(featureChild); + } + + public virtual void Visit(Rule rule) + { + OnVisiting(rule); + Accept(rule.Location); + foreach (var ruleChild in rule.Children ?? new List()) + { + Accept(ruleChild); + } + foreach (var tag in rule.Tags ?? new List()) + { + Accept(tag); + } + OnVisited(rule); + } + + public virtual void Visit(RuleChild ruleChild) + { + OnVisiting(ruleChild); + if (ruleChild.Background != null) + Accept(ruleChild.Background); + else if (ruleChild.Scenario != null) + Accept(ruleChild.Scenario); + OnVisited(ruleChild); + } + + public virtual void Visit(Background background) + { + OnVisiting(background); + Accept(background.Location); + foreach (var step in background.Steps ?? new List()) + { + Accept(step); + } + OnVisited(background); + } + + public virtual void Visit(Scenario scenario) + { + OnVisiting(scenario); + Accept(scenario.Location); + foreach (var tag in scenario.Tags ?? new List()) + { + Accept(tag); + } + foreach (var step in scenario.Steps ?? new List()) + { + Accept(step); + } + foreach (var example in scenario.Examples ?? new List()) + { + Accept(example); + } + OnVisited(scenario); + } + + public virtual void Visit(Examples examples) + { + OnVisiting(examples); + Accept(examples.Location); + foreach (var tag in examples.Tags ?? new List()) + { + Accept(tag); + } + Accept(examples.TableHeader); + foreach (var tableRow in examples.TableBody ?? new List()) + { + Accept(tableRow); + } + OnVisited(examples); + } + + public virtual void Visit(Step step) + { + OnVisiting(step); + Accept(step.Location); + Accept(step.DocString); + Accept(step.DataTable); + OnVisited(step); + } + + public virtual void Visit(TableRow tableRow) + { + OnVisiting(tableRow); + Accept(tableRow.Location); + foreach (var tableCell in tableRow.Cells ?? new List()) + { + Accept(tableCell); + } + OnVisited(tableRow); + } + + public virtual void Visit(TableCell tableCell) + { + OnVisiting(tableCell); + Accept(tableCell.Location); + OnVisited(tableCell); + } + + public virtual void Visit(Tag tag) + { + OnVisiting(tag); + Accept(tag.Location); + OnVisited(tag); + } + + public virtual void Visit(Pickle pickle) + { + OnVisiting(pickle); + foreach (var pickleStep in pickle.Steps ?? new List()) + { + Accept(pickleStep); + } + foreach (var tag in pickle.Tags ?? new List()) + { + Accept(tag); + } + OnVisited(pickle); + } + + public virtual void Visit(PickleStep pickleStep) + { + OnVisiting(pickleStep); + Accept(pickleStep.Argument); + OnVisited(pickleStep); + } + + public virtual void Visit(PickleStepArgument pickleStepArgument) + { + OnVisiting(pickleStepArgument); + if (pickleStepArgument.DataTable != null) + Accept(pickleStepArgument.DataTable); + else if (pickleStepArgument.DocString != null) + Accept(pickleStepArgument.DocString); + OnVisited(pickleStepArgument); + } + + public virtual void Visit(PickleTable pickleTable) + { + OnVisiting(pickleTable); + foreach (var pickleTableRow in pickleTable.Rows ?? new List()) + { + Accept(pickleTableRow); + } + OnVisited(pickleTable); + } + + public virtual void Visit(PickleTableRow pickleTableRow) + { + OnVisiting(pickleTableRow); + foreach (var pickleTableCell in pickleTableRow.Cells ?? new List()) + { + Accept(pickleTableCell); + } + OnVisited(pickleTableRow); + } + + public virtual void Visit(PickleTableCell pickleTableCell) + { + OnVisiting(pickleTableCell); + OnVisited(pickleTableCell); + } + + public virtual void Visit(PickleTag pickleTag) + { + OnVisiting(pickleTag); + OnVisited(pickleTag); + } + + public virtual void Visit(TestCase testCase) + { + OnVisiting(testCase); + foreach (var step in testCase.TestSteps ?? new List()) + { + Accept(step); + } + OnVisited(testCase); + } + + public virtual void Visit(TestCaseStarted testCaseStarted) + { + OnVisiting(testCaseStarted); + Accept(testCaseStarted.Timestamp); + OnVisited(testCaseStarted); + } + + public virtual void Visit(TestCaseFinished testCaseFinished) + { + OnVisiting(testCaseFinished); + Accept(testCaseFinished.Timestamp); + OnVisited(testCaseFinished); + } + + public virtual void Visit(TestStep testStep) + { + OnVisiting(testStep); + foreach (var argumentList in testStep.StepMatchArgumentsLists ?? new List()) + { + Accept(argumentList); + } + OnVisited(testStep); + } + + public virtual void Visit(TestStepStarted testStepStarted) + { + OnVisiting(testStepStarted); + Accept(testStepStarted.Timestamp); + OnVisited(testStepStarted); + } + + public virtual void Visit(TestStepFinished testStepFinished) + { + OnVisiting(testStepFinished); + Accept(testStepFinished.TestStepResult); + Accept(testStepFinished.Timestamp); + OnVisited(testStepFinished); + } + + public virtual void Visit(TestStepResult testStepResult) + { + OnVisiting(testStepResult); + Accept(testStepResult.Duration); + Accept(testStepResult.Exception); + OnVisited(testStepResult); + } + + public virtual void Visit(Hook hook) + { + OnVisiting(hook); + Accept(hook.SourceReference); + OnVisited(hook); + } + + public virtual void Visit(StepDefinition stepDefinition) + { + OnVisiting(stepDefinition); + Accept(stepDefinition.Pattern); + Accept(stepDefinition.SourceReference); + OnVisited(stepDefinition); + } + + public virtual void Visit(ParameterType parameterType) + { + OnVisiting(parameterType); + Accept(parameterType.SourceReference); + OnVisited(parameterType); + } + + public virtual void Visit(UndefinedParameterType undefinedParameterType) + { + OnVisiting(undefinedParameterType); + OnVisited(undefinedParameterType); + } + + public virtual void Visit(SourceReference sourceReference) + { + OnVisiting(sourceReference); + if (sourceReference.Location != null) Accept(sourceReference.Location); + else if (sourceReference.JavaMethod != null) Accept(sourceReference.JavaMethod); + else if (sourceReference.JavaStackTraceElement != null) Accept(sourceReference.JavaStackTraceElement); + OnVisited(sourceReference); + } + + public virtual void Visit(Duration duration) + { + OnVisiting(duration); + OnVisited(duration); + } + + public virtual void Visit(Timestamp timestamp) + { + OnVisiting(timestamp); + OnVisited(timestamp); + } + + public virtual void Visit(Io.Cucumber.Messages.Types.Exception exception) + { + OnVisiting(exception); + OnVisited(exception); + } + + public virtual void Visit(Meta meta) + { + OnVisiting(meta); + Accept(meta.Implementation); + Accept(meta.Runtime); + Accept(meta.Os); + Accept(meta.Cpu); + Accept(meta.Ci); + OnVisited(meta); + } + + public virtual void Visit(Product product) + { + OnVisiting(product); + OnVisited(product); + } + + public virtual void Visit(Ci ci) + { + OnVisiting(ci); + Accept(ci.Git); + OnVisited(ci); + } + + public virtual void Visit(Git git) + { + OnVisiting(git); + OnVisited(git); + } + + public virtual void Visit(Source source) + { + OnVisiting(source); + OnVisited(source); + } + + public virtual void Visit(Comment comment) + { + OnVisiting(comment); + Accept(comment.Location); + OnVisited(comment); + } + + public virtual void Visit(Io.Cucumber.Messages.Types.DataTable dataTable) + { + OnVisiting(dataTable); + Accept(dataTable.Location); + foreach (var row in dataTable.Rows ?? new List()) + { + Accept(row); + } + OnVisited(dataTable); + } + + public virtual void Visit(DocString docString) + { + OnVisiting(docString); + Accept(docString.Location); + OnVisited(docString); + } + + public virtual void Visit(Group group) + { + OnVisiting(group); + foreach (var child in group.Children ?? new List()) + { + Accept(child); + } + OnVisited(group); + } + + public virtual void Visit(JavaMethod javaMethod) + { + OnVisiting(javaMethod); + OnVisited(javaMethod); + } + + public virtual void Visit(JavaStackTraceElement javaStackTraceElement) + { + OnVisiting(javaStackTraceElement); + OnVisited(javaStackTraceElement); + } + + public virtual void Visit(Location location) + { + OnVisiting(location); + OnVisited(location); + } + + public virtual void Visit(ParseError parseError) + { + OnVisiting(parseError); + Accept(parseError.Source); + OnVisited(parseError); + } + + public virtual void Visit(PickleDocString pickleDocString) + { + OnVisiting(pickleDocString); + OnVisited(pickleDocString); + } + + public virtual void Visit(StepDefinitionPattern stepDefinitionPattern) + { + OnVisiting(stepDefinitionPattern); + OnVisited(stepDefinitionPattern); + } + + public virtual void Visit(StepMatchArgument stepMatchArgument) + { + OnVisiting(stepMatchArgument); + Accept(stepMatchArgument.Group); + OnVisited(stepMatchArgument); + } + + public virtual void Visit(StepMatchArgumentsList stepMatchArgumentsList) + { + OnVisiting(stepMatchArgumentsList); + foreach (var stepMatchArgument in stepMatchArgumentsList.StepMatchArguments ?? new List()) + { + Accept(stepMatchArgument); + } + OnVisited(stepMatchArgumentsList); + } + + public virtual void Visit(TestRunStarted testRunStarted) + { + OnVisiting(testRunStarted); + Accept(testRunStarted.Timestamp); + OnVisited(testRunStarted); + } + + public virtual void Visit(TestRunFinished testRunFinished) + { + OnVisiting(testRunFinished); + Accept(testRunFinished.Timestamp); + Accept(testRunFinished.Exception); + OnVisited(testRunFinished); + } + + public virtual void Visit(TestRunHookStarted testRunHookStarted) + { + OnVisiting(testRunHookStarted); + Accept(testRunHookStarted.Timestamp); + OnVisited(testRunHookStarted); + } + + public void Visit(TestRunHookFinished testRunHookFinished) + { + OnVisiting(testRunHookFinished); + Accept(testRunHookFinished.Result); + Accept(testRunHookFinished.Timestamp); + OnVisited(testRunHookFinished); + } + + public virtual void OnVisiting(Attachment attachment) + { } + + public virtual void OnVisited(Attachment attachment) + { } + + public virtual void OnVisiting(Envelope envelope) + { } + + public virtual void OnVisited(Envelope envelope) + { } + + public virtual void OnVisiting(Feature feature) + { } + + public virtual void OnVisited(Feature feature) + { } + + public virtual void OnVisiting(FeatureChild featureChild) + { } + + public virtual void OnVisited(FeatureChild featureChild) + { } + + public virtual void OnVisiting(Examples examples) + { } + + public virtual void OnVisited(Examples examples) + { } + + public virtual void OnVisiting(Step step) + { } + + public virtual void OnVisited(Step step) + { } + + public virtual void OnVisiting(TableRow tableRow) + { } + + public virtual void OnVisited(TableRow tableRow) + { } + + public virtual void OnVisiting(TableCell tableCell) + { } + + public virtual void OnVisited(TableCell tableCell) + { } + + public virtual void OnVisiting(Tag tag) + { } + + public virtual void OnVisited(Tag tag) + { } + + public virtual void OnVisiting(Pickle pickle) + { } + + public virtual void OnVisited(Pickle pickle) + { } + + public virtual void OnVisiting(PickleStep pickleStep) + { } + + public virtual void OnVisited(PickleStep pickleStep) + { } + + public virtual void OnVisiting(PickleStepArgument pickleStepArgument) + { } + + public virtual void OnVisited(PickleStepArgument pickleStepArgument) + { } + + public virtual void OnVisiting(PickleTable pickleTable) + { } + + public virtual void OnVisited(PickleTable pickleTable) + { } + + public virtual void OnVisiting(PickleTableRow pickleTableRow) + { } + + public virtual void OnVisited(PickleTableRow pickleTableRow) + { } + + public virtual void OnVisiting(PickleTableCell pickleTableCell) + { } + + public virtual void OnVisited(PickleTableCell pickleTableCell) + { } + + public virtual void OnVisiting(PickleTag pickelTag) + { } + + public virtual void OnVisited(PickleTag pickelTag) + { } + + public virtual void OnVisiting(Rule rule) + { } + + public virtual void OnVisited(Rule rule) + { } + + public virtual void OnVisiting(RuleChild ruleChild) + { } + + public virtual void OnVisited(RuleChild ruleChild) + { } + + public virtual void OnVisiting(Background background) + { } + + public virtual void OnVisited(Background background) + { } + + public virtual void OnVisiting(Scenario scenario) + { } + + public virtual void OnVisited(Scenario scenario) + { } + + public virtual void OnVisiting(GherkinDocument gherkinDocument) + { } + + public virtual void OnVisited(GherkinDocument gherkinDocument) + { } + + public virtual void OnVisiting(TestCaseFinished testCaseFinished) + { } + + public virtual void OnVisited(TestCaseFinished testCaseFinished) + { } + + public virtual void OnVisiting(TestCaseStarted testCaseStarted) + { } + + public virtual void OnVisited(TestCaseStarted testCaseStarted) + { } + + public virtual void OnVisiting(TestStep testStep) + { } + + public virtual void OnVisited(TestStep testStep) + { } + + public virtual void OnVisiting(TestStepFinished testStepFinished) + { } + + public virtual void OnVisited(TestStepFinished testStepFinished) + { } + + public virtual void OnVisiting(TestStepStarted testStepStarted) + { } + + public virtual void OnVisited(TestStepStarted testStepStarted) + { } + + public virtual void OnVisiting(TestStepResult testStepResult) + { } + + public virtual void OnVisited(TestStepResult testStepResult) + { } + + public virtual void OnVisiting(TestCase testCase) + { } + + public virtual void OnVisited(TestCase testCase) + { } + + public virtual void OnVisiting(StepDefinition stepDefinition) + { } + + public virtual void OnVisited(StepDefinition stepDefinition) + { } + + public virtual void OnVisiting(UndefinedParameterType undefinedParameterType) + { } + + public virtual void OnVisited(UndefinedParameterType undefinedParameterType) + { } + + public virtual void OnVisiting(ParameterType parameterType) + { } + + public virtual void OnVisited(ParameterType parameterType) + { } + + public virtual void OnVisiting(ParseError parseError) + { } + + public virtual void OnVisited(ParseError parseError) + { } + + public virtual void OnVisiting(Source source) + { } + + public virtual void OnVisited(Source source) + { } + + public virtual void OnVisiting(Hook hook) + { } + + public virtual void OnVisited(Hook hook) + { } + + public virtual void OnVisiting(Meta meta) + { } + + public virtual void OnVisited(Meta meta) + { } + + public virtual void OnVisiting(Ci ci) + { } + + public virtual void OnVisited(Ci ci) + { } + + public virtual void OnVisiting(Comment comment) + { } + + public virtual void OnVisited(Comment comment) + { } + + public virtual void OnVisiting(DocString docString) + { } + + public virtual void OnVisited(DocString docString) + { } + + public virtual void OnVisiting(Duration duration) + { } + + public virtual void OnVisited(Duration duration) + { } + + public virtual void OnVisiting(Io.Cucumber.Messages.Types.DataTable dataTable) + { } + + public virtual void OnVisited(Io.Cucumber.Messages.Types.DataTable dataTable) + { } + + public virtual void OnVisiting(Io.Cucumber.Messages.Types.Exception exception) + { } + + public virtual void OnVisited(Io.Cucumber.Messages.Types.Exception exception) + { } + + public virtual void OnVisiting(JavaMethod javaMethod) + { } + + public virtual void OnVisited(JavaMethod javaMethod) + { } + + public virtual void OnVisiting(JavaStackTraceElement javaStackTraceElement) + { } + + public virtual void OnVisited(JavaStackTraceElement javaStackTraceElement) + { } + + public virtual void OnVisiting(Location location) + { } + + public virtual void OnVisited(Location location) + { } + + public virtual void OnVisiting(Product product) + { } + + public virtual void OnVisited(Product product) + { } + + public virtual void OnVisiting(SourceReference sourceReference) + { } + + public virtual void OnVisited(SourceReference sourceReference) + { } + + public virtual void OnVisiting(StepDefinitionPattern stepDefinitionPattern) + { } + + public virtual void OnVisited(StepDefinitionPattern stepDefinitionPattern) + { } + + public virtual void OnVisiting(StepMatchArgument stepMatchArgument) + { } + + public virtual void OnVisited(StepMatchArgument stepMatchArgument) + { } + + public virtual void OnVisiting(StepMatchArgumentsList stepMatchArgumentsList) + { } + + public virtual void OnVisited(StepMatchArgumentsList stepMatchArgumentsList) + { } + + public virtual void OnVisiting(Timestamp timestamp) + { } + + public virtual void OnVisited(Timestamp timestamp) + { } + + public virtual void OnVisiting(Git git) + { } + + public virtual void OnVisited(Git git) + { } + + public virtual void OnVisiting(Group group) + { } + + public virtual void OnVisited(Group group) + { } + + public virtual void OnVisiting(PickleDocString pickleDocString) + { } + + public virtual void OnVisited(PickleDocString pickleDocString) + { } + + public virtual void OnVisiting(TestRunStarted testRunStarted) + { } + + public virtual void OnVisited(TestRunStarted testRunStarted) + { } + + public virtual void OnVisiting(TestRunFinished testRunFinished) + { } + + public virtual void OnVisited(TestRunFinished testRunFinished) + { } + + public virtual void OnVisiting(TestRunHookStarted testRunHookStarted) + { } + + public virtual void OnVisited(TestRunHookStarted testRunHookStarted) + { } + + public virtual void OnVisiting(TestRunHookFinished testRunHookFinished) + { } + + public virtual void OnVisited(TestRunHookFinished testRunHookFinished) + { } + } +} \ No newline at end of file diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/ICucumberMessageVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/ICucumberMessageVisitor.cs new file mode 100644 index 000000000..51664d2d0 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Cucumber/ICucumberMessageVisitor.cs @@ -0,0 +1,68 @@ +using Io.Cucumber.Messages.Types; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; + +// This interface is used to support the implementation of an External Vistor pattern against the Cucumber Messages. +// Visitors impmlement this interface and then invoke it using the helper class below. + +public interface ICucumberMessageVisitor +{ + // Existing methods + void Visit(Envelope envelope); + void Visit(Attachment attachment); + void Visit(GherkinDocument gherkinDocument); + void Visit(Feature feature); + void Visit(FeatureChild featureChild); + void Visit(Rule rule); + void Visit(RuleChild ruleChild); + void Visit(Background background); + void Visit(Scenario scenario); + void Visit(Examples examples); + void Visit(Step step); + void Visit(TableRow tableRow); + void Visit(TableCell tableCell); + void Visit(Tag tag); + void Visit(Pickle pickle); + void Visit(PickleStep pickleStep); + void Visit(PickleStepArgument pickleStepArgument); + void Visit(PickleTable pickleTable); + void Visit(PickleTableRow pickleTableRow); + void Visit(PickleTableCell pickleTableCell); + void Visit(PickleTag pickleTag); + void Visit(TestCase testCase); + void Visit(TestCaseStarted testCaseStarted); + void Visit(TestCaseFinished testCaseFinished); + void Visit(TestStep testStep); + void Visit(TestStepStarted testStepStarted); + void Visit(TestStepFinished testStepFinished); + void Visit(TestStepResult testStepResult); + void Visit(Hook hook); + void Visit(StepDefinition stepDefinition); + void Visit(ParameterType parameterType); + void Visit(UndefinedParameterType undefinedParameterType); + void Visit(SourceReference sourceReference); + void Visit(Duration duration); + void Visit(Timestamp timestamp); + void Visit(Exception exception); + void Visit(Meta meta); + void Visit(Product product); + void Visit(Ci ci); + void Visit(Git git); + void Visit(Source source); + void Visit(Comment comment); + void Visit(Io.Cucumber.Messages.Types.DataTable dataTable); + void Visit(DocString docString); + void Visit(Group group); + void Visit(JavaMethod javaMethod); + void Visit(JavaStackTraceElement javaStackTraceElement); + void Visit(Location location); + void Visit(ParseError parseError); + void Visit(PickleDocString pickleDocString); + void Visit(StepDefinitionPattern stepDefinitionPattern); + void Visit(StepMatchArgument stepMatchArgument); + void Visit(StepMatchArgumentsList stepMatchArgumentsList); + void Visit(TestRunStarted testRunStarted); + void Visit(TestRunFinished testRunFinished); + void Visit(TestRunHookStarted testRunHookStarted); + void Visit(TestRunHookFinished testRunHookFinished); +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/FileExtensionToMIMETypeMap.cs b/Reqnroll/CucumberMessages/PayloadProcessing/FileExtensionToMIMETypeMap.cs new file mode 100644 index 000000000..9a70e7689 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/FileExtensionToMIMETypeMap.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reqnroll.CucumberMessages.PayloadProcessing +{ + public static class FileExtensionToMIMETypeMap + { + public static string GetMimeType(string extension) + { + if (ExtensionToMimeType.TryGetValue(extension.ToLower(), out var mimeType)) + return mimeType; + + return "application/octet-stream"; + } + + // Source of this list: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + public static readonly Dictionary ExtensionToMimeType = new Dictionary + { + {".aac", "audio/aac"}, + {".abw", "application/x-abiword"}, + {".apng", "image/apng"}, + {".arc", "application/x-freearc"}, + {".avif", "image/avif"}, + {".avi", "video/x-msvideo"}, + {".azw", "application/vnd.amazon.ebook"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".bz", "application/x-bzip"}, + {".bz2", "application/x-bzip2"}, + {".cda", "application/x-cdf"}, + {".csh", "application/x-csh"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".doc", "application/msword"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".eot", "application/vnd.ms-fontobject"}, + {".epub", "application/epub+zip"}, + {".gz", "application/gzip"}, + {".gif", "image/gif"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".ico", "image/vnd.microsoft.icon"}, + {".ics", "text/calendar"}, + {".jar", "application/java-archive"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "text/javascript"}, + {".json", "application/json"}, + {".jsonld", "application/ld+json"}, + {".mid", "audio/midi"}, + {".midi", "audio/midi"}, + {".mjs", "text/javascript"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mpeg", "video/mpeg"}, + {".mpkg", "application/vnd.apple.installer+xml"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".oga", "audio/ogg"}, + {".ogv", "video/ogg"}, + {".ogx", "application/ogg"}, + {".opus", "audio/ogg"}, + {".otf", "font/otf"}, + {".png", "image/png"}, + {".pdf", "application/pdf"}, + {".php", "application/x-httpd-php"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".rar", "application/vnd.rar"}, + {".rtf", "application/rtf"}, + {".sh", "application/x-sh"}, + {".svg", "image/svg+xml"}, + {".tar", "application/x-tar"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".ts", "video/mp2t"}, + {".ttf", "font/ttf"}, + {".txt", "text/plain"}, + {".vsd", "application/vnd.visio"}, + {".wav", "audio/wav"}, + {".weba", "audio/webm"}, + {".webm", "video/webm"}, + {".webp", "image/webp"}, + {".woff", "font/woff"}, + {".woff2", "font/woff2"}, + {".xhtml", "application/xhtml+xml"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xml", "application/xml"}, + {".xul", "application/vnd.mozilla.xul+xml"}, + {".zip", "application/zip"}, + {".3gp", "video/3gpp"}, + {".3g2", "video/3gpp2"}, + {".7z", "application/x-7z-compressed"} + }; + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs new file mode 100644 index 000000000..63275849e --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesGherkinDocumentVisitor.cs @@ -0,0 +1,247 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using Gherkin.CucumberMessages.Types; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + /// + /// Absstract base class for implementing a visitor for + /// + abstract class GherkinTypesGherkinDocumentVisitor + { + protected virtual void AcceptDocument(GherkinDocument document) + { + OnDocumentVisiting(document); + if (document.Feature != null) + { + AcceptFeature(document.Feature); + } + OnDocumentVisited(document); + } + + protected virtual void AcceptFeature(Feature feature) + { + OnFeatureVisiting(feature); + foreach (var tag in feature.Tags) + AcceptTag(tag); + + foreach (var featureChild in feature.Children) + { + if (featureChild.Rule != null) AcceptRule(featureChild.Rule); + else if (featureChild.Background != null) AcceptBackground(featureChild.Background); + else if (featureChild.Scenario != null && IsScenarioOutline(featureChild.Scenario)) AcceptScenarioOutline(featureChild.Scenario); + else if (featureChild.Scenario != null) AcceptScenario(featureChild.Scenario); + } + OnFeatureVisited(feature); + } + private bool IsScenarioOutline(Scenario scenario) + { + return scenario.Examples != null && scenario.Examples.Any(); + } + protected virtual void AcceptStep(Step step) + { + OnStepVisiting(step); + if (step.DataTable != null) + AcceptDataTable(step.DataTable); + OnStepVisited(step); + } + + protected virtual void AcceptDataTable(global::Gherkin.CucumberMessages.Types.DataTable dataTable) + { + OnDataTableVisiting(dataTable); + foreach (var row in dataTable.Rows) + { + AcceptTableRow(row); + } + OnDataTableVisited(dataTable); + } + + protected virtual void AcceptScenario(Scenario scenario) + { + OnScenarioVisiting(scenario); + NavigateScenarioInner(scenario); + OnScenarioVisited(scenario); + } + + private void NavigateScenarioInner(Scenario scenario) + { + foreach (var tag in scenario.Tags) + { + AcceptTag(tag); + } + foreach (var step in scenario.Steps) + { + AcceptStep(step); + } + } + + protected virtual void AcceptScenarioOutline(Scenario scenarioOutline) + { + OnScenarioOutlineVisiting(scenarioOutline); + foreach (var examples in scenarioOutline.Examples) + { + AcceptExamples(examples); + } + + NavigateScenarioInner(scenarioOutline); + OnScenarioOutlineVisited(scenarioOutline); + } + + protected virtual void AcceptBackground(Background background) + { + OnBackgroundVisiting(background); + foreach (var step in background.Steps) + { + AcceptStep(step); + } + OnBackgroundVisited(background); + } + + protected virtual void AcceptRule(Rule rule) + { + OnRuleVisiting(rule); + foreach (var tag in rule.Tags) + AcceptTag(tag); + + foreach (var ruleChild in rule.Children) + { + if (ruleChild.Background != null) AcceptBackground(ruleChild.Background); + else if (ruleChild.Scenario != null && IsScenarioOutline(ruleChild.Scenario)) AcceptScenarioOutline(ruleChild.Scenario); + else if (ruleChild.Scenario != null) AcceptScenario(ruleChild.Scenario); + } + OnRuleVisited(rule); + } + + protected virtual void AcceptTableRow(TableRow row) + { + OnTableRowVisited(row); + } + + protected virtual void AcceptTag(Tag tag) + { + OnTagVisited(tag); + } + + protected virtual void AcceptExamples(Examples examples) + { + OnExamplesVisiting(examples); + foreach (var tag in examples.Tags) + AcceptTag(tag); + AcceptTableHeader(examples.TableHeader); + foreach (var row in examples.TableBody) + AcceptTableRow(row); + OnExamplesVisited(examples); + } + + protected virtual void AcceptTableHeader(TableRow header) + { + OnTableHeaderVisited(header); + } + + protected virtual void OnDocumentVisiting(GherkinDocument document) + { + //nop + } + + protected virtual void OnDocumentVisited(GherkinDocument document) + { + //nop + } + + protected virtual void OnFeatureVisiting(Feature feature) + { + //nop + } + + protected virtual void OnFeatureVisited(Feature feature) + { + //nop + } + + protected virtual void OnBackgroundVisiting(Background background) + { + //nop + } + + protected virtual void OnBackgroundVisited(Background background) + { + //nop + } + + protected virtual void OnRuleVisiting(Rule rule) + { + //nop + } + + protected virtual void OnRuleVisited(Rule rule) + { + //nop + } + + protected virtual void OnScenarioOutlineVisiting(Scenario scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioOutlineVisited(Scenario scenarioOutline) + { + //nop + } + + protected virtual void OnScenarioVisiting(Scenario scenario) + { + //nop + } + + protected virtual void OnScenarioVisited(Scenario scenario) + { + //nop + } + + protected virtual void OnStepVisiting(Step step) + { + //nop + } + + protected virtual void OnStepVisited(Step step) + { + //nop + } + + protected virtual void OnDataTableVisiting(global::Gherkin.CucumberMessages.Types.DataTable dataTable) + { + //nop + } + + protected virtual void OnDataTableVisited(global::Gherkin.CucumberMessages.Types.DataTable dataTable) + { + //nop + } + + protected virtual void OnTableRowVisited(TableRow row) + { + //nop + } + + protected virtual void OnTagVisited(Tag tag) + { + //nop + } + + protected virtual void OnExamplesVisiting(Examples examples) + { + //nop + } + + protected virtual void OnExamplesVisited(Examples examples) + { + //nop + } + + protected virtual void OnTableHeaderVisited(TableRow header) + { + //nop + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs new file mode 100644 index 000000000..ecd53fb45 --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/Gherkin/GherkinTypesPickleVisitor.cs @@ -0,0 +1,133 @@ +using Gherkin.CucumberMessages.Types; +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; + +namespace Reqnroll.CucumberMessages.PayloadProcessing.Gherkin +{ + /// + /// Abstract base class for visiting Gherkin Pickle (and nested types) + /// + abstract class GherkinTypesPickleVisitor + { + + public virtual void AcceptPickle(Pickle pickle) + { + OnVisitingPickle(pickle); + + foreach (var tag in pickle.Tags) + { + AcceptTag(tag); + } + foreach (var step in pickle.Steps) + { + AcceptStep(step); + } + OnVisitedPickle(pickle); + } + + protected virtual void AcceptStep(PickleStep step) + { + OnVisitingPickleStep(step); + AcceptPickleStepArgument(step.Argument); + OnVisitedPickleStep(step); + } + + protected virtual void AcceptPickleStepArgument(PickleStepArgument argument) + { + OnVisitingPickleStepArgument(argument); + AcceptPickleTable(argument.DataTable); + OnVisitedPickleStepArgument(argument); + } + + protected virtual void AcceptPickleTable(PickleTable dataTable) + { + OnVisitingPickleTable(dataTable); + foreach (var row in dataTable.Rows) + { + AcceptPickleTableRow(row); + } + OnVisitedPickleTable(dataTable); + } + + protected virtual void AcceptPickleTableRow(PickleTableRow row) + { + OnVisitingPickleTableRow(row); + foreach (var cell in row.Cells) + { + AcceptPickleTableCell(cell); + } + OnVisitedPickleTableRow(row); + } + + protected virtual void AcceptPickleTableCell(PickleTableCell cell) + { + OnVisitedPickleTableCell(cell); + } + protected virtual void AcceptTag(PickleTag tag) + { + OnVisitedPickleTag(tag); + } + + protected virtual void OnVisitingPickle(Pickle pickle) + { + //nop + } + + protected virtual void OnVisitedPickle(Pickle pickle) + { + //nop + } + + protected virtual void OnVisitedPickleTag(PickleTag tag) + { + //nop + } + + protected virtual void OnVisitingPickleStep(PickleStep step) + { + //nop + } + + protected virtual void OnVisitedPickleStep(PickleStep step) + { + //nop + } + + protected virtual void OnVisitingPickleStepArgument(PickleStepArgument argument) + { + //nop + } + + protected virtual void OnVisitedPickleStepArgument(PickleStepArgument argument) + { + //nop + } + + protected virtual void OnVisitingPickleTable(PickleTable dataTable) + { + //nop + } + + protected virtual void OnVisitedPickleTable(PickleTable dataTable) + { + //nop + } + + protected virtual void OnVisitingPickleTableRow(PickleTableRow row) + { + //nop + } + + protected virtual void OnVisitedPickleTableRow(PickleTableRow row) + { + //nop + } + + protected virtual void OnVisitedPickleTableCell(PickleTableCell cell) + { + //nop + } + } +} diff --git a/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs new file mode 100644 index 000000000..0d9daf9ad --- /dev/null +++ b/Reqnroll/CucumberMessages/PayloadProcessing/NdjsonSerializer.cs @@ -0,0 +1,66 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using System; +using System.IO; +using System.Text.Json; + +namespace Reqnroll.CucumberMessages.PayloadProcessing +{ + /// + /// When using System.Text.Json to serialize a Cucumber Message Envelope, the following serialization options are used. + /// Consumers of Cucumber.Messages should use these options, or their serialization library's equivalent options. + /// These options should work with System.Text.Json v6 or above. + /// + public class NdjsonSerializer + { + private static readonly Lazy _jsonOptions = new(() => + { + var options = new JsonSerializerOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.Converters.Add(new CucumberMessageEnumConverter()); + options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + + return options; + }); + + private static JsonSerializerOptions JsonOptions + { + get + { + return _jsonOptions.Value; + } + } + + public static string Serialize(Envelope message) + { + return Serialize(message); + } + + internal static string Serialize(T message) + { + return JsonSerializer.Serialize(message, JsonOptions); + } + + public static Envelope Deserialize(string json) + { + return Deserialize(json); + } + + internal static T Deserialize(string json) + { + return JsonSerializer.Deserialize(json, JsonOptions)!; + } + + public static void SerializeToStream(Stream fs, Envelope message) + { + JsonSerializer.Serialize(fs, message, JsonOptions); + } + } +} diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs new file mode 100644 index 000000000..8b8b86cd4 --- /dev/null +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessageBroker.cs @@ -0,0 +1,50 @@ +using Reqnroll.BoDi; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Reqnroll.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication.ExtendedProtection; +using System.Text; +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMessages.PubSub +{ + + public interface ICucumberMessageBroker + { + bool Enabled { get; } + Task PublishAsync(ReqnrollCucumberMessage featureMessages); + } + + /// + /// Cucumber Message implementation is a simple Pub/Sub implementation. + /// This broker mediates between the (singleton) CucumberMessagePublisher and (one or more) CucumberMessageSinks + /// + /// The pub/sub mechanism is considered to be turned "OFF" if no sinks are registered + /// + public class CucumberMessageBroker : ICucumberMessageBroker + { + private IObjectContainer _objectContainer; + + public bool Enabled => RegisteredSinks.Value.ToList().Count > 0; + + private Lazy> RegisteredSinks; + + public CucumberMessageBroker(IObjectContainer objectContainer) + { + _objectContainer = objectContainer; + RegisteredSinks = new Lazy>(() => _objectContainer.ResolveAll()); + } + public async Task PublishAsync(ReqnrollCucumberMessage message) + { + foreach (var sink in RegisteredSinks.Value) + { + await sink.PublishAsync(message); + } + } + + } +} diff --git a/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs new file mode 100644 index 000000000..1a91b21fa --- /dev/null +++ b/Reqnroll/CucumberMessages/PubSub/CucumberMessagePublisher.cs @@ -0,0 +1,501 @@ +using Reqnroll.BoDi; +using Reqnroll.Events; +using Reqnroll.Tracing; +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using System.Collections.Concurrent; +using System; +using System.Linq; +using Reqnroll.CucumberMessages.ExecutionTracking; +using Reqnroll.CucumberMessages.PayloadProcessing.Cucumber; +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.RuntimeSupport; +using Reqnroll.CucumberMessages.Configuration; +using Gherkin.CucumberMessages; +using Reqnroll.Bindings; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using Cucumber.Messages; +using System.Diagnostics; + +namespace Reqnroll.CucumberMessages.PubSub +{ + /// + /// Cucumber Message Publisher + /// This class is responsible for publishing CucumberMessages to the CucumberMessageBroker + /// + /// It uses the set of ExecutionEvents to track overall execution of Features and steps and drive generation of messages + /// + /// It uses the IRuntimePlugin interface to force the runtime to load it during startup (although it is not an external plugin per se). + /// + public class CucumberMessagePublisher : IRuntimePlugin, IAsyncExecutionEventListener + { + private Lazy _brokerFactory; + private ICucumberMessageBroker _broker; + private IObjectContainer _testThreadObjectContainer; + + public static object _lock = new object(); + + // Started Features by name + private ConcurrentDictionary _startedFeatures = new(); + + // This dictionary tracks the StepDefintions(ID) by their method signature + // used during TestCase creation to map from a Step Definition binding to its ID + // shared to each Feature tracker so that we keep a single list + internal ConcurrentDictionary StepDefinitionsByPattern { get; } = new(); + private ConcurrentBag _stepArgumentTransforms = new(); + private ConcurrentBag _undefinedParameterTypeBindings = new(); + public IIdGenerator SharedIDGenerator { get; private set; } + + private string _testRunStartedId; + bool _enabled = false; + + // This tracks the set of BeforeTestRun and AfterTestRun hooks that were called during the test run + private readonly ConcurrentDictionary _testRunHookTrackers = new(); + // This tracks all Attachments and Output Events; used during publication to sequence them in the correct order. + private readonly AttachmentTracker _attachmentTracker = new(); + + // Holds all Messages that are pending publication (collected from Feature Trackers as each Feature completes) + private List _messages = new(); + + public CucumberMessagePublisher() + { + } + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) => + { + var pluginLifecycleEvents = args.ObjectContainer.Resolve(); + pluginLifecycleEvents.BeforeTestRun += PublisherStartup; + pluginLifecycleEvents.AfterTestRun += PublisherTestRunComplete; + }; + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + _testThreadObjectContainer = args.ObjectContainer; + _brokerFactory = new Lazy(() => _testThreadObjectContainer.Resolve()); + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + testThreadExecutionEventPublisher.AddAsyncListener(this); + }; + } + + public async Task OnEventAsync(IExecutionEvent executionEvent) + { + switch (executionEvent) + { + case FeatureStartedEvent featureStartedEvent: + await FeatureStartedEventHandler(featureStartedEvent); + break; + case FeatureFinishedEvent featureFinishedEvent: + await FeatureFinishedEventHandler(featureFinishedEvent); + break; + case ScenarioStartedEvent scenarioStartedEvent: + await ScenarioStartedEventHandler(scenarioStartedEvent); + break; + case ScenarioFinishedEvent scenarioFinishedEvent: + await ScenarioFinishedEventHandler(scenarioFinishedEvent); + break; + case StepStartedEvent stepStartedEvent: + await StepStartedEventHandler(stepStartedEvent); + break; + case StepFinishedEvent stepFinishedEvent: + await StepFinishedEventHandler(stepFinishedEvent); + break; + case HookBindingStartedEvent hookBindingStartedEvent: + await HookBindingStartedEventHandler(hookBindingStartedEvent); + break; + case HookBindingFinishedEvent hookBindingFinishedEvent: + await HookBindingFinishedEventHandler(hookBindingFinishedEvent); + break; + case AttachmentAddedEvent attachmentAddedEvent: + await AttachmentAddedEventHandler(attachmentAddedEvent); + break; + case OutputAddedEvent outputAddedEvent: + await OutputAddedEventHandler(outputAddedEvent); + break; + default: + break; + } + } + + // This method will get called after TestRunStartedEvent has been published and after any BeforeTestRun hooks have been called + // The TestRunStartedEvent will be used by the FileOutputPlugin to launch the File writing thread and establish Messages configuration + // Running this after the BeforeTestRun hooks will allow them to programmatically configure CucumberMessages + private void PublisherStartup(object sender, RuntimePluginBeforeTestRunEventArgs args) + { + _broker = _brokerFactory.Value; + + _enabled = _broker.Enabled; + + if (!_enabled) + { + return; + } + + SharedIDGenerator = new GuidIdGenerator(); + _testRunStartedId = SharedIDGenerator.GetNewId(); + + Task.Run(async () => + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunStarted(DateTime.Now, _testRunStartedId)) }); + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = CucumberMessageFactory.ToMeta(args.ObjectContainer) }); + foreach (var msg in PopulateBindingCachesAndGenerateBindingMessages(args.ObjectContainer)) + { + // this publishes StepDefinition, Hook, StepArgumentTransform messages + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "startup", Envelope = msg }); + } + }).Wait(); + } + private IEnumerable PopulateBindingCachesAndGenerateBindingMessages(IObjectContainer objectContainer) + { + var bindingRegistry = objectContainer.Resolve(); + + foreach (var stepTransform in bindingRegistry.GetStepTransformations()) + { + if (_stepArgumentTransforms.Contains(stepTransform)) + continue; + _stepArgumentTransforms.Add(stepTransform); + var parameterType = CucumberMessageFactory.ToParameterType(stepTransform, SharedIDGenerator); + yield return Envelope.Create(parameterType); + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => !sd.IsValid)) + { + var errmsg = binding.ErrorMessage; + if (errmsg.Contains("Undefined parameter type")) + { + var paramName = Regex.Match(errmsg, "Undefined parameter type '(.*)'").Groups[1].Value; + if (_undefinedParameterTypeBindings.Contains(binding)) + continue; + _undefinedParameterTypeBindings.Add(binding); + var undefinedParameterType = CucumberMessageFactory.ToUndefinedParameterType(binding.SourceExpression, paramName, SharedIDGenerator); + yield return Envelope.Create(undefinedParameterType); + } + } + + foreach (var binding in bindingRegistry.GetStepDefinitions().Where(sd => sd.IsValid)) + { + var pattern = CucumberMessageFactory.CanonicalizeStepDefinitionPattern(binding); + if (StepDefinitionsByPattern.ContainsKey(pattern)) + continue; + var stepDefinition = CucumberMessageFactory.ToStepDefinition(binding, SharedIDGenerator); + if (StepDefinitionsByPattern.TryAdd(pattern, stepDefinition.Id)) + { + yield return Envelope.Create(stepDefinition); + } + } + + foreach (var hookBinding in bindingRegistry.GetHooks()) + { + var hookId = CucumberMessageFactory.CanonicalizeHookBinding(hookBinding); + if (StepDefinitionsByPattern.ContainsKey(hookId)) + continue; + var hook = CucumberMessageFactory.ToHook(hookBinding, SharedIDGenerator); + if (StepDefinitionsByPattern.TryAdd(hookId, hook.Id)) + { + yield return Envelope.Create(hook); + }; + } + } + + private DateTime RetrieveDateTime(Envelope e) + { + if (e.Attachment == null) + return Converters.ToDateTime(e.Timestamp()); + // what remains is an Attachment. + // match it up with an AttachmentAddedEvent in the AttachmentTracker + // and use that event's timestamp as a proxy for the timestamp of the Attachment + // Adjust the Reqnroll ExecutionEvent's DateTime to UTC to make it comparable to the Event's timestamp (which are all in UTC) + return _attachmentTracker.FindMatchingAttachment(e.Attachment).Timestamp.ToUniversalTime(); + } + + private void PublisherTestRunComplete(object sender, RuntimePluginAfterTestRunEventArgs e) + { + if (!_enabled) + return; + var status = _startedFeatures.Values.All(f => f.FeatureExecutionSuccess); + // publish all TestCase messages + var testCaseMessages = _messages.Where(e => e.Content() is TestCase).ToList(); + // sort the remaining Messages by timestamp + var executionMessages = _messages.Except(testCaseMessages).OrderBy(e => RetrieveDateTime(e)).ToList(); + + // publish them in order to the broker + Task.Run(async () => + { + foreach (var env in testCaseMessages) + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "testCaseMessages", Envelope = env }); + } + + foreach (var env in executionMessages) + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "testExecutionMessages", Envelope = env }); + } + + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = "shutdown", Envelope = Envelope.Create(CucumberMessageFactory.ToTestRunFinished(status, DateTime.Now, _testRunStartedId)) }); + }).Wait(); + + _startedFeatures.Clear(); + } + + #region TestThreadExecutionEventPublisher Event Handling Methods + + // The following methods handle the events published by the TestThreadExecutionEventPublisher + // When one of these calls the Broker, that method is async; otherwise these are sync methods that return a completed Task (to allow them to be called async from the TestThreadExecutionEventPublisher) + private async Task FeatureStartedEventHandler(FeatureStartedEvent featureStartedEvent) + { + _broker = _brokerFactory.Value; + var traceListener = _testThreadObjectContainer.Resolve(); + + var featureName = featureStartedEvent.FeatureContext?.FeatureInfo?.Title; + + _enabled = _broker.Enabled; + if (!_enabled || String.IsNullOrEmpty(featureName)) + { + return; + } + + // The following should be thread safe when multiple instances of the Test Class are running in parallel. + // If _startedFeatures.ContainsKey returns true, then we know another instance of this Feature class has already started. We don't need a second instance of the + // FeatureTracker, and we don't want multiple copies of the static messages to be published. + if (_startedFeatures.ContainsKey(featureName)) + { + // Already started, don't repeat the following steps + return; + } + + await Task.Run(() => + { + lock (_lock) + { + // This will add a FeatureTracker to the _startedFeatures dictionary only once, and if it is enabled, it will publish the static messages shared by all steps. + + // We publish the messages before adding the featureTracker to the _startedFeatures dictionary b/c other parallel scenario threads might be running. + // We don't want them to run until after the static messages have been published (and the PickleJar has been populated as a result). + if (!_startedFeatures.ContainsKey(featureName)) + { + var ft = new FeatureTracker(featureStartedEvent, _testRunStartedId, SharedIDGenerator, StepDefinitionsByPattern); + if (ft.Enabled) + Task.Run(async () => + { + foreach (var msg in ft.StaticMessages) // Static Messages == known at compile-time (Source, Gerkhin Document, and Pickle messages) + { + await _broker.PublishAsync(new ReqnrollCucumberMessage() { CucumberMessageSource = featureName, Envelope = msg }); + } + }).Wait(); + _startedFeatures.TryAdd(featureName, ft); + } + } + }); + } + + private Task FeatureFinishedEventHandler(FeatureFinishedEvent featureFinishedEvent) + { + // For this and subsequent events, we pull up the FeatureTracker by feature name. + // If the feature name is not avaiable (such as might be the case in certain test setups), we ignore the event. + var featureName = featureFinishedEvent.FeatureContext?.FeatureInfo?.Title; + if (!_enabled || String.IsNullOrEmpty(featureName)) + { + return Task.CompletedTask; + } + if (!_startedFeatures.ContainsKey(featureName) || !_startedFeatures[featureName].Enabled) + { + return Task.CompletedTask; + } + var featureTracker = _startedFeatures[featureName]; + featureTracker.ProcessEvent(featureFinishedEvent); + foreach (var msg in featureTracker.RuntimeGeneratedMessages) + { + _messages.Add(msg); + } + + return Task.CompletedTask; + // throw an exception if any of the TestCaseCucumberMessageTrackers are not done? + + } + + private Task ScenarioStartedEventHandler(ScenarioStartedEvent scenarioStartedEvent) + { + var featureName = scenarioStartedEvent.FeatureContext?.FeatureInfo?.Title; + if (!_enabled || String.IsNullOrEmpty(featureName)) + return Task.CompletedTask; + var traceListener = _testThreadObjectContainer.Resolve(); + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + if (featureTracker.Enabled) + { + featureTracker.ProcessEvent(scenarioStartedEvent); + } + else + { + return Task.CompletedTask; + } + } + else + { + traceListener.WriteTestOutput($"Cucumber Message Publisher: ScenarioStartedEventHandler: {featureName} FeatureTracker not available"); + throw new ApplicationException("FeatureTracker not available"); + } + + return Task.CompletedTask; + } + + private Task ScenarioFinishedEventHandler(ScenarioFinishedEvent scenarioFinishedEvent) + { + var featureName = scenarioFinishedEvent.FeatureContext?.FeatureInfo?.Title; + + if (!_enabled || String.IsNullOrEmpty(featureName)) + return Task.CompletedTask; + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(scenarioFinishedEvent); + } + return Task.CompletedTask; + } + + private Task StepStartedEventHandler(StepStartedEvent stepStartedEvent) + { + var featureName = stepStartedEvent.FeatureContext?.FeatureInfo?.Title; + + if (!_enabled || String.IsNullOrEmpty(featureName)) + return Task.CompletedTask; + + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(stepStartedEvent); + } + + return Task.CompletedTask; + } + + private Task StepFinishedEventHandler(StepFinishedEvent stepFinishedEvent) + { + var featureName = stepFinishedEvent.FeatureContext?.FeatureInfo?.Title; + if (!_enabled || String.IsNullOrEmpty(featureName)) + return Task.CompletedTask; + + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(stepFinishedEvent); + } + + return Task.CompletedTask; + } + + private Task HookBindingStartedEventHandler(HookBindingStartedEvent hookBindingStartedEvent) + { + if (!_enabled) + return Task.CompletedTask; + + switch (hookBindingStartedEvent.HookBinding.HookType) + { + case Bindings.HookType.BeforeTestRun: + case Bindings.HookType.AfterTestRun: + case Bindings.HookType.BeforeFeature: + case Bindings.HookType.AfterFeature: + string hookRunStartedId = SharedIDGenerator.GetNewId(); + var signature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingStartedEvent.HookBinding); + var hookId = StepDefinitionsByPattern[signature]; + var hookTracker = new TestRunHookTracker(hookRunStartedId, hookId, hookBindingStartedEvent.Timestamp, _testRunStartedId); + _testRunHookTrackers.TryAdd(signature, hookTracker); + _messages.AddRange(hookTracker.GenerateFrom(hookBindingStartedEvent)); + return Task.CompletedTask; + + default: + var featureName = hookBindingStartedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + if (!_enabled || String.IsNullOrEmpty(featureName)) + return Task.CompletedTask; + + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(hookBindingStartedEvent); + } + break; + } + + return Task.CompletedTask; + } + + private Task HookBindingFinishedEventHandler(HookBindingFinishedEvent hookBindingFinishedEvent) + { + if (!_enabled) + return Task.CompletedTask; + + switch (hookBindingFinishedEvent.HookBinding.HookType) + { + case Bindings.HookType.BeforeTestRun: + case Bindings.HookType.AfterTestRun: + case Bindings.HookType.BeforeFeature: + case Bindings.HookType.AfterFeature: + var signature = CucumberMessageFactory.CanonicalizeHookBinding(hookBindingFinishedEvent.HookBinding); + if (!_testRunHookTrackers.TryGetValue(signature, out var hookTracker)) // should not happen + return Task.CompletedTask; + hookTracker.Duration = hookBindingFinishedEvent.Duration; + hookTracker.Exception = hookBindingFinishedEvent.HookException; + hookTracker.TimeStamp = hookBindingFinishedEvent.Timestamp; + + _messages.AddRange(hookTracker.GenerateFrom(hookBindingFinishedEvent)); + return Task.CompletedTask; + + default: + var featureName = hookBindingFinishedEvent.ContextManager?.FeatureContext?.FeatureInfo?.Title; + if (!_enabled || String.IsNullOrEmpty(featureName)) + return Task.CompletedTask; + + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(hookBindingFinishedEvent); + } + break; + } + + return Task.CompletedTask; + } + + private Task AttachmentAddedEventHandler(AttachmentAddedEvent attachmentAddedEvent) + { + if (!_enabled) + return Task.CompletedTask; + + _attachmentTracker.RecordAttachment(attachmentAddedEvent); + + var featureName = attachmentAddedEvent.FeatureInfo?.Title; + if (String.IsNullOrEmpty(featureName) || String.IsNullOrEmpty(attachmentAddedEvent.ScenarioInfo?.Title)) + { + // This is a TestRun-level attachment (not tied to any feature) + _messages.Add(Envelope.Create(CucumberMessageFactory.ToAttachment(new AttachmentAddedEventWrapper(attachmentAddedEvent, _testRunStartedId, null, null)))); + return Task.CompletedTask; + } + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(attachmentAddedEvent); + } + + return Task.CompletedTask; + } + + private Task OutputAddedEventHandler(OutputAddedEvent outputAddedEvent) + { + if (!_enabled) + return Task.CompletedTask; + + _attachmentTracker.RecordOutput(outputAddedEvent); + + var featureName = outputAddedEvent.FeatureInfo?.Title; + if (String.IsNullOrEmpty(featureName) || String.IsNullOrEmpty(outputAddedEvent.ScenarioInfo?.Title)) + { + // This is a TestRun-level attachment (not tied to any feature) or is an Output coming from a Before/AfterFeature hook + _messages.Add(Envelope.Create(CucumberMessageFactory.ToAttachment(new OutputAddedEventWrapper(outputAddedEvent, _testRunStartedId, null, null)))); + return Task.CompletedTask; + } + + if (_startedFeatures.TryGetValue(featureName, out var featureTracker)) + { + featureTracker.ProcessEvent(outputAddedEvent); + } + + return Task.CompletedTask; + } + #endregion + } +} diff --git a/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs new file mode 100644 index 000000000..f6d11ec6a --- /dev/null +++ b/Reqnroll/CucumberMessages/PubSub/FileOutputPlugin.cs @@ -0,0 +1,161 @@ +#nullable enable + +using Reqnroll.Plugins; +using Reqnroll.UnitTestProvider; +using Reqnroll.Events; +using Reqnroll.Tracing; +using Reqnroll.BoDi; +using System; +using System.Threading.Tasks; +using System.IO; +using System.Linq; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PayloadProcessing; +using System.Text; +using System.Collections.Concurrent; + + +namespace Reqnroll.CucumberMessages.PubSub +{ + /// + /// The FileOutputPlugin is the subscriber to the CucumberMessageBroker. + /// It receives Cucumber Messages and writes them to a file. + /// + /// + public class FileOutputPlugin : ICucumberMessageSink, IDisposable, IRuntimePlugin + { + private Task? fileWritingTask; + + //Thread safe collections to hold: + // 1. Inbound Cucumber Messages - BlockingCollection + private readonly BlockingCollection _postedMessages = new(); + + private ICucumberMessagesConfiguration _configuration; + private Lazy traceListener; + private ITraceListener? trace => traceListener.Value; + private IObjectContainer? testThreadObjectContainer; + private IObjectContainer? globalObjectContainer; + + public FileOutputPlugin(ICucumberMessagesConfiguration configuration) + { + _configuration = configuration; + traceListener = new Lazy(() => testThreadObjectContainer!.Resolve()); + } + + public void Initialize(RuntimePluginEvents runtimePluginEvents, RuntimePluginParameters runtimePluginParameters, UnitTestProviderConfiguration unitTestProviderConfiguration) + { + runtimePluginEvents.CustomizeGlobalDependencies += (sender, args) => + { + globalObjectContainer = args.ObjectContainer; + }; + + runtimePluginEvents.CustomizeTestThreadDependencies += (sender, args) => + { + var testThreadExecutionEventPublisher = args.ObjectContainer.Resolve(); + testThreadObjectContainer = args.ObjectContainer; + testThreadExecutionEventPublisher.AddHandler(LaunchFileSink); + testThreadExecutionEventPublisher.AddHandler(Close); + }; + } + + private void Close(TestRunFinishedEvent @event) + { + // Dispose will call CloseFileSink and CloseStream. + // The former will shut down the message pipe and wait for the writer to complete. + // The latter will close down the file stream. + Dispose(true); + } + + private const int TUNING_PARAM_FILE_WRITE_BUFFER_SIZE = 65536; + private void LaunchFileSink(TestRunStartedEvent testRunStarted) + { + ICucumberMessagesConfiguration config = _configuration; + + if (!config.Enabled) + { + // By returning here, we don't launch the File writing thread, + // and this class is not registered as a CucumberMessageSink, which indicates to the Broker that Messages are disabled. + return; + } + string baseDirectory = Path.GetDirectoryName(config.OutputFilePath); + string fileName = SanitizeFileName(Path.GetFileName(config.OutputFilePath)); + string outputPath = Path.Combine(baseDirectory, fileName); + fileWritingTask = Task.Factory.StartNew(() => ConsumeAndWriteToFilesBackgroundTask(outputPath), TaskCreationOptions.LongRunning); + + globalObjectContainer!.RegisterInstanceAs(this, "CucumberMessages_FileOutputPlugin", true); + } + private static byte[] nl = Encoding.UTF8.GetBytes(Environment.NewLine); + public async Task PublishAsync(ReqnrollCucumberMessage message) + { + await Task.Run( () => _postedMessages.Add(message)); + } + + private void ConsumeAndWriteToFilesBackgroundTask(string outputPath) + { + using var fileStream = File.Create(outputPath, TUNING_PARAM_FILE_WRITE_BUFFER_SIZE); + + foreach (var message in _postedMessages.GetConsumingEnumerable()) + { + if (message.Envelope != null) + { + NdjsonSerializer.SerializeToStream(fileStream!, message.Envelope); + + // Write a newline after each message, except for the last one + if(message.Envelope.TestRunFinished == null) + fileStream!.Write(nl, 0, nl.Length); + } + } + + } + + private bool disposedValue = false; + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _postedMessages.CompleteAdding(); + fileWritingTask?.Wait(); + fileWritingTask = null; + _postedMessages.Dispose(); + } + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + private static string SanitizeFileName(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + // Get the invalid characters for file names + char[] invalidChars = Path.GetInvalidFileNameChars(); + + // Replace invalid characters with underscores + string sanitized = new string(input.Select(c => invalidChars.Contains(c) ? '_' : c).ToArray()); + + // Remove leading and trailing spaces and dots + sanitized = sanitized.Trim().Trim('.'); + + // Ensure the filename is not empty after sanitization + if (string.IsNullOrEmpty(sanitized)) + return "_"; + + // Truncate the filename if it's too long (255 characters is a common limit) + const int maxLength = 255; + if (sanitized.Length > maxLength) + sanitized = sanitized.Substring(0, maxLength); + + return sanitized; + } + + } +} diff --git a/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs new file mode 100644 index 000000000..df04e6243 --- /dev/null +++ b/Reqnroll/CucumberMessages/PubSub/ICucumberMessageSink.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + + +namespace Reqnroll.CucumberMessages.PubSub +{ + public interface ICucumberMessageSink + { + Task PublishAsync(ReqnrollCucumberMessage message); + } +} diff --git a/Reqnroll/CucumberMessages/PubSub/ReqnrollCucumberMessage.cs b/Reqnroll/CucumberMessages/PubSub/ReqnrollCucumberMessage.cs new file mode 100644 index 000000000..6fc5412e8 --- /dev/null +++ b/Reqnroll/CucumberMessages/PubSub/ReqnrollCucumberMessage.cs @@ -0,0 +1,11 @@ +using Io.Cucumber.Messages.Types; + + +namespace Reqnroll.CucumberMessages.PubSub +{ + public class ReqnrollCucumberMessage + { + public string CucumberMessageSource { get; set; } + public Envelope Envelope { get; set; } + } +} diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs new file mode 100644 index 000000000..aaf52c4db --- /dev/null +++ b/Reqnroll/CucumberMessages/RuntimeSupport/FeatureLevelCucumberMessages.cs @@ -0,0 +1,29 @@ +using Io.Cucumber.Messages.Types; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PayloadProcessing.Gherkin; +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Reqnroll.CucumberMessages.RuntimeSupport +{ + /// + /// This class is used at Code Generation time to provide serialized representations of the Source, GherkinDocument, and Pickles + /// to be used at runtime. + /// + public class FeatureLevelCucumberMessages + { + public FeatureLevelCucumberMessages(Func source, Func gherkinDocument, Func> pickles, string location) + { + Source = source; + GherkinDocument = gherkinDocument; + Pickles = pickles; + Location = location; + } + + public string Location { get; } + public Func Source { get; } + public Func GherkinDocument { get; } + public Func> Pickles { get; } + } +} diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs new file mode 100644 index 000000000..7902bdb95 --- /dev/null +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleJar.cs @@ -0,0 +1,31 @@ +using Io.Cucumber.Messages.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Reqnroll.CucumberMessages.RuntimeSupport +{ + /// + /// This class is used at runtime to provide the appropriate PickleId and PickleStepId to the TestCaseTracker and StepTracker. + /// + internal class PickleJar + { + private bool HasPickles => Pickles != null && Pickles.Count() > 0; + private IEnumerable Pickles; + + internal PickleJar(IEnumerable pickles) + { + Pickles = pickles; + } + + internal PickleStepSequence PickleStepSequenceFor(string pickleIndex) + { + var pickleIndexInt = int.Parse(pickleIndex); + if (HasPickles && (pickleIndexInt < 0 || pickleIndexInt >= Pickles.Count())) + throw new ArgumentException("Invalid pickle index: " + pickleIndex); + + return new PickleStepSequence(HasPickles, HasPickles ? Pickles.ElementAt(pickleIndexInt) : null); + } + } +} diff --git a/Reqnroll/CucumberMessages/RuntimeSupport/PickleStepSequence.cs b/Reqnroll/CucumberMessages/RuntimeSupport/PickleStepSequence.cs new file mode 100644 index 000000000..20fb217a6 --- /dev/null +++ b/Reqnroll/CucumberMessages/RuntimeSupport/PickleStepSequence.cs @@ -0,0 +1,33 @@ +using Io.Cucumber.Messages.Types; +using System.Linq; + +namespace Reqnroll.CucumberMessages.RuntimeSupport +{ + public class PickleStepSequence + { + public bool HasPickles { get; } + public Pickle CurrentPickle { get; } + + private int _PickleStepCounter; + + public PickleStepSequence(bool hasPickles, Pickle pickle) + { + HasPickles = hasPickles; + CurrentPickle = pickle; + _PickleStepCounter = 0; + } + public void NextStep() + { + _PickleStepCounter++; + } + public string CurrentPickleStepId + { + get + { + if (!HasPickles) return null; + return CurrentPickle.Steps.ElementAt(_PickleStepCounter).Id; + } + } + + } +} diff --git a/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs new file mode 100644 index 000000000..c2b2036cd --- /dev/null +++ b/Reqnroll/EnvironmentAccess/EnvironmentInfoProvider.cs @@ -0,0 +1,100 @@ +using Reqnroll.CommonModels; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Reqnroll.EnvironmentAccess +{ + /// + /// This provides an abstraction for obtaining platform and runtime information. Used by Anaytics and Cucumber Messages + /// + public class EnvironmentInfoProvider : IEnvironmentInfoProvider + { + private IEnvironmentWrapper EnvironmentWrapper { get; set; } + + public EnvironmentInfoProvider(IEnvironmentWrapper environmentWrapper) + { + EnvironmentWrapper = environmentWrapper; + } + + public string GetOSPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Windows"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Linux"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "OSX"; + } + + throw new InvalidOperationException("Platform cannot be identified"); + } + + private readonly Dictionary buildServerTypes + = new Dictionary { + { "TF_BUILD","Azure Pipelines"}, + { "TEAMCITY_VERSION","TeamCity"}, + { "JENKINS_HOME","Jenkins"}, + { "GITHUB_ACTIONS","GitHub Actions"}, + { "GITLAB_CI","GitLab CI/CD"}, + { "CODEBUILD_BUILD_ID","AWS CodeBuild"}, + { "TRAVIS","Travis CI"}, + { "APPVEYOR","AppVeyor"}, + { "BITBUCKET_BUILD_NUMBER", "Bitbucket Pipelines" }, + { "bamboo_agentId", "Atlassian Bamboo" }, + { "CIRCLECI", "CircleCI" }, + { "GO_PIPELINE_NAME", "GoCD" }, + { "BUDDY", "Buddy" }, + { "NEVERCODE", "Nevercode" }, + { "SEMAPHORE", "SEMAPHORE" }, + { "BROWSERSTACK_USERNAME", "BrowserStack" }, + { "CF_BUILD_ID", "Codefresh" }, + { "TentacleVersion", "Octopus Deploy" }, + + { "CI_NAME", "CodeShip" } + }; + + + public string GetBuildServerName() + { + foreach (var buildServerType in buildServerTypes) + { + var envVariable = EnvironmentWrapper.GetEnvironmentVariable(buildServerType.Key); + if (envVariable is ISuccess) + return buildServerType.Value; + } + return null; + } + + public bool IsRunningInDockerContainer() + { + return EnvironmentWrapper.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") is ISuccess; + } + + public string GetReqnrollVersion() + { + return VersionInfo.NuGetVersion; + } + public string GetNetCoreVersion() + { + var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) + { + return assemblyPath[netCoreAppIndex + 1]; + } + + return null; + } + + } +} diff --git a/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs b/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs new file mode 100644 index 000000000..562372ec4 --- /dev/null +++ b/Reqnroll/EnvironmentAccess/IEnvironmentInfoProvider.cs @@ -0,0 +1,11 @@ +namespace Reqnroll.EnvironmentAccess +{ + public interface IEnvironmentInfoProvider + { + string GetOSPlatform(); + string GetBuildServerName(); + bool IsRunningInDockerContainer(); + string GetReqnrollVersion(); + string GetNetCoreVersion(); + } +} \ No newline at end of file diff --git a/Reqnroll/ErrorHandling/AmbiguousBindingException.cs b/Reqnroll/ErrorHandling/AmbiguousBindingException.cs new file mode 100644 index 000000000..aec1054df --- /dev/null +++ b/Reqnroll/ErrorHandling/AmbiguousBindingException.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Reqnroll.Bindings; + +// the exceptions are part of the public API, keep them in Reqnroll namespace +// ReSharper disable once CheckNamespace +namespace Reqnroll; + +/// +/// This subclass is added for support of Cucumber Messages. +/// When emitting the Cucumber Message that describes an ambiguous matching situation, the Message will contain the list of possible matches. +/// We use this subclass of BindingException to convey that information. +/// +[Serializable] +public class AmbiguousBindingException : BindingException +{ + public IEnumerable Matches { get; private set; } + + public AmbiguousBindingException() + { + } + + public AmbiguousBindingException(string message) : base(message) + { + } + + public AmbiguousBindingException(string message, Exception inner) : base(message, inner) + { + } + + public AmbiguousBindingException(string message, IEnumerable matches) : base(message) + { + Matches = new List(matches); + } + + protected AmbiguousBindingException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + Matches = (List)info.GetValue("Matches", typeof(List)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + base.GetObjectData(info, context); + info.AddValue("Matches", Matches); + } +} diff --git a/Reqnroll/ErrorHandling/ErrorProvider.cs b/Reqnroll/ErrorHandling/ErrorProvider.cs index ce6731d3f..b1dc709cb 100644 --- a/Reqnroll/ErrorHandling/ErrorProvider.cs +++ b/Reqnroll/ErrorHandling/ErrorProvider.cs @@ -59,17 +59,19 @@ public Exception GetParameterCountError(BindingMatch match, int expectedParamete public Exception GetAmbiguousMatchError(List matches, StepInstance stepInstance) { string stepDescription = stepFormatter.GetStepDescription(stepInstance); - return new BindingException( - $"Ambiguous step definitions found for step '{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}"); + return new AmbiguousBindingException( + $"Ambiguous step definitions found for step '{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}", + matches); } public Exception GetAmbiguousBecauseParamCheckMatchError(List matches, StepInstance stepInstance) { string stepDescription = stepFormatter.GetStepDescription(stepInstance); - return new BindingException( + return new AmbiguousBindingException( "Multiple step definitions found, but none of them have matching parameter count and type for step " - + $"'{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}"); + + $"'{stepDescription}': {string.Join(", ", matches.Select(m => GetMethodText(m.StepBinding.Method)).ToArray())}", + matches); } public Exception GetNoMatchBecauseOfScopeFilterError(List matches, StepInstance stepInstance) diff --git a/Reqnroll/Events/ExecutionEvent.cs b/Reqnroll/Events/ExecutionEvent.cs index afa94a42d..2857ebd4f 100644 --- a/Reqnroll/Events/ExecutionEvent.cs +++ b/Reqnroll/Events/ExecutionEvent.cs @@ -1,10 +1,18 @@ using System; using Reqnroll.Bindings; +using Reqnroll.Infrastructure; namespace Reqnroll.Events { + // Cucumber Messages implementation note: Added various forms of context information to + // many of the ExecutionEvents. This allows the CucumberMessages implementation to + // align events with the Scenarios and Features to which they belong. + public class ExecutionEvent : IExecutionEvent { + public DateTime Timestamp { get; } + + public ExecutionEvent() => Timestamp = DateTime.Now; } public class TestRunStartedEvent : ExecutionEvent @@ -167,11 +175,19 @@ public StepBindingFinishedEvent(IStepDefinitionBinding stepDefinitionBinding, Ti public class HookBindingStartedEvent : ExecutionEvent { public IHookBinding HookBinding { get; } + public IContextManager ContextManager { get; private set; } + [Obsolete("Use HookBindingStartedEvent(IHookBinding, IContextManager) instead")] public HookBindingStartedEvent(IHookBinding hookBinding) { HookBinding = hookBinding; } + + public HookBindingStartedEvent(IHookBinding hookBinding, IContextManager contextManager) + { + HookBinding = hookBinding; + ContextManager = contextManager; + } } public class HookBindingFinishedEvent : ExecutionEvent @@ -179,12 +195,23 @@ public class HookBindingFinishedEvent : ExecutionEvent public IHookBinding HookBinding { get; } public TimeSpan Duration { get; } + public IContextManager ContextManager { get; private set; } + public Exception HookException { get; private set; } + [Obsolete("Use HookBindingFinishedEvent(IHookBinding, TimeSpan, IContextManager) instead")] public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration) { HookBinding = hookBinding; Duration = duration; } + + public HookBindingFinishedEvent(IHookBinding hookBinding, TimeSpan duration, IContextManager contextManager, Exception hookException = null) + { + HookBinding = hookBinding; + Duration = duration; + ContextManager = contextManager; + HookException = hookException; + } } public interface IExecutionOutputEvent @@ -193,20 +220,41 @@ public interface IExecutionOutputEvent public class OutputAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string Text { get; } + public FeatureInfo FeatureInfo { get; } + public ScenarioInfo ScenarioInfo { get; } + public string StepText { get; } + [Obsolete("Use OutputAddedEvent(string, FeatureInfo) instead")] public OutputAddedEvent(string text) { Text = text; } + + public OutputAddedEvent(string text, FeatureInfo featureInfo, ScenarioInfo scenarioInfo) + { + Text = text; + FeatureInfo = featureInfo; + ScenarioInfo = scenarioInfo; + } } public class AttachmentAddedEvent : ExecutionEvent, IExecutionOutputEvent { public string FilePath { get; } + public FeatureInfo FeatureInfo { get; } + public ScenarioInfo ScenarioInfo { get; } + [Obsolete("Use AttachmentAddedEvent(string, FeatureInfo) instead")] public AttachmentAddedEvent(string filePath) { FilePath = filePath; } + + public AttachmentAddedEvent(string filePath, FeatureInfo featureInfo, ScenarioInfo scenarioInfo) + { + FilePath = filePath; + FeatureInfo = featureInfo; + ScenarioInfo = scenarioInfo; + } } } diff --git a/Reqnroll/Events/IExecutionEventListener.cs b/Reqnroll/Events/IExecutionEventListener.cs index ec87491ba..8c7c12cf5 100644 --- a/Reqnroll/Events/IExecutionEventListener.cs +++ b/Reqnroll/Events/IExecutionEventListener.cs @@ -1,7 +1,14 @@ +using System.Threading.Tasks; + namespace Reqnroll.Events { public interface IExecutionEventListener { void OnEvent(IExecutionEvent executionEvent); } + + public interface IAsyncExecutionEventListener + { + Task OnEventAsync(IExecutionEvent executionEvent); + } } diff --git a/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs b/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs index b00b20753..e05fe3b12 100644 --- a/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs +++ b/Reqnroll/Events/ITestThreadExecutionEventPublisher.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; namespace Reqnroll.Events { @@ -6,8 +7,12 @@ public interface ITestThreadExecutionEventPublisher { void PublishEvent(IExecutionEvent executionEvent); + Task PublishEventAsync(IExecutionEvent executionEvent); + void AddListener(IExecutionEventListener listener); + void AddAsyncListener(IAsyncExecutionEventListener listener); + void AddHandler(Action handler) where TEvent: IExecutionEvent; } } diff --git a/Reqnroll/Events/TestThreadExecutionEventPublisher.cs b/Reqnroll/Events/TestThreadExecutionEventPublisher.cs index 081263fc5..dddacc640 100644 --- a/Reqnroll/Events/TestThreadExecutionEventPublisher.cs +++ b/Reqnroll/Events/TestThreadExecutionEventPublisher.cs @@ -1,14 +1,22 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; namespace Reqnroll.Events { public class TestThreadExecutionEventPublisher : ITestThreadExecutionEventPublisher { private readonly List _listeners = new(); + private readonly List _asyncListeners = new(); private readonly Dictionary> _handlersDictionary = new(); public void PublishEvent(IExecutionEvent executionEvent) + { + Task.Run(async () => await PublishEventAsync(executionEvent)).Wait(); + } + + private void PublishSync(IExecutionEvent executionEvent) { foreach (var listener in _listeners) { @@ -24,11 +32,26 @@ public void PublishEvent(IExecutionEvent executionEvent) } } + public async Task PublishEventAsync(IExecutionEvent executionEvent) + { + PublishSync(executionEvent); + + foreach (var listener in _asyncListeners) + { + await listener.OnEventAsync(executionEvent); + } + } + public void AddListener(IExecutionEventListener listener) { _listeners.Add(listener); } + public void AddAsyncListener(IAsyncExecutionEventListener listener) + { + _asyncListeners.Add(listener); + } + public void AddHandler(Action handler) where TEvent : IExecutionEvent { if (!_handlersDictionary.TryGetValue(typeof(TEvent), out var handlers)) diff --git a/Reqnroll/FeatureInfo.cs b/Reqnroll/FeatureInfo.cs index d6c7cfaed..1c50d11f3 100644 --- a/Reqnroll/FeatureInfo.cs +++ b/Reqnroll/FeatureInfo.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using Reqnroll.CucumberMessages.RuntimeSupport; using Reqnroll.Tracing; namespace Reqnroll @@ -17,6 +18,12 @@ public class FeatureInfo public string Description { get; private set; } public CultureInfo Language { get; private set; } + + // This holds the cucumber messages at the feature level created by the test class generator; populated when the FeatureStartedEvent is fired + public FeatureLevelCucumberMessages FeatureCucumberMessages { get; set; } + // This holds the unique identifier for the tracker instance that is being used to generate cucumber messages for this Test Case + public string CucumberMessages_PickleId { get; set; } + public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags) : this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags) { @@ -38,5 +45,11 @@ public FeatureInfo(CultureInfo language, string folderPath, string title, string GenerationTargetLanguage = programmingLanguage; Tags = tags ?? Array.Empty(); } + + public FeatureInfo(CultureInfo language, string folderPath, string title, string description, ProgrammingLanguage programmingLanguage, string[] tags, FeatureLevelCucumberMessages featureLevelCucumberMessages = null) + : this(language, folderPath, title, description, programmingLanguage, tags) + { + FeatureCucumberMessages = featureLevelCucumberMessages; + } } } \ No newline at end of file diff --git a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs index eb489540e..8cc98655b 100644 --- a/Reqnroll/Infrastructure/DefaultDependencyProvider.cs +++ b/Reqnroll/Infrastructure/DefaultDependencyProvider.cs @@ -16,6 +16,8 @@ using Reqnroll.Time; using Reqnroll.Tracing; using Reqnroll.PlatformCompatibility; +using Reqnroll.CucumberMessages.Configuration; +using Reqnroll.CucumberMessages.PubSub; namespace Reqnroll.Infrastructure { @@ -71,6 +73,7 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs(); + container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs(); container.RegisterTypeAs(); @@ -98,6 +101,12 @@ public virtual void RegisterGlobalContainerDefaults(ObjectContainer container) container.RegisterTypeAs(); container.RegisterTypeAs(); + + //Support for publishing Cucumber Messages + container.RegisterTypeAs(); + container.RegisterTypeAs("FileOutputPlugin"); + container.RegisterTypeAs(); + container.RegisterTypeAs("CucumberMessagePublisher"); } public virtual void RegisterTestThreadContainerDefaults(ObjectContainer testThreadContainer) diff --git a/Reqnroll/Infrastructure/MatchArgumentCalculator.cs b/Reqnroll/Infrastructure/MatchArgumentCalculator.cs index 07af450d4..3ad76130a 100644 --- a/Reqnroll/Infrastructure/MatchArgumentCalculator.cs +++ b/Reqnroll/Infrastructure/MatchArgumentCalculator.cs @@ -4,21 +4,33 @@ namespace Reqnroll.Infrastructure { + // The MatchArgument structure holds information about arguments extracted from a Step match. The StartOffset indicates where in the step text the value of the argument begins in that string. + public class MatchArgument + { + public object Value { get; private set; } + public int? StartOffset { get; private set; } + + public MatchArgument(object value, int? startOffset = null) + { + Value = value; + StartOffset = startOffset; + } + } public interface IMatchArgumentCalculator { - object[] CalculateArguments(Match match, StepInstance stepInstance, IStepDefinitionBinding stepDefinitionBinding); + MatchArgument[] CalculateArguments(Match match, StepInstance stepInstance, IStepDefinitionBinding stepDefinitionBinding); } public class MatchArgumentCalculator : IMatchArgumentCalculator { - public object[] CalculateArguments(Match match, StepInstance stepInstance, IStepDefinitionBinding stepDefinitionBinding) + public MatchArgument[] CalculateArguments(Match match, StepInstance stepInstance, IStepDefinitionBinding stepDefinitionBinding) { - var regexArgs = match.Groups.Cast().Skip(1).Where(g => g.Success).Select(g => g.Value); - var arguments = regexArgs.Cast().ToList(); + var regexArgs = match.Groups.Cast().Skip(1).Where(g => g.Success).Select(g => new MatchArgument(g.Value, g.Index)); + var arguments = regexArgs.ToList(); if (stepInstance.MultilineTextArgument != null) - arguments.Add(stepInstance.MultilineTextArgument); + arguments.Add(new MatchArgument(stepInstance.MultilineTextArgument)); if (stepInstance.TableArgument != null) - arguments.Add(stepInstance.TableArgument); + arguments.Add(new MatchArgument(stepInstance.TableArgument)); return arguments.ToArray(); } diff --git a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs index accb95460..49f6fb0b4 100644 --- a/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs +++ b/Reqnroll/Infrastructure/ReqnrollOutputHelper.cs @@ -8,17 +8,22 @@ public class ReqnrollOutputHelper : IReqnrollOutputHelper private readonly ITestThreadExecutionEventPublisher _testThreadExecutionEventPublisher; private readonly ITraceListener _traceListener; private readonly IReqnrollAttachmentHandler _reqnrollAttachmentHandler; + private readonly IContextManager _contextManager; - public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecutionEventPublisher, ITraceListener traceListener, IReqnrollAttachmentHandler reqnrollAttachmentHandler) + public ReqnrollOutputHelper(ITestThreadExecutionEventPublisher testThreadExecutionEventPublisher, ITraceListener traceListener, IReqnrollAttachmentHandler reqnrollAttachmentHandler, IContextManager contextManager) { _testThreadExecutionEventPublisher = testThreadExecutionEventPublisher; _traceListener = traceListener; _reqnrollAttachmentHandler = reqnrollAttachmentHandler; + _contextManager = contextManager; } public void WriteLine(string message) { - _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message)); + var featureInfo = _contextManager.FeatureContext?.FeatureInfo; + var scenarioInfo = _contextManager.ScenarioContext?.ScenarioInfo; + + _testThreadExecutionEventPublisher.PublishEvent(new OutputAddedEvent(message, featureInfo, scenarioInfo)); _traceListener.WriteToolOutput(message); } @@ -29,7 +34,10 @@ public void WriteLine(string format, params object[] args) public void AddAttachment(string filePath) { - _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath)); + var featureInfo = _contextManager.FeatureContext.FeatureInfo; + var scenarioInfo = _contextManager.ScenarioContext?.ScenarioInfo; + + _testThreadExecutionEventPublisher.PublishEvent(new AttachmentAddedEvent(filePath, featureInfo, scenarioInfo)); _reqnrollAttachmentHandler.AddAttachment(filePath); } } diff --git a/Reqnroll/Infrastructure/StepDefinitionMatchService.cs b/Reqnroll/Infrastructure/StepDefinitionMatchService.cs index 7e434ec4d..f7b63c26a 100644 --- a/Reqnroll/Infrastructure/StepDefinitionMatchService.cs +++ b/Reqnroll/Infrastructure/StepDefinitionMatchService.cs @@ -7,6 +7,7 @@ using Reqnroll.Bindings; using Reqnroll.Bindings.Reflection; using Reqnroll.ErrorHandling; +using Reqnroll.Assist; namespace Reqnroll.Infrastructure { @@ -75,7 +76,7 @@ public BindingMatch Match(IStepDefinitionBinding stepDefinitionBinding, StepInst if (useScopeMatching && stepDefinitionBinding.IsScoped && stepInstance.StepContext != null && !stepDefinitionBinding.BindingScope.Match(stepInstance.StepContext, out scopeMatches)) return BindingMatch.NonMatching; - var arguments = match == null ? Array.Empty() : _matchArgumentCalculator.CalculateArguments(match, stepInstance, stepDefinitionBinding); + var arguments = match == null ? Array.Empty() : _matchArgumentCalculator.CalculateArguments(match, stepInstance, stepDefinitionBinding); if (useParamMatching) { @@ -88,7 +89,7 @@ public BindingMatch Match(IStepDefinitionBinding stepDefinitionBinding, StepInst // Check if regex & extra arguments can be converted to the method parameters //if (arguments.Zip(bindingParameters, (arg, parameter) => CanConvertArg(arg, parameter.Type)).Any(canConvert => !canConvert)) - if (arguments.Where((arg, argIndex) => !CanConvertArg(arg, bindingParameters[argIndex].Type, bindingCulture)).Any()) + if (arguments.Where((arg, argIndex) => !CanConvertArg(arg.Value, bindingParameters[argIndex].Type, bindingCulture)).Any()) return BindingMatch.NonMatching; } diff --git a/Reqnroll/Infrastructure/TestExecutionEngine.cs b/Reqnroll/Infrastructure/TestExecutionEngine.cs index c0af86828..f918914c4 100644 --- a/Reqnroll/Infrastructure/TestExecutionEngine.cs +++ b/Reqnroll/Infrastructure/TestExecutionEngine.cs @@ -117,7 +117,7 @@ public virtual async Task OnTestRunStartAsync() _testRunnerStartExecuted = true; - _testThreadExecutionEventPublisher.PublishEvent(new TestRunStartedEvent()); + await _testThreadExecutionEventPublisher.PublishEventAsync(new TestRunStartedEvent()); await FireEventsAsync(HookType.BeforeTestRun); } @@ -136,14 +136,14 @@ public virtual async Task OnTestRunEndAsync() await FireEventsAsync(HookType.AfterTestRun); - _testThreadExecutionEventPublisher.PublishEvent(new TestRunFinishedEvent()); + await _testThreadExecutionEventPublisher.PublishEventAsync(new TestRunFinishedEvent()); } public virtual async Task OnFeatureStartAsync(FeatureInfo featureInfo) { _contextManager.InitializeFeatureContext(featureInfo); - _testThreadExecutionEventPublisher.PublishEvent(new FeatureStartedEvent(FeatureContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new FeatureStartedEvent(FeatureContext)); await FireEventsAsync(HookType.BeforeFeature); } @@ -159,7 +159,7 @@ public virtual async Task OnFeatureEndAsync() _testTracer.TraceDuration(duration, "Feature: " + FeatureContext.FeatureInfo.Title); } - _testThreadExecutionEventPublisher.PublishEvent(new FeatureFinishedEvent(FeatureContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new FeatureFinishedEvent(FeatureContext)); _contextManager.CleanupFeatureContext(); } @@ -171,7 +171,7 @@ public virtual void OnScenarioInitialize(ScenarioInfo scenarioInfo) public virtual async Task OnScenarioStartAsync() { - _testThreadExecutionEventPublisher.PublishEvent(new ScenarioStartedEvent(FeatureContext, ScenarioContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new ScenarioStartedEvent(FeatureContext, ScenarioContext)); try { @@ -231,10 +231,11 @@ public virtual async Task OnScenarioEndAsync() { await FireScenarioEventsAsync(HookType.AfterScenario); } - _testThreadExecutionEventPublisher.PublishEvent(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); } finally { + await _testThreadExecutionEventPublisher.PublishEventAsync(new ScenarioFinishedEvent(FeatureContext, ScenarioContext)); + _contextManager.CleanupScenarioContext(); } } @@ -303,7 +304,7 @@ protected virtual async Task FireScenarioEventsAsync(HookType bindingEvent) private async Task FireEventsAsync(HookType hookType) { - _testThreadExecutionEventPublisher.PublishEvent(new HookStartedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookStartedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext)); var stepContext = _contextManager.GetStepContext(); var matchingHooks = _bindingRegistry.GetHooks(hookType) @@ -335,7 +336,7 @@ private async Task FireEventsAsync(HookType hookType) //A plugin-hook should not throw an exception under normal circumstances, exceptions are not handled/caught here FireRuntimePluginTestExecutionLifecycleEvents(hookType); - _testThreadExecutionEventPublisher.PublishEvent(new HookFinishedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext, hookException)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookFinishedEvent(hookType, FeatureContext, ScenarioContext, _contextManager.StepContext, hookException)); //Note: the (user-)hook exception (if any) will be thrown after the plugin hooks executed to fail the test with the right error if (hookException != null) ExceptionDispatchInfo.Capture(hookException).Throw(); @@ -353,16 +354,24 @@ public virtual async Task InvokeHookAsync(IAsyncBindingInvoker invoker, IHookBin var currentContainer = GetHookContainer(hookType); var arguments = ResolveArguments(hookBinding, currentContainer); - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingStartedEvent(hookBinding)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookBindingStartedEvent(hookBinding, _contextManager)); var durationHolder = new DurationHolder(); - + Exception exceptionthrown = null; try { await invoker.InvokeBindingAsync(hookBinding, _contextManager, arguments, _testTracer, durationHolder); } + catch (Exception exception) + { + // This exception is caught in order to be able to inform consumers of the HookBindingFinishedEvent; + // This is used by CucumberMessages to include information about the exception in the hook TestStepResult + // The throw; statement ensures that the exception is propagated up to the FireEventsAsync method + exceptionthrown = exception; + throw; + } finally { - _testThreadExecutionEventPublisher.PublishEvent(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new HookBindingFinishedEvent(hookBinding, durationHolder.Duration, _contextManager, exceptionthrown)); } } @@ -549,7 +558,7 @@ protected virtual BindingMatch GetStepMatch(StepInstance stepInstance) protected virtual async Task ExecuteStepMatchAsync(BindingMatch match, object[] arguments, DurationHolder durationHolder) { - _testThreadExecutionEventPublisher.PublishEvent(new StepBindingStartedEvent(match.StepBinding)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepBindingStartedEvent(match.StepBinding)); try { @@ -557,7 +566,7 @@ protected virtual async Task ExecuteStepMatchAsync(BindingMatch match, object[] } finally { - _testThreadExecutionEventPublisher.PublishEvent(new StepBindingFinishedEvent(match.StepBinding, durationHolder.Duration)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepBindingFinishedEvent(match.StepBinding, durationHolder.Duration)); } } @@ -595,7 +604,7 @@ private async Task GetExecuteArgumentsAsync(BindingMatch match) for (var i = 0; i < match.Arguments.Length; i++) { - arguments[i] = await ConvertArg(match.Arguments[i], bindingParameters[i].Type); + arguments[i] = await ConvertArg(match.Arguments[i].Value, bindingParameters[i].Type); } return arguments; @@ -618,8 +627,11 @@ public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, StepDefinitionType stepDefinitionType = stepDefinitionKeyword == StepDefinitionKeyword.And || stepDefinitionKeyword == StepDefinitionKeyword.But ? GetCurrentBindingType() : (StepDefinitionType) stepDefinitionKeyword; - _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg)); - _testThreadExecutionEventPublisher.PublishEvent(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); + var stepSequenceIdentifiers = ScenarioContext.ScenarioInfo.PickleStepSequence; + var pickleStepId = stepSequenceIdentifiers?.CurrentPickleStepId ?? ""; + + _contextManager.InitializeStepContext(new StepInfo(stepDefinitionType, text, tableArg, multilineTextArg, pickleStepId)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepStartedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); try { @@ -628,7 +640,8 @@ public virtual async Task StepAsync(StepDefinitionKeyword stepDefinitionKeyword, } finally { - _testThreadExecutionEventPublisher.PublishEvent(new StepFinishedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); + await _testThreadExecutionEventPublisher.PublishEventAsync(new StepFinishedEvent(FeatureContext, ScenarioContext, _contextManager.StepContext)); + stepSequenceIdentifiers?.NextStep(); _contextManager.CleanupStepContext(); } } diff --git a/Reqnroll/Reqnroll.csproj b/Reqnroll/Reqnroll.csproj index 992299285..12c73c5c0 100644 --- a/Reqnroll/Reqnroll.csproj +++ b/Reqnroll/Reqnroll.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 Reqnroll @@ -26,6 +26,7 @@ +