@@ -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