Skip to content

Commit 705290b

Browse files
authored
feat: Support coping directories using WithResourceMapping(string, string) (#932)
1 parent 0ecda30 commit 705290b

File tree

10 files changed

+139
-41
lines changed

10 files changed

+139
-41
lines changed

docs/api/create_docker_container.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ _ = new ContainerBuilder()
2929
.WithEnvironment("ASPNETCORE_URLS", "https://+")
3030
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Path", "/app/certificate.crt")
3131
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Password", "password")
32-
.WithResourceMapping("certificate.crt", "/app/certificate.crt");
32+
.WithResourceMapping("certificate.crt", "/app/");
3333
```
3434

3535
`WithBindMount(string, string)` is another option to provide access to directories or files. It mounts a host directory or file into the container. Note, this does not follow our best practices. Host paths differ between environments and may not be available on every system or Docker setup, e.g. CI.

src/Testcontainers.WebDriver/WebDriverBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public WebDriverBuilder WithBrowser(WebDriverBrowser webDriverBrowser)
6464
/// <returns>A configured instance of <see cref="WebDriverBuilder" />.</returns>
6565
public WebDriverBuilder WithConfigurationFromTomlFile(string configTomlFilePath)
6666
{
67-
return WithResourceMapping(configTomlFilePath, "/opt/bin/config.toml");
67+
return WithResourceMapping(File.ReadAllBytes(configTomlFilePath), "/opt/bin/config.toml");
6868
}
6969

7070
/// <summary>

src/Testcontainers/Builders/ContainerBuilder`3.cs

+33-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace DotNet.Testcontainers.Builders
33
using System;
44
using System.Collections.Generic;
55
using System.Globalization;
6+
using System.IO;
67
using System.Linq;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -181,22 +182,47 @@ public TBuilderEntity WithPortBinding(string hostPort, string containerPort)
181182
}
182183

183184
/// <inheritdoc />
184-
public TBuilderEntity WithResourceMapping(string source, string destination)
185+
public TBuilderEntity WithResourceMapping(IResourceMapping resourceMapping)
185186
{
186-
return WithResourceMapping(new FileResourceMapping(source, destination));
187+
var resourceMappings = new Dictionary<string, IResourceMapping> { { resourceMapping.Target, resourceMapping } };
188+
return Clone(new ContainerConfiguration(resourceMappings: resourceMappings));
187189
}
188190

189191
/// <inheritdoc />
190-
public TBuilderEntity WithResourceMapping(byte[] resourceContent, string destination)
192+
public TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePath, UnixFileModes fileMode = Unix.FileMode644)
191193
{
192-
return WithResourceMapping(new BinaryResourceMapping(resourceContent, destination));
194+
return WithResourceMapping(new BinaryResourceMapping(resourceContent, filePath, fileMode));
193195
}
194196

195197
/// <inheritdoc />
196-
public TBuilderEntity WithResourceMapping(IResourceMapping resourceMapping)
198+
public TBuilderEntity WithResourceMapping(string source, string target, UnixFileModes fileMode = Unix.FileMode644)
197199
{
198-
var resourceMappings = new Dictionary<string, IResourceMapping> { { resourceMapping.Target, resourceMapping } };
199-
return Clone(new ContainerConfiguration(resourceMappings: resourceMappings));
200+
return WithResourceMapping(new FileResourceMapping(source, target, fileMode));
201+
}
202+
203+
/// <inheritdoc />
204+
public TBuilderEntity WithResourceMapping(DirectoryInfo source, string target, UnixFileModes fileMode = Unix.FileMode644)
205+
{
206+
return WithResourceMapping(source.FullName, target, fileMode);
207+
}
208+
209+
/// <inheritdoc />
210+
public TBuilderEntity WithResourceMapping(FileInfo source, string target, UnixFileModes fileMode = Unix.FileMode644)
211+
{
212+
return WithResourceMapping(source.FullName, target, fileMode);
213+
}
214+
215+
/// <inheritdoc />
216+
public TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, UnixFileModes fileMode = Unix.FileMode644)
217+
{
218+
using (var fileStream = File.Open(source.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
219+
{
220+
using (var streamReader = new BinaryReader(fileStream))
221+
{
222+
var resourceContent = streamReader.ReadBytes((int)streamReader.BaseStream.Length);
223+
return WithResourceMapping(new BinaryResourceMapping(resourceContent, target.ToString(), fileMode));
224+
}
225+
}
200226
}
201227

202228
/// <inheritdoc />

src/Testcontainers/Builders/IContainerBuilder`2.cs

+43-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Builders
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.IO;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Docker.DotNet.Models;
@@ -200,29 +201,62 @@ public interface IContainerBuilder<out TBuilderEntity, out TContainerEntity> : I
200201
TBuilderEntity WithPortBinding(string hostPort, string containerPort);
201202

202203
/// <summary>
203-
/// Copies the source file to the created container before it starts.
204+
/// Copies the byte array content of the resource mapping to the created container before it starts.
204205
/// </summary>
205-
/// <param name="source">An absolute path or a name value within the host machine.</param>
206-
/// <param name="destination">An absolute path as destination in the container.</param>
206+
/// <param name="resourceMapping">The resource mapping.</param>
207207
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
208208
[PublicAPI]
209-
TBuilderEntity WithResourceMapping(string source, string destination);
209+
TBuilderEntity WithResourceMapping(IResourceMapping resourceMapping);
210210

211211
/// <summary>
212212
/// Copies the byte array content to the created container before it starts.
213213
/// </summary>
214214
/// <param name="resourceContent">The byte array content of the resource mapping.</param>
215-
/// <param name="destination">An absolute path as destination in the container.</param>
215+
/// <param name="filePath">The target file path to copy the file to.</param>
216+
/// <param name="fileMode">The POSIX file mode permission.</param>
216217
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
217218
[PublicAPI]
218-
TBuilderEntity WithResourceMapping(byte[] resourceContent, string destination);
219+
TBuilderEntity WithResourceMapping(byte[] resourceContent, string filePath, UnixFileModes fileMode = Unix.FileMode644);
219220

220221
/// <summary>
221-
/// Copies the byte array content of the resource mapping to the created container before it starts.
222+
/// Copies a test host directory or file to the container before it starts.
222223
/// </summary>
223-
/// <param name="resourceMapping">The resource mapping.</param>
224+
/// <param name="source">The source directory or file to be copied.</param>
225+
/// <param name="target">The target directory path to copy the files to.</param>
226+
/// <param name="fileMode">The POSIX file mode permission.</param>
224227
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
225-
TBuilderEntity WithResourceMapping(IResourceMapping resourceMapping);
228+
[PublicAPI]
229+
TBuilderEntity WithResourceMapping(string source, string target, UnixFileModes fileMode = Unix.FileMode644);
230+
231+
/// <summary>
232+
/// Copies a test host directory or file to the container before it starts.
233+
/// </summary>
234+
/// <param name="source">The source directory to be copied.</param>
235+
/// <param name="target">The target directory path to copy the files to.</param>
236+
/// <param name="fileMode">The POSIX file mode permission.</param>
237+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
238+
[PublicAPI]
239+
TBuilderEntity WithResourceMapping(DirectoryInfo source, string target, UnixFileModes fileMode = Unix.FileMode644);
240+
241+
/// <summary>
242+
/// Copies a test host directory or file to the container before it starts.
243+
/// </summary>
244+
/// <param name="source">The source file to be copied.</param>
245+
/// <param name="target">The target directory path to copy the file to.</param>
246+
/// <param name="fileMode">The POSIX file mode permission.</param>
247+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
248+
[PublicAPI]
249+
TBuilderEntity WithResourceMapping(FileInfo source, string target, UnixFileModes fileMode = Unix.FileMode644);
250+
251+
/// <summary>
252+
/// Copies a test host file to the container before it starts.
253+
/// </summary>
254+
/// <param name="source">The source file to be copied.</param>
255+
/// <param name="target">The target file path to copy the file to.</param>
256+
/// <param name="fileMode">The POSIX file mode permission.</param>
257+
/// <returns>A configured instance of <typeparamref name="TBuilderEntity" />.</returns>
258+
[PublicAPI]
259+
TBuilderEntity WithResourceMapping(FileInfo source, FileInfo target, UnixFileModes fileMode = Unix.FileMode644);
226260

227261
/// <summary>
228262
/// Assigns the mount configuration to manage data in the container.

src/Testcontainers/Clients/TestcontainersClient.cs

+16
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,22 @@ public Task<ExecResult> ExecAsync(string id, IList<string> command, Cancellation
165165
/// <inheritdoc />
166166
public async Task CopyAsync(string id, IResourceMapping resourceMapping, CancellationToken ct = default)
167167
{
168+
if (Directory.Exists(resourceMapping.Source))
169+
{
170+
await CopyAsync(id, new DirectoryInfo(resourceMapping.Source), resourceMapping.Target, resourceMapping.FileMode, ct)
171+
.ConfigureAwait(false);
172+
173+
return;
174+
}
175+
176+
if (File.Exists(resourceMapping.Source))
177+
{
178+
await CopyAsync(id, new FileInfo(resourceMapping.Source), resourceMapping.Target, resourceMapping.FileMode, ct)
179+
.ConfigureAwait(false);
180+
181+
return;
182+
}
183+
168184
using (var tarOutputMemStream = new TarOutputMemoryStream())
169185
{
170186
await tarOutputMemStream.AddAsync(resourceMapping, ct)

src/Testcontainers/Configurations/Volumes/BinaryResourceMapping.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal class BinaryResourceMapping : FileResourceMapping
1414
/// <param name="resourceContent">The byte array content to map in the container.</param>
1515
/// <param name="containerPath">The absolute path of a file to map in the container.</param>
1616
/// <param name="fileMode">The POSIX file mode permission.</param>
17-
public BinaryResourceMapping(byte[] resourceContent, string containerPath, UnixFileModes fileMode = Unix.FileMode644)
17+
public BinaryResourceMapping(byte[] resourceContent, string containerPath, UnixFileModes fileMode)
1818
: base(string.Empty, containerPath, fileMode)
1919
{
2020
_resourceContent = resourceContent;

src/Testcontainers/Configurations/Volumes/FileResourceMapping.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal class FileResourceMapping : IResourceMapping
1313
/// <param name="hostPath">The absolute path of a file to map on the host system.</param>
1414
/// <param name="containerPath">The absolute path of a file to map in the container.</param>
1515
/// <param name="fileMode">The POSIX file mode permission.</param>
16-
public FileResourceMapping(string hostPath, string containerPath, UnixFileModes fileMode = Unix.FileMode644)
16+
public FileResourceMapping(string hostPath, string containerPath, UnixFileModes fileMode)
1717
{
1818
Type = MountType.Bind;
1919
Source = hostPath;

src/Testcontainers/Configurations/Volumes/IResourceMapping.cs

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public interface IResourceMapping : IMount
1313
/// <summary>
1414
/// Gets the Unix file mode.
1515
/// </summary>
16+
/// <remarks>
17+
/// The <see cref="Unix" /> class provides access to common constant POSIX file mode permissions.
18+
/// </remarks>
1619
UnixFileModes FileMode { get; }
1720

1821
/// <summary>

tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs

+18-6
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,32 @@ public Task<byte[]> GetAllBytesAsync(CancellationToken ct = default)
8585
public async Task TestFileExistsInContainer()
8686
{
8787
// Given
88-
var targetFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);
88+
var targetFilePath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);
89+
90+
var targetFilePath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name);
8991

9092
var targetDirectoryPath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
9193

9294
var targetDirectoryPath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
9395

96+
var targetDirectoryPath3 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
97+
98+
var targetDirectoryPath4 = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
99+
94100
IList<string> targetFilePaths = new List<string>();
95-
targetFilePaths.Add(targetFilePath);
101+
targetFilePaths.Add(targetFilePath1);
102+
targetFilePaths.Add(targetFilePath2);
96103
targetFilePaths.Add(string.Join("/", targetDirectoryPath1, _testFile.Name));
97104
targetFilePaths.Add(string.Join("/", targetDirectoryPath2, _testFile.Name));
105+
targetFilePaths.Add(string.Join("/", targetDirectoryPath3, _testFile.Name));
106+
targetFilePaths.Add(string.Join("/", targetDirectoryPath4, _testFile.Name));
98107

99108
await using var container = new ContainerBuilder()
100109
.WithImage(CommonImages.Alpine)
101110
.WithEntrypoint(CommonCommands.SleepInfinity)
111+
.WithResourceMapping(_testFile, new FileInfo(targetFilePath1))
112+
.WithResourceMapping(_testFile, targetDirectoryPath1)
113+
.WithResourceMapping(_testFile.Directory, targetDirectoryPath2)
102114
.Build();
103115

104116
// When
@@ -108,17 +120,17 @@ public async Task TestFileExistsInContainer()
108120
await container.StartAsync()
109121
.ConfigureAwait(false);
110122

111-
await container.CopyAsync(fileContent, targetFilePath)
123+
await container.CopyAsync(fileContent, targetFilePath2)
112124
.ConfigureAwait(false);
113125

114-
await container.CopyAsync(_testFile, targetDirectoryPath1)
126+
await container.CopyAsync(_testFile, targetDirectoryPath3)
115127
.ConfigureAwait(false);
116128

117-
await container.CopyAsync(_testFile.Directory, targetDirectoryPath2)
129+
await container.CopyAsync(_testFile.Directory, targetDirectoryPath4)
118130
.ConfigureAwait(false);
119131

120132
// Then
121-
var execResults = await Task.WhenAll(targetFilePaths.Select(targetFilePath => container.ExecAsync(new[] { "test", "-f", targetFilePath })))
133+
var execResults = await Task.WhenAll(targetFilePaths.Select(containerFilePath => container.ExecAsync(new[] { "test", "-f", containerFilePath })))
122134
.ConfigureAwait(false);
123135

124136
Assert.All(execResults, result => Assert.Equal(0, result.ExitCode));
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace DotNet.Testcontainers.Tests.Unit
22
{
33
using System;
4+
using System.Collections.Generic;
45
using System.IO;
56
using System.Linq;
67
using System.Text;
@@ -14,26 +15,34 @@ public sealed class CopyResourceMappingContainerTest : IAsyncLifetime, IDisposab
1415
{
1516
private const string ResourceMappingContent = "👋";
1617

17-
private readonly string _resourceMappingSourceFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
18+
private readonly FileInfo _sourceFilePath = new FileInfo(Path.Combine(TestSession.TempDirectoryPath, Path.GetRandomFileName()));
1819

19-
private readonly string _resourceMappingFileDestinationFilePath = string.Join("/", string.Empty, "tmp", Path.GetRandomFileName());
20+
private readonly string _bytesTargetFilePath;
2021

21-
private readonly string _resourceMappingBytesDestinationFilePath = string.Join("/", string.Empty, "tmp", Path.GetRandomFileName());
22+
private readonly string _fileTargetFilePath;
2223

2324
private readonly IContainer _container;
2425

2526
public CopyResourceMappingContainerTest()
2627
{
28+
var resourceContent = Encoding.Default.GetBytes(ResourceMappingContent);
29+
30+
using var fileStream = _sourceFilePath.Create();
31+
fileStream.Write(resourceContent);
32+
33+
_bytesTargetFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _sourceFilePath.Name);
34+
35+
_fileTargetFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid());
36+
2737
_container = new ContainerBuilder()
2838
.WithImage(CommonImages.Alpine)
29-
.WithResourceMapping(_resourceMappingSourceFilePath, _resourceMappingFileDestinationFilePath)
30-
.WithResourceMapping(Encoding.Default.GetBytes(ResourceMappingContent), _resourceMappingBytesDestinationFilePath)
39+
.WithResourceMapping(resourceContent, _bytesTargetFilePath)
40+
.WithResourceMapping(_sourceFilePath, _fileTargetFilePath)
3141
.Build();
3242
}
3343

3444
public Task InitializeAsync()
3545
{
36-
File.WriteAllText(_resourceMappingSourceFilePath, ResourceMappingContent);
3746
return _container.StartAsync();
3847
}
3948

@@ -44,25 +53,23 @@ public Task DisposeAsync()
4453

4554
public void Dispose()
4655
{
47-
if (File.Exists(_resourceMappingSourceFilePath))
48-
{
49-
File.Delete(_resourceMappingSourceFilePath);
50-
}
56+
_sourceFilePath.Delete();
5157
}
5258

5359
[Fact]
5460
public async Task ReadExistingFile()
5561
{
5662
// Given
57-
var resourceMappingBytes = await Task.WhenAll(new[] { _resourceMappingFileDestinationFilePath, _resourceMappingBytesDestinationFilePath }
58-
.Select(resourceMappingFilePath => _container.ReadFileAsync(resourceMappingFilePath)))
59-
.ConfigureAwait(false);
63+
IList<string> targetFilePaths = new List<string>();
64+
targetFilePaths.Add(_bytesTargetFilePath);
65+
targetFilePaths.Add(string.Join("/", _fileTargetFilePath, _sourceFilePath.Name));
6066

6167
// When
62-
var resourceMappingContent = resourceMappingBytes.Select(Encoding.Default.GetString);
68+
var resourceContents = await Task.WhenAll(targetFilePaths.Select(containerFilePath => _container.ReadFileAsync(containerFilePath)))
69+
.ConfigureAwait(false);
6370

6471
// Then
65-
Assert.All(resourceMappingContent, item => Assert.Equal(ResourceMappingContent, item));
72+
Assert.All(resourceContents.Select(Encoding.Default.GetString), resourceContent => Assert.Equal(ResourceMappingContent, resourceContent));
6673
}
6774
}
6875
}

0 commit comments

Comments
 (0)