123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- // 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 System.Linq;
- using Microsoft.AspNetCore.StaticWebAssets.Tasks;
- using Microsoft.NET.Sdk.StaticWebAssets.Tasks;
- namespace Microsoft.NET.Sdk.Razor.Tests;
- public class StaticWebAssetsBaselineComparer
- {
- private static readonly string BaselineGenerationInstructions =
- @"If the difference in baselines is expected, please re-generate the baselines.
- Start by ensuring you're dogfooding the SDK from the current branch (dotnet --version should be '*.0.0-dev').
- If you're not on the dogfood sdk, from the root of the repository run:
- 1. dotnet clean
- 2. .\restore.cmd or ./restore.sh
- 3. .\build.cmd ./build.sh
- 4. .\eng\dogfood.cmd or . ./eng/dogfood.sh
- Then, using the dogfood SDK run the .\src\RazorSdk\update-test-baselines.ps1 script.";
- public static StaticWebAssetsBaselineComparer Instance { get; } = new();
- internal void AssertManifest(StaticWebAssetsManifest expected, StaticWebAssetsManifest actual)
- {
- //Many of the properties in the manifest contain full paths, to avoid flakiness on the tests, we don't compare the full paths.
- actual.Version.Should().Be(expected.Version);
- actual.Source.Should().Be(expected.Source);
- actual.BasePath.Should().Be(expected.BasePath);
- actual.Mode.Should().Be(expected.Mode);
- actual.ManifestType.Should().Be(expected.ManifestType);
- actual.ReferencedProjectsConfiguration.Should().HaveSameCount(expected.ReferencedProjectsConfiguration);
- // Relax the check for project reference configuration items see
- // https://github.com/dotnet/sdk/pull/27381#issuecomment-1228764471
- // for details.
- //manifest.ReferencedProjectsConfiguration.OrderBy(cm => cm.Identity)
- // .Should()
- // .BeEquivalentTo(expected.ReferencedProjectsConfiguration.OrderBy(cm => cm.Identity));
- actual.DiscoveryPatterns.OrderBy(dp => dp.Name).Should().BeEquivalentTo(expected.DiscoveryPatterns.OrderBy(dp => dp.Name));
- var actualAssets = actual.Assets
- .OrderBy(a => a.BasePath)
- .ThenBy(a => a.RelativePath)
- .ThenBy(a => a.AssetKind)
- .GroupBy(a => GetAssetGroup(a))
- .ToDictionary(a => a.Key, a => a.ToArray());
- var duplicateAssets = actual.Assets
- .GroupBy(a => a)
- .ToDictionary(a => a.Key, a => a.ToArray());
- var foundDuplicateAssetss = duplicateAssets.Where(a => a.Value.Length > 1).ToArray();
- duplicateAssets.Where(a => a.Value.Length > 1).Should().BeEmpty($@"no duplicate assets should exist. But found:
- {string.Join($"{Environment.NewLine} ", foundDuplicateAssetss.Select(a => @$"{a.Key.Identity} - {a.Value.Length}"))}{Environment.NewLine}");
- var expectedAssets = expected.Assets
- .OrderBy(a => a.BasePath)
- .ThenBy(a => a.RelativePath)
- .ThenBy(a => a.AssetKind)
- .GroupBy(a => GetAssetGroup(a))
- .ToDictionary(a => a.Key, a => a.ToArray());
- var actualAssetsByIdentity = actual.Assets.GroupBy(a => a.Identity).ToDictionary(a => a.Key, a => a.ToArray());
- foreach (var asset in actual.Assets)
- {
- if (!string.IsNullOrEmpty(asset.RelatedAsset))
- {
- actualAssetsByIdentity.Should().ContainKey(asset.RelatedAsset);
- }
- }
- foreach (var (group, actualAssetsGroup) in actualAssets)
- {
- var expectedAssetsGroup = expectedAssets[group];
- CompareAssetGroup(group, actualAssetsGroup, expectedAssetsGroup);
- }
- var actualEndpoints = actual.Endpoints
- .OrderBy(a => a.Route)
- .ThenBy(a => a.AssetFile)
- .GroupBy(a => GetEndpointGroup(a))
- .ToDictionary(a => a.Key, a => a.ToArray());
- SortEndpointProperties(actualEndpoints);
- var duplicateEndpoints = actual.Endpoints
- .GroupBy(a => a)
- .ToDictionary(a => a.Key, a => a.ToArray());
- var foundDuplicateEndpoints = duplicateEndpoints.Where(a => DuplicatesExist(a)).ToArray();
- duplicateEndpoints.Where(a => DuplicatesExist(a)).Should().BeEmpty($@"no duplicate endpoints should exist. But found:
- {string.Join($"{Environment.NewLine} ", foundDuplicateEndpoints.Select(a => @$"{a.Key.Route} - {a.Key.AssetFile} - {a.Key.Selectors.Length} - {a.Value.Length}"))}{Environment.NewLine}");
- foreach (var endpoint in actual.Endpoints)
- {
- actualAssetsByIdentity.Should().ContainKey(endpoint.AssetFile);
- }
- var expectedEndpoints = expected.Endpoints
- .OrderBy(a => a.Route)
- .ThenBy(a => a.AssetFile)
- .GroupBy(a => GetEndpointGroup(a))
- .ToDictionary(a => a.Key, a => a.ToArray());
- SortEndpointProperties(expectedEndpoints);
- foreach (var (group, actualEndpointsGroup) in actualEndpoints)
- {
- var expectedEndpointsGroup = expectedEndpoints[group];
- CompareEndpointGroup(group, actualEndpointsGroup, expectedEndpointsGroup);
- }
- static bool DuplicatesExist(KeyValuePair<StaticWebAssetEndpoint, StaticWebAssetEndpoint[]> a)
- {
- var endpoint = a.Key;
- if (endpoint.Route.EndsWith(".gz") || endpoint.Route.EndsWith(".br") || endpoint.Selectors.Length == 1)
- {
- // This is not exact, but there are situations in which our templatization process is not biyective and Build and Publish assets defined during build for
- // the same asset end up having the same endpoint. To avoid issues with this, we relax the check to support finding more than one.
- return a.Value.Length > 2;
- }
- else
- {
- return a.Value.Length > 1;
- }
- }
- }
- private static void SortEndpointProperties(Dictionary<string, StaticWebAssetEndpoint[]> endpoints)
- {
- foreach (var endpointGroup in endpoints.Values)
- {
- foreach (var endpoint in endpointGroup)
- {
- Array.Sort(endpoint.Selectors, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value)));
- Array.Sort(endpoint.ResponseHeaders, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value)));
- Array.Sort(endpoint.EndpointProperties, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value)));
- }
- }
- }
- protected virtual void CompareAssetGroup(string group, StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets)
- {
- var comparisonMode = CompareAssetCounts(group, manifestAssets, expectedAssets);
- // Otherwise, do a property level comparison of all assets
- switch (comparisonMode)
- {
- case GroupComparisonMode.Exact:
- break;
- case GroupComparisonMode.AllowAdditionalAssets:
- break;
- default:
- break;
- }
- var differences = new List<string>();
- var assetDifferences = new List<string>();
- var groupLength = Math.Min(manifestAssets.Length, expectedAssets.Length);
- for (var i = 0; i < groupLength; i++)
- {
- var manifestAsset = manifestAssets[i];
- var expectedAsset = expectedAssets[i];
- ComputeAssetDifferences(assetDifferences, manifestAsset, expectedAsset);
- if (assetDifferences.Any())
- {
- differences.Add(@$"
- ==================================================
- For {expectedAsset.Identity}:
- {string.Join(Environment.NewLine, assetDifferences)}
- ==================================================");
- }
- assetDifferences.Clear();
- }
- differences.Should().BeEmpty(
- @$" the generated manifest should match the expected baseline.
- {BaselineGenerationInstructions}
- ");
- }
- private GroupComparisonMode CompareAssetCounts(string group, StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets)
- {
- var comparisonMode = GetGroupComparisonMode(group);
- // If there's a mismatch in the number of assets, just print the strict difference in the asset `Identity`
- switch (comparisonMode)
- {
- case GroupComparisonMode.Exact:
- if (manifestAssets.Length != expectedAssets.Length)
- {
- ThrowAssetCountMismatchError(manifestAssets, expectedAssets);
- }
- break;
- case GroupComparisonMode.AllowAdditionalAssets:
- if (expectedAssets.Except(manifestAssets).Any())
- {
- ThrowAssetCountMismatchError(manifestAssets, expectedAssets);
- }
- break;
- default:
- break;
- }
- return comparisonMode;
- static void ThrowAssetCountMismatchError(StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets)
- {
- var missingAssets = expectedAssets.Except(manifestAssets);
- var unexpectedAssets = manifestAssets.Except(expectedAssets);
- var differences = new List<string>();
- if (missingAssets.Any())
- {
- differences.Add($@"The following expected assets weren't found in the manifest:
- {string.Join($"{Environment.NewLine}\t", missingAssets.Select(a => a.Identity))}");
- }
- if (unexpectedAssets.Any())
- {
- differences.Add($@"The following additional unexpected assets were found in the manifest:
- {string.Join($"{Environment.NewLine}\t", unexpectedAssets.Select(a => a.Identity))}");
- }
- throw new Exception($@"{string.Join(Environment.NewLine, differences)}
- {BaselineGenerationInstructions}");
- }
- }
- protected virtual GroupComparisonMode GetGroupComparisonMode(string group)
- {
- return GroupComparisonMode.Exact;
- }
- private static void ComputeAssetDifferences(List<string> assetDifferences, StaticWebAsset manifestAsset, StaticWebAsset expectedAsset)
- {
- if (manifestAsset.Identity != expectedAsset.Identity)
- {
- assetDifferences.Add($"Expected manifest Identity of {expectedAsset.Identity} but found {manifestAsset.Identity}.");
- }
- if (manifestAsset.SourceType != expectedAsset.SourceType)
- {
- assetDifferences.Add($"Expected manifest SourceType of {expectedAsset.SourceType} but found {manifestAsset.SourceType}.");
- }
- if (manifestAsset.SourceId != expectedAsset.SourceId)
- {
- assetDifferences.Add($"Expected manifest SourceId of {expectedAsset.SourceId} but found {manifestAsset.SourceId}.");
- }
- if (manifestAsset.ContentRoot != expectedAsset.ContentRoot)
- {
- assetDifferences.Add($"Expected manifest ContentRoot of {expectedAsset.ContentRoot} but found {manifestAsset.ContentRoot}.");
- }
- if (manifestAsset.BasePath != expectedAsset.BasePath)
- {
- assetDifferences.Add($"Expected manifest BasePath of {expectedAsset.BasePath} but found {manifestAsset.BasePath}.");
- }
- if (manifestAsset.RelativePath != expectedAsset.RelativePath)
- {
- assetDifferences.Add($"Expected manifest RelativePath of {expectedAsset.RelativePath} but found {manifestAsset.RelativePath}.");
- }
- if (manifestAsset.AssetKind != expectedAsset.AssetKind)
- {
- assetDifferences.Add($"Expected manifest AssetKind of {expectedAsset.AssetKind} but found {manifestAsset.AssetKind}.");
- }
- if (manifestAsset.AssetMode != expectedAsset.AssetMode)
- {
- assetDifferences.Add($"Expected manifest AssetMode of {expectedAsset.AssetMode} but found {manifestAsset.AssetMode}.");
- }
- if (manifestAsset.AssetRole != expectedAsset.AssetRole)
- {
- assetDifferences.Add($"Expected manifest AssetRole of {expectedAsset.AssetRole} but found {manifestAsset.AssetRole}.");
- }
- if (manifestAsset.RelatedAsset != expectedAsset.RelatedAsset)
- {
- assetDifferences.Add($"Expected manifest RelatedAsset of {expectedAsset.RelatedAsset} but found {manifestAsset.RelatedAsset}.");
- }
- if (manifestAsset.AssetTraitName != expectedAsset.AssetTraitName)
- {
- assetDifferences.Add($"Expected manifest AssetTraitName of {expectedAsset.AssetTraitName} but found {manifestAsset.AssetTraitName}.");
- }
- if (manifestAsset.AssetTraitValue != expectedAsset.AssetTraitValue)
- {
- assetDifferences.Add($"Expected manifest AssetTraitValue of {expectedAsset.AssetTraitValue} but found {manifestAsset.AssetTraitValue}.");
- }
- if (manifestAsset.CopyToOutputDirectory != expectedAsset.CopyToOutputDirectory)
- {
- assetDifferences.Add($"Expected manifest CopyToOutputDirectory of {expectedAsset.CopyToOutputDirectory} but found {manifestAsset.CopyToOutputDirectory}.");
- }
- if (manifestAsset.CopyToPublishDirectory != expectedAsset.CopyToPublishDirectory)
- {
- assetDifferences.Add($"Expected manifest CopyToPublishDirectory of {expectedAsset.CopyToPublishDirectory} but found {manifestAsset.CopyToPublishDirectory}.");
- }
- if (manifestAsset.OriginalItemSpec != expectedAsset.OriginalItemSpec)
- {
- assetDifferences.Add($"Expected manifest OriginalItemSpec of {expectedAsset.OriginalItemSpec} but found {manifestAsset.OriginalItemSpec}.");
- }
- }
- protected virtual string GetAssetGroup(StaticWebAsset asset)
- {
- return Path.GetExtension(asset.Identity.TrimEnd(']'));
- }
- protected virtual void CompareEndpointGroup(string group, StaticWebAssetEndpoint[] manifestAssets, StaticWebAssetEndpoint[] expectedAssets)
- {
- var comparisonMode = CompareEndpointCounts(group, manifestAssets, expectedAssets);
- // Otherwise, do a property level comparison of all assets
- switch (comparisonMode)
- {
- case GroupComparisonMode.Exact:
- break;
- case GroupComparisonMode.AllowAdditionalAssets:
- break;
- default:
- break;
- }
- var differences = new List<string>();
- var assetDifferences = new List<string>();
- var groupLength = Math.Min(manifestAssets.Length, expectedAssets.Length);
- for (var i = 0; i < groupLength; i++)
- {
- var manifestAsset = manifestAssets[i];
- var expectedAsset = expectedAssets[i];
- ComputeEndpointDifferences(assetDifferences, manifestAsset, expectedAsset);
- if (assetDifferences.Any())
- {
- differences.Add(@$"
- ==================================================
- For {expectedAsset.AssetFile}:
- {string.Join(Environment.NewLine, assetDifferences)}
- ==================================================");
- }
- assetDifferences.Clear();
- }
- differences.Should().BeEmpty(
- @$" the generated manifest should match the expected baseline.
- {BaselineGenerationInstructions}
- ");
- }
- private GroupComparisonMode CompareEndpointCounts(string group, StaticWebAssetEndpoint[] manifestEndpoints, StaticWebAssetEndpoint[] expectedEndpoints)
- {
- var comparisonMode = GetGroupComparisonMode(group);
- // If there's a mismatch in the number of assets, just print the strict difference in the asset `Identity`
- switch (comparisonMode)
- {
- case GroupComparisonMode.Exact:
- if (manifestEndpoints.Length != expectedEndpoints.Length)
- {
- ThrowEndpointCountMismatchError(group, manifestEndpoints, expectedEndpoints);
- }
- break;
- case GroupComparisonMode.AllowAdditionalAssets:
- if (expectedEndpoints.Except(manifestEndpoints).Any())
- {
- ThrowEndpointCountMismatchError(group, manifestEndpoints, expectedEndpoints);
- }
- break;
- default:
- break;
- }
- return comparisonMode;
- static void ThrowEndpointCountMismatchError(string group, StaticWebAssetEndpoint[] manifestEndpoints, StaticWebAssetEndpoint[] expectedEndpoints)
- {
- var missingEndpoints = expectedEndpoints.Except(manifestEndpoints);
- var unexpectedEndpoints = manifestEndpoints.Except(expectedEndpoints);
- var differences = new List<string>
- {
- $"Expected group '{group}' to have '{expectedEndpoints.Length}' endpoints but found '{manifestEndpoints.Length}'.",
- "Expected Endpoints:",
- string.Join($"{Environment.NewLine}\t", expectedEndpoints.Select(a => $"{a.Route} - {a.Selectors.Length} - {a.AssetFile}")),
- "Actual Endpoints:",
- string.Join($"{Environment.NewLine}\t", manifestEndpoints.Select(a => $"{a.Route} - {a.Selectors.Length} - {a.AssetFile}"))
- };
- if (missingEndpoints.Any())
- {
- differences.Add($@"The following expected assets weren't found in the manifest:
- {string.Join($"{Environment.NewLine}\t", missingEndpoints.Select(a => $"{a.Route} - {a.AssetFile}"))}");
- }
- if (unexpectedEndpoints.Any())
- {
- differences.Add($@"The following additional unexpected assets were found in the manifest:
- {string.Join($"{Environment.NewLine}\t", unexpectedEndpoints.Select(a => $"{a.Route} - {a.AssetFile}"))}");
- }
- throw new Exception($@"{string.Join(Environment.NewLine, differences)}
- {BaselineGenerationInstructions}");
- }
- }
- protected virtual GroupComparisonMode GetAssetGroupComparisonMode(string group)
- {
- return GroupComparisonMode.Exact;
- }
- private static void ComputeEndpointDifferences(List<string> assetDifferences, StaticWebAssetEndpoint manifestAsset, StaticWebAssetEndpoint expectedAsset)
- {
- if (manifestAsset.Route != expectedAsset.Route)
- {
- assetDifferences.Add($"Expected manifest Identity of {expectedAsset.Route} but found {manifestAsset.Route}.");
- }
- if (manifestAsset.AssetFile != expectedAsset.AssetFile)
- {
- assetDifferences.Add($"Expected manifest SourceType of {expectedAsset.AssetFile} but found {manifestAsset.AssetFile}.");
- }
- ComputeSelectorDifferences(assetDifferences, manifestAsset.Selectors, expectedAsset.Selectors);
- ComputeResponseHeaderDifferences(assetDifferences, manifestAsset.ResponseHeaders, expectedAsset.ResponseHeaders);
- }
- private static void ComputeResponseHeaderDifferences(
- List<string> assetDifferences,
- StaticWebAssetEndpointResponseHeader[] manifestResponseHeaders,
- StaticWebAssetEndpointResponseHeader[] expectedResponseHeaders)
- {
- if (manifestResponseHeaders.Length != expectedResponseHeaders.Length)
- {
- assetDifferences.Add($"Expected manifest to have {expectedResponseHeaders.Length} response headers but found {manifestResponseHeaders.Length}.");
- }
- var manifest = new HashSet<StaticWebAssetEndpointResponseHeader>(manifestResponseHeaders);
- var differences = new HashSet<StaticWebAssetEndpointResponseHeader>(manifestResponseHeaders);
- var expected = new HashSet<StaticWebAssetEndpointResponseHeader>(expectedResponseHeaders);
- differences.SymmetricExceptWith(expected);
- foreach (var difference in differences)
- {
- if (!manifest.Contains(difference))
- {
- assetDifferences.Add($"Expected manifest to have response header '{difference.Name}={difference.Value}' but it was not found.");
- }
- else
- {
- assetDifferences.Add($"Found unexpected response header '{difference.Name}={difference.Value}'.");
- }
- }
- }
- private static void ComputeSelectorDifferences(
- List<string> assetDifferences,
- StaticWebAssetEndpointSelector[] manifestSelectors,
- StaticWebAssetEndpointSelector[] expectedSelectors)
- {
- if (manifestSelectors.Length != expectedSelectors.Length)
- {
- assetDifferences.Add($"Expected manifest to have {expectedSelectors.Length} selectors but found {manifestSelectors.Length}.");
- }
- var manifest = new HashSet<StaticWebAssetEndpointSelector>(manifestSelectors);
- var differences = new HashSet<StaticWebAssetEndpointSelector>(manifestSelectors);
- var expected = new HashSet<StaticWebAssetEndpointSelector>(expectedSelectors);
- differences.SymmetricExceptWith(expected);
- foreach (var difference in differences)
- {
- if (!manifest.Contains(difference))
- {
- assetDifferences.Add($"Expected manifest to have selector '{difference.Name}={difference.Value};q={difference.Quality}' but it was not found.");
- }
- else
- {
- assetDifferences.Add($"Found unexpected selector '{difference.Name}={difference.Value};q={difference.Quality}'.");
- }
- }
- }
- protected virtual string GetEndpointGroup(StaticWebAssetEndpoint asset)
- {
- return Path.GetExtension(asset.AssetFile.TrimEnd(']'));
- }
- }
- public enum GroupComparisonMode
- {
- // We require the same number of assets in a group for the baseline and the template.
- Exact,
- // We won't fail when we check against the baseline if additional assets are present for a group.
- AllowAdditionalAssets
- }
|