< Summary

Class:Azure.Core.TestFramework.RecordMatcher
Assembly:Azure.Core.TestFramework
File(s):C:\Git\azure-sdk-for-net\sdk\core\Azure.Core.TestFramework\src\RecordMatcher.cs
Covered lines:137
Uncovered lines:21
Coverable lines:158
Total lines:338
Line coverage:86.7% (137 of 158)
Covered branches:65
Total branches:86
Branch coverage:75.5% (65 of 86)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.ctor(...)-100%100%
FindMatch(...)-84%87.5%
CompareBodies(...)-100%86.36%
IsEquivalentRecord(...)-0%0%
IsEquivalentRequest(...)-0%0%
AreUrisSame(...)-100%100%
NormalizeUri(...)-100%100%
IsEquivalentUri(...)-0%100%
IsEquivalentResponse(...)-0%0%
IsBodyEquivalent(...)-0%0%
GenerateException(...)-100%100%
JoinHeaderValues(...)-100%100%
RenormalizeSemicolons(...)-100%100%
CompareHeaderDictionaries(...)-100%100%
Equals(...)-0%0%
GetHashCode(...)-0%100%

File(s)

C:\Git\azure-sdk-for-net\sdk\core\Azure.Core.TestFramework\src\RecordMatcher.cs

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Collections.Specialized;
 7using System.Linq;
 8using System.Text;
 9using System.Web;
 10
 11namespace Azure.Core.TestFramework
 12{
 13    public class RecordMatcher
 14    {
 15        // Headers that are normalized by HttpClient
 139416        private HashSet<string> _normalizedHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 139417        {
 139418            "Accept",
 139419            "Content-Type"
 139420        };
 21
 22        private bool _compareBodies;
 23
 139424        public RecordMatcher(bool compareBodies = true)
 25        {
 139426            _compareBodies = compareBodies;
 139427        }
 28
 139429        public HashSet<string> ExcludeHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 139430        {
 139431            "Date",
 139432            "x-ms-date",
 139433            "x-ms-client-request-id",
 139434            "User-Agent",
 139435            "Request-Id",
 139436            "traceparent"
 139437        };
 38
 39        // Headers that don't indicate meaningful changes between updated recordings
 139440        public HashSet<string> VolatileHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 139441        {
 139442            "Date",
 139443            "x-ms-date",
 139444            "x-ms-client-request-id",
 139445            "User-Agent",
 139446            "Request-Id",
 139447            "If-Match",
 139448            "If-None-Match",
 139449            "If-Modified-Since",
 139450            "If-Unmodified-Since"
 139451        };
 52
 53        // Headers that don't indicate meaningful changes between updated recordings
 139454        public HashSet<string> VolatileResponseHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 139455        {
 139456            "Date",
 139457            "ETag",
 139458            "Last-Modified",
 139459            "x-ms-request-id",
 139460            "x-ms-correlation-request-id"
 139461        };
 62
 63        /// <summary>
 64        /// Query parameters whose values can change between recording and playback without causing URI matching
 65        /// to fail. The presence or absence of the query parameter itself is still respected in matching.
 66        /// </summary>
 139467        public HashSet<string> VolatileQueryParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 139468        {
 139469        };
 70
 71        private const string VolatileValue = "Volatile";
 72
 73        public virtual RecordEntry FindMatch(RecordEntry request, IList<RecordEntry> entries)
 74        {
 32640875            int bestScore = int.MaxValue;
 32640876            RecordEntry bestScoreEntry = null;
 77
 98418478            foreach (RecordEntry entry in entries)
 79            {
 32885480                int score = 0;
 81
 32885482                var uri = request.RequestUri;
 32885483                var recordRequestUri = entry.RequestUri;
 32885484                if (entry.IsTrack1Recording)
 85                {
 86                    //there's no domain name for request uri in track 1 record, so add it from reqeust uri
 087                    int len = 8; //length of "https://"
 088                    int domainEndingIndex = uri.IndexOf('/', len);
 089                    if (domainEndingIndex > 0)
 90                    {
 091                        recordRequestUri = uri.Substring(0, domainEndingIndex) + recordRequestUri;
 92                    }
 93                }
 94
 32885495                if (!AreUrisSame(recordRequestUri, uri))
 96                {
 246097                    score++;
 98                }
 99
 328854100                if (entry.RequestMethod != request.RequestMethod)
 101                {
 1434102                    score++;
 103                }
 104
 105                //we only check Uri + RequestMethod for track1 record
 328854106                if (!entry.IsTrack1Recording)
 107                {
 328854108                    score += CompareHeaderDictionaries(request.Request.Headers, entry.Request.Headers, ExcludeHeaders);
 328854109                    score += CompareBodies(request.Request.Body, entry.Request.Body);
 110                }
 111
 328854112                if (score == 0)
 113                {
 326340114                    return entry;
 115                }
 116
 2514117                if (score < bestScore)
 118                {
 774119                    bestScoreEntry = entry;
 774120                    bestScore = score;
 121                }
 122            }
 123
 68124            throw new InvalidOperationException(GenerateException(request, bestScoreEntry));
 326340125        }
 126
 127        private int CompareBodies(byte[] requestBody, byte[] responseBody, StringBuilder descriptionBuilder = null)
 128        {
 328918129            if (!_compareBodies)
 130            {
 43726131                return 0;
 132            }
 133
 285192134            if (requestBody == null && responseBody == null)
 135            {
 274934136                return 0;
 137            }
 138
 10258139            if (requestBody == null)
 140            {
 4141                descriptionBuilder?.AppendLine("Request has body but response doesn't");
 4142                return 1;
 143            }
 144
 10254145            if (responseBody == null)
 146            {
 622147                descriptionBuilder?.AppendLine("Response has body but request doesn't");
 622148                return 1;
 149            }
 150
 9632151            if (!requestBody.SequenceEqual(responseBody))
 152            {
 12153                if (descriptionBuilder != null)
 154                {
 6155                    var minLength = Math.Min(requestBody.Length, responseBody.Length);
 156                    int i;
 5592157                    for (i = 0; i < minLength - 1; i++)
 158                    {
 2794159                        if (requestBody[i] != responseBody[i])
 160                        {
 161                            break;
 162                        }
 163                    }
 6164                    descriptionBuilder.AppendLine($"Request and response bodies do not match at index {i}:");
 6165                    var before = Math.Max(0, i - 10);
 6166                    var afterRequest = Math.Min(i + 20, requestBody.Length);
 6167                    var afterResponse = Math.Min(i + 20, responseBody.Length);
 6168                    descriptionBuilder.AppendLine($"     request: \"{Encoding.UTF8.GetString(requestBody, before, afterR
 6169                    descriptionBuilder.AppendLine($"     record:  \"{Encoding.UTF8.GetString(responseBody, before, after
 170                }
 12171                return 1;
 172            }
 173
 9620174            return 0;
 175        }
 176
 177        public virtual bool IsEquivalentRecord(RecordEntry entry, RecordEntry otherEntry) =>
 0178            IsEquivalentRequest(entry, otherEntry) &&
 0179            IsEquivalentResponse(entry, otherEntry);
 180
 181        protected virtual bool IsEquivalentRequest(RecordEntry entry, RecordEntry otherEntry) =>
 0182            entry.RequestMethod == otherEntry.RequestMethod &&
 0183            IsEquivalentUri(entry.RequestUri, otherEntry.RequestUri) &&
 0184            CompareHeaderDictionaries(entry.Request.Headers, otherEntry.Request.Headers, VolatileHeaders) == 0;
 185
 186        private bool AreUrisSame(string entryUri, string otherEntryUri) =>
 328918187            NormalizeUri(entryUri) == NormalizeUri(otherEntryUri);
 188
 189        private string NormalizeUri(string uriToNormalize)
 190        {
 657836191            var req = new RequestUriBuilder();
 657836192            var uri = new Uri(uriToNormalize);
 657836193            req.Reset(uri);
 657836194            req.Query = "";
 657836195            NameValueCollection queryParams = HttpUtility.ParseQueryString(uri.Query);
 2701140196            foreach (string param in queryParams)
 197            {
 693070198                req.AppendQuery(
 693070199                    param,
 693070200                    VolatileQueryParameters.Contains(param) ? VolatileValue : queryParams[param]);
 201            }
 657836202            return req.ToUri().ToString();
 203        }
 204
 205        protected virtual bool IsEquivalentUri(string entryUri, string otherEntryUri) =>
 0206            AreUrisSame(entryUri, otherEntryUri);
 207
 208        protected virtual bool IsEquivalentResponse(RecordEntry entry, RecordEntry otherEntry)
 209        {
 0210            IEnumerable<KeyValuePair<string, string[]>> entryHeaders = entry.Response.Headers.Where(h => !VolatileRespon
 0211            IEnumerable<KeyValuePair<string, string[]>> otherEntryHeaders = otherEntry.Response.Headers.Where(h => !Vola
 212
 0213            return
 0214                entry.StatusCode == otherEntry.StatusCode &&
 0215                entryHeaders.SequenceEqual(otherEntryHeaders, new HeaderComparer()) &&
 0216                IsBodyEquivalent(entry, otherEntry);
 217        }
 218
 219        protected virtual bool IsBodyEquivalent(RecordEntry record, RecordEntry otherRecord)
 220        {
 0221            return (record.Response.Body ?? Array.Empty<byte>()).AsSpan()
 0222                .SequenceEqual((otherRecord.Response.Body ?? Array.Empty<byte>()));
 223        }
 224
 225        private string GenerateException(RecordEntry request, RecordEntry bestScoreEntry)
 226        {
 68227            StringBuilder builder = new StringBuilder();
 68228            builder.AppendLine($"Unable to find a record for the request {request.RequestMethod} {request.RequestUri}");
 229
 68230            if (bestScoreEntry == null)
 231            {
 4232                builder.AppendLine("No records to match.");
 4233                return builder.ToString();
 234            }
 235
 64236            if (request.RequestMethod != bestScoreEntry.RequestMethod)
 237            {
 2238                builder.AppendLine($"Method doesn't match, request <{request.RequestMethod}> record <{bestScoreEntry.Req
 239            }
 240
 64241            if (!AreUrisSame(request.RequestUri, bestScoreEntry.RequestUri))
 242            {
 16243                builder.AppendLine("Uri doesn't match:");
 16244                builder.AppendLine($"    request <{request.RequestUri}>");
 16245                builder.AppendLine($"    record  <{bestScoreEntry.RequestUri}>");
 246            }
 247
 64248            builder.AppendLine("Header differences:");
 249
 64250            CompareHeaderDictionaries(request.Request.Headers, bestScoreEntry.Request.Headers, ExcludeHeaders, builder);
 251
 64252            builder.AppendLine("Body differences:");
 253
 64254            CompareBodies(request.Request.Body, bestScoreEntry.Request.Body, builder);
 255
 64256            return builder.ToString();
 257        }
 258
 259        private string JoinHeaderValues(string[] values)
 260        {
 68261            return string.Join(",", values);
 262        }
 263
 264        private string[] RenormalizeSemicolons(string[] values)
 265        {
 46036266            string[] outputs = new string[values.Length];
 184144267            for (int i = 0; i < values.Length; i++)
 268            {
 98048269                outputs[i] = string.Join("; ", values[i].Split(';').Select(part => part.Trim()));
 270            }
 271
 46036272            return outputs;
 273        }
 274
 275        private int CompareHeaderDictionaries(SortedDictionary<string, string[]> headers, SortedDictionary<string, strin
 276        {
 328918277            int difference = 0;
 328918278            var remaining = new SortedDictionary<string, string[]>(entryHeaders, entryHeaders.Comparer);
 3794100279            foreach (KeyValuePair<string, string[]> header in headers)
 280            {
 1568132281                var requestHeaderValues = header.Value;
 1568132282                var headerName = header.Key;
 283
 1568132284                if (ignoredHeaders.Contains(headerName))
 285                {
 286                    continue;
 287                }
 288
 793354289                if (remaining.TryGetValue(headerName, out string[] entryHeaderValues))
 290                {
 291                    // Content-Type, Accept headers are normalized by HttpClient, re-normalize them before comparing
 791122292                    if (_normalizedHeaders.Contains(headerName))
 293                    {
 23018294                        requestHeaderValues = RenormalizeSemicolons(requestHeaderValues);
 23018295                        entryHeaderValues = RenormalizeSemicolons(entryHeaderValues);
 296                    }
 297
 791122298                    remaining.Remove(headerName);
 791122299                    if (!entryHeaderValues.SequenceEqual(requestHeaderValues))
 300                    {
 34301                        difference++;
 34302                        descriptionBuilder?.AppendLine($"    <{headerName}> values differ, request <{JoinHeaderValues(re
 303                    }
 304                }
 305                else
 306                {
 2232307                    difference++;
 2232308                    descriptionBuilder?.AppendLine($"    <{headerName}> is absent in record, value <{JoinHeaderValues(re
 309                }
 310            }
 311
 2208216312            foreach (KeyValuePair<string, string[]> header in remaining)
 313            {
 775190314                if (!ignoredHeaders.Contains(header.Key))
 315                {
 1512316                    difference++;
 1512317                    descriptionBuilder?.AppendLine($"    <{header.Key}> is absent in request, value <{JoinHeaderValues(h
 318                }
 319            }
 320
 328918321            return difference;
 322        }
 323
 324        private class HeaderComparer : IEqualityComparer<KeyValuePair<string, string[]>>
 325        {
 326            public bool Equals(KeyValuePair<string, string[]> x, KeyValuePair<string, string[]> y)
 327            {
 0328                return x.Key.Equals(y.Key, StringComparison.OrdinalIgnoreCase) &&
 0329                       x.Value.SequenceEqual(y.Value);
 330            }
 331
 332            public int GetHashCode(KeyValuePair<string, string[]> obj)
 333            {
 0334                return obj.GetHashCode();
 335            }
 336        }
 337    }
 338}