diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..448c9e3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ + + +## Why is the change needed? + + + +## What was changed? + + diff --git a/.gitignore b/.gitignore index 708828b..25e787d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ obj **/*.DotSettings.user # JetBrains -.idea \ No newline at end of file +.idea + +# Temp file +temp.md \ No newline at end of file diff --git a/AzureLiquid.Preview/PreviewProcess.cs b/AzureLiquid.Preview/PreviewProcess.cs index 8cbd8f3..95e6d97 100644 --- a/AzureLiquid.Preview/PreviewProcess.cs +++ b/AzureLiquid.Preview/PreviewProcess.cs @@ -12,6 +12,11 @@ namespace AzureLiquid.Preview; /// public class PreviewProcess { + /// + /// The argument parser. + /// + private readonly PreviewProcessArguments _args; + /// /// Handles writing console output to private persisted log. /// @@ -32,6 +37,7 @@ public class PreviewProcess /// public PreviewProcess() { + _args = new PreviewProcessArguments(); Template = string.Empty; Content = string.Empty; Output = "./preview.txt"; @@ -68,10 +74,10 @@ public PreviewProcess() public string Output { get; set; } /// - /// Gets a value indicating whether the process should watch for changes to template or content files. + /// Gets or sets a value indicating whether the process should watch for changes to template or content files. /// /// - /// true if should watch; otherwise, false. + /// true if the process should watch for changes; otherwise, false. /// [ExcludeFromCodeCoverage] private bool ShouldWatch { get; set; } @@ -87,16 +93,23 @@ public PreviewProcess() /// /// Start a new instance of the class using the incoming arguments. /// - /// The process arguments + /// The process arguments. /// A new instance of the class. [ExcludeFromCodeCoverage] public static PreviewProcess Create(string[] args) { var preview = new PreviewProcess(); - // deepcode ignore XmlInjection: XML is not used by this application, it is passed back to the user, deepcode ignore XXE: - preview.ParseArguments(args); - HandleNoArgumentsPassed(args, preview); + preview.Template = preview._args.ParsePath(args, "template"); + preview.Content = preview._args.ParsePath(args, "content"); + preview.Output = preview._args.ParsePath(args, "output"); + preview.ShouldWatch = PreviewProcessArguments.HasArgument(args, "watch"); + + if (args.Length == 0) + { + preview.WriteHelpOutput(); + } + if (preview.CanRender) { RenderAndWatch(preview); @@ -109,107 +122,6 @@ public static PreviewProcess Create(string[] args) return preview; } - /// Parses the arguments and sets process options. - /// - /// The arguments. Values are expected to be "--template", "--help", "--content", "--output" or - /// "--watch". - /// - private void ParseArguments(string[] args) - { - for (var index = 0; index < args.Length; index++) - { - var arg = args[index]; - var path = Directory.GetCurrentDirectory(); - ParseTemplate(args, index, arg, path); - ParseContent(args, index, arg, path); - ParseOutputResults(args, index, arg, path); - - // Switch watch param if needed - if (IsArgMatch(arg, "watch")) - { - ShouldWatch = true; - } - - // Show help info - if (IsArgMatch(arg, "help")) - { - WriteHelpOutput(); - } - } - } - - /// - /// Parses the output results file path if specified. - /// - /// The passed command arguments. - /// The parameter index. - /// The current argument. - /// The target path. - private void ParseOutputResults(string[] args, int index, string arg, string path) - { - if (!IsArgMatch(arg, "output") || index - 1 >= args.Length) - { - return; - } - - try - { - Output = Path.GetFullPath(args[index + 1], path); - } - catch - { - WriteErrorLine($"Invalid output path: {args[index + 1]}"); - } - } - - /// - /// Parses the incoming content file path if specified. - /// - /// The passed command arguments. - /// The parameter index. - /// The current argument. - /// The target path. - private void ParseContent(string[] args, int index, string arg, string path) - { - if (!IsArgMatch(arg, "content") || index - 1 >= args.Length) - { - return; - } - - try - { - Content = Path.GetFullPath(args[index + 1], path); - } - catch - { - WriteErrorLine($"Invalid content path: {args[index + 1]}"); - } - } - - /// - /// Parses the incoming template file path if specified. - /// - /// The passed command arguments. - /// The parameter index. - /// The current argument. - /// The target path. - private void ParseTemplate(string[] args, int index, string arg, string path) - { - if (!IsArgMatch(arg, "template") || index - 1 >= args.Length) - { - return; - } - - try - { - Template = Path.GetFullPath(args[index + 1], path); - } - catch - { - WriteErrorLine($"Invalid template path: {args[index + 1]}"); - } - } - /// /// Renders the output and watches for changes if specified. /// @@ -246,19 +158,6 @@ private static void LogMissingFiles(PreviewProcess preview) preview.LogMessage(); } - /// - /// Handles the scenario where no arguments are passed to the application. - /// - /// The array of arguments passed to the application. - /// The instance of to handle the output. - private static void HandleNoArgumentsPassed(string[] args, PreviewProcess preview) - { - if (args.Length == 0) - { - preview.WriteHelpOutput(); - } - } - /// /// Writes the help output. /// @@ -287,19 +186,6 @@ private static void WriteErrorLine(string error) Console.ForegroundColor = ConsoleColor.White; } - /// - /// Determines whether the argument matches the partial argument key name. - /// - /// The argument. - /// The key. - /// - /// true if argument found; otherwise, false. - /// - private static bool IsArgMatch(string arg, string key) - { - return string.CompareOrdinal(arg, "--" + key) == 0; - } - /// /// Renders the output using the specified properties of the instance. /// @@ -308,67 +194,99 @@ public string Render() { if (!CanRender) { - WriteErrorLine("Unable to render as inputs our outputs not found or not specified"); + WriteErrorLine("Unable to render as inputs or outputs not found or not specified"); return string.Empty; } - string content; - try + var content = ReadFileContent(Content); + if (string.IsNullOrEmpty(content)) { - content = File.ReadAllText(Content); + return string.Empty; } - catch (IOException) + + var template = ReadFileContent(Template); + if (string.IsNullOrEmpty(template)) { - // Lock issue, wait and retry - Thread.Sleep(TimeSpan.FromSeconds(1)); - return Render(); + return string.Empty; } - string template; + var parser = new LiquidParser(); + return !SetParserContent(parser, content) ? string.Empty : RenderTemplate(parser, template); + } + + /// + /// Reads the file content. + /// + /// The file path. + /// The operation is being retried. + /// The file content. + internal string ReadFileContent(string filePath, bool retry = false) + { try { - template = File.ReadAllText(Template); + return File.ReadAllText(filePath); } - catch (IOException) + catch { // Lock issue, wait and retry Thread.Sleep(TimeSpan.FromSeconds(1)); - return Render(); - } + if (!retry) + { + return ReadFileContent(filePath, true); + } - var parser = new LiquidParser(); + LogWarning($"Unable to read file: {filePath}"); + return string.Empty; + } + } - if (Content.ToLowerInvariant().EndsWith(".json")) + /// + /// Sets the parser content. + /// + /// The parser. + /// The content. + /// + /// true if the content was set; otherwise, false. + /// + private bool SetParserContent(LiquidParser parser, string content) + { + try { - try + if (Content.ToLowerInvariant().EndsWith(".json")) { parser.SetContentJson(content); } - catch (Exception e) - { - LogWarning(" Unable to read input JSON file", e); - return string.Empty; - } - } - - if (Content.ToLowerInvariant().EndsWith(".xml")) - { - try + else if (Content.ToLowerInvariant().EndsWith(".xml")) { parser.SetContentXml(content); } - catch (Exception ex) + else { - LogWarning(" Unable to read input XML file", ex); - return string.Empty; + WriteErrorLine("Unsupported content type"); + return false; } } + catch (Exception e) + { + LogWarning("Unable to set parser content", e); + return false; + } + return true; + } + + /// + /// Renders the template. + /// + /// The parser. + /// The template. + /// The output from the template. + private string RenderTemplate(LiquidParser parser, string template) + { try { var output = parser.Parse(template).Render(); File.WriteAllText(Output, output); - return output; } catch (Exception e) @@ -376,8 +294,6 @@ public string Render() WriteErrorLine($"Error: {e.Message}"); return string.Empty; } - - // TODO: Refactor this method } /// diff --git a/AzureLiquid.Preview/PreviewProcessArguments.cs b/AzureLiquid.Preview/PreviewProcessArguments.cs new file mode 100644 index 0000000..812a0d3 --- /dev/null +++ b/AzureLiquid.Preview/PreviewProcessArguments.cs @@ -0,0 +1,77 @@ +namespace AzureLiquid.Preview; + +/// +/// Handles the arguments passed to the preview process. +/// +public class PreviewProcessArguments +{ + /// + /// The current path of the process. + /// + private readonly string _path; + + /// + /// Initializes a new instance of the class. + /// + public PreviewProcessArguments() => _path = Directory.GetCurrentDirectory(); + + /// + /// Gets the index of the argument, if it exists. + /// + /// The arguments. + /// The key. + /// The index of the argument. + internal static int GetArgumentIndex(string[] args, string key) + { + for (int i = 0; i < args.Length; i++) + { + if (IsArgMatch(args[i], key)) + { + return i; + } + } + + return -1; + } + + /// + /// Determines whether the argument matches the partial argument key name. + /// + /// The argument. + /// The key. + /// + /// true if argument found; otherwise, false. + /// + internal static bool IsArgMatch(string arg, string key) + { + return string.CompareOrdinal(arg, "--" + key) == 0; + } + + /// + /// Gets a value indicating whether the specific argument key was found in the arguments. + /// + /// The passed arguments. + /// The key. + /// true if the argument was found, otherwise false. + public static bool HasArgument(string[] args, string key) => args?.Length > 0 && args.Any(arg => IsArgMatch(arg, key)); + + /// + /// Parses the argument value. + /// + /// The arguments. + /// The key. + /// The argument value. + public string ParsePath(string[] args, string key) + { + if (args == null || args.Length == 0) + { + return string.Empty; + } + + var index = GetArgumentIndex(args, key); + return + index == -1 || index - 1 >= args.Length + ? string.Empty // No match, or no arguments passed + : Path.GetFullPath(args[index + 1], _path); // Argument found, parsing path + } +} \ No newline at end of file diff --git a/AzureLiquid.Preview/Program.cs b/AzureLiquid.Preview/Program.cs index 238c068..bcc91a8 100644 --- a/AzureLiquid.Preview/Program.cs +++ b/AzureLiquid.Preview/Program.cs @@ -2,8 +2,9 @@ // Licensed under the open source Apache License, Version 2.0. // -// deepcode ignore XXE: All input is returned to original source and is not used internally - +using System.Runtime.CompilerServices; using AzureLiquid.Preview; +[assembly: InternalsVisibleTo("AzureLiquid.Tests")] + PreviewProcess.Create(args); \ No newline at end of file diff --git a/AzureLiquid.Tests/PreviewProcessArgumentsTests.cs b/AzureLiquid.Tests/PreviewProcessArgumentsTests.cs new file mode 100644 index 0000000..bd49493 --- /dev/null +++ b/AzureLiquid.Tests/PreviewProcessArgumentsTests.cs @@ -0,0 +1,100 @@ +// +// Licensed under the open source Apache License, Version 2.0. +// + +using AzureLiquid.Preview; +using FluentAssertions; +using Xunit; + +namespace AzureLiquid.Tests; + +/// +/// Unit tests for the class. +/// +public class PreviewProcessArgumentsTests +{ + /// + /// Tests the method. + /// + /// The array of arguments. + /// The key to search for. + /// The expected index of the key in the arguments array. + [Theory] + [InlineData(new[] { "--template", "template.liquid" }, "template", 0)] + [InlineData(new[] { "--content", "content.json" }, "content", 0)] + [InlineData(new[] { "--output", "output.txt" }, "output", 0)] + [InlineData(new[] { "--watch" }, "watch", 0)] + [InlineData(new[] { "--template", "template.liquid" }, "content", -1)] + public void GetArgumentIndex_ShouldReturnCorrectIndex(string[] args, string key, int expectedIndex) + { + // Act + var index = PreviewProcessArguments.GetArgumentIndex(args, key); + + // Assert + index.Should().Be(expectedIndex); + } + + /// + /// Tests the method. + /// + /// The argument to check. + /// The key to match against. + /// The expected result of the match. + [Theory] + [InlineData("--template", "template", true)] + [InlineData("--content", "content", true)] + [InlineData("--output", "output", true)] + [InlineData("--watch", "watch", true)] + [InlineData("--template", "content", false)] + public void IsArgMatch_ShouldReturnCorrectResult(string arg, string key, bool expectedResult) + { + // Act + var result = PreviewProcessArguments.IsArgMatch(arg, key); + + // Assert + result.Should().Be(expectedResult); + } + + /// + /// Tests the method. + /// + /// The array of arguments. + /// The key to search for. + /// The expected result of the search. + [Theory] + [InlineData(new[] { "--template", "template.liquid" }, "template", true)] + [InlineData(new[] { "--content", "content.json" }, "content", true)] + [InlineData(new[] { "--output", "output.txt" }, "output", true)] + [InlineData(new[] { "--watch" }, "watch", true)] + [InlineData(new[] { "--template", "template.liquid" }, "content", false)] + public void HasArgument_ShouldReturnCorrectResult(string[] args, string key, bool expectedResult) + { + // Act + var result = PreviewProcessArguments.HasArgument(args, key); + + // Assert + result.Should().Be(expectedResult); + } + + /// + /// Tests the method. + /// + /// The array of arguments. + /// The key to search for. + /// The expected path associated with the key. + [Theory] + [InlineData(new[] { "--template", "template.liquid" }, "template", "template.liquid")] + [InlineData(new[] { "--content", "content.json" }, "content", "content.json")] + [InlineData(new[] { "--output", "output.txt" }, "output", "output.txt")] + public void ParsePath_ShouldReturnCorrectPath(string[] args, string key, string expectedPath) + { + // Arrange + var previewArgs = new PreviewProcessArguments(); + + // Act + var path = previewArgs.ParsePath(args, key); + + // Assert + path.Should().Contain(expectedPath); + } +} \ No newline at end of file diff --git a/AzureLiquid.Tests/PreviewProcessTests.cs b/AzureLiquid.Tests/PreviewProcessTests.cs index 0a3e993..6b54340 100644 --- a/AzureLiquid.Tests/PreviewProcessTests.cs +++ b/AzureLiquid.Tests/PreviewProcessTests.cs @@ -72,7 +72,6 @@ public void EnsurePreviewParsingCSharpArguments() /// /// Ensure the preview process can be created from a set of arguments. /// - /// Determine if a log should be produced. /// First argument. /// Second argument. /// Third argument. @@ -82,16 +81,16 @@ public void EnsurePreviewParsingCSharpArguments() /// Seventh argument. /// Eighth argument. [Theory] - [InlineData(false, "", "", "", "", "", "", "", "")] - [InlineData(false, "--template", "./Resources/event.liquid", "--content", "./Resources/event.json", "--output", - "./Resources/preview.txt", "", "")] - [InlineData(false, "--template", "./Resources/event.liquid", "", "", "", "", "", "")] - [InlineData(false, "--watch", "", "", "", "", "", "", "")] - [InlineData(true, "--help", "", "", "", "", "", "", "")] - [InlineData(true, "--template", "./Resources/event_not_found.liquid", "--content", "./Resources/event.xml", - "--output", "./Resources/preview.txt", "", "")] - public void EnsureArgumentParsing(bool shouldLog, string arg1, string arg2, string arg3, string arg4, string arg5, - string arg6, string arg7, string arg8) + [InlineData("", "", "", "", "", "", "", "")] + [InlineData("--template", "./Resources/event.liquid", "--content", "./Resources/event.json", "--output", "./Resources/preview.txt", "", "")] + [InlineData("--template", "./Resources/event.liquid", "", "", "", "", "", "")] + [InlineData("--watch", "", "", "", "", "", "", "")] + [InlineData("--help", "", "", "", "", "", "", "")] + [InlineData("--template", "./Resources/event_not_found.liquid", "--content", "./Resources/event.xml", "--output", "./Resources/preview.txt", "", "")] + [InlineData("--template", "./Resources/empty.liquid", "--content", "./Resources/event.json", "--output", "./Resources/preview.txt", "", "")] + [InlineData("--template", "./Resources/empty.liquid", "--content", "./Resources/empty.json", "--output", "./Resources/preview.txt", "", "")] + [InlineData("--template", "./Resources/empty.liquid", "--content", "./Resources/empty.pdf", "--output", "./Resources/preview.txt", "", "")] + public void EnsureArgumentParsing(string arg1, string arg2, string arg3, string arg4, string arg5, string arg6, string arg7, string arg8) { // Arrange var args = new[] { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 }; @@ -101,17 +100,27 @@ public void EnsureArgumentParsing(bool shouldLog, string arg1, string arg2, stri // Assert preview.Should().NotBeNull("A preview process should have been created"); + } - if (shouldLog) - { - preview.Log.Should().NotBeEmpty("A log should have been created"); - } - else - { - preview.Log.Should().BeEmpty("No log should have been created"); - } + /// + /// Ensure the preview process cannot run with correct input. + /// + [Fact] + public void EnsureCannotRenderWithoutContent() + { + // Arrange + var instance = new PreviewProcess(); + + // Act + var result = instance.Render(); + + // Assert + result.Should().BeEmpty("A result should not have been created"); } + /// + /// Ensure the preview process can be created from a set of arguments. + /// [Fact] public void EnsureObjectCreation() { @@ -153,7 +162,41 @@ public void EnsureWatcher() instance.Log.Should().NotBeEmpty("A log should have been created"); } - #region Nested type: Arrangement + /// + /// Ensure the preview process can read a file. + /// + [Fact] + public void EnsureFileReadExceptionHandling() + { + // Arrange + var instance = new PreviewProcess(); + const string file = "notfound.liquid"; + + // Act + var result = instance.ReadFileContent(file); + + // Assert + result.Should().BeEmpty("A result should not have been created"); + instance.Log.Should().NotBeEmpty("A log should not have been created"); + } + + /// + /// Ensure the preview process can handle missing arguments. + /// + [Fact] + public void EnsureHelpMessageShown() + { + // Arrange + var empty = new string[0]; + var instance = PreviewProcess.Create(empty); + + // Act + var result = instance.Render(); + + // Assert + result.Should().BeEmpty("A result should not have been created"); + instance.Log.Should().NotBeEmpty("A log should not have been created"); + } /// /// Contains arranged values used for testing, containing mock instances and expected return values. @@ -222,6 +265,4 @@ public static string GetPath(string path) return Path.GetFullPath(path, basePath); } } - - #endregion } \ No newline at end of file diff --git a/AzureLiquid.Tests/Resources/empty.json b/AzureLiquid.Tests/Resources/empty.json new file mode 100644 index 0000000..e69de29 diff --git a/AzureLiquid.Tests/Resources/empty.liquid b/AzureLiquid.Tests/Resources/empty.liquid new file mode 100644 index 0000000..e69de29 diff --git a/Create-PullRequest.ps1 b/Create-PullRequest.ps1 new file mode 100644 index 0000000..538d650 --- /dev/null +++ b/Create-PullRequest.ps1 @@ -0,0 +1,130 @@ +Param ( + [switch]$all, + [string]$filter, + [switch]$help +) + +# Define the base branch (e.g., main or master) +$BASE_BRANCH = "main" + +# Function to display help message +function Show-Help { + Write-Output "Usage: .\New-ChangeSet.ps1 [--all] [--filter ] [--help]" + Write-Output "" + Write-Output "Options:" + Write-Output " --all Include 'Chores' and 'Other' sections in the output." + Write-Output " --filter Filter out commits matching the given regular expression." + Write-Output " --help Show this help message and exit." +} + +# Parse command-line arguments +$show_all = $false +$filter_regex = "" + +if ($help) { + Show-Help + exit 0 +} + +if ($all) { + $show_all = $true +} + +if ($filter) { + $filter_regex = $filter +} + +# Get the current branch name +$current_branch = git rev-parse --abbrev-ref HEAD + +# Extract the issue identifier from the branch name +$issue_identifier = if ($current_branch -match '(patch|feature)/[a-z0-9]+-[0-9]+') { $matches[0] -match '[A-Z]+-[0-9]+'; $matches[0] } + +# Fetch the commit messages from the current branch that are not in the base branch and reverse the order +$commits = git log "$BASE_BRANCH..HEAD" --pretty=format:"%s" | ForEach-Object { $_ } | Sort-Object { $_ } -Descending + +# Initialize the changeset +$changeset = "" + +# Initialize variables for each conventional commit type +$docs_commits = "" +$feat_commits = "" +$fix_commits = "" +$test_commits = "" +$chore_commits = "" +$ci_commits = "" +$other_commits = "" + +# Filter and format the commits +foreach ($commit in $commits) { + # Apply filter if specified + if ($filter_regex -ne "" -and $commit -match $filter_regex) { + continue + } + + # Extract the conventional commit type and the message + if ($commit -match '^(Merge.*|.*\(#\d+\))$') { + continue + } + + if ($commit -match '^(feat|fix|docs|test|chore|ci)(\([^\)]+\))?:\s*(.*)$') { + $commit_type = $matches[1] + $commit_message = $matches[3] + switch ($commit_type) { + "docs" { $docs_commits += "- $commit_message`n" } + "feat" { $feat_commits += "- $commit_message`n" } + "test" { $test_commits += "- $commit_message`n" } + "fix" { $fix_commits += "- $commit_message`n" } + "chore" { $chore_commits += "- $commit_message`n" } + default { $other_commits += "- $commit_message`n" } + } + } + else { + if ($show_all) { + $other_commits += "- $commit`n" + } + } +} + +# Format the changeset +if ($feat_commits -ne "") { + $changeset += "### Features:`n`n$feat_commits`n" +} +if ($fix_commits -ne "") { + $changeset += "### Fixes:`n`n$fix_commits`n" +} +if ($test_commits -ne "") { + $changeset += "### Tests:`n`n$test_commits`n" +} +if ($docs_commits -ne "") { + $changeset += "### Documentation:`n`n$docs_commits`n" +} +if ($ci_commits -ne "") { + $changeset += "### Continuous Integration:`n`n$ci_commits`n" +} +if ($show_all) { + if ($chore_commits -ne "") { + $changeset += "### Chores:`n`n$chore_commits`n" + } + if ($other_commits -ne "") { + $changeset += "### Other:`n`n$other_commits`n" + } +} + +# Find the first pull_request_template.md file in the repository +$template_file = Get-ChildItem -Recurse -Filter "pull_request_template.md" -Force | Select-Object -First 1 + +# Check if the template file was found +if (-not $template_file) { + Write-Warning "pull_request_template.md file not found in the repository." +} +else { + # Read the pull request template + $template_content = Get-Content -Path $template_file.FullName -Raw +} + +# Append updated content to end of the template +$updated_content = $template_content + $changeset + +# Output the updated content +Write-Output $updated_content \ No newline at end of file