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
Expand Up @@ -138,8 +138,35 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo

string uid = update.TestNode.Uid;
string displayName = update.TestNode.DisplayName;
string fullyQualifiedName = GetFullyQualifiedName(update.TestNode);
TimeSpan duration = GetDuration(update.TestNode);

// Single-pass collection of TimingProperty and the FQN SerializableKeyValuePairStringProperty:
// replaces 1 × SingleOrDefault<TimingProperty>() + 1 × OfType<>().FirstOrDefault() with one
// GetStructEnumerator() walk, saving 1 linked-list traversal and 1 LINQ allocation per terminal result.
// Singleton-typed properties use the local GetSingleOrDefaultValue helper to preserve the
// throw-on-duplicate invariant that SingleOrDefault<T>() provided; the FQN key keeps the
// prior FirstOrDefault semantics (first match wins) so we don't silently overwrite earlier values.
TimingProperty? timing = null;
string? fqnValue = null;
PropertyBag.PropertyBagEnumerator enumerator = update.TestNode.Properties.GetStructEnumerator();
while (enumerator.MoveNext())
{
switch (enumerator.Current)
{
case TimingProperty t: timing = GetSingleOrDefaultValue(timing, t); break;
case SerializableKeyValuePairStringProperty kv when kv.Key == FullyQualifiedNamePropertyKey && fqnValue is null:
fqnValue = kv.Value;
break;
}
}

static TProperty GetSingleOrDefaultValue<TProperty>(TProperty? existingProperty, TProperty property)
where TProperty : class, IProperty
=> existingProperty is not null
? throw new InvalidOperationException($"Found multiple properties of type '{typeof(TProperty)}'.")
: property;

string fullyQualifiedName = fqnValue ?? displayName;
TimeSpan duration = timing?.GlobalTiming.Duration ?? TimeSpan.Zero;

lock (_stateLock)
{
Expand Down Expand Up @@ -410,18 +437,6 @@ private static string EscapeCell(string value)
return sb.ToString();
}

private static string GetFullyQualifiedName(TestNode testNode)
=> testNode.Properties
.OfType<SerializableKeyValuePairStringProperty>()
.FirstOrDefault(static property => property.Key == FullyQualifiedNamePropertyKey)?.Value
?? testNode.DisplayName;

private static TimeSpan GetDuration(TestNode testNode)
{
TimingProperty? timing = testNode.Properties.SingleOrDefault<TimingProperty>();
return timing?.GlobalTiming.Duration ?? TimeSpan.Zero;
}

private static TerminalKind GetTerminalKind(TestNodeStateProperty? state)
=> state switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,47 @@ or CancelledTestNodeStateProperty
private static IReadOnlyList<AzureDevOpsTestResultAttachment> BuildAttachmentsFromTestNode(TestNode testNode)
{
List<AzureDevOpsTestResultAttachment>? attachments = null;

foreach (FileArtifactProperty fileArtifact in testNode.Properties.OfType<FileArtifactProperty>())
{
string? fullPath;
try
{
fullPath = fileArtifact.FileInfo.FullName;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException or PathTooLongException)
StandardOutputProperty? stdout = null;
StandardErrorProperty? stderr = null;

// Single-pass collection: replaces 1 × OfType<FileArtifactProperty>() loop + 2 × SingleOrDefault<T>()
// with one GetStructEnumerator() walk, saving 2 linked-list traversals + 1 LINQ allocation per failure.
// Singleton-typed properties (stdout/stderr) use the local GetSingleOrDefaultValue helper to preserve
// the throw-on-duplicate invariant that SingleOrDefault<T>() provided; FileArtifactProperty is
// intentionally multi-valued and accumulates into a list.
PropertyBag.PropertyBagEnumerator enumerator = testNode.Properties.GetStructEnumerator();
while (enumerator.MoveNext())
{
switch (enumerator.Current)
{
continue;
case FileArtifactProperty fileArtifact:
string? fullPath;
try
{
fullPath = fileArtifact.FileInfo.FullName;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException or PathTooLongException)
{
break;
}

attachments ??= [];
attachments.Add(AzureDevOpsTestResultAttachment.FromFile(
fullPath,
AzureDevOpsAttachmentTypes.GeneralAttachment,
comment: fileArtifact.Description ?? fileArtifact.DisplayName));
break;
case StandardOutputProperty so: stdout = GetSingleOrDefaultValue(stdout, so); break;
case StandardErrorProperty se: stderr = GetSingleOrDefaultValue(stderr, se); break;
}

attachments ??= [];
attachments.Add(AzureDevOpsTestResultAttachment.FromFile(
fullPath,
AzureDevOpsAttachmentTypes.GeneralAttachment,
comment: fileArtifact.Description ?? fileArtifact.DisplayName));
}

StandardOutputProperty? stdout = testNode.Properties.SingleOrDefault<StandardOutputProperty>();
static TProperty GetSingleOrDefaultValue<TProperty>(TProperty? existingProperty, TProperty property)
where TProperty : class, IProperty
=> existingProperty is not null
? throw new InvalidOperationException($"Found multiple properties of type '{typeof(TProperty)}'.")
: property;

if (stdout is not null && !RoslynString.IsNullOrEmpty(stdout.StandardOutput))
{
attachments ??= [];
Expand All @@ -89,7 +109,6 @@ private static IReadOnlyList<AzureDevOpsTestResultAttachment> BuildAttachmentsFr
AzureDevOpsAttachmentTypes.ConsoleLog));
}

StandardErrorProperty? stderr = testNode.Properties.SingleOrDefault<StandardErrorProperty>();
if (stderr is not null && !RoslynString.IsNullOrEmpty(stderr.StandardError))
{
attachments ??= [];
Expand Down
Loading