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 }