Skip to content

Commit

Permalink
Add FilteringRule to Husky tasks (#115)
Browse files Browse the repository at this point in the history
* 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
alirezanet authored Jun 16, 2024
1 parent 91383d1 commit 5bc3b51
Showing 21 changed files with 615 additions and 249 deletions.
33 changes: 16 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
7 changes: 5 additions & 2 deletions Husky.sln
Original file line number Diff line number Diff line change
@@ -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}
13 changes: 12 additions & 1 deletion docs/.vuepress/public/schema.json
Original file line number Diff line number Diff line change
@@ -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'."
}
}
}
9 changes: 9 additions & 0 deletions src/Husky/Husky.csproj
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<IncludeSymbols>true</IncludeSymbols>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Configurations>Debug;Release;IntegrationTest</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="templates\husky.sh" />
@@ -60,4 +62,11 @@
<Exec Command="dotnet tool restore" StandardOutputImportance="Low" StandardErrorImportance="High" />
<Exec Command="dotnet husky install" StandardOutputImportance="Low" StandardErrorImportance="High" WorkingDirectory="..\.." />
</Target>

<PropertyGroup Condition="'$(Configuration)' == 'IntegrationTest'">
<DefineConstants>TEST</DefineConstants>
<Optimize>false</Optimize>
<OutputPath>bin\IntegrationTest\</OutputPath>
<IntermediateOutputPath>obj\IntegrationTest\</IntermediateOutputPath>
</PropertyGroup>
</Project>
20 changes: 20 additions & 0 deletions src/Husky/Program.cs
Original file line number Diff line number Diff line change
@@ -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<CleanupCommand>();
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
2 changes: 1 addition & 1 deletion src/Husky/TaskRunner/ArgumentParser.cs
Original file line number Diff line number Diff line change
@@ -302,7 +302,7 @@ private static void AddCustomArguments(List<ArgumentInfo> 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;
37 changes: 32 additions & 5 deletions src/Husky/TaskRunner/ExecutableTaskFactory.cs
Original file line number Diff line number Diff line change
@@ -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<bool> 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,
7 changes: 7 additions & 0 deletions src/Husky/TaskRunner/FilteringRules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Husky.TaskRunner;

public enum FilteringRules
{
Variable,
Staged,
}
1 change: 1 addition & 0 deletions src/Husky/TaskRunner/HuskyTask.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
8 changes: 8 additions & 0 deletions tests/HuskyIntegrationTests/HuskyIntegrationTests.csproj
Original file line number Diff line number Diff line change
@@ -10,7 +10,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Meziantou.Xunit.ParallelTestFramework" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
<PackageReference Include="Testcontainers" Version="3.6.0" />
<PackageReference Include="xunit" Version="2.4.2"/>
@@ -27,5 +29,11 @@
<ItemGroup>
<Folder Include="TestProjectBase\" />
</ItemGroup>

<ItemGroup>
<Content Include="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
279 changes: 279 additions & 0 deletions tests/HuskyIntegrationTests/Issue106Tests.cs
Original file line number Diff line number Diff line change
@@ -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<IContainer> 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;
}
}
30 changes: 16 additions & 14 deletions tests/HuskyIntegrationTests/Issue99Tests.cs
Original file line number Diff line number Diff line change
@@ -4,14 +4,13 @@

namespace HuskyIntegrationTests;

[Collection("docker fixture")]
public class Issue99Tests(DockerFixture docker, ITestOutputHelper output) : IClassFixture<DockerFixture>
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,36 +27,39 @@ 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
var result = await c.BashAsync(output, "git commit -m 'add task-runner.json'");

// assert
result.ExitCode.Should().Be(0);
result.Stderr.Should().Contain(Extensions.Skipped);
result.Stderr.Should().Contain(DockerHelper.Skipped);
}

private async Task<IContainer> ArrangeContainer([CallerMemberName] string name = null!)
private async Task<IContainer> 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 =
"""
8 changes: 8 additions & 0 deletions tests/HuskyIntegrationTests/Readme.md
Original file line number Diff line number Diff line change
@@ -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
63 changes: 36 additions & 27 deletions tests/HuskyIntegrationTests/Tests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
104 changes: 0 additions & 104 deletions tests/HuskyIntegrationTests/Utilities/DockerFixture.cs

This file was deleted.

This file was deleted.

98 changes: 98 additions & 0 deletions tests/HuskyIntegrationTests/Utilities/DockerHelper.cs
Original file line number Diff line number Diff line change
@@ -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<ExecResult> 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<ExecResult> 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<ExecResult> UpdateTaskRunner(this IContainer container, string content)
{
return container.BashAsync($"echo -e '{content}' > /test/.husky/task-runner.json");
}

public static Task<ExecResult> AddCsharpClass(this IContainer container, string content, string fileName = "Class2.cs")
{
return container.BashAsync($"echo -e '{content}' > /test/{fileName}");
}

public static async Task<IContainer> 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<IContainer> 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);
}
}
30 changes: 0 additions & 30 deletions tests/HuskyIntegrationTests/Utilities/DockerLogger.cs

This file was deleted.

41 changes: 0 additions & 41 deletions tests/HuskyIntegrationTests/Utilities/Extensions.cs

This file was deleted.

63 changes: 63 additions & 0 deletions tests/HuskyIntegrationTests/Utilities/GlobalImageBuilder.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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
/// </summary>
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<bool> 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<string, IDictionary<string, bool>>
{
["reference"] = new Dictionary<string, bool> { [imageName] = true }
}
});
return _imageBuilt = images.Count > 0;
}
}
4 changes: 4 additions & 0 deletions tests/HuskyIntegrationTests/xunit.runner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"showLiveOutput": true
}

0 comments on commit 5bc3b51

Please sign in to comment.