diff --git a/src/Testcontainers.MsSql/MsSqlBuilder.cs b/src/Testcontainers.MsSql/MsSqlBuilder.cs index 402986a81..819c8ffdd 100644 --- a/src/Testcontainers.MsSql/MsSqlBuilder.cs +++ b/src/Testcontainers.MsSql/MsSqlBuilder.cs @@ -126,17 +126,24 @@ private MsSqlBuilder WithUsername(string username) /// /// - /// Uses the sqlcmd utility scripting variables to detect readiness of the MsSql container: + /// Uses the sqlcmd utility scripting variables to detect readiness of the MsSql container: /// https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility?view=sql-server-linux-ver15#sqlcmd-scripting-variables. /// private sealed class WaitUntil : IWaitUntil { - private readonly string[] _command = { "/opt/mssql-tools/bin/sqlcmd", "-Q", "SELECT 1;" }; - /// - public async Task UntilAsync(IContainer container) + public Task UntilAsync(IContainer container) + { + return UntilAsync(container as MsSqlContainer); + } + + /// + private async Task UntilAsync(MsSqlContainer container) { - var execResult = await container.ExecAsync(_command) + var sqlCmdFilePath = await container.GetSqlCmdFilePathAsync() + .ConfigureAwait(false); + + var execResult = await container.ExecAsync(new[] { sqlCmdFilePath, "-C", "-Q", "SELECT 1;" }) .ConfigureAwait(false); return 0L.Equals(execResult.ExitCode); diff --git a/src/Testcontainers.MsSql/MsSqlContainer.cs b/src/Testcontainers.MsSql/MsSqlContainer.cs index 626fec38d..6f3b13f1e 100644 --- a/src/Testcontainers.MsSql/MsSqlContainer.cs +++ b/src/Testcontainers.MsSql/MsSqlContainer.cs @@ -4,6 +4,10 @@ namespace Testcontainers.MsSql; [PublicAPI] public sealed class MsSqlContainer : DockerContainer, IDatabaseContainer { + private static readonly string[] FindSqlCmdFilePath = { "/bin/sh", "-c", "find /opt/mssql-tools*/bin/sqlcmd -type f -print -quit" }; + + private readonly Lazy> _lazySqlCmdFilePath; + private readonly MsSqlConfiguration _configuration; /// @@ -13,6 +17,7 @@ public sealed class MsSqlContainer : DockerContainer, IDatabaseContainer public MsSqlContainer(MsSqlConfiguration configuration) : base(configuration) { + _lazySqlCmdFilePath = new Lazy>(FindSqlCmdFilePathAsync); _configuration = configuration; } @@ -31,6 +36,19 @@ public string GetConnectionString() return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value))); } + /// + /// Gets the sqlcmd utility file path. + /// + /// + /// The file path represents the path from the container, not from the Docker or test host. + /// + /// Cancellation token. + /// Task that completes when the sqlcmd utility file path has been found. + public Task GetSqlCmdFilePathAsync(CancellationToken ct = default) + { + return _lazySqlCmdFilePath.Value; + } + /// /// Executes the SQL script in the MsSql container. /// @@ -41,10 +59,26 @@ public async Task ExecScriptAsync(string scriptContent, Cancellation { var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName()); + var sqlCmdFilePath = await GetSqlCmdFilePathAsync(ct) + .ConfigureAwait(false); + await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, Unix.FileMode644, ct) .ConfigureAwait(false); - return await ExecAsync(new[] { "/opt/mssql-tools/bin/sqlcmd", "-b", "-r", "1", "-U", _configuration.Username, "-P", _configuration.Password, "-i", scriptFilePath }, ct) + return await ExecAsync(new[] { sqlCmdFilePath, "-C", "-b", "-r", "1", "-U", _configuration.Username, "-P", _configuration.Password, "-i", scriptFilePath }, ct) + .ConfigureAwait(false); + } + + private async Task FindSqlCmdFilePathAsync() + { + var findSqlCmdFilePathExecResult = await ExecAsync(FindSqlCmdFilePath) .ConfigureAwait(false); + + if (findSqlCmdFilePathExecResult.ExitCode == 0) + { + return findSqlCmdFilePathExecResult.Stdout.Trim(); + } + + throw new NotSupportedException("The sqlcmd binary could not be found."); } } \ No newline at end of file diff --git a/tests/Testcontainers.MsSql.Tests/MsSqlContainerTest.cs b/tests/Testcontainers.MsSql.Tests/MsSqlContainerTest.cs index f7d18102f..1f94c3b82 100644 --- a/tests/Testcontainers.MsSql.Tests/MsSqlContainerTest.cs +++ b/tests/Testcontainers.MsSql.Tests/MsSqlContainerTest.cs @@ -1,8 +1,13 @@ namespace Testcontainers.MsSql; -public sealed class MsSqlContainerTest : IAsyncLifetime +public abstract class MsSqlContainerTest : IAsyncLifetime { - private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder().Build(); + private readonly MsSqlContainer _msSqlContainer; + + public MsSqlContainerTest(MsSqlContainer msSqlContainer) + { + _msSqlContainer = msSqlContainer; + } public Task InitializeAsync() { @@ -43,4 +48,22 @@ public async Task ExecScriptReturnsSuccessful() Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); Assert.Empty(execResult.Stderr); } + + [UsedImplicitly] + public sealed class MsSqlDefaultConfiguration : MsSqlContainerTest + { + public MsSqlDefaultConfiguration() + : base(new MsSqlBuilder().Build()) + { + } + } + + [UsedImplicitly] + public sealed class MsSqlTools18Configuration : MsSqlContainerTest + { + public MsSqlTools18Configuration() + : base(new MsSqlBuilder().WithImage("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04").Build()) + { + } + } } \ No newline at end of file diff --git a/tests/Testcontainers.MsSql.Tests/Usings.cs b/tests/Testcontainers.MsSql.Tests/Usings.cs index 02acb4398..493aff2cb 100644 --- a/tests/Testcontainers.MsSql.Tests/Usings.cs +++ b/tests/Testcontainers.MsSql.Tests/Usings.cs @@ -2,5 +2,6 @@ global using System.Data.Common; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; +global using JetBrains.Annotations; global using Microsoft.Data.SqlClient; global using Xunit; \ No newline at end of file