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
+}