Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Testing] Populate AppHost environment from launch profile and support launch profile override #7363

Merged
merged 1 commit into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 90 additions & 22 deletions src/Aspire.Hosting.Testing/DistributedApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private void OnBuiltCore(DistributedApplication application)
OnBuilt(application);
}

internal static void PreConfigureBuilderOptions(
private static void PreConfigureBuilderOptions(
DistributedApplicationOptions applicationOptions,
HostApplicationBuilderSettings hostBuilderOptions,
string[] args,
Expand All @@ -148,57 +148,125 @@ internal static void PreConfigureBuilderOptions(
{ } existing => [.. existing, .. args],
null => args
};

applicationOptions.Args = applicationOptions.Args switch
{
{ } existing => [.. existing, .. args],
null => args
};
applicationOptions.Args = hostBuilderOptions.Args;

hostBuilderOptions.EnvironmentName = Environments.Development;
hostBuilderOptions.ApplicationName = entryPointAssembly.GetName().Name ?? string.Empty;
applicationOptions.AssemblyName = entryPointAssembly.GetName().Name ?? string.Empty;
applicationOptions.DisableDashboard = true;
applicationOptions.EnableResourceLogging = true;
var cfg = hostBuilderOptions.Configuration ??= new();
var existingConfig = new ConfigurationManager();
existingConfig.AddCommandLine(applicationOptions.Args ?? []);
if (hostBuilderOptions.Configuration is not null)
{
existingConfig.AddConfiguration(hostBuilderOptions.Configuration);
}

var additionalConfig = new Dictionary<string, string?>();
SetDefault("DcpPublisher:ContainerRuntimeInitializationTimeout", "00:00:30");
SetDefault("DcpPublisher:RandomizePorts", "true");
SetDefault("DcpPublisher:DeleteResourcesOnShutdown", "true");
SetDefault("DcpPublisher:ResourceNameSuffix", $"{Random.Shared.Next():x}");
Comment on lines 166 to 169
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider making some of these first class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eg, by giving them a publicly accessible type or property? Agreed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, file an issue for this one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick write-up #7365


// Make sure we have a dashboard URL and OTLP endpoint URL.
SetDefault("ASPNETCORE_URLS", "http://localhost:8080");
SetDefault("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL", "http://localhost:4317");

var appHostProjectPath = ResolveProjectPath(entryPointAssembly);
if (!string.IsNullOrEmpty(appHostProjectPath) && Directory.Exists(appHostProjectPath))
{
hostBuilderOptions.ContentRootPath = appHostProjectPath;
}

// Populate the default launch profile name.
var appHostLaunchSettings = GetLaunchSettings(appHostProjectPath);
if (appHostLaunchSettings?.Profiles.FirstOrDefault().Key is { } defaultLaunchProfileName)
{
SetDefault("AppHost:DefaultLaunchProfileName", defaultLaunchProfileName);
}
hostBuilderOptions.Configuration ??= new();
hostBuilderOptions.Configuration.AddInMemoryCollection(additionalConfig);

// Make sure we have a dashboard URL and OTLP endpoint URL.
SetDefault("ASPNETCORE_URLS", "http://localhost:8080");
SetDefault("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL", "http://localhost:4317");
cfg.AddInMemoryCollection(additionalConfig);
void SetDefault(string key, string? value)
{
if (cfg[key] is null)
if (existingConfig[key] is null)
{
additionalConfig[key] = value;
}
}
}

internal void OnBuilderCreatingCore(
internal static void ConfigureBuilder(
string[] args,
DistributedApplicationOptions applicationOptions,
HostApplicationBuilderSettings hostBuilderOptions,
Assembly entryPointAssembly,
Action<DistributedApplicationOptions, HostApplicationBuilderSettings> configureBuilder)
{
PreConfigureBuilderOptions(applicationOptions, hostBuilderOptions, args, entryPointAssembly);
configureBuilder(applicationOptions, hostBuilderOptions);
PostConfigureBuilderOptions(hostBuilderOptions, entryPointAssembly);
}

private void OnBuilderCreatingCore(
DistributedApplicationOptions applicationOptions,
HostApplicationBuilderSettings hostBuilderOptions)
{
PreConfigureBuilderOptions(applicationOptions, hostBuilderOptions, args, _entryPoint.Assembly);
OnBuilderCreating(applicationOptions, hostBuilderOptions);
ConfigureBuilder(args, applicationOptions, hostBuilderOptions, _entryPoint.Assembly, OnBuilderCreating);
}

private static void PostConfigureBuilderOptions(
HostApplicationBuilderSettings hostBuilderOptions,
Assembly entryPointAssembly)
{
var existingConfig = new ConfigurationManager();
existingConfig.AddCommandLine(hostBuilderOptions.Args ?? []);
if (hostBuilderOptions.Configuration is not null)
{
existingConfig.AddConfiguration(hostBuilderOptions.Configuration);
}

var additionalConfig = new Dictionary<string, string?>();
var appHostProjectPath = ResolveProjectPath(entryPointAssembly);

// Populate the launch profile name.
var appHostLaunchSettings = GetLaunchSettings(appHostProjectPath);
var launchProfileName = existingConfig["DOTNET_LAUNCH_PROFILE"];

// Load the launch profile and populate configuration with environment variables.
if (appHostLaunchSettings is not null)
{
var launchProfiles = appHostLaunchSettings.Profiles;
LaunchProfile? launchProfile;
if (string.IsNullOrEmpty(launchProfileName))
{
// If a launch profile was not specified, select the first launch profile.
var firstLaunchProfile = launchProfiles.FirstOrDefault();
launchProfile = firstLaunchProfile.Value;
SetDefault("DOTNET_LAUNCH_PROFILE", firstLaunchProfile.Key);
}
else
{
if (!launchProfiles.TryGetValue(launchProfileName, out launchProfile))
{
throw new InvalidOperationException($"The configured launch profile, '{launchProfileName}', was not found in the launch settings file.");
}
}

// Populate config from env vars.
if (launchProfile?.EnvironmentVariables is { Count: > 0 } envVars)
{
foreach (var (key, value) in envVars)
{
SetDefault(key, value);
}
}
}

hostBuilderOptions.Configuration ??= new();
hostBuilderOptions.Configuration.AddInMemoryCollection(additionalConfig);

void SetDefault(string key, string? value)
{
if (existingConfig[key] is null)
{
additionalConfig[key] = value;
}
}
}

private static string? ResolveProjectPath(Assembly? assembly)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,7 @@ private static DistributedApplicationBuilder CreateInnerBuilder(
{
var builder = TestingBuilderFactory.CreateBuilder(args, onConstructing: (applicationOptions, hostBuilderOptions) =>
{
DistributedApplicationFactory.PreConfigureBuilderOptions(applicationOptions, hostBuilderOptions, args, FindApplicationAssembly());
configureBuilder(applicationOptions, hostBuilderOptions);
DistributedApplicationFactory.ConfigureBuilder(args, applicationOptions, hostBuilderOptions, FindApplicationAssembly(), configureBuilder);
});

if (!builder.Configuration.GetValue("ASPIRE_TESTING_DISABLE_HTTP_CLIENT", false))
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ private static IResourceBuilder<ProjectResource> WithProjectDefaults(this IResou
else
{
var appHostDefaultLaunchProfileName = builder.ApplicationBuilder.Configuration["AppHost:DefaultLaunchProfileName"]
?? Environment.GetEnvironmentVariable("DOTNET_LAUNCH_PROFILE");
?? builder.ApplicationBuilder.Configuration["DOTNET_LAUNCH_PROFILE"];
if (!string.IsNullOrEmpty(appHostDefaultLaunchProfileName))
{
builder.WithAnnotation(new DefaultLaunchProfileAnnotation(appHostDefaultLaunchProfileName));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"LAUNCH_PROFILE_VAR_FROM_APP_HOST": "app-host-is-https"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"LAUNCH_PROFILE_VAR_FROM_APP_HOST": "app-host-is-http"
}
}
}
}
166 changes: 165 additions & 1 deletion tests/Aspire.Hosting.Testing.Tests/TestingBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,170 @@ public async Task GetHttpClientBeforeStart(bool genericEntryPoint)
Assert.Throws<InvalidOperationException>(() => app.CreateHttpClient("mywebapp1"));
}

/// <summary>
/// Tests that arguments propagate into the application host.
/// </summary>
[Theory]
[RequiresDocker]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public async Task ArgsPropagateToAppHostConfiguration(bool genericEntryPoint, bool directArgs)
{
string[] args = directArgs ? ["APP_HOST_ARG=42"] : [];
Action<DistributedApplicationOptions, HostApplicationBuilderSettings> configureBuilder = directArgs switch
{
true => (_, _) => { },
false => (dao, habs) => habs.Args = ["APP_HOST_ARG=42"]
};

IDistributedApplicationTestingBuilder builder;
if (genericEntryPoint)
{
builder = await (DistributedApplicationTestingBuilder.CreateAsync<Projects.TestingAppHost1_AppHost>(args, configureBuilder));
}
else
{
builder = await (DistributedApplicationTestingBuilder.CreateAsync(typeof(Projects.TestingAppHost1_AppHost), args, configureBuilder));
}

await using var app = await builder.BuildAsync();
await app.StartAsync();

// Wait for the application to be ready
await app.WaitForTextAsync("Application started.").WaitAsync(TimeSpan.FromMinutes(1));

var httpClient = app.CreateHttpClientWithResilience("mywebapp1");
var appHostArg = await httpClient.GetStringAsync("/get-app-host-arg");
Assert.NotNull(appHostArg);
Assert.Equal("42", appHostArg);
}

/// <summary>
/// Tests that arguments propagate into the application host.
/// </summary>
[Theory]
[RequiresDocker]
[InlineData(true)]
[InlineData(false)]
public async Task ArgsPropagateToAppHostConfigurationAdHocBuilder(bool directArgs)
{
IDistributedApplicationTestingBuilder builder;
if (directArgs)
{
builder = DistributedApplicationTestingBuilder.Create(["APP_HOST_ARG=42"]);
}
else
{
builder = DistributedApplicationTestingBuilder.Create([], (dao, habs) => habs.Args = ["APP_HOST_ARG=42"]);
}

builder.AddProject<Projects.TestingAppHost1_MyWebApp>("mywebapp1")
.WithEnvironment("APP_HOST_ARG", builder.Configuration["APP_HOST_ARG"])
.WithEnvironment("LAUNCH_PROFILE_VAR_FROM_APP_HOST", builder.Configuration["LAUNCH_PROFILE_VAR_FROM_APP_HOST"]);
await using var app = await builder.BuildAsync();
await app.StartAsync();

// Wait for the application to be ready
await app.WaitForTextAsync("Application started.").WaitAsync(TimeSpan.FromMinutes(1));

var httpClient = app.CreateHttpClientWithResilience("mywebapp1");
var appHostArg = await httpClient.GetStringAsync("/get-app-host-arg");
Assert.NotNull(appHostArg);
Assert.Equal("42", appHostArg);
}

/// <summary>
/// Tests that setting the launch profile works and results in environment variables from the launch profile
/// populating in configuration.
/// </summary>
[Theory]
[RequiresDocker]
[InlineData("http", false)]
[InlineData("http", true)]
[InlineData("https", false)]
[InlineData("https", true)]
public async Task CanOverrideLaunchProfileViaArgs(string launchProfileName, bool directArgs)
{
var arg = $"DOTNET_LAUNCH_PROFILE={launchProfileName}";
string[] args;
Action<DistributedApplicationOptions, HostApplicationBuilderSettings> configureBuilder;
if (directArgs)
{
args = [arg];
configureBuilder = (_, _) => { };
}
else
{
args = [];
configureBuilder = (dao, habs) => habs.Args = [arg];
}

var appHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.TestingAppHost1_AppHost>(args, configureBuilder);
await using var app = await appHost.BuildAsync();
await app.StartAsync();

// Wait for the application to be ready
await app.WaitForTextAsync("Application started.").WaitAsync(TimeSpan.FromMinutes(1));

var httpClient = app.CreateHttpClientWithResilience("mywebapp1");
var appHostArg = await httpClient.GetStringAsync("/get-launch-profile-var");
Assert.NotNull(appHostArg);
Assert.Equal($"it-is-{launchProfileName}", appHostArg);

// Check that, aside from the launch profile, the app host loaded environment settings from its launch profile
var appHostLaunchProfileVar = await httpClient.GetStringAsync("/get-launch-profile-var-from-app-host");
Assert.NotNull(appHostLaunchProfileVar);
Assert.Equal($"app-host-is-{launchProfileName}", appHostLaunchProfileVar);
}

/// <summary>
/// Tests that setting the launch profile works and results in environment variables from the launch profile
/// populating in configuration.
/// </summary>
[Theory]
[RequiresDocker]
[InlineData("http", false)]
[InlineData("http", true)]
[InlineData("https", false)]
[InlineData("https", true)]
public async Task CanOverrideLaunchProfileViaArgsAdHocBuilder(string launchProfileName, bool directArgs)
{
var arg = $"DOTNET_LAUNCH_PROFILE={launchProfileName}";
string[] args;
Action<DistributedApplicationOptions, HostApplicationBuilderSettings> configureBuilder;
if (directArgs)
{
args = [arg];
configureBuilder = (_, _) => { };
}
else
{
args = [];
configureBuilder = (dao, habs) => habs.Args = [arg];
}

var builder = DistributedApplicationTestingBuilder.Create(args, configureBuilder);
builder.AddProject<Projects.TestingAppHost1_MyWebApp>("mywebapp1")
.WithEnvironment("LAUNCH_PROFILE_VAR_FROM_APP_HOST", builder.Configuration["LAUNCH_PROFILE_VAR_FROM_APP_HOST"]);
await using var app = await builder.BuildAsync();
await app.StartAsync();

// Wait for the application to be ready
await app.WaitForTextAsync("Application started.").WaitAsync(TimeSpan.FromMinutes(1));

var httpClient = app.CreateHttpClientWithResilience("mywebapp1");
var appHostArg = await httpClient.GetStringAsync("/get-launch-profile-var");
Assert.NotNull(appHostArg);
Assert.Equal($"it-is-{launchProfileName}", appHostArg);

// Check that, aside from the launch profile, the app host loaded environment settings from its launch profile
var appHostLaunchProfileVar = await httpClient.GetStringAsync("/get-launch-profile-var-from-app-host");
Assert.NotNull(appHostLaunchProfileVar);
Assert.Equal($"app-host-is-{launchProfileName}", appHostLaunchProfileVar);
}

[Theory]
[RequiresDocker]
[InlineData(false)]
Expand Down Expand Up @@ -203,7 +367,7 @@ public async Task SelectsFirstLaunchProfile(bool genericEntryPoint)
await using var app = await appHost.BuildAsync();
await app.StartAsync();
var config = app.Services.GetRequiredService<IConfiguration>();
var profileName = config["AppHost:DefaultLaunchProfileName"];
var profileName = config["DOTNET_LAUNCH_PROFILE"];
Assert.Equal("https", profileName);

// Wait for the application to be ready
Expand Down
Loading
Loading