Skip to content

Commit

Permalink
feat(#603): Add default logger implementation (#684)
Browse files Browse the repository at this point in the history
  • Loading branch information
HofmeisterAn committed Nov 23, 2022
1 parent 446f002 commit b73e6c0
Show file tree
Hide file tree
Showing 41 changed files with 256 additions and 190 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### Changed

- 642 Expose container port bindings automatically
- 603 Add default logger that forwards messages to the console (does not support every test environment)

### Fixed

Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers/Builders/AbstractBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public abstract class AbstractBuilder<TBuilderEntity, TConfigurationEntity>
/// <param name="dockerResourceConfiguration">The Docker resource configuration.</param>
protected AbstractBuilder(TConfigurationEntity dockerResourceConfiguration)
{
_ = TestcontainersSettings.SettingsInitialized.WaitOne(TimeSpan.FromSeconds(5));
this.DockerResourceConfiguration = dockerResourceConfiguration;
}

Expand Down
33 changes: 21 additions & 12 deletions src/Testcontainers/Builders/DockerEndpointAuthenticationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,30 @@ public virtual bool IsAvailable()
return false;
}

using (var dockerClientConfiguration = authConfig.GetDockerClientConfiguration(ResourceReaper.DefaultSessionId))
{
using (var dockerClient = dockerClientConfiguration.CreateClient())
return TaskFactory.StartNew(async () =>
{
try
{
TaskFactory.StartNew(() => dockerClient.System.PingAsync()).Unwrap().GetAwaiter().GetResult();
return true;
}
catch (Exception)
using (var dockerClientConfiguration = authConfig.GetDockerClientConfiguration(ResourceReaper.DefaultSessionId))
{
return false;
using (var dockerClient = dockerClientConfiguration.CreateClient())
{
try
{
await dockerClient.System.PingAsync()
.ConfigureAwait(false);
return true;
}
catch (Exception)
{
return false;
}
}
}
}
}
})
.Unwrap()
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
}

/// <inheritdoc />
Expand Down
4 changes: 3 additions & 1 deletion src/Testcontainers/Clients/DockerImageOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ await this.DeleteAsync(image, ct)
await this.Docker.Images.BuildImageFromDockerfileAsync(buildParameters, dockerfileStream, Array.Empty<AuthConfig>(), new Dictionary<string, string>(), this.traceProgress, ct)
.ConfigureAwait(false);

var imageHasBeenCreated = await this.ExistsWithNameAsync(image.FullName, ct).ConfigureAwait(false);
var imageHasBeenCreated = await this.ExistsWithNameAsync(image.FullName, ct)
.ConfigureAwait(false);

if (!imageHasBeenCreated)
{
throw new InvalidOperationException($"Docker image {image.FullName} has not been created.");
Expand Down
17 changes: 13 additions & 4 deletions src/Testcontainers/Clients/TraceProgress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@ public TraceProgress(ILogger logger)

public void Report(JSONMessage value)
{
this.logger.Trace(value.Stream);
this.logger.Trace(value.ProgressMessage);
this.logger.Trace(value.Status);
this.logger.Error(value.ErrorMessage);
#pragma warning disable CA1848, CA2254

if (!string.IsNullOrWhiteSpace(value.ProgressMessage))
{
this.logger.LogTrace(value.ProgressMessage);
}

if (!string.IsNullOrWhiteSpace(value.ErrorMessage))
{
this.logger.LogError(value.ErrorMessage);
}

#pragma warning restore CA1848, CA2254
}
}
}
72 changes: 70 additions & 2 deletions src/Testcontainers/Configurations/TestcontainersSettings.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
namespace DotNet.Testcontainers.Configurations
{
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using DotNet.Testcontainers.Images;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

/// <summary>
/// This class represents the Testcontainers settings.
Expand All @@ -20,6 +23,8 @@ public static class TestcontainersSettings
{
private static readonly IDockerImage RyukContainerImage = new DockerImage("testcontainers/ryuk:0.3.4");

private static readonly ManualResetEventSlim ManualResetEvent = new ManualResetEventSlim(false);

private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig =
new IDockerEndpointAuthenticationProvider[]
{
Expand All @@ -37,6 +42,63 @@ public static class TestcontainersSettings

static TestcontainersSettings()
{
Task.Run(async () =>
{
var runtimeInfo = new StringBuilder();
if (DockerEndpointAuthConfig != null)
{
using (var dockerClientConfiguration = DockerEndpointAuthConfig.GetDockerClientConfiguration())
{
using (var dockerClient = dockerClientConfiguration.CreateClient())
{
try
{
var byteUnits = new[] { "KB", "MB", "GB" };
var dockerInfo = await dockerClient.System.GetSystemInfoAsync()
.ConfigureAwait(false);
var dockerVersion = await dockerClient.System.GetVersionAsync()
.ConfigureAwait(false);
runtimeInfo.AppendLine("Connected to Docker:");
runtimeInfo.Append(" Host: ");
runtimeInfo.AppendLine(dockerClient.Configuration.EndpointBaseUri.ToString());
runtimeInfo.Append(" Server Version: ");
runtimeInfo.AppendLine(dockerInfo.ServerVersion);
runtimeInfo.Append(" Kernel Version: ");
runtimeInfo.AppendLine(dockerInfo.KernelVersion);
runtimeInfo.Append(" API Version: ");
runtimeInfo.AppendLine(dockerVersion.APIVersion);
runtimeInfo.Append(" Operating System: ");
runtimeInfo.AppendLine(dockerInfo.OperatingSystem);
runtimeInfo.Append(" Total Memory: ");
runtimeInfo.AppendFormat(CultureInfo.InvariantCulture, "{0:F} {1}", dockerInfo.MemTotal / Math.Pow(1024, byteUnits.Length), byteUnits.Last());
}
catch
{
// Ignore exceptions in auto discovery. Users can provide the Docker endpoint with the builder too.
}
}
}
}
else
{
runtimeInfo.AppendLine("Auto discovery did not detect a Docker host configuration");
}
#pragma warning disable CA1848, CA2254
Logger.LogInformation(runtimeInfo.ToString());
#pragma warning restore CA1848, CA2254
ManualResetEvent.Set();
});
}

/// <summary>
Expand Down Expand Up @@ -83,7 +145,7 @@ static TestcontainersSettings()
[PublicAPI]
[NotNull]
public static ILogger Logger { get; set; }
= NullLogger.Instance;
= new Logger();

/// <summary>
/// Gets or sets the host operating system.
Expand All @@ -93,6 +155,12 @@ static TestcontainersSettings()
public static IOperatingSystem OS { get; set; }
= RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IOperatingSystem)new Windows(DockerEndpointAuthConfig) : new Unix(DockerEndpointAuthConfig);

/// <summary>
/// Gets the wait handle that signals settings initialized.
/// </summary>
internal static WaitHandle SettingsInitialized
=> ManualResetEvent.WaitHandle;

private static ushort GetResourceReaperPublicHostPort(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig)
{
// Let Docker choose the random public host port. This includes Docker Engines exposed via TCP (Docker Desktop for Windows).
Expand Down
2 changes: 1 addition & 1 deletion src/Testcontainers/Containers/TestcontainersContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ private void ThrowIfContainerHasNotBeenCreated()

private string GetContainerGateway()
{
const string localhost = "localhost";
const string localhost = "127.0.0.1";

if (!ContainerHasBeenCreatedStates.HasFlag(this.State))
{
Expand Down
111 changes: 111 additions & 0 deletions src/Testcontainers/Logger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
namespace DotNet.Testcontainers
{
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;

/// <summary>
/// An <see cref="ILogger" /> implementation that forwards messages to the console. Not every test framework or environment supports this approach. Developers may still need to configure their own logging implementation.
/// If VSTest.Console.exe loads the test adapter in a deterministic order, we can write our own test adapter and intercept the IMessageLogger instance: https://github.com/microsoft/vstest/issues/4125#issuecomment-1320880502.
/// To debug the test host and runner set the environment variables VSTEST_HOST_DEBUG and VSTEST_RUNNER_DEBUG to 1. To enable VSTest logging set VSTEST_DIAG to 1 and VSTEST_DIAG_VERBOSITY to verbose.
/// The following example contains the ITestExecutor implementations. It is important that the assembly ends with TestAdapter.dll.
/// </summary>
/// <example>
/// <code>
/// [FileExtension(DllFileExtension)]
/// [FileExtension(ExeFileExtension)]
/// [DefaultExecutorUri(ExecutorUri)]
/// [ExtensionUri(ExecutorUri)]
/// [Category(Category)]
/// internal sealed class UssDiscovery : ITestDiscoverer, ITestExecutor
/// {
/// private const string DllFileExtension = &quot;.dll&quot;;
///
/// private const string ExeFileExtension = &quot;.exe&quot;;
///
/// private const string ExecutorUri = &quot;executor://testcontainers.org/v1&quot;;
///
/// private const string Category = &quot;managed&quot;;
///
/// public void DiscoverTests(IEnumerable&lt;string&gt; sources, IDiscoveryContext discoveryContext, IMessageLogger logger, ITestCaseDiscoverySink discoverySink)
/// {
/// }
///
/// public void RunTests(IEnumerable&lt;TestCase&gt; tests, IRunContext runContext, IFrameworkHandle frameworkHandle)
/// {
/// SetLogger(frameworkHandle);
/// }
///
/// public void RunTests(IEnumerable&lt;string&gt; sources, IRunContext runContext, IFrameworkHandle frameworkHandle)
/// {
/// SetLogger(frameworkHandle);
/// }
///
/// public void Cancel()
/// {
/// }
///
/// private static void SetLogger(IMessageLogger logger)
/// {
/// // Set the TestcontainersSettings.Logger. Use a semaphore to block the test execution until the logger is set.
/// }
/// }
/// </code>
/// </example>
internal sealed class Logger : ILogger, IDisposable
{
private readonly Stopwatch stopwatch = Stopwatch.StartNew();

public Logger()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !Console.IsOutputRedirected && !Console.IsErrorRedirected)
{
Console.BufferWidth = short.MaxValue - 1;
}
}

public void Dispose()
{
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
TextWriter console;

switch (logLevel)
{
case LogLevel.Information:
console = Console.Out;
break;
case LogLevel.Warning:
console = Console.Out;
break;
case LogLevel.Error:
console = Console.Error;
break;
case LogLevel.Critical:
console = Console.Error;
break;
default:
return;
}

var message = string.Format(CultureInfo.CurrentCulture, "[testcontainers.org {0:hh\\:mm\\:ss\\.ff}] {1}", this.stopwatch.Elapsed, formatter.Invoke(state, exception));
console.WriteLine(message);
Debug.WriteLine(message);
}

public bool IsEnabled(LogLevel logLevel)
{
return true;
}

public IDisposable BeginScope<TState>(TState state)
{
return this;
}
}
}
Loading

0 comments on commit b73e6c0

Please sign in to comment.