diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..b9c8d9b70f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,94 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "release/6.0" ] + pull_request: + branches: [ "release/6.0" ] + schedule: + - cron: '33 23 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: csharp + build-mode: manual + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v5.0.1 + with: + global-json-file: global.json + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + mkdir packages + dotnet build src/Microsoft.Data.SqlClient.sln + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml b/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml index b76a0099b2..76a9b07939 100644 --- a/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml @@ -61,10 +61,10 @@ jobs: displayName: '[Debug] Show Disk Usage' - task: UseDotNet@2 - displayName: 'Use .NET SDK 8.0.x' + displayName: 'Install .NET SDK' inputs: packageType: sdk - version: 8.0.x + useGlobalJson: true - pwsh: | dotnet tool install --global dotnet-coverage diff --git a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml index e113692ab9..ebdd99b3c6 100644 --- a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml @@ -232,18 +232,21 @@ jobs: dotnet --info Invoke-WebRequest https://dot.net/v1/dotnet-install.ps1 -OutFile dotnet-install.ps1 - - # install .net x86 + $version = "LTS" if (!"${{parameters.targetFramework }}".StartsWith("net4")) { $version = "${{parameters.targetFramework }}".Substring(3, "${{parameters.targetFramework }}".Length-3) } + # Install targetFramework specific .NET runtime (and sdk) .\dotnet-install.ps1 -Channel $version -Architecture x86 -InstallDir "$(dotnetx86RootPath)" + # Install globally required .NET sdk + .\dotnet-install.ps1 -Architecture x86 -InstallDir "$(dotnetx86RootPath)" -JSonFile global.json + $(dotnetx86RootPath)dotnet.exe --info - displayName: 'Install .NET x86 ' + displayName: 'Install .NET x86' condition: ne(variables['dotnetx86RootPath'], '') - template: ../steps/run-all-tests-step.yml@self diff --git a/global.json b/global.json new file mode 100644 index 0000000000..f923e6de97 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "9.0.308", + "rollForward": "latestFeature" + } +} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 56843f73e1..63256d721a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -491,8 +491,8 @@ Microsoft\Data\SqlClient\SqlInternalTransaction.cs - - Microsoft\Data\SqlClient\SqlMetadataFactory.cs + + Microsoft\Data\SqlClient\SqlMetaDataFactory.cs Microsoft\Data\SqlClient\SqlNotificationEventArgs.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index de61ff0a54..26b95d1871 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -1,4 +1,4 @@ - + {407890AC-9876-4FEF-A6F1-F36A876BAADE} @@ -12,9 +12,6 @@ $(OutputPath)\Microsoft.Data.SqlClient.xml $(ObjPath)$(AssemblyName)\netfx\ Framework $(BaseProduct) - - True false $(DefineConstants);NETFRAMEWORK; @@ -56,7 +53,8 @@ True True None - MinimumRecommendedRules.ruleset + + MinimumRecommendedRules.ruleset True True $(DefineConstants);USEOFFSET;CODE_ANALYSIS_BASELINE;FEATURE_LEGACYSURFACEAREA;FEATURE_UTF32;FEATURE_UTF7;TRACE; @@ -806,7 +804,7 @@ - + @@ -875,17 +873,6 @@ PreserveNewest - - - {5477469E-83B1-11D2-8B49-00A0C9B7C9C4} - 2 - 4 - 0 - tlbimp - False - True - - diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetadataFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs similarity index 100% rename from src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetadataFactory.cs rename to src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj index d788ea079a..83cadc2092 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj @@ -93,7 +93,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 02a893eaf5..b2d05cf3f1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -7,16 +7,17 @@ using System.Data; using System.Data.SqlTypes; using System.Diagnostics.Tracing; -using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Reflection; using System.Runtime.InteropServices; using System.Security; using System.Security.Principal; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -24,6 +25,7 @@ using Microsoft.Data.SqlClient.TestUtilities; using Microsoft.Identity.Client; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { @@ -102,7 +104,7 @@ public static bool IsAzureSynapse { if (!string.IsNullOrEmpty(TCPConnectionString)) { - s_sqlServerEngineEdition ??= GetSqlServerProperty(TCPConnectionString, "EngineEdition"); + s_sqlServerEngineEdition ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.EngineEdition); } _ = int.TryParse(s_sqlServerEngineEdition, out int engineEditon); return engineEditon == 6; @@ -124,7 +126,7 @@ public static string SQLServerVersion { if (!string.IsNullOrEmpty(TCPConnectionString)) { - s_sQLServerVersion ??= GetSqlServerProperty(TCPConnectionString, "ProductMajorVersion"); + s_sQLServerVersion ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.ProductMajorVersion); } return s_sQLServerVersion; } @@ -238,7 +240,9 @@ public static IEnumerable GetConnectionStrings(bool withEnclave) yield return TCPConnectionString; } // Named Pipes are not supported on Unix platform and for Azure DB - if (Environment.OSVersion.Platform != PlatformID.Unix && IsNotAzureServer() && !string.IsNullOrEmpty(NPConnectionString)) + if (Environment.OSVersion.Platform != PlatformID.Unix && + IsNotAzureServer() && + !string.IsNullOrEmpty(NPConnectionString)) { yield return NPConnectionString; } @@ -287,29 +291,99 @@ private static Task AcquireTokenAsync(string authorityURL, string userID public static bool IsKerberosTest => !string.IsNullOrEmpty(KerberosDomainUser) && !string.IsNullOrEmpty(KerberosDomainPassword); - public static string GetSqlServerProperty(string connectionString, string propertyName) + #nullable enable + + /// + /// Returns the current test name as: + /// + /// ClassName.MethodName + /// + /// xUnit v2 doesn't provide access to a test context, so we use + /// reflection into the ITestOutputHelper to get the test name. + /// + /// + /// + /// The output helper instance for the currently running test. + /// + /// + /// The current test name. + public static string CurrentTestName(ITestOutputHelper outputHelper) + { + // Reflect our way to the ITest instance. + var type = outputHelper.GetType(); + Assert.NotNull(type); + var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(testMember); + var test = testMember.GetValue(outputHelper) as ITest; + Assert.NotNull(test); + + // The DisplayName is in the format: + // + // Namespace.ClassName.MethodName(args) + // + // We only want the ClassName.MethodName portion. + // + Match match = TestNameRegex.Match(test.DisplayName); + Assert.True(match.Success); + // There should be 2 groups: the overall match, and the capture + // group. + Assert.Equal(2, match.Groups.Count); + + // The portion we want is in the capture group. + return match.Groups[1].Value; + } + + private static readonly Regex TestNameRegex = new( + // Capture the ClassName.MethodName portion, which may terminate + // the name, or have (args...) appended. + @"\.((?:[^.]+)\.(?:[^.\(]+))(?:\(.*\))?$", + RegexOptions.Compiled); + + /// + /// SQL Server properties we can query. + /// + /// GOTCHA: The enum member names must match the property names + /// queryable via T-SQL SERVERPROPERTY(). See: + /// + /// https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql + /// + public enum ServerProperty + { + ProductMajorVersion, + EngineEdition + } + + public static string GetSqlServerProperty(string connectionString, ServerProperty property) { - string propertyValue = string.Empty; using SqlConnection conn = new(connectionString); conn.Open(); - SqlCommand command = conn.CreateCommand(); - command.CommandText = $"SELECT SERVERProperty('{propertyName}')"; - SqlDataReader reader = command.ExecuteReader(); - if (reader.Read()) + return GetSqlServerProperty(conn, property); + } + + public static string GetSqlServerProperty(SqlConnection connection, ServerProperty property) + { + using SqlCommand command = connection.CreateCommand(); + command.CommandText = $"SELECT SERVERProperty('{property}')"; + using SqlDataReader reader = command.ExecuteReader(); + + Assert.True(reader.Read()); + + switch (property) { - switch (propertyName) - { - case "EngineEdition": - propertyValue = reader.GetInt32(0).ToString(); - break; - case "ProductMajorVersion": - propertyValue = reader.GetString(0); - break; - } + case ServerProperty.EngineEdition: + // EngineEdition is returned as an int. + return reader.GetInt32(0).ToString(); + case ServerProperty.ProductMajorVersion: + default: + // ProductMajorVersion is returned as a string. + // + // Assume any unknown property is also a string. + return reader.GetString(0); } - return propertyValue; } + #nullable disable + public static bool GetSQLServerStatusOnTDS8(string connectionString) { bool isTDS8Supported = false; @@ -1168,8 +1242,200 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) { IDs.Add(eventData.EventId); EventData.Add(eventData); + OnMatchingEventWritten(eventData); } } + + protected virtual void OnMatchingEventWritten(EventWrittenEventArgs eventData) + { + } + } + + #nullable enable + + public sealed class XEventScope : IDisposable + { + #region Private Fields + + // Maximum dispatch latency for XEvents, in seconds. + private const int MaxDispatchLatencySeconds = 5; + + // The connection to use for all operations. + private readonly SqlConnection _connection; + + // True if connected to an Azure SQL instance. + private readonly bool _isAzureSql; + + // True if connected to a non-Azure SQL Server 2025 (version 17) or + // higher. + private readonly bool _isVersion17OrHigher; + + // Duration for the XEvent session, in minutes. + private readonly ushort _durationInMinutes; + + #endregion + + #region Properties + + /// + /// The name of the XEvent session, derived from the session name + /// provided at construction time, with a unique suffix appended. + /// + public string SessionName { get; } + + #endregion + + #region Construction + + /// + /// Construct with the specified parameters. + /// + /// This will use the connection to query the server properties and + /// setup and start the XEvent session. + /// + /// The base name of the session. + /// The SQL connection to use. (Must already be open.) + /// The event specification T-SQL string. + /// The target specification T-SQL string. + /// The duration of the session in minutes. + public XEventScope( + string sessionName, + // The connection must already be open. + SqlConnection connection, + string eventSpecification, + string targetSpecification, + ushort durationInMinutes = 5) + { + SessionName = GenerateRandomCharacters(sessionName); + + _connection = connection; + Assert.Equal(ConnectionState.Open, _connection.State); + + _durationInMinutes = durationInMinutes; + + // EngineEdition 5 indicates Azure SQL. + _isAzureSql = GetSqlServerProperty(connection, ServerProperty.EngineEdition) == "5"; + + // Determine if we're connected to a SQL Server instance version + // 17 or higher. + if (!_isAzureSql) + { + int majorVersion; + Assert.True( + int.TryParse( + GetSqlServerProperty(connection, ServerProperty.ProductMajorVersion), + out majorVersion)); + _isVersion17OrHigher = majorVersion >= 17; + } + + // Setup and start the XEvent session. + string sessionLocation = _isAzureSql ? "DATABASE" : "SERVER"; + + // Both Azure SQL and SQL Server 2025+ support setting a maximum + // duration for the XEvent session. + string duration = + _isAzureSql || _isVersion17OrHigher + ? $"MAX_DURATION={_durationInMinutes} MINUTES," + : string.Empty; + + string xEventCreateAndStartCommandText = + $@"CREATE EVENT SESSION [{SessionName}] ON {sessionLocation} + {eventSpecification} + {targetSpecification} + WITH ( + {duration} + MAX_MEMORY=16 MB, + EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS, + MAX_DISPATCH_LATENCY={MaxDispatchLatencySeconds} SECONDS, + MAX_EVENT_SIZE=0 KB, + MEMORY_PARTITION_MODE=NONE, + TRACK_CAUSALITY=ON, + STARTUP_STATE=OFF) + + ALTER EVENT SESSION [{SessionName}] ON {sessionLocation} STATE = START "; + + using SqlCommand createXEventSession = new SqlCommand(xEventCreateAndStartCommandText, _connection); + createXEventSession.ExecuteNonQuery(); + } + + /// + /// Disposal stops and drops the XEvent session. + /// + /// + /// Disposal isn't perfect - tests can abort without cleaning up the + /// events they have created. For Azure SQL targets that outlive the + /// test pipelines, it is beneficial to periodically log into the + /// database and drop old XEvent sessions using T-SQL similar to + /// this: + /// + /// DECLARE @sql NVARCHAR(MAX) = N''; + /// + /// -- Identify inactive (stopped) event sessions and generate DROP commands + /// SELECT @sql += N'DROP EVENT SESSION [' + name + N'] ON SERVER;' + CHAR(13) + CHAR(10) + /// FROM sys.server_event_sessions + /// WHERE running = 0; -- Filter for sessions that are not running (inactive) + /// + /// -- Print the generated commands for review (optional, but recommended) + /// PRINT @sql; + /// + /// -- Execute the generated commands + /// EXEC sys.sp_executesql @sql; + /// + public void Dispose() + { + string dropXEventSessionCommand = _isAzureSql + // We choose the sys.(database|server)_event_sessions views + // here to ensure we find sessions that may not be running. + ? $"IF EXISTS (select * from sys.database_event_sessions where name ='{SessionName}')" + + $" DROP EVENT SESSION [{SessionName}] ON DATABASE" + : $"IF EXISTS (select * from sys.server_event_sessions where name ='{SessionName}')" + + $" DROP EVENT SESSION [{SessionName}] ON SERVER"; + + using SqlCommand command = new SqlCommand(dropXEventSessionCommand, _connection); + command.ExecuteNonQuery(); + } + + #endregion + + #region Public Methods + + /// + /// Query the XEvent session for its collected events, returning + /// them as an XML document. + /// + /// This always blocks the thread for MaxDispatchLatencySeconds to + /// ensure that all events have been flushed into the ring buffer. + /// + public System.Xml.XmlDocument GetEvents() + { + string xEventQuery = _isAzureSql + ? $@"SELECT xet.target_data + FROM sys.dm_xe_database_session_targets AS xet + INNER JOIN sys.dm_xe_database_sessions AS xe + ON (xe.address = xet.event_session_address) + WHERE xe.name = '{SessionName}'" + : $@"SELECT xet.target_data + FROM sys.dm_xe_session_targets AS xet + INNER JOIN sys.dm_xe_sessions AS xe + ON (xe.address = xet.event_session_address) + WHERE xe.name = '{SessionName}'"; + + using SqlCommand command = new SqlCommand(xEventQuery, _connection); + + // Wait for maximum dispatch latency to ensure all events + // have been flushed to the ring buffer. + Thread.Sleep(MaxDispatchLatencySeconds * 1000); + + string? targetData = command.ExecuteScalar() as string; + Assert.NotNull(targetData); + + System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument(); + + xmlDocument.LoadXml(targetData); + return xmlDocument; + } + + #endregion } /// @@ -1198,4 +1464,6 @@ public static string GetMachineFQDN(string hostname) return fqdn.ToString(); } } + + #nullable disable } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index e42387f253..e901c019fc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -345,7 +345,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs index 18de7b7133..506d8f81df 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs @@ -13,13 +13,21 @@ using System.Threading.Tasks; using System.Xml; using Xunit; +using Xunit.Abstractions; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { - public static class DataStreamTest + public class DataStreamTest { + private readonly string _testName; + + public DataStreamTest(ITestOutputHelper outputHelper) + { + _testName = DataTestUtility.CurrentTestName(outputHelper); + } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] - public static void RunAllTestsForSingleServer_NP() + public void RunAllTestsForSingleServer_NP() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -33,7 +41,7 @@ public static void RunAllTestsForSingleServer_NP() [ActiveIssue("5540")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void RunAllTestsForSingleServer_TCP() + public void RunAllTestsForSingleServer_TCP() { RunAllTestsForSingleServer(DataTestUtility.TCPConnectionString); } @@ -152,7 +160,8 @@ IF OBJECT_ID('dbo.{tableName}', 'U') IS NOT NULL return data; } - private static void RunAllTestsForSingleServer(string connectionString, bool usingNamePipes = false) + // @TODO: Split into separate tests! + private void RunAllTestsForSingleServer(string connectionString, bool usingNamePipes = false) { RowBuffer(connectionString); InvalidRead(connectionString); @@ -1911,100 +1920,61 @@ private static void VariantCollationsTest(string connectionString) } } - private static void TestXEventsStreaming(string connectionString) - { - string sessionName = DataTestUtility.GenerateRandomCharacters("Session"); + #nullable enable - try - { - //Create XEvent - SetupXevent(connectionString, sessionName); - Task.Factory.StartNew(() => - { - // Read XEvents - int streamXeventCount = 3; - using (SqlConnection xEventsReadConnection = new SqlConnection(connectionString)) - { - xEventsReadConnection.Open(); - string xEventDataStreamCommand = "USE master; " + @"select [type], [data] from sys.fn_MSxe_read_event_stream ('" + sessionName + "',0)"; - using (SqlCommand cmd = new SqlCommand(xEventDataStreamCommand, xEventsReadConnection)) - { - SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.SequentialAccess); - for (int i = 0; i < streamXeventCount && reader.Read(); i++) - { - int colType = reader.GetInt32(0); - int cb = (int)reader.GetBytes(1, 0, null, 0, 0); + private void TestXEventsStreaming(string connectionString) + { + // Create XEvent + using SqlConnection xEventManagementConnection = new SqlConnection(connectionString); + xEventManagementConnection.Open(); - byte[] bytes = new byte[cb]; - long read = reader.GetBytes(1, 0, bytes, 0, cb); + using DataTestUtility.XEventScope xEventScope = + new DataTestUtility.XEventScope( + _testName, + xEventManagementConnection, + "ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence))", + "ADD TARGET package0.ring_buffer"); - // Don't send data on the first read because there is already data in the buffer. - // Don't send data on the last iteration. We will not be reading that data. - if (i == 0 || i == streamXeventCount - 1) - continue; + string sessionName = xEventScope.SessionName; - using (SqlConnection xEventWriteConnection = new SqlConnection(connectionString)) - { - xEventWriteConnection.Open(); - string xEventWriteCommandText = @"exec sp_trace_generateevent 90, N'Test2'"; - using (SqlCommand xEventWriteCommand = new SqlCommand(xEventWriteCommandText, xEventWriteConnection)) - { - xEventWriteCommand.ExecuteNonQuery(); - } - } - } - } - } - }).Wait(10000); - } - finally + Task.Factory.StartNew(() => { - //Delete XEvent - DeleteXevent(connectionString, sessionName); - } - } + // Read XEvents + int streamXeventCount = 3; + using SqlConnection xEventsReadConnection = new SqlConnection(connectionString); + xEventsReadConnection.Open(); - private static void SetupXevent(string connectionString, string sessionName) - { - string xEventCreateAndStartCommandText = @"CREATE EVENT SESSION [" + sessionName + @"] ON SERVER - ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence)) - ADD TARGET package0.ring_buffer - WITH ( - MAX_MEMORY=4096 KB, - EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS, - MAX_DISPATCH_LATENCY=30 SECONDS, - MAX_EVENT_SIZE=0 KB, - MEMORY_PARTITION_MODE=NONE, - TRACK_CAUSALITY=ON, - STARTUP_STATE=OFF) - - ALTER EVENT SESSION [" + sessionName + "] ON SERVER STATE = START "; + string xEventDataStreamCommand = "USE master; " + @"select [type], [data] from sys.fn_MSxe_read_event_stream ('" + sessionName + "',0)"; + using SqlCommand cmd = new SqlCommand(xEventDataStreamCommand, xEventsReadConnection); + using SqlDataReader reader = cmd.ExecuteReader(System.Data.CommandBehavior.SequentialAccess); - using (SqlConnection connection = new SqlConnection(connectionString)) - { - connection.Open(); - using (SqlCommand createXeventSession = new SqlCommand(xEventCreateAndStartCommandText, connection)) + for (int i = 0; i < streamXeventCount && reader.Read(); i++) { - createXeventSession.ExecuteNonQuery(); - } - } - } + int colType = reader.GetInt32(0); + int cb = (int)reader.GetBytes(1, 0, null, 0, 0); - private static void DeleteXevent(string connectionString, string sessionName) - { - string deleteXeventSessionCommand = $"IF EXISTS (select * from sys.server_event_sessions where name ='{sessionName}')" + - $" DROP EVENT SESSION [{sessionName}] ON SERVER"; + byte[] bytes = new byte[cb]; + long read = reader.GetBytes(1, 0, bytes, 0, cb); - using (SqlConnection connection = new SqlConnection(connectionString)) - { - connection.Open(); - using (SqlCommand deleteXeventSession = new SqlCommand(deleteXeventSessionCommand, connection)) - { - deleteXeventSession.ExecuteNonQuery(); + // Don't send data on the first read because there is already data in the buffer. + // Don't send data on the last iteration. We will not be reading that data. + if (i == 0 || i == streamXeventCount - 1) + { + continue; + } + + using SqlConnection xEventWriteConnection = new SqlConnection(connectionString); + xEventWriteConnection.Open(); + + string xEventWriteCommandText = @"exec sp_trace_generateevent 90, N'Test2'"; + using SqlCommand xEventWriteCommand = new SqlCommand(xEventWriteCommandText, xEventWriteConnection); + xEventWriteCommand.ExecuteNonQuery(); } - } + }).Wait(10000); } + #nullable disable + private static void TimeoutDuringReadAsyncWithClosedReaderTest(string connectionString) { // Create the proxy diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs new file mode 100644 index 0000000000..bcb23d11bd --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.XPath; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class XEventsTracingTest + { + private readonly string _testName; + + public XEventsTracingTest(ITestOutputHelper outputHelper) + { + _testName = DataTestUtility.CurrentTestName(outputHelper); + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData("SELECT @@VERSION", System.Data.CommandType.Text, "sql_statement_starting")] + [InlineData("sp_help", System.Data.CommandType.StoredProcedure, "rpc_starting")] + public void XEventActivityIDConsistentWithTracing(string query, System.Data.CommandType commandType, string xEvent) + { + // This test validates that the activity ID recorded in the client-side trace is passed through to the server, + // where it can be recorded in an XEvent session. This is documented at: + // https://learn.microsoft.com/en-us/sql/relational-databases/native-client/features/accessing-diagnostic-information-in-the-extended-events-log + + using SqlConnection activityConnection = new(DataTestUtility.TCPConnectionString); + activityConnection.Open(); + + Guid connectionId = activityConnection.ClientConnectionId; + HashSet ids; + + using SqlConnection xEventManagementConnection = new(DataTestUtility.TCPConnectionString); + xEventManagementConnection.Open(); + + using DataTestUtility.XEventScope xEventSession = new( + _testName, + xEventManagementConnection, + $@"ADD EVENT SQL_STATEMENT_STARTING (ACTION (client_connection_id) WHERE (client_connection_id='{connectionId}')), + ADD EVENT RPC_STARTING (ACTION (client_connection_id) WHERE (client_connection_id='{connectionId}'))", + "ADD TARGET ring_buffer"); + + using (DataTestUtility.MDSEventListener TraceListener = new()) + { + using SqlCommand command = new(query, activityConnection) { CommandType = commandType }; + using SqlDataReader reader = command.ExecuteReader(); + while (reader.Read()) + { + // Flush data + } + + ids = TraceListener.ActivityIDs; + } + + XmlDocument eventList = xEventSession.GetEvents(); + // Get the associated activity ID from the XEvent session. We expect to see the same ID in the trace as well. + string activityId = GetCommandActivityId(query, xEvent, connectionId, eventList); + + Assert.Contains(activityId, ids); + } + + private static string GetCommandActivityId(string commandText, string eventName, Guid connectionId, XmlDocument xEvents) + { + XPathNavigator? xPathRoot = xEvents.CreateNavigator(); + Assert.NotNull(xPathRoot); + + // The transferred activity ID is attached to the "attach_activity_id_xfer" action within + // the "sql_statement_starting" and the "rpc_starting" events. + XPathNodeIterator statementStartingQuery = xPathRoot.Select( + $"/RingBufferTarget/event[@name='{eventName}'" + + $" and action[@name='client_connection_id']/value='{connectionId.ToString().ToUpper()}'" + + $" and (data[@name='statement']='{commandText}' or data[@name='object_name']='{commandText}')]"); + + Assert.Equal(1, statementStartingQuery.Count); + Assert.True(statementStartingQuery.MoveNext()); + + XPathNavigator? current = statementStartingQuery.Current; + Assert.NotNull(current); + XPathNavigator? activityIdElement = current.SelectSingleNode("action[@name='attach_activity_id_xfer']/value"); + + Assert.NotNull(activityIdElement); + Assert.NotNull(activityIdElement.Value); + + return activityIdElement.Value; + } + } +} diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 09398b9e6f..03e4ff61f1 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -54,25 +54,24 @@ 0.13.2 3.1.6 - 10.0.0-beta.24564.1 - 8.0.0-beta.24123.1 - 6.0.1 - 2.0.8 + 10.0.0-beta.25164.6 + 10.0.0-beta.25164.6 + 8.0.1 1.0.3 - 17.8.0 + 17.11.1 172.52.0 10.50.1600.1 160.1000.6 5.0.0 - 13.0.1 - 6.0.1 + 13.0.3 + 8.0.1 6.0.1 4.3.0 5.0.0 5.0.0 - 6.0.0 + 8.0.1 6.0.0 - 2.9.3 + 2.9.2 2.8.2