Skip to content

Commit f075984

Browse files
authored
Merge pull request #1337 from dotnet/jnm2/tools2
Enable running example tests massively in parallel via `dotnet test` or UI
2 parents 16870d3 + e46e108 commit f075984

File tree

10 files changed

+94
-46
lines changed

10 files changed

+94
-46
lines changed

.github/workflows/tools-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ jobs:
3333
- name: Run all tests
3434
run: |
3535
cd tools
36-
dotnet test
36+
dotnet test --filter Name!~ExampleTests

tools/ExampleExtractor/ExampleMetadata.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,6 @@ public class ExampleMetadata
8484

8585
[JsonIgnore]
8686
public string Source => $"{MarkdownFile}:{StartLine}-{EndLine}";
87+
88+
public override string ToString() => Name;
8789
}

tools/ExampleTester/ExampleTester.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8+
<GenerateProgramFile>false</GenerateProgramFile>
89
</PropertyGroup>
910

1011
<ItemGroup>
1112
<PackageReference Include="Basic.Reference.Assemblies.Net60" Version="1.8.2" />
1213
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
1314
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
16+
<PackageReference Include="NUnit" Version="4.3.2" />
17+
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
1418
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
1519
</ItemGroup>
1620

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using NUnit.Framework;
2+
using Utilities;
3+
4+
[assembly: Parallelizable(ParallelScope.Children)]
5+
6+
namespace ExampleTester;
7+
8+
public static class ExampleTests
9+
{
10+
private static TesterConfiguration TesterConfiguration { get; } = new(Path.Join(FindSlnDirectory(), "tmp"));
11+
12+
public static IEnumerable<object[]> LoadExamples() =>
13+
from example in GeneratedExample.LoadAllExamples(TesterConfiguration.ExtractedOutputDirectory)
14+
select new object[] { example };
15+
16+
[TestCaseSource(nameof(LoadExamples))]
17+
public static async Task ExamplePasses(GeneratedExample example)
18+
{
19+
var logger = new StatusCheckLogger(TestContext.Out, "..", "Example tester");
20+
21+
if (!await example.Test(TesterConfiguration, logger))
22+
Assert.Fail("There were one or more failures. See the logged output for details.");
23+
}
24+
25+
private static string FindSlnDirectory()
26+
{
27+
for (string? current = AppDomain.CurrentDomain.BaseDirectory; current != null; current = Path.GetDirectoryName(current))
28+
{
29+
if (Directory.EnumerateFiles(current, "*.sln").Any())
30+
return current;
31+
}
32+
33+
throw new InvalidOperationException($"Can't find a directory containing a .sln file in {AppDomain.CurrentDomain.BaseDirectory} or any parent directories.");
34+
}
35+
}

tools/ExampleTester/GeneratedExample.cs

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
namespace ExampleTester;
1010

11-
internal class GeneratedExample
11+
public class GeneratedExample
1212
{
13+
private static readonly object CodeExecutionLock = new();
14+
1315
private readonly string directory;
1416
internal ExampleMetadata Metadata { get; }
1517

@@ -20,6 +22,8 @@ private GeneratedExample(string directory)
2022
Metadata = JsonConvert.DeserializeObject<ExampleMetadata>(metadataJson) ?? throw new ArgumentException($"Invalid (null) metadata in {directory}");
2123
}
2224

25+
public override string? ToString() => Metadata.ToString();
26+
2327
internal static List<GeneratedExample> LoadAllExamples(string parentDirectory) =>
2428
Directory.GetDirectories(parentDirectory).Select(Load).ToList();
2529

@@ -134,46 +138,52 @@ bool ValidateOutput()
134138
? new object[] { Metadata.ExecutionArgs ?? new string[0] }
135139
: new object[0];
136140

137-
var oldOut = Console.Out;
138141
List<string> actualLines;
139142
Exception? actualException = null;
140-
try
143+
lock (CodeExecutionLock)
141144
{
142-
var builder = new StringBuilder();
143-
Console.SetOut(new StringWriter(builder));
145+
var oldOut = Console.Out;
144146
try
145147
{
146-
var result = method.Invoke(null, arguments);
147-
// For async Main methods, the compilation's entry point is still the Main
148-
// method, so we explicitly wait for the returned task just like the synthesized
149-
// entry point would.
150-
if (result is Task task)
148+
var builder = new StringBuilder();
149+
Console.SetOut(new StringWriter(builder));
150+
try
151+
{
152+
var result = method.Invoke(null, arguments);
153+
// For async Main methods, the compilation's entry point is still the Main
154+
// method, so we explicitly wait for the returned task just like the synthesized
155+
// entry point would.
156+
if (result is Task task)
157+
{
158+
task.GetAwaiter().GetResult();
159+
}
160+
161+
// For some reason, we don't *actually* get the result of all finalizers
162+
// without this. We shouldn't need it (as relevant examples already have it) but
163+
// code that works outside the test harness doesn't work inside it.
164+
GC.Collect();
165+
GC.WaitForPendingFinalizers();
166+
}
167+
catch (TargetInvocationException outer)
151168
{
152-
task.GetAwaiter().GetResult();
169+
actualException = outer.InnerException ?? throw new InvalidOperationException("TargetInvocationException had no nested exception");
153170
}
154-
// For some reason, we don't *actually* get the result of all finalizers
155-
// without this. We shouldn't need it (as relevant examples already have it) but
156-
// code that works outside the test harness doesn't work inside it.
157-
GC.Collect();
158-
GC.WaitForPendingFinalizers();
171+
172+
// Skip blank lines, to avoid unnecessary trailing empties.
173+
// Also trim the end of each actual line, to avoid trailing spaces being necessary in the metadata
174+
// or listed console output.
175+
actualLines = builder.ToString()
176+
.Replace("\r\n", "\n")
177+
.Split('\n')
178+
.Select(line => line.TrimEnd())
179+
.Where(line => line != "").ToList();
159180
}
160-
catch (TargetInvocationException outer)
181+
finally
161182
{
162-
actualException = outer.InnerException ?? throw new InvalidOperationException("TargetInvocationException had no nested exception");
183+
Console.SetOut(oldOut);
163184
}
164-
// Skip blank lines, to avoid unnecessary trailing empties.
165-
// Also trim the end of each actual line, to avoid trailing spaces being necessary in the metadata
166-
// or listed console output.
167-
actualLines = builder.ToString()
168-
.Replace("\r\n", "\n")
169-
.Split('\n')
170-
.Select(line => line.TrimEnd())
171-
.Where(line => line != "").ToList();
172-
}
173-
finally
174-
{
175-
Console.SetOut(oldOut);
176185
}
186+
177187
var expectedLines = Metadata.ExpectedOutput ?? new List<string>();
178188
return ValidateException(actualException, Metadata.ExpectedException) &&
179189
(Metadata.IgnoreOutput || ValidateExpectedAgainstActual("output", expectedLines, actualLines));

tools/ExampleTester/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.CommandLine;
33
using Utilities;
44

5-
var logger = new StatusCheckLogger("..", "Example tester");
5+
var logger = new StatusCheckLogger(Console.Out, "..", "Example tester");
66
var headSha = Environment.GetEnvironmentVariable("HEAD_SHA");
77
var token = Environment.GetEnvironmentVariable("GH_TOKEN");
88

tools/ExampleTester/TesterConfiguration.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@ namespace ExampleTester;
55

66
public record TesterConfiguration(
77
string ExtractedOutputDirectory,
8-
bool Quiet,
9-
string? SourceFile,
10-
string? ExampleName)
11-
{
12-
13-
}
8+
bool Quiet = false,
9+
string? SourceFile = null,
10+
string? ExampleName = null);
1411

1512
public class TesterConfigurationBinder : BinderBase<TesterConfiguration>
1613
{

tools/MarkdownConverter/Spec/Reporter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public Reporter() : this(null, null) { }
2828
public Reporter(Reporter? parent, string? filename)
2929
{
3030
// This is needed so that all Reporters share the same GitHub logger.
31-
this.githubLogger = parent?.githubLogger ?? new StatusCheckLogger("..", "Markdown to Word Converter");
31+
this.githubLogger = parent?.githubLogger ?? new StatusCheckLogger(Console.Out, "..", "Markdown to Word Converter");
3232
this.parent = parent;
3333
Location = new SourceLocation(filename, null, null, null);
3434
}

tools/StandardAnchorTags/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class Program
2525
/// <returns>0 on success, non-zero on failure</returns>
2626
static async Task<int> Main(string owner, string repo, bool dryrun =false)
2727
{
28-
var logger = new StatusCheckLogger("..", "TOC and Anchor updater");
28+
var logger = new StatusCheckLogger(Console.Out, "..", "TOC and Anchor updater");
2929
var headSha = Environment.GetEnvironmentVariable("HEAD_SHA");
3030
var token = Environment.GetEnvironmentVariable("GH_TOKEN");
3131
using FileStream openStream = File.OpenRead(FilesPath);

tools/Utilities/StatusCheckLogger.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ public record StatusCheckMessage(string file, int StartLine, int EndLine, string
2222
/// </remarks>
2323
/// <param name="pathToRoot">The path to the root of the repository</param>
2424
/// <param name="toolName">The name of the tool that is running the check</param>
25-
public class StatusCheckLogger(string pathToRoot, string toolName)
25+
public class StatusCheckLogger(TextWriter writer, string pathToRoot, string toolName)
2626
{
2727
private List<NewCheckRunAnnotation> annotations = [];
2828
public bool Success { get; private set; } = true;
2929

3030
// Utility method to format the path to unix style, from the root of the repository.
3131
private string FormatPath(string path) => Path.GetRelativePath(pathToRoot, path).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
3232

33-
private void WriteMessageToConsole(string prefix, StatusCheckMessage d) => Console.WriteLine($"{prefix}{toolName}-{d.Id}::file={FormatPath(d.file)},line={d.StartLine}::{d.Message}");
33+
private void WriteMessageToConsole(string prefix, StatusCheckMessage d) => writer.WriteLine($"{prefix}{toolName}-{d.Id}::file={FormatPath(d.file)},line={d.StartLine}::{d.Message}");
3434

3535
/// <summary>
3636
/// Log a notice from the status check to the console only
@@ -178,9 +178,9 @@ public async Task BuildCheckRunResult(string token, string owner, string repo, s
178178
// Once running on a branch on the dotnet org, this should work correctly.
179179
catch (ForbiddenException e)
180180
{
181-
Console.WriteLine("===== WARNING: Could not create a check run.=====");
182-
Console.WriteLine("Exception details:");
183-
Console.WriteLine(e);
181+
writer.WriteLine("===== WARNING: Could not create a check run.=====");
182+
writer.WriteLine("Exception details:");
183+
writer.WriteLine(e);
184184
}
185185
}
186186
}

0 commit comments

Comments
 (0)