Skip to content

Commit a8b4a8b

Browse files
authored
Tests | Msc Test Improvements/Cleanup (#3842)
* Split async cancelled connection test into mars and no-mars tests * Small improvements to AsyncCancelledConnectionsTest to run async and fail fast on any unexpected exception * Introduce extensions for SqlDataReader [MOVE] * Split XEventScope into separate file * Rewrite mechaniism for extracting test name from ITest * Patch up XEventTracingTest * Address copilot comments, strip out a lot of useless cruft in the async cancellation tests * Make sure files have license headers (and don't have ZWNBSP)
1 parent 1c450c0 commit a8b4a8b

27 files changed

Lines changed: 461 additions & 457 deletions

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ISqlVector.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
15
namespace Microsoft.Data.SqlClient
26
{
37
/// <summary>

src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
15
using System;
26
using System.Collections.Generic;
37
using System.Reflection;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.Data.SqlClient.Tests.Common
6+
{
7+
/// <summary>
8+
/// Extensions on the <see cref="SqlDataReader"/> class.
9+
/// </summary>
10+
public static class SqlDataReaderExtensions
11+
{
12+
/// <summary>
13+
/// Reads all result sets in the provided <paramref name="dataReader"/> and discards them.
14+
/// </summary>
15+
/// <param name="dataReader">Reader to flush results from.</param>
16+
public static void FlushAllResults(this SqlDataReader dataReader)
17+
{
18+
do
19+
{
20+
dataReader.FlushResultSet();
21+
} while (dataReader.NextResult());
22+
}
23+
24+
/// <summary>
25+
/// Reads all results in the current result set of the provided <paramref name="dataReader"/>
26+
/// and discards them.
27+
/// </summary>
28+
/// <param name="dataReader">Reader to flush results from.</param>
29+
public static void FlushResultSet(this SqlDataReader dataReader)
30+
{
31+
while (dataReader.Read())
32+
{
33+
// Discard results.
34+
}
35+
}
36+
}
37+
}

src/Microsoft.Data.SqlClient/tests/FunctionalTests/DataCommon/TestUtility.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System;
5+
using System;
66
using System.Runtime.InteropServices;
77

88
namespace Microsoft.Data.SqlClient.Tests

src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlNotificationRequestTest.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using System;
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
26
using Microsoft.Data.Sql;
37
using Xunit;
48

src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
15
using System;
26
using System.Data;
37
using Xunit;

src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-

2-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
32
// The .NET Foundation licenses this file to you under the MIT license.
43
// See the LICENSE file in the project root for more information.using System;
54

src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestTrustedMasterKeyPaths.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using System;
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
26
using System.Collections.Generic;
37
using System.Security.Cryptography.X509Certificates;
48
using Xunit;

src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs

Lines changed: 29 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -292,51 +292,38 @@ private static Task<string> AcquireTokenAsync(string authorityURL, string userID
292292
#nullable enable
293293

294294
/// <summary>
295-
/// Returns the current test name as:
296-
///
297-
/// ClassName.MethodName
298-
///
299-
/// xUnit v2 doesn't provide access to a test context, so we use
300-
/// reflection into the ITestOutputHelper to get the test name.
295+
/// Returns the current test name as: ClassName.MethodName
296+
/// xUnit v2 doesn't provide access to a test context, so we use reflection into the
297+
/// ITestOutputHelper to get the test name.
301298
/// </summary>
302-
///
303-
/// <param name="outputHelper">
304-
/// The output helper instance for the currently running test.
305-
/// </param>
306-
///
307-
/// <returns>The current test name.</returns>
299+
/// <exception cref="Exception">
300+
/// Thrown if any intermediate step of getting to the test name fails or is inaccessible.
301+
/// </exception>
302+
/// <param name="outputHelper">Output helper instance for the currently running test</param>
303+
/// <returns>Current test name</returns>
308304
public static string CurrentTestName(ITestOutputHelper outputHelper)
309305
{
310-
// Reflect our way to the ITest instance.
311-
var type = outputHelper.GetType();
312-
Assert.NotNull(type);
313-
var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic);
314-
Assert.NotNull(testMember);
315-
var test = testMember.GetValue(outputHelper) as ITest;
316-
Assert.NotNull(test);
317-
318-
// The DisplayName is in the format:
319-
//
320-
// Namespace.ClassName.MethodName(args)
321-
//
322-
// We only want the ClassName.MethodName portion.
323-
//
324-
Match match = TestNameRegex.Match(test.DisplayName);
325-
Assert.True(match.Success);
326-
// There should be 2 groups: the overall match, and the capture
327-
// group.
328-
Assert.Equal(2, match.Groups.Count);
329-
330-
// The portion we want is in the capture group.
331-
return match.Groups[1].Value;
332-
}
333-
334-
private static readonly Regex TestNameRegex = new(
335-
// Capture the ClassName.MethodName portion, which may terminate
336-
// the name, or have (args...) appended.
337-
@"\.((?:[^.]+)\.(?:[^.\(]+))(?:\(.*\))?$",
338-
RegexOptions.Compiled);
339-
306+
// Reflect our way to the ITestMethod.
307+
Type type = outputHelper.GetType();
308+
309+
FieldInfo testField = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic)
310+
?? throw new Exception("Could not find field 'test' on ITestOutputHelper");
311+
312+
ITest test = testField.GetValue(outputHelper) as ITest
313+
?? throw new Exception("Field 'test' on outputHelper is null or not an ITest object.");
314+
315+
ITestMethod testMethod = test.TestCase.TestMethod;
316+
317+
// Class name will be fully-qualified. We only want the class name, so take the last part.
318+
string[] testClassNameParts = testMethod.TestClass.Class.Name.Split('.');
319+
string testClassName = testClassNameParts[testClassNameParts.Length - 1];
320+
321+
string testMethodName = testMethod.Method.Name;
322+
323+
// Reconstitute the test name as classname.methodname
324+
return $"{testClassName}.{testMethodName}";
325+
}
326+
340327
/// <summary>
341328
/// SQL Server properties we can query.
342329
///
@@ -1295,191 +1282,6 @@ protected virtual void OnMatchingEventWritten(EventWrittenEventArgs eventData)
12951282

12961283
#nullable enable
12971284

1298-
public readonly ref struct XEventScope : IDisposable
1299-
{
1300-
#region Private Fields
1301-
1302-
// Maximum dispatch latency for XEvents, in seconds.
1303-
private const int MaxDispatchLatencySeconds = 5;
1304-
1305-
// The connection to use for all operations.
1306-
private readonly SqlConnection _connection;
1307-
1308-
// True if connected to an Azure SQL instance.
1309-
private readonly bool _isAzureSql;
1310-
1311-
// True if connected to a non-Azure SQL Server 2025 (version 17) or
1312-
// higher.
1313-
private readonly bool _isVersion17OrHigher;
1314-
1315-
// Duration for the XEvent session, in minutes.
1316-
private readonly ushort _durationInMinutes;
1317-
1318-
#endregion
1319-
1320-
#region Properties
1321-
1322-
/// <summary>
1323-
/// The name of the XEvent session, derived from the session name
1324-
/// provided at construction time, with a unique suffix appended.
1325-
/// </summary>
1326-
public string SessionName { get; }
1327-
1328-
#endregion
1329-
1330-
#region Construction
1331-
1332-
/// <summary>
1333-
/// Construct with the specified parameters.
1334-
///
1335-
/// This will use the connection to query the server properties and
1336-
/// setup and start the XEvent session.
1337-
/// </summary>
1338-
/// <param name="sessionName">The base name of the session.</param>
1339-
/// <param name="connection">The SQL connection to use. (Must already be open.)</param>
1340-
/// <param name="eventSpecification">The event specification T-SQL string.</param>
1341-
/// <param name="targetSpecification">The target specification T-SQL string.</param>
1342-
/// <param name="durationInMinutes">The duration of the session in minutes.</param>
1343-
public XEventScope(
1344-
string sessionName,
1345-
// The connection must already be open.
1346-
SqlConnection connection,
1347-
string eventSpecification,
1348-
string targetSpecification,
1349-
ushort durationInMinutes = 5)
1350-
{
1351-
SessionName = GenerateRandomCharacters(sessionName);
1352-
1353-
_connection = connection;
1354-
Assert.Equal(ConnectionState.Open, _connection.State);
1355-
1356-
_durationInMinutes = durationInMinutes;
1357-
1358-
// EngineEdition 5 indicates Azure SQL.
1359-
_isAzureSql = GetSqlServerProperty(connection, ServerProperty.EngineEdition) == "5";
1360-
1361-
// Determine if we're connected to a SQL Server instance version
1362-
// 17 or higher.
1363-
if (!_isAzureSql)
1364-
{
1365-
int majorVersion;
1366-
Assert.True(
1367-
int.TryParse(
1368-
GetSqlServerProperty(connection, ServerProperty.ProductMajorVersion),
1369-
out majorVersion));
1370-
_isVersion17OrHigher = majorVersion >= 17;
1371-
}
1372-
1373-
// Setup and start the XEvent session.
1374-
string sessionLocation = _isAzureSql ? "DATABASE" : "SERVER";
1375-
1376-
// Both Azure SQL and SQL Server 2025+ support setting a maximum
1377-
// duration for the XEvent session.
1378-
string duration =
1379-
_isAzureSql || _isVersion17OrHigher
1380-
? $"MAX_DURATION={_durationInMinutes} MINUTES,"
1381-
: string.Empty;
1382-
1383-
string xEventCreateAndStartCommandText =
1384-
$@"CREATE EVENT SESSION [{SessionName}] ON {sessionLocation}
1385-
{eventSpecification}
1386-
{targetSpecification}
1387-
WITH (
1388-
{duration}
1389-
MAX_MEMORY=16 MB,
1390-
EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
1391-
MAX_DISPATCH_LATENCY={MaxDispatchLatencySeconds} SECONDS,
1392-
MAX_EVENT_SIZE=0 KB,
1393-
MEMORY_PARTITION_MODE=NONE,
1394-
TRACK_CAUSALITY=ON,
1395-
STARTUP_STATE=OFF)
1396-
1397-
ALTER EVENT SESSION [{SessionName}] ON {sessionLocation} STATE = START ";
1398-
1399-
using SqlCommand createXEventSession = new SqlCommand(xEventCreateAndStartCommandText, _connection);
1400-
createXEventSession.ExecuteNonQuery();
1401-
}
1402-
1403-
/// <summary>
1404-
/// Disposal stops and drops the XEvent session.
1405-
/// </summary>
1406-
/// <remarks>
1407-
/// Disposal isn't perfect - tests can abort without cleaning up the
1408-
/// events they have created. For Azure SQL targets that outlive the
1409-
/// test pipelines, it is beneficial to periodically log into the
1410-
/// database and drop old XEvent sessions using T-SQL similar to
1411-
/// this:
1412-
///
1413-
/// DECLARE @sql NVARCHAR(MAX) = N'';
1414-
///
1415-
/// -- Identify inactive (stopped) event sessions and generate DROP commands
1416-
/// SELECT @sql += N'DROP EVENT SESSION [' + name + N'] ON SERVER;' + CHAR(13) + CHAR(10)
1417-
/// FROM sys.server_event_sessions
1418-
/// WHERE running = 0; -- Filter for sessions that are not running (inactive)
1419-
///
1420-
/// -- Print the generated commands for review (optional, but recommended)
1421-
/// PRINT @sql;
1422-
///
1423-
/// -- Execute the generated commands
1424-
/// EXEC sys.sp_executesql @sql;
1425-
/// </remarks>
1426-
public void Dispose()
1427-
{
1428-
string dropXEventSessionCommand = _isAzureSql
1429-
// We choose the sys.(database|server)_event_sessions views
1430-
// here to ensure we find sessions that may not be running.
1431-
? $"IF EXISTS (select * from sys.database_event_sessions where name ='{SessionName}')" +
1432-
$" DROP EVENT SESSION [{SessionName}] ON DATABASE"
1433-
: $"IF EXISTS (select * from sys.server_event_sessions where name ='{SessionName}')" +
1434-
$" DROP EVENT SESSION [{SessionName}] ON SERVER";
1435-
1436-
using SqlCommand command = new SqlCommand(dropXEventSessionCommand, _connection);
1437-
command.ExecuteNonQuery();
1438-
}
1439-
1440-
#endregion
1441-
1442-
#region Public Methods
1443-
1444-
/// <summary>
1445-
/// Query the XEvent session for its collected events, returning
1446-
/// them as an XML document.
1447-
///
1448-
/// This always blocks the thread for MaxDispatchLatencySeconds to
1449-
/// ensure that all events have been flushed into the ring buffer.
1450-
/// </summary>
1451-
public System.Xml.XmlDocument GetEvents()
1452-
{
1453-
string xEventQuery = _isAzureSql
1454-
? $@"SELECT xet.target_data
1455-
FROM sys.dm_xe_database_session_targets AS xet
1456-
INNER JOIN sys.dm_xe_database_sessions AS xe
1457-
ON (xe.address = xet.event_session_address)
1458-
WHERE xe.name = '{SessionName}'"
1459-
: $@"SELECT xet.target_data
1460-
FROM sys.dm_xe_session_targets AS xet
1461-
INNER JOIN sys.dm_xe_sessions AS xe
1462-
ON (xe.address = xet.event_session_address)
1463-
WHERE xe.name = '{SessionName}'";
1464-
1465-
using SqlCommand command = new SqlCommand(xEventQuery, _connection);
1466-
1467-
// Wait for maximum dispatch latency to ensure all events
1468-
// have been flushed to the ring buffer.
1469-
Thread.Sleep(MaxDispatchLatencySeconds * 1000);
1470-
1471-
string? targetData = command.ExecuteScalar() as string;
1472-
Assert.NotNull(targetData);
1473-
1474-
System.Xml.XmlDocument xmlDocument = new System.Xml.XmlDocument();
1475-
1476-
xmlDocument.LoadXml(targetData);
1477-
return xmlDocument;
1478-
}
1479-
1480-
#endregion
1481-
}
1482-
14831285
/// <summary>
14841286
/// Resolves the machine's fully qualified domain name if it is applicable.
14851287
/// </summary>

0 commit comments

Comments
 (0)