Skip to content

Commit c481674

Browse files
authored
Merge branch 'develop' into feat/support-spicedb
2 parents f71a8fb + 897dfd3 commit c481674

File tree

18 files changed

+848
-75
lines changed

18 files changed

+848
-75
lines changed

docs/api/create_docker_container.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,37 @@ _ = new ContainerBuilder()
100100

101101
The static class `Consume` offers pre-configured implementations of the `IOutputConsumer` interface for common use cases. If you need additional functionalities beyond those provided by the default implementations, you can create your own implementations of `IOutputConsumer`.
102102

103+
## Composing command arguments
104+
105+
Testcontainers for .NET provides the `WithCommand(ComposableEnumerable<string>)` API to give you flexible control over container command arguments. While currently used for container commands, the `ComposableEnumerable<T>` abstraction is designed to support other builder APIs in the future, allowing similar composition and override functionality.
106+
107+
Because our builders are immutable, this feature allows you to extend or override pre-configured configurations, such as those in Testcontainers [modules](../modules/index.md), without modifying the original builder.
108+
109+
`ComposableEnumerable<T>` lets you decide how new API arguments should be combined with existing ones. You can choose to append, overwrite, or apply other strategies based on your needs.
110+
111+
If a module applies default commands and you need to override or remove them entirely, you can do this e.g. by explicitly resetting the command list:
112+
113+
```csharp title="Resetting command arguments"
114+
// Default PostgreSQL builder configuration:
115+
//
116+
// base.Init()
117+
// ...
118+
// .WithCommand("-c", "fsync=off")
119+
// .WithCommand("-c", "full_page_writes=off")
120+
// .WithCommand("-c", "synchronous_commit=off")
121+
// ...
122+
123+
var postgreSqlContainer = new PostgreSqlBuilder()
124+
.WithCommand(new OverwriteEnumerable<string>(Array.Empty<string>()))
125+
.Build();
126+
```
127+
128+
Using `OverwriteEnumerable<string>(Array.Empty<string>())` removes all default command configurations. This is useful when you want full control over the PostgreSQL startup or when the default configurations do not match your requirements.
129+
130+
!!!tip
131+
132+
You can create your own `ComposableEnumerable<T>` implementation to control exactly how configuration values are composed or modified.
133+
103134
## Examples
104135

105136
An NGINX container that binds the HTTP port to a random host port and hosts static content. The example connects to the web server and checks the HTTP status code.

