Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
<!-- Not auto-updated. -->
<MicrosoftDiaSymReaderVersion>2.0.0</MicrosoftDiaSymReaderVersion>
<MicrosoftDiaSymReaderNativeVersion>17.10.0-beta1.24272.1</MicrosoftDiaSymReaderNativeVersion>
<TraceEventVersion>3.1.16</TraceEventVersion>
<TraceEventVersion>3.1.28</TraceEventVersion>
<MicrosoftDiagnosticsNetCoreClientVersion>0.2.621003</MicrosoftDiagnosticsNetCoreClientVersion>
<NETStandardLibraryRefVersion>2.1.0</NETStandardLibraryRefVersion>
<NetStandardLibraryVersion>2.0.3</NetStandardLibraryVersion>
Expand All @@ -124,6 +124,7 @@
<!-- Testing -->
<MicrosoftNETCoreCoreDisToolsVersion>1.6.0</MicrosoftNETCoreCoreDisToolsVersion>
<MicrosoftNETTestSdkVersion>17.4.0-preview-20220707-01</MicrosoftNETTestSdkVersion>
<MicrosoftOneCollectRecordTraceVersion>0.1.32221</MicrosoftOneCollectRecordTraceVersion>
<NUnitVersion>3.12.0</NUnitVersion>
<NUnit3TestAdapterVersion>4.5.0</NUnit3TestAdapterVersion>
<CoverletCollectorVersion>6.0.4</CoverletCollectorVersion>
Expand Down
16 changes: 16 additions & 0 deletions src/tests/Common/helixpublishwitharcade.proj
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,28 @@
<XUnitLogCheckerCommand>$(XUnitLogCheckerHelixPath)XUnitLogChecker$(ExeSuffix) $(XUnitLogCheckerArgs)</XUnitLogCheckerCommand>
</PropertyGroup>

<ItemGroup>
<_ExtraTestExecutablesListFiles Remote="@(_ExtraTestExecutablesListFiles)" />
<_ExtraTestExecutablesListFiles Include="@(_MergedPayloadFiles)"
Condition="$([System.String]::Copy('%(Identity)').ToLower().EndsWith('helix-extra-executables.list'))" />
<_ExtraTestExecutables Remove="@(_ExtraTestExecutables)" />
</ItemGroup>
<ReadLinesFromFile File="%(_ExtraTestExecutablesListFiles.Identity)" Condition="'@(_ExtraTestExecutablesListFiles)' != ''">
<Output TaskParameter="Lines" ItemName="_ExtraTestExecutables" />
</ReadLinesFromFile>
<ItemGroup>
<_ExtraTestExecutables Remove="@(_ExtraTestExecutables)" Condition="'%(Identity)' == ''" />
</ItemGroup>

<ItemGroup>
<!-- We need to ensure that the test run script is marked as executable. -->
<HelixCommandLines Condition="'$(TestWrapperTargetsWindows)' == 'true'" Include="set TEST_HARNESS_STRIPE_TO_EXECUTE=.0.1" />
<HelixCommandLines Condition="'$(TestWrapperTargetsWindows)' != 'true'" Include="export TEST_HARNESS_STRIPE_TO_EXECUTE=.0.1" />
<HelixCommandLines Condition="'$(TestWrapperTargetsWindows)' != 'true'" Include="chmod +x $(_MergedWrapperRunScriptRelative)" />

<!-- Tests may depend on other executables. Copying files to Helix removes execute permissions, so mark them as executable as well. -->
<HelixCommandLines Condition="'$(TestWrapperTargetsWindows)' != 'true' and Exists('$(TestBinDir)%(Identity)')" Include="@(_ExtraTestExecutables->'chmod +x %(Identity)')" />

<HelixCommandLines Include="$(_WorkaroundForNuGetMigrations)" />

<!-- Force assemblies to lazy-load for LLVM AOT test runs to enable using tests that fail at AOT time (and as a result can't be AOTd) -->
Expand Down
1 change: 1 addition & 0 deletions src/tests/build.proj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<RestoreProjects Include="Common\XHarnessRunnerLibrary\XHarnessRunnerLibrary.csproj" />
<RestoreProjects Include="Common\external\external.csproj" />
<RestoreProjects Include="Common\ilasm\ilasm.ilproj" />
<RestoreProjects Include="tracing\userevents\common\userevents_common.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/tests/issues.targets
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,9 @@
<ExcludeList Include="$(XunitTestBinBase)/baseservices/exceptions/UnhandledExceptionHandler/NoEffectInMainThread/*">
<Issue>Test issue. The test relies on overriding the process return code.</Issue>
</ExcludeList>
<ExcludeList Include="$(XunitTestBinBase)/tracing/userevents/**">
<Issue>Record-Trace's diagnostic port detection logic relies on memfd-based double mapping</Issue>
</ExcludeList>
</ItemGroup>

<!-- Known failures for mono runtime on Windows -->
Expand Down
29 changes: 29 additions & 0 deletions src/tests/tracing/userevents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# User Events Functional Tests

This directory contains **functional tests** for the .NET user_events scenario. These tests validate that .NET Runtime user events can be emitted via EventPipe and collected by [one-collect](https://github.com/microsoft/one-collect/)'s `record-trace` tool.

## High-level Test Flow

Each scenario (for example, `basic`) uses the same pattern:

1. **Scenario invokes the shared test runner**

User events scenarios can differ in their tracee logic, the events expected in the .nettrace, the record-trace script used to collect those events, and how long it takes for the tracee to emit them and for record-trace to resolve symbols and write the .nettrace. To handle this variance, UserEventsTestRunner lets each scenario pass in its scenario-specific record-trace script path, the path to its test assembly (used to spawn the tracee process), a validator that checks for the expected events from the tracee, and optional timeouts for both the tracee and record-trace to exit gracefully.

2. **`UserEventsTestRunner` orchestrates tracing and validation**

Using this configuration, UserEventsTestRunner first checks whether user events are supported. It then starts record-trace with the scenario’s script and launches the tracee process so it can emit events. After the run completes, the runner stops both the tracee and record-trace, opens the resulting .nettrace with EventPipeEventSource, and applies the scenario’s validator to confirm that the expected events were recorded. Finally, it returns an exit code indicating whether the scenario passed or failed.

## Layout

- `common/`
- `UserEventsRequirements.cs` - Checks whether the environment supports user events.
- `UserEventsTestRunner.cs` - Shared runner that coordinates `record-trace`, the tracee process, and event validation.
- `userevents_common.csproj` - Common project for shared user events test logic.
- `NuGet.config` - Configures dotnet-diagnostics-tests nuget source which transports Microsoft.OneCollect.RecordTrace.
- `<scenario>/`
- `<scenario>.cs` - The tracee workload logic used when invoked with the `tracee` argument.
- `<scenario>.csproj` - Project file for the scenario.
- `<scenario>.script` - `record-trace` script that configures how to collect the trace for the scenario.

Each scenario reuses the common runner and shared `record-trace` deployable instead of duplicating binaries or orchestration logic. The `basic` scenario serves as a concrete example of how to add additional scenarios.
182 changes: 182 additions & 0 deletions src/tests/tracing/userevents/activity/activity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.Tracing;
using System.Threading.Tasks;
using Tracing.UserEvents.Tests.Common;
using Microsoft.Diagnostics.Tracing;

namespace Tracing.UserEvents.Tests.Activity
{
public static class Activity
{
public static void ActivityTracee()
{
ActivityTraceeAsync().GetAwaiter().GetResult();
}

private static async Task ActivityTraceeAsync()
{
Task requestA = ProcessRequest("RequestA");
Task requestB = ProcessRequest("RequestB");

await Task.WhenAll(requestA, requestB);
}

private static async Task ProcessRequest(string requestName)
{
ActivityEventSource.Log.WorkStart(requestName);

Task query1 = Query("Query1 for " + requestName);
Task query2 = Query("Query2 for " + requestName);

await Task.WhenAll(query1, query2);

ActivityEventSource.Log.WorkStop();
}

private static async Task Query(string query)
{
ActivityEventSource.Log.QueryStart(query);
await Task.Delay(50);
ActivityEventSource.Log.DebugMessage("processing " + query);
await Task.Delay(50);
ActivityEventSource.Log.QueryStop();
}

private static readonly Func<EventPipeEventSource, bool> s_traceValidator = source =>
{
Guid firstWorkActivityId = Guid.Empty;
Guid secondWorkActivityId = Guid.Empty;

Guid firstWorkQuery1ActivityId = Guid.Empty;
Guid firstWorkQuery2ActivityId = Guid.Empty;
Guid secondWorkQuery1ActivityId = Guid.Empty;
Guid secondWorkQuery2ActivityId = Guid.Empty;

Guid firstWorkQuery1RelatedActivityId = Guid.Empty;
Guid firstWorkQuery2RelatedActivityId = Guid.Empty;
Guid secondWorkQuery1RelatedActivityId = Guid.Empty;
Guid secondWorkQuery2RelatedActivityId = Guid.Empty;

source.Dynamic.All += e =>
{
if (!string.Equals(e.ProviderName, "DemoActivityIDs", StringComparison.Ordinal))
{
return;
}

if (e.EventName is null)
{
return;
}

if (e.EventName.Equals("Work/Start", StringComparison.OrdinalIgnoreCase))
{
string requestName = e.PayloadByName("requestName") as string ?? string.Empty;

if (string.Equals(requestName, "RequestA", StringComparison.Ordinal))
{
firstWorkActivityId = e.ActivityID;
}
else if (string.Equals(requestName, "RequestB", StringComparison.Ordinal))
{
secondWorkActivityId = e.ActivityID;
}
}
else if (e.EventName.Equals("Query/Start", StringComparison.OrdinalIgnoreCase))
{
string queryText = e.PayloadByName("query") as string ?? string.Empty;

if (string.Equals(queryText, "Query1 for RequestA", StringComparison.Ordinal))
{
firstWorkQuery1ActivityId = e.ActivityID;
firstWorkQuery1RelatedActivityId = e.RelatedActivityID;
}
else if (string.Equals(queryText, "Query2 for RequestA", StringComparison.Ordinal))
{
firstWorkQuery2ActivityId = e.ActivityID;
firstWorkQuery2RelatedActivityId = e.RelatedActivityID;
}
else if (string.Equals(queryText, "Query1 for RequestB", StringComparison.Ordinal))
{
secondWorkQuery1ActivityId = e.ActivityID;
secondWorkQuery1RelatedActivityId = e.RelatedActivityID;
}
else if (string.Equals(queryText, "Query2 for RequestB", StringComparison.Ordinal))
{
secondWorkQuery2ActivityId = e.ActivityID;
secondWorkQuery2RelatedActivityId = e.RelatedActivityID;
}
}
};

source.Process();

if (firstWorkActivityId == Guid.Empty || secondWorkActivityId == Guid.Empty)
{
Console.Error.WriteLine("The trace did not contain two WorkStart events with ActivityIds for RequestA and RequestB.");
return false;
}

if (firstWorkQuery1ActivityId == Guid.Empty || firstWorkQuery2ActivityId == Guid.Empty ||
secondWorkQuery1ActivityId == Guid.Empty || secondWorkQuery2ActivityId == Guid.Empty)
{
Console.Error.WriteLine("The trace did not contain all expected QueryStart events with ActivityIds for both requests.");
return false;
}

if (firstWorkQuery1RelatedActivityId == Guid.Empty || firstWorkQuery2RelatedActivityId == Guid.Empty ||
secondWorkQuery1RelatedActivityId == Guid.Empty || secondWorkQuery2RelatedActivityId == Guid.Empty)
{
Console.Error.WriteLine("The trace did not contain RelatedActivityIds on all QueryStart events.");
return false;
}

if (firstWorkQuery1RelatedActivityId != firstWorkActivityId ||
firstWorkQuery2RelatedActivityId != firstWorkActivityId ||
secondWorkQuery1RelatedActivityId != secondWorkActivityId ||
secondWorkQuery2RelatedActivityId != secondWorkActivityId)
{
Console.Error.WriteLine("QueryStart RelatedActivityIds did not match their corresponding WorkStart ActivityIds.");
return false;
}

return true;
};

public static int Main(string[] args)
{
return UserEventsTestRunner.Run(
args,
"activity",
typeof(Activity).Assembly.Location,
ActivityTracee,
s_traceValidator);
}
}

[EventSource(Name = "DemoActivityIDs")]
internal sealed class ActivityEventSource : EventSource
{
public static readonly ActivityEventSource Log = new ActivityEventSource();

private ActivityEventSource() {}

[Event(1)]
public void WorkStart(string requestName) => WriteEvent(1, requestName);

[Event(2)]
public void WorkStop() => WriteEvent(2);

[Event(3)]
public void DebugMessage(string message) => WriteEvent(3, message);

[Event(4)]
public void QueryStart(string query) => WriteEvent(4, query);

[Event(5)]
public void QueryStop() => WriteEvent(5);
}
}
19 changes: 19 additions & 0 deletions src/tests/tracing/userevents/activity/activity.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<CLRTestTargetUnsupported Condition="'$(TargetOS)' != 'linux' or ('$(TargetArchitecture)' != 'x64' and '$(TargetArchitecture)' != 'arm64')">true</CLRTestTargetUnsupported>
<RequiresProcessIsolation>true</RequiresProcessIsolation>
<ReferenceXUnitWrapperGenerator>false</ReferenceXUnitWrapperGenerator>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
<ProjectReference Include="../common/userevents_common.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="activity.script">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>activity.script</TargetPath>
</Content>
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions src/tests/tracing/userevents/activity/activity.script
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let DemoActivityIds_flags = new_dotnet_provider_flags();
record_dotnet_provider("DemoActivityIDs", 0x0, 5, DemoActivityIds_flags);

let System_Threading_Tasks_TplEventSource_flags = new_dotnet_provider_flags();
record_dotnet_provider("System.Threading.Tasks.TplEventSource", 0x80, 5, System_Threading_Tasks_TplEventSource_flags);
63 changes: 63 additions & 0 deletions src/tests/tracing/userevents/basic/basic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Threading;
using Tracing.UserEvents.Tests.Common;
using Microsoft.Diagnostics.Tracing;

namespace Tracing.UserEvents.Tests.Basic
{
public class Basic
{
private static byte[] s_array;

public static void BasicTracee()
{
long startTimestamp = Stopwatch.GetTimestamp();
long targetTicks = Stopwatch.Frequency; // 1s

while (Stopwatch.GetTimestamp() - startTimestamp < targetTicks)
{
s_array = new byte[1024 * 100];
Thread.Sleep(100);
}
}

private readonly static Func<EventPipeEventSource, bool> s_traceValidator = source =>
{
bool allocationSampledEventFound = false;

source.Dynamic.All += (TraceEvent e) =>
{
if (e.ProviderName == "Microsoft-Windows-DotNETRuntime")
{
// TraceEvent's ClrTraceEventParser does not know about the AllocationSampled Event, so it shows up as "Unknown(303)"
if (e.EventName.StartsWith("Unknown") && e.ID == (TraceEventID)303)
{
allocationSampledEventFound = true;
}
}
};

source.Process();

if (!allocationSampledEventFound)
{
Console.Error.WriteLine("The trace did not contain an AllocationSampled event.");
}
return allocationSampledEventFound;
};

public static int Main(string[] args)
{
return UserEventsTestRunner.Run(
args,
"basic",
typeof(Basic).Assembly.Location,
BasicTracee,
s_traceValidator);
}
}
}
19 changes: 19 additions & 0 deletions src/tests/tracing/userevents/basic/basic.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<CLRTestTargetUnsupported Condition="'$(TargetOS)' != 'linux' or ('$(TargetArchitecture)' != 'x64' and '$(TargetArchitecture)' != 'arm64')">true</CLRTestTargetUnsupported>
<RequiresProcessIsolation>true</RequiresProcessIsolation>
<ReferenceXUnitWrapperGenerator>false</ReferenceXUnitWrapperGenerator>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
<ProjectReference Include="../common/userevents_common.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="basic.script">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>basic.script</TargetPath>
</Content>
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions src/tests/tracing/userevents/basic/basic.script
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let Microsoft_Windows_DotNETRuntime_flags = new_dotnet_provider_flags();
record_dotnet_provider("Microsoft-Windows-DotNETRuntime", 0x80000000000, 4, Microsoft_Windows_DotNETRuntime_flags);
6 changes: 6 additions & 0 deletions src/tests/tracing/userevents/common/NuGet.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="dotnet-diagnostics-tests" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-diagnostics-tests/nuget/v3/index.json" />
</packageSources>
</configuration>
Loading
Loading