From 848241d28b5a0e152e490d1c79e42c815df27827 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 3 Nov 2024 09:44:49 +0000 Subject: [PATCH 1/6] Converted Threads to long-running Tasks The key advantage is that exceptions propagate properly. If a thread throws an exception (as a result of a failed test assertion, or otherwise) then the test host crashes and must be restarted. --- .../ManualTests/AlwaysEncrypted/ApiShould.cs | 63 +++++++++++-------- .../SQL/ConnectivityTests/ConnectivityTest.cs | 7 ++- .../ConnectionOnMirroringTest.cs | 8 +-- .../SQL/ParameterTest/ParametersTest.cs | 17 ++--- .../SQL/RandomStressTest/RandomStressTest.cs | 4 +- 5 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index f6e203c8b7..c1a3284dcb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -1983,29 +1983,34 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, true); - Thread[] threads = new Thread[2]; + Task[] tasks = new Task[2]; // Invoke ExecuteReader or ExecuteNonQuery in another thread. + // Use long-running tasks to create the thread. This enables any failed assertions to propagate, rather than + // allowing the exception to kill the thread and the process. if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); } - - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + tasks[0].Wait(); + tasks[1].Wait(); CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, false); @@ -2033,26 +2038,30 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, true); // Invoke ExecuteReader or ExecuteNonQuery in another thread. - threads = new Thread[2]; + tasks = new Task[2]; if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); } - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + tasks[0].Wait(); + tasks[1].Wait(); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, false); @@ -2079,26 +2088,30 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, true); - threads = new Thread[2]; + tasks = new Task[2]; if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); } - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + tasks[0].Wait(); + tasks[1].Wait(); CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, false); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index da09f3406c..32e6af8a7a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -7,6 +7,7 @@ using System.Data; using System.Diagnostics; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -170,7 +171,7 @@ public class ConnectionWorker : IDisposable private static List s_workerList = new(); private ManualResetEventSlim _doneEvent = new(false); private double _timeElapsed; - private Thread _thread; + private Task _task; private string _connectionString; private int _numOfTry; @@ -179,7 +180,7 @@ public ConnectionWorker(string connectionString, int numOfTry) s_workerList.Add(this); _connectionString = connectionString; _numOfTry = numOfTry; - _thread = new Thread(new ThreadStart(SqlConnectionOpen)); + _task = new Task(SqlConnectionOpen, TaskCreationOptions.LongRunning); } public static List WorkerList => s_workerList; @@ -190,7 +191,7 @@ public static void Start() { foreach (ConnectionWorker w in s_workerList) { - w._thread.Start(); + w._task.Start(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs index 93101d5d38..460d566a6d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Data; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -28,17 +29,14 @@ public static void TestMultipleConnectionToMirroredServer() builder.ConnectTimeout = 0; TestWorker worker = new TestWorker(builder.ConnectionString); - Thread childThread = new Thread(() => worker.TestMultipleConnection()); - childThread.Start(); + Task childTask = Task.Factory.StartNew(() => worker.TestMultipleConnection(), TaskCreationOptions.LongRunning); if (workerCompletedEvent.WaitOne(10000)) { - childThread.Join(); + childTask.Wait(); } else { - // currently Thread.Abort() throws PlatformNotSupportedException in CoreFx. - childThread.Interrupt(); throw new Exception("SqlConnection could not open and close successfully in timely manner. Possibly connection hangs."); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs index f4ec851fec..05ce2d62d8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -9,6 +9,8 @@ using System.Data.SqlTypes; using System.Threading; using Xunit; +using System.Threading.Tasks; + #if NET6_0_OR_GREATER using Microsoft.SqlServer.Types; using Microsoft.Data.SqlClient.Server; @@ -886,10 +888,10 @@ private static void EnableOptimizedParameterBinding_ReturnSucceeds() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static void ClosedConnection_SqlParameterValueTest() { - var threads = new List(); + var tasks = new List(); for (int i = 0; i < 100; i++) { - var t = new Thread(() => + var t = Task.Factory.StartNew(() => { for (int j = 0; j < 1000; j++) { @@ -902,13 +904,12 @@ public static void ClosedConnection_SqlParameterValueTest() Assert.Fail($"Unexpected exception occurred: {e.Message}"); } } - }); - t.Start(); - threads.Add(t); + }, TaskCreationOptions.LongRunning); + tasks.Add(t); } - for (int i = 0; i < threads.Count; i++) + for (int i = 0; i < tasks.Count; i++) { - threads[i].Join(); + tasks[i].Wait(); } } @@ -927,7 +928,7 @@ private static void RunParameterTest() cm.Parameters.Add(new SqlParameter("@id2", SqlDbType.UniqueIdentifier) { Direction = ParameterDirection.Output }); try { - System.Threading.Tasks.Task task = cm.ExecuteNonQueryAsync(cancellationToken.Token); + Task task = cm.ExecuteNonQueryAsync(cancellationToken.Token); task.Wait(); } catch (Exception) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs index 86b5438a1a..79bf05a7f8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Text; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -70,8 +71,7 @@ public void TestMain() { for (int tcount = 0; tcount < ThreadCountDefault; tcount++) { - Thread t = new Thread(TestThread); - t.Start(); + _ = Task.Factory.StartNew(TestThread, TaskCreationOptions.LongRunning); } } From 4bd9b8ac1dccab4d5633493a5efe6020a20d8007 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:41:07 +0000 Subject: [PATCH 2/6] Corrected the instantiation of the cancellation task - missing state parameter. --- .../tests/ManualTests/AlwaysEncrypted/ApiShould.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index c1a3284dcb..dda2818d5b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -2000,7 +2000,9 @@ public void TestSqlCommandCancel(string connection, string value, int number) new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), TaskCreationOptions.LongRunning); } - tasks[1] = new Task(Thread_Cancel, TaskCreationOptions.LongRunning); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); // Start the execute thread. tasks[0].Start(); @@ -2051,7 +2053,9 @@ public void TestSqlCommandCancel(string connection, string value, int number) new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), TaskCreationOptions.LongRunning); } - tasks[1] = new Task(Thread_Cancel, TaskCreationOptions.LongRunning); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); // Start the execute thread. tasks[0].Start(); @@ -2101,7 +2105,9 @@ public void TestSqlCommandCancel(string connection, string value, int number) new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), TaskCreationOptions.LongRunning); } - tasks[1] = new Task(Thread_Cancel, TaskCreationOptions.LongRunning); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + TaskCreationOptions.LongRunning); // Start the execute thread. tasks[0].Start(); From 7c766b44dceee221d5c6b6eef002eb2865649fd7 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sun, 3 Nov 2024 14:36:07 +0000 Subject: [PATCH 3/6] Changes to TestSqlCommandCancel, eliminating timing-specific cancellation behaviour testing. This should also allow the test to run on both netcore and netfx. --- .../ManualTests/AlwaysEncrypted/ApiShould.cs | 161 +++++++++--------- .../TestFixtures/DatabaseHelper.cs | 8 +- 2 files changed, 88 insertions(+), 81 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index dda2818d5b..9439440395 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -1949,19 +1949,15 @@ public void TestBeginAndEndExecuteReaderWithAsyncCallback(string connection, Com } } - [SkipOnTargetFramework(TargetFrameworkMonikers.Netcoreapp)] [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE))] [ClassData(typeof(AEConnectionStringProviderWithExecutionMethod))] - public void TestSqlCommandCancel(string connection, string value, int number) + public void TestSqlCommandCancel(string connection, string value) { CleanUpTable(connection, _tableName); string executeMethod = value; Assert.True(!string.IsNullOrWhiteSpace(executeMethod), @"executeMethod should not be null or empty"); - int numberOfCancelCalls = number; - Assert.True(numberOfCancelCalls >= 0, "numberofCancelCalls should be >=0."); - IList values = GetValues(dataHint: 58); Assert.True(values != null && values.Count >= 3, @"values should not be null and count should be >= 3."); @@ -1984,6 +1980,8 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, true); Task[] tasks = new Task[2]; + ManualResetEventSlim entryLock = new ManualResetEventSlim(false); + ManualResetEventSlim exitLock = new ManualResetEventSlim(false); // Invoke ExecuteReader or ExecuteNonQuery in another thread. // Use long-running tasks to create the thread. This enables any failed assertions to propagate, rather than @@ -1991,17 +1989,17 @@ public void TestSqlCommandCancel(string connection, string value, int number) if (executeMethod == @"ExecuteReader") { tasks[0] = new Task(Thread_ExecuteReader, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); } else { tasks[0] = new Task(Thread_ExecuteNonQuery, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); } tasks[1] = new Task(Thread_Cancel, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); // Start the execute thread. @@ -2034,8 +2032,11 @@ public void TestSqlCommandCancel(string connection, string value, int number) Assert.True(rowsAffected == numberOfRows, "Unexpected number of rows affected as returned by EndExecuteReader."); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(exitLock.IsSet); + entryLock.Reset(); + exitLock.Reset(); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, true); @@ -2044,17 +2045,17 @@ public void TestSqlCommandCancel(string connection, string value, int number) if (executeMethod == @"ExecuteReader") { tasks[0] = new Task(Thread_ExecuteReader, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); } else { tasks[0] = new Task(Thread_ExecuteNonQuery, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); } tasks[1] = new Task(Thread_Cancel, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); // Start the execute thread. @@ -2069,8 +2070,11 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, false); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(exitLock.IsSet); + entryLock.Reset(); + exitLock.Reset(); rowsAffected = 0; @@ -2096,17 +2100,17 @@ public void TestSqlCommandCancel(string connection, string value, int number) if (executeMethod == @"ExecuteReader") { tasks[0] = new Task(Thread_ExecuteReader, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); } else { tasks[0] = new Task(Thread_ExecuteNonQuery, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); } tasks[1] = new Task(Thread_Cancel, - new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls), + new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), TaskCreationOptions.LongRunning); // Start the execute thread. @@ -2121,8 +2125,9 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, false); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(exitLock.IsSet); rowsAffected = 0; @@ -3115,47 +3120,74 @@ private void TestCancellationToken(FieldInfo failpoint, SqlCommand sqlCommand, i Assert.True(rowsAffected == numberOfRows, "Unexpected number of rows affected as returned by EndExecuteReader."); } - private void Thread_ExecuteReader(object cancelCommandTestParamsObject) + private void Thread_ExecuteReader(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - string.Format(@"SELECT * FROM {0} WHERE FirstName = @FirstName AND CustomerId = @CustomerId", ((TestCommandCancelParams)cancelCommandTestParamsObject).TableName); - using (SqlDataReader reader = sqlCommand.ExecuteReader()) + try { - while (reader.Read()) - { - Assert.Throws(() => sqlCommand.ExecuteReader()); - } + // Wait for the cancellation thread to open this lock... + cancelCommandTestParamsObject.EntryLock.Wait(); + + Exception ex = Assert.ThrowsAny(() => sqlCommand.ExecuteReader().Dispose()); + Assert.Equal(@"Operation cancelled by user.", ex.Message); + } + finally + { + // ...and unlock the cancellation thread once we finish. + cancelCommandTestParamsObject.ExitLock.Set(); } } - private void Thread_ExecuteNonQuery(object cancelCommandTestParamsObject) + private void Thread_ExecuteNonQuery(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - string.Format(@"UPDATE {0} SET FirstName = @FirstName WHERE FirstName = @FirstName AND CustomerId = @CustomerId", ((TestCommandCancelParams)cancelCommandTestParamsObject).TableName); + try + { + // Wait for the cancellation thread to open this lock... + cancelCommandTestParamsObject.EntryLock.Wait(); - Exception ex = Assert.Throws(() => sqlCommand.ExecuteNonQuery()); - Assert.Equal(@"Operation cancelled by user.", ex.Message); + Exception ex = Assert.ThrowsAny(() => sqlCommand.ExecuteNonQuery()); + Assert.Equal(@"Operation cancelled by user.", ex.Message); + } + finally + { + // ...and unlock the cancellation thread once we finish. + cancelCommandTestParamsObject.ExitLock.Set(); + } } - private void Thread_Cancel(object cancelCommandTestParamsObject) + private void Thread_Cancel(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + int cancellations = 0; + System.Diagnostics.Stopwatch cancellationStart = new System.Diagnostics.Stopwatch(); + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - Thread.Sleep(millisecondsTimeout: 500); + cancellationStart.Start(); + cancelCommandTestParamsObject.EntryLock.Set(); - // Repeatedly cancel. - for (int i = 0; i < ((TestCommandCancelParams)cancelCommandTestParamsObject).NumberofTimesToRunCancel; i++) + // Repeatedly cancel until the other thread signals that it's completed execution + // (or until the command timeout has passed.) + do { sqlCommand.Cancel(); - } + cancellations++; + } while (!cancelCommandTestParamsObject.ExitLock.Wait(0) + && cancellationStart.ElapsedMilliseconds <= sqlCommand.CommandTimeout); + cancellationStart.Stop(); } public void Dispose() @@ -3283,67 +3315,42 @@ internal TestAsyncCallBackStateObject(SqlCommand sqlCommand, int expectedRowsAff internal class TestCommandCancelParams { /// - /// SqlCommand object. - /// - private readonly object _sqlCommand; - - /// - /// Name of the test/works as table name - /// - private readonly string _tableName; - - /// - /// number of times to run cancel. + /// Return the SqlCommand object. /// - private readonly int _numberofCancelCommands; + public SqlCommand SqlCommand { get; } /// - /// Return the SqlCommand object. + /// Return the table name (usually equal to the name of the test.) /// - public object SqlCommand - { - get - { - return _sqlCommand; - } - } + public string TableName { get; } /// - /// Return the tablename. + /// Lock set by the thread cancelling the SqlCommand. /// - public object TableName - { - get - { - return _tableName; - } - } + public ManualResetEventSlim EntryLock { get; } /// - /// Return the number of times to run cancel. + /// Lock set by the thread executing the SqlCommand. /// - public int NumberofTimesToRunCancel - { - get - { - return _numberofCancelCommands; - } - } + public ManualResetEventSlim ExitLock { get; } /// /// Constructor. /// /// /// - /// - public TestCommandCancelParams(object sqlCommand, string tableName, int numberofTimesToCancel) + /// + /// + /// + public TestCommandCancelParams(SqlCommand sqlCommand, string tableName, ManualResetEventSlim entryLock, ManualResetEventSlim exitLock) { Assert.True(sqlCommand != null, "sqlCommand should not be null."); Assert.True(!string.IsNullOrWhiteSpace(tableName), "tableName should not be null or empty."); - _sqlCommand = sqlCommand; - _tableName = tableName; - _numberofCancelCommands = numberofTimesToCancel; + SqlCommand = sqlCommand; + TableName = tableName; + EntryLock = entryLock; + ExitLock = exitLock; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs index 459573606e..94aa84e664 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs @@ -299,10 +299,10 @@ public IEnumerator GetEnumerator() { foreach (string connStrAE in DataTestUtility.AEConnStrings) { - yield return new object[] { connStrAE, @"ExecuteReader", 1 }; - yield return new object[] { connStrAE, @"ExecuteReader", 3 }; - yield return new object[] { connStrAE, @"ExecuteNonQuery", 1 }; - yield return new object[] { connStrAE, @"ExecuteNonQuery", 3 }; + yield return new object[] { connStrAE, @"ExecuteReader" }; + yield return new object[] { connStrAE, @"ExecuteReader" }; + yield return new object[] { connStrAE, @"ExecuteNonQuery" }; + yield return new object[] { connStrAE, @"ExecuteNonQuery" }; } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); From ad1a572996105f77ada31e4615489c4ea6696747 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:59:11 +0000 Subject: [PATCH 4/6] Responding to code review. * Removed two unnecessary iterations from DatabaseHelper. * Added explanatory comments to ApiShould. * Switched to using Task.WaitAll rather than waiting for each Task in sequence. --- .../ManualTests/AlwaysEncrypted/ApiShould.cs | 96 +++++++++++-------- .../TestFixtures/DatabaseHelper.cs | 2 - .../SQL/ParameterTest/ParametersTest.cs | 20 +--- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 9439440395..b132ff0d05 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -1980,26 +1980,39 @@ public void TestSqlCommandCancel(string connection, string value) CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, true); Task[] tasks = new Task[2]; - ManualResetEventSlim entryLock = new ManualResetEventSlim(false); - ManualResetEventSlim exitLock = new ManualResetEventSlim(false); - - // Invoke ExecuteReader or ExecuteNonQuery in another thread. - // Use long-running tasks to create the thread. This enables any failed assertions to propagate, rather than - // allowing the exception to kill the thread and the process. + ManualResetEventSlim startWorkloadSignal = new ManualResetEventSlim(false); + ManualResetEventSlim workloadCompleteSignal = new ManualResetEventSlim(false); + + /* + * Invoke ExecuteReader or ExecuteNonQuery in another thread. + * Use long-running tasks to create the thread. This enables any failed assertions to propagate, rather than + * allowing the exception to kill the thread and the process. + * These threads should progress in the sequence below: + * + * Workload Thread | Cancel Thread + * ------------------------------------ | ------------- + * Start thread | Start thread + * Wait for signal | - + * - | Set signal for workload start + * Start workload execution | Loop, cancelling workload until workload complete signal set + * Throw cancellation exception | - + * Set signal for workload complete | - + * End thread | Finish loop and end thread + */ if (executeMethod == @"ExecuteReader") { tasks[0] = new Task(Thread_ExecuteReader, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); } else { tasks[0] = new Task(Thread_ExecuteNonQuery, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); } tasks[1] = new Task(Thread_Cancel, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); // Start the execute thread. @@ -2009,8 +2022,7 @@ public void TestSqlCommandCancel(string connection, string value) tasks[1].Start(); // Wait for the threads to finish. - tasks[0].Wait(); - tasks[1].Wait(); + Task.WaitAll(tasks); CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, false); @@ -2034,9 +2046,9 @@ public void TestSqlCommandCancel(string connection, string value) // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); - Assert.True(exitLock.IsSet); - entryLock.Reset(); - exitLock.Reset(); + Assert.True(workloadCompleteSignal.IsSet); + startWorkloadSignal.Reset(); + workloadCompleteSignal.Reset(); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, true); @@ -2045,17 +2057,17 @@ public void TestSqlCommandCancel(string connection, string value) if (executeMethod == @"ExecuteReader") { tasks[0] = new Task(Thread_ExecuteReader, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); } else { tasks[0] = new Task(Thread_ExecuteNonQuery, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); } tasks[1] = new Task(Thread_Cancel, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); // Start the execute thread. @@ -2065,16 +2077,15 @@ public void TestSqlCommandCancel(string connection, string value) tasks[1].Start(); // Wait for the threads to finish. - tasks[0].Wait(); - tasks[1].Wait(); + Task.WaitAll(tasks); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, false); // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); - Assert.True(exitLock.IsSet); - entryLock.Reset(); - exitLock.Reset(); + Assert.True(workloadCompleteSignal.IsSet); + startWorkloadSignal.Reset(); + workloadCompleteSignal.Reset(); rowsAffected = 0; @@ -2100,17 +2111,17 @@ public void TestSqlCommandCancel(string connection, string value) if (executeMethod == @"ExecuteReader") { tasks[0] = new Task(Thread_ExecuteReader, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); } else { tasks[0] = new Task(Thread_ExecuteNonQuery, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); } tasks[1] = new Task(Thread_Cancel, - new TestCommandCancelParams(sqlCommand, _tableName, entryLock, exitLock), + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), TaskCreationOptions.LongRunning); // Start the execute thread. @@ -2120,14 +2131,13 @@ public void TestSqlCommandCancel(string connection, string value) tasks[1].Start(); // Wait for the threads to finish. - tasks[0].Wait(); - tasks[1].Wait(); + Task.WaitAll(tasks); CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, false); // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); - Assert.True(exitLock.IsSet); + Assert.True(workloadCompleteSignal.IsSet); rowsAffected = 0; @@ -3124,6 +3134,7 @@ private void Thread_ExecuteReader(object state) { TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + SqlDataReader reader = null; Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); Assert.True(sqlCommand != null, "sqlCommand should not be null."); @@ -3131,15 +3142,16 @@ private void Thread_ExecuteReader(object state) try { // Wait for the cancellation thread to open this lock... - cancelCommandTestParamsObject.EntryLock.Wait(); + cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); - Exception ex = Assert.ThrowsAny(() => sqlCommand.ExecuteReader().Dispose()); + Exception ex = Assert.ThrowsAny(() => (reader = sqlCommand.ExecuteReader()).Read()); Assert.Equal(@"Operation cancelled by user.", ex.Message); } finally { + reader?.Dispose(); // ...and unlock the cancellation thread once we finish. - cancelCommandTestParamsObject.ExitLock.Set(); + cancelCommandTestParamsObject.WorkloadCompleteSignal.Set(); } } @@ -3154,7 +3166,7 @@ private void Thread_ExecuteNonQuery(object state) try { // Wait for the cancellation thread to open this lock... - cancelCommandTestParamsObject.EntryLock.Wait(); + cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); Exception ex = Assert.ThrowsAny(() => sqlCommand.ExecuteNonQuery()); Assert.Equal(@"Operation cancelled by user.", ex.Message); @@ -3162,7 +3174,7 @@ private void Thread_ExecuteNonQuery(object state) finally { // ...and unlock the cancellation thread once we finish. - cancelCommandTestParamsObject.ExitLock.Set(); + cancelCommandTestParamsObject.WorkloadCompleteSignal.Set(); } } @@ -3177,7 +3189,7 @@ private void Thread_Cancel(object state) Assert.True(sqlCommand != null, "sqlCommand should not be null."); cancellationStart.Start(); - cancelCommandTestParamsObject.EntryLock.Set(); + cancelCommandTestParamsObject.StartWorkloadSignal.Set(); // Repeatedly cancel until the other thread signals that it's completed execution // (or until the command timeout has passed.) @@ -3185,7 +3197,7 @@ private void Thread_Cancel(object state) { sqlCommand.Cancel(); cancellations++; - } while (!cancelCommandTestParamsObject.ExitLock.Wait(0) + } while (!cancelCommandTestParamsObject.WorkloadCompleteSignal.Wait(0) && cancellationStart.ElapsedMilliseconds <= sqlCommand.CommandTimeout); cancellationStart.Stop(); } @@ -3327,12 +3339,12 @@ internal class TestCommandCancelParams /// /// Lock set by the thread cancelling the SqlCommand. /// - public ManualResetEventSlim EntryLock { get; } + public ManualResetEventSlim StartWorkloadSignal { get; } /// /// Lock set by the thread executing the SqlCommand. /// - public ManualResetEventSlim ExitLock { get; } + public ManualResetEventSlim WorkloadCompleteSignal { get; } /// /// Constructor. @@ -3340,17 +3352,17 @@ internal class TestCommandCancelParams /// /// /// - /// - /// - public TestCommandCancelParams(SqlCommand sqlCommand, string tableName, ManualResetEventSlim entryLock, ManualResetEventSlim exitLock) + /// + /// + public TestCommandCancelParams(SqlCommand sqlCommand, string tableName, ManualResetEventSlim startWorkloadSignal, ManualResetEventSlim workloadCompleteSignal) { Assert.True(sqlCommand != null, "sqlCommand should not be null."); Assert.True(!string.IsNullOrWhiteSpace(tableName), "tableName should not be null or empty."); SqlCommand = sqlCommand; TableName = tableName; - EntryLock = entryLock; - ExitLock = exitLock; + StartWorkloadSignal = startWorkloadSignal; + WorkloadCompleteSignal = workloadCompleteSignal; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs index 94aa84e664..60717476fb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs @@ -300,8 +300,6 @@ public IEnumerator GetEnumerator() foreach (string connStrAE in DataTestUtility.AEConnStrings) { yield return new object[] { connStrAE, @"ExecuteReader" }; - yield return new object[] { connStrAE, @"ExecuteReader" }; - yield return new object[] { connStrAE, @"ExecuteNonQuery" }; yield return new object[] { connStrAE, @"ExecuteNonQuery" }; } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs index 05ce2d62d8..9342a840eb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -888,29 +888,19 @@ private static void EnableOptimizedParameterBinding_ReturnSucceeds() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static void ClosedConnection_SqlParameterValueTest() { - var tasks = new List(); - for (int i = 0; i < 100; i++) + var tasks = new Task[100]; + for (int i = 0; i < tasks.Length; i++) { var t = Task.Factory.StartNew(() => { for (int j = 0; j < 1000; j++) { - try - { - RunParameterTest(); - } - catch (Exception e) - { - Assert.Fail($"Unexpected exception occurred: {e.Message}"); - } + RunParameterTest(); } }, TaskCreationOptions.LongRunning); - tasks.Add(t); - } - for (int i = 0; i < tasks.Count; i++) - { - tasks[i].Wait(); + tasks[i] = t; } + Task.WaitAll(tasks); } private static void RunParameterTest() From db4945ea8bafff680cdab97c3c47c485fad44ab3 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:30:59 +0000 Subject: [PATCH 5/6] Improve cancellation detection Cancellation can trigger one of several different errors, resulting in a flakier test. Also ensure that the query always takes more than 150ms, ensuring that a quick query execution doesn't cause the test to fail. Finally, make sure that we try to read everything from the SqlDataReader. --- .../ManualTests/AlwaysEncrypted/ApiShould.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index b132ff0d05..f360a28671 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -45,6 +45,14 @@ public sealed class ApiShould : IClassFixture, IDis "'MSSQL_CERTIFICATE_STORE', 'MSSQL_CNG_STORE', 'MSSQL_CSP_PROVIDER'", $"'{NotRequiredProviderName}'"); + private HashSet _cancellationExceptionMessages = new HashSet() + { + "A severe error occurred on the current command. The results, if any, should be discarded.\r\nOperation cancelled by user.", + "A severe error occurred on the current command. The results, if any, should be discarded.", + "Operation cancelled by user.", + "The request failed to run because the batch is aborted, this can be caused by abort signal sent from client, or another request is running in the same session, which makes the session busy.\r\nOperation cancelled by user." + }; + public ApiShould(PlatformSpecificTestContext context) { _fixture = context.Fixture; @@ -1969,7 +1977,10 @@ public void TestSqlCommandCancel(string connection, string value) { sqlConnection.Open(); - using (SqlCommand sqlCommand = new SqlCommand($@"SELECT * FROM [{_tableName}] WHERE FirstName = @FirstName AND CustomerId = @CustomerId", + // WAITFOR DELAY is present to ensure that the command is always executing by the time the cancellation occurs. + // Without this, a command which has completed by the time the cancellation occurs will fail the test. It's last + // in the command to ensure that cancellation is tested while row data or metadata is flowing. + using (SqlCommand sqlCommand = new SqlCommand($@"SELECT * FROM [{_tableName}] WHERE FirstName = @FirstName AND CustomerId = @CustomerId; WAITFOR DELAY '00:00:00.150'", sqlConnection, transaction: null, columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) @@ -3144,8 +3155,13 @@ private void Thread_ExecuteReader(object state) // Wait for the cancellation thread to open this lock... cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); - Exception ex = Assert.ThrowsAny(() => (reader = sqlCommand.ExecuteReader()).Read()); - Assert.Equal(@"Operation cancelled by user.", ex.Message); + Exception ex = Assert.ThrowsAny(() => + { + reader = sqlCommand.ExecuteReader(); + while (reader.Read()) + { } + }); + Assert.Contains(ex.Message, _cancellationExceptionMessages); } finally { @@ -3169,7 +3185,7 @@ private void Thread_ExecuteNonQuery(object state) cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); Exception ex = Assert.ThrowsAny(() => sqlCommand.ExecuteNonQuery()); - Assert.Equal(@"Operation cancelled by user.", ex.Message); + Assert.Contains(ex.Message, _cancellationExceptionMessages); } finally { From 821ea46a264db98a1ddb92afd35b008129b51f59 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 15 Nov 2024 21:36:20 +0000 Subject: [PATCH 6/6] Correcting previous merge --- .../tests/ManualTests/SQL/ParameterTest/ParametersTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs index ab91a84112..0ac8918570 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -8,6 +8,7 @@ using System.Data; using System.Data.SqlTypes; using System.Threading; +using System.Threading.Tasks; using Xunit; #if !NETFRAMEWORK using Microsoft.SqlServer.Types;