diff --git a/Cmdlets/SyncALTestCodeunitCmdlet.cs b/Cmdlets/SyncALTestCodeunitCmdlet.cs new file mode 100644 index 0000000..dd84c03 --- /dev/null +++ b/Cmdlets/SyncALTestCodeunitCmdlet.cs @@ -0,0 +1,208 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Text.RegularExpressions; + +namespace ATDD.TestScriptor +{ + [Cmdlet(VerbsData.Sync, "ALTestCodeunit")] + public class SyncALTestCodeunitCmdlet : PSCmdlet + { + private List scenarioCache = new List(); + + [Parameter(Mandatory = true)] + public string CodeunitPath { get; set; } + + private SwitchParameter InitializeFunction { get; set; } + + [Parameter(ValueFromPipeline = true)] + [ValidateNotNull()] + public TestFeature[] Feature { get; set; } = new TestFeature[] { }; + + [Parameter()] + [ValidateNotNullOrEmpty()] + public string GivenFunctionName { get; set; } = "{0}"; + + [Parameter()] + [ValidateNotNullOrEmpty()] + public string WhenFunctionName { get; set; } = "{0}"; + + [Parameter()] + [ValidateNotNullOrEmpty()] + public string ThenFunctionName { get; set; } = "{0}"; + + [Parameter()] + [ValidateNotNull()] + public string BannerFormat { get; set; } = "// Generated on {0} at {1} by {2}"; + + protected override void ProcessRecord() => scenarioCache.AddRange(Feature.SelectMany(f => f.Scenarios)); + + protected override void EndProcessing() + { + var uniqueFeatureNames = + scenarioCache + .Select(s => s.Feature.ToString()) + .Distinct(); + + var elementFunctionNames = + scenarioCache + .SelectMany(s => s.Elements) + .Select(e => new { Element = e, FunctionName = GetElementFunctionName(e) }) + .ToDictionary(o => o.Element, o => o.FunctionName); + + var uniqueFunctionNames = + elementFunctionNames + .Values + .Distinct() + .OrderBy(f => f); + + + CodeunitPath = File.Exists(CodeunitPath) ? CodeunitPath : Path.Combine(this.SessionState.Path.CurrentFileSystemLocation.Path, CodeunitPath); + if (!File.Exists(CodeunitPath)) + { + return; + } + + var lines = File.ReadAllLines(CodeunitPath).ToList(); + var uniqueProcedures = scenarioCache.Select(s => s.ToString()).Union(elementFunctionNames.Select(s => s.ToString())); + var existingFunctions = uniqueFunctionNames.Where(w => lines.Any(a => a.Contains(w.ToString()))); + var newFunctions = uniqueProcedures.Except(existingFunctions); + var existingScenarios = scenarioCache.Where(s => lines.Any(a => a.Contains(s.ToString()))); + var newScenarios = scenarioCache.Except(existingScenarios); + + var newElementFunctionNames = + newScenarios + .SelectMany(s => s.Elements) + .Select(e => new { Element = e, FunctionName = GetElementFunctionName(e) }) + .ToDictionary(o => o.Element, o => o.FunctionName); + + var newUniqueFunctionNames = + newElementFunctionNames + .Values + .Distinct() + .OrderBy(f => f); + + newScenarios.ForEach(e => WriteObject($"New Scenario: {e.ToString()}")); + newUniqueFunctionNames.ForEach(e => WriteObject($"New helper: {e.ToString()}")); + + if (newScenarios.Count() == 0) + { + return; + } + + WarnIfPlaceHolderMissing(GivenFunctionName); + WarnIfPlaceHolderMissing(WhenFunctionName); + WarnIfPlaceHolderMissing(ThenFunctionName); + + // scenarios + using (var stringWriter = new StringWriter()) + { + using (var writer = new IndentedTextWriter(stringWriter)) + { + writer.Indent++; + writer.WriteLine(); + newScenarios.ForEach(s => WriteALTestFunction(s, newElementFunctionNames, writer)); + writer.Indent--; + } + + var ti = lines.FindLastIndex(f => f.Contains("// [SCENARIO")); + var li = lines.FindIndex(ti, f => f.Contains("var")); + + lines.Insert(li, stringWriter.ToString()); + } + + // helpers + using (var stringWriter = new StringWriter()) + { + using (var writer = new IndentedTextWriter(stringWriter)) + { + writer.Indent++; + writer.WriteLine(); + newUniqueFunctionNames.ForEach(f => WriteDummyFunction(f, writer)); + writer.Indent--; + } + + var ti = lines.LastIndexOf("}"); + + lines.Insert(ti, stringWriter.ToString()); + } + + //WriteObject(lines.Join("\r\n")); + + File.WriteAllLines(CodeunitPath, lines); + } + + protected void WriteALTestFunction(TestScenario scenario, Dictionary elementFunctionNames, IndentedTextWriter writer) + { + writer.WriteLine("[Test]"); + writer.WriteLine($"procedure {SanitizeName(scenario.Name)}()"); + writer.WriteLine($"// {scenario.Feature.ToString()}"); + writer.WriteLine("begin"); + writer.Indent++; + writer.WriteLine($"// {scenario.ToString()}"); + writer.WriteLineIf(InitializeFunction, "Initialize();"); + writer.WriteLine(); + writer.WriteLines(scenario.Elements.OfType().SelectMany(g => ElementLines(g, elementFunctionNames))); + writer.WriteLines(scenario.Elements.OfType().SelectMany(w => ElementLines(w, elementFunctionNames))); + writer.WriteLines(scenario.Elements.OfType().SelectMany(t => ElementLines(t, elementFunctionNames))); + writer.WriteLines(scenario.Elements.OfType().SelectMany(c => ElementLines(c, elementFunctionNames))); + writer.Indent--; + writer.WriteLine("end;"); + writer.WriteLine(); + } + + protected IEnumerable ElementLines(TestScenarioElement element, Dictionary elementFunctionNames) + { + yield return $"// {element.ToString()}"; + yield return $"{elementFunctionNames[element]}();"; + yield return ""; + } + + protected void WriteDummyFunction(string name, IndentedTextWriter writer) + { + writer.WriteLine($"local procedure {name}()"); + writer.WriteLine("begin"); + writer.WriteLine("end;"); + writer.WriteLine(); + } + + protected string GetElementFunctionName(TestScenarioElement element) + { + switch (element) + { + case Given given: return FormatElement(element, GivenFunctionName); + case When @when: return FormatElement(element, WhenFunctionName); + case Then then: return FormatElement(element, ThenFunctionName); + default: return SanitizeName(element.Value); + } + } + + protected string FormatElement(TestScenarioElement element, string format) + { + try + { + return SanitizeName(string.Format(format, element.Value)); + } + catch (FormatException e) + { + throw new FormatException($"Function name format '{format}' should not contain placeholders other than '{{0}}'", e); + } + } + + protected void WarnIfPlaceHolderMissing(string format) + { + if (!format.Contains("{0}")) + WriteWarning($"Function name format '{format}' does not contain placeholder '{{0}}'"); + } + + protected static string SanitizeName(string name) => + Regex + .Split(name, @"\W", RegexOptions.CultureInvariant) + .Where(s => !string.IsNullOrEmpty(s)) + .Select(s => Regex.Replace(s, "^.", m => m.Value.ToUpperInvariant())) + .Join(""); + } +} \ No newline at end of file diff --git a/demo2.ps1 b/demo2.ps1 new file mode 100644 index 0000000..cc86cb0 Binary files /dev/null and b/demo2.ps1 differ diff --git a/docs/Sync-ALTestCodeunit.md b/docs/Sync-ALTestCodeunit.md new file mode 100644 index 0000000..1212469 --- /dev/null +++ b/docs/Sync-ALTestCodeunit.md @@ -0,0 +1,118 @@ +--- +external help file: ATDD.TestScriptor.dll-Help.xml +Module Name: ATDD.TestScriptor +online version: +schema: 2.0.0 +--- + +# Sync-ALTestCodeunit + +## SYNOPSIS +Inserts new test features into an existing AL codeunit. + +## SYNTAX + +``` +Sync-ALTestCodeunit [-CodeunitPath] + [-Feature ] [-GivenFunctionName ] [-WhenFunctionName ] + [-ThenFunctionName ] [] +``` + +## DESCRIPTION +Insert one or more new test features into an existing AL codeunit. Each **scenario** contained in a feature will result in 1 test function. Each **given**, **when** and **then** tag in a scenario will result in a helper function placeholder to be completed by manual AL coding. + +## EXAMPLES + +### Example 1 +```powershell +PS C:\> Feature 'My Feature' { Scenario 1 'My Scenario' { Given Foo; When Baz; Then Bar } } | Sync-ALTestCodeunit -CodeunitPath 'C:\\test.al' +``` + +## PARAMETERS + +### -CodeunitPath +Full Path of an existing test codeunit + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Feature +The feature(s) whose scenarios must be included in the test codeunit + +```yaml +Type: TestFeature[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -GivenFunctionName +Specify the format for the AL function that is created for a Given element. Use the placeholder {0} to specify where you want the Given's situation description to go. Leaving a space between the placeholder and the rest of your text ensures that it's seen as a separate word, and therefore gets an initial capital letter when converting to title case, e.g. 'Create {0}' for a Given whose situation is 'a Customer' will lead to 'CreateACustomer' as the function name. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ThenFunctionName +Specify the format for the AL function that is created for a Then element. Use the placeholder {0} to specify where you want the Then's expected result description to go. Leaving a space between the placeholder and the rest of your text ensures that it's seen as a separate word, and therefore gets an initial capital letter when converting to title case, e.g. 'Verify {0}' for a Given whose situation is 'customer exists' will lead to 'VerifyCustomerExists' as the function name. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhenFunctionName +Specify the format for the AL function that is created for a When element. Use the placeholder {0} to specify where you want the When's condition description to go. Leaving a space between the placeholder and the rest of your text ensures that it's seen as a separate word, and therefore gets an initial capital letter when converting to title case. +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### ATDD.TestScriptor.TestFeature[] +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/manifest.psd1 b/manifest.psd1 index b52b352..edc03da 100644 --- a/manifest.psd1 +++ b/manifest.psd1 @@ -73,7 +73,7 @@ # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = 'New-ATDDGiven', 'New-ATDDTestFeature', 'New-ATDDTestScenario', 'New-ATDDThen', - 'New-ATDDWhen', 'ConvertTo-ALTestCodeunit', 'New-ATDDCleanup' + 'New-ATDDWhen', 'ConvertTo-ALTestCodeunit', 'New-ATDDCleanup', 'Sync-ALTestCodeunit' # Variables to export from this module VariablesToExport = '*'