You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

DistributedApplicationExtension.cs 14 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. // Copyright (c) Microsoft Corporation. All rights reserved.
  2. // DistributedApplicationExtension.cs
  3. using System.Security.Cryptography;
  4. using Aspire.Hosting;
  5. using Aspire.Hosting.Python;
  6. using Microsoft.Extensions.Hosting;
  7. using Microsoft.Extensions.Logging;
  8. using Microsoft.Extensions.Logging.Testing;
  9. namespace Microsoft.AutoGen.Integration.Tests;
  10. public static partial class DistributedApplicationExtensions
  11. {
  12. /* /// <summary>
  13. /// Ensures all parameters in the application configuration have values set.
  14. /// </summary>
  15. public static TBuilder WithRandomParameterValues<TBuilder>(this TBuilder builder)
  16. where TBuilder : IDistributedApplicationTestingBuilder
  17. {
  18. var parameters = builder.Resources.OfType<ParameterResource>().Where(p => !p.IsConnectionString).ToList();
  19. foreach (var parameter in parameters)
  20. {
  21. builder.Configuration[$"Parameters:{parameter.Name}"] = parameter.Secret
  22. ? PasswordGenerator.Generate(16, true, true, true, false, 1, 1, 1, 0)
  23. : Convert.ToHexString(RandomNumberGenerator.GetBytes(4));
  24. }
  25. return builder;
  26. } */
  27. /// <summary>
  28. /// Sets the container lifetime for all container resources in the application.
  29. /// </summary>
  30. public static TBuilder WithContainersLifetime<TBuilder>(this TBuilder builder, ContainerLifetime containerLifetime)
  31. where TBuilder : IDistributedApplicationTestingBuilder
  32. {
  33. var containerLifetimeAnnotations = builder.Resources.SelectMany(r => r.Annotations
  34. .OfType<ContainerLifetimeAnnotation>()
  35. .Where(c => c.Lifetime != containerLifetime))
  36. .ToList();
  37. foreach (var annotation in containerLifetimeAnnotations)
  38. {
  39. annotation.Lifetime = containerLifetime;
  40. }
  41. return builder;
  42. }
  43. /// <summary>
  44. /// Replaces all named volumes with anonymous volumes so they're isolated across test runs and from the volume the app uses during development.
  45. /// </summary>
  46. /// <remarks>
  47. /// Note that if multiple resources share a volume, the volume will instead be given a random name so that it's still shared across those resources in the test run.
  48. /// </remarks>
  49. public static TBuilder WithRandomVolumeNames<TBuilder>(this TBuilder builder)
  50. where TBuilder : IDistributedApplicationTestingBuilder
  51. {
  52. // Named volumes that aren't shared across resources should be replaced with anonymous volumes.
  53. // Named volumes shared by mulitple resources need to have their name randomized but kept shared across those resources.
  54. // Find all shared volumes and make a map of their original name to a new randomized name
  55. var allResourceNamedVolumes = builder.Resources.SelectMany(r => r.Annotations
  56. .OfType<ContainerMountAnnotation>()
  57. .Where(m => m.Type == ContainerMountType.Volume && !string.IsNullOrEmpty(m.Source))
  58. .Select(m => (Resource: r, Volume: m)))
  59. .ToList();
  60. var seenVolumes = new HashSet<string>();
  61. var renamedVolumes = new Dictionary<string, string>();
  62. foreach (var resourceVolume in allResourceNamedVolumes)
  63. {
  64. var name = resourceVolume.Volume.Source!;
  65. if (!seenVolumes.Add(name) && !renamedVolumes.ContainsKey(name))
  66. {
  67. renamedVolumes[name] = $"{name}-{Convert.ToHexString(RandomNumberGenerator.GetBytes(4))}";
  68. }
  69. }
  70. // Replace all named volumes with randomly named or anonymous volumes
  71. foreach (var resourceVolume in allResourceNamedVolumes)
  72. {
  73. var resource = resourceVolume.Resource;
  74. var volume = resourceVolume.Volume;
  75. var newName = renamedVolumes.TryGetValue(volume.Source!, out var randomName) ? randomName : null;
  76. var newMount = new ContainerMountAnnotation(newName, volume.Target, ContainerMountType.Volume, volume.IsReadOnly);
  77. resource.Annotations.Remove(volume);
  78. resource.Annotations.Add(newMount);
  79. }
  80. return builder;
  81. }
  82. /// <summary>
  83. /// Waits for the specified resource to reach the specified state.
  84. /// </summary>
  85. public static Task WaitForResource(this DistributedApplication app, string resourceName, string? targetState = null, CancellationToken cancellationToken = default)
  86. {
  87. targetState ??= KnownResourceStates.Running;
  88. var resourceNotificationService = app.Services.GetRequiredService<ResourceNotificationService>();
  89. return resourceNotificationService.WaitForResourceAsync(resourceName, targetState, cancellationToken);
  90. }
  91. /// <summary>
  92. /// Waits for all resources in the application to reach one of the specified states.
  93. /// </summary>
  94. /// <remarks>
  95. /// If <paramref name="targetStates"/> is null, the default states are <see cref="KnownResourceStates.Running"/> and <see cref="KnownResourceStates.Hidden"/>.
  96. /// </remarks>
  97. public static async Task WaitForResourcesAsync(this DistributedApplication app, IEnumerable<string>? targetStates = null, CancellationToken cancellationToken = default)
  98. {
  99. var logger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(WaitForResourcesAsync));
  100. targetStates ??= [KnownResourceStates.Running, KnownResourceStates.Hidden, .. KnownResourceStates.TerminalStates];
  101. var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
  102. var resourceNotificationService = app.Services.GetRequiredService<ResourceNotificationService>();
  103. var resourceTasks = new Dictionary<string, Task<(string Name, string State)>>();
  104. foreach (var resource in applicationModel.Resources)
  105. {
  106. resourceTasks[resource.Name] = GetResourceWaitTask(resource.Name, targetStates, cancellationToken);
  107. }
  108. logger.LogInformation("Waiting for resources [{Resources}] to reach one of target states [{TargetStates}].",
  109. string.Join(',', resourceTasks.Keys),
  110. string.Join(',', targetStates));
  111. while (resourceTasks.Count > 0)
  112. {
  113. var completedTask = await Task.WhenAny(resourceTasks.Values);
  114. var (completedResourceName, targetStateReached) = await completedTask;
  115. if (targetStateReached == KnownResourceStates.FailedToStart)
  116. {
  117. throw new DistributedApplicationException($"Resource '{completedResourceName}' failed to start.");
  118. }
  119. resourceTasks.Remove(completedResourceName);
  120. logger.LogInformation("Wait for resource '{ResourceName}' completed with state '{ResourceState}'", completedResourceName, targetStateReached);
  121. // Ensure resources being waited on still exist
  122. var remainingResources = resourceTasks.Keys.ToList();
  123. for (var i = remainingResources.Count - 1; i > 0; i--)
  124. {
  125. var name = remainingResources[i];
  126. if (!applicationModel.Resources.Any(r => r.Name == name))
  127. {
  128. logger.LogInformation("Resource '{ResourceName}' was deleted while waiting for it.", name);
  129. resourceTasks.Remove(name);
  130. remainingResources.RemoveAt(i);
  131. }
  132. }
  133. if (resourceTasks.Count > 0)
  134. {
  135. logger.LogInformation("Still waiting for resources [{Resources}] to reach one of target states [{TargetStates}].",
  136. string.Join(',', remainingResources),
  137. string.Join(',', targetStates));
  138. }
  139. }
  140. logger.LogInformation("Wait for all resources completed successfully!");
  141. async Task<(string Name, string State)> GetResourceWaitTask(string resourceName, IEnumerable<string> targetStates, CancellationToken cancellationToken)
  142. {
  143. var state = await resourceNotificationService.WaitForResourceAsync(resourceName, targetStates, cancellationToken);
  144. return (resourceName, state);
  145. }
  146. }
  147. /// <summary>
  148. /// Gets the app host and resource logs from the application.
  149. /// </summary>
  150. public static (IReadOnlyList<FakeLogRecord> AppHostLogs, IReadOnlyList<FakeLogRecord> ResourceLogs) GetLogs(this DistributedApplication app)
  151. {
  152. var environment = app.Services.GetRequiredService<IHostEnvironment>();
  153. var logCollector = app.Services.GetFakeLogCollector();
  154. var logs = logCollector.GetSnapshot();
  155. var appHostLogs = logs.Where(l => l.Category?.StartsWith($"{environment.ApplicationName}.Resources") == false).ToList();
  156. var resourceLogs = logs.Where(l => l.Category?.StartsWith($"{environment.ApplicationName}.Resources") == true).ToList();
  157. return (appHostLogs, resourceLogs);
  158. }
  159. /// <summary>
  160. /// Get all logs from the whole test run.
  161. /// </summary>
  162. /// <param name="app"></param>
  163. /// <returns>List</returns>
  164. public static IReadOnlyList<FakeLogRecord> GetAllLogs(this DistributedApplication app)
  165. {
  166. var logCollector = app.Services.GetFakeLogCollector();
  167. return logCollector.GetSnapshot();
  168. }
  169. /// <summary>
  170. /// Asserts that no errors were logged by the application or any of its resources.
  171. /// </summary>
  172. /// <remarks>
  173. /// Some resource types are excluded from this check because they tend to write to stderr for various non-error reasons.
  174. /// </remarks>
  175. /// <param name="app"></param>
  176. public static void EnsureNoErrorsLogged(this DistributedApplication app)
  177. {
  178. var environment = app.Services.GetRequiredService<IHostEnvironment>();
  179. var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
  180. var assertableResourceLogNames = applicationModel.Resources.Where(ShouldAssertErrorsForResource).Select(r => $"{environment.ApplicationName}.Resources.{r.Name}").ToList();
  181. var (appHostlogs, resourceLogs) = app.GetLogs();
  182. Assert.DoesNotContain(appHostlogs, log => log.Level >= LogLevel.Error);
  183. Assert.DoesNotContain(resourceLogs, log => log.Category is { Length: > 0 } category && assertableResourceLogNames.Contains(category) && log.Level >= LogLevel.Error);
  184. static bool ShouldAssertErrorsForResource(IResource resource)
  185. {
  186. #pragma warning disable ASPIREHOSTINGPYTHON001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
  187. return resource
  188. is
  189. // Container resources tend to write to stderr for various reasons so only assert projects and executables
  190. (ProjectResource or ExecutableResource)
  191. // Node & Python resources tend to have modules that write to stderr so ignore them
  192. and not (PythonAppResource)
  193. // Dapr resources write to stderr about deprecated --components-path flag
  194. && !resource.Name.EndsWith("-dapr-cli");
  195. #pragma warning restore ASPIREHOSTINGPYTHON001
  196. }
  197. }
  198. /// <summary>
  199. /// Asserts that the application and resource logs contain the specified message.
  200. /// </summary>
  201. /// <param name="app"></param>
  202. /// <param name="message"></param>
  203. public static void EnsureLogContains(this DistributedApplication app, string message)
  204. {
  205. var resourceLogs = app.GetAllLogs();
  206. Assert.Contains(resourceLogs, log => log.Message.Contains(message));
  207. }
  208. /// <summary>
  209. /// Creates an <see cref="HttpClient"/> configured to communicate with the specified resource.
  210. /// </summary>
  211. public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, bool useHttpClientFactory)
  212. => app.CreateHttpClient(resourceName, null, useHttpClientFactory);
  213. /// <summary>
  214. /// Creates an <see cref="HttpClient"/> configured to communicate with the specified resource.
  215. /// </summary>
  216. public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, bool useHttpClientFactory)
  217. {
  218. if (useHttpClientFactory)
  219. {
  220. return app.CreateHttpClient(resourceName, endpointName);
  221. }
  222. // Don't use the HttpClientFactory to create the HttpClient so, e.g., no resilience policies are applied
  223. var httpClient = new HttpClient
  224. {
  225. BaseAddress = app.GetEndpoint(resourceName, endpointName)
  226. };
  227. return httpClient;
  228. }
  229. /// <summary>
  230. /// Creates an <see cref="HttpClient"/> configured to communicate with the specified resource with custom configuration.
  231. /// </summary>
  232. public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, Action<IHttpClientBuilder> configure)
  233. {
  234. var services = new ServiceCollection()
  235. .AddHttpClient()
  236. .ConfigureHttpClientDefaults(configure)
  237. .BuildServiceProvider();
  238. var httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
  239. var httpClient = httpClientFactory.CreateClient();
  240. httpClient.BaseAddress = app.GetEndpoint(resourceName, endpointName);
  241. return httpClient;
  242. }
  243. private static bool DerivesFromDbContext(Type type)
  244. {
  245. var baseType = type.BaseType;
  246. while (baseType is not null)
  247. {
  248. if (baseType.FullName == "Microsoft.EntityFrameworkCore.DbContext" && baseType.Assembly.GetName().Name == "Microsoft.EntityFrameworkCore")
  249. {
  250. return true;
  251. }
  252. baseType = baseType.BaseType;
  253. }
  254. return false;
  255. }
  256. }