Skip to content

Commit f50fd25

Browse files
authored
[Test Optimization] Avoid crashing on Allure.Xunit usage (#7305)
## Summary of changes This PR avoid crashing testhost when Allure.XUnit is being used. ## Reason for change Currently using Allure in your XUnit tests will fail. ## Implementation details Ensure to bypass some messages to the messagebus in order to trigger some XUnit internal events in the MessageSink. Also when we disable a test we stop sending messages in the messagebus regarding that test. ## Test coverage Added the Allure.XUnit package in one of the XUnit sample test. Also it tries to add the allure api inside a test. The goal is to don't crash the samples app and have the same test passing without any exception (errors in the logs). ## Other details <!-- Fixes #{issue} --> <!-- ⚠️ Note: where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. -->
1 parent 4f79fc2 commit f50fd25

File tree

9 files changed

+118
-5
lines changed

9 files changed

+118
-5
lines changed

tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Datadog.Trace.Telemetry;
1717
using Datadog.Trace.Util;
1818
using Datadog.Trace.Vendors.MessagePack;
19+
using Datadog.Trace.Vendors.Serilog.Events;
1920

2021
namespace Datadog.Trace.Ci.Agent;
2122

@@ -292,6 +293,11 @@ private static async Task InternalFlushEventsAsync(CIVisibilityProtocolWriter wr
292293

293294
completionSource.TrySetResult(true);
294295
Log.Debug("CIVisibilityProtocolWriter: InternalFlushEventsAsync/ Finishing FlushEventsAsync loop");
296+
if (Log.IsEnabled(LogEventLevel.Debug))
297+
{
298+
Log.Debug("CIVisibilityProtocolWriter: TestCycle stats: {Stats}", $"[Tests: {buffers.CiTestCycleBuffer.TestEventsCount}, TestSuites: {buffers.CiTestCycleBuffer.TestSuiteEventsCount}, TestModules: {buffers.CiTestCycleBuffer.TestModuleEventsCount}, TestSessions: {buffers.CiTestCycleBuffer.TestSessionEventsCount}, Spans: {buffers.CiTestCycleBuffer.SpanEventsCount}]");
299+
Log.Debug<int>("CIVisibilityProtocolWriter: CodeCoverage stats: [Coverage: {Coverage}]", buffers.CiCodeCoverageBuffer.TestCoverageEventsCount);
300+
}
295301
}
296302

297303
internal class WatermarkEvent : IEvent

tracer/src/Datadog.Trace/Ci/Agent/Payloads/CICodeCoveragePayload.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public override MetricTags.CIVisibilityEndpointAndCompression TelemetryEndpointA
4545
? MetricTags.CIVisibilityEndpointAndCompression.CodeCoverageRequestCompressed
4646
: MetricTags.CIVisibilityEndpointAndCompression.CodeCoverageUncompressed;
4747

48+
public int TestCoverageEventsCount { get; private set; }
49+
4850
public override bool CanProcessEvent(IEvent @event)
4951
{
5052
return @event is TestCoverage;
@@ -62,6 +64,11 @@ public override bool TryProcessEvent(IEvent @event)
6264
{
6365
_serializationWatch.Restart();
6466
var success = base.TryProcessEvent(@event);
67+
if (success && @event is TestCoverage)
68+
{
69+
TestCoverageEventsCount++;
70+
}
71+
6572
TelemetryFactory.Metrics.RecordDistributionCIVisibilityEndpointEventsSerializationMs(TelemetryEndpoint, _serializationWatch.Elapsed.TotalMilliseconds);
6673
return success;
6774
}

tracer/src/Datadog.Trace/Ci/Agent/Payloads/CIVisibilityProtocolPayload.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.IO;
99
using Datadog.Trace.Ci.Agent.MessagePack;
1010
using Datadog.Trace.Ci.Configuration;
11+
using Datadog.Trace.Ci.EventModel;
1112
using Datadog.Trace.Telemetry;
1213
using Datadog.Trace.Vendors.MessagePack;
1314

@@ -35,10 +36,44 @@ public CIVisibilityProtocolPayload(TestOptimizationSettings settings, IFormatter
3536

3637
internal EventsBuffer<IEvent> Events => _events;
3738

39+
public int TestEventsCount { get; private set; }
40+
41+
public int TestSuiteEventsCount { get; private set; }
42+
43+
public int TestModuleEventsCount { get; private set; }
44+
45+
public int TestSessionEventsCount { get; private set; }
46+
47+
public int SpanEventsCount { get; private set; }
48+
3849
public override bool TryProcessEvent(IEvent @event)
3950
{
4051
_serializationWatch.Restart();
4152
var success = _events.TryWrite(@event);
53+
if (success)
54+
{
55+
if (@event is TestEvent)
56+
{
57+
TestEventsCount++;
58+
}
59+
else if (@event is TestSuiteEvent)
60+
{
61+
TestSuiteEventsCount++;
62+
}
63+
else if (@event is TestModuleEvent)
64+
{
65+
TestModuleEventsCount++;
66+
}
67+
else if (@event is TestSessionEvent)
68+
{
69+
TestSessionEventsCount++;
70+
}
71+
else if (@event is EventModel.SpanEvent)
72+
{
73+
SpanEventsCount++;
74+
}
75+
}
76+
4277
TelemetryFactory.Metrics.RecordDistributionCIVisibilityEndpointEventsSerializationMs(TelemetryEndpoint, _serializationWatch.Elapsed.TotalMilliseconds);
4378
return success;
4479
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/RetryMessageBus.cs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public bool QueueMessage(object? message)
7777
else
7878
{
7979
Common.Log.Debug("RetryMessageBus.QueueMessage: Message is not a supported message. Flushing: {Message}", message);
80-
return _innerMessageBus.QueueMessage(message);
80+
return InternalQueueMessage(message);
8181
}
8282

8383
Common.Log.Debug("RetryMessageBus.QueueMessage: Message: {Message} | UniqueID: {UniqueID}", message, uniqueID);
@@ -88,7 +88,7 @@ public bool QueueMessage(object? message)
8888
if (metadata.Disposed)
8989
{
9090
Common.Log.Debug("RetryMessageBus.QueueMessage: Metadata is disposed for: {UniqueID} direct flush of the message.", uniqueID);
91-
return _innerMessageBus.QueueMessage(message);
91+
return InternalQueueMessage(message);
9292
}
9393

9494
var totalExecutions = metadata.TotalExecutions;
@@ -122,11 +122,25 @@ public bool QueueMessage(object? message)
122122
}
123123

124124
lstRetryInstance.Add(message);
125+
126+
// Bypass some events to trigger MessageSink events. (Allure lib required it to create the test context)
127+
// but just send the event once.
128+
var messageTypeName = message.GetType().Name;
129+
if (messageTypeName is "TestStarting" or "TestClassConstructionStarting" or "TestClassConstructionFinished")
130+
{
131+
if ((!metadata.Skipped && metadata.BypassedMessageTypes.Add(messageTypeName)) ||
132+
metadata.EarlyFlakeDetectionEnabled)
133+
{
134+
Common.Log.Debug("RetryMessageBus.QueueMessage: Message bypass, flushing directly for: {UniqueID} | {MessageType}", uniqueID, messageTypeName);
135+
return InternalQueueMessage(message);
136+
}
137+
}
138+
125139
return true;
126140
}
127141

128142
Common.Log.Error("RetryMessageBus.QueueMessage: Message doesn't have an UniqueID. Flushing: {Message}", message);
129-
return _innerMessageBus.QueueMessage(message);
143+
return InternalQueueMessage(message);
130144
}
131145

132146
public bool FlushMessages(string uniqueID)
@@ -135,9 +149,10 @@ public bool FlushMessages(string uniqueID)
135149

136150
var metadata = (RetryTestCaseMetadata)GetMetadata(uniqueID);
137151
var listOfMessages = metadata.ListOfMessages;
138-
if (listOfMessages is null || listOfMessages.Length == 0 || metadata.Disposed)
152+
if (listOfMessages is null || listOfMessages.Length == 0 || metadata.Disposed || metadata.Skipped)
139153
{
140154
Common.Log.Debug("RetryMessageBus.FlushMessages: Nothing to flush for: {UniqueID}", uniqueID);
155+
metadata.ListOfMessages = null;
141156
return true;
142157
}
143158

@@ -169,9 +184,19 @@ public bool FlushMessages(string uniqueID)
169184
bool InternalFlushMessages(List<object> messages)
170185
{
171186
var retValue = true;
187+
Common.Log.Debug("RetryMessageBus.InternalFlushMessages: Flushing messages for: {UniqueID}", uniqueID);
172188
foreach (var messageInList in messages)
173189
{
174-
retValue = retValue && _innerMessageBus.QueueMessage(messageInList);
190+
var messageTypeName = messageInList.GetType().Name;
191+
if (messageTypeName is "TestStarting" or "TestClassConstructionStarting" or "TestClassConstructionFinished")
192+
{
193+
Common.Log.Debug("RetryMessageBus.InternalFlushMessages: Skipping message: {Message} for: {UniqueID}", messageInList, uniqueID);
194+
}
195+
else
196+
{
197+
Common.Log.Debug("RetryMessageBus.InternalFlushMessages: Flushing message: {Message} for: {UniqueID}", messageInList, uniqueID);
198+
retValue = InternalQueueMessage(messageInList) && retValue;
199+
}
175200
}
176201

177202
Common.Log.Debug<int, string>("RetryMessageBus.InternalFlushMessages: {Count} messages flushed for: {UniqueID}", messages.Count, uniqueID);
@@ -181,6 +206,19 @@ bool InternalFlushMessages(List<object> messages)
181206
}
182207
}
183208

209+
private bool InternalQueueMessage(object message)
210+
{
211+
try
212+
{
213+
return _innerMessageBus.QueueMessage(message);
214+
}
215+
catch (Exception ex)
216+
{
217+
Common.Log.Error(ex, "RetryMessageBus.InternalQueueMessage: Error while queueing message: {Message}", message);
218+
return false;
219+
}
220+
}
221+
184222
#pragma warning disable SA1201 // ElementsMustAppearInTheCorrectOrder
185223
internal interface ITestCaseMessage
186224
{
@@ -214,6 +252,14 @@ public List<object>?[]? ListOfMessages
214252

215253
public bool Disposed { get; set; }
216254

255+
/// <summary>
256+
/// Gets the messages types that were bypassed to trigger MessageSink events.
257+
/// This is used to avoid sending the same message multiple times.
258+
/// For example, TestStarting, TestClassConstructionStarting, TestClassConstructionFinished, etc.
259+
/// "TestCaseStarting" or "TestMethodStarting" are not bypassed, as they are used to create the test context.
260+
/// </summary>
261+
public HashSet<string> BypassedMessageTypes { get; } = new();
262+
217263
public void ResizeListOfMessages(int totalExecutions)
218264
{
219265
Array.Resize(ref _listOfMessages, totalExecutions);

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/TestCaseMetadata.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,7 @@ public TestCaseMetadata(string uniqueID, int totalExecution, int countDownExecut
4343

4444
public bool AllRetriesFailed { get; set; } = true;
4545

46+
public bool Skipped { get; set; }
47+
4648
public string UniqueID { get; }
4749
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestRunnerRunAsyncIntegration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ internal static CallTargetState OnMethodBegin<TTarget>(TTarget instance)
129129
{
130130
runnerInstance.SkipReason = "Flaky test is disabled by Datadog";
131131
testRunnerInstance.SkipReason = runnerInstance.SkipReason;
132+
testCaseMetadata.Skipped = true;
132133
Common.Log.Debug("XUnitTestRunnerRunAsyncIntegration: Skipping test: {Class}.{Name} Reason: {Reason}", runnerInstance.TestClass?.ToString() ?? string.Empty, runnerInstance.TestMethod?.Name ?? string.Empty, runnerInstance.SkipReason);
133134
XUnitIntegration.CreateTest(ref runnerInstance, testCaseMetadata);
134135
}

tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TcpXUnitEvpTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// </copyright>
55
#if NETCOREAPP3_1_OR_GREATER
66
using System.Threading.Tasks;
7+
using Datadog.Trace.Configuration;
78
using Xunit;
89
using Xunit.Abstractions;
910

tracer/test/test-applications/integrations/Samples.XUnitTests/Samples.XUnitTests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4040
<PrivateAssets>all</PrivateAssets>
4141
</PackageReference>
42+
<PackageReference
43+
Condition="$([MSBuild]::VersionGreaterThanOrEquals($(ApiVersion), '2.9.0')) AND $([MSBuild]::VersionLessThan($(ApiVersion), '3.0.0'))"
44+
Include="Allure.Xunit"
45+
Version="2.12.1" />
4246
</ItemGroup>
4347

4448
<ItemGroup>

tracer/test/test-applications/integrations/Samples.XUnitTests/TestSuite.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Diagnostics;
4+
using System.Linq;
45
using Xunit;
56
using Xunit.Abstractions;
67

@@ -20,6 +21,16 @@ public TestSuite(ITestOutputHelper output)
2021
public void SimplePassTest()
2122
{
2223
_output.WriteLine("Test:SimplePassTest");
24+
25+
// Let's call the Allure API to add tags if available
26+
var allureApi = AppDomain.CurrentDomain.GetAssemblies()
27+
.Select(asm => asm.GetType("Allure.Net.Commons.AllureApi", false))
28+
.FirstOrDefault(type => type != null);
29+
if (allureApi is not null)
30+
{
31+
_output.WriteLine("Allure API found, adding tags.");
32+
allureApi.GetMethod("AddTags")?.Invoke(null, [new[] { "tag1", "tag2" }]);
33+
}
2334
}
2435

2536
[Fact(Skip = "Simple skip reason")]

0 commit comments

Comments
 (0)