diff --git a/README.md b/README.md
index 3ce3a99ca..509a44280 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,15 @@
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=dotnet-testcontainers&metric=coverage)](https://sonarcloud.io/dashboard?id=dotnet-testcontainers)
# .NET Testcontainers
-.NET Testcontainers is a library to support tests with throwaway instances of Docker containers. The library is built on top of the .NET Docker remote API and provides a lightweight implementation to support your test environment in all circumstances.
+.NET Testcontainers is a library to support tests with throwaway instances of Docker containers for `netstandard2.0`, `net452` and `net462`. The library is built on top of the .NET Docker remote API and provides a lightweight implementation to support your test environment in all circumstances.
Choose from existing pre-configured configurations [^1] and start containers within a second, to support and run your tests.
## Supported commands
- `WithImage` specifies an `IMAGE[:TAG]` to derive the container from.
- `WithCommand` specifies and overrides the `[COMMAND]` instruction provided from the Dockerfile.
-- `WithEnvironment` set an environment variable in the container e. g.`-e "test=containers"`.
-- `WithLabel` applies metadata to a container e. g.`-l, --label dotnet.testcontainers=awesome`.
+- `WithEnvironment` set an environment variable in the container e. g. `-e "test=containers"`.
+- `WithLabel` applies metadata to a container e. g. `-l, --label dotnet.testcontainers=awesome`.
- `WithExposedPort` exposes a port inside the container e. g. `--expose=80`.
- `WithPortBinding` publishes a container port to the host e. g. `-p 80:80`.
- `WithMount` mounts a volume into the container e. g. `-v, --volume .:/tmp`.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index d295c3905..f829f8277 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -20,16 +20,16 @@ jobs:
echo "##vso[task.setvariable variable=sonarcloudOrganization;]$(sonarcloud.organization)"
echo "##vso[task.setvariable variable=nuGetSource;]$(feed.source)"
echo "##vso[task.setvariable variable=nuGetApiKey;]$(feed.apikey)"
- ./build.sh --target=Restore-NuGet-Packages
+ ./build.ps1 --target=Restore-NuGet-Packages
displayName: 'Prepare'
- - powershell: ./build.sh --target=Build
+ - powershell: ./build.ps1 --target=Build
displayName: 'Build'
- - powershell: ./build.sh --target=Test
+ - powershell: ./build.ps1 --target=Test
displayName: 'Test'
- - powershell: ./build.sh --target=Sonar
+ - powershell: ./build.ps1 --target=Sonar
displayName: 'Sonar'
env:
SONARQUBE_URL: $(sonarcloudUrl)
@@ -37,7 +37,7 @@ jobs:
SONARQUBE_TOKEN: $(sonarcloudToken)
SONARQUBE_ORGANIZATION: $(sonarcloudOrganization)
- - powershell: ./build.sh --target=Publish
+ - powershell: ./build.ps1 --target=Publish
displayName: 'Publish'
env:
NUGET_SOURCE: $(nuGetSource)
diff --git a/src/DotNet.Testcontainers.Tests/DotNet.Testcontainers.Tests.csproj b/src/DotNet.Testcontainers.Tests/DotNet.Testcontainers.Tests.csproj
index 791a37645..3806e21ea 100644
--- a/src/DotNet.Testcontainers.Tests/DotNet.Testcontainers.Tests.csproj
+++ b/src/DotNet.Testcontainers.Tests/DotNet.Testcontainers.Tests.csproj
@@ -7,16 +7,17 @@
-
-
+
+
-
- PreserveNewest
-
+
-
+
+ xunit.runner.json
+ PreserveNewest
+
diff --git a/src/DotNet.Testcontainers.Tests/TestcontainersTests.cs b/src/DotNet.Testcontainers.Tests/TestcontainersTests.cs
index cb3b453d4..0df31be54 100644
--- a/src/DotNet.Testcontainers.Tests/TestcontainersTests.cs
+++ b/src/DotNet.Testcontainers.Tests/TestcontainersTests.cs
@@ -37,25 +37,59 @@ public class AccessDockerInformation
[Fact]
public void QueryNotExistingDockerImageById()
{
- Assert.False(TestcontainersClient.Instance.ExistImageById(string.Empty));
+ Assert.False(MetaDataClientImages.Instance.ExistsWithId(string.Empty));
}
[Fact]
public void QueryNotExistingDockerContainerById()
{
- Assert.False(TestcontainersClient.Instance.ExistContainerById(string.Empty));
+ Assert.False(MetaDataClientContainers.Instance.ExistsWithId(string.Empty));
}
[Fact]
public void QueryNotExistingDockerImageByName()
{
- Assert.False(TestcontainersClient.Instance.ExistImageByName(string.Empty));
+ Assert.False(MetaDataClientImages.Instance.ExistsWithName(string.Empty));
}
[Fact]
public void QueryNotExistingDockerContainerByName()
{
- Assert.False(TestcontainersClient.Instance.ExistContainerByName(string.Empty));
+ Assert.False(MetaDataClientContainers.Instance.ExistsWithName(string.Empty));
+ }
+
+ [Fact]
+ public void QueryContainerInformationOfRunningContainer()
+ {
+ // Given
+ // When
+ var testcontainersBuilder = new TestcontainersBuilder()
+ .WithImage("alpine");
+
+ // Then
+ using (var testcontainer = testcontainersBuilder.Build())
+ {
+ testcontainer.Start();
+
+ Assert.NotEmpty(testcontainer.Name);
+ Assert.NotEmpty(testcontainer.IPAddress);
+ Assert.NotEmpty(testcontainer.MacAddress);
+ }
+ }
+
+ [Fact]
+ public void QueryContainerInformationOfStoppedContainer()
+ {
+ // Given
+ // When
+ var testcontainersBuilder = new TestcontainersBuilder()
+ .WithImage("alpine");
+
+ // Then
+ using (var testcontainer = testcontainersBuilder.Build())
+ {
+ Assert.Throws(() => testcontainer.Name);
+ }
}
}
diff --git a/src/DotNet.Testcontainers/Clients/DockerMetaDataClient.cs b/src/DotNet.Testcontainers/Clients/DockerMetaDataClient.cs
index 16cc6e2d0..2611ea222 100644
--- a/src/DotNet.Testcontainers/Clients/DockerMetaDataClient.cs
+++ b/src/DotNet.Testcontainers/Clients/DockerMetaDataClient.cs
@@ -1,6 +1,7 @@
namespace DotNet.Testcontainers.Clients
{
using System.Collections.Generic;
+ using static LanguageExt.Prelude;
internal abstract class DockerMetaDataClient : DockerApiClient
{
@@ -11,5 +12,15 @@ internal abstract class DockerMetaDataClient : DockerApiClient
internal abstract T ByName(string name);
internal abstract T ByProperty(string property, string value);
+
+ internal bool ExistsWithId(string id)
+ {
+ return notnull(this.ById(id));
+ }
+
+ internal bool ExistsWithName(string name)
+ {
+ return notnull(this.ByName(name));
+ }
}
}
diff --git a/src/DotNet.Testcontainers/Clients/ITestcontainersClient.cs b/src/DotNet.Testcontainers/Clients/ITestcontainersClient.cs
index d6148495f..8b028ad9c 100644
--- a/src/DotNet.Testcontainers/Clients/ITestcontainersClient.cs
+++ b/src/DotNet.Testcontainers/Clients/ITestcontainersClient.cs
@@ -4,23 +4,6 @@ namespace DotNet.Testcontainers.Clients
internal interface ITestcontainersClient
{
- // Wrap image and container queries into proper response classes.
- bool ExistImageById(string id);
-
- bool ExistImageByName(string name);
-
- bool ExistContainerById(string id);
-
- bool ExistContainerByName(string name);
-
- string FindImageNameById(string id);
-
- string FindImageNameByName(string name);
-
- string FindContainerNameById(string id);
-
- string FindContainerNameByName(string name);
-
void Start(string id);
void Stop(string id);
diff --git a/src/DotNet.Testcontainers/Clients/TestcontainersClient.cs b/src/DotNet.Testcontainers/Clients/TestcontainersClient.cs
index 95da64457..0c7a081b7 100644
--- a/src/DotNet.Testcontainers/Clients/TestcontainersClient.cs
+++ b/src/DotNet.Testcontainers/Clients/TestcontainersClient.cs
@@ -2,13 +2,11 @@ namespace DotNet.Testcontainers.Clients
{
using System;
using System.Collections.Generic;
- using System.Linq;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Core.Mapper;
using DotNet.Testcontainers.Core.Mapper.Converters;
using DotNet.Testcontainers.Core.Models;
using DotNet.Testcontainers.Diagnostics;
- using static LanguageExt.Prelude;
internal class TestcontainersClient : DockerApiClient, ITestcontainersClient
{
@@ -18,6 +16,8 @@ internal class TestcontainersClient : DockerApiClient, ITestcontainersClient
private static readonly GenericConverter GenericConverter = new GenericConverter(ConverterFactory);
+ private static object lockObject = new object();
+
static TestcontainersClient()
{
ConverterFactory.Register, IList>(
@@ -47,65 +47,9 @@ internal static ITestcontainersClient Instance
}
}
- public bool ExistImageById(string id)
- {
- return Optional(id).Match(
- Some: value => notnull(MetaDataClientImages.Instance.ById(value)),
- None: () => false);
- }
-
- public bool ExistImageByName(string name)
- {
- return Optional(name).Match(
- Some: value => notnull(MetaDataClientImages.Instance.ByName(value)),
- None: () => false);
- }
-
- public bool ExistContainerById(string id)
- {
- return Optional(id).Match(
- Some: value => notnull(MetaDataClientContainers.Instance.ById(value)),
- None: () => false);
- }
-
- public bool ExistContainerByName(string name)
- {
- return Optional(name).Match(
- Some: value => notnull(MetaDataClientContainers.Instance.ByName(value)),
- None: () => false);
- }
-
- public string FindImageNameById(string id)
- {
- return Optional(id).Match(
- Some: value => MetaDataClientImages.Instance.ById(value).RepoTags.FirstOrDefault(),
- None: () => string.Empty);
- }
-
- public string FindImageNameByName(string name)
- {
- return Optional(name).Match(
- Some: value => MetaDataClientImages.Instance.ByName(value).RepoTags.FirstOrDefault(),
- None: () => string.Empty);
- }
-
- public string FindContainerNameById(string id)
- {
- return Optional(id).Match(
- Some: value => MetaDataClientContainers.Instance.ById(value).Names.FirstOrDefault(),
- None: () => string.Empty);
- }
-
- public string FindContainerNameByName(string name)
- {
- return Optional(name).Match(
- Some: value => MetaDataClientContainers.Instance.ByName(value).Names.FirstOrDefault(),
- None: () => string.Empty);
- }
-
public void Start(string id)
{
- if (this.ExistContainerById(id))
+ if (MetaDataClientContainers.Instance.ExistsWithId(id))
{
Docker.Containers.StartContainerAsync(id, new ContainerStartParameters { }).Wait();
}
@@ -113,7 +57,7 @@ public void Start(string id)
public void Stop(string id)
{
- if (this.ExistContainerById(id))
+ if (MetaDataClientContainers.Instance.ExistsWithId(id))
{
Docker.Containers.StopContainerAsync(id, new ContainerStopParameters { WaitBeforeKillSeconds = 15 }).Wait();
}
@@ -121,7 +65,7 @@ public void Stop(string id)
public void Remove(string id)
{
- if (this.ExistContainerById(id))
+ if (MetaDataClientContainers.Instance.ExistsWithId(id))
{
Docker.Containers.RemoveContainerAsync(id, new ContainerRemoveParameters { Force = true }).Wait();
}
@@ -133,9 +77,12 @@ public string Run(TestcontainersConfiguration configuration)
var name = configuration.Container.Name;
- if (!this.ExistImageByName(image))
+ lock (lockObject)
{
- Docker.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = image }, null, DebugProgress.Instance).Wait();
+ if (!MetaDataClientImages.Instance.ExistsWithName(image))
+ {
+ Docker.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = image }, null, DebugProgress.Instance).Wait();
+ }
}
var cmd = GenericConverter.Convert,
diff --git a/src/DotNet.Testcontainers/Core/Builder/TestcontainersBuilder.cs b/src/DotNet.Testcontainers/Core/Builder/TestcontainersBuilder.cs
index c3cb27d0c..5e7e38c1c 100644
--- a/src/DotNet.Testcontainers/Core/Builder/TestcontainersBuilder.cs
+++ b/src/DotNet.Testcontainers/Core/Builder/TestcontainersBuilder.cs
@@ -136,6 +136,7 @@ public IDockerContainer Build()
configuration.Container.Command = this.command;
configuration.Container.Environments = this.environments;
configuration.Container.ExposedPorts = this.exposedPorts;
+ configuration.Container.Labels = this.labels;
configuration.Host.PortBindings = this.portBindings;
configuration.Host.Mounts = this.mounts;
diff --git a/src/DotNet.Testcontainers/Core/Containers/IDockerContainer.cs b/src/DotNet.Testcontainers/Core/Containers/IDockerContainer.cs
index 10d595a12..41d4e113a 100644
--- a/src/DotNet.Testcontainers/Core/Containers/IDockerContainer.cs
+++ b/src/DotNet.Testcontainers/Core/Containers/IDockerContainer.cs
@@ -12,6 +12,14 @@ public interface IDockerContainer : IDisposable
/// Returns the Docker container name if present or an empty string instead.
string Name { get; }
+ /// Gets the Testcontainer ip address.
+ /// Returns the Docker container ip address if present or an empty string instead.
+ string IPAddress { get; }
+
+ /// Gets the Testcontainer mac address.
+ /// Returns the Docker container mac address if present or an empty string instead.
+ string MacAddress { get; }
+
///
/// Starts the Testcontainer. If the image does not exist, it will be downloaded automatically. Non-existing containers are created at first start.
///
diff --git a/src/DotNet.Testcontainers/Core/Containers/TestcontainersContainer.cs b/src/DotNet.Testcontainers/Core/Containers/TestcontainersContainer.cs
index 1aec329ca..42c1b4aa1 100644
--- a/src/DotNet.Testcontainers/Core/Containers/TestcontainersContainer.cs
+++ b/src/DotNet.Testcontainers/Core/Containers/TestcontainersContainer.cs
@@ -1,6 +1,8 @@
namespace DotNet.Testcontainers.Core.Containers
{
using System;
+ using System.Linq;
+ using Docker.DotNet.Models;
using DotNet.Testcontainers.Clients;
using DotNet.Testcontainers.Core.Models;
using LanguageExt;
@@ -8,7 +10,9 @@ namespace DotNet.Testcontainers.Core.Containers
public class TestcontainersContainer : IDockerContainer
{
- private Option id;
+ private Option id = None;
+
+ private Option container = None;
internal TestcontainersContainer(TestcontainersConfiguration configuration, bool cleanUp = true)
{
@@ -33,9 +37,37 @@ public string Name
{
get
{
- return this.id.Match(
- Some: TestcontainersClient.Instance.FindContainerNameById,
- None: () => $"/{this.Configuration.Container.Name}");
+ return this.container.Match(
+ Some: value => value.Names.FirstOrDefault() ?? string.Empty,
+ None: () => throw new InvalidOperationException("Testcontainer not running."));
+ }
+ }
+
+ public string IPAddress
+ {
+ get
+ {
+ return this.container.Match(
+ Some: value =>
+ {
+ var ipAddress = value.NetworkSettings.Networks.FirstOrDefault();
+ return notnull(ipAddress) ? ipAddress.Value.IPAddress : string.Empty;
+ },
+ None: () => throw new InvalidOperationException("Testcontainer not running."));
+ }
+ }
+
+ public string MacAddress
+ {
+ get
+ {
+ return this.container.Match(
+ Some: value =>
+ {
+ var macAddress = value.NetworkSettings.Networks.FirstOrDefault();
+ return notnull(macAddress) ? macAddress.Value.MacAddress : string.Empty;
+ },
+ None: () => throw new InvalidOperationException("Testcontainer not running."));
}
}
@@ -48,11 +80,21 @@ public void Start()
this.id = this.id.IfNone(TestcontainersClient.Instance.Run(this.Configuration));
TestcontainersClient.Instance.Start(this.Id);
+
+ this.id.IfSome(id =>
+ {
+ this.container = MetaDataClientContainers.Instance.ById(id);
+ });
}
public void Stop()
{
TestcontainersClient.Instance.Stop(this.Id);
+
+ this.id.IfSome(id =>
+ {
+ this.container = None;
+ });
}
public void Dispose()
@@ -65,11 +107,9 @@ protected virtual void Dispose(bool disposing)
{
this.id.IfSomeAsync(id =>
{
- if (!this.CleanUp)
- {
- TestcontainersClient.Instance.Stop(id);
- }
- else
+ TestcontainersClient.Instance.Stop(id);
+
+ if (this.CleanUp)
{
this.id = None;
TestcontainersClient.Instance.Remove(id);
diff --git a/src/xunit.runner.json b/src/xunit.runner.json
index c836d34a4..9bf5c606f 100644
--- a/src/xunit.runner.json
+++ b/src/xunit.runner.json
@@ -1,3 +1,5 @@
{
- "$schema": "https://xunit.github.io/schema/current/xunit.runner.schema.json"
+ "$schema": "https://xunit.github.io/schema/current/xunit.runner.schema.json",
+ "parallelizeAssembly": false,
+ "parallelizeTestCollections": false
}