-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathGeneratedExample.cs
More file actions
209 lines (192 loc) · 9.39 KB
/
GeneratedExample.cs
File metadata and controls
209 lines (192 loc) · 9.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
using ExampleExtractor;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.MSBuild;
using Newtonsoft.Json;
using System.Reflection;
using System.Text;
using Utilities;
namespace ExampleTester;
internal class GeneratedExample
{
private readonly string directory;
internal ExampleMetadata Metadata { get; }
private GeneratedExample(string directory)
{
this.directory = directory;
string metadataJson = File.ReadAllText(Path.Combine(directory, ExampleMetadata.MetadataFile));
Metadata = JsonConvert.DeserializeObject<ExampleMetadata>(metadataJson) ?? throw new ArgumentException($"Invalid (null) metadata in {directory}");
}
internal static List<GeneratedExample> LoadAllExamples(string parentDirectory) =>
Directory.GetDirectories(parentDirectory).Select(Load).ToList();
private static GeneratedExample Load(string directory)
{
return new GeneratedExample(directory);
}
internal async Task<bool> Test(TesterConfiguration configuration, StatusCheckLogger logger)
{
logger.ConsoleOnlyLog(Metadata.Source, Metadata.StartLine, Metadata.EndLine, $"Testing {Metadata.Name} from {Metadata.Source}", "ExampleTester");
// Explicitly do a release build, to avoid implicitly defining DEBUG.
var properties = new Dictionary<string, string> { { "Configuration", "Release" } };
using var workspace = MSBuildWorkspace.Create(properties);
// TODO: Validate this more cleanly.
var projectFile = Metadata.Project is string specifiedProject
? Path.Combine(directory, $"{specifiedProject}.csproj")
: Directory.GetFiles(directory, "*.csproj").Single();
var project = await workspace.OpenProjectAsync(projectFile);
var compilation = await project.GetCompilationAsync();
if (compilation is null)
{
throw new InvalidOperationException("Project has no Compilation");
}
bool ret = true;
ret &= ValidateDiagnostics("errors", DiagnosticSeverity.Error, Metadata.ExpectedErrors, logger);
ret &= ValidateDiagnostics("warnings", DiagnosticSeverity.Warning, Metadata.ExpectedWarnings, logger, Metadata.IgnoredWarnings);
// Don't try to validate output if we've already failed in terms of errors and warnings, or if we expect errors.
if (ret && Metadata.ExpectedErrors is null)
{
ret &= ValidateOutput();
}
return ret;
bool ValidateDiagnostics(string type, DiagnosticSeverity severity, List<string> expected, StatusCheckLogger logger, List<string>? ignored = null)
{
expected ??= new List<string>();
ignored ??= new List<string>();
var actualDiagnostics = compilation.GetDiagnostics()
.Where(d => d.Severity == severity)
.OrderBy(d => d.Location.GetLineSpan().StartLinePosition.Line)
.ThenBy(d => d.Id);
var actualIds = actualDiagnostics
.Select(d => d.Id)
.Where(id => !ignored.Contains(id))
.ToList();
bool ret = ValidateExpectedAgainstActual(type, expected, actualIds);
if (!ret)
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine, $" Details of actual {type}:", "ExampleTester");
foreach (var diagnostic in actualDiagnostics)
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine,
$" Line {diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1}: {diagnostic.Id}: {diagnostic.GetMessage()}",
"ExampleTester");
}
}
return ret;
}
bool ValidateOutput()
{
var entryPoint = compilation.GetEntryPoint(cancellationToken: default);
if (entryPoint is null)
{
if (Metadata.ExpectedOutput != null)
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine, " Output expected, but project has no entry point.", "ExampleTester");
return false;
}
return true;
}
string typeName = entryPoint.ContainingType.MetadataName;
if (entryPoint.ContainingNamespace?.MetadataName is string ns)
{
typeName = $"{ns}.{typeName}";
}
string methodName = entryPoint.MetadataName;
var ms = new MemoryStream();
var emitResult = compilation.Emit(ms);
if (!emitResult.Success)
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine, " Failed to emit assembly", "ExampleTester");
return false;
}
var generatedAssembly = Assembly.Load(ms.ToArray());
var type = generatedAssembly.GetType(typeName);
if (type is null)
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine, $" Failed to find entry point type {typeName}", "ExampleTester");
return false;
}
var method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
if (method is null)
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine, $" Failed to find entry point method {typeName}.{methodName}", "ExampleTester");
return false;
}
var arguments = method.GetParameters().Any()
? new object[] { Metadata.ExecutionArgs ?? new string[0] }
: new object[0];
var oldOut = Console.Out;
List<string> actualLines;
Exception? actualException = null;
try
{
var builder = new StringBuilder();
Console.SetOut(new StringWriter(builder));
try
{
var result = method.Invoke(null, arguments);
// For async Main methods, the compilation's entry point is still the Main
// method, so we explicitly wait for the returned task just like the synthesized
// entry point would.
if (result is Task task)
{
task.GetAwaiter().GetResult();
}
// For some reason, we don't *actually* get the result of all finalizers
// without this. We shouldn't need it (as relevant examples already have it) but
// code that works outside the test harness doesn't work inside it.
GC.Collect();
GC.WaitForPendingFinalizers();
}
catch (TargetInvocationException outer)
{
actualException = outer.InnerException ?? throw new InvalidOperationException("TargetInvocationException had no nested exception");
}
// Skip blank lines, to avoid unnecessary trailing empties.
// Also trim the end of each actual line, to avoid trailing spaces being necessary in the metadata
// or listed console output.
actualLines = builder.ToString()
.Replace("\r\n", "\n")
.Split('\n')
.Select(line => line.TrimEnd())
.Where(line => line != "").ToList();
}
finally
{
Console.SetOut(oldOut);
}
var expectedLines = Metadata.ExpectedOutput ?? new List<string>();
return ValidateException(actualException, Metadata.ExpectedException) &&
(Metadata.IgnoreOutput || ValidateExpectedAgainstActual("output", expectedLines, actualLines));
}
bool ValidateException(Exception? actualException, string? expectedExceptionName)
{
return (actualException, expectedExceptionName) switch
{
(null, null) => true,
(Exception ex, string name) =>
MaybeReportError(ex.GetType().Name == name, $" Mismatched exception type: Expected {name}; Was {ex.GetType().Name}"),
(null, string name) =>
MaybeReportError(false, $" Expected exception type {name}; no exception was thrown"),
(Exception ex, null) =>
MaybeReportError(false, $" Exception type {ex.GetType().Name} was thrown unexpectedly; Message: {ex.Message}")
};
bool MaybeReportError(bool result, string message)
{
if (!result)
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine, message, "ExampleTester");
}
return result;
}
}
bool ValidateExpectedAgainstActual(string type, List<string> expected, List<string> actual)
{
if (!expected.SequenceEqual(actual))
{
logger.LogFailure(Metadata.Source, Metadata.StartLine, Metadata.EndLine,
$" Mismatched {type}: Expected {string.Join(", ", expected)}; Was {string.Join(", ", actual)}", "ExampleTester");
return false;
}
return true;
}
}
}