Skip to content

Commit 8d2522c

Browse files
committed
Extend LineRewriter to tests
1 parent 532b7e6 commit 8d2522c

File tree

6 files changed

+96
-55
lines changed

6 files changed

+96
-55
lines changed

src/Tools/Common/Commands/Utils.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Collections.Generic;
77
using Microsoft.Diagnostics.NETCore.Client;
8+
using Microsoft.Diagnostics.Tools.Common;
89

910
namespace Microsoft.Internal.Common.Utils
1011
{
@@ -147,7 +148,12 @@ internal sealed class LineRewriter
147148
{
148149
public int LineToClear { get; set; }
149150

150-
public LineRewriter() { }
151+
private IConsole Console { get; }
152+
153+
public LineRewriter(IConsole console)
154+
{
155+
Console = console;
156+
}
151157

152158
// ANSI escape codes:
153159
// [2K => clear current line

src/Tools/Common/DefaultConsole.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public DefaultConsole(bool useAnsi)
2929
public bool CursorVisible { get => Console.CursorVisible; set { Console.CursorVisible = value; } }
3030
#pragma warning restore CA1416
3131

32+
public int CursorLeft => Console.CursorLeft;
33+
3234
public int CursorTop => Console.CursorTop;
3335

3436
public int BufferWidth => Console.BufferWidth;

src/Tools/Common/IConsole.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal interface IConsole
1515
int WindowHeight { get; }
1616
int WindowWidth { get; }
1717
bool CursorVisible { get; set; }
18+
int CursorLeft { get; }
1819
int CursorTop { get; }
1920
int BufferWidth { get; }
2021
int BufferHeight { get; }

src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public CollectCommandHandler()
2929
StartTraceSessionAsync = async (client, config, ct) => new CollectSession(await client.StartEventPipeSessionAsync(config, ct).ConfigureAwait(false));
3030
ResumeRuntimeAsync = (client, ct) => client.ResumeRuntimeAsync(ct);
3131
CollectSessionEventStream = name => new FileStream(name, FileMode.Create, FileAccess.Write);
32-
IsOutputRedirected = Console.IsOutputRedirected;
3332
}
3433

3534
private void ConsoleWriteLine(string str)
@@ -89,7 +88,7 @@ internal async Task<int> Collect(CancellationToken ct, CommandLineConfiguration
8988
{
9089
cancelOnCtrlC = true;
9190
cancelOnEnter = !Console.IsInputRedirected;
92-
printStatusOverTime = !IsOutputRedirected;
91+
printStatusOverTime = !Console.IsOutputRedirected;
9392
}
9493

9594
if (!cancelOnCtrlC)
@@ -378,7 +377,6 @@ internal async Task<int> Collect(CancellationToken ct, CommandLineConfiguration
378377

379378
ConsoleWriteLine("\n\n");
380379

381-
FileInfo fileInfo = new(output.FullName);
382380
EventMonitor eventMonitor = null;
383381
Task copyTask = null;
384382
if (hasStoppingEventProviderName)
@@ -410,23 +408,35 @@ internal async Task<int> Collect(CancellationToken ct, CommandLineConfiguration
410408

411409
if (printStatusOverTime)
412410
{
413-
if (CursorOperationsSupported)
414-
{
415-
rewriter = new LineRewriter { LineToClear = Console.CursorTop - 1 };
411+
rewriter = new LineRewriter(Console) { LineToClear = Console.CursorTop - 1 };
412+
try {
416413
Console.CursorVisible = false;
417414
}
418-
if (rewriter == null || !rewriter.IsRewriteConsoleLineSupported)
415+
catch {}
416+
if (!rewriter.IsRewriteConsoleLineSupported)
419417
{
420418
ConsoleWriteLine("Recording trace in progress. Press <Enter> or <Ctrl+C> to exit...");
421419
}
422420
}
423421

422+
FileInfo fileInfo = null;
423+
if (FileSizeForStatus == 0)
424+
{
425+
fileInfo = new(output.FullName);
426+
}
424427
Action printStatus = () => {
425-
if (printStatusOverTime && rewriter != null && rewriter.IsRewriteConsoleLineSupported)
428+
if (printStatusOverTime && rewriter.IsRewriteConsoleLineSupported)
426429
{
427-
rewriter.RewriteConsoleLine();
428-
fileInfo.Refresh();
429-
ConsoleWriteLine($"[{stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace {GetSize(fileInfo.Length)}");
430+
rewriter?.RewriteConsoleLine();
431+
if (fileInfo != null)
432+
{
433+
fileInfo.Refresh();
434+
ConsoleWriteLine($"[{stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace {GetSize(fileInfo.Length)}");
435+
}
436+
else
437+
{
438+
ConsoleWriteLine($"[{stopwatch.Elapsed:dd\\:hh\\:mm\\:ss}]\tRecording trace {GetSize(FileSizeForStatus)}");
439+
}
430440
ConsoleWriteLine("Press <Enter> or <Ctrl+C> to exit...");
431441
}
432442

@@ -516,11 +526,15 @@ internal async Task<int> Collect(CancellationToken ct, CommandLineConfiguration
516526
}
517527
finally
518528
{
519-
if (printStatusOverTime && CursorOperationsSupported)
529+
if (printStatusOverTime)
520530
{
521531
if (!Console.IsOutputRedirected)
522532
{
523-
Console.CursorVisible = true;
533+
try
534+
{
535+
Console.CursorVisible = true;
536+
}
537+
catch { }
524538
}
525539
}
526540

@@ -750,10 +764,9 @@ private sealed class CollectSession : ICollectSession
750764

751765
internal Func<DiagnosticsClient, EventPipeSessionConfiguration, CancellationToken, Task<ICollectSession>> StartTraceSessionAsync { get; set; }
752766
internal Func<DiagnosticsClient, CancellationToken, Task> ResumeRuntimeAsync { get; set; }
753-
internal bool CursorOperationsSupported { get; set; } = true;
754767
internal Func<string, Stream> CollectSessionEventStream { get; set; }
755768
internal IConsole Console { get; set; }
756-
internal bool IsOutputRedirected { get; set; }
769+
internal long FileSizeForStatus { get; set; }
757770
#endregion
758771
}
759772
}

src/tests/Common/MockConsole.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ public MockConsole(int width, int height)
3131

3232
public bool CursorVisible { get => throw new NotSupportedException(); set => throw new NotImplementedException(); }
3333

34+
public int CursorLeft { get; private set; }
35+
3436
public int CursorTop { get; private set; }
3537

3638
public int BufferWidth { get; private set; }
3739

3840
public int BufferHeight { get; private set; }
3941

40-
public bool IsOutputRedirected { get; private set; }
42+
public bool IsOutputRedirected { get; set; }
4143

4244
public bool IsInputRedirected { get; private set; }
4345

src/tests/dotnet-trace/CollectCommandFunctionalTests.cs

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public class CollectCommandFunctionalTests
1919
private const string ExpectedPayload = "CollectCommandFunctionalTestsTraceData";
2020

2121
public sealed record CollectArgs(
22-
FileInfo output,
2322
CancellationToken ct = default,
2423
CommandLineConfiguration cliConfig = null,
2524
int processId = -1,
@@ -41,8 +40,8 @@ public sealed record CollectArgs(
4140
string dsrouter = "")
4241
{
4342
internal TraceFileFormat Format => (TraceFileFormat)formatValue;
44-
public string OutputFilePath => output.FullName;
4543
public int ProcessId => processId == -1 ? Environment.ProcessId : processId;
44+
public FileInfo Output => new FileInfo("trace.nettrace");
4645
private MemoryStream _eventStream = new();
4746
public MemoryStream EventStream => _eventStream;
4847
}
@@ -51,7 +50,7 @@ public sealed record CollectArgs(
5150
[MemberData(nameof(BasicCases))]
5251
public async Task CollectCommandProviderConfigurationConsolidation(CollectArgs args, string[] expectedSubset)
5352
{
54-
MockConsole console = new(200, 30);
53+
MockConsole console = new(200, 30) { IsOutputRedirected = false };
5554
string[] rawLines = await RunAsync(args, console).ConfigureAwait(true);
5655
console.AssertSanitizedLinesEqual(CollectSanitizer, expectedSubset);
5756

@@ -64,16 +63,15 @@ private static async Task<string[]> RunAsync(CollectArgs config, MockConsole con
6463
var handler = new CollectCommandHandler();
6564
handler.StartTraceSessionAsync = (client, cfg, ct) => Task.FromResult<CollectCommandHandler.ICollectSession>(new TestCollectSession());
6665
handler.ResumeRuntimeAsync = (client, ct) => Task.CompletedTask;
67-
handler.CursorOperationsSupported = false;
68-
handler.CollectSessionEventStream = (name) => config.EventStream;
66+
handler.CollectSessionEventStream = (name) => new SlowSinkStream(config.EventStream);
6967
handler.Console = console;
70-
handler.IsOutputRedirected = false;
68+
handler.FileSizeForStatus = Encoding.UTF8.GetByteCount(ExpectedPayload);
7169

7270
int exit = await handler.Collect(
7371
config.ct,
7472
config.cliConfig,
7573
config.ProcessId,
76-
config.output,
74+
config.Output,
7775
config.buffersize,
7876
config.providers,
7977
config.profile,
@@ -98,6 +96,32 @@ private static async Task<string[]> RunAsync(CollectArgs config, MockConsole con
9896
return console.Lines;
9997
}
10098

99+
// As the test payload is small, we need to delay writes to the output stream to ensure
100+
// that the status update logic in CollectCommandHandler has a chance to run.
101+
private sealed class SlowSinkStream : Stream
102+
{
103+
private readonly Stream _inner;
104+
105+
public SlowSinkStream(Stream inner) { _inner = inner; }
106+
107+
public override bool CanRead => false;
108+
public override bool CanSeek => false;
109+
public override bool CanWrite => true;
110+
public override long Length => throw new NotSupportedException();
111+
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
112+
113+
public override void Flush() { }
114+
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
115+
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
116+
public override void SetLength(long value) => throw new NotSupportedException();
117+
public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
118+
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
119+
{
120+
await Task.Delay(200, cancellationToken).ConfigureAwait(false);
121+
await _inner.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
122+
}
123+
}
124+
101125
private static string[] CollectSanitizer(string[] lines)
102126
{
103127
List<string> result = new();
@@ -107,6 +131,10 @@ private static string[] CollectSanitizer(string[] lines)
107131
{
108132
result.Add("Process : <PROCESS>");
109133
}
134+
else if (line.StartsWith('[') && line.Contains("Recording trace"))
135+
{
136+
result.Add("[dd:hh:mm:ss]\t" + line.Substring(14));
137+
}
110138
else
111139
{
112140
result.Add(line);
@@ -126,11 +154,11 @@ public void Stop() {}
126154

127155
public static IEnumerable<object[]> BasicCases()
128156
{
157+
FileInfo fi = new("trace.nettrace");
129158
yield return new object[]
130159
{
131-
new CollectArgs(output: new FileInfo("noProviders")),
160+
new CollectArgs(),
132161
ExpectProvidersWithMessages(
133-
"noProviders",
134162
new[]
135163
{
136164
"No profile or providers specified, defaulting to trace profile 'cpu-sampling'"
@@ -141,45 +169,40 @@ public static IEnumerable<object[]> BasicCases()
141169

142170
yield return new object[]
143171
{
144-
new CollectArgs(output: new FileInfo("singleProvider"), providers: "Foo:0x1:4"),
172+
new CollectArgs(providers: "Foo:0x1:4"),
145173
ExpectProviders(
146-
"singleProvider",
147174
FormatProvider("Foo", "0000000000000001", "Informational", 4, "--providers"))
148175
};
149176

150177
yield return new object[]
151178
{
152-
new CollectArgs(output: new FileInfo("multipleProviders"), providers: "Foo:0x1:4,Bar:0x2:4"),
179+
new CollectArgs(providers: "Foo:0x1:4,Bar:0x2:4"),
153180
ExpectProviders(
154-
"multipleProviders",
155181
FormatProvider("Foo", "0000000000000001", "Informational", 4, "--providers"),
156182
FormatProvider("Bar", "0000000000000002", "Informational", 4, "--providers"))
157183
};
158184

159185
yield return new object[]
160186
{
161-
new CollectArgs(output: new FileInfo("profileOnly"), profile: "cpu-sampling"),
187+
new CollectArgs(profile: "cpu-sampling"),
162188
ExpectProviders(
163-
"profileOnly",
164189
FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile"),
165190
FormatProvider("Microsoft-Windows-DotNETRuntime", "00000014C14FCCBD", "Informational", 4, "--profile"))
166191
};
167192

168193
yield return new object[]
169194
{
170-
new CollectArgs(output: new FileInfo("profileAndProviders"), profile: "cpu-sampling", providers: "Foo:0x1:4"),
195+
new CollectArgs(profile: "cpu-sampling", providers: "Foo:0x1:4"),
171196
ExpectProviders(
172-
"profileAndProviders",
173197
FormatProvider("Foo", "0000000000000001", "Informational", 4, "--providers"),
174198
FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile"),
175199
FormatProvider("Microsoft-Windows-DotNETRuntime", "00000014C14FCCBD", "Informational", 4, "--profile"))
176200
};
177201

178202
yield return new object[]
179203
{
180-
new CollectArgs(output: new FileInfo("profileAndClrEventsIgnored"), profile: "cpu-sampling", clrevents: "gc"),
204+
new CollectArgs(profile: "cpu-sampling", clrevents: "gc"),
181205
ExpectProvidersWithMessages(
182-
"profileAndClrEventsIgnored",
183206
new[]
184207
{
185208
"The argument --clrevents gc will be ignored because the CLR provider was configured via either --profile or --providers command."
@@ -190,18 +213,16 @@ public static IEnumerable<object[]> BasicCases()
190213

191214
yield return new object[]
192215
{
193-
new CollectArgs(output: new FileInfo("profileOverriddenRuntime"), profile: "cpu-sampling", providers: "Microsoft-Windows-DotNETRuntime:0x1:4"),
216+
new CollectArgs(profile: "cpu-sampling", providers: "Microsoft-Windows-DotNETRuntime:0x1:4"),
194217
ExpectProviders(
195-
"profileOverriddenRuntime",
196218
FormatProvider("Microsoft-Windows-DotNETRuntime", "0000000000000001", "Informational", 4, "--providers"),
197219
FormatProvider("Microsoft-DotNETCore-SampleProfiler", "0000F00000000000", "Informational", 4, "--profile"))
198220
};
199221

200222
yield return new object[]
201223
{
202-
new CollectArgs(output: new FileInfo("providersClrEventsIgnored"), providers: "Microsoft-Windows-DotNETRuntime:0x1:4", clrevents: "gc"),
224+
new CollectArgs(providers: "Microsoft-Windows-DotNETRuntime:0x1:4", clrevents: "gc"),
203225
ExpectProvidersWithMessages(
204-
"providersClrEventsIgnored",
205226
new[]
206227
{
207228
"The argument --clrevents gc will be ignored because the CLR provider was configured via either --profile or --providers command."
@@ -211,38 +232,34 @@ public static IEnumerable<object[]> BasicCases()
211232

212233
yield return new object[]
213234
{
214-
new CollectArgs(output: new FileInfo("clrEventsOnly"), clrevents: "gc+jit"),
235+
new CollectArgs(clrevents: "gc+jit"),
215236
ExpectProviders(
216-
"clrEventsOnly",
217237
FormatProvider("Microsoft-Windows-DotNETRuntime", "0000000000000011", "Informational", 4, "--clrevents"))
218238
};
219239

220240
yield return new object[]
221241
{
222-
new CollectArgs(output: new FileInfo("clrEventsVerbose"), clrevents: "gc+jit", clreventlevel: "5"),
242+
new CollectArgs(clrevents: "gc+jit", clreventlevel: "5"),
223243
ExpectProviders(
224-
"clrEventsVerbose",
225244
FormatProvider("Microsoft-Windows-DotNETRuntime", "0000000000000011", "Verbose", 5, "--clrevents"))
226245
};
227246
}
228247

229-
private const string ProcessPlaceHolder = "Process : <PROCESS>";
230-
private static string outputPrefix = $"Output File : {Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar}";
248+
private static string outputFile = $"Output File : {Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar}trace.nettrace";
231249
private const string ProviderHeader = "Provider Name Keywords Level Enabled By";
232250
private static readonly string[] CommonTail = [
233-
"",
234-
"Recording trace in progress. Press <Enter> or <Ctrl+C> to exit...",
251+
"Process : <PROCESS>",
252+
outputFile,
253+
"[dd:hh:mm:ss]\tRecording trace 38.00 (B)",
254+
"Press <Enter> or <Ctrl+C> to exit...",
235255
"\nTrace completed."
236256
];
237257

238-
private static string[] Expect(string outputFile, params string[] lines)
239-
=> [.. lines, ProcessPlaceHolder, outputPrefix + outputFile, .. CommonTail];
240-
241-
private static string[] ExpectProviders(string outputFile, params string[] providerLines)
242-
=> Expect(outputFile, ["", ProviderHeader, .. providerLines, ""]);
258+
private static string[] ExpectProviders(params string[] providerLines)
259+
=> ExpectProvidersWithMessages(new string[0], providerLines);
243260

244-
private static string[] ExpectProvidersWithMessages(string outputFile, string[] messages, params string[] providerLines)
245-
=> Expect(outputFile, [.. messages, "", ProviderHeader, .. providerLines, ""]);
261+
private static string[] ExpectProvidersWithMessages(string[] messages, params string[] providerLines)
262+
=> [.. messages, "", ProviderHeader, .. providerLines, "", .. CommonTail];
246263

247264
private static string FormatProvider(string name, string keywordsHex, string levelName, int levelValue, string enabledBy)
248265
{

0 commit comments

Comments
 (0)