-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathCheckCommandPipelineTests.cs
More file actions
371 lines (327 loc) · 15.4 KB
/
CheckCommandPipelineTests.cs
File metadata and controls
371 lines (327 loc) · 15.4 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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// Pipeline-level tests for `mur check`. We exercise the parsing + emission +
// trace plumbing without spinning up `dotnet build`. Spec 038 §0.5 + §0.3.
using System.Text.Json;
using Microsoft.UI.Reactor.Cli.Check;
using Microsoft.UI.Reactor.Cli.Check.Ranker;
using Xunit;
namespace Microsoft.UI.Reactor.Tests.CheckCommandTests;
public class CheckCommandPipelineTests
{
const string SampleMsBuildOutput = """
Determining projects to restore...
Build started 2026-05-09 12:00:00.
Program.cs(34,16): error CS1061: 'ButtonElement' does not contain a definition for 'OnClick' [C:\src\Foo\Foo.csproj]
Program.cs(34,16): error CS1061: 'ButtonElement' does not contain a definition for 'OnClick' [C:\src\Foo\Foo.csproj]
Program.cs(40,8): warning CS8602: Dereference of a possibly null reference. [C:\src\Foo\Foo.csproj]
Build FAILED.
""";
[Fact]
public void Parses_msbuild_lines_into_diag_records()
{
var diags = CheckCommand.ParseDiagnostics(SampleMsBuildOutput);
Assert.Equal(3, diags.Count);
Assert.Equal("CS1061", diags[0].Code);
Assert.Equal("error", diags[0].Severity);
Assert.Equal(34, diags[0].Line);
Assert.Equal(16, diags[0].Col);
Assert.Contains("OnClick", diags[0].Message);
Assert.Equal("CS8602", diags[2].Code);
Assert.Equal("warning", diags[2].Severity);
}
[Fact]
public void Parses_msbuild_lines_with_parens_in_file_path()
{
// Reluctant file capture must still anchor on (line,col): even when
// the path itself contains parentheses (agent/temp dirs with labels).
const string output = """
C:\src\Reactor (test)\Program.cs(10,5): error CS1061: 'X' does not contain a definition for 'Y' [C:\src\Foo.csproj]
""";
var diags = CheckCommand.ParseDiagnostics(output);
Assert.Single(diags);
Assert.Equal(@"C:\src\Reactor (test)\Program.cs", diags[0].File);
Assert.Equal(10, diags[0].Line);
Assert.Equal(5, diags[0].Col);
Assert.Equal("CS1061", diags[0].Code);
}
[Fact]
public void Emit_dedupes_repeated_diagnostics()
{
var diags = CheckCommand.ParseDiagnostics(SampleMsBuildOutput);
var sw = new StringWriter();
CheckCommand.EmitDiagnostics(diags, sw, trace: null);
var lines = sw.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(2, lines.Length); // 3 diags - 1 dup
}
[Fact]
public void Emit_with_trace_writes_one_jsonl_row_per_unique_diagnostic()
{
var diags = CheckCommand.ParseDiagnostics(SampleMsBuildOutput);
var stdout = new StringWriter();
var tracePath = Path.Combine(Path.GetTempPath(), "mur-check-pipeline-" + Guid.NewGuid() + ".jsonl");
try
{
using (var trace = TraceWriter.Open(tracePath, Path.GetFullPath(".")))
{
CheckCommand.EmitDiagnostics(diags, stdout, trace);
}
var traceLines = File.ReadAllLines(tracePath);
Assert.Equal(2, traceLines.Length); // dedupe matches stdout
foreach (var line in traceLines)
{
using var doc = JsonDocument.Parse(line);
Assert.True(doc.RootElement.TryGetProperty("ts", out _));
Assert.True(doc.RootElement.TryGetProperty("code", out _));
Assert.True(doc.RootElement.TryGetProperty("severity", out _));
Assert.True(doc.RootElement.TryGetProperty("file", out _));
Assert.True(doc.RootElement.TryGetProperty("line", out _));
Assert.True(doc.RootElement.TryGetProperty("col", out _));
Assert.True(doc.RootElement.TryGetProperty("msg", out _));
Assert.True(doc.RootElement.TryGetProperty("mode", out _));
Assert.Equal("iteration", doc.RootElement.GetProperty("mode").GetString());
}
// Trace is *in addition to* stdout — both populated.
Assert.NotEmpty(stdout.ToString());
}
finally
{
try { File.Delete(tracePath); } catch { }
}
}
[Fact]
public void Gate_suppresses_suggestions_when_cs_count_below_threshold()
{
// Single CS diagnostic, threshold 3 → gate closed.
var diags = CheckCommand.ParseDiagnostics(
"Program.cs(10,5): error CS1061: 'Foo' does not contain a definition for 'Bar' [Foo.csproj]");
Assert.False(CheckCommand.ShouldEmitSuggestions(diags, threshold: 3));
}
[Fact]
public void Gate_opens_when_cs_count_meets_threshold()
{
var diags = CheckCommand.ParseDiagnostics("""
A.cs(1,1): error CS1061: a [P.csproj]
B.cs(2,2): error CS0103: b [P.csproj]
C.cs(3,3): error CS0117: c [P.csproj]
""");
Assert.True(CheckCommand.ShouldEmitSuggestions(diags, threshold: 3));
}
[Fact]
public void Gate_threshold_zero_always_opens()
{
var diags = CheckCommand.ParseDiagnostics(
"Program.cs(10,5): error CS1061: 'Foo' does not contain a definition for 'Bar' [Foo.csproj]");
Assert.True(CheckCommand.ShouldEmitSuggestions(diags, threshold: 0));
Assert.True(CheckCommand.ShouldEmitSuggestions(Array.Empty<CheckCommand.Diag>(), threshold: 0));
}
[Fact]
public void Gate_only_counts_cs_prefixed_codes()
{
// Two REACTOR_* hits + one CS hit. Threshold 2 should NOT open — the
// REACTOR_* diagnostics are Tier-1 territory (static hint table) and
// don't pay for Tier-2 setup.
var diags = CheckCommand.ParseDiagnostics("""
App.cs(1,1): warning REACTOR_HOOKS_001: bad [P.csproj]
App.cs(2,1): warning REACTOR_HOOKS_004: bad [P.csproj]
App.cs(3,1): error CS1061: 'Foo' has no member [P.csproj]
""");
Assert.False(CheckCommand.ShouldEmitSuggestions(diags, threshold: 2));
}
[Fact]
public void Gate_dedupes_repeated_diagnostics_when_counting()
{
// MSBuild often prints the same diagnostic twice (per project). The
// gate uses the same dedup key EmitDiagnostics uses, so a dup pair
// does NOT push count over the threshold.
var diags = CheckCommand.ParseDiagnostics("""
X.cs(1,1): error CS1061: a [P.csproj]
X.cs(1,1): error CS1061: a [P.csproj]
Y.cs(2,2): error CS0103: b [P.csproj]
""");
Assert.False(CheckCommand.ShouldEmitSuggestions(diags, threshold: 3));
Assert.True(CheckCommand.ShouldEmitSuggestions(diags, threshold: 2));
}
[Fact]
public void Default_threshold_matches_documented_value()
{
// Lock the constant into a test so the EC1-tuned default can only
// change with an intentional code edit (and a failing test the author
// has to update).
Assert.Equal(3, CheckCommand.DefaultSuggestThreshold);
}
[Fact]
public void Ranker_filter_suppresses_stdout_but_trace_still_records_every_row()
{
// Spec 038 §8: ranker filters stdout; trace receives every parsed
// diagnostic so the suppressed-then-resurfaced telemetry hook
// (failure-mode mitigation) can mine the full stream.
var diags = CheckCommand.ParseDiagnostics("""
X.cs(1,1): error CS1061: missing member [P.csproj]
X.cs(2,2): warning CS1591: missing xml doc [P.csproj]
X.cs(3,3): warning IDE0001: simplify [P.csproj]
""");
var iterCtx = new RankerContext(Mode.Iteration, null);
var stdout = new StringWriter();
var tracePath = Path.Combine(Path.GetTempPath(), "mur-check-ranker-" + Guid.NewGuid() + ".jsonl");
try
{
using (var trace = TraceWriter.Open(tracePath, Path.GetFullPath(".")))
{
CheckCommand.EmitDiagnostics(diags, stdout, trace,
suggest: null,
stdoutFilter: d => Ranker.ShouldEmit(d, iterCtx));
}
// stdout: only the CS1061 error (CS1591 + IDE0001 are 0.0 score).
var stdoutLines = stdout.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
Assert.Single(stdoutLines);
Assert.Contains("CS1061", stdoutLines[0]);
// trace: all three deduped rows.
var traceLines = File.ReadAllLines(tracePath);
Assert.Equal(3, traceLines.Length);
}
finally { try { File.Delete(tracePath); } catch { } }
}
[Fact]
public void Emit_writes_rule_fired_trace_event_when_rule_suggestion_attached()
{
// Spec 038 EC3-final watch-item: rule fires must show up in the trace
// as a structured `rule_fired` row so per-rule firing-rate audits are
// a 1-line grep. End-to-end through EmitDiagnostics: when the suggest
// callback returns a rule-flagged Suggestion, the trace file gains a
// rule_fired row alongside the diagnostic row.
var diags = CheckCommand.ParseDiagnostics("""
X.cs(1,1): error CS1955: Non-invocable member [P.csproj]
""");
var stdout = new StringWriter();
var tracePath = Path.Combine(Path.GetTempPath(), "mur-check-rulefire-" + Guid.NewGuid() + ".jsonl");
try
{
using (var trace = TraceWriter.Open(tracePath, Path.GetFullPath(".")))
{
CheckCommand.EmitDiagnostics(diags, stdout, trace,
suggest: _ => new Suggestion(
Text: ".Auto (drop parens)",
Confidence: 0.95,
Evidence: "GridSize.Auto is a property (cluster:C0004)",
SuggesterName: "GridSizeFactoryParensRule",
IsRule: true));
}
var lines = File.ReadAllLines(tracePath);
Assert.Equal(2, lines.Length); // 1 diag row + 1 rule_fired row
// Find the rule_fired row and detach its element so the parsed
// JsonDocument can be disposed inside the loop (Copilot CR: avoid
// leaking pooled buffers across the assertion block).
JsonElement ruleFired = default;
var found = false;
foreach (var line in lines)
{
using var doc = JsonDocument.Parse(line);
if (doc.RootElement.TryGetProperty("kind", out var k) &&
k.GetString() == "rule_fired")
{
ruleFired = doc.RootElement.Clone();
found = true;
break;
}
}
Assert.True(found, "trace did not contain a rule_fired row.");
Assert.Equal("GridSizeFactoryParensRule", ruleFired.GetProperty("rule").GetString());
Assert.Equal("CS1955", ruleFired.GetProperty("code").GetString());
Assert.Equal(0.95, ruleFired.GetProperty("confidence").GetDouble());
Assert.Equal("X.cs", ruleFired.GetProperty("file").GetString());
Assert.Equal(1, ruleFired.GetProperty("line").GetInt32());
// The stdout line still carries the → try: suffix — trace event
// is in addition to, not in place of.
Assert.Contains(".Auto (drop parens)", stdout.ToString());
}
finally { try { File.Delete(tracePath); } catch { } }
}
[Fact]
public void Emit_does_not_write_rule_fired_for_tier2_suggestions()
{
// Tier-2 firing rates are visible via the opt-in telemetry channel;
// the trace's rule_fired row is Tier-3-only by design. A Tier-2 hit
// here must leave no rule_fired marker in the trace.
var diags = CheckCommand.ParseDiagnostics("""
X.cs(1,1): error CS1061: missing member [P.csproj]
""");
var stdout = new StringWriter();
var tracePath = Path.Combine(Path.GetTempPath(), "mur-check-tier2-" + Guid.NewGuid() + ".jsonl");
try
{
using (var trace = TraceWriter.Open(tracePath, Path.GetFullPath(".")))
{
CheckCommand.EmitDiagnostics(diags, stdout, trace,
suggest: _ => new Suggestion(
Text: "Button(label, onClick: x)",
Confidence: 0.91,
Evidence: "factory has Action onClick parameter",
SuggesterName: "SymbolSuggester")); // IsRule default = false
}
var lines = File.ReadAllLines(tracePath);
// Exactly one row — the diagnostic — and it is NOT a rule_fired row.
Assert.Single(lines);
using var doc = JsonDocument.Parse(lines[0]);
var hasKind = doc.RootElement.TryGetProperty("kind", out var k);
if (hasKind)
Assert.NotEqual("rule_fired", k.GetString());
}
finally { try { File.Delete(tracePath); } catch { } }
}
[Fact]
public void Ranker_final_mode_emits_everything_to_stdout()
{
var diags = CheckCommand.ParseDiagnostics("""
X.cs(1,1): error CS1061: missing member [P.csproj]
X.cs(2,2): warning CS1591: missing xml doc [P.csproj]
X.cs(3,3): warning IDE0001: simplify [P.csproj]
""");
var finalCtx = new RankerContext(Mode.Final, null);
var stdout = new StringWriter();
CheckCommand.EmitDiagnostics(diags, stdout, trace: null,
suggest: null,
stdoutFilter: d => Ranker.ShouldEmit(d, finalCtx));
var lines = stdout.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(3, lines.Length);
}
[Fact]
public void Trace_command_row_records_full_effective_dotnet_build_args()
{
// Spec §8 "Tracing": replays need the exact effective command line.
var tracePath = Path.Combine(Path.GetTempPath(), "mur-check-cmd-" + Guid.NewGuid() + ".jsonl");
try
{
using (var trace = TraceWriter.Open(tracePath, Path.GetFullPath(".")))
{
trace.WriteCommand(new[] { "build", "./app", "--nologo", "-v:m", "-p:Platform=x64" }, "iteration");
}
var line = File.ReadAllLines(tracePath).Single();
using var doc = JsonDocument.Parse(line);
Assert.Equal("command", doc.RootElement.GetProperty("kind").GetString());
var argv = doc.RootElement.GetProperty("argv").EnumerateArray()
.Select(e => e.GetString()!).ToArray();
Assert.Equal("dotnet", argv[0]);
Assert.Contains("build", argv);
Assert.Contains("-p:Platform=x64", argv);
Assert.Equal("iteration", doc.RootElement.GetProperty("mode").GetString());
}
finally { try { File.Delete(tracePath); } catch { } }
}
[Fact]
public void Trace_row_is_under_2KB_for_realistic_msbuild_output()
{
var diags = CheckCommand.ParseDiagnostics(SampleMsBuildOutput);
var stdout = new StringWriter();
var tracePath = Path.Combine(Path.GetTempPath(), "mur-check-pipeline-len-" + Guid.NewGuid() + ".jsonl");
try
{
using (var trace = TraceWriter.Open(tracePath, Path.GetFullPath(".")))
CheckCommand.EmitDiagnostics(diags, stdout, trace);
foreach (var line in File.ReadAllLines(tracePath))
Assert.True(line.Length <= 2048, $"trace row {line.Length} bytes exceeds 2 KB cap.");
}
finally
{
try { File.Delete(tracePath); } catch { }
}
}
}