-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#23: BDD: Completely rebuild engine for generating Feature class cont…
…ent based on Gherkin *.feature file. Added: - GherkinTokenizer - GherkinParser - XUnitFeatureGenerator
- Loading branch information
1 parent
5e97bb1
commit 902a5d4
Showing
21 changed files
with
565 additions
and
372 deletions.
There are no files selected for viewing
334 changes: 11 additions & 323 deletions
334
Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,359 +1,47 @@ | ||
using System.Runtime.CompilerServices; | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
using Synergy.Behaviours.Testing.Generator; | ||
using Synergy.Behaviours.Testing.Gherkin; | ||
using Synergy.Behaviours.Testing.Gherkin.File; | ||
using Synergy.Behaviours.Testing.Gherkin.Parser; | ||
|
||
namespace Synergy.Behaviours.Testing; | ||
|
||
public static class FeatureGenerator | ||
{ | ||
private const string Background = nameof(Feature<object>.Background); | ||
private const string Given = nameof(Feature<object>.Given); | ||
private const string When = nameof(Feature<object>.When); | ||
private const string Then = nameof(Feature<object>.Then); | ||
private const string And = nameof(Feature<object>.And); | ||
private const string But = nameof(Feature<object>.But); | ||
private const string Moreover = nameof(Feature<object>.Moreover); | ||
|
||
// TODO: Marcin Celej [from: Marcin Celej on: 10-05-2023]: Add include / exclude as functions | ||
public static void Generate<TBehaviour>( | ||
this TBehaviour feature, | ||
string from, | ||
string to, | ||
string[]? include = null, | ||
string[]? exclude = null, | ||
Func<Scenario, bool>? include = null, | ||
Func<Scenario, bool>? generateAfter = null, | ||
[CallerFilePath] string callerFilePath = "" | ||
) | ||
{ | ||
var code = feature.Generate( | ||
from, | ||
include, | ||
exclude, | ||
generateAfter, | ||
// ReSharper disable once ExplicitCallerInfoArgument | ||
callerFilePath | ||
); | ||
var destinationFilePath = Path.Combine(PathFor(callerFilePath), to); | ||
File.WriteAllText(destinationFilePath, code); | ||
} | ||
|
||
private static String PathFor(string callerFilePath) | ||
{ | ||
return Path.GetDirectoryName(callerFilePath) ?? throw new ArgumentException("Improper path: " + callerFilePath, nameof(callerFilePath)); | ||
GherkinWriter.Write(callerFilePath, to, code); | ||
} | ||
|
||
public static string Generate<TBehaviour>( | ||
this TBehaviour featureClass, | ||
string from, | ||
string[]? include = null, | ||
string[]? exclude = null, | ||
Func<Scenario, bool>? include = null, | ||
Func<Scenario, bool>? generateAfter = null, | ||
[CallerFilePath] string callerFilePath = "" | ||
) | ||
{ | ||
if (featureClass == null) | ||
throw new ArgumentNullException(nameof(featureClass)); | ||
|
||
if (from == null) | ||
throw new ArgumentNullException(nameof(from)); | ||
|
||
StringBuilder code = new StringBuilder(); | ||
string className = featureClass.GetType() | ||
.Name; | ||
var gherkinPath = Path.Combine(PathFor(callerFilePath), from); | ||
|
||
string[] gherkins = FeatureGenerator.ReadAllLinesFrom(gherkinPath); | ||
|
||
code.AppendLine("// <auto-generated />"); | ||
code.AppendLine("using System.CodeDom.Compiler;"); | ||
code.AppendLine(); | ||
code.AppendLine($"namespace {featureClass.GetType().Namespace};"); | ||
code.AppendLine(); | ||
code.AppendLine( | ||
$"[GeneratedCode(\"{typeof(FeatureGenerator).Assembly.GetName().Name}\", " + | ||
$"\"{typeof(FeatureGenerator).Assembly.GetName().Version?.ToString()}\")]" | ||
); | ||
// TODO: Marcin Celej [from: Marcin Celej on: 09-01-2024]: Introduce here [Xunit.Trait("Category", featureName)] | ||
code.AppendLine($"partial class {className}"); | ||
code.AppendLine("{"); | ||
|
||
Scenario? scenario = null; | ||
bool includeScenario = ResetInclude(); | ||
List<string>? tags = null; | ||
int lineNo = 0; | ||
string? backgroundMethod = null; | ||
string? backgroundStarted = null; | ||
string? featureName = null; | ||
string? ruleName = null; | ||
|
||
foreach (var line in gherkins) | ||
{ | ||
lineNo++; | ||
|
||
var comment = Regex.Match(line, "^\\s*#(.*)"); | ||
if (comment.Success) | ||
{ | ||
tags = null; | ||
includeScenario = ResetInclude(); | ||
//code.AppendLine(line.Replace("#", "//")); | ||
continue; | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(line)) | ||
{ | ||
tags = null; | ||
includeScenario = ResetInclude(); | ||
|
||
continue; | ||
} | ||
|
||
var feature = Regex.Match(line, "^\\s*Feature\\: (.*)"); | ||
if (feature.Success) | ||
{ | ||
featureName = feature.Groups[1] | ||
.Value; | ||
continue; | ||
} | ||
|
||
// TODO: Marcin Celej [from: Marcin Celej on: 15-05-2023]: check if tags above Rule will work | ||
var rule = Regex.Match(line, "^\\s*Rule\\: (.*)"); | ||
if (rule.Success) | ||
{ | ||
CloseBackground(); | ||
CloseScenario(); | ||
|
||
ruleName = rule.Groups[1] | ||
.Value; | ||
|
||
code.AppendLine($" // {line.Trim()}"); | ||
code.AppendLine(); | ||
|
||
continue; | ||
} | ||
|
||
var background = Regex.Match(line, "^\\s*Background\\:"); | ||
if (background.Success) | ||
{ | ||
CloseScenario(); | ||
|
||
backgroundMethod = Sentence.ToMethod(ruleName ?? featureName ?? "Feature") + "Background"; | ||
backgroundStarted = backgroundMethod; | ||
code.AppendLine($" private void {backgroundMethod}() // {line.Trim()}"); | ||
code.AppendLine(" {"); | ||
continue; | ||
} | ||
|
||
// TODO: Marcin Celej [from: Marcin Celej on: 10-05-2023]: Support Scenario Outline along with Examples | ||
|
||
var outline = Regex.Match(line, "^\\s*Scenario (Outline|Template)\\: (.*)"); | ||
if (outline.Success) | ||
{ | ||
throw new NotSupportedException($"Scenario Outline keyword is not supported\nLine {lineNo}: {line.Trim()}"); | ||
} | ||
|
||
var example = Regex.Match(line, "^\\s*(Examples|Scenarios)\\: (.*)"); | ||
if (example.Success) | ||
{ | ||
throw new NotSupportedException($"Examples keyword is not supported\nLine {lineNo}: {line.Trim()}"); | ||
} | ||
|
||
if (line.Trim() | ||
.StartsWith("@")) | ||
{ | ||
CloseScenario(); | ||
|
||
tags = Regex.Matches(line, "\\@\\w+") | ||
.Select(m => m.Value) | ||
.ToList(); | ||
|
||
if (include != null) | ||
includeScenario = tags.Intersect(include) | ||
.Any(); | ||
|
||
if (exclude != null) | ||
includeScenario = !tags.Intersect(exclude) | ||
.Any(); | ||
|
||
// if (include) | ||
// code.AppendLine($" [Xunit.Trait({string.Join(", ", tags.Select(t => "\"" + t.TrimStart('@') + "\""))})]"); | ||
continue; | ||
} | ||
|
||
if (includeScenario == false && backgroundStarted == null) | ||
continue; | ||
|
||
var scenarioMatch = Regex.Match(line, "^\\s*Scenario\\: (.*)"); | ||
if (scenarioMatch.Success) | ||
{ | ||
CloseBackground(); | ||
CloseScenario(); | ||
|
||
scenario = new Scenario(scenarioMatch.Groups[1].Value, (tags ?? new List<string>(0)).AsReadOnly()); | ||
|
||
//code.AppendLine(" [Xunit.Trait(\"Category\", \"" + featureName + "\")]"); | ||
code.AppendLine(" [Xunit.Fact(DisplayName = \"Scenario: " + scenario.Title.Replace("\"", "\\\"") + "\")]"); | ||
if (tags != null) | ||
code.AppendLine($" // {String.Join(" ", tags)}"); | ||
code.AppendLine($" public void {scenario.Method}() // {line.Trim()}"); | ||
code.AppendLine(" {"); | ||
var backgroundCall = ""; | ||
if (backgroundMethod != null) | ||
backgroundCall = $".{backgroundMethod}()"; | ||
code.AppendLine($" {Background}(){backgroundCall};"); | ||
code.AppendLine(); | ||
|
||
continue; | ||
} | ||
|
||
var given = Regex.Match(line, "^\\s*Given (.*)"); | ||
if (given.Success) | ||
{ | ||
// if (backgroundStarted == null) | ||
// { | ||
// code.Append($" "); | ||
// } | ||
// else | ||
// { | ||
// code.Append($" "); | ||
// } | ||
|
||
code.AppendLine($" {Given}().{Sentence.ToMethod(given.Groups[1].Value)}(); // {line.Trim()}"); | ||
continue; | ||
} | ||
|
||
var and = Regex.Match(line, "^\\s*And (.*)"); | ||
if (and.Success) | ||
{ | ||
code.AppendLine($" {And}().{Sentence.ToMethod(and.Groups[1].Value)}(); // {line.Trim()}"); | ||
continue; | ||
} | ||
|
||
var asterisk = Regex.Match(line, "^\\s*\\* (.*)"); | ||
if (asterisk.Success) | ||
{ | ||
code.AppendLine($" {And}().{Sentence.ToMethod(asterisk.Groups[1].Value)}(); // {line.Trim()}"); | ||
continue; | ||
} | ||
|
||
var but = Regex.Match(line, "^\\s*But (.*)"); | ||
if (but.Success) | ||
{ | ||
code.AppendLine($" {But}().{Sentence.ToMethod(but.Groups[1].Value)}(); // {line.Trim()}"); | ||
continue; | ||
} | ||
|
||
var when = Regex.Match(line, "^\\s*When (.*)"); | ||
if (when.Success) | ||
{ | ||
code.AppendLine($" {When}().{Sentence.ToMethod(when.Groups[1].Value)}(); // {line.Trim()}"); | ||
continue; | ||
} | ||
|
||
var then = Regex.Match(line, "^\\s*Then (.*)"); | ||
if (then.Success) | ||
{ | ||
code.AppendLine($" {Then}().{Sentence.ToMethod(then.Groups[1].Value)}(); // {line.Trim()}"); | ||
continue; | ||
} | ||
} | ||
|
||
CloseBackground(); | ||
CloseScenario(); | ||
|
||
code.AppendLine("}"); | ||
|
||
var gherkin = GherkinReader.ReadAllLinesFrom(callerFilePath, from); | ||
var feature = GherkinParser.Parse(gherkin); | ||
var code = new XUnitFeatureGenerator(include, generateAfter).Generate(feature, featureClass); | ||
return code.ToString(); | ||
|
||
void CloseScenario() | ||
{ | ||
if (scenario != null) | ||
{ | ||
if (generateAfter != null && generateAfter(scenario)) | ||
{ | ||
code.AppendLine(); | ||
code.AppendLine($" {Moreover}().After{scenario.Method}();"); | ||
} | ||
|
||
code.AppendLine(" }"); | ||
code.AppendLine(); | ||
} | ||
|
||
scenario = null; | ||
} | ||
|
||
void CloseBackground() | ||
{ | ||
if (backgroundStarted != null) | ||
{ | ||
code.AppendLine(" }"); | ||
code.AppendLine(); | ||
} | ||
|
||
backgroundStarted = null; | ||
} | ||
|
||
bool ResetInclude() | ||
{ | ||
if (include != null) | ||
return false; | ||
|
||
if (exclude != null) | ||
return true; | ||
|
||
return true; | ||
} | ||
} | ||
|
||
private static string[] ReadAllLinesFrom(string gherkinPath) | ||
{ | ||
if (File.Exists(gherkinPath)) | ||
return File.ReadAllLines(gherkinPath); | ||
|
||
var gherkins = new[] | ||
{ | ||
$"Feature: {Path.GetFileNameWithoutExtension(gherkinPath)}", | ||
"", | ||
"# TODO: Provide scenarios here. Check the sample down here.", | ||
"", | ||
"# Scenario: There can be only one", | ||
"# Given there are 3 ninjas", | ||
"# And there are more than one ninja alive", | ||
"# When Two ninjas meet, they will fight", | ||
"# Then one ninja dies (but not me)", | ||
"# And there is one ninja less alive", | ||
}; | ||
|
||
using var stream = File.CreateText(gherkinPath); | ||
foreach (string line in gherkins) | ||
{ | ||
stream.WriteLine(line); | ||
} | ||
|
||
stream.Close(); | ||
|
||
return gherkins; | ||
} | ||
|
||
|
||
|
||
// private static string[]? ReadTagsFrom(string line) | ||
// { | ||
// if (line.TrimStart().StartsWith("@") == false) | ||
// return null; | ||
// | ||
// return Regex.Match(line, "\\@\\w+") | ||
// .Groups.Values.Select(g => g.Value) | ||
// .ToArray(); | ||
// } | ||
|
||
// private static bool StartsWithAny(this string line, params string[] starts) | ||
// { | ||
// foreach (string start in starts) | ||
// { | ||
// if (line.StartsWith(start, StringComparison.InvariantCultureIgnoreCase)) | ||
// return true; | ||
// } | ||
// | ||
// return false; | ||
// } | ||
} |
Oops, something went wrong.