Skip to content

Commit 169774b

Browse files
committed
[Tests] Standup shared UserEventsTestRunner
1 parent d25c63d commit 169774b

File tree

6 files changed

+423
-1
lines changed

6 files changed

+423
-1
lines changed

eng/Versions.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
<!-- Not auto-updated. -->
110110
<MicrosoftDiaSymReaderVersion>2.0.0</MicrosoftDiaSymReaderVersion>
111111
<MicrosoftDiaSymReaderNativeVersion>17.10.0-beta1.24272.1</MicrosoftDiaSymReaderNativeVersion>
112-
<TraceEventVersion>3.1.16</TraceEventVersion>
112+
<TraceEventVersion>3.1.28</TraceEventVersion>
113113
<MicrosoftDiagnosticsNetCoreClientVersion>0.2.621003</MicrosoftDiagnosticsNetCoreClientVersion>
114114
<NETStandardLibraryRefVersion>2.1.0</NETStandardLibraryRefVersion>
115115
<NetStandardLibraryVersion>2.0.3</NetStandardLibraryVersion>
@@ -124,6 +124,7 @@
124124
<!-- Testing -->
125125
<MicrosoftNETCoreCoreDisToolsVersion>1.6.0</MicrosoftNETCoreCoreDisToolsVersion>
126126
<MicrosoftNETTestSdkVersion>17.4.0-preview-20220707-01</MicrosoftNETTestSdkVersion>
127+
<MicrosoftOneCollectRecordTraceVersion>0.1.32221</MicrosoftOneCollectRecordTraceVersion>
127128
<NUnitVersion>3.12.0</NUnitVersion>
128129
<NUnit3TestAdapterVersion>4.5.0</NUnit3TestAdapterVersion>
129130
<CoverletCollectorVersion>6.0.4</CoverletCollectorVersion>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# User Events Functional Tests
2+
3+
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.
4+
5+
## High-level Test Flow
6+
7+
Each scenario uses the same pattern:
8+
9+
1. **Scenario invokes the shared test runner**
10+
11+
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.
12+
13+
2. **`UserEventsTestRunner` orchestrates tracing and validation**
14+
15+
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.
16+
17+
## Layout
18+
19+
- `common/`
20+
- `UserEventsRequirements.cs` - Checks whether the environment supports user events.
21+
- `UserEventsTestRunner.cs` - Shared runner that coordinates `record-trace`, the tracee process, and event validation.
22+
- `userevents_common.csproj` - Common project for shared user events test logic.
23+
- `NuGet.config` - Configures dotnet-diagnostics-tests nuget source which transports Microsoft.OneCollect.RecordTrace.
24+
- `<scenario>/`
25+
- `<scenario>.cs` - The tracee workload logic used when invoked with the `tracee` argument.
26+
- `<scenario>.csproj` - Project file for the scenario.
27+
- `<scenario>.script` - `record-trace` script that configures how to collect the trace for the scenario.
28+
29+
Each scenario reuses the common runner and shared `record-trace` deployable instead of duplicating binaries or orchestration logic.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<packageSources>
4+
<add key="dotnet-diagnostics-tests" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-diagnostics-tests/nuget/v3/index.json" />
5+
</packageSources>
6+
</configuration>
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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+
4+
using System;
5+
using System.Diagnostics;
6+
using System.IO;
7+
8+
namespace Tracing.UserEvents.Tests.Common
9+
{
10+
internal static class UserEventsRequirements
11+
{
12+
private static readonly Version s_minKernelVersion = new(6, 4);
13+
private const string TracefsPath = "/sys/kernel/tracing";
14+
private const string UserEventsDataPath = "/sys/kernel/tracing/user_events_data";
15+
private static readonly Version s_minGlibcVersion = new(2, 35);
16+
17+
internal static bool IsSupported()
18+
{
19+
if (Environment.OSVersion.Version < s_minKernelVersion)
20+
{
21+
Console.WriteLine($"Kernel version '{Environment.OSVersion.Version}' is less than the minimum required '{s_minKernelVersion}'");
22+
return false;
23+
}
24+
25+
return IsGlibcAtLeast(s_minGlibcVersion) &&
26+
IsTracefsMounted() &&
27+
IsUserEventsDataAvailable();
28+
}
29+
30+
private static bool IsTracefsMounted()
31+
{
32+
ProcessStartInfo checkTracefsStartInfo = new();
33+
checkTracefsStartInfo.FileName = "sudo";
34+
checkTracefsStartInfo.Arguments = $"-n test -d {TracefsPath}";
35+
checkTracefsStartInfo.UseShellExecute = false;
36+
checkTracefsStartInfo.RedirectStandardOutput = true;
37+
checkTracefsStartInfo.RedirectStandardError = true;
38+
39+
using Process checkTracefs = new() { StartInfo = checkTracefsStartInfo };
40+
checkTracefs.OutputDataReceived += (_, e) =>
41+
{
42+
if (!string.IsNullOrEmpty(e.Data))
43+
{
44+
Console.WriteLine($"[tracefs-check] {e.Data}");
45+
}
46+
};
47+
checkTracefs.ErrorDataReceived += (_, e) =>
48+
{
49+
if (!string.IsNullOrEmpty(e.Data))
50+
{
51+
Console.Error.WriteLine($"[tracefs-check] {e.Data}");
52+
}
53+
};
54+
55+
checkTracefs.Start();
56+
checkTracefs.BeginOutputReadLine();
57+
checkTracefs.BeginErrorReadLine();
58+
checkTracefs.WaitForExit();
59+
if (checkTracefs.ExitCode == 0)
60+
{
61+
return true;
62+
}
63+
64+
Console.WriteLine($"Tracefs not mounted at '{TracefsPath}'.");
65+
return false;
66+
}
67+
68+
private static bool IsUserEventsDataAvailable()
69+
{
70+
ProcessStartInfo checkUserEventsDataStartInfo = new();
71+
checkUserEventsDataStartInfo.FileName = "sudo";
72+
checkUserEventsDataStartInfo.Arguments = $"-n test -e {UserEventsDataPath}";
73+
checkUserEventsDataStartInfo.UseShellExecute = false;
74+
checkUserEventsDataStartInfo.RedirectStandardOutput = true;
75+
checkUserEventsDataStartInfo.RedirectStandardError = true;
76+
77+
using Process checkUserEventsData = new() { StartInfo = checkUserEventsDataStartInfo };
78+
checkUserEventsData.OutputDataReceived += (_, e) =>
79+
{
80+
if (!string.IsNullOrEmpty(e.Data))
81+
{
82+
Console.WriteLine($"[user-events-check] {e.Data}");
83+
}
84+
};
85+
checkUserEventsData.ErrorDataReceived += (_, e) =>
86+
{
87+
if (!string.IsNullOrEmpty(e.Data))
88+
{
89+
Console.Error.WriteLine($"[user-events-check] {e.Data}");
90+
}
91+
};
92+
93+
checkUserEventsData.Start();
94+
checkUserEventsData.BeginOutputReadLine();
95+
checkUserEventsData.BeginErrorReadLine();
96+
checkUserEventsData.WaitForExit();
97+
if (checkUserEventsData.ExitCode == 0)
98+
{
99+
return true;
100+
}
101+
102+
Console.WriteLine($"User events data not available at '{UserEventsDataPath}'.");
103+
return false;
104+
}
105+
106+
private static bool IsGlibcAtLeast(Version minVersion)
107+
{
108+
ProcessStartInfo lddStartInfo = new();
109+
lddStartInfo.FileName = "ldd";
110+
lddStartInfo.Arguments = "--version";
111+
lddStartInfo.UseShellExecute = false;
112+
lddStartInfo.RedirectStandardOutput = true;
113+
lddStartInfo.RedirectStandardError = true;
114+
115+
using Process lddProcess = new() { StartInfo = lddStartInfo };
116+
string? detectedVersionLine = null;
117+
118+
lddProcess.OutputDataReceived += (_, e) =>
119+
{
120+
if (!string.IsNullOrEmpty(e.Data) && detectedVersionLine is null)
121+
{
122+
detectedVersionLine = e.Data;
123+
}
124+
};
125+
lddProcess.ErrorDataReceived += (_, e) =>
126+
{
127+
if (!string.IsNullOrEmpty(e.Data))
128+
{
129+
Console.Error.WriteLine($"[ldd] {e.Data}");
130+
}
131+
};
132+
133+
try
134+
{
135+
lddProcess.Start();
136+
}
137+
catch (Exception ex)
138+
{
139+
Console.WriteLine($"Failed to start 'ldd --version': {ex.Message}");
140+
return false;
141+
}
142+
143+
lddProcess.BeginOutputReadLine();
144+
lddProcess.BeginErrorReadLine();
145+
lddProcess.WaitForExit();
146+
147+
if (lddProcess.ExitCode != 0)
148+
{
149+
Console.WriteLine($"'ldd --version' exited with code {lddProcess.ExitCode}.");
150+
return false;
151+
}
152+
153+
if (string.IsNullOrEmpty(detectedVersionLine))
154+
{
155+
Console.WriteLine("Could not read glibc version from 'ldd --version' output.");
156+
return false;
157+
}
158+
159+
string[] tokens = detectedVersionLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
160+
Version? glibcVersion = null;
161+
foreach (string token in tokens)
162+
{
163+
if (Version.TryParse(token, out Version? parsedVersion))
164+
{
165+
glibcVersion = parsedVersion;
166+
break;
167+
}
168+
}
169+
170+
if (glibcVersion is null)
171+
{
172+
Console.WriteLine($"Failed to parse glibc version from 'ldd --version' output line: {detectedVersionLine}");
173+
return false;
174+
}
175+
176+
if (glibcVersion < minVersion)
177+
{
178+
Console.WriteLine($"glibc version '{glibcVersion}' is less than required '{minVersion}'.");
179+
return false;
180+
}
181+
182+
return true;
183+
}
184+
}
185+
}

0 commit comments

Comments
 (0)