AspNetSdkBaselineTest.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System.Reflection;
  4. using System.Runtime.CompilerServices;
  5. using System.Text.Json;
  6. using Microsoft.AspNetCore.StaticWebAssets.Tasks;
  7. namespace Microsoft.NET.Sdk.Razor.Tests
  8. {
  9. [Trait("AspNetCore", "BaselineTest")]
  10. public class AspNetSdkBaselineTest : AspNetSdkTest
  11. {
  12. private static readonly JsonSerializerOptions BaselineSerializationOptions = new() { WriteIndented = true };
  13. private readonly StaticWebAssetsBaselineComparer _comparer;
  14. private readonly StaticWebAssetsBaselineFactory _baselineFactory;
  15. private string _baselinesFolder;
  16. #if GENERATE_SWA_BASELINES
  17. public static bool GenerateBaselines = true;
  18. #else
  19. public static bool GenerateBaselines = bool.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_TEST_BASELINES"), out var result) && result;
  20. #endif
  21. private bool _generateBaselines = GenerateBaselines;
  22. public AspNetSdkBaselineTest(ITestOutputHelper log) : base(log)
  23. {
  24. TestAssembly = Assembly.GetCallingAssembly();
  25. var testAssemblyMetadata = TestAssembly.GetCustomAttributes<AssemblyMetadataAttribute>();
  26. RuntimeVersion = testAssemblyMetadata.SingleOrDefault(a => a.Key == "NetCoreAppRuntimePackageVersion").Value;
  27. DefaultPackageVersion = testAssemblyMetadata.SingleOrDefault(a => a.Key == "DefaultTestBaselinePackageVersion").Value;
  28. _comparer = CreateBaselineComparer();
  29. _baselineFactory = CreateBaselineFactory();
  30. }
  31. public AspNetSdkBaselineTest(ITestOutputHelper log, bool generateBaselines) : this(log)
  32. {
  33. _generateBaselines = generateBaselines;
  34. _comparer = CreateBaselineComparer();
  35. }
  36. public TestAsset ProjectDirectory { get; set; }
  37. public string RuntimeVersion { get; set; }
  38. public string DefaultPackageVersion { get; set; }
  39. public string BaselinesFolder =>
  40. _baselinesFolder ??= ComputeBaselineFolder();
  41. protected Assembly TestAssembly { get; }
  42. protected virtual StaticWebAssetsBaselineComparer CreateBaselineComparer() => StaticWebAssetsBaselineComparer.Instance;
  43. private StaticWebAssetsBaselineFactory CreateBaselineFactory() => StaticWebAssetsBaselineFactory.Instance;
  44. protected virtual string ComputeBaselineFolder() =>
  45. Path.Combine(TestContext.GetRepoRoot() ?? AppContext.BaseDirectory, "test", "Microsoft.NET.Sdk.Razor.Tests", "StaticWebAssetsBaselines");
  46. protected virtual string EmbeddedResourcePrefix => string.Join('.', "Microsoft.NET.Sdk.Razor.Tests", "StaticWebAssetsBaselines");
  47. public StaticWebAssetsManifest LoadBuildManifest(string suffix = "", [CallerMemberName] string name = "")
  48. {
  49. if (_generateBaselines)
  50. {
  51. return default;
  52. }
  53. else
  54. {
  55. using var stream = GetManifestEmbeddedResource(suffix, name, "Build");
  56. var manifest = StaticWebAssetsManifest.FromStream(stream);
  57. return manifest;
  58. }
  59. }
  60. public StaticWebAssetsManifest LoadPublishManifest(string suffix = "", [CallerMemberName] string name = "")
  61. {
  62. if (_generateBaselines)
  63. {
  64. return default;
  65. }
  66. else
  67. {
  68. using var stream = GetManifestEmbeddedResource(suffix, name, "Publish");
  69. var manifest = StaticWebAssetsManifest.FromStream(stream);
  70. return manifest;
  71. }
  72. }
  73. protected void AssertBuildAssets(
  74. StaticWebAssetsManifest manifest,
  75. string outputFolder,
  76. string intermediateOutputPath,
  77. string suffix = "",
  78. [CallerMemberName] string name = "")
  79. {
  80. var fileEnumerationOptions = new EnumerationOptions { RecurseSubdirectories = true };
  81. var wwwRootFolder = Path.Combine(outputFolder, "wwwroot");
  82. var wwwRootFiles = Directory.Exists(wwwRootFolder) ?
  83. Directory.GetFiles(wwwRootFolder, "*", fileEnumerationOptions) :
  84. [];
  85. var computedFiles = manifest.Assets
  86. .Where(a => a.SourceType is StaticWebAsset.SourceTypes.Computed &&
  87. a.AssetKind is not StaticWebAsset.AssetKinds.Publish);
  88. // We keep track of assets that need to be copied to the output folder.
  89. // In addition to that, we copy assets that are defined somewhere different
  90. // from their content root folder when the content root does not match the output folder.
  91. // We do this to allow copying things like Publish assets to temporary locations during the
  92. // build process if they are later on going to be transformed.
  93. var copyToOutputDirectoryAssets = manifest.Assets.Where(a => a.ShouldCopyToOutputDirectory()).ToArray();
  94. var temporaryAsssets = manifest.Assets
  95. .Where(a =>
  96. !a.HasContentRoot(Path.Combine(outputFolder, "wwwroot")) &&
  97. File.Exists(a.Identity) &&
  98. !File.Exists(Path.Combine(a.ContentRoot, a.RelativePath)) &&
  99. a.AssetTraitName != "Content-Encoding").ToArray();
  100. var copyToOutputDirectoryFiles = copyToOutputDirectoryAssets
  101. .Select(a => Path.GetFullPath(Path.Combine(outputFolder, "wwwroot", a.RelativePath)))
  102. .Concat(temporaryAsssets
  103. .Select(a => Path.GetFullPath(Path.Combine(a.ContentRoot, a.RelativePath))))
  104. .ToArray();
  105. var existingFiles = _baselineFactory.TemplatizeExpectedFiles(
  106. wwwRootFiles
  107. .Concat(computedFiles.Select(a => a.Identity))
  108. .Concat(copyToOutputDirectoryFiles)
  109. .Distinct()
  110. .OrderBy(f => f, StringComparer.Ordinal)
  111. .ToArray(),
  112. TestContext.Current.NuGetCachePath,
  113. ProjectDirectory.TestRoot,
  114. intermediateOutputPath,
  115. outputFolder).ToArray();
  116. if (!_generateBaselines)
  117. {
  118. var expected = LoadExpectedFilesBaseline(manifest.ManifestType, suffix, name)
  119. .OrderBy(f => f, StringComparer.Ordinal);
  120. AssertFilesCore(existingFiles, expected);
  121. }
  122. else
  123. {
  124. File.WriteAllText(
  125. GetExpectedFilesPath(suffix, name, manifest.ManifestType),
  126. JsonSerializer.Serialize(existingFiles, BaselineSerializationOptions));
  127. }
  128. }
  129. private void AssertFilesCore(IEnumerable<string> existingFiles, IEnumerable<string> expected)
  130. {
  131. var existingSet = new HashSet<string>(existingFiles);
  132. var expectedSet = new HashSet<string>(expected);
  133. var different = new HashSet<string>(existingFiles);
  134. different.SymmetricExceptWith(expectedSet);
  135. var messages = new List<string>();
  136. if (existingSet.Count < expectedSet.Count)
  137. {
  138. messages.Add("The build produced less files than expected.");
  139. }
  140. else if (expectedSet.Count < existingSet.Count)
  141. {
  142. messages.Add("The build produced more files than expected.");
  143. }
  144. else if (different.Count > 0)
  145. {
  146. messages.Add("The build produced different files than expected.");
  147. }
  148. ComputeDifferences(expectedSet, different, messages);
  149. string.Join(Environment.NewLine, messages).Should().BeEmpty();
  150. static void ComputeDifferences(HashSet<string> existingSet, HashSet<string> different, List<string> messages)
  151. {
  152. foreach (var file in different)
  153. {
  154. if (existingSet.Contains(file))
  155. {
  156. messages.Add($"The file '{file}' is not in the baseline.");
  157. }
  158. else
  159. {
  160. messages.Add($"The file '{file}' is missing from the build.");
  161. }
  162. }
  163. }
  164. }
  165. protected void AssertPublishAssets(
  166. StaticWebAssetsManifest manifest,
  167. string publishFolder,
  168. string intermediateOutputPath,
  169. string suffix = "",
  170. [CallerMemberName] string name = "")
  171. {
  172. var fileEnumerationOptions = new EnumerationOptions { RecurseSubdirectories = true };
  173. string wwwRootFolder = Path.Combine(publishFolder, "wwwroot");
  174. var wwwRootFiles = Directory.Exists(wwwRootFolder) ?
  175. Directory.GetFiles(wwwRootFolder, "*", fileEnumerationOptions)
  176. .Select(f => _baselineFactory.TemplatizeFilePath(f, null, null, intermediateOutputPath, publishFolder, null)) :
  177. [];
  178. // Computed publish assets must exist on disk (we do this check to quickly identify when something is not being
  179. // generated vs when its being copied to the wrong place)
  180. var computedFiles = manifest.Assets
  181. .Where(a => a.SourceType is StaticWebAsset.SourceTypes.Computed &&
  182. a.AssetKind is not StaticWebAsset.AssetKinds.Build);
  183. // For assets that are copied to the publish folder, the path is always based on
  184. // the wwwroot folder, the relative path and the base path for project or package
  185. // assets.
  186. var copyToPublishDirectoryFiles = manifest.Assets
  187. .Where(a => !string.Equals(a.SourceId, manifest.Source, StringComparison.Ordinal) ||
  188. !string.Equals(a.AssetMode, StaticWebAsset.AssetModes.Reference))
  189. .Select(a => Path.Combine(wwwRootFolder, a.ComputeTargetPath("", Path.DirectorySeparatorChar)));
  190. var existingFiles = _baselineFactory.TemplatizeExpectedFiles(
  191. [.. wwwRootFiles
  192. .Concat(computedFiles.Select(a => a.Identity))
  193. .Concat(copyToPublishDirectoryFiles)
  194. .Distinct()
  195. .OrderBy(f => f, StringComparer.Ordinal)],
  196. TestContext.Current.NuGetCachePath,
  197. ProjectDirectory.TestRoot,
  198. intermediateOutputPath,
  199. publishFolder);
  200. if (!_generateBaselines)
  201. {
  202. var expected = LoadExpectedFilesBaseline(manifest.ManifestType, suffix, name);
  203. existingFiles.Should().BeEquivalentTo(expected);
  204. }
  205. else
  206. {
  207. File.WriteAllText(
  208. GetExpectedFilesPath(suffix, name, manifest.ManifestType),
  209. JsonSerializer.Serialize(existingFiles, BaselineSerializationOptions));
  210. }
  211. }
  212. public string[] LoadExpectedFilesBaseline(
  213. string type,
  214. string suffix,
  215. string name)
  216. {
  217. if (!_generateBaselines)
  218. {
  219. using var filesBaselineStream = GetExpectedFilesEmbeddedResource(suffix, name, type);
  220. return JsonSerializer.Deserialize<string[]>(filesBaselineStream);
  221. }
  222. else
  223. {
  224. return [];
  225. }
  226. }
  227. internal void AssertManifest(
  228. StaticWebAssetsManifest actual,
  229. StaticWebAssetsManifest expected,
  230. string suffix = "",
  231. string runtimeIdentifier = null,
  232. [CallerMemberName] string name = "")
  233. {
  234. if (!_generateBaselines)
  235. {
  236. // We are going to compare the generated manifest with the current manifest.
  237. // For that, we "templatize" the current manifest to avoid issues with hashes, versions, etc.
  238. _baselineFactory.ToTemplate(
  239. actual,
  240. ProjectDirectory.Path,
  241. TestContext.Current.NuGetCachePath,
  242. runtimeIdentifier);
  243. _comparer.AssertManifest(expected, actual);
  244. }
  245. else
  246. {
  247. var template = Templatize(actual, ProjectDirectory.Path, TestContext.Current.NuGetCachePath, runtimeIdentifier);
  248. if (!Directory.Exists(Path.Combine(BaselinesFolder)))
  249. {
  250. Directory.CreateDirectory(Path.Combine(BaselinesFolder));
  251. }
  252. File.WriteAllText(GetManifestPath(suffix, name, actual.ManifestType), template);
  253. }
  254. }
  255. private string GetManifestPath(string suffix, string name, string manifestType)
  256. => Path.Combine(BaselinesFolder, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.staticwebassets.json");
  257. private Stream GetManifestEmbeddedResource(string suffix, string name, string manifestType)
  258. => TestAssembly.GetManifestResourceStream(string.Join('.', EmbeddedResourcePrefix, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.staticwebassets.json"));
  259. private string GetExpectedFilesPath(string suffix, string name, string manifestType)
  260. => Path.Combine(BaselinesFolder, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.files.json");
  261. private Stream GetExpectedFilesEmbeddedResource(string suffix, string name, string manifestType)
  262. => TestAssembly.GetManifestResourceStream(string.Join('.', EmbeddedResourcePrefix, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.files.json"));
  263. private string Templatize(StaticWebAssetsManifest manifest, string projectRoot, string restorePath, string runtimeIdentifier)
  264. {
  265. _baselineFactory.ToTemplate(manifest, projectRoot, restorePath, runtimeIdentifier);
  266. return JsonSerializer.Serialize(manifest, BaselineSerializationOptions);
  267. }
  268. }
  269. }