From 5bc3b51fc03cc992ac700cf472270e571cfaa5c2 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri <7004080+alirezanet@users.noreply.github.com> Date: Sun, 16 Jun 2024 19:18:05 +0200 Subject: [PATCH] Add FilteringRule to Husky tasks (#115) * test: add EchoWithInclude test * test: improve integration test speed * test: enable remote debugging inside the container * feat: add FilteringRules * test: test skip when no match files found * fix: update sln file * test: add integration tests --- Dockerfile | 33 +-- Husky.sln | 7 +- docs/.vuepress/public/schema.json | 13 +- src/Husky/Husky.csproj | 9 + src/Husky/Program.cs | 20 ++ src/Husky/TaskRunner/ArgumentParser.cs | 2 +- src/Husky/TaskRunner/ExecutableTaskFactory.cs | 37 ++- src/Husky/TaskRunner/FilteringRules.cs | 7 + src/Husky/TaskRunner/HuskyTask.cs | 1 + .../HuskyIntegrationTests.csproj | 8 + tests/HuskyIntegrationTests/Issue106Tests.cs | 279 ++++++++++++++++++ tests/HuskyIntegrationTests/Issue99Tests.cs | 30 +- tests/HuskyIntegrationTests/Readme.md | 8 + tests/HuskyIntegrationTests/Tests.cs | 63 ++-- .../Utilities/DockerFixture.cs | 104 ------- .../Utilities/DockerFixtureCollection.cs | 7 - .../Utilities/DockerHelper.cs | 98 ++++++ .../Utilities/DockerLogger.cs | 30 -- .../Utilities/Extensions.cs | 41 --- .../Utilities/GlobalImageBuilder.cs | 63 ++++ tests/HuskyIntegrationTests/xunit.runner.json | 4 + 21 files changed, 615 insertions(+), 249 deletions(-) create mode 100644 src/Husky/TaskRunner/FilteringRules.cs create mode 100644 tests/HuskyIntegrationTests/Issue106Tests.cs create mode 100644 tests/HuskyIntegrationTests/Readme.md delete mode 100644 tests/HuskyIntegrationTests/Utilities/DockerFixture.cs delete mode 100644 tests/HuskyIntegrationTests/Utilities/DockerFixtureCollection.cs create mode 100644 tests/HuskyIntegrationTests/Utilities/DockerHelper.cs delete mode 100644 tests/HuskyIntegrationTests/Utilities/DockerLogger.cs delete mode 100644 tests/HuskyIntegrationTests/Utilities/Extensions.cs create mode 100644 tests/HuskyIntegrationTests/Utilities/GlobalImageBuilder.cs create mode 100644 tests/HuskyIntegrationTests/xunit.runner.json diff --git a/Dockerfile b/Dockerfile index 2cf2112..5f6a4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,3 @@ -# This dockerfile is used in the integration tests - # Use the official .NET SDK image as a base FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env ARG RESOURCE_REAPER_SESSION_ID="00000000-0000-0000-0000-000000000000" @@ -8,22 +6,22 @@ LABEL "org.testcontainers.resource-reaper-session"=$RESOURCE_REAPER_SESSION_ID # Set the working directory WORKDIR /app -# Copy the .csproj file to the container -COPY src/Husky/Husky.csproj ./ - ENV HUSKY=0 -# Copy the remaining files to the container -COPY . ./ +# Copy the .csproj file to the container +COPY src/Husky/Husky.csproj ./src/Husky/ # Restore dependencies RUN dotnet restore /app/src/Husky -# Build the application -RUN dotnet build --no-restore -c Release -f net8.0 /app/src/Husky +# Copy the remaining files to the container +COPY . ./ -# Create a NuGet package -RUN dotnet pack --no-build --no-restore -c Release -o out /app/src/Husky/Husky.csproj -p:TargetFrameworks=net8.0 +# Build the application using the custom 'IntegrationTest' configuration +RUN dotnet build --no-restore -c IntegrationTest -f net8.0 /app/src/Husky/Husky.csproj -p:Version=99.1.1-test -p:TargetFrameworks=net8.0 + +# Create a NuGet package using the 'IntegrationTest' configuration +RUN dotnet pack --no-build --no-restore -c IntegrationTest -o out /app/src/Husky/Husky.csproj -p:Version=99.1.1-test -p:TargetFrameworks=net8.0 # Use the same .NET SDK image for the final stage FROM mcr.microsoft.com/dotnet/sdk:8.0 @@ -35,14 +33,15 @@ WORKDIR /app # Install Git RUN apt-get update && \ - apt-get install -y git + apt-get install -y git && \ + rm -rf /var/lib/apt/lists/* # Copy the NuGet package from the build-env to the runtime image COPY --from=build-env /app/out/*.nupkg /app/nupkg/ -# Install Husky tool and add the global tools path to the PATH -RUN dotnet tool install -g --no-cache --add-source /app/nupkg/ husky \ - && echo "export PATH=\$PATH:/root/.dotnet/tools" >> ~/.bashrc +# Install the specific version from the local source +RUN dotnet tool install -g husky --version 99.1.1-test --add-source /app/nupkg/ --no-cache + +RUN echo "export PATH=\$PATH:/root/.dotnet/tools" >> ~/.bashrc -# Set the entry point to a simple shell -ENTRYPOINT ["/bin/bash"] +CMD ["/root/.dotnet/tools/husky"] diff --git a/Husky.sln b/Husky.sln index 1ee66cd..37fbf60 100644 --- a/Husky.sln +++ b/Husky.sln @@ -13,20 +13,23 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + IntegrationTest|Any CPU = IntegrationTest|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {12AB4B33-47A6-49D5-872A-5BA6DD634E9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {12AB4B33-47A6-49D5-872A-5BA6DD634E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {12AB4B33-47A6-49D5-872A-5BA6DD634E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {12AB4B33-47A6-49D5-872A-5BA6DD634E9C}.Release|Any CPU.Build.0 = Release|Any CPU + {12AB4B33-47A6-49D5-872A-5BA6DD634E9C}.IntegrationTest|Any CPU.ActiveCfg = IntegrationTest|Any CPU + {12AB4B33-47A6-49D5-872A-5BA6DD634E9C}.IntegrationTest|Any CPU.Build.0 = IntegrationTest|Any CPU {57EE798B-0FE0-42A4-BDB9-D168109D3E7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {57EE798B-0FE0-42A4-BDB9-D168109D3E7D}.Debug|Any CPU.Build.0 = Debug|Any CPU {57EE798B-0FE0-42A4-BDB9-D168109D3E7D}.Release|Any CPU.ActiveCfg = Release|Any CPU {57EE798B-0FE0-42A4-BDB9-D168109D3E7D}.Release|Any CPU.Build.0 = Release|Any CPU + {57EE798B-0FE0-42A4-BDB9-D168109D3E7D}.Debug|Any CPU.Build.0 = Debug|Any CPU {FAF049CD-6E45-4A66-A88F-C2EB007DFCC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FAF049CD-6E45-4A66-A88F-C2EB007DFCC3}.Debug|Any CPU.Build.0 = Debug|Any CPU {FAF049CD-6E45-4A66-A88F-C2EB007DFCC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {FAF049CD-6E45-4A66-A88F-C2EB007DFCC3}.Release|Any CPU.Build.0 = Release|Any CPU + {FAF049CD-6E45-4A66-A88F-C2EB007DFCC3}.Debug|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {57EE798B-0FE0-42A4-BDB9-D168109D3E7D} = {302A5F25-CB44-4ED0-A65E-06C04648D211} diff --git a/docs/.vuepress/public/schema.json b/docs/.vuepress/public/schema.json index a808065..b196391 100644 --- a/docs/.vuepress/public/schema.json +++ b/docs/.vuepress/public/schema.json @@ -79,6 +79,11 @@ }, "description": "Glob pattern to exclude files." }, + "filteringRule": { + "$ref": "#/definitions/filteringRules", + "description": "The filtering rule for this task. Can be 'variable' or 'staged'.", + "default": "variable" + }, "windows": { "$ref": "#/definitions/windowsOverrides", "description": "Overrides all settings for Windows." @@ -211,6 +216,12 @@ "enum": ["relative", "absolute"], "description": "Specifies the file path style.", "default": "relative" - } + }, + "filteringRules": { + "type": "string", + "enum": ["variable", "staged"], + "default": "variable", + "description": "The filtering rule for this task. Can be 'variable' or 'staged'." + } } } diff --git a/src/Husky/Husky.csproj b/src/Husky/Husky.csproj index 86441d5..bcb6a29 100644 --- a/src/Husky/Husky.csproj +++ b/src/Husky/Husky.csproj @@ -26,6 +26,8 @@ snupkg true README.md + Debug;Release;IntegrationTest + AnyCPU @@ -60,4 +62,11 @@ + + + TEST + false + bin\IntegrationTest\ + obj\IntegrationTest\ + diff --git a/src/Husky/Program.cs b/src/Husky/Program.cs index 534d0cc..8db434f 100644 --- a/src/Husky/Program.cs +++ b/src/Husky/Program.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.IO.Abstractions; using System.Text.RegularExpressions; using CliFx; @@ -14,6 +15,10 @@ var exitCode = 0; +#if TEST +WaitForDebuggerIfNeeded(); +#endif + #if DEBUG "Starting development mode ... ".Log(ConsoleColor.DarkGray); while (true) @@ -73,3 +78,18 @@ ServiceProvider BuildServiceProvider() .AddTransient(); return services.BuildServiceProvider(); } + +#if TEST + static void WaitForDebuggerIfNeeded() + { + if (Environment.GetEnvironmentVariable("HUSKY_INTEGRATION_TEST") != "1") return; + Console.WriteLine("Waiting for debugger to attach..."); + + while (!Debugger.IsAttached) + { + Thread.Sleep(100); + } + + Console.WriteLine("Debugger attached."); + } +#endif diff --git a/src/Husky/TaskRunner/ArgumentParser.cs b/src/Husky/TaskRunner/ArgumentParser.cs index 05e67a4..91efa2c 100644 --- a/src/Husky/TaskRunner/ArgumentParser.cs +++ b/src/Husky/TaskRunner/ArgumentParser.cs @@ -302,7 +302,7 @@ private static void AddCustomArguments(List args, string[]? option "⚠️ No arguments passed to the run command".Husky(ConsoleColor.Yellow); } - private static Matcher GetPatternMatcher(HuskyTask task) + public static Matcher GetPatternMatcher(HuskyTask task) { var matcher = new Matcher(); var hasMatcher = false; diff --git a/src/Husky/TaskRunner/ExecutableTaskFactory.cs b/src/Husky/TaskRunner/ExecutableTaskFactory.cs index b5eecbd..132d14d 100644 --- a/src/Husky/TaskRunner/ExecutableTaskFactory.cs +++ b/src/Husky/TaskRunner/ExecutableTaskFactory.cs @@ -3,6 +3,7 @@ using Husky.Stdout; using Husky.Utils; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileSystemGlobbing; namespace Husky.TaskRunner; @@ -38,11 +39,8 @@ public ExecutableTaskFactory(IServiceProvider provider, IGit git, IArgumentParse var cwd = await _git.GetTaskCwdAsync(huskyTask); var argsInfo = await _argumentParser.ParseAsync(huskyTask, options.Arguments?.ToArray()); - if (huskyTask.Args != null && huskyTask.Args.Length > argsInfo.Length) - { - "💤 Skipped, no matched files".Husky(ConsoleColor.Blue); - return null; - } + if (await CheckIfWeShouldSkipTheTask(huskyTask, argsInfo)) + return null; // skip the task // check for chunk var totalCommandLength = argsInfo.Sum(q => q.Argument.Length) + huskyTask.Command.Length; @@ -65,6 +63,35 @@ public ExecutableTaskFactory(IServiceProvider provider, IGit git, IArgumentParse ); } + private async Task CheckIfWeShouldSkipTheTask(HuskyTask huskyTask, ArgumentInfo[] argsInfo) + { + if (huskyTask is { FilteringRule: FilteringRules.Variable, Args: not null } && huskyTask.Args.Length > argsInfo.Length) + { + "💤 Skipped, no matched files".Husky(ConsoleColor.Blue); + return true; + } + + if (huskyTask.FilteringRule != FilteringRules.Staged) return false; + + var stagedFiles = (await _git.GetStagedFilesAsync()) + .Where(q => !string.IsNullOrWhiteSpace(q)) + .ToArray(); + if (stagedFiles.Length == 0) + { + "💤 Skipped, no staged files".Husky(ConsoleColor.Blue); + return true; + } + + var matcher = ArgumentParser.GetPatternMatcher(huskyTask); + + // get match staged files with glob + var matches = matcher.Match(stagedFiles); + if (matches.HasMatches) return false; + "💤 Skipped, no staged matched files".Husky(ConsoleColor.Blue); + return true; + + } + private IExecutableTask CreateChunkTask( HuskyTask huskyTask, int totalCommandLength, diff --git a/src/Husky/TaskRunner/FilteringRules.cs b/src/Husky/TaskRunner/FilteringRules.cs new file mode 100644 index 0000000..690c869 --- /dev/null +++ b/src/Husky/TaskRunner/FilteringRules.cs @@ -0,0 +1,7 @@ +namespace Husky.TaskRunner; + +public enum FilteringRules +{ + Variable, + Staged, +} diff --git a/src/Husky/TaskRunner/HuskyTask.cs b/src/Husky/TaskRunner/HuskyTask.cs index cc56abc..e8c5811 100644 --- a/src/Husky/TaskRunner/HuskyTask.cs +++ b/src/Husky/TaskRunner/HuskyTask.cs @@ -11,6 +11,7 @@ public class HuskyTask public string? Group { get; set; } public string? Branch { get; set; } public HuskyTask? Windows { get; set; } + public FilteringRules FilteringRule { get; set; } = FilteringRules.Variable; public string[]? Include { get; set; } public string[]? Exclude { get; set; } } diff --git a/tests/HuskyIntegrationTests/HuskyIntegrationTests.csproj b/tests/HuskyIntegrationTests/HuskyIntegrationTests.csproj index 20e4701..f393df5 100644 --- a/tests/HuskyIntegrationTests/HuskyIntegrationTests.csproj +++ b/tests/HuskyIntegrationTests/HuskyIntegrationTests.csproj @@ -10,7 +10,9 @@ + + @@ -27,5 +29,11 @@ + + + + PreserveNewest + + diff --git a/tests/HuskyIntegrationTests/Issue106Tests.cs b/tests/HuskyIntegrationTests/Issue106Tests.cs new file mode 100644 index 0000000..5b12890 --- /dev/null +++ b/tests/HuskyIntegrationTests/Issue106Tests.cs @@ -0,0 +1,279 @@ +using System.Runtime.CompilerServices; +using DotNet.Testcontainers.Containers; +using FluentAssertions; + +namespace HuskyIntegrationTests; +public class Issue106Tests (ITestOutputHelper output) +{ + [Fact] + public async Task FilteringRuleNotDefined_WithInclude_ShouldNotSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "args": [ + "Husky.Net is awesome!" + ], + "include": [ + "client/**/*" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().NotContain(DockerHelper.Skipped); + } + + [Fact] + public async Task FilteringRuleNotDefined_WithStagedVariable_WithExcludeCommitedFile_ShouldSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "args": [ + "${staged}" + ], + "exclude": [ + "**/task-runner.json" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().Contain(DockerHelper.Skipped); + } + + [Fact] + public async Task FilteringRuleVariable_WithStagedVariable_WithExcludeCommitedFile_ShouldSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "filteringRule": "variable", + "args": [ + "${staged}" + ], + "exclude": [ + "**/task-runner.json" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().Contain(DockerHelper.Skipped); + } + + [Fact] + public async Task FilteringRuleVariable_WithStagedVariable_WithIncludeCommitedFile_ShouldNotSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "pathMode": "absolute", + "filteringRule": "variable", + "args": [ + "${staged}" + ], + "include": [ + "**/task-runner.json" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().NotContain(DockerHelper.Skipped); + result.Stderr.Should().Contain(".husky/task-runner.json"); + } + + [Fact] + public async Task FilteringRuleStaged_WithoutAnyMatchedFile_ShouldSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "filteringRule": "staged", + "args": [ + "Husky.Net is awesome!" + ], + "include": [ + "client/**/*" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().Contain(DockerHelper.Skipped); + } + + [Fact] + public async Task FilteringRuleStaged_WithoutIncludeAndExclude_ShouldNotSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "filteringRule": "staged", + "args": [ + "Husky.Net is awesome!" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().NotContain(DockerHelper.Skipped); + } + + [Fact] + public async Task FilteringRuleStaged_WithExcludeCommitedFile_ShouldSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "filteringRule": "staged", + "args": [ + "Husky.Net is awesome!" + ], + "exclude": [ + "**/task-runner.json" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().Contain(DockerHelper.Skipped); + } + + [Fact] + public async Task FilteringRuleStaged_WithIncludeCommitedFile_ShouldNotSkip() + { + // arrange + const string taskRunner = + """ + { + "tasks": [ + { + "name": "Echo", + "command": "echo", + "filteringRule": "staged", + "args": [ + "Husky.Net is awesome!" + ], + "include": [ + "**/task-runner.json" + ] + } + ] + } + """; + await using var c = await ArrangeContainer(taskRunner); + await c.BashAsync("git add ."); + + // act + var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'"); + + // assert + result.ExitCode.Should().Be(0); + result.Stderr.Should().NotContain(DockerHelper.Skipped); + } + + + private async Task ArrangeContainer(string taskRunner, [CallerMemberName] string name = null!) + { + var c = await DockerHelper.StartWithInstalledHusky(name); + await c.BashAsync("dotnet tool restore"); + await c.BashAsync("git add ."); + await c.UpdateTaskRunner(taskRunner); + await c.BashAsync("dotnet husky add pre-commit -c 'dotnet husky run'"); + return c; + } +} diff --git a/tests/HuskyIntegrationTests/Issue99Tests.cs b/tests/HuskyIntegrationTests/Issue99Tests.cs index b51b8ff..02da342 100644 --- a/tests/HuskyIntegrationTests/Issue99Tests.cs +++ b/tests/HuskyIntegrationTests/Issue99Tests.cs @@ -4,14 +4,13 @@ namespace HuskyIntegrationTests; -[Collection("docker fixture")] -public class Issue99Tests(DockerFixture docker, ITestOutputHelper output) : IClassFixture +public class Issue99Tests(ITestOutputHelper output) { [Fact] public async Task StagedFiles_ShouldPassToJbCleanup_WithASemicolonSeparator() { // arrange - var c = await ArrangeContainer(); + await using var c = await ArrangeContainer(); // add 4 c# files for (var i = 2; i <= 4; i++) @@ -28,19 +27,19 @@ public static void TestMethod() { } await c.BashAsync("git add ."); -// act + // act var result = await c.BashAsync(output, "git commit -m 'add 4 new csharp classes'"); -// assert + // assert result.ExitCode.Should().Be(0); - result.Stderr.Should().Contain(Extensions.SuccessfullyExecuted); + result.Stderr.Should().Contain(DockerHelper.SuccessfullyExecuted); } [Fact] public async Task StagedFiles_ShouldSkip_WhenNoMatchFilesFound() { // arrange - var c = await ArrangeContainer(); + await using var c = await ArrangeContainer(installJetBrains: false); await c.BashAsync("git add ."); // act @@ -48,16 +47,19 @@ public async Task StagedFiles_ShouldSkip_WhenNoMatchFilesFound() // assert result.ExitCode.Should().Be(0); - result.Stderr.Should().Contain(Extensions.Skipped); + result.Stderr.Should().Contain(DockerHelper.Skipped); } - private async Task ArrangeContainer([CallerMemberName] string name = null!) + private async Task ArrangeContainer([CallerMemberName] string name = null!, bool installJetBrains = true) { - var c = await docker.StartWithInstalledHusky(name); - await c.BashAsync("dotnet tool install JetBrains.ReSharper.GlobalTools"); - await c.BashAsync("dotnet tool restore"); - await c.BashAsync("git add ."); - await c.BashAsync("git commit -m 'add jb tool'"); + var c = await DockerHelper.StartWithInstalledHusky(name); + if (installJetBrains) + { + await c.BashAsync("dotnet tool install JetBrains.ReSharper.GlobalTools --version 2024.1.3"); + await c.BashAsync("dotnet tool restore"); + await c.BashAsync("git add ."); + await c.BashAsync("git commit -m 'add jb tool'"); + } const string tasks = """ diff --git a/tests/HuskyIntegrationTests/Readme.md b/tests/HuskyIntegrationTests/Readme.md new file mode 100644 index 0000000..e9f1dac --- /dev/null +++ b/tests/HuskyIntegrationTests/Readme.md @@ -0,0 +1,8 @@ +To debug the tests remotely: + +1. Run the test in debug mode. +2. The test will freeze when Husky processes run. +3. In Rider, go to "Attach to Remote Process." +4. Select Docker and locate the test container. +5. Install remote debugging tools and select the running .NET process. +6. It should hit the breakpoint in the code diff --git a/tests/HuskyIntegrationTests/Tests.cs b/tests/HuskyIntegrationTests/Tests.cs index 7fb2ed7..a633936 100644 --- a/tests/HuskyIntegrationTests/Tests.cs +++ b/tests/HuskyIntegrationTests/Tests.cs @@ -2,39 +2,48 @@ namespace HuskyIntegrationTests; -[Collection("docker fixture")] -public class Tests(DockerFixture docker, ITestOutputHelper output) +public class Tests(ITestOutputHelper output) { [Fact] -public async Task IntegrationTestCopyFolder() -{ - var c = await docker.CopyAndStartAsync(nameof(TestProjectBase)); + public async Task IntegrationTestCopyFolder() + { + await using var c = await DockerHelper.StartContainerAsync(nameof(TestProjectBase)); - await c.BashAsync("git init"); - await c.BashAsync("dotnet new tool-manifest"); - await c.BashAsync("dotnet tool install --no-cache --add-source /app/nupkg/ husky"); - await c.BashAsync("dotnet tool restore"); - await c.BashAsync("dotnet husky install"); - var result = await c.BashAsync(output, "dotnet husky run"); + await c.BashAsync("git init"); + await c.BashAsync("dotnet new tool-manifest"); + await c.BashAsync("dotnet tool install --no-cache --add-source /app/nupkg/ husky --version 99.1.1-test"); + await c.BashAsync("dotnet tool restore"); + await c.BashAsync("dotnet husky install"); + var result = await c.BashAsync(output, "dotnet husky run"); - result.Stdout.Should().Contain(Extensions.SuccessfullyExecuted); - result.ExitCode.Should().Be(0); -} + result.Stdout.Should().Contain(DockerHelper.SuccessfullyExecuted); + result.ExitCode.Should().Be(0); + } -[Fact] -public async Task IntegrationTest() -{ - var c = await docker.StartAsync(); + [Fact] + public async Task IntegrationTest() + { + await using var c = await DockerHelper.StartContainerAsync(); - await c.BashAsync("dotnet new classlib"); - await c.BashAsync("dotnet new tool-manifest"); - await c.BashAsync("dotnet tool install --no-cache --add-source /app/nupkg/ husky"); - await c.BashAsync("git init"); - await c.BashAsync("dotnet husky install"); - var result = await c.BashAsync(output, "dotnet husky run"); + await c.BashAsync("dotnet new classlib"); + await c.BashAsync("dotnet new tool-manifest"); + await c.BashAsync("dotnet tool install --no-cache --add-source /app/nupkg/ husky --version 99.1.1-test"); + await c.BashAsync("git init"); + await c.BashAsync("dotnet husky install"); + var result = await c.BashAsync(output, "dotnet husky run"); - result.Stdout.Should().Contain(Extensions.SuccessfullyExecuted); - result.ExitCode.Should().Be(0); -} + result.Stdout.Should().Contain(DockerHelper.SuccessfullyExecuted); + result.ExitCode.Should().Be(0); + } + + [Fact] + public async Task CheckVersion() + { + await using var c = await DockerHelper.StartWithInstalledHusky(); + var result = await c.BashAsync(output, "dotnet husky --version"); + + result.Stdout.Should().Contain("99.1.1"); + result.ExitCode.Should().Be(0); + } } diff --git a/tests/HuskyIntegrationTests/Utilities/DockerFixture.cs b/tests/HuskyIntegrationTests/Utilities/DockerFixture.cs deleted file mode 100644 index 8d775a5..0000000 --- a/tests/HuskyIntegrationTests/Utilities/DockerFixture.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Runtime.CompilerServices; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; -using DotNet.Testcontainers.Images; -using HuskyIntegrationTests; - -public class DockerFixture : IAsyncDisposable -{ - public IFutureDockerImage? Image { get; set; } - - private void BuildImage() - { - Image = new ImageFromDockerfileBuilder() - .WithBuildArgument("RESOURCE_REAPER_SESSION_ID", ResourceReaper.DefaultSessionId.ToString("D")) - .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), string.Empty) - .WithDockerfile("Dockerfile") - .WithName("husky") - .WithCleanUp(false) - .Build(); - - Image.CreateAsync().GetAwaiter().GetResult(); - } - - public async Task CopyAndStartAsync(string folderNameToCopy, [CallerMemberName] string name = null!) - { - var container = new ContainerBuilder() - .WithResourceMapping(GetTestFolderPath(folderNameToCopy), "/test/") - .WithName(GenerateContainerName(name)) - .WithImage("husky") - .WithWorkingDirectory("/test/") - .WithEntrypoint("/bin/bash", "-c") - .WithCommand("tail -f /dev/null") - .WithImagePullPolicy(response => - { - if (response == null) - { - BuildImage(); - } - - return false; - }) - .Build(); - - await container.StartAsync(); - return container; - } - - public async Task StartAsync([CallerMemberName] string name = null!) - { - var container = new ContainerBuilder() - .WithName(GenerateContainerName(name)) - .WithImage("husky") - .WithWorkingDirectory("/test/") - .WithEntrypoint("/bin/bash", "-c") - .WithCommand("tail -f /dev/null") - .WithImagePullPolicy(response => - { - if (response == null) - { - BuildImage(); - } - - return false; - }) - .Build(); - - await container.StartAsync(); - return container; - } - - - public async Task StartWithInstalledHusky([CallerMemberName] string name = null!) - { - var c = await CopyAndStartAsync(nameof(TestProjectBase), name); - await c.BashAsync("git init"); - await c.BashAsync("dotnet new tool-manifest"); - await c.BashAsync("dotnet tool install --no-cache --add-source /app/nupkg/ husky"); - await c.BashAsync("dotnet tool restore"); - await c.BashAsync("dotnet husky install"); - await c.BashAsync("git config --global user.email \"you@example.com\""); - await c.BashAsync("git config --global user.name \"Your Name\""); - await c.BashAsync("git add ."); - await c.BashAsync("git commit -m 'initial commit'"); - return c; - } - - - private static string GenerateContainerName(string name) - { - return $"{name}-{Guid.NewGuid().ToString("N")[..4]}"; - } - - - private static string GetTestFolderPath(string folderName) - { - var baseDirectory = CommonDirectoryPath.GetProjectDirectory().DirectoryPath; - return Path.Combine(baseDirectory, folderName); - } - - public ValueTask DisposeAsync() - { - return Image?.DisposeAsync() ?? ValueTask.CompletedTask; - } -} diff --git a/tests/HuskyIntegrationTests/Utilities/DockerFixtureCollection.cs b/tests/HuskyIntegrationTests/Utilities/DockerFixtureCollection.cs deleted file mode 100644 index 399365b..0000000 --- a/tests/HuskyIntegrationTests/Utilities/DockerFixtureCollection.cs +++ /dev/null @@ -1,7 +0,0 @@ -[CollectionDefinition("docker fixture")] -public class DockerFixtureCollection : ICollectionFixture -{ - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. -} diff --git a/tests/HuskyIntegrationTests/Utilities/DockerHelper.cs b/tests/HuskyIntegrationTests/Utilities/DockerHelper.cs new file mode 100644 index 0000000..d98f696 --- /dev/null +++ b/tests/HuskyIntegrationTests/Utilities/DockerHelper.cs @@ -0,0 +1,98 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using HuskyIntegrationTests.Utilities; + +namespace HuskyIntegrationTests; + +public static class DockerHelper +{ + public const string SuccessfullyExecuted = "✔ Successfully executed"; + public const string Skipped = "💤 Skipped,"; + + public static async Task BashAsync(this IContainer container, params string[] command) + { + var result = await container.ExecAsync(["/bin/bash", "-c", ..command]); + if (result.ExitCode != 0) + throw new Exception(result.Stderr + result.Stdout); + return result; + } + + public static async Task BashAsync(this IContainer container, ITestOutputHelper output, params string[] command) + { + var result = await container.ExecAsync(["/bin/bash", "-c", ..command]); + output.WriteLine($"{string.Join(" ", command)}:"); + + if (!string.IsNullOrEmpty(result.Stdout)) + output.WriteLine(result.Stdout); + + if (!string.IsNullOrEmpty(result.Stderr)) + output.WriteLine(result.Stderr); + + return result; + } + + public static Task UpdateTaskRunner(this IContainer container, string content) + { + return container.BashAsync($"echo -e '{content}' > /test/.husky/task-runner.json"); + } + + public static Task AddCsharpClass(this IContainer container, string content, string fileName = "Class2.cs") + { + return container.BashAsync($"echo -e '{content}' > /test/{fileName}"); + } + + public static async Task StartContainerAsync(string? folderNameToCopy = null, [CallerMemberName] string name = null!) + { + await GlobalImageBuilder.BuildImageAsync(); + var builder = new ContainerBuilder() + .WithName(GenerateContainerName(name)) + .WithImage("husky") + .WithWorkingDirectory("/test/") + .WithEntrypoint("/bin/bash", "-c") + .WithCleanUp(true) + .WithCommand("tail -f /dev/null"); + + if (Debugger.IsAttached) + { + builder = builder.WithEnvironment("HUSKY_INTEGRATION_TEST", "1"); + } + + if (!string.IsNullOrEmpty(folderNameToCopy)) + { + builder = builder.WithResourceMapping(GetTestFolderPath(folderNameToCopy), "/test/"); + } + + var container = builder.Build(); + await container.StartAsync(); + return container; + } + + public static async Task StartWithInstalledHusky([CallerMemberName] string name = null!) + { + await GlobalImageBuilder.BuildImageAsync(); + var c = await StartContainerAsync(nameof(TestProjectBase), name); + await c.BashAsync("git init"); + await c.BashAsync("dotnet new tool-manifest"); + await c.BashAsync("dotnet tool install --no-cache --add-source /app/nupkg/ husky --version 99.1.1-test"); + await c.BashAsync("dotnet tool restore"); + await c.BashAsync("dotnet husky install"); + await c.BashAsync("git config --global user.email \"you@example.com\""); + await c.BashAsync("git config --global user.name \"Your Name\""); + await c.BashAsync("git add ."); + await c.BashAsync("git commit -m 'initial commit'"); + return c; + } + + private static string GenerateContainerName(string name) + { + return $"{name}-{Guid.NewGuid().ToString("N")[..4]}"; + } + + private static string GetTestFolderPath(string folderName) + { + var baseDirectory = CommonDirectoryPath.GetProjectDirectory().DirectoryPath; + return Path.Combine(baseDirectory, folderName); + } +} diff --git a/tests/HuskyIntegrationTests/Utilities/DockerLogger.cs b/tests/HuskyIntegrationTests/Utilities/DockerLogger.cs deleted file mode 100644 index fd4da4a..0000000 --- a/tests/HuskyIntegrationTests/Utilities/DockerLogger.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace HuskyIntegrationTests; - -public class DockerLogger(ITestOutputHelper output) : ILogger -{ - public IDisposable BeginScope(TState state) -{ - return null!; // You can implement if needed -} - -public bool IsEnabled(LogLevel logLevel) -{ - // Adjust the log level as needed - return true; -} - -public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) -{ - if (!IsEnabled(logLevel)) - { - return; - } - - var logMessage = formatter(state, exception); - - // Write to ITestOutputHelper - output.WriteLine($"[{logLevel}] {logMessage}"); -} -} diff --git a/tests/HuskyIntegrationTests/Utilities/Extensions.cs b/tests/HuskyIntegrationTests/Utilities/Extensions.cs deleted file mode 100644 index 39d5d69..0000000 --- a/tests/HuskyIntegrationTests/Utilities/Extensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using DotNet.Testcontainers.Containers; - -namespace HuskyIntegrationTests; - -public static class Extensions -{ - public const string SuccessfullyExecuted = "✔ Successfully executed"; - public const string Skipped = "💤 Skipped, no matched files"; - - public static async Task BashAsync(this IContainer container, params string[] command) - { - var result = await container.ExecAsync(["/bin/bash", "-c", ..command]); - if (result.ExitCode != 0) - throw new Exception(result.Stderr); - return result; - } - - public static async Task BashAsync(this IContainer container, ITestOutputHelper output, params string[] command) - { - var result = await container.ExecAsync(["/bin/bash", "-c", ..command]); - output.WriteLine($"{string.Join(" ", command)}:"); - - if (!string.IsNullOrEmpty(result.Stdout)) - output.WriteLine(result.Stdout); - - if (!string.IsNullOrEmpty(result.Stderr)) - output.WriteLine(result.Stderr); - - return result; - } - - public static Task UpdateTaskRunner(this IContainer container, string content) - { - return container.BashAsync($"echo -e '{content}' > /test/.husky/task-runner.json"); - } - - public static Task AddCsharpClass(this IContainer container, string content, string fileName = "Class2.cs") - { - return container.BashAsync($"echo -e '{content}' > /test/{fileName}"); - } -} diff --git a/tests/HuskyIntegrationTests/Utilities/GlobalImageBuilder.cs b/tests/HuskyIntegrationTests/Utilities/GlobalImageBuilder.cs new file mode 100644 index 0000000..d157bff --- /dev/null +++ b/tests/HuskyIntegrationTests/Utilities/GlobalImageBuilder.cs @@ -0,0 +1,63 @@ +using Docker.DotNet.Models; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; + +namespace HuskyIntegrationTests.Utilities; + +public static class GlobalImageBuilder +{ + /// + /// Set this value to false if you don't want the image to be removed after the test + /// This is useful if you want to debug the image, or fix tests issues + /// + private const bool RemoveHuskyImageAfterTest = true; + + + private static bool _imageBuilt; + private static readonly SemaphoreSlim SemaphoreSlim = new(1, 1); + + public static async ValueTask BuildImageAsync() + { + if (_imageBuilt) + { + return; + } + + await SemaphoreSlim.WaitAsync(); + try + { + if (!_imageBuilt && !await ImageExistsAsync("husky")) + { + var image = new ImageFromDockerfileBuilder() + .WithBuildArgument("RESOURCE_REAPER_SESSION_ID", ResourceReaper.DefaultSessionId.ToString("D")) + .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), string.Empty) + .WithDockerfile("Dockerfile") + .WithName("husky") + .WithCleanUp(RemoveHuskyImageAfterTest) + .Build(); + + await image.CreateAsync(); + _imageBuilt = true; + } + } + finally + { + SemaphoreSlim.Release(); + } + } + + private static async Task ImageExistsAsync(string imageName) + { + var clientConfiguration = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration(); + using var dockerClient = clientConfiguration.CreateClient(); + var images = await dockerClient.Images.ListImagesAsync(new ImagesListParameters + { + Filters = new Dictionary> + { + ["reference"] = new Dictionary { [imageName] = true } + } + }); + return _imageBuilt = images.Count > 0; + } +} diff --git a/tests/HuskyIntegrationTests/xunit.runner.json b/tests/HuskyIntegrationTests/xunit.runner.json new file mode 100644 index 0000000..b63d12d --- /dev/null +++ b/tests/HuskyIntegrationTests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "showLiveOutput": true +}