|
| 1 | +/* Copyright 2025-present MongoDB Inc. |
| 2 | +* |
| 3 | +* Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +* you may not use this file except in compliance with the License. |
| 5 | +* You may obtain a copy of the License at |
| 6 | +* |
| 7 | +* http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +* |
| 9 | +* Unless required by applicable law or agreed to in writing, software |
| 10 | +* distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +* See the License for the specific language governing permissions and |
| 13 | +* limitations under the License. |
| 14 | +*/ |
| 15 | + |
| 16 | +using System; |
| 17 | +using System.Collections.Generic; |
| 18 | +using FluentAssertions; |
| 19 | +using MongoDB.Bson; |
| 20 | +using MongoDB.Bson.IO; |
| 21 | +using MongoDB.Driver.TestHelpers.Core; |
| 22 | +using MongoDB.TestHelpers.XunitExtensions; |
| 23 | +using Xunit.Sdk; |
| 24 | + |
| 25 | +namespace MongoDB.Driver.Tests.UnifiedTestOperations.Matchers |
| 26 | +{ |
| 27 | + public class UnifiedSpanMatcher |
| 28 | + { |
| 29 | + private readonly UnifiedValueMatcher _valueMatcher; |
| 30 | + |
| 31 | + public UnifiedSpanMatcher(UnifiedValueMatcher valueMatcher) |
| 32 | + { |
| 33 | + _valueMatcher = valueMatcher; |
| 34 | + } |
| 35 | + |
| 36 | + public void AssertSpansMatch(List<CapturedSpan> actualSpans, BsonArray expectedSpans, bool ignoreExtraSpans) |
| 37 | + { |
| 38 | + try |
| 39 | + { |
| 40 | + AssertSpans(actualSpans, expectedSpans, ignoreExtraSpans); |
| 41 | + } |
| 42 | + catch (XunitException exception) |
| 43 | + { |
| 44 | + throw new AssertionException( |
| 45 | + userMessage: GetAssertionErrorMessage(actualSpans, expectedSpans), |
| 46 | + innerException: exception); |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + private void AssertSpans(List<CapturedSpan> actualSpans, BsonArray expectedSpans, bool ignoreExtraSpans) |
| 51 | + { |
| 52 | + if (ignoreExtraSpans) |
| 53 | + { |
| 54 | + actualSpans.Count.Should().BeGreaterOrEqualTo(expectedSpans.Count); |
| 55 | + |
| 56 | + // When ignoring extra spans, find each expected span in order within the actual spans |
| 57 | + int actualIndex = 0; |
| 58 | + for (int expectedIndex = 0; expectedIndex < expectedSpans.Count; expectedIndex++) |
| 59 | + { |
| 60 | + var expectedSpan = expectedSpans[expectedIndex].AsBsonDocument; |
| 61 | + var expectedName = expectedSpan["name"].AsString; |
| 62 | + |
| 63 | + // Find the next actual span that matches this expected span's name |
| 64 | + bool found = false; |
| 65 | + while (actualIndex < actualSpans.Count) |
| 66 | + { |
| 67 | + var actualSpan = actualSpans[actualIndex]; |
| 68 | + if (actualSpan.Name == expectedName) |
| 69 | + { |
| 70 | + AssertSpan(actualSpan, expectedSpan); |
| 71 | + actualIndex++; |
| 72 | + found = true; |
| 73 | + break; |
| 74 | + } |
| 75 | + actualIndex++; |
| 76 | + } |
| 77 | + |
| 78 | + if (!found) |
| 79 | + { |
| 80 | + throw new AssertionException($"Expected span with name '{expectedName}' not found in actual spans starting from index {actualIndex}"); |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + else |
| 85 | + { |
| 86 | + actualSpans.Should().HaveSameCount(expectedSpans); |
| 87 | + |
| 88 | + for (int i = 0; i < expectedSpans.Count; i++) |
| 89 | + { |
| 90 | + var actualSpan = actualSpans[i]; |
| 91 | + var expectedSpan = expectedSpans[i].AsBsonDocument; |
| 92 | + |
| 93 | + AssertSpan(actualSpan, expectedSpan); |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private void AssertSpan(CapturedSpan actualSpan, BsonDocument expectedSpan) |
| 99 | + { |
| 100 | + foreach (var element in expectedSpan) |
| 101 | + { |
| 102 | + switch (element.Name) |
| 103 | + { |
| 104 | + case "name": |
| 105 | + actualSpan.Name.Should().Be(element.Value.AsString); |
| 106 | + break; |
| 107 | + case "attributes": |
| 108 | + AssertAttributes(actualSpan.Attributes, element.Value.AsBsonDocument); |
| 109 | + break; |
| 110 | + case "nested": |
| 111 | + AssertNestedSpans(actualSpan.NestedSpans, element.Value.AsBsonArray); |
| 112 | + break; |
| 113 | + default: |
| 114 | + throw new FormatException($"Unexpected span field: '{element.Name}'."); |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + private void AssertAttributes(Dictionary<string, object> actualAttributes, BsonDocument expectedAttributes) |
| 120 | + { |
| 121 | + foreach (var expectedAttribute in expectedAttributes) |
| 122 | + { |
| 123 | + var attributeName = expectedAttribute.Name; |
| 124 | + var expectedValue = expectedAttribute.Value; |
| 125 | + |
| 126 | + // Check if this is a $$exists matcher |
| 127 | + if (expectedValue.IsBsonDocument) |
| 128 | + { |
| 129 | + var expectedDoc = expectedValue.AsBsonDocument; |
| 130 | + if (expectedDoc.Contains("$$exists")) |
| 131 | + { |
| 132 | + var shouldExist = expectedDoc["$$exists"].AsBoolean; |
| 133 | + if (shouldExist) |
| 134 | + { |
| 135 | + actualAttributes.Should().ContainKey(attributeName, |
| 136 | + $"span should have attribute '{attributeName}'"); |
| 137 | + } |
| 138 | + else |
| 139 | + { |
| 140 | + actualAttributes.Should().NotContainKey(attributeName, |
| 141 | + $"span should not have attribute '{attributeName}'"); |
| 142 | + } |
| 143 | + continue; |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + actualAttributes.Should().ContainKey(attributeName, $"span should have attribute '{attributeName}'"); |
| 148 | + var actualValue = actualAttributes[attributeName]; |
| 149 | + |
| 150 | + // Convert the actual value to BsonValue |
| 151 | + var actualBsonValue = ConvertToBsonValue(actualValue); |
| 152 | + _valueMatcher.AssertValuesMatch(actualBsonValue, expectedValue); |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + private BsonValue ConvertToBsonValue(object value) |
| 157 | + { |
| 158 | + return value switch |
| 159 | + { |
| 160 | + null => BsonNull.Value, |
| 161 | + BsonValue bv => bv, // Already a BsonValue (including BsonDocument), return as-is |
| 162 | + string s => new BsonString(s), |
| 163 | + int i => new BsonInt32(i), |
| 164 | + long l => new BsonInt64(l), |
| 165 | + double d => new BsonDouble(d), |
| 166 | + bool b => new BsonBoolean(b), |
| 167 | + _ => throw new InvalidOperationException($"Unsupported span attribute type: {value.GetType().Name}") |
| 168 | + }; |
| 169 | + } |
| 170 | + |
| 171 | + private void AssertNestedSpans(List<CapturedSpan> actualNestedSpans, BsonArray expectedNestedSpans) |
| 172 | + { |
| 173 | + actualNestedSpans.Should().HaveSameCount(expectedNestedSpans, "nested spans count should match"); |
| 174 | + |
| 175 | + for (int i = 0; i < expectedNestedSpans.Count; i++) |
| 176 | + { |
| 177 | + AssertSpan(actualNestedSpans[i], expectedNestedSpans[i].AsBsonDocument); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + private string GetAssertionErrorMessage(List<CapturedSpan> actualSpans, BsonArray expectedSpans) |
| 182 | + { |
| 183 | + var jsonWriterSettings = new JsonWriterSettings { Indent = true }; |
| 184 | + |
| 185 | + var actualSpansDocuments = new BsonArray(); |
| 186 | + foreach (var actualSpan in actualSpans) |
| 187 | + { |
| 188 | + actualSpansDocuments.Add(ConvertSpanToBsonDocument(actualSpan)); |
| 189 | + } |
| 190 | + |
| 191 | + return |
| 192 | + $"Expected spans to be: {expectedSpans.ToJson(jsonWriterSettings)}{Environment.NewLine}" + |
| 193 | + $"But found: {actualSpansDocuments.ToJson(jsonWriterSettings)}."; |
| 194 | + } |
| 195 | + |
| 196 | + private BsonDocument ConvertSpanToBsonDocument(CapturedSpan span) |
| 197 | + { |
| 198 | + var spanDocument = new BsonDocument |
| 199 | + { |
| 200 | + { "name", span.Name }, |
| 201 | + { "status", span.StatusCode.ToString() } |
| 202 | + }; |
| 203 | + |
| 204 | + if (span.Attributes.Count > 0) |
| 205 | + { |
| 206 | + var attributesDocument = new BsonDocument(); |
| 207 | + foreach (var attribute in span.Attributes) |
| 208 | + { |
| 209 | + attributesDocument[attribute.Key] = ConvertToBsonValue(attribute.Value); |
| 210 | + } |
| 211 | + spanDocument["attributes"] = attributesDocument; |
| 212 | + } |
| 213 | + |
| 214 | + if (span.NestedSpans.Count > 0) |
| 215 | + { |
| 216 | + var nestedArray = new BsonArray(); |
| 217 | + foreach (var nestedSpan in span.NestedSpans) |
| 218 | + { |
| 219 | + nestedArray.Add(ConvertSpanToBsonDocument(nestedSpan)); |
| 220 | + } |
| 221 | + spanDocument["nested"] = nestedArray; |
| 222 | + } |
| 223 | + |
| 224 | + return spanDocument; |
| 225 | + } |
| 226 | + } |
| 227 | +} |
0 commit comments