diff --git a/src/Testcontainers/Builders/ContainerBuilder`3.cs b/src/Testcontainers/Builders/ContainerBuilder`3.cs index b1c48147a..32957700c 100644 --- a/src/Testcontainers/Builders/ContainerBuilder`3.cs +++ b/src/Testcontainers/Builders/ContainerBuilder`3.cs @@ -9,6 +9,7 @@ namespace DotNet.Testcontainers.Builders using System.Threading.Tasks; using Docker.DotNet.Models; using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Configurations.Containers; using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Images; using DotNet.Testcontainers.Networks; @@ -271,6 +272,13 @@ public TBuilderEntity WithResourceMapping(Uri source, string target, uint uid = } } + /// + public TBuilderEntity WithCopyTarArchive(Stream tarArchive, string containerPath = "/") + { + var tarArchiveMappings = new[] { new TarArchiveMapping(tarArchive, containerPath) }; + return Clone(new ContainerConfiguration(tarArchiveMappings: tarArchiveMappings)); + } + /// public TBuilderEntity WithMount(IMount mount) { diff --git a/src/Testcontainers/Builders/IContainerBuilder`2.cs b/src/Testcontainers/Builders/IContainerBuilder`2.cs index 71b3c9c5b..8747f8f74 100644 --- a/src/Testcontainers/Builders/IContainerBuilder`2.cs +++ b/src/Testcontainers/Builders/IContainerBuilder`2.cs @@ -34,6 +34,7 @@ public interface IContainerBuilderA boolean value indicating whether the license agreement is accepted. /// A configured instance of . /// Thrown when the module does not require a license agreement. + [PublicAPI] TBuilderEntity WithAcceptLicenseAgreement(bool acceptLicenseAgreement); /// @@ -321,8 +322,27 @@ public interface IContainerBuilderThe group ID to set for the copied file or directory. Defaults to 0 (root). /// The POSIX file mode permission. /// A configured instance of . + [PublicAPI] TBuilderEntity WithResourceMapping(Uri source, string target, uint uid = 0, uint gid = 0, UnixFileModes fileMode = Unix.FileMode644); + /// + /// Copies a tar archive contents to the container before it starts. + /// + /// + /// + /// Set the property to 0 before calling this method. + /// + /// + /// The caller retains ownership of the stream and is responsible for disposal. + /// The stream content is copied during container startup, so the stream must remain open and readable until the container starts. + /// + /// + /// The with the tar archive contents. + /// The path where tar archive contents should be placed. + /// A configured instance of . + [PublicAPI] + TBuilderEntity WithCopyTarArchive(Stream tarArchive, string containerPath = "/"); + /// /// Assigns the mount configuration to manage data in the container. /// diff --git a/src/Testcontainers/Clients/DockerContainerOperations.cs b/src/Testcontainers/Clients/DockerContainerOperations.cs index 23aacf026..d8f363b5e 100644 --- a/src/Testcontainers/Clients/DockerContainerOperations.cs +++ b/src/Testcontainers/Clients/DockerContainerOperations.cs @@ -1,16 +1,16 @@ namespace DotNet.Testcontainers.Clients { + using Docker.DotNet; + using Docker.DotNet.Models; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Containers; + using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; - using Docker.DotNet; - using Docker.DotNet.Models; - using DotNet.Testcontainers.Configurations; - using DotNet.Testcontainers.Containers; - using Microsoft.Extensions.Logging; internal sealed class DockerContainerOperations : DockerApiClient, IDockerContainerOperations { @@ -109,9 +109,9 @@ public Task RemoveAsync(string id, CancellationToken ct = default) return DockerClient.Containers.RemoveContainerAsync(id, new ContainerRemoveParameters { Force = true, RemoveVolumes = true }, ct); } - public Task ExtractArchiveToContainerAsync(string id, string path, TarOutputMemoryStream tarStream, CancellationToken ct = default) + public Task ExtractArchiveToContainerAsync(string id, string path, Stream tarStream, CancellationToken ct = default) { - Logger.CopyArchiveToDockerContainer(id, tarStream.ContentLength); + Logger.CopyArchiveToDockerContainer(id, tarStream.Length); var copyToContainerParameters = new CopyToContainerParameters { diff --git a/src/Testcontainers/Clients/IDockerContainerOperations.cs b/src/Testcontainers/Clients/IDockerContainerOperations.cs index a7f98d530..c7f4497e3 100644 --- a/src/Testcontainers/Clients/IDockerContainerOperations.cs +++ b/src/Testcontainers/Clients/IDockerContainerOperations.cs @@ -1,13 +1,13 @@ namespace DotNet.Testcontainers.Clients { + using Docker.DotNet.Models; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Containers; using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; - using Docker.DotNet.Models; - using DotNet.Testcontainers.Configurations; - using DotNet.Testcontainers.Containers; internal interface IDockerContainerOperations : IHasListOperations { @@ -25,7 +25,7 @@ internal interface IDockerContainerOperations : IHasListOperations GetArchiveFromContainerAsync(string id, string path, CancellationToken ct = default); diff --git a/src/Testcontainers/Clients/ITestcontainersClient.cs b/src/Testcontainers/Clients/ITestcontainersClient.cs index 76be7bf47..cf31d0fe4 100644 --- a/src/Testcontainers/Clients/ITestcontainersClient.cs +++ b/src/Testcontainers/Clients/ITestcontainersClient.cs @@ -155,6 +155,25 @@ internal interface ITestcontainersClient /// A task that completes when the file has been copied. Task CopyAsync(string id, FileInfo source, string target, uint uid, uint gid, UnixFileModes fileMode, CancellationToken ct = default); + /// + /// Copies a tar archive contents to the container. + /// + /// + /// + /// Set the property to 0 before calling this method. + /// + /// + /// The caller retains ownership of the stream and is responsible for disposal. + /// The stream content is copied during container startup, so the stream must remain open and readable until the container starts. + /// + /// + /// The container id. + /// The with the tar archive contents. + /// The path where tar archive contents should be placed. + /// Cancellation token. + /// A task that completes when the tar archive has been copied. + Task CopyTarArchiveAsync(string id, Stream tarArchive, string containerPath = "/", CancellationToken ct = default); + /// /// Reads a file from the container. /// diff --git a/src/Testcontainers/Clients/TestcontainersClient.cs b/src/Testcontainers/Clients/TestcontainersClient.cs index c7ddeb8ca..adfd3d7a7 100644 --- a/src/Testcontainers/Clients/TestcontainersClient.cs +++ b/src/Testcontainers/Clients/TestcontainersClient.cs @@ -263,6 +263,13 @@ await Container.ExtractArchiveToContainerAsync(id, "/", tarOutputMemStream, ct) } } + /// + public async Task CopyTarArchiveAsync(string id, Stream tarArchive, string containerPath = "/", CancellationToken ct = default) + { + await Container.ExtractArchiveToContainerAsync(id, containerPath, tarArchive, ct) + .ConfigureAwait(false); + } + /// public async Task ReadFileAsync(string id, string filePath, CancellationToken ct = default) { @@ -353,6 +360,12 @@ await Task.WhenAll(configuration.ResourceMappings.Select(resourceMapping => Copy .ConfigureAwait(false); } + if (configuration.TarArchiveMappings.Any()) + { + await Task.WhenAll(configuration.TarArchiveMappings.Select(tarArchive => CopyTarArchiveAsync(id, tarArchive.TarArchive, tarArchive.ContainerPath, ct))) + .ConfigureAwait(false); + } + return id; } diff --git a/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs b/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs index a56845e42..f7a6ed87f 100644 --- a/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs +++ b/src/Testcontainers/Configurations/Containers/ContainerConfiguration.cs @@ -1,17 +1,18 @@ +using Docker.DotNet.Models; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations.Containers; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Images; +using DotNet.Testcontainers.Networks; +using JetBrains.Annotations; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + namespace DotNet.Testcontainers.Configurations { - using System; - using System.Collections.Generic; - using System.Text.Json.Serialization; - using System.Threading; - using System.Threading.Tasks; - using Docker.DotNet.Models; - using DotNet.Testcontainers.Builders; - using DotNet.Testcontainers.Containers; - using DotNet.Testcontainers.Images; - using DotNet.Testcontainers.Networks; - using JetBrains.Annotations; - /// [PublicAPI] public class ContainerConfiguration : ResourceConfiguration, IContainerConfiguration @@ -42,6 +43,7 @@ public class ContainerConfiguration : ResourceConfigurationThe connection string provider. /// A value indicating whether Docker removes the container after it exits or not. /// A value indicating whether the privileged flag is set or not. + /// A list of tar archive mappings. public ContainerConfiguration( IImage image = null, Func imagePullPolicy = null, @@ -65,7 +67,8 @@ public ContainerConfiguration( Func startupCallback = null, IConnectionStringProvider connectionStringProvider = null, bool? autoRemove = null, - bool? privileged = null) + bool? privileged = null, + IEnumerable tarArchiveMappings = null) { AutoRemove = autoRemove; Privileged = privileged; @@ -90,6 +93,7 @@ public ContainerConfiguration( WaitStrategies = waitStrategies; StartupCallback = startupCallback; ConnectionStringProvider = connectionStringProvider; + TarArchiveMappings = tarArchiveMappings; } /// @@ -130,6 +134,7 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig ExposedPorts = BuildConfiguration.Combine(oldValue.ExposedPorts, newValue.ExposedPorts); PortBindings = BuildConfiguration.Combine(oldValue.PortBindings, newValue.PortBindings); ResourceMappings = BuildConfiguration.Combine(oldValue.ResourceMappings, newValue.ResourceMappings); + TarArchiveMappings = BuildConfiguration.Combine(oldValue.TarArchiveMappings, newValue.TarArchiveMappings); Containers = BuildConfiguration.Combine(oldValue.Containers, newValue.Containers); Mounts = BuildConfiguration.Combine(oldValue.Mounts, newValue.Mounts); Networks = BuildConfiguration.Combine(oldValue.Networks, newValue.Networks); @@ -192,6 +197,10 @@ public ContainerConfiguration(IContainerConfiguration oldValue, IContainerConfig [JsonIgnore] public IEnumerable ResourceMappings { get; } + /// + [JsonIgnore] + public IEnumerable TarArchiveMappings { get; } + /// [JsonIgnore] public IEnumerable Containers { get; } diff --git a/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs b/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs index e4369fe07..e0accc2db 100644 --- a/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs +++ b/src/Testcontainers/Configurations/Containers/IContainerConfiguration.cs @@ -1,14 +1,16 @@ namespace DotNet.Testcontainers.Configurations { - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; using Docker.DotNet.Models; + using DotNet.Testcontainers.Configurations.Containers; using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Images; using DotNet.Testcontainers.Networks; using JetBrains.Annotations; + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; /// /// A container configuration. @@ -86,6 +88,11 @@ public interface IContainerConfiguration : IResourceConfiguration IEnumerable ResourceMappings { get; } + /// + /// Gets a list of tar archive mappings. + /// + IEnumerable TarArchiveMappings { get; } + /// /// Gets a list of dependent containers. /// diff --git a/src/Testcontainers/Configurations/Containers/TarArchiveMapping.cs b/src/Testcontainers/Configurations/Containers/TarArchiveMapping.cs new file mode 100644 index 000000000..e4f853fc9 --- /dev/null +++ b/src/Testcontainers/Configurations/Containers/TarArchiveMapping.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace DotNet.Testcontainers.Configurations.Containers +{ + public sealed record TarArchiveMapping + { + public TarArchiveMapping(Stream tarArchive, string containerPath) + { + TarArchive = tarArchive; + ContainerPath = containerPath; + } + + public Stream TarArchive { get; } + + public string ContainerPath { get; } + } +} diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs index 510e66a5e..8401d502c 100644 --- a/src/Testcontainers/Containers/DockerContainer.cs +++ b/src/Testcontainers/Containers/DockerContainer.cs @@ -152,28 +152,28 @@ public string Hostname case "http": case "https": case "tcp": - { - return dockerEndpoint.Host; - } + { + return dockerEndpoint.Host; + } case "npipe": case "unix": - { - const string localhost = "127.0.0.1"; - - if (!Exists()) { - return localhost; - } + const string localhost = "127.0.0.1"; - if (!_client.IsRunningInsideDocker) - { - return localhost; - } + if (!Exists()) + { + return localhost; + } + + if (!_client.IsRunningInsideDocker) + { + return localhost; + } - var endpointSettings = _container.NetworkSettings.Networks.First().Value; - return endpointSettings.Gateway; - } + var endpointSettings = _container.NetworkSettings.Networks.First().Value; + return endpointSettings.Gateway; + } default: throw new InvalidOperationException($"Docker endpoint {dockerEndpoint} is not supported."); @@ -406,6 +406,12 @@ public Task CopyAsync(DirectoryInfo source, string target, uint uid = 0, uint gi return _client.CopyAsync(Id, source, target, uid, gid, fileMode, ct); } + /// + public Task CopyTarArchiveAsync(Stream tarArchive, string containerPath = "/", CancellationToken ct = default) + { + return _client.CopyTarArchiveAsync(Id, tarArchive, containerPath, ct); + } + /// public Task ReadFileAsync(string filePath, CancellationToken ct = default) { diff --git a/src/Testcontainers/Containers/IContainer.cs b/src/Testcontainers/Containers/IContainer.cs index 04eb6a5a7..2fa936dec 100644 --- a/src/Testcontainers/Containers/IContainer.cs +++ b/src/Testcontainers/Containers/IContainer.cs @@ -300,6 +300,24 @@ public interface IContainer : IConnectionStringProvider, IAsyncDisposable /// A task that completes when the file has been copied. Task CopyAsync(FileInfo source, string target, uint uid = 0, uint gid = 0, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default); + /// + /// Copies a tar archive contents to the container. + /// + /// + /// + /// Set the property to 0 before calling this method. + /// + /// + /// The caller retains ownership of the stream and is responsible for disposal. + /// The stream content is copied during container startup, so the stream must remain open and readable until the container starts. + /// + /// + /// The with the tar archive contents. + /// The path where tar archive contents should be placed. + /// Cancellation token. + /// A task that completes when the tar archive has been copied. + Task CopyTarArchiveAsync(Stream tarArchive, string containerPath = "/", CancellationToken ct = default); + /// /// Reads a file from the container. /// diff --git a/tests/Testcontainers.Platform.Linux.Tests/CopyTarArchiveTests.cs b/tests/Testcontainers.Platform.Linux.Tests/CopyTarArchiveTests.cs new file mode 100644 index 000000000..5cf4da183 --- /dev/null +++ b/tests/Testcontainers.Platform.Linux.Tests/CopyTarArchiveTests.cs @@ -0,0 +1,156 @@ +using ICSharpCode.SharpZipLib.Core; +using System.Formats.Tar; + +namespace Testcontainers.Tests +{ + public class CopyTarArchiveTests : FileTestBase + { + public CopyTarArchiveTests() : base() + { + } + + [Fact] + public async Task Should_Copy_TarArchive_ToContainer_BigFile_SystemIO() + { + const long fiveMb = 5L * 1024L * 1024L; + const long fiveGb = fiveMb * 1024L; + + // Given + using (var fs = new FileStream(_testFile.FullName, FileMode.Open, FileAccess.Write, FileShare.ReadWrite)) + { + fs.SetLength(fiveMb); // change for fiveGb + fs.Close(); + } + + var targetDirectoryPath1 = string.Join("/", string.Empty, "tmp", string.Empty); + + var targetFilePaths = new List(); + targetFilePaths.Add(Path.Combine(targetDirectoryPath1, _testFile.Name)); + + var bufferFilePath = Path.Combine(_testFile.Directory.Parent.FullName, Path.GetRandomFileName()); + + using var tarBuffer = new FileStream(bufferFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); + await TarFile.CreateFromDirectoryAsync(_testFile.Directory.FullName, tarBuffer, false, TestContext.Current.CancellationToken); + await tarBuffer.FlushAsync(TestContext.Current.CancellationToken); + tarBuffer.Position = 0; + + await using var container = new ContainerBuilder(CommonImages.Alpine) + .WithEntrypoint(CommonCommands.SleepInfinity) + .WithCopyTarArchive(tarBuffer, targetDirectoryPath1) + .Build(); + + // When + await container.StartAsync(TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + var execResults = await Task.WhenAll(targetFilePaths.Select(containerFilePath => container.ExecAsync(new[] { "test", "-f", containerFilePath }, TestContext.Current.CancellationToken))) + .ConfigureAwait(true); + + Assert.All(execResults, result => Assert.Equal(0, result.ExitCode)); + + // add proper cleanup in case of test failure? + if (File.Exists(bufferFilePath)) + File.Delete(bufferFilePath); + } + + [Fact] + public async Task Should_Copy_TarArchive_ToContainer_SystemIO() + { + // Given + var targetDirectoryPath1 = string.Join("/", string.Empty, "tmp", string.Empty); + + var targetFilePaths = new List(); + targetFilePaths.Add(Path.Combine(targetDirectoryPath1, _testFile.Name)); + + using var memStore = new MemoryStream(); // the underlying storage for tar archive, can be any Stream. + await TarFile.CreateFromDirectoryAsync(_testFile.Directory.FullName, memStore, false, TestContext.Current.CancellationToken); + memStore.Position = 0; // must rewind underlying Stream to start position before copying + + await using var container = new ContainerBuilder(CommonImages.Alpine) + .WithEntrypoint(CommonCommands.SleepInfinity) + .WithCopyTarArchive(memStore, targetDirectoryPath1) // this will copy the Stream with tar archive contents in container just before the container startup + .Build(); + + // When + await container.StartAsync(TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + var execResults = await Task.WhenAll(targetFilePaths.Select(containerFilePath => container.ExecAsync(new[] { "test", "-f", containerFilePath }, TestContext.Current.CancellationToken))) + .ConfigureAwait(true); + + Assert.All(execResults, result => Assert.Equal(0, result.ExitCode)); + } + + [Fact] + public async Task Should_Copy_TarArchive_ToContainer_SharpZipLib() + { + static string ToTarArchivePath(string s) + { + return PathUtils.DropPathRoot(s).Replace(Path.DirectorySeparatorChar, '/'); + } + + // Given + var targetFilePath1 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name); + var targetFilePath2 = string.Join("/", string.Empty, "tmp", Guid.NewGuid(), _testFile.Name); + + var targetFilePaths = new List(); + targetFilePaths.Add(targetFilePath1); + targetFilePaths.Add(targetFilePath2); + if (OperatingSystem.IsWindows()) + { + targetFilePaths.Add(ToTarArchivePath(_testFile.FullName)); + } + + using var memStore = new MemoryStream(); // the underlying storage for tar archive, can be any Stream. + using var tarArchive = TarArchive.CreateOutputTarArchive(memStore, Encoding.UTF8); + tarArchive.IsStreamOwner = false; // setting this property to false is required for copying underlying Stream into container later. + + // entry #1 from the file on host disk, using full path to file + var entry1 = ICSharpCode.SharpZipLib.Tar.TarEntry.CreateEntryFromFile(_testFile.FullName); + // explicitly set a path for file in container to targetFilePath1 + entry1.Name = targetFilePath1; + tarArchive.WriteEntry(entry1, false); + + // entry #2 + var entry2 = ICSharpCode.SharpZipLib.Tar.TarEntry.CreateEntryFromFile(_testFile.FullName); + entry2.Name = targetFilePath2; + // custom userid:groupid + entry2.UserId = 1000; + entry2.GroupId = 1000; + tarArchive.WriteEntry(entry2, false); + + if (OperatingSystem.IsWindows()) + { + // entry #3 + var entry3 = ICSharpCode.SharpZipLib.Tar.TarEntry.CreateEntryFromFile(_testFile.FullName); + // on Windows entry5.Name will be without C:\\ prefix + tarArchive.WriteEntry(entry3, false); + } + + // close the TarArchive, forcing it to write all neccessary data as bytes into underlying Stream + tarArchive.Close(); + memStore.Position = 0; // must rewind underlying Stream to start position before copying + + await using var container = new ContainerBuilder(CommonImages.Alpine) + .WithEntrypoint(CommonCommands.SleepInfinity) + .WithCopyTarArchive(memStore) // this will copy the Stream with tar archive contents in container just before the container startup + .Build(); + + // When + await container.StartAsync(TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + var execResults = await Task.WhenAll(targetFilePaths.Select(containerFilePath => container.ExecAsync(new[] { "test", "-f", containerFilePath }, TestContext.Current.CancellationToken))) + .ConfigureAwait(true); + + Assert.All(execResults, result => Assert.Equal(0, result.ExitCode)); + + // check that uid is correct + var result = await container.ExecAsync(new[] { "stat", "-c", "'%u'", targetFilePath2 }, TestContext.Current.CancellationToken); + Assert.Equal("'1000'\n", result.Stdout); + } + } +} diff --git a/tests/Testcontainers.Platform.Linux.Tests/FileTestBase.cs b/tests/Testcontainers.Platform.Linux.Tests/FileTestBase.cs new file mode 100644 index 000000000..be65b1a25 --- /dev/null +++ b/tests/Testcontainers.Platform.Linux.Tests/FileTestBase.cs @@ -0,0 +1,48 @@ +namespace Testcontainers.Tests +{ + public abstract class FileTestBase : IDisposable + { + protected readonly FileInfo _testFile = new FileInfo(Path.Combine(TestSession.TempDirectoryPath, Guid.NewGuid().ToString("D"), Path.GetRandomFileName())); + + private bool _disposed; + + protected FileTestBase() + { + _ = Directory.CreateDirectory(_testFile.Directory!.FullName); + + using var fileStream = _testFile.Open(FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + fileStream.WriteByte(13); + } + + ~FileTestBase() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void DisposeManagedResources() + { + _testFile.Directory!.Delete(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + DisposeManagedResources(); + } + + _disposed = true; + } + } +} diff --git a/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs b/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs index a375daa02..e03066f86 100644 --- a/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs +++ b/tests/Testcontainers.Platform.Linux.Tests/TarOutputMemoryStreamTest.cs @@ -1,43 +1,20 @@ namespace Testcontainers.Tests; -public abstract class TarOutputMemoryStreamTest : IDisposable +public abstract class TarOutputMemoryStreamTest : FileTestBase { private const string TargetDirectoryPath = "/tmp"; private readonly TarOutputMemoryStream _tarOutputMemoryStream = new TarOutputMemoryStream(TargetDirectoryPath, NullLogger.Instance); - private readonly FileInfo _testFile = new FileInfo(Path.Combine(TestSession.TempDirectoryPath, Guid.NewGuid().ToString("D"), Path.GetRandomFileName())); - - private bool _disposed; - - protected TarOutputMemoryStreamTest() - { - _ = Directory.CreateDirectory(_testFile.Directory!.FullName); - - using var fileStream = _testFile.Open(FileMode.Create, FileAccess.Write, FileShare.ReadWrite); - fileStream.WriteByte(13); - } - - public void Dispose() + protected TarOutputMemoryStreamTest() : base() { - Dispose(true); - GC.SuppressFinalize(this); } - protected virtual void Dispose(bool disposing) + protected override void DisposeManagedResources() { - if (_disposed) - { - return; - } - - if (disposing) - { - _tarOutputMemoryStream.Dispose(); - _testFile.Directory!.Delete(true); - } + base.DisposeManagedResources(); - _disposed = true; + _tarOutputMemoryStream.Dispose(); } [Fact]