docs/custom_configuration/index.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,36 @@ Setting the context to `tcc` in this example will use the Docker host running at
6060
docker.context=tcc
6161
```
6262

63+
## Automatically modify Docker Hub image names
64+
65+
Testcontainers can automatically add a registry prefix to Docker Hub image names used in your tests. This is handy if you use a private registry that mirrors Docker Hub images with predictable naming.
66+
67+
You can set this up in two ways:
68+
69+
=== "Environment Variable"
70+
```
71+
TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX=registry.mycompany.com/mirror/
72+
```
73+
74+
=== "Properties File"
75+
```
76+
hub.image.name.prefix=registry.mycompany.com/mirror/
77+
```
78+
79+
Once configured, Testcontainers will rewrite Docker Hub image names by adding the prefix.
80+
81+
For example, the image:
82+
83+
```
84+
testcontainers/helloworld:1.2.0
85+
```
86+
87+
will automatically become:
88+
89+
```
90+
registry.mycompany.com/mirror/testcontainers/helloworld:1.2.0
91+
```
92+
6393
## Enable logging
6494

6595
In .NET logging usually goes through the test framework. Testcontainers is not aware of the project's test framework and may not forward log messages to the appropriate output stream. The default implementation forwards log messages to the `Console` (respectively `stdout` and `stderr`). The output should at least pop up in the IDE running tests in the `Debug` configuration. To override the default implementation, use the builder's `WithLogger(ILogger)` method and provide an `ILogger` instance to replace the default console logger.

src/Testcontainers/Builders/BuildConfiguration.cs

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,38 @@ namespace DotNet.Testcontainers.Builders
44
using System.Collections.Generic;
55
using System.Collections.ObjectModel;
66
using System.Linq;
7+
using DotNet.Testcontainers.Configurations;
78

9+
/// <summary>
10+
/// Provides static utility methods for combining old and new configuration values
11+
/// across various collection and value types.
12+
/// </summary>
813
public static class BuildConfiguration
914
{
1015
/// <summary>
11-
/// Returns the changed configuration object. If there is no change, the previous configuration object is returned.
16+
/// Returns the updated configuration value. If the new value is <c>null</c> or
17+
/// <c>default</c>, the old value is returned.
1218
/// </summary>
13-
/// <param name="oldValue">The old configuration object.</param>
14-
/// <param name="newValue">The new configuration object.</param>
19+
/// <param name="oldValue">The old configuration value.</param>
20+
/// <param name="newValue">The new configuration value.</param>
1521
/// <typeparam name="T">Any class.</typeparam>
16-
/// <returns>Changed configuration object. If there is no change, the previous configuration object.</returns>
22+
/// <returns>The updated value, or the old value if unchanged.</returns>
1723
public static T Combine<T>(T oldValue, T newValue)
1824
{
1925
return Equals(default(T), newValue) ? oldValue : newValue;
2026
}
2127

2228
/// <summary>
23-
/// Combines all existing and new configuration changes. If there are no changes, the previous configurations are returned.
29+
/// Combines all existing and new configuration changes. If there are no changes,
30+
/// the previous configurations are returned.
2431
/// </summary>
2532
/// <param name="oldValue">The old configuration.</param>
2633
/// <param name="newValue">The new configuration.</param>
27-
/// <typeparam name="T">Type of <see cref="IEnumerable{T}" />.</typeparam>
34+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
2835
/// <returns>An updated configuration.</returns>
29-
public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T> newValue)
36+
public static IEnumerable<T> Combine<T>(
37+
IEnumerable<T> oldValue,
38+
IEnumerable<T> newValue)
3039
{
3140
if (newValue == null && oldValue == null)
3241
{
@@ -42,14 +51,17 @@ public static IEnumerable<T> Combine<T>(IEnumerable<T> oldValue, IEnumerable<T>
4251
}
4352

4453
/// <summary>
45-
/// Combines all existing and new configuration changes while preserving the order of insertion.
46-
/// If there are no changes, the previous configurations are returned.
54+
/// Combines all existing and new configuration changes while preserving the
55+
/// order of insertion. If there are no changes, the previous configurations
56+
/// are returned.
4757
/// </summary>
4858
/// <param name="oldValue">The old configuration.</param>
4959
/// <param name="newValue">The new configuration.</param>
50-
/// <typeparam name="T">Type of <see cref="IReadOnlyList{T}" />.</typeparam>
60+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
5161
/// <returns>An updated configuration.</returns>
52-
public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyList<T> newValue)
62+
public static IReadOnlyList<T> Combine<T>(
63+
IReadOnlyList<T> oldValue,
64+
IReadOnlyList<T> newValue)
5365
{
5466
if (newValue == null && oldValue == null)
5567
{
@@ -65,14 +77,51 @@ public static IReadOnlyList<T> Combine<T>(IReadOnlyList<T> oldValue, IReadOnlyLi
6577
}
6678

6779
/// <summary>
68-
/// Combines all existing and new configuration changes. If there are no changes, the previous configurations are returned.
80+
/// Combines all existing and new configuration changes. If there are no changes,
81+
/// the previous configuration is returned.
6982
/// </summary>
83+
/// <remarks>
84+
/// Uses <see cref="ComposableEnumerable{T}.Compose" /> on <paramref name="newValue" />
85+
/// to combine configurations. The existing <paramref name="oldValue" /> is passed as
86+
/// an argument to that method.
87+
/// </remarks>
7088
/// <param name="oldValue">The old configuration.</param>
7189
/// <param name="newValue">The new configuration.</param>
72-
/// <typeparam name="TKey">The type of keys in the read-only dictionary.</typeparam>
73-
/// <typeparam name="TValue">The type of values in the read-only dictionary.</typeparam>
90+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
7491
/// <returns>An updated configuration.</returns>
75-
public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(IReadOnlyDictionary<TKey, TValue> oldValue, IReadOnlyDictionary<TKey, TValue> newValue)
92+
public static ComposableEnumerable<T> Combine<T>(
93+
ComposableEnumerable<T> oldValue,
94+
ComposableEnumerable<T> newValue)
95+
{
96+
// Creating a new container configuration before merging will follow this branch
97+
// and return the default value. If we use the overwrite implementation,
98+
// merging will reset the collection, we should either return null or use
99+
// the append implementation.
100+
if (newValue == null && oldValue == null)
101+
{
102+
return new AppendEnumerable<T>(Array.Empty<T>());
103+
}
104+
105+
if (newValue == null || oldValue == null)
106+
{
107+
return newValue ?? oldValue;
108+
}
109+
110+
return newValue.Compose(oldValue);
111+
}
112+
113+
/// <summary>
114+
/// Combines all existing and new configuration changes. If there are no changes,
115+
/// the previous configurations are returned.
116+
/// </summary>
117+
/// <param name="oldValue">The old configuration.</param>
118+
/// <param name="newValue">The new configuration.</param>
119+
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
120+
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
121+
/// <returns>An updated configuration.</returns>
122+
public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(
123+
IReadOnlyDictionary<TKey, TValue> oldValue,
124+
IReadOnlyDictionary<TKey, TValue> newValue)
76125
{
77126
if (newValue == null && oldValue == null)
78127
{
@@ -84,7 +133,19 @@ public static IReadOnlyDictionary<TKey, TValue> Combine<TKey, TValue>(IReadOnlyD
84133
return newValue ?? oldValue;
85134
}
86135

87-
return newValue.Concat(oldValue.Where(item => !newValue.Keys.Contains(item.Key))).ToDictionary(item => item.Key, item => item.Value);
136+
var result = new Dictionary<TKey, TValue>(oldValue.Count + newValue.Count);
137+
138+
foreach (var kvp in oldValue)
139+
{
140+
result[kvp.Key] = kvp.Value;
141+
}
142+
143+
foreach (var kvp in newValue)
144+
{
145+
result[kvp.Key] = kvp.Value;
146+
}
147+
148+
return new ReadOnlyDictionary<TKey, TValue>(result);
88149
}
89150
}
90151
}

src/Testcontainers/Builders/ContainerBuilder`3.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ public TBuilderEntity WithEntrypoint(params string[] entrypoint)
133133

134134
/// <inheritdoc />
135135
public TBuilderEntity WithCommand(params string[] command)
136+
{
137+
var composable = new AppendEnumerable<string>(command);
138+
return WithCommand(composable);
139+
}
140+
141+
/// <inheritdoc />
142+
public TBuilderEntity WithCommand(ComposableEnumerable<string> command)
136143
{
137144
return Clone(new ContainerConfiguration(command: command));
138145
}

src/Testcontainers/Builders/IContainerBuilder`2.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
140140
[PublicAPI]
141141
TBuilderEntity WithCommand(params string[] command);
142142

143+
/// <summary>
144+
/// Overrides the container's command arguments.
145+
/// </summary>
146+
/// <remarks>
147+
/// The <see cref="ComposableEnumerable{T}" /> allows to choose how existing builder configurations are composed.
148+
/// </remarks>
149+
/// <param name="command">A list of commands, "executable", "param1", "param2" or "param1", "param2".</param>
150+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
151+
[PublicAPI]
152+
TBuilderEntity WithCommand(ComposableEnumerable<string> command);
153+
143154
/// <summary>
144155
/// Sets the environment variable.
145156
/// </summary>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Collections.Generic;
4+
using JetBrains.Annotations;
5+
using DotNet.Testcontainers.Builders;
6+
7+
/// <summary>
8+
/// Represents a composable dictionary that combines its elements by appending
9+
/// the elements of another dictionary with overwriting existing keys.
10+
/// </summary>
11+
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
12+
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
13+
[PublicAPI]
14+
public sealed class AppendDictionary<TKey, TValue> : ComposableDictionary<TKey, TValue>
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="AppendDictionary{TKey,TValue}" /> class.
18+
/// </summary>
19+
/// <param name="dictionary">The dictionary whose elements are copied to the new dictionary.</param>
20+
public AppendDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
21+
: base(dictionary)
22+
{
23+
}
24+
25+
/// <inheritdoc />
26+
public override ComposableDictionary<TKey, TValue> Compose(IReadOnlyDictionary<TKey, TValue> other)
27+
{
28+
return new AppendDictionary<TKey, TValue>(BuildConfiguration.Combine(other, this));
29+
}
30+
}
31+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Collections.Generic;
4+
using JetBrains.Annotations;
5+
using DotNet.Testcontainers.Builders;
6+
7+
/// <summary>
8+
/// Represents a composable collection that combines its elements by appending
9+
/// the elements of another collection.
10+
/// </summary>
11+
/// <typeparam name="T">The type of elements in the collection.</typeparam>
12+
[PublicAPI]
13+
public sealed class AppendEnumerable<T> : ComposableEnumerable<T>
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="AppendEnumerable{T}" /> class.
17+
/// </summary>
18+
/// <param name="collection">The collection of items. If <c>null</c>, an empty collection is used.</param>
19+
public AppendEnumerable(IEnumerable<T> collection)
20+
: base(collection)
21+
{
22+
}
23+
24+
/// <inheritdoc />
25+
public override ComposableEnumerable<T> Compose(IEnumerable<T> other)
26+
{
27+
return new AppendEnumerable<T>(BuildConfiguration.Combine(other, this));
28+
}
29+
}
30+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace DotNet.Testcontainers.Configurations
2+
{
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Collections.ObjectModel;
6+
using JetBrains.Annotations;
7+
8+
/// <summary>
9+
/// Represents an immutable dictionary that defines a custom strategy for
10+
/// composing its elements with those of another dictionary. This class is
11+
/// intended to be inherited by implementations that specify how two dictionaries
12+
/// should be combined.
13+
/// </summary>
14+
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
15+
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
16+
[PublicAPI]
17+
public abstract class ComposableDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
18+
{
19+
private readonly IReadOnlyDictionary<TKey, TValue> _dictionary;
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="ComposableDictionary{TKey, TValue}" /> class.
23+
/// </summary>
24+
/// <param name="dictionary">The dictionary of items. If <c>null</c>, an empty dictionary is used.</param>
25+
protected ComposableDictionary(IReadOnlyDictionary<TKey, TValue> dictionary)
26+
{
27+
_dictionary = dictionary ?? new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>());
28+
}
29+
30+
/// <summary>
31+
/// Combines the current dictionary with the specified dictionary according to
32+
/// the composition strategy defined by the class.
33+
/// </summary>
34+
/// <remarks>
35+
/// The <paramref name="other" /> parameter corresponds to the previous builder
36+
/// configuration.
37+
/// </remarks>
38+
/// <param name="other">The incoming dictionary to compose with this dictionary.</param>
39+
/// <returns>A new <see cref="IReadOnlyDictionary{TKey, TValue}" /> that contains the result of the composition.</returns>
40+
public abstract ComposableDictionary<TKey, TValue> Compose([NotNull] IReadOnlyDictionary<TKey, TValue> other);
41+
42+
/// <inheritdoc />
43+
public IEnumerable<TKey> Keys => _dictionary.Keys;
44+
45+
/// <inheritdoc />
46+
public IEnumerable<TValue> Values => _dictionary.Values;
47+
48+
/// <inheritdoc />
49+
public int Count => _dictionary.Count;
50+
51+
/// <inheritdoc />
52+
public TValue this[TKey key] => _dictionary[key];
53+
54+
/// <inheritdoc />
55+
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
56+
57+
/// <inheritdoc />
58+
public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value);
59+
60+
/// <inheritdoc />
61+
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => _dictionary.GetEnumerator();
62+
63+
/// <inheritdoc />
64+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
65+
}
66+
}

0 commit comments

Comments
 (0)