diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs index ca9fe35743..0ecab5ab0f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs @@ -1,3 +1,7 @@ +// 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. + namespace Microsoft.Data.SqlClient { /// diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 1798e1fcf4..03c61f1881 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -1,3 +1,7 @@ +// 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.Reflection; diff --git a/src/Microsoft.Data.SqlClient/tests/Common/SqlDataReaderExtensions.cs b/src/Microsoft.Data.SqlClient/tests/Common/SqlDataReaderExtensions.cs new file mode 100644 index 0000000000..43c7bde4bd --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/SqlDataReaderExtensions.cs @@ -0,0 +1,37 @@ +// 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. + +namespace Microsoft.Data.SqlClient.Tests.Common +{ + /// + /// Extensions on the class. + /// + public static class SqlDataReaderExtensions + { + /// + /// Reads all result sets in the provided and discards them. + /// + /// Reader to flush results from. + public static void FlushAllResults(this SqlDataReader dataReader) + { + do + { + dataReader.FlushResultSet(); + } while (dataReader.NextResult()); + } + + /// + /// Reads all results in the current result set of the provided + /// and discards them. + /// + /// Reader to flush results from. + public static void FlushResultSet(this SqlDataReader dataReader) + { + while (dataReader.Read()) + { + // Discard results. + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs index 259dc8817a..c4c2ac4d0f 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs @@ -2,7 +2,7 @@ // 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; using System.Runtime.InteropServices; namespace Microsoft.Data.SqlClient.Tests diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlNotificationRequestTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlNotificationRequestTest.cs index eb7d204a95..9a32deefdb 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlNotificationRequestTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlNotificationRequestTest.cs @@ -1,4 +1,8 @@ -using System; +// 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 Microsoft.Data.Sql; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs index b3c51d7fbd..1e3a00931a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs @@ -1,3 +1,7 @@ +// 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.Data; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs index ad9181736f..9929db55d8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs @@ -1,5 +1,4 @@ - -// Licensed to the .NET Foundation under one or more agreements. +// 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; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestTrustedMasterKeyPaths.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestTrustedMasterKeyPaths.cs index 4012f00914..5a4d3579be 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestTrustedMasterKeyPaths.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestTrustedMasterKeyPaths.cs @@ -1,4 +1,8 @@ -using System; +// 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.Security.Cryptography.X509Certificates; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 7b1fb3bc30..b3daa8dea1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -292,51 +292,38 @@ private static Task AcquireTokenAsync(string authorityURL, string userID #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. + /// 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. + /// + /// Thrown if any intermediate step of getting to the test name fails or is inaccessible. + /// + /// Output helper instance for the currently running test + /// 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); - + // Reflect our way to the ITestMethod. + Type type = outputHelper.GetType(); + + FieldInfo testField = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new Exception("Could not find field 'test' on ITestOutputHelper"); + + ITest test = testField.GetValue(outputHelper) as ITest + ?? throw new Exception("Field 'test' on outputHelper is null or not an ITest object."); + + ITestMethod testMethod = test.TestCase.TestMethod; + + // Class name will be fully-qualified. We only want the class name, so take the last part. + string[] testClassNameParts = testMethod.TestClass.Class.Name.Split('.'); + string testClassName = testClassNameParts[testClassNameParts.Length - 1]; + + string testMethodName = testMethod.Method.Name; + + // Reconstitute the test name as classname.methodname + return $"{testClassName}.{testMethodName}"; + } + /// /// SQL Server properties we can query. /// @@ -1295,191 +1282,6 @@ protected virtual void OnMatchingEventWritten(EventWrittenEventArgs eventData) #nullable enable - public readonly ref struct 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 - } - /// /// Resolves the machine's fully qualified domain name if it is applicable. /// diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/XEventScope.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/XEventScope.cs new file mode 100644 index 0000000000..ce697c9147 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/XEventScope.cs @@ -0,0 +1,208 @@ +// 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.Data; +using System.Threading; +using System.Xml; + +#nullable enable + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public readonly ref struct 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; + + /// + /// Duration for the XEvent session, in minutes. + /// + private readonly ushort _durationInMinutes; + + /// + /// 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; + + #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, + SqlConnection connection, + string eventSpecification, + string targetSpecification, + ushort durationInMinutes = 5) + { + SessionName = DataTestUtility.GenerateRandomCharacters(sessionName); + + _connection = connection; + if (connection.State is not ConnectionState.Open) + { + throw new InvalidOperationException("Connection is not open"); + } + + _durationInMinutes = durationInMinutes; + + // EngineEdition 5 indicates Azure SQL. + _isAzureSql = DataTestUtility.GetSqlServerProperty(connection, DataTestUtility.ServerProperty.EngineEdition) == "5"; + if (!_isAzureSql) + { + // Determine if we're connected to a SQL Server instance version 17 or higher. + string majorVersionString = DataTestUtility.GetSqlServerProperty( + connection, + DataTestUtility.ServerProperty.ProductMajorVersion); + int majorVersionInt = int.Parse(majorVersionString); + + _isVersion17OrHigher = majorVersionInt >= 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(xEventCreateAndStartCommandText, _connection); + createXEventSession.ExecuteNonQuery(); + } + + #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 Public Methods + + /// + /// 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() + { + // We choose the sys.(database|server)_event_sessions views here to ensure we find + // sessions that may not be running. + string dropXEventSessionCommand = _isAzureSql + ? $"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(); + } + + /// + /// 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. + /// + /// Thrown if the query did not return a string result. + public 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 + ?? throw new Exception("Command did not return a string result"); + + XmlDocument xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(targetData); + + return xmlDocument; + } + + #endregion + } +} 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 6bc05a443e..dc454c2884 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 @@ -278,6 +278,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs index 027acfde23..54900a4dd0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AADFedAuthTokenRefreshTest/AADFedAuthTokenRefreshTest.cs @@ -2,7 +2,7 @@ // 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; using System.Diagnostics; using Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.Common.SystemDataInternals; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs index 0ae12be917..9dd87d8f21 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs @@ -1,129 +1,83 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; +// 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.Text; -using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class AsyncCancelledConnectionsTest { - private readonly ITestOutputHelper _output; - - private const int NumberOfTasks = 100; // How many attempts to poison the connection pool we will try + /// + /// How many attempts to poison the connection pool we will try. + /// + private const int NumberOfTasks = 100; - private const int NumberOfNonPoisoned = 10; // Number of normal requests for each attempt + /// + /// Number of normal requests for each attempt + /// + private const int NumberOfNonPoisoned = 10; - public AsyncCancelledConnectionsTest(ITestOutputHelper output) - { - _output = output; - } + private bool _continue = true; + private Random _random; // Disabled on Azure since this test fails on concurrent runs on same database. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] - public void CancelAsyncConnections() + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] + [InlineData(true)] + [InlineData(false)] + public async Task CancelAsyncConnections(bool useMars) { - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); - builder.MultipleActiveResultSets = false; - RunCancelAsyncConnections(builder); - builder.MultipleActiveResultSets = true; - RunCancelAsyncConnections(builder); - } + // Arrange + SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString); + builder.MultipleActiveResultSets = useMars; - private void RunCancelAsyncConnections(SqlConnectionStringBuilder connectionStringBuilder) - { SqlConnection.ClearAllPools(); - - ParallelLoopResult results = new ParallelLoopResult(); - ConcurrentDictionary tracker = new ConcurrentDictionary(); - _random = new Random(4); // chosen via fair dice roll. - _watch = Stopwatch.StartNew(); + _random = new Random(4); - try - { - // Setup a timer so that we can see what is going on while our tasks run - using (new Timer(TimerCallback, state: null, dueTime: TimeSpan.FromSeconds(5), period: TimeSpan.FromSeconds(5))) - { - results = Parallel.For( - fromInclusive: 0, - toExclusive: NumberOfTasks, - (int i) => DoManyAsync(i, tracker, connectionStringBuilder).GetAwaiter().GetResult()); - } - } - catch (Exception ex) - { - _output.WriteLine(ex.ToString()); - } - while (!results.IsCompleted) - { - Thread.Sleep(50); - } - DisplaySummary(); - foreach (var detail in _exceptionDetails) + // Act + Task[] tasks = new Task[NumberOfTasks]; + for (int i = 0; i < tasks.Length; i++) { - _output.WriteLine(detail); + tasks[i] = DoManyAsync(builder); } - Assert.Empty(_exceptionDetails); - } - // Display one row every 5'ish seconds - private void TimerCallback(object state) - { - lock (_lockObject) - { - DisplaySummary(); - } - } + await Task.WhenAll(tasks); - private void DisplaySummary() - { - int count; - lock (_exceptionDetails) - { - count = _exceptionDetails.Count; - } - _output.WriteLine($"{_watch.Elapsed} {_continue} Started:{_start} Done:{_done} InFlight:{_inFlight} RowsRead:{_rowsRead} ResultRead:{_resultRead} PoisonedEnded:{_poisonedEnded} nonPoisonedExceptions:{_nonPoisonedExceptions} PoisonedCleanupExceptions:{_poisonCleanUpExceptions} Count:{count} Found:{_found}"); + // Assert - If test runs to completion, it is successful } - // This is the the main body that our Tasks run - private async Task DoManyAsync(int index, ConcurrentDictionary tracker, SqlConnectionStringBuilder connectionStringBuilder) + // This is the main body that our Tasks run + private async Task DoManyAsync(SqlConnectionStringBuilder connectionStringBuilder) { - Interlocked.Increment(ref _start); - Interlocked.Increment(ref _inFlight); - tracker[index] = true; + string connectionString = connectionStringBuilder.ToString(); - using (SqlConnection marsConnection = new SqlConnection(connectionStringBuilder.ToString())) + using SqlConnection connection = new SqlConnection(connectionString); + if (connectionStringBuilder.MultipleActiveResultSets) { - if (connectionStringBuilder.MultipleActiveResultSets) - { - await marsConnection.OpenAsync(); - } + await connection.OpenAsync(); + } - // First poison - await DoOneAsync(marsConnection, connectionStringBuilder.ToString(), poison: true, index); + // First poison + await DoOneAsync(connection, connectionString, poison: true); - for (int i = 0; i < NumberOfNonPoisoned && _continue; i++) - { - // now run some without poisoning - await DoOneAsync(marsConnection, connectionStringBuilder.ToString(),false,index); - } + for (int i = 0; i < NumberOfNonPoisoned && _continue; i++) + { + // now run some without poisoning + await DoOneAsync(connection, connectionString, poison: false); } - tracker.TryRemove(index, out var _); - Interlocked.Decrement(ref _inFlight); - Interlocked.Increment(ref _done); } - // This will do our work, open a connection, and run a query (that returns 4 results sets) - // if we are poisoning we will - // 1 - Interject some sleeps in the sql statement so that it will run long enough that we can cancel it - // 2 - Setup a time bomb task that will cancel the command a random amount of time later - private async Task DoOneAsync(SqlConnection marsConnection, string connectionString, bool poison, int parent) + private async Task DoOneAsync(SqlConnection marsConnection, string connectionString, bool poison) { + // This will do our work, open a connection, and run a query (that returns 4 results sets) + // if we are poisoning we will + // 1 - Interject some sleeps in the sql statement so that it will run long enough that we can cancel it + // 2 - Set up a time bomb task that will cancel the command a random amount of time later + try { StringBuilder builder = new StringBuilder(); @@ -140,159 +94,108 @@ private async Task DoOneAsync(SqlConnection marsConnection, string connectionStr { if (marsConnection != null && marsConnection.State == System.Data.ConnectionState.Open) { - await RunCommand(marsConnection, builder.ToString(), poison, parent); + await RunCommand(marsConnection, builder.ToString(), poison); } else { await connection.OpenAsync(); - await RunCommand(connection, builder.ToString(), poison, parent); + await RunCommand(connection, builder.ToString(), poison); } } } + catch (Exception ex) when (poison && IsExpectedCancellation(ex)) + { + // Expected cancellation from the time bomb when poisoning. + } catch (Exception ex) { - if (!poison) - { - Interlocked.Increment(ref _nonPoisonedExceptions); - - string details = ex.ToString(); - details = details.Substring(0, Math.Min(200, details.Length)); - lock (_exceptionDetails) - { - _exceptionDetails.Add(details); - } - } - if (ex.Message.Contains("The MARS TDS header contained errors.")) { _continue = false; - if (_found == 0) // This check is not really safe we may list more than one. - { - lock (_lockObject) - { - // You will notice that poison will be likely be false here, it is the normal commands that suffer - // Once we have successfully poisoned the connection pool, we may start to see some other request to poison fail just like the normal requests - _output.WriteLine($"{poison} {DateTime.UtcNow.ToString("O")}"); - _output.WriteLine(ex.ToString()); - } - } - Interlocked.Increment(ref _found); } + + throw; } } - private async Task RunCommand(SqlConnection connection, string commandText, bool poison, int parent) + private static bool IsExpectedCancellation(Exception ex) { - int rowsRead = 0; - int resultRead = 0; + switch (ex) + { + case OperationCanceledException: + return true; + case SqlException sqlEx: + return sqlEx.Message.Contains("operation cancelled", StringComparison.OrdinalIgnoreCase) || + sqlEx.Message.Contains("operation canceled", StringComparison.OrdinalIgnoreCase); + default: + return false; + } + } + private async Task RunCommand(SqlConnection connection, string commandText, bool poison) + { + using SqlCommand command = connection.CreateCommand(); + command.CommandText = commandText; + + Task timeBombTask = null; try { - using (var command = connection.CreateCommand()) + // Set us up the (time) bomb + if (poison) { - Task timeBombTask = default; - try - { - // Setup our time bomb - if (poison) - { - timeBombTask = TimeBombAsync(command); - } - - command.CommandText = commandText; + timeBombTask = TimeBombAsync(command); + } - // Attempt to read all of the data - using (var reader = await command.ExecuteReaderAsync()) + // Attempt to read all the data + using SqlDataReader reader = await command.ExecuteReaderAsync(); + try + { + do + { + while (await reader.ReadAsync() && _continue) { - try - { - do - { - resultRead++; - while (await reader.ReadAsync() && _continue) - { - rowsRead++; - } - } - while (await reader.NextResultAsync() && _continue); - } - catch (SqlException) when (poison) - { - // This looks a little strange, we failed to read above so this should fail too - // But consider the case where this code is elsewhere (in the Dispose method of a class holding this logic) - try - { - while (await reader.NextResultAsync()) - { - } - } - catch - { - Interlocked.Increment(ref _poisonCleanUpExceptions); - } - - throw; - } - catch (Exception ex) - { - Assert.Fail("unexpected exception: " + ex.GetType().Name + " " +ex.Message); - } + // Discard results } } - finally + while (await reader.NextResultAsync() && _continue); + } + catch (SqlException) when (poison) + { + // This looks a little strange, we failed to read above so this should + // fail too. But consider the case where this code is elsewhere (in the + // Dispose method of a class holding this logic) + while (await reader.NextResultAsync()) { - // Make sure to clean up our time bomb - // It is unlikely, but the timebomb may get delayed in the Task Queue - // And we don't want it running after we dispose the command - if (timeBombTask != default) - { - await timeBombTask; - } + // Discard all results } + + throw; } } finally { - Interlocked.Add(ref _rowsRead, rowsRead); - Interlocked.Add(ref _resultRead, resultRead); - if (poison) + // Make sure to clean up our time bomb + // It is unlikely, but the timebomb may get delayed in the task queue, and we don't + // want it running after we dispose the command. + if (timeBombTask != null) { - Interlocked.Increment(ref _poisonedEnded); + await timeBombTask; } } } private async Task TimeBombAsync(SqlCommand command) { - await SleepAsync(100, 3000); - command.Cancel(); - } - - private async Task SleepAsync(int minMs, int maxMs) - { + // Sleep a random amount between 100 and 3000 ms. int delayMs; lock (_random) { - delayMs = _random.Next(minMs, maxMs); + delayMs = _random.Next(100, 3000); } await Task.Delay(delayMs); + + // Cancel the command + command.Cancel(); } - - private Stopwatch _watch; - - private int _inFlight; - private int _start; - private int _done; - private int _rowsRead; - private int _resultRead; - private int _nonPoisonedExceptions; - private int _poisonedEnded; - private int _poisonCleanUpExceptions; - private bool _continue = true; - private int _found; - private Random _random; - private object _lockObject = new object(); - - private HashSet _exceptionDetails = new HashSet(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/FedAuthTokenHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/FedAuthTokenHelper.cs index a0a3bc4498..6e925d1809 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/FedAuthTokenHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/FedAuthTokenHelper.cs @@ -2,7 +2,7 @@ // 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; using System.Collections; using System.Linq; using System.Reflection; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/TransactionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/TransactionPoolTest.cs index d510b03953..5a6051625d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/TransactionPoolTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/TransactionPoolTest.cs @@ -1,4 +1,8 @@ -using System.Transactions; +// 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.Transactions; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests 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 b4b7271216..74423f9f13 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs @@ -1959,12 +1959,11 @@ private void TestXEventsStreaming(string connectionString) using SqlConnection xEventManagementConnection = new SqlConnection(connectionString); xEventManagementConnection.Open(); - using DataTestUtility.XEventScope xEventScope = - new DataTestUtility.XEventScope( - _testName, - xEventManagementConnection, - "ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence))", - "ADD TARGET package0.ring_buffer"); + using XEventScope xEventScope = new( + _testName, + xEventManagementConnection, + "ADD EVENT sqlserver.user_event(ACTION(package0.event_sequence))", + "ADD TARGET package0.ring_buffer"); string sessionName = xEventScope.SessionName; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs index d722ceb4b3..03c5f794c0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs @@ -1,4 +1,8 @@ -using System; +// 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.Data; using System.IO; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs index 19a0559e1b..f580347f67 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonStreamTest.cs @@ -1,4 +1,8 @@ -using System; +// 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.IO; using System.Data; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs index 21ff771ac0..1176447204 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCompletedTest.cs @@ -1,4 +1,8 @@ -using System.Data; +// 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.Data; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs index 7f28a4a09a..b694bc6aee 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandSetTest.cs @@ -1,4 +1,8 @@ -using System; +// 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.Data.SqlTypes; using System.Reflection; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs index 211e9c0d38..1c26151d94 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandStoredProcTest.cs @@ -2,7 +2,7 @@ // 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; using System.Data; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs index bcb23d11bd..ffd6c8ff79 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/XEventsTracingTest.cs @@ -40,7 +40,7 @@ public void XEventActivityIDConsistentWithTracing(string query, System.Data.Comm using SqlConnection xEventManagementConnection = new(DataTestUtility.TCPConnectionString); xEventManagementConnection.Open(); - using DataTestUtility.XEventScope xEventSession = new( + using XEventScope xEventSession = new( _testName, xEventManagementConnection, $@"ADD EVENT SQL_STATEMENT_STARTING (ACTION (client_connection_id) WHERE (client_connection_id='{connectionId}')), diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ADPHelper.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ADPHelper.cs index d78c36f785..85ad1d7d37 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ADPHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ADPHelper.cs @@ -1,4 +1,8 @@ -using System; +// 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.Linq; using System.Text; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs index eb924990db..b271b0c52e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/InternalsVisibleToTest.cs @@ -1,4 +1,8 @@ -using System; +// 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 Microsoft.Data.SqlClient.ConnectionPool; using Xunit; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/NativeAeadBaseline.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/NativeAeadBaseline.cs index 9c5c67de32..945351056f 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/NativeAeadBaseline.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/NativeAeadBaseline.cs @@ -1,4 +1,8 @@ -using System; +// 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.Linq; using System.Text; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs index 14bc51d520..eeb6bff3db 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs @@ -1,4 +1,8 @@ -using System; +// 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 Microsoft.Data.SqlClient.Tests.Common; using Xunit; using static Microsoft.Data.SqlClient.Tests.Common.LocalAppContextSwitchesHelper; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs index 86c0f3ef6c..adca0e2b99 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs @@ -1,5 +1,4 @@ - -// Licensed to the .NET Foundation under one or more agreements. +// 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.