Skip to content

Commit a3723c4

Browse files
authored
Clean up test discovery (#76663)
* Clean up test discovery Make a few small changes here: 1. Added a README.md to explain the purpose of `TestDiscoveryWorker` 2. Moved to using standard command line arguments so the tool can be run locally more easily * PR feedback
1 parent 8096a7c commit a3723c4

File tree

5 files changed

+108
-132
lines changed

5 files changed

+108
-132
lines changed

src/Tools/PrepareTests/MinimizeUtil.cs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,18 @@ internal static void Run(string sourceDirectory, string destinationDirectory, bo
5353
// 2. Hard link all other files into destination directory
5454
Dictionary<Guid, List<FilePathInfo>> initialWalk()
5555
{
56-
IEnumerable<string> directories = new[] {
57-
Path.Combine(sourceDirectory, "eng"),
58-
};
59-
60-
if (!isUnix)
61-
{
62-
directories = directories.Concat([Path.Combine(sourceDirectory, "artifacts", "VSSetup")]);
63-
}
64-
6556
var artifactsDir = Path.Combine(sourceDirectory, "artifacts/bin");
66-
directories = directories.Concat(Directory.EnumerateDirectories(artifactsDir, "*.UnitTests"));
67-
directories = directories.Concat(Directory.EnumerateDirectories(artifactsDir, "*.IntegrationTests"));
68-
directories = directories.Concat(Directory.EnumerateDirectories(artifactsDir, "RunTests"));
57+
List<string> directories =
58+
[
59+
Path.Combine(sourceDirectory, "eng"),
60+
Path.Combine(sourceDirectory, "artifacts", "VSSetup"),
61+
.. Directory.EnumerateDirectories(artifactsDir, "*.UnitTests"),
62+
.. Directory.EnumerateDirectories(artifactsDir, "*.IntegrationTests"),
63+
.. Directory.EnumerateDirectories(artifactsDir, "RunTests")
64+
];
6965

7066
var idToFilePathMap = directories.AsParallel()
67+
.Where(x => Directory.Exists(x))
7168
.SelectMany(unitDirPath => walkDirectory(unitDirPath, sourceDirectory, destinationDirectory))
7269
.GroupBy(pair => pair.mvid)
7370
.ToDictionary(
@@ -79,7 +76,7 @@ Dictionary<Guid, List<FilePathInfo>> initialWalk()
7976

8077
static IEnumerable<(Guid mvid, FilePathInfo pathInfo)> walkDirectory(string unitDirPath, string sourceDirectory, string destinationDirectory)
8178
{
82-
Console.WriteLine($"[{DateTime.UtcNow}] Walking {unitDirPath}");
79+
Console.WriteLine($"Walking {unitDirPath}");
8380
string? lastOutputDirectory = null;
8481
foreach (var sourceFilePath in Directory.EnumerateFiles(unitDirPath, "*", SearchOption.AllDirectories))
8582
{

src/Tools/PrepareTests/TestDiscovery.cs

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ public static bool RunDiscovery(string repoRootDirectory, string dotnetPath, boo
3535
? dotnetFrameworkWorker
3636
: dotnetCoreWorker;
3737

38-
var result = RunWorker(dotnetPath, workerPath, assembly);
38+
var (workerSucceeded, output) = RunWorker(dotnetPath, workerPath, assembly);
3939
lock (s_lock)
4040
{
41-
success &= result;
41+
Console.WriteLine(output);
42+
success &= workerSucceeded;
4243
}
4344
});
4445
stopwatch.Stop();
@@ -71,73 +72,35 @@ public static bool RunDiscovery(string repoRootDirectory, string dotnetPath, boo
7172
Path.Combine(testDiscoveryWorkerFolder, configuration, "net472", "TestDiscoveryWorker.exe"));
7273
}
7374

74-
static bool RunWorker(string dotnetPath, string pathToWorker, string pathToAssembly)
75+
static (bool Succeeded, string Output) RunWorker(string dotnetPath, string pathToWorker, string pathToAssembly)
7576
{
76-
var success = true;
77-
var pipeClient = new Process();
78-
var arguments = new List<string>();
77+
var worker = new Process();
78+
var arguments = new StringBuilder();
7979
if (pathToWorker.EndsWith("dll"))
8080
{
81-
arguments.Add(pathToWorker);
82-
pipeClient.StartInfo.FileName = dotnetPath;
81+
arguments.Append($"exec {pathToWorker}");
82+
worker.StartInfo.FileName = dotnetPath;
8383
}
8484
else
8585
{
86-
pipeClient.StartInfo.FileName = pathToWorker;
86+
worker.StartInfo.FileName = pathToWorker;
8787
}
8888

89-
var errorOutput = new StringBuilder();
90-
91-
using (var pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
92-
{
93-
// Pass the client process a handle to the server.
94-
arguments.Add(pipeServer.GetClientHandleAsString());
95-
pipeClient.StartInfo.Arguments = string.Join(" ", arguments);
96-
pipeClient.StartInfo.UseShellExecute = false;
97-
98-
// Errors will be logged to stderr, redirect to us so we can capture it.
99-
pipeClient.StartInfo.RedirectStandardError = true;
100-
pipeClient.ErrorDataReceived += PipeClient_ErrorDataReceived;
101-
pipeClient.Start();
102-
103-
pipeClient.BeginErrorReadLine();
104-
105-
pipeServer.DisposeLocalCopyOfClientHandle();
106-
107-
try
108-
{
109-
// Read user input and send that to the client process.
110-
using var sw = new StreamWriter(pipeServer);
111-
sw.AutoFlush = true;
112-
// Send a 'sync message' and wait for client to receive it.
113-
sw.WriteLine("ASSEMBLY");
114-
// Send the console input to the client process.
115-
sw.WriteLine(pathToAssembly);
116-
}
117-
// Catch the IOException that is raised if the pipe is broken
118-
// or disconnected.
119-
catch (Exception e)
120-
{
121-
Console.Error.WriteLine($"Error: {e.Message}");
122-
success = false;
123-
}
124-
}
125-
126-
pipeClient.WaitForExit();
127-
success &= pipeClient.ExitCode == 0;
128-
pipeClient.Close();
129-
130-
if (!success)
131-
{
132-
Console.WriteLine($"Failed to discover tests in {pathToAssembly}:{Environment.NewLine}{errorOutput}");
133-
}
134-
135-
return success;
136-
137-
void PipeClient_ErrorDataReceived(object sender, DataReceivedEventArgs e)
138-
{
139-
errorOutput.AppendLine(e.Data);
140-
}
89+
var pathToOutput = Path.Combine(Path.GetDirectoryName(pathToAssembly)!, "testlist.json");
90+
arguments.Append($" --assembly {pathToAssembly} --out {pathToOutput}");
91+
92+
var output = new StringBuilder();
93+
worker.StartInfo.Arguments = arguments.ToString();
94+
worker.StartInfo.UseShellExecute = false;
95+
worker.StartInfo.RedirectStandardOutput = true;
96+
worker.OutputDataReceived += (sender, e) => output.Append(e.Data);
97+
worker.Start();
98+
worker.BeginOutputReadLine();
99+
worker.WaitForExit();
100+
var success = worker.ExitCode == 0;
101+
worker.Close();
102+
103+
return (success, output.ToString());
141104
}
142105

143106
private static List<string> GetAssemblies(string binDirectory, bool isUnix)

src/Tools/TestDiscoveryWorker/Program.cs

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,96 +12,102 @@
1212
using System.Text.Json;
1313
using System.Threading;
1414
using System.Threading.Channels;
15-
15+
using Mono.Options;
1616
using Xunit;
1717
using Xunit.Abstractions;
1818

19-
int ExitFailure = 1;
20-
int ExitSuccess = 0;
19+
const int ExitFailure = 1;
20+
const int ExitSuccess = 0;
21+
22+
string? assemblyFilePath = null;
23+
string? outputFilePath = null;
2124

22-
if (args.Length != 1)
25+
var options = new OptionSet
2326
{
24-
return ExitFailure;
25-
}
27+
{ "assembly=", "The assembly file to process.", v => assemblyFilePath = v },
28+
{ "out=", "The output file name.", v => outputFilePath = v }
29+
};
2630

2731
try
2832
{
29-
using var pipeClient = new AnonymousPipeClientStream(PipeDirection.In, args[0]);
30-
using var sr = new StreamReader(pipeClient);
31-
string? output;
33+
List<string> extra = options.Parse(args);
3234

33-
// Wait for 'sync message' from the server.
34-
do
35+
if (assemblyFilePath is null)
3536
{
36-
output = await sr.ReadLineAsync().ConfigureAwait(false);
37+
Console.WriteLine("Must pass an assembly file name.");
38+
return ExitFailure;
3739
}
38-
while (!(output?.StartsWith("ASSEMBLY", StringComparison.OrdinalIgnoreCase) == true));
3940

40-
if ((output = await sr.ReadLineAsync().ConfigureAwait(false)) is not null)
41+
if (extra.Count > 0)
4142
{
42-
var assemblyFileName = output;
43+
Console.WriteLine($"Unknown arguments: {string.Join(" ", extra)}");
44+
return ExitFailure;
45+
}
46+
47+
if (outputFilePath is null)
48+
{
49+
outputFilePath = Path.Combine(Path.GetDirectoryName(assemblyFilePath)!, "testlist.json");
50+
}
4351

44-
#if NET6_0_OR_GREATER
45-
var resolver = new System.Runtime.Loader.AssemblyDependencyResolver(assemblyFileName);
46-
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (context, assemblyName) =>
52+
#if NET
53+
var resolver = new System.Runtime.Loader.AssemblyDependencyResolver(assemblyFilePath);
54+
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (context, assemblyName) =>
55+
{
56+
var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
57+
if (assemblyPath is not null)
4758
{
48-
var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
49-
if (assemblyPath is not null)
50-
{
51-
return context.LoadFromAssemblyPath(assemblyPath);
52-
}
59+
return context.LoadFromAssemblyPath(assemblyPath);
60+
}
5361

54-
return null;
55-
};
62+
return null;
63+
};
5664
#endif
5765

58-
string testDescriptor = Path.GetFileName(assemblyFileName);
66+
string assemblyFileName = Path.GetFileName(assemblyFilePath);
5967
#if NET
60-
testDescriptor += " (.NET Core)";
68+
string tfm = "(.NET Core)";
6169
#else
62-
testDescriptor += " (.NET Framework)";
70+
string tfm = "(.NET Framework)";
6371
#endif
6472

65-
await Console.Out.WriteLineAsync($"Discovering tests in {testDescriptor}...").ConfigureAwait(false);
73+
Console.Write($"Discovering tests in {tfm} {assemblyFileName} ... ");
6674

67-
using var xunit = new XunitFrontController(AppDomainSupport.IfAvailable, assemblyFileName, shadowCopy: false);
68-
var configuration = ConfigReader.Load(assemblyFileName, configFileName: null);
69-
var sink = new Sink();
70-
xunit.Find(includeSourceInformation: false,
71-
messageSink: sink,
72-
discoveryOptions: TestFrameworkOptions.ForDiscovery(configuration));
75+
using var xunit = new XunitFrontController(AppDomainSupport.IfAvailable, assemblyFilePath, shadowCopy: false);
76+
var configuration = ConfigReader.Load(assemblyFileName, configFileName: null);
77+
var sink = new Sink();
78+
xunit.Find(includeSourceInformation: false,
79+
messageSink: sink,
80+
discoveryOptions: TestFrameworkOptions.ForDiscovery(configuration));
7381

74-
var testsToWrite = new HashSet<string>();
75-
await foreach (var fullyQualifiedName in sink.GetTestCaseNamesAsync())
76-
{
77-
testsToWrite.Add(fullyQualifiedName);
78-
}
79-
80-
if (sink.AnyWriteFailures)
81-
{
82-
await Console.Error.WriteLineAsync($"Channel failed to write for '{assemblyFileName}'").ConfigureAwait(false);
83-
return ExitFailure;
84-
}
85-
86-
#if NET6_0_OR_GREATER
87-
await Console.Out.WriteLineAsync($"Discovered {testsToWrite.Count} tests in {testDescriptor}").ConfigureAwait(false);
88-
#else
89-
await Console.Out.WriteLineAsync($"Discovered {testsToWrite.Count} tests in {testDescriptor}").ConfigureAwait(false);
90-
#endif
82+
var testsToWrite = new HashSet<string>();
83+
await foreach (var fullyQualifiedName in sink.GetTestCaseNamesAsync())
84+
{
85+
testsToWrite.Add(fullyQualifiedName);
86+
}
9187

92-
var directory = Path.GetDirectoryName(assemblyFileName);
93-
using var fileStream = File.Create(Path.Combine(directory!, "testlist.json"));
94-
await JsonSerializer.SerializeAsync(fileStream, testsToWrite).ConfigureAwait(false);
95-
return ExitSuccess;
88+
if (sink.AnyWriteFailures)
89+
{
90+
Console.WriteLine($"Channel failed to write for '{assemblyFileName}'");
91+
return ExitFailure;
9692
}
9793

94+
Console.WriteLine($"{testsToWrite.Count} found");
95+
96+
using var fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);
97+
await JsonSerializer.SerializeAsync(fileStream, testsToWrite.OrderBy(x => x)).ConfigureAwait(false);
98+
return ExitSuccess;
99+
}
100+
catch (OptionException e)
101+
{
102+
Console.WriteLine(e.Message);
103+
options.WriteOptionDescriptions(Console.Out);
98104
return ExitFailure;
99105
}
100106
catch (Exception ex)
101107
{
102108
// Write the exception details to stderr so the host process can pick it up.
103-
await Console.Error.WriteLineAsync(ex.ToString()).ConfigureAwait(false);
104-
return 1;
109+
Console.WriteLine(ex.ToString());
110+
return ExitFailure;
105111
}
106112

107113
file class Sink : IMessageSink
@@ -151,3 +157,4 @@ private void OnTestDiscovered(ITestCaseDiscoveryMessage testCaseDiscovered)
151157
}
152158
}
153159
}
160+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# TestDiscoveryWorker
2+
3+
This program runs xUnit discovery on an assembly and writes the results to a file.
4+
5+
```cmd
6+
> dotnet exec TestDiscoveryWorker.dll --assembly artifacts\bin\Microsoft.CodeAnalysis.UnitTests\Debug\net9.0\Microsoft.CodeAnalysis.UnitTests.dll --out testlist.json
7+
```

src/Tools/TestDiscoveryWorker/TestDiscoveryWorker.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
<OutputType>Exe</OutputType>
55
<TargetFrameworks>$(NetRoslyn);net472</TargetFrameworks>
66
<Nullable>enable</Nullable>
7+
<SignAssembly>false</SignAssembly>
78
</PropertyGroup>
89

910
<ItemGroup>
1011
<PackageReference Include="xunit.abstractions" />
1112
<PackageReference Include="xunit.runner.utility" />
1213
<PackageReference Include="xunit.extensibility.execution" />
14+
<PackageReference Include="Mono.Options" />
1315
</ItemGroup>
1416

1517
<ItemGroup Condition="'$(TargetFramework)'=='net472'">

0 commit comments

Comments
 (0)