From c50354a28f1db4066d048f502827b1ed95827cc4 Mon Sep 17 00:00:00 2001 From: Valentin Date: Thu, 19 Dec 2024 15:56:35 +0100 Subject: [PATCH] Add mutation commit message --- .github/workflows/ci.yml | 8 ++ Pipeline/Build.MutationTests.cs | 171 +++++++++++++++++++++++++++----- 2 files changed, 156 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30c6b455..5061b8e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,8 @@ jobs: env: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} DOTNET_NOLOGO: true + permissions: + pull-requests: write steps: - uses: actions/checkout@v4 with: @@ -85,6 +87,8 @@ jobs: 8.0.x - name: Run mutation tests run: ./build.sh MutationTestsLinux + env: + GithubToken: ${{ secrets.GITHUB_TOKEN }} mutation-tests-windows: name: "Mutation tests (Windows)" @@ -93,6 +97,8 @@ jobs: env: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} DOTNET_NOLOGO: true + permissions: + pull-requests: write steps: - uses: actions/checkout@v4 with: @@ -106,6 +112,8 @@ jobs: 8.0.x - name: Run mutation tests run: ./build.ps1 MutationTestsWindows + env: + GithubToken: ${{ secrets.GITHUB_TOKEN }} static-code-analysis: name: "Static code analysis" diff --git a/Pipeline/Build.MutationTests.cs b/Pipeline/Build.MutationTests.cs index 2469789a..cb4f6a7a 100644 --- a/Pipeline/Build.MutationTests.cs +++ b/Pipeline/Build.MutationTests.cs @@ -1,14 +1,16 @@ using Nuke.Common; using Nuke.Common.IO; -using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; +using Octokit; using Serilog; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using static Nuke.Common.Tools.DotNet.DotNetTasks; +using Project = Nuke.Common.ProjectModel.Project; // ReSharper disable AllUnderscoreLocalParameterName @@ -16,22 +18,33 @@ namespace Build; partial class Build { + static Dictionary MutationCommentBody = new(); + AbsolutePath StrykerToolPath => TestResultsDirectory / "dotnet-stryker"; + AbsolutePath StrykerOutputDirectory => ArtifactsDirectory / "Stryker"; + Target MutationTests => _ => _ .DependsOn(MutationTestsWindows) - .DependsOn(MutationTestsLinux); + .DependsOn(MutationTestsLinux) + .DependsOn(MutationComment); - Target MutationTestsWindows => _ => _ - .DependsOn(Compile) + Target MutationTestPreparation => _ => _ .Executes(() => { - AbsolutePath toolPath = TestResultsDirectory / "dotnet-stryker"; - AbsolutePath configFile = toolPath / "Stryker.Config.json"; - toolPath.CreateOrCleanDirectory(); + StrykerToolPath.CreateOrCleanDirectory(); DotNetToolInstall(_ => _ .SetPackageName("dotnet-stryker") - .SetToolInstallationPath(toolPath)); + .SetToolInstallationPath(StrykerToolPath)); + StrykerOutputDirectory.CreateOrCleanDirectory(); + }); + + Target MutationTestsWindows => _ => _ + .DependsOn(Compile) + .DependsOn(MutationTestPreparation) + .Executes(() => + { + AbsolutePath configFile = StrykerToolPath / "Stryker.Config.json"; Dictionary projects = new() { { Solution.Testably_Abstractions_AccessControl, [Solution.Tests.Testably_Abstractions_AccessControl_Tests] }, @@ -82,12 +95,12 @@ partial class Build Log.Debug($"Created '{configFile}':{Environment.NewLine}{configText}"); string arguments = IsServerBuild - ? $"-f \"{configFile}\" -r \"Dashboard\" -r \"cleartext\"" - : $"-f \"{configFile}\" -r \"cleartext\""; + ? $"-f \"{configFile}\" -O \"{StrykerOutputDirectory}\" -r \"Markdown\" -r \"Dashboard\" -r \"cleartext\"" + : $"-f \"{configFile}\" -O \"{StrykerOutputDirectory}\" -r \"Markdown\" -r \"cleartext\""; string executable = EnvironmentInfo.IsWin ? "dotnet-stryker.exe" : "dotnet-stryker"; IProcess process = ProcessTasks.StartProcess( - Path.Combine(toolPath, executable), + Path.Combine(StrykerToolPath, executable), arguments, Solution.Directory) .AssertWaitForExit(); @@ -96,21 +109,17 @@ partial class Build Assert.Fail( $"Stryker did not execute successfully for {project.Key.Name}: (exit code {process.ExitCode})."); } + + MutationCommentBody.Add(project.Key.Name, CreateMutationCommentBody(project.Key.Name)); } }); Target MutationTestsLinux => _ => _ .DependsOn(Compile) + .DependsOn(MutationTestPreparation) .Executes(() => { - AbsolutePath toolPath = TestResultsDirectory / "dotnet-stryker"; - AbsolutePath configFile = toolPath / "Stryker.Config.json"; - toolPath.CreateOrCleanDirectory(); - - DotNetToolInstall(_ => _ - .SetPackageName("dotnet-stryker") - .SetToolInstallationPath(toolPath)); - + AbsolutePath configFile = StrykerToolPath / "Stryker.Config.json"; Dictionary projects = new() { { Solution.Testably_Abstractions_Testing, [ Solution.Tests.Testably_Abstractions_Testing_Tests, Solution.Tests.Testably_Abstractions_Tests ] }, @@ -187,12 +196,12 @@ partial class Build Log.Debug($"Created '{configFile}':{Environment.NewLine}{configText}"); string arguments = IsServerBuild - ? $"-f \"{configFile}\" -r \"Dashboard\" -r \"cleartext\"" - : $"-f \"{configFile}\" -r \"cleartext\""; + ? $"-f \"{configFile}\" -O \"{StrykerOutputDirectory}\" -r \"Markdown\" -r \"Dashboard\" -r \"cleartext\"" + : $"-f \"{configFile}\" -O \"{StrykerOutputDirectory}\" -r \"Markdown\" -r \"cleartext\""; string executable = EnvironmentInfo.IsWin ? "dotnet-stryker.exe" : "dotnet-stryker"; IProcess process = ProcessTasks.StartProcess( - Path.Combine(toolPath, executable), + Path.Combine(StrykerToolPath, executable), arguments, Solution.Directory) .AssertWaitForExit(); @@ -201,8 +210,124 @@ partial class Build Assert.Fail( $"Stryker did not execute successfully for {project.Key.Name}: (exit code {process.ExitCode})."); } + + MutationCommentBody.Add(project.Key.Name, CreateMutationCommentBody(project.Key.Name)); } }); - + + Target MutationComment => _ => _ + .After(MutationTestsLinux) + .After(MutationTestsWindows) + .OnlyWhenDynamic(() => GitHubActions.IsPullRequest) + .Executes(async () => + { + int? prId = GitHubActions.PullRequestNumber; + Log.Debug("Pull request number: {PullRequestId}", prId); + if (MutationCommentBody.Count == 0) + { + return; + } + + if (prId != null) + { + GitHubClient gitHubClient = new(new ProductHeaderValue("Nuke")); + Credentials tokenAuth = new(GithubToken); + gitHubClient.Credentials = tokenAuth; + IReadOnlyList comments = + await gitHubClient.Issue.Comment.GetAllForIssue("Textably", "Textably.Abstractions", prId.Value); + IssueComment? existingComment = null; + Log.Information($"Found {comments.Count} comments"); + foreach (IssueComment comment in comments) + { + if (comment.Body.Contains("## :alien: Mutation Results")) + { + Log.Information($"Found comment: {comment.Body}"); + existingComment = comment; + } + } + + if (existingComment == null) + { + string body = "## :alien: Mutation Results" + + Environment.NewLine + + $"[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FTextably%2FTextably.Abstractions%2Fpull/{prId}/merge)](https://dashboard.stryker-mutator.io/reports/github.com/Textably/Textably.Abstractions/pull/{prId}/merge)" + + Environment.NewLine + + string.Join(Environment.NewLine, MutationCommentBody.Values); + + Log.Information($"Create comment:\n{body}"); + await gitHubClient.Issue.Comment.Create("Textably", "Textably.Abstractions", prId.Value, body); + } + else + { + string body = existingComment.Body; + foreach ((var project, var value) in MutationCommentBody) + { + body = ReplaceProject(body, project, value); + } + + Log.Information($"Update comment:\n{body}"); + await gitHubClient.Issue.Comment.Update("Textably", "Textably.Abstractions", existingComment.Id, body); + } + } + }); + + string ReplaceProject(string body, string project, string value) + { + var startIndex = body.IndexOf($"", StringComparison.OrdinalIgnoreCase); + var endIndex = body.IndexOf($"", StringComparison.OrdinalIgnoreCase); + if (startIndex >= 0 && endIndex > startIndex) + { + var prefix = body.Substring(0, startIndex); + var suffix = body.Substring(endIndex + 1); + return prefix + value + suffix; + } + return body + Environment.NewLine + value + } + + string CreateMutationCommentBody(string projectName) + { + string[] fileContent = File.ReadAllLines(ArtifactsDirectory / "Stryker" / "reports" / "mutation-report.md"); + StringBuilder sb = new(); + sb.AppendLine($""); + sb.AppendLine($"### {projectName}"); + sb.AppendLine("
"); + sb.AppendLine("Details"); + sb.AppendLine(); + int count = 0; + foreach (string line in fileContent.Skip(1)) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + if (line.StartsWith("#")) + { + if (++count == 1) + { + sb.AppendLine(); + sb.AppendLine("
"); + sb.AppendLine(); + } + + sb.AppendLine("##" + line); + continue; + } + + if (count == 0 && + line.StartsWith("|") && + line.Contains("| N\\/A")) + { + continue; + } + + sb.AppendLine(line); + } + + sb.AppendLine($""); + string body = sb.ToString(); + return body; + } + static string PathForJson(Project project) => $"\"{project.Path.ToString().Replace(@"\", @"\\")}\""; }