Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Cli.Commands.Test.IPC.Models;

internal sealed record TestInProgressMessage(string? Uid, string? DisplayName);

internal sealed record TestInProgressMessages(string? ExecutionId, string? InstanceId, TestInProgressMessage[] InProgressMessages) : IRequest;
15 changes: 15 additions & 0 deletions src/Cli/dotnet/Commands/Test/MTP/IPC/ObjectFieldIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,18 @@ internal static class HandshakeMessageFieldsId
{
public const int MessagesSerializerId = 9;
}

internal static class TestInProgressMessagesFieldsId
{
public const int MessagesSerializerId = 10;

public const ushort ExecutionId = 1;
public const ushort InstanceId = 2;
public const ushort TestInProgressMessageList = 3;
}

internal static class TestInProgressMessageFieldsId
{
public const ushort Uid = 1;
public const ushort DisplayName = 2;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace Microsoft.DotNet.Cli.Commands.Test.IPC.Serializers;
* FileArtifactMessageSerializer: 7
* TestSessionEventSerializer: 8
* HandshakeMessageSerializer: 9
* TestInProgressMessagesSerializer: 10
*/

internal static class RegisterSerializers
Expand All @@ -31,5 +32,6 @@ public static void RegisterAllSerializers(this NamedPipeBase namedPipeBase)
namedPipeBase.RegisterSerializer(new FileArtifactMessagesSerializer(), typeof(FileArtifactMessages));
namedPipeBase.RegisterSerializer(new TestSessionEventSerializer(), typeof(TestSessionEvent));
namedPipeBase.RegisterSerializer(new HandshakeMessageSerializer(), typeof(HandshakeMessage));
namedPipeBase.RegisterSerializer(new TestInProgressMessagesSerializer(), typeof(TestInProgressMessages));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.DotNet.Cli.Commands.Test.IPC.Models;

namespace Microsoft.DotNet.Cli.Commands.Test.IPC.Serializers;

/*
|---FieldCount---| 2 bytes

|---ExecutionId Id---| (2 bytes)
|---ExecutionId Size---| (4 bytes)
|---ExecutionId Value---| (n bytes)

|---InstanceId Id---| (2 bytes)
|---InstanceId Size---| (4 bytes)
|---InstanceId Value---| (n bytes)

|---TestInProgressMessageList Id---| (2 bytes)
|---TestInProgressMessageList Size---| (4 bytes)
|---TestInProgressMessageList Value---| (n bytes)
|---TestInProgressMessageList Length---| (4 bytes)

|---TestInProgressMessageList[0] FieldCount---| 2 bytes

|---TestInProgressMessageList[0].Uid Id---| (2 bytes)
|---TestInProgressMessageList[0].Uid Size---| (4 bytes)
|---TestInProgressMessageList[0].Uid Value---| (n bytes)

|---TestInProgressMessageList[0].DisplayName Id---| (2 bytes)
|---TestInProgressMessageList[0].DisplayName Size---| (4 bytes)
|---TestInProgressMessageList[0].DisplayName Value---| (n bytes)
*/

internal sealed class TestInProgressMessagesSerializer : BaseSerializer, INamedPipeSerializer
{
public int Id => TestInProgressMessagesFieldsId.MessagesSerializerId;

public object Deserialize(Stream stream)
{
string? executionId = null;
string? instanceId = null;
List<TestInProgressMessage>? inProgressMessages = null;

ushort fieldCount = ReadUShort(stream);

for (int i = 0; i < fieldCount; i++)
{
int fieldId = ReadUShort(stream);
int fieldSize = ReadInt(stream);

switch (fieldId)
{
case TestInProgressMessagesFieldsId.ExecutionId:
executionId = ReadStringValue(stream, fieldSize);
break;

case TestInProgressMessagesFieldsId.InstanceId:
instanceId = ReadStringValue(stream, fieldSize);
break;

case TestInProgressMessagesFieldsId.TestInProgressMessageList:
inProgressMessages = ReadInProgressMessagesPayload(stream);
break;

default:
// If we don't recognize the field id, skip the payload corresponding to that field
SetPosition(stream, stream.Position + fieldSize);
break;
}
}

return new TestInProgressMessages(executionId, instanceId, inProgressMessages is null ? [] : [.. inProgressMessages]);
}

private static List<TestInProgressMessage> ReadInProgressMessagesPayload(Stream stream)
{
List<TestInProgressMessage> inProgressMessages = [];

int length = ReadInt(stream);
for (int i = 0; i < length; i++)
{
string? uid = null, displayName = null;

int fieldCount = ReadUShort(stream);

for (int j = 0; j < fieldCount; j++)
{
int fieldId = ReadUShort(stream);
int fieldSize = ReadInt(stream);

switch (fieldId)
{
case TestInProgressMessageFieldsId.Uid:
uid = ReadStringValue(stream, fieldSize);
break;

case TestInProgressMessageFieldsId.DisplayName:
displayName = ReadStringValue(stream, fieldSize);
break;

default:
SetPosition(stream, stream.Position + fieldSize);
break;
}
}

inProgressMessages.Add(new TestInProgressMessage(uid, displayName));
}

return inProgressMessages;
}

public void Serialize(object objectToSerialize, Stream stream)
{
Debug.Assert(stream.CanSeek, "We expect a seekable stream.");

var inProgressMessages = (TestInProgressMessages)objectToSerialize;

WriteUShort(stream, GetFieldCount(inProgressMessages));

WriteField(stream, TestInProgressMessagesFieldsId.ExecutionId, inProgressMessages.ExecutionId);
WriteField(stream, TestInProgressMessagesFieldsId.InstanceId, inProgressMessages.InstanceId);
WriteInProgressMessagesPayload(stream, inProgressMessages.InProgressMessages);
}

private static void WriteInProgressMessagesPayload(Stream stream, TestInProgressMessage[]? inProgressMessageList)
{
if (inProgressMessageList is null || inProgressMessageList.Length == 0)
{
return;
}

WriteUShort(stream, TestInProgressMessagesFieldsId.TestInProgressMessageList);

// We will reserve an int (4 bytes)
// so that we fill the size later, once we write the payload
WriteInt(stream, 0);

long before = stream.Position;
WriteInt(stream, inProgressMessageList.Length);
foreach (TestInProgressMessage inProgressMessage in inProgressMessageList)
{
WriteUShort(stream, GetFieldCount(inProgressMessage));

WriteField(stream, TestInProgressMessageFieldsId.Uid, inProgressMessage.Uid);
WriteField(stream, TestInProgressMessageFieldsId.DisplayName, inProgressMessage.DisplayName);
}

// NOTE: We are able to seek only if we are using a MemoryStream
// thus, the seek operation is fast as we are only changing the value of a property
WriteAtPosition(stream, (int)(stream.Position - before), before - sizeof(int));
}

private static ushort GetFieldCount(TestInProgressMessages inProgressMessages) =>
(ushort)((inProgressMessages.ExecutionId is null ? 0 : 1) +
(inProgressMessages.InstanceId is null ? 0 : 1) +
(IsNullOrEmpty(inProgressMessages.InProgressMessages) ? 0 : 1));

private static ushort GetFieldCount(TestInProgressMessage inProgressMessage) =>
(ushort)((inProgressMessage.Uid is null ? 0 : 1) +
(inProgressMessage.DisplayName is null ? 0 : 1));
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ private static TerminalTestReporter InitializeOutput(int degreeOfParallelism, Pa
{
ShowPassedTests = showPassedTests,
ShowProgress = !noProgress,
ShowActiveTests = !noProgress && ansiMode == AnsiMode.AnsiIfPossible,
Comment thread
Evangelink marked this conversation as resolved.
AnsiMode = ansiMode,
ShowAssembly = true,
ShowAssemblyStartAndComplete = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ internal sealed partial class TerminalTestReporter : IDisposable

private readonly TestProgressStateAwareTerminal _terminalWithProgress;

/// <summary>
/// Whether to track and render currently running tests. Gated on both the caller-requested
/// <see cref="TerminalTestReporterOptions.ShowActiveTests"/> and the effective progress
/// capability of the console: if progress cannot actually be rendered (e.g. redirected stdout,
/// non-TTY, or ANSI not supported) there is no point allocating per-test running-state.
/// </summary>
private readonly bool _showActiveTests;

private int _handshakeFailuresCount;

private readonly uint? _originalConsoleMode;
Expand Down Expand Up @@ -94,6 +102,7 @@ public TerminalTestReporter(IConsole console, TerminalTestReporterOptions option
}

_terminalWithProgress = new TestProgressStateAwareTerminal(terminal, showProgress);
_showActiveTests = _options.ShowActiveTests && showProgress;
}

public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery, bool isHelp, bool isRetry)
Expand Down Expand Up @@ -407,9 +416,9 @@ internal void TestCompleted(
TestProgressState asm = _assemblies[executionId];
var attempt = asm.TryCount;

if (_options.ShowActiveTests)
if (_showActiveTests)
{
asm.TestNodeResultsState?.RemoveRunningTestNode(testNodeUid);
asm.TestNodeResultsState?.RemoveRunningTestNode(instanceId, testNodeUid);
}

switch (outcome)
Expand Down Expand Up @@ -950,17 +959,21 @@ private static TerminalColor ToTerminalColor(ConsoleColor consoleColor)
};

public void TestInProgress(
string assembly,
string? targetFramework,
string? architecture,
string executionId,
string instanceId,
string testNodeUid,
string displayName,
string executionId)
string displayName)
{
TestProgressState asm = _assemblies[executionId];

if (_options.ShowActiveTests)
if (_showActiveTests)
{
asm.TestNodeResultsState ??= new(Interlocked.Increment(ref _counter));
asm.TestNodeResultsState.AddRunningTestNode(
Interlocked.Increment(ref _counter), testNodeUid, displayName, CreateStopwatch());
Interlocked.Increment(ref _counter), instanceId, testNodeUid, displayName, CreateStopwatch());
}

_terminalWithProgress.UpdateWorker(asm.SlotIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,33 @@ internal sealed class TestNodeResultsState(long id)

private readonly TestDetailState _summaryDetail = new(id, stopwatch: null, text: string.Empty);
private readonly ConcurrentDictionary<string, TestDetailState> _testNodeProgressStates = new();
private readonly ConcurrentDictionary<string, byte> _completed = new();

public int Count => _testNodeProgressStates.Count;

public void AddRunningTestNode(int id, string uid, string name, IStopwatch stopwatch) => _testNodeProgressStates[uid] = new TestDetailState(id, stopwatch, name);
public void AddRunningTestNode(int id, string instanceId, string uid, string name, IStopwatch stopwatch)
{
string key = MakeKey(instanceId, uid);

// Guard against stale "in-progress" notifications that arrive after the
// test already completed. Without this we could surface a "running"
// entry that will never be removed.
if (_completed.ContainsKey(key))
{
return;
}

_testNodeProgressStates[key] = new TestDetailState(id, stopwatch, name);
}

public void RemoveRunningTestNode(string instanceId, string uid)
{
string key = MakeKey(instanceId, uid);
_completed[key] = 0;
_testNodeProgressStates.TryRemove(key, out _);
}

public void RemoveRunningTestNode(string uid) => _testNodeProgressStates.TryRemove(uid, out _);
private static string MakeKey(string instanceId, string uid) => $"{instanceId}\u0000{uid}";

public IEnumerable<TestDetailState> GetRunningTasks(int maxCount)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Cli/dotnet/Commands/Test/MTP/TestApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ private Task<IResponse> OnRequest(NamedPipeServer server, IRequest request)
OnFileArtifactMessages(fileArtifactMessages);
break;

case TestInProgressMessages testInProgressMessages:
OnTestInProgressMessages(testInProgressMessages);
break;

case TestSessionEvent sessionEvent:
OnSessionEvent(sessionEvent);
break;
Expand Down Expand Up @@ -375,6 +379,9 @@ private void OnTestResultMessages(TestResultMessages testResultMessage)
private void OnFileArtifactMessages(FileArtifactMessages fileArtifactMessages)
=> _handler.OnFileArtifactsReceived(fileArtifactMessages);

private void OnTestInProgressMessages(TestInProgressMessages testInProgressMessages)
=> _handler.OnTestInProgressReceived(testInProgressMessages);

private void OnSessionEvent(TestSessionEvent sessionEvent)
=> _handler.OnSessionEventReceived(sessionEvent);

Expand Down
Loading
Loading