File: Model\SpanWaterfallViewModelTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Tests.Shared.Telemetry;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Model;
 
public sealed class SpanWaterfallViewModelTests
{
    [Fact]
    public void Create_HasChildren_ChildrenPopulated()
    {
        // Arrange
        var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
        var app1 = new OtlpApplication("app1", "instance", context);
        var app2 = new OtlpApplication("app2", "instance", context);
 
        var trace = new OtlpTrace(new byte[] { 1, 2, 3 });
        var scope = new OtlpScope(TelemetryTestHelpers.CreateScope(), context);
        trace.AddSpan(TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: "1", parentSpanId: null, startDate: new DateTime(2001, 1, 1, 1, 1, 2, DateTimeKind.Utc)));
        trace.AddSpan(TelemetryTestHelpers.CreateOtlpSpan(app2, trace, scope, spanId: "1-1", parentSpanId: "1", startDate: new DateTime(2001, 1, 1, 1, 1, 3, DateTimeKind.Utc)));
 
        // Act
        var vm = SpanWaterfallViewModel.Create(trace, new SpanWaterfallViewModel.TraceDetailState([], []));
 
        // Assert
        Assert.Collection(vm,
            e =>
            {
                Assert.Equal("1", e.Span.SpanId);
                Assert.Equal("1-1", Assert.Single(e.Children).Span.SpanId);
            },
            e =>
            {
                Assert.Equal("1-1", e.Span.SpanId);
                Assert.Empty(e.Children);
            });
    }
 
    [Theory]
    [InlineData("1234", true)]  // Matches span ID
    [InlineData("app1", true)]  // Matches application name
    [InlineData("Test", true)]  // Matches display summary
    [InlineData("peer-service", true)]  // Matches uninstrumented peer
    [InlineData("nonexistent", false)]  // Doesn't match anything
    public void MatchesFilter_VariousCases_ReturnsExpected(string filter, bool expected)
    {
        // Arrange
        var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
        var app = new OtlpApplication("app1", "instance", context);
        var trace = new OtlpTrace(new byte[] { 1, 2, 3 });
        var scope = new OtlpScope(TelemetryTestHelpers.CreateScope(), context);
 
        // Create a span with an attribute that simulates uninstrumented peer
        var attributes = new[]
        {
            new KeyValuePair<string, string>("peer.service", "peer-service")
        };
 
        var span = TelemetryTestHelpers.CreateOtlpSpan(
            app,
            trace,
            scope,
            spanId: "12345",
            parentSpanId: null,
            startDate: new DateTime(2001, 1, 1, 1, 1, 2, DateTimeKind.Utc),
            attributes: attributes,
            statusCode: OtlpSpanStatusCode.Unset,
            statusMessage: null,
            kind: OtlpSpanKind.Client);
 
        trace.AddSpan(span);
 
        var vm = SpanWaterfallViewModel.Create(
            trace,
            new SpanWaterfallViewModel.TraceDetailState(
                [new TestPeerResolver()],
                []
            )).First();
 
        // Act
        var result = vm.MatchesFilter(filter, a => a.Application.ApplicationName, out _);
 
        // Assert
        Assert.Equal(expected, result);
    }
 
    [Fact]
    public void MatchesFilter_ParentSpanIncludedWhenChildMatched()
    {
        // Arrange
        var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
        var app1 = new OtlpApplication("app1", "instance", context);
        var trace = new OtlpTrace(new byte[] { 1, 2, 3 });
        var scope = new OtlpScope(TelemetryTestHelpers.CreateScope(), context);
        var parentSpan = TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: "parent", parentSpanId: null, startDate: new DateTime(2001, 1, 1, 1, 1, 2, DateTimeKind.Utc));
        var childSpan = TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: "child", parentSpanId: "parent", startDate: new DateTime(2001, 1, 1, 1, 1, 3, DateTimeKind.Utc));
        trace.AddSpan(parentSpan);
        trace.AddSpan(childSpan);
 
        var vms = SpanWaterfallViewModel.Create(trace, new SpanWaterfallViewModel.TraceDetailState([], []));
        var parent = vms[0];
        var child = vms[1];
 
        // Act and assert
        Assert.True(parent.MatchesFilter("child", a => a.Application.ApplicationName, out _));
        Assert.True(child.MatchesFilter("child", a => a.Application.ApplicationName, out _));
    }
 
    [Fact]
    public void MatchesFilter_ChildSpanIncludedWhenParentMatched()
    {
        // Arrange
        var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
        var app1 = new OtlpApplication("app1", "instance", context);
        var trace = new OtlpTrace(new byte[] { 1, 2, 3 });
        var scope = new OtlpScope(TelemetryTestHelpers.CreateScope(), context);
        var parentSpan = TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: "parent", parentSpanId: null, startDate: new DateTime(2001, 1, 1, 1, 1, 2, DateTimeKind.Utc));
        var childSpan = TelemetryTestHelpers.CreateOtlpSpan(app1, trace, scope, spanId: "child", parentSpanId: "parent", startDate: new DateTime(2001, 1, 1, 1, 1, 3, DateTimeKind.Utc));
        trace.AddSpan(parentSpan);
        trace.AddSpan(childSpan);
 
        var vms = SpanWaterfallViewModel.Create(trace, new SpanWaterfallViewModel.TraceDetailState([], []));
        var parent = vms[0];
        var child = vms[1];
 
        // Act and assert
        Assert.True(parent.MatchesFilter("parent", a => a.Application.ApplicationName, out var descendents));
        Assert.Equal("child", Assert.Single(descendents).Span.SpanId);
        Assert.False(child.MatchesFilter("parent", a => a.Application.ApplicationName, out _));
    }
 
    private sealed class TestPeerResolver : IOutgoingPeerResolver
    {
        public bool TryResolvePeerName(KeyValuePair<string, string>[] attributes, out string? name)
        {
            var peerService = attributes.FirstOrDefault(a => a.Key == "peer.service");
            if (!string.IsNullOrEmpty(peerService.Value))
            {
                name = peerService.Value;
                return true;
            }
 
            name = null;
            return false;
        }
 
        public IDisposable OnPeerChanges(Func<Task> callback)
        {
            return EmptyDisposable.Instance;
        }
    }
 
    private sealed class EmptyDisposable : IDisposable
    {
        public static EmptyDisposable Instance { get; } = new();
 
        public void Dispose()
        {
        }
    }
}