diff --git a/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj b/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj index b469bbc..1da3e5b 100644 --- a/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj +++ b/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj @@ -3,12 +3,13 @@ Xunit.Abstractions Extends ITestOutputHelper to allow output of a generic type using a serializer. - test, testing, extensions, xunit, output, helper, serializer, json, xml, csharp, dump, var, dumpvar, vardump, xmlserializer, jsonserializer, testoutputhelper, testoutput, outputhelper, ITestOutputHelper, abstractions, xunit.abstractions, xunit.abstractions.extensions, xunit.extensions.ordering + test, testing, extensions, xunit, output, helper, table, output, serializer, json, xml, csharp, dump, var, dumpvar, vardump, xmlserializer, jsonserializer, testoutputhelper, testoutput, outputhelper, ITestOutputHelper, abstractions, xunit.abstractions, xunit.abstractions.extensions, xunit.extensions.ordering - - + + + diff --git a/Frank.Testing.TestOutputExtensions/TestOutputTableExtensions.cs b/Frank.Testing.TestOutputExtensions/TestOutputTableExtensions.cs new file mode 100644 index 0000000..2700673 --- /dev/null +++ b/Frank.Testing.TestOutputExtensions/TestOutputTableExtensions.cs @@ -0,0 +1,13 @@ +using ConsoleTableExt; + +namespace Xunit.Abstractions; + +public static class TestOutputTableExtensions +{ + public static void WriteTable(this ITestOutputHelper outputHelper, IEnumerable source, ConsoleTableBuilderFormat format = ConsoleTableBuilderFormat.Minimal) where T : class => + outputHelper.WriteLine(ConsoleTableBuilder + .From(source.ToList()) + .WithFormat(format) + .Export() + .ToString()); +} diff --git a/Frank.Testing.Testcontainers/ContainerRunner.cs b/Frank.Testing.Testcontainers/ContainerRunner.cs new file mode 100644 index 0000000..26d045c --- /dev/null +++ b/Frank.Testing.Testcontainers/ContainerRunner.cs @@ -0,0 +1,64 @@ +using DotNet.Testcontainers.Containers; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Frank.Testing.Testcontainers; + +public class ContainerRunner : ITestcontainerRunner where T : class, IContainer +{ + private readonly ILogger _logger; + private readonly T _container; + private readonly CancellationToken _cancellationToken; + private readonly TimeSpan _timeout; + + private CancellationTokenSource? _cancellationTokenSource; + + internal ContainerRunner(ILogger? logger, T container, TimeSpan timeout, CancellationToken cancellationToken) + { + _logger = logger ??= new NullLogger(); + _container = container ?? throw new ArgumentNullException(nameof(container)); + _cancellationToken = cancellationToken; + _timeout = timeout; + } + + public async Task StartAsync() + { + _cancellationTokenSource = new CancellationTokenSource(_timeout); + _cancellationToken.Register(() => _cancellationTokenSource.Cancel()); + await _container.StartAsync(_cancellationTokenSource.Token); + } + + public async Task StopAsync() + { + await _container.StopAsync(_cancellationToken); + await DisposeAsync(); + } + + public TestcontainersStates GetState() => _container.State; + + /// + public async Task ExecuteCommandAsync(string command, CancellationToken cancellationToken = default) + { + await _container.ExecAsync(new[] { command }, _cancellationToken); + } + + public async Task ExecuteAsync(Func actionAsync) + { + try + { + await actionAsync(); + } + catch (Exception? exception) + { + _logger.LogError(exception, "Failed to execute action in container {ContainerName}", _container.Name); + } + } + + /// + public async ValueTask DisposeAsync() + { + await _container.DisposeAsync(); + _cancellationTokenSource?.Dispose(); + } +} \ No newline at end of file diff --git a/Frank.Testing.Testcontainers/Frank.Testing.Testcontainers.csproj b/Frank.Testing.Testcontainers/Frank.Testing.Testcontainers.csproj new file mode 100644 index 0000000..10db732 --- /dev/null +++ b/Frank.Testing.Testcontainers/Frank.Testing.Testcontainers.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Frank.Testing.Testcontainers/ITestcontainerRunner.cs b/Frank.Testing.Testcontainers/ITestcontainerRunner.cs new file mode 100644 index 0000000..f714e63 --- /dev/null +++ b/Frank.Testing.Testcontainers/ITestcontainerRunner.cs @@ -0,0 +1,16 @@ +using DotNet.Testcontainers.Containers; + +namespace Frank.Testing.Testcontainers; + +public interface ITestcontainerRunner : IAsyncDisposable +{ + Task StartAsync(); + + Task StopAsync(); + + TestcontainersStates GetState(); + + Task ExecuteCommandAsync(string command, CancellationToken cancellationToken = default); + + Task ExecuteAsync(Func actionAsync); +} \ No newline at end of file diff --git a/Frank.Testing.Testcontainers/TestContainerRunnerBuilder.cs b/Frank.Testing.Testcontainers/TestContainerRunnerBuilder.cs new file mode 100644 index 0000000..0ec924b --- /dev/null +++ b/Frank.Testing.Testcontainers/TestContainerRunnerBuilder.cs @@ -0,0 +1,51 @@ +using DotNet.Testcontainers.Containers; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Frank.Testing.Testcontainers; + +public class TestContainerRunnerBuilder where T : class, IContainer +{ + private ILogger? _logger; + private TimeSpan _maxLifetime; + private CancellationToken _cancellationToken; + private Func? _containerFactory; + + public TestContainerRunnerBuilder WithLogger(ILogger? logger) + { + _logger = logger; + return this; + } + + public TestContainerRunnerBuilder WithMaxLifetime(TimeSpan maxLifetime) + { + _maxLifetime = maxLifetime; + return this; + } + + public TestContainerRunnerBuilder WithCancellationToken(CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + return this; + } + + public TestContainerRunnerBuilder WithContainerFactory(Func? containerFactory) + { + _containerFactory = containerFactory; + return this; + } + + public ITestcontainerRunner Build() + { + _logger ??= new NullLogger(); + if (_containerFactory == null) + throw new ArgumentNullException(nameof(_containerFactory)); + if (_maxLifetime == default) + _maxLifetime = TimeSpan.FromMinutes(1); + if (_cancellationToken == default) + _cancellationToken = CancellationToken.None; + + return new ContainerRunner(_logger, _containerFactory(), _maxLifetime, _cancellationToken); + } +} \ No newline at end of file diff --git a/Frank.Testing.Tests/Frank.Testing.Tests.csproj b/Frank.Testing.Tests/Frank.Testing.Tests.csproj index 75f4672..0d61dde 100644 --- a/Frank.Testing.Tests/Frank.Testing.Tests.csproj +++ b/Frank.Testing.Tests/Frank.Testing.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/Frank.Testing.Tests/TestOutputHelperExtensionsTests.cs b/Frank.Testing.Tests/TestOutputExtensionsTests/TestOutputHelperExtensionsTests.cs similarity index 97% rename from Frank.Testing.Tests/TestOutputHelperExtensionsTests.cs rename to Frank.Testing.Tests/TestOutputExtensionsTests/TestOutputHelperExtensionsTests.cs index 491334b..0a065c3 100644 --- a/Frank.Testing.Tests/TestOutputHelperExtensionsTests.cs +++ b/Frank.Testing.Tests/TestOutputExtensionsTests/TestOutputHelperExtensionsTests.cs @@ -6,7 +6,7 @@ using Xunit.Abstractions; -namespace Frank.Testing.Tests; +namespace Frank.Testing.Tests.TestOutputExtensionsTests; [TestSubject(typeof(TestOutputHelperExtensions))] public class TestOutputHelperExtensionsTests(ITestOutputHelper testOutputHelper) diff --git a/Frank.Testing.Tests/TestOutputExtensionsTests/TestOutputTableExtensionsTests.cs b/Frank.Testing.Tests/TestOutputExtensionsTests/TestOutputTableExtensionsTests.cs new file mode 100644 index 0000000..fcd9030 --- /dev/null +++ b/Frank.Testing.Tests/TestOutputExtensionsTests/TestOutputTableExtensionsTests.cs @@ -0,0 +1,27 @@ +using Xunit.Abstractions; + +namespace Frank.Testing.Tests.TestOutputExtensionsTests; + +public class TestOutputTableExtensionsTests +{ + private readonly ITestOutputHelper _outputHelper; + + public TestOutputTableExtensionsTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Fact] + public void ToTable_WithEnumerable_ReturnsTable() + { + // Arrange + var passwords = new[] + { + new { Sha1Hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8", Sha1Prefix = "5BAA6", Sha2Suffix = "1E4C9B93F3F0682250B6CF8331B7EE68FD8", TimesPwned = 3645844 }, new { Sha1Hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8", Sha1Prefix = "5BAA6", Sha2Suffix = "1E4C9B93F3F0682250B6CF8331B7EE68FD8", TimesPwned = 3645844 }, new { Sha1Hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8", Sha1Prefix = "5BAA6", Sha2Suffix = "1E4C9B93F3F0682250B6CF8331B7EE68FD8", TimesPwned = 3645844 }, + new { Sha1Hash = "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8", Sha1Prefix = "5BAA6", Sha2Suffix = "1E4C9B93F3F0682250B6CF8331B7EE68FD8", TimesPwned = 364584 }, + }; + + // Act + _outputHelper.WriteTable(passwords); + } +} \ No newline at end of file diff --git a/Frank.Testing.sln b/Frank.Testing.sln index b0ace23..58df3c9 100644 --- a/Frank.Testing.sln +++ b/Frank.Testing.sln @@ -24,6 +24,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frank.Testing.ApiTesting", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frank.Testing.EntityFrameworkCore", "Frank.Testing.EntityFrameworkCore\Frank.Testing.EntityFrameworkCore.csproj", "{A64BD8FC-364F-40E5-A276-F1DEA9D98F67}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frank.Testing.Testcontainers", "Frank.Testing.Testcontainers\Frank.Testing.Testcontainers.csproj", "{13E7B32B-CBA5-4BA3-854B-697B835796F7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,5 +55,9 @@ Global {A64BD8FC-364F-40E5-A276-F1DEA9D98F67}.Debug|Any CPU.Build.0 = Debug|Any CPU {A64BD8FC-364F-40E5-A276-F1DEA9D98F67}.Release|Any CPU.ActiveCfg = Release|Any CPU {A64BD8FC-364F-40E5-A276-F1DEA9D98F67}.Release|Any CPU.Build.0 = Release|Any CPU + {13E7B32B-CBA5-4BA3-854B-697B835796F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13E7B32B-CBA5-4BA3-854B-697B835796F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13E7B32B-CBA5-4BA3-854B-697B835796F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13E7B32B-CBA5-4BA3-854B-697B835796F7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal