Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions Reqnroll.Generator/Generation/ScenarioPartHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,18 +233,5 @@ private CodeExpression GetSubstitutedString(string text, ParameterSubstitution p
"Format",
formatArguments.ToArray());
}
public void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, int? pickleIndex)
{
if (!pickleIdIncludedInParameters && pickleIndex == null)
throw new ArgumentNullException(nameof(pickleIndex));

// string pickleId = "<pickleJar.CurrentPickleId>"; or
// string pickleId = __pickleId;
var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), GeneratorConstants.PICKLEINDEX_VARIABLE_NAME,
pickleIdIncludedInParameters ?
new CodeVariableReferenceExpression(GeneratorConstants.PICKLEINDEX_PARAMETER_NAME) :
new CodePrimitiveExpression(pickleIndex.Value.ToString()));
testMethod.Statements.Add(pickleIdVariable);
}
}
}
12 changes: 9 additions & 3 deletions Reqnroll.Generator/Generation/UnitTestFeatureGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public UnitTestFeatureGenerationResult GenerateUnitTestFixture(ReqnrollDocument

_unitTestMethodGenerator.CreateUnitTests(feature, generationContext);

SerializeFeatureEnvelopes(generationContext);
//before returning the generated code, call the provider's method in case the generated code needs to be customized
_testGeneratorProvider.FinalizeTestClass(generationContext);

Expand Down Expand Up @@ -236,11 +237,9 @@ string GetFeatureMessagesResourceName()
Envelope.Create(featureGherkinDocumentMessage)
};
envelopes.AddRange(featurePickleMessages.Select(Envelope.Create));
generationContext.FeatureEnvelopes = envelopes;
envelopeCount = envelopes.Count;

// Serialize each envelope and append into a ndjson format
generationContext.FeatureMessages = string.Join(Environment.NewLine, envelopes.Select(NdjsonSerializer.Serialize));

}
catch (System.Exception e)
{
Expand All @@ -263,6 +262,13 @@ string GetFeatureMessagesResourceName()
featureLevelCucumberMessagesExpression));
}

private void SerializeFeatureEnvelopes(TestClassGenerationContext generationContext)
{

// Serialize each envelope and append into a ndjson format
generationContext.FeatureMessages = string.Join(Environment.NewLine, generationContext.FeatureEnvelopes.Select(NdjsonSerializer.Serialize));
}

private void SetupTestClassInitializeMethod(TestClassGenerationContext generationContext)
{
var testClassInitializeMethod = generationContext.TestClassInitializeMethod;
Expand Down
63 changes: 50 additions & 13 deletions Reqnroll.Generator/Generation/UnitTestMethodGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Gherkin.Ast;
using Reqnroll.Configuration;
using Reqnroll.Formatters.RuntimeSupport;
using Reqnroll.Generator.CodeDom;
using Reqnroll.Generator.UnitTestConverter;
using Reqnroll.Generator.UnitTestProvider;
using Reqnroll.Parser;
using Reqnroll.Tracing;
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;

namespace Reqnroll.Generator.Generation
{
public class UnitTestMethodGenerator
public partial class UnitTestMethodGenerator
{
private const string IGNORE_TAG = "@Ignore";
private const string TESTRUNNER_FIELD = "testRunner";
Expand Down Expand Up @@ -211,7 +212,7 @@ private void GenerateTestBody(
// 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);
AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, pickleIndex, feature.Name, scenarioDefinition.Name, additionalTagsExpression, new CodeVariableReferenceExpression(GeneratorConstants.SCENARIO_ARGUMENTS_VARIABLE_NAME));

testMethod.Statements.Add(
new CodeVariableDeclarationStatement(new CodeTypeReference(typeof(ScenarioInfo), CodeTypeReferenceOptions.GlobalReference), "scenarioInfo",
Expand Down Expand Up @@ -242,9 +243,38 @@ private void GenerateTestBody(
GenerateScenarioCleanupMethodCall(generationContext, testMethod);
}

internal void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, int? pickleIndex)
internal void AddVariableForPickleIndex(CodeMemberMethod testMethod, bool pickleIdIncludedInParameters, int? pickleIndex, string featureName, string scenarioName, CodeExpression exampleTags, CodeVariableReferenceExpression arguments)
{
_scenarioPartHelper.AddVariableForPickleIndex(testMethod, pickleIdIncludedInParameters, pickleIndex);
if (!pickleIdIncludedInParameters && pickleIndex == null)
throw new ArgumentNullException(nameof(pickleIndex));

// if simple scenario, just assign the pickle index value directly
// string pickleIndex = "<pickleIndex";

// if scenario outline with row tests, we generate code that invokes the TestRowPickleMapper to get the pickle index from the row hash.

CodeExpression valueExpression;
if (!pickleIdIncludedInParameters)
{
valueExpression = new CodePrimitiveExpression(pickleIndex.Value.ToString());
}
else
{
var testRowPickleMapperTypeRef = new CodeTypeReferenceExpression(new CodeTypeReference(typeof(TestRowPickleMapper), CodeTypeReferenceOptions.GlobalReference));
valueExpression = new CodeMethodInvokeExpression(
testRowPickleMapperTypeRef,
nameof(TestRowPickleMapper.GetPickleIndexFromTestRow),
new CodePrimitiveExpression(featureName),
new CodePrimitiveExpression(scenarioName),
exampleTags,
new CodePropertyReferenceExpression(arguments, "Values"),
new CodePropertyReferenceExpression(new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(GeneratorConstants.FEATUREINFO_FIELD), "FeatureCucumberMessages"), "Pickles"));
}

var pickleIdVariable = new CodeVariableDeclarationStatement(typeof(string), GeneratorConstants.PICKLEINDEX_VARIABLE_NAME,
valueExpression);

testMethod.Statements.Add(pickleIdVariable);
}

private void AddVariableForTags(CodeMemberMethod testMethod, CodeExpression tagsExpression)
Expand Down Expand Up @@ -383,7 +413,7 @@ private CodeMethodInvokeExpression CreateTestRunnerSkipScenarioCall()
private void GenerateScenarioOutlineExamplesAsIndividualMethods(
TestClassGenerationContext generationContext,
ScenarioDefinitionInFeatureFile scenarioDefinitionInFeature,
CodeMemberMethod scenarioOutlineTestMethod,
CodeMemberMethod scenarioOutlineTestMethod,
ParameterSubstitution paramToIdentifier,
ref int pickleIndex)
{
Expand Down Expand Up @@ -432,10 +462,18 @@ private void GenerateScenarioOutlineExamplesAsRowTests(TestClassGenerationContex
{
foreach (var row in examples.TableBody)
{
var arguments = row.Cells.Select(c => c.Value).Concat([pickleIndex.ToString()]);

var arguments = row.Cells.Select(c => c.Value);
_unitTestGeneratorProvider.SetRow(generationContext, scenarioOutlineTestMethod, arguments, GetNonIgnoreTags(examples.Tags), HasIgnoreTag(examples.Tags));

// hash the featurename, scenariooutline name, example set tags and row values to create a unique id for the row.
// Look up the pickle in the generation context Feature Envelopes using the pickleIndex, and add the hash to the Pickle's list of tags.
var featureName = generationContext.Feature.Name;
var scenarioOutlineName = scenarioOutline.Name;
var rowValues = string.Join("|", arguments);
var rowHash = TestRowPickleMapper.ComputeHash(featureName, scenarioOutlineName, examples.Tags.Select(t => t.Name), arguments);

TestRowPickleMapper.MarkPickleWithRowHash(generationContext.FeatureEnvelopes, pickleIndex, rowHash);

pickleIndex++;
}
}
Expand Down Expand Up @@ -497,7 +535,6 @@ private CodeMemberMethod CreateScenarioOutlineTestMethod(TestClassGenerationCont
{
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;
}
Expand Down
2 changes: 2 additions & 0 deletions Reqnroll.Generator/TestClassGenerationContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.CodeDom;
using System.Collections.Generic;
using Io.Cucumber.Messages.Types;
using Reqnroll.Generator.UnitTestProvider;
using Reqnroll.Parser;

Expand Down Expand Up @@ -35,6 +36,7 @@ public class TestClassGenerationContext

public string FeatureMessagesResourceName { get; set; }
internal string FeatureMessages { get; set; }
internal IEnumerable<Envelope> FeatureEnvelopes { get; set; }

public TestClassGenerationContext(
IUnitTestGeneratorProvider unitTestGeneratorProvider,
Expand Down
2 changes: 1 addition & 1 deletion Reqnroll/FeatureInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class FeatureInfo
/// <summary>
/// This property holds the cucumber messages at the feature level created by the test class generator; populated when the FeatureStartedEvent is fired. Used internally.
/// </summary>
internal IFeatureLevelCucumberMessages FeatureCucumberMessages { get; set; }
public IFeatureLevelCucumberMessages FeatureCucumberMessages { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to make this public? It would be better if feature info would only expose things publicly that the users can/should work with.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it now - it is used in the generated code. I would pass the entire FeatureInfo to GetPickleIndexFromTestRow, so that it can access the internal prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverted and modified code gen per suggestion.


public FeatureInfo(CultureInfo language, string folderPath, string title, string description, params string[] tags)
: this(language, folderPath, title, description, ProgrammingLanguage.CSharp, tags)
Expand Down
50 changes: 50 additions & 0 deletions Reqnroll/Formatters/RuntimeSupport/TestRowPickleMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Io.Cucumber.Messages.Types;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Reqnroll.Formatters.RuntimeSupport;

public static class TestRowPickleMapper
{
public static object ComputeHash(string featureName, string scenarioOutlineName, IEnumerable<string> tags, IEnumerable<string> rowValues)
{
var tagsList = tags ?? Enumerable.Empty<string>();
var rowValuesList = rowValues ?? Enumerable.Empty<string>();
var v = $"{featureName}|{scenarioOutlineName}|{string.Join("|", tagsList)}|{string.Join("|", rowValuesList)}";
return v.GetHashCode();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HashCodes aren't likely to overlap but they are not unique.
Is this a problem here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed this is an MD5 hash.

}

public static void MarkPickleWithRowHash(IEnumerable<Envelope> envelopes, int pickleIndex, object rowHash)
{
var pickle = envelopes
.Where(e => e.Pickle != null)
.Select(e => e.Pickle)
.ElementAt(pickleIndex);
pickle.Tags.Add(new Io.Cucumber.Messages.Types.PickleTag($"@RowHash_{rowHash}", ""));
}

public static string GetPickleIndexFromTestRow(string featureName, string scenarioOutlineName, IEnumerable<string> tags, ICollection rowValues, IEnumerable<Pickle> pickles)
{
var rowValuesStrings = rowValues.Cast<object>().Select(v => v?.ToString() ?? string.Empty);
var rowHash = ComputeHash(featureName, scenarioOutlineName, tags, rowValuesStrings);
var tagName = $"@RowHash_{rowHash}";
for (int i = 0; i < pickles.Count(); i++)
{
var pickle = pickles.ElementAt(i);
// at this point, if the pickle has a tag with the row hash, we found it;
// in a thread safe way, remove the tag so that subsequent calls do not find the same pickle again
lock (pickle.Tags)
{
var tag = pickle.Tags.FirstOrDefault(t => t.Name == tagName);
if (tag != null)
{
pickle.Tags.Remove(tag);
return i.ToString();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with retries?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following; what type of retry are you considering as a potential problem?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a test runner decides to re-run the outline example test within the same execution process, the second run will not find the pickle index.

}
return null;
}
}

Loading