From 533513f65b5abb007f51eb6a6c9335053522a200 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Thu, 30 Mar 2023 01:16:00 +0200 Subject: [PATCH] feat: Enable SuccessfulCommandTest by leveraging Testcontainers (#4198) Co-authored-by: Alan West <3676547+alanwest@users.noreply.github.com> --- .github/workflows/integration.yml | 12 -- build/Common.nonprod.props | 1 + .../Dockerfile | 22 --- ...try.Instrumentation.SqlClient.Tests.csproj | 3 + .../SqlClientIntegrationTests.cs | 131 ++++++++++++++++++ .../SqlClientTests.cs | 90 +----------- .../docker-compose.yml | 25 ---- .../EnabledOnDockerPlatformTheoryAttribute.cs | 86 ++++++++++++ 8 files changed, 224 insertions(+), 146 deletions(-) delete mode 100644 test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile create mode 100644 test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs delete mode 100644 test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml create mode 100644 test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index f28c7a51e3e..3127c132789 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -11,18 +11,6 @@ on: - '**.md' jobs: - sql-test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - version: [net6.0,net7.0] - steps: - - uses: actions/checkout@v3 - - - name: Run sql docker-compose.integration - run: docker-compose --file=test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build - w3c-trace-context-test: runs-on: ubuntu-latest strategy: diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props index fc4cb09c5a8..2f550fb8ff0 100644 --- a/build/Common.nonprod.props +++ b/build/Common.nonprod.props @@ -42,6 +42,7 @@ [6.0.0,) [17.4.1] [4.18.3,5.0) + 3.0.0 [6.4.0,7.0) [1.0.0,2.0) [6.4.0] diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile deleted file mode 100644 index 1083201dec8..00000000000 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# Create a container for running the OpenTelemetry SQL Client integration tests. -# This should be run from the root of the repo: -# docker build --file test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile . - -ARG BUILD_SDK_VERSION=7.0 -ARG TEST_SDK_VERSION=7.0 - -FROM mcr.microsoft.com/dotnet/sdk:${BUILD_SDK_VERSION} AS build -ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net7.0 -WORKDIR /repo -COPY . ./ -RUN ls -la /repo -WORKDIR "/repo/test/OpenTelemetry.Instrumentation.SqlClient.Tests" -RUN dotnet publish "OpenTelemetry.Instrumentation.SqlClient.Tests.csproj" -c "${PUBLISH_CONFIGURATION}" -f "${PUBLISH_FRAMEWORK}" -o /drop -p:IntegrationBuild=true -p:TARGET_FRAMEWORK=${PUBLISH_FRAMEWORK} - -FROM mcr.microsoft.com/dotnet/sdk:${TEST_SDK_VERSION} AS final -ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait -RUN chmod +x /wait -WORKDIR /test -COPY --from=build /drop . -ENTRYPOINT ["dotnet", "vstest", "OpenTelemetry.Instrumentation.SqlClient.Tests.dll", "--logger:console;verbosity=detailed"] diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj index 3d0c9a1acba..67a1af302c9 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj @@ -11,6 +11,7 @@ + @@ -21,6 +22,8 @@ + + all diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs new file mode 100644 index 00000000000..45f4408d61f --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientIntegrationTests.cs @@ -0,0 +1,131 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Data; +using System.Diagnostics; +using System.Runtime.InteropServices; +using DotNet.Testcontainers.Containers; +using Microsoft.Data.SqlClient; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Testcontainers.MsSql; +using Testcontainers.SqlEdge; +using Xunit; + +namespace OpenTelemetry.Instrumentation.SqlClient.Tests +{ + public sealed class SqlClientIntegrationTests : IAsyncLifetime + { + // The Microsoft SQL Server Docker image is not compatible with ARM devices, such as Macs with Apple Silicon. + private readonly IContainer databaseContainer = Architecture.Arm64.Equals(RuntimeInformation.ProcessArchitecture) ? new SqlEdgeBuilder().Build() : new MsSqlBuilder().Build(); + + public Task InitializeAsync() + { + return this.databaseContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return this.databaseContainer.DisposeAsync().AsTask(); + } + + [Trait("CategoryName", "SqlIntegrationTests")] + [EnabledOnDockerPlatformTheory(EnabledOnDockerPlatformTheoryAttribute.DockerPlatform.Linux)] + [InlineData(CommandType.Text, "select 1/1", false)] + [InlineData(CommandType.Text, "select 1/1", false, true)] + [InlineData(CommandType.Text, "select 1/0", false, false, true)] + [InlineData(CommandType.Text, "select 1/0", false, false, true, false, false)] + [InlineData(CommandType.Text, "select 1/0", false, false, true, true, false)] + [InlineData(CommandType.StoredProcedure, "sp_who", false)] + [InlineData(CommandType.StoredProcedure, "sp_who", true)] + public void SuccessfulCommandTest( + CommandType commandType, + string commandText, + bool captureStoredProcedureCommandName, + bool captureTextCommandContent = false, + bool isFailure = false, + bool recordException = false, + bool shouldEnrich = true) + { +#if NETFRAMEWORK + // Disable things not available on netfx + recordException = false; + shouldEnrich = false; +#endif + + var sampler = new TestSampler(); + var activities = new List(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddInMemoryExporter(activities) + .AddSqlClientInstrumentation(options => + { +#if !NETFRAMEWORK + options.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; + options.SetDbStatementForText = captureTextCommandContent; +#else + options.SetDbStatementForText = captureStoredProcedureCommandName || captureTextCommandContent; +#endif + options.RecordException = recordException; + if (shouldEnrich) + { + options.Enrich = SqlClientTests.ActivityEnrichment; + } + }) + .Build(); + + using SqlConnection sqlConnection = new SqlConnection(this.GetConnectionString()); + + sqlConnection.Open(); + + string dataSource = sqlConnection.DataSource; + + sqlConnection.ChangeDatabase("master"); + + using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) + { + CommandType = commandType, + }; + + try + { + sqlCommand.ExecuteNonQuery(); + } + catch + { + } + + Assert.Single(activities); + var activity = activities[0]; + + SqlClientTests.VerifyActivityData(commandType, commandText, captureStoredProcedureCommandName, captureTextCommandContent, isFailure, recordException, shouldEnrich, dataSource, activity); + SqlClientTests.VerifySamplingParameters(sampler.LatestSamplingParameters); + } + + private string GetConnectionString() + { + switch (this.databaseContainer) + { + case SqlEdgeContainer container: + return container.GetConnectionString(); + case MsSqlContainer container: + return container.GetConnectionString(); + default: + throw new InvalidOperationException($"Container type ${this.databaseContainer.GetType().Name} not supported."); + } + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs index b72bcbacf5a..b7124668b22 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs @@ -27,19 +27,8 @@ namespace OpenTelemetry.Instrumentation.SqlClient.Tests { public class SqlClientTests : IDisposable { - /* - To run the integration tests, set the OTEL_SQLCONNECTIONSTRING machine-level environment variable to a valid Sql Server connection string. - - To use Docker... - 1) Run: docker run -d --name sql2019 -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Pass@word" -p 5433:1433 mcr.microsoft.com/mssql/server:2019-latest - 2) Set OTEL_SQLCONNECTIONSTRING as: Data Source=127.0.0.1,5433; User ID=sa; Password=Pass@word - */ - - private const string SqlConnectionStringEnvVarName = "OTEL_SQLCONNECTIONSTRING"; private const string TestConnectionString = "Data Source=(localdb)\\MSSQLLocalDB;Database=master"; - private static readonly string SqlConnectionString = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(SqlConnectionStringEnvVarName); - private readonly FakeSqlClientDiagnosticSource fakeSqlClientDiagnosticSource; public SqlClientTests() @@ -81,79 +70,6 @@ public void SqlClient_NamedOptions() Assert.Equal(1, namedExporterOptionsConfigureOptionsInvocations); } - [Trait("CategoryName", "SqlIntegrationTests")] - [SkipUnlessEnvVarFoundTheory(SqlConnectionStringEnvVarName)] - [InlineData(CommandType.Text, "select 1/1", false)] - [InlineData(CommandType.Text, "select 1/1", false, true)] - [InlineData(CommandType.Text, "select 1/0", false, false, true)] - [InlineData(CommandType.Text, "select 1/0", false, false, true, false, false)] - [InlineData(CommandType.Text, "select 1/0", false, false, true, true, false)] - [InlineData(CommandType.StoredProcedure, "sp_who", false)] - [InlineData(CommandType.StoredProcedure, "sp_who", true)] - public void SuccessfulCommandTest( - CommandType commandType, - string commandText, - bool captureStoredProcedureCommandName, - bool captureTextCommandContent = false, - bool isFailure = false, - bool recordException = false, - bool shouldEnrich = true) - { -#if NETFRAMEWORK - // Disable things not available on netfx - recordException = false; - shouldEnrich = false; -#endif - - var sampler = new TestSampler(); - var activities = new List(); - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(sampler) - .AddInMemoryExporter(activities) - .AddSqlClientInstrumentation(options => - { -#if !NETFRAMEWORK - options.SetDbStatementForStoredProcedure = captureStoredProcedureCommandName; - options.SetDbStatementForText = captureTextCommandContent; -#else - options.SetDbStatementForText = captureStoredProcedureCommandName || captureTextCommandContent; -#endif - options.RecordException = recordException; - if (shouldEnrich) - { - options.Enrich = ActivityEnrichment; - } - }) - .Build(); - - using SqlConnection sqlConnection = new SqlConnection(SqlConnectionString); - - sqlConnection.Open(); - - string dataSource = sqlConnection.DataSource; - - sqlConnection.ChangeDatabase("master"); - - using SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection) - { - CommandType = commandType, - }; - - try - { - sqlCommand.ExecuteNonQuery(); - } - catch - { - } - - Assert.Single(activities); - var activity = activities[0]; - - VerifyActivityData(commandType, commandText, captureStoredProcedureCommandName, captureTextCommandContent, isFailure, recordException, shouldEnrich, dataSource, activity); - VerifySamplingParameters(sampler.LatestSamplingParameters); - } - // DiagnosticListener-based instrumentation is only available on .NET Core #if !NETFRAMEWORK [Theory] @@ -384,7 +300,7 @@ public void ShouldNotCollectTelemetryAndShouldNotPropagateExceptionWhenFilterThr } #endif - private static void VerifyActivityData( + internal static void VerifyActivityData( CommandType commandType, string commandText, bool captureStoredProcedureCommandName, @@ -464,7 +380,7 @@ private static void VerifyActivityData( Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); } - private static void VerifySamplingParameters(SamplingParameters samplingParameters) + internal static void VerifySamplingParameters(SamplingParameters samplingParameters) { Assert.NotNull(samplingParameters.Tags); Assert.Contains( @@ -473,7 +389,7 @@ private static void VerifySamplingParameters(SamplingParameters samplingParamete && (string)kvp.Value == SqlActivitySourceHelper.MicrosoftSqlServerDatabaseSystemName); } - private static void ActivityEnrichment(Activity activity, string method, object obj) + internal static void ActivityEnrichment(Activity activity, string method, object obj) { activity.SetTag("enriched", "yes"); diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml deleted file mode 100644 index 2c4b2b2763a..00000000000 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Start a sql container and then run OpenTelemetry sql integration tests. -# This should be run from the root of the repo: -# opentelemetry>docker-compose --file=test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml --project-directory=. up --exit-code-from=tests --build -version: '3.7' - -services: - sql: - image: mcr.microsoft.com/mssql/server:2019-latest - environment: - - ACCEPT_EULA=Y - # Note: This password is for the ephemeral sql instance running in the container used for tests. Nothing that needs to be handled securely. - - SA_PASSWORD=Pass@word18 - ports: - - "1433:1433" - - tests: - build: - context: . - dockerfile: ./test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile - entrypoint: ["bash", "-c", "/wait && dotnet vstest OpenTelemetry.Instrumentation.SqlClient.Tests.dll --TestCaseFilter:CategoryName=SqlIntegrationTests"] - environment: - - OTEL_SQLCONNECTIONSTRING=Data Source=sql; User ID=sa; Password=Pass@word18 - - WAIT_HOSTS=sql:1433 - depends_on: - - sql diff --git a/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs b/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs new file mode 100644 index 00000000000..fd99e0c4499 --- /dev/null +++ b/test/OpenTelemetry.Tests/Shared/EnabledOnDockerPlatformTheoryAttribute.cs @@ -0,0 +1,86 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics; +using System.Text; +using Xunit; + +namespace OpenTelemetry.Tests; + +/// +/// This skips tests if the required Docker engine is not available. +/// +internal class EnabledOnDockerPlatformTheoryAttribute : TheoryAttribute +{ + /// + /// Initializes a new instance of the class. + /// + public EnabledOnDockerPlatformTheoryAttribute(DockerPlatform dockerPlatform) + { + const string executable = "docker"; + + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + + void AppendStdout(object sender, DataReceivedEventArgs e) => stdout.Append(e.Data); + void AppendStderr(object sender, DataReceivedEventArgs e) => stderr.Append(e.Data); + + var processStartInfo = new ProcessStartInfo(); + processStartInfo.FileName = executable; + processStartInfo.Arguments = string.Join(" ", "version", "--format '{{.Server.Os}}'"); + processStartInfo.RedirectStandardOutput = true; + processStartInfo.RedirectStandardError = true; + processStartInfo.UseShellExecute = false; + + var process = new Process(); + process.StartInfo = processStartInfo; + process.OutputDataReceived += AppendStdout; + process.ErrorDataReceived += AppendStderr; + + try + { + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + } + finally + { + process.OutputDataReceived -= AppendStdout; + process.ErrorDataReceived -= AppendStderr; + } + + if (0.Equals(process.ExitCode) && stdout.ToString().Contains(dockerPlatform.ToString().ToLowerInvariant())) + { + return; + } + + this.Skip = $"The Docker {dockerPlatform} engine is not available."; + } + + public enum DockerPlatform + { + /// + /// Docker Linux engine. + /// + Linux, + + /// + /// Docker Windows engine. + /// + Windows, + } +}