From 4642769a5a2ec908eaf09401aef63a28bd3481d9 Mon Sep 17 00:00:00 2001 From: obligaron Date: Mon, 2 Dec 2024 12:06:51 +0100 Subject: [PATCH] Improve code-behind feature file compilation speed (#336) * TestGenerator: Don't call VB.NET fixes when compiling C# code and compile RegEx in VB.NET * ReqnrollGherkinParserFactory: Cache ReqnrollGherkinDialectProvider * ScenarioPartHelper: Avoid Enum reflaction * ReqnrollGherkinDialectProvider: Avoid NoSuchLanguageException --- CHANGELOG.md | 1 + .../Generation/ScenarioPartHelper.cs | 12 +++++++++++- Reqnroll.Generator/TestGenerator.cs | 13 +++++++++---- Reqnroll.Parser/ReqnrollGherkinParser.cs | 16 ++++++++-------- Reqnroll.Parser/ReqnrollGherkinParserFactory.cs | 7 ++++++- .../Parser/ReqnrollGherkinDialectProvider.cs | 17 +++++++---------- 6 files changed, 42 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eae31865b..c64259b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Improvements: * Enhance BoDi error handling to provide the name of the interface being registered when that interface has already been resolved (#324) +* Improve code-behind feature file compilation speed (#336) ## Bug fixes: diff --git a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs index e8395f841..769f9fcd5 100644 --- a/Reqnroll.Generator/Generation/ScenarioPartHelper.cs +++ b/Reqnroll.Generator/Generation/ScenarioPartHelper.cs @@ -1,3 +1,4 @@ +using System; using System.CodeDom; using System.Collections.Generic; using System.Linq; @@ -94,9 +95,18 @@ public void GenerateStep(TestClassGenerationContext generationContext, List "Given", + StepKeyword.When => "When", + StepKeyword.Then => "Then", + StepKeyword.And => "And", + StepKeyword.But => "But", + _ => throw new NotImplementedException(), + }; var expression = new CodeMethodInvokeExpression( testRunnerField, - scenarioStep.StepKeyword + "Async", + stepKeyWord + "Async", arguments.ToArray()); _codeDomHelper.MarkCodeMethodInvokeExpressionAsAwait(expression); diff --git a/Reqnroll.Generator/TestGenerator.cs b/Reqnroll.Generator/TestGenerator.cs index f39e285c3..b064edf17 100644 --- a/Reqnroll.Generator/TestGenerator.cs +++ b/Reqnroll.Generator/TestGenerator.cs @@ -106,7 +106,9 @@ protected string GetGeneratedTestCode(FeatureFileInput featureFileInput) outputWriter.Flush(); var generatedTestCode = outputWriter.ToString(); - return FixVb(generatedTestCode); + if (codeDomHelper.TargetLanguage == CodeDomProviderLanguage.VB) + generatedTestCode = FixVb(generatedTestCode); + return generatedTestCode; } } @@ -121,14 +123,17 @@ private string FixVBNetGlobalNamespace(string generatedTestCode) .Replace("Global.GlobalVBNetNamespace", "Global") .Replace("GlobalVBNetNamespace", "Global"); } - + + static readonly Lazy VBFixAsyncFunctionRegex = new Lazy(() => new Regex(@"^([^\n]*)Function[ ]*([^(\n]*)(\([^\n]*\)[ ]*As) async([^\n]*)$", RegexOptions.Multiline)); + static readonly Lazy VBFixAsyncSubRegex = new Lazy(() => new Regex(@"^([^\n]*)Sub[ ]*([^(\n]*)(\([^\n]*\)[ ]*As) async([^\n]*)$", RegexOptions.Multiline)); + /// /// This is a workaround to allow async/await calls in VB.NET. Works in cooperation with CodeDomHelper.MarkCodeMemberMethodAsAsync() method /// private string FixVBNetAsyncMethodDeclarations(string generatedTestCode) { - var functionRegex = new Regex(@"^([^\n]*)Function[ ]*([^(\n]*)(\([^\n]*\)[ ]*As) async([^\n]*)$", RegexOptions.Multiline); - var subRegex = new Regex(@"^([^\n]*)Sub[ ]*([^(\n]*)(\([^\n]*\)[ ]*As) async([^\n]*)$", RegexOptions.Multiline); + var functionRegex = VBFixAsyncFunctionRegex.Value; + var subRegex = VBFixAsyncSubRegex.Value; var result = functionRegex.Replace(generatedTestCode, "$1 Async Function $2$3$4"); result = subRegex.Replace(result, "$1 Async Sub $2$3$4"); diff --git a/Reqnroll.Parser/ReqnrollGherkinParser.cs b/Reqnroll.Parser/ReqnrollGherkinParser.cs index d2f97e85b..6d4fb235e 100644 --- a/Reqnroll.Parser/ReqnrollGherkinParser.cs +++ b/Reqnroll.Parser/ReqnrollGherkinParser.cs @@ -11,7 +11,13 @@ namespace Reqnroll.Parser public class ReqnrollGherkinParser : IGherkinParser { private readonly IGherkinDialectProvider dialectProvider; - private readonly List semanticValidators; + private static readonly List semanticValidators = new List(4) + { + new DuplicateScenariosValidator(), + new DuplicateExamplesValidator(), + new MissingExamplesValidator(), + new DuplicateExamplesColumnHeadersValidator() + }; public IGherkinDialectProvider DialectProvider { @@ -26,13 +32,7 @@ public ReqnrollGherkinParser(IGherkinDialectProvider dialectProvider) public ReqnrollGherkinParser(CultureInfo defaultLanguage) : this(new ReqnrollGherkinDialectProvider(defaultLanguage.Name)) { - semanticValidators = new List - { - new DuplicateScenariosValidator(), - new DuplicateExamplesValidator(), - new MissingExamplesValidator(), - new DuplicateExamplesColumnHeadersValidator() - }; + } private static StepKeyword GetStepKeyword(GherkinDialect dialect, string stepKeyword) diff --git a/Reqnroll.Parser/ReqnrollGherkinParserFactory.cs b/Reqnroll.Parser/ReqnrollGherkinParserFactory.cs index 988b5369f..2242296e5 100644 --- a/Reqnroll.Parser/ReqnrollGherkinParserFactory.cs +++ b/Reqnroll.Parser/ReqnrollGherkinParserFactory.cs @@ -1,12 +1,17 @@ using Gherkin; +using System; +using System.Collections.Concurrent; using System.Globalization; namespace Reqnroll.Parser { public class ReqnrollGherkinParserFactory : IGherkinParserFactory { + readonly ConcurrentDictionary _CachedDialectProvider = new ConcurrentDictionary(); + static readonly Func _DialectProviderFactory = (defaultLanguage) => new ReqnrollGherkinDialectProvider(defaultLanguage); + public IGherkinParser Create(IGherkinDialectProvider dialectProvider) => new ReqnrollGherkinParser(dialectProvider); - public IGherkinParser Create(CultureInfo cultureInfo) => new ReqnrollGherkinParser(cultureInfo); + public IGherkinParser Create(CultureInfo cultureInfo) => new ReqnrollGherkinParser(_CachedDialectProvider.GetOrAdd(cultureInfo.Name, _DialectProviderFactory)); } } \ No newline at end of file diff --git a/Reqnroll/Parser/ReqnrollGherkinDialectProvider.cs b/Reqnroll/Parser/ReqnrollGherkinDialectProvider.cs index 17f097ffd..ef501f217 100644 --- a/Reqnroll/Parser/ReqnrollGherkinDialectProvider.cs +++ b/Reqnroll/Parser/ReqnrollGherkinDialectProvider.cs @@ -13,16 +13,13 @@ public override GherkinDialect GetDialect(string language, Location location) { if (language.Contains("-")) { - try - { - return base.GetDialect(language, location); - } - catch (NoSuchLanguageException) - { - var languageBase = language.Split('-')[0]; - var languageBaseDialect = base.GetDialect(languageBase, location); - return new GherkinDialect(language, languageBaseDialect.FeatureKeywords, languageBaseDialect.RuleKeywords, languageBaseDialect.BackgroundKeywords, languageBaseDialect.ScenarioKeywords, languageBaseDialect.ScenarioOutlineKeywords, languageBaseDialect.ExamplesKeywords, languageBaseDialect.GivenStepKeywords, languageBaseDialect.WhenStepKeywords, languageBaseDialect.ThenStepKeywords, languageBaseDialect.AndStepKeywords, languageBaseDialect.ButStepKeywords); - } + // Use TryGetDialect to avoid NoSuchLanguageException, if culture specific language is not present + if (TryGetDialect(language, location, out var dialect)) + return dialect; + + var languageBase = language.Split('-')[0]; + var languageBaseDialect = base.GetDialect(languageBase, location); + return new GherkinDialect(language, languageBaseDialect.FeatureKeywords, languageBaseDialect.RuleKeywords, languageBaseDialect.BackgroundKeywords, languageBaseDialect.ScenarioKeywords, languageBaseDialect.ScenarioOutlineKeywords, languageBaseDialect.ExamplesKeywords, languageBaseDialect.GivenStepKeywords, languageBaseDialect.WhenStepKeywords, languageBaseDialect.ThenStepKeywords, languageBaseDialect.AndStepKeywords, languageBaseDialect.ButStepKeywords); } return base.GetDialect(language, location);