diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6bbdb82e8..76457b6d8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -61,6 +61,26 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install jupyter and ipykernel + run: | + python -m pip install --upgrade pip + python -m pip install jupyter + python -m pip install ipykernel + - name: list available kernels + run: | + python -m jupyter kernelspec list + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - run: uv sync --locked --all-extras + working-directory: ./python + - name: Prepare python venv + run: | + source ${{ github.workspace }}/python/.venv/bin/activate # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 52701740d..e70a943cc 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -76,12 +76,18 @@ jobs: - name: list available kernels run: | python -m jupyter kernelspec list - - name: Setup .NET + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - run: uv sync --locked --all-extras + working-directory: ./python + - name: Prepare python venv + run: | + source ${{ github.workspace }}/python/.venv/bin/activate + - name: Setup .NET 8.0 uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' - - name: Install .NET Aspire workload - run: dotnet workload install aspire - name: Restore dependencies run: | # dotnet nuget add source --name dotnet-tool https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json --configfile NuGet.config @@ -96,7 +102,57 @@ jobs: echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - name: Unit Test - run: dotnet test --no-build -bl --configuration Release + run: dotnet test --no-build -bl --configuration Release --filter type=!integration + + integration-test: + strategy: + fail-fast: true + matrix: + os: [ ubuntu-latest] + version: [ net8.0 ] + needs: build + defaults: + run: + working-directory: dotnet + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: uv sync --locked --all-extras + working-directory: ./python + - name: Prepare python venv + run: | + source ${{ github.workspace }}/python/.venv/bin/activate + - name: Setup .NET 9.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + - name: Install Temp Global.JSON + run: | + echo "{\"sdk\": {\"version\": \"9.0.101\"}}" > global.json + - name: Install .NET Aspire workload + run: dotnet workload install aspire + - name: Install dev certs + run: dotnet --version && dotnet dev-certs https --trust + - name: Restore dependencies + run: | + dotnet restore -bl + - name: Build + run: | + echo "Build AutoGen" + dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true + - name: Integration Test + run: dotnet --version && dotnet test --no-build -bl --configuration Release --filter type=integration + - name: Restore the global.json + run: rm global.json && git checkout -- global.json + aot-test: # this make sure the AutoGen.Core is aot compatible strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails @@ -147,9 +203,17 @@ jobs: - name: list available kernels run: | python -m jupyter kernelspec list - - name: Setup .NET + - uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Setup .NET 8.0 uses: actions/setup-dotnet@v4 with: + dotnet-version: '8.0.x' global-json-file: dotnet/global.json - name: Restore dependencies run: | @@ -159,7 +223,7 @@ jobs: echo "Build AutoGen" dotnet build --no-restore --configuration Release -bl /p:SignAssembly=true - name: OpenAI Test - run: dotnet test --no-build -bl --configuration Release + run: dotnet test --no-build -bl --configuration Release --filter type!=integration env: AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index 5b26e2716..21c1a62a3 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -132,6 +132,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Extension EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Agents.Tests", "test\Microsoft.AutoGen.Agents.Tests\Microsoft.AutoGen.Agents.Tests.csproj", "{394FDAF8-74F9-4977-94A5-3371737EB774}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AutoGen.Integration.Tests", "test\Microsoft.AutoGen.Integration.Tests\Microsoft.AutoGen.Integration.Tests.csproj", "{D04C6153-8EAF-4E54-9852-52CEC1BE8D31}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -342,6 +344,10 @@ Global {394FDAF8-74F9-4977-94A5-3371737EB774}.Debug|Any CPU.Build.0 = Debug|Any CPU {394FDAF8-74F9-4977-94A5-3371737EB774}.Release|Any CPU.ActiveCfg = Release|Any CPU {394FDAF8-74F9-4977-94A5-3371737EB774}.Release|Any CPU.Build.0 = Release|Any CPU + {D04C6153-8EAF-4E54-9852-52CEC1BE8D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D04C6153-8EAF-4E54-9852-52CEC1BE8D31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D04C6153-8EAF-4E54-9852-52CEC1BE8D31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D04C6153-8EAF-4E54-9852-52CEC1BE8D31}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -402,6 +408,7 @@ Global {64EF61E7-00A6-4E5E-9808-62E10993A0E5} = {7EB336C2-7C0A-4BC8-80C6-A3173AB8DC45} {65059914-5527-4A00-9308-9FAF23D5E85A} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {394FDAF8-74F9-4977-94A5-3371737EB774} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {D04C6153-8EAF-4E54-9852-52CEC1BE8D31} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 008c28647..8223a3e1b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -15,6 +15,7 @@ + @@ -31,12 +32,17 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + @@ -54,6 +60,7 @@ + @@ -118,5 +125,6 @@ + - + \ No newline at end of file diff --git a/dotnet/test/Microsoft.AutoGen.Integration.Tests/HelloAppHostIntegrationTests.cs b/dotnet/test/Microsoft.AutoGen.Integration.Tests/HelloAppHostIntegrationTests.cs new file mode 100644 index 000000000..dea3eec41 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Integration.Tests/HelloAppHostIntegrationTests.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// HelloAppHostIntegrationTests.cs + +using System.Text.Json; +using Xunit.Abstractions; + +namespace Microsoft.AutoGen.Integration.Tests; + +public class HelloAppHostIntegrationTests(ITestOutputHelper testOutput) +{ + [Theory, Trait("type", "integration")] + [MemberData(nameof(AppHostAssemblies))] + public async Task AppHostRunsCleanly(string appHostPath) + { + var appHost = await DistributedApplicationTestFactory.CreateAsync(appHostPath, testOutput); + await using var app = await appHost.BuildAsync().WaitAsync(TimeSpan.FromSeconds(15)); + + await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(120)); + await app.WaitForResourcesAsync().WaitAsync(TimeSpan.FromSeconds(120)); + + app.EnsureNoErrorsLogged(); + await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(15)); + } + + [Theory, Trait("type", "integration")] + [MemberData(nameof(TestEndpoints))] + public async Task AppHostLogsHelloAgentE2E(TestEndpoints testEndpoints) + { + var appHostName = testEndpoints.AppHost!; + var appHostPath = $"{appHostName}.dll"; + var appHost = await DistributedApplicationTestFactory.CreateAsync(appHostPath, testOutput); + await using var app = await appHost.BuildAsync().WaitAsync(TimeSpan.FromSeconds(15)); + + await app.StartAsync().WaitAsync(TimeSpan.FromSeconds(120)); + await app.WaitForResourcesAsync().WaitAsync(TimeSpan.FromSeconds(120)); + if (testEndpoints.WaitForResources?.Count > 0) + { + // Wait until each resource transitions to the required state + var timeout = TimeSpan.FromMinutes(5); + foreach (var (ResourceName, TargetState) in testEndpoints.WaitForResources) + { + await app.WaitForResource(ResourceName, TargetState).WaitAsync(timeout); + } + } + //sleep 5 seconds to make sure the app is running + await Task.Delay(5000); + app.EnsureNoErrorsLogged(); + app.EnsureLogContains("HelloAgents said Goodbye"); + app.EnsureLogContains("Wild Hello from Python!"); + + await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(15)); + } + public static TheoryData AppHostAssemblies() + { + var appHostAssemblies = GetSamplesAppHostAssemblyPaths(); + var theoryData = new TheoryData(); + return new(appHostAssemblies.Select(p => Path.GetRelativePath(AppContext.BaseDirectory, p))); + } + + public static TheoryData TestEndpoints() => + new([ + new TestEndpoints("Hello.AppHost", new() { + { "backend", ["/"] } + }), + ]); + + private static IEnumerable GetSamplesAppHostAssemblyPaths() + { + // All the AppHost projects are referenced by this project so we can find them by looking for all their assemblies in the base directory + return Directory.GetFiles(AppContext.BaseDirectory, "*.AppHost.dll") + .Where(fileName => !fileName.EndsWith("Aspire.Hosting.AppHost.dll", StringComparison.OrdinalIgnoreCase)); + } +} + +public class TestEndpoints : IXunitSerializable +{ + // Required for deserialization + public TestEndpoints() { } + + public TestEndpoints(string appHost, Dictionary> resourceEndpoints) + { + AppHost = appHost; + ResourceEndpoints = resourceEndpoints; + } + + public string? AppHost { get; set; } + + public List? WaitForResources { get; set; } + + public Dictionary>? ResourceEndpoints { get; set; } + + public void Deserialize(IXunitSerializationInfo info) + { + AppHost = info.GetValue(nameof(AppHost)); + WaitForResources = JsonSerializer.Deserialize>(info.GetValue(nameof(WaitForResources))); + ResourceEndpoints = JsonSerializer.Deserialize>>(info.GetValue(nameof(ResourceEndpoints))); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(AppHost), AppHost); + info.AddValue(nameof(WaitForResources), JsonSerializer.Serialize(WaitForResources)); + info.AddValue(nameof(ResourceEndpoints), JsonSerializer.Serialize(ResourceEndpoints)); + } + + public override string? ToString() => $"{AppHost} ({ResourceEndpoints?.Count ?? 0} resources)"; + + public class ResourceWait(string resourceName, string targetState) + { + public string ResourceName { get; } = resourceName; + + public string TargetState { get; } = targetState; + + public void Deconstruct(out string resourceName, out string targetState) + { + resourceName = ResourceName; + targetState = TargetState; + } + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Integration.Tests/Infrastructure/DistributedApplicationExtension.cs b/dotnet/test/Microsoft.AutoGen.Integration.Tests/Infrastructure/DistributedApplicationExtension.cs new file mode 100644 index 000000000..e3ad24f9f --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Integration.Tests/Infrastructure/DistributedApplicationExtension.cs @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DistributedApplicationExtension.cs + +using System.Security.Cryptography; +using Aspire.Hosting; +using Aspire.Hosting.Python; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace Microsoft.AutoGen.Integration.Tests; + +public static partial class DistributedApplicationExtensions +{ + /* /// + /// Ensures all parameters in the application configuration have values set. + /// + public static TBuilder WithRandomParameterValues(this TBuilder builder) + where TBuilder : IDistributedApplicationTestingBuilder + { + var parameters = builder.Resources.OfType().Where(p => !p.IsConnectionString).ToList(); + foreach (var parameter in parameters) + { + builder.Configuration[$"Parameters:{parameter.Name}"] = parameter.Secret + ? PasswordGenerator.Generate(16, true, true, true, false, 1, 1, 1, 0) + : Convert.ToHexString(RandomNumberGenerator.GetBytes(4)); + } + + return builder; + } */ + + /// + /// Sets the container lifetime for all container resources in the application. + /// + public static TBuilder WithContainersLifetime(this TBuilder builder, ContainerLifetime containerLifetime) + where TBuilder : IDistributedApplicationTestingBuilder + { + var containerLifetimeAnnotations = builder.Resources.SelectMany(r => r.Annotations + .OfType() + .Where(c => c.Lifetime != containerLifetime)) + .ToList(); + + foreach (var annotation in containerLifetimeAnnotations) + { + annotation.Lifetime = containerLifetime; + } + + return builder; + } + + /// + /// Replaces all named volumes with anonymous volumes so they're isolated across test runs and from the volume the app uses during development. + /// + /// + /// 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. + /// + public static TBuilder WithRandomVolumeNames(this TBuilder builder) + where TBuilder : IDistributedApplicationTestingBuilder + { + // Named volumes that aren't shared across resources should be replaced with anonymous volumes. + // Named volumes shared by mulitple resources need to have their name randomized but kept shared across those resources. + + // Find all shared volumes and make a map of their original name to a new randomized name + var allResourceNamedVolumes = builder.Resources.SelectMany(r => r.Annotations + .OfType() + .Where(m => m.Type == ContainerMountType.Volume && !string.IsNullOrEmpty(m.Source)) + .Select(m => (Resource: r, Volume: m))) + .ToList(); + var seenVolumes = new HashSet(); + var renamedVolumes = new Dictionary(); + foreach (var resourceVolume in allResourceNamedVolumes) + { + var name = resourceVolume.Volume.Source!; + if (!seenVolumes.Add(name) && !renamedVolumes.ContainsKey(name)) + { + renamedVolumes[name] = $"{name}-{Convert.ToHexString(RandomNumberGenerator.GetBytes(4))}"; + } + } + + // Replace all named volumes with randomly named or anonymous volumes + foreach (var resourceVolume in allResourceNamedVolumes) + { + var resource = resourceVolume.Resource; + var volume = resourceVolume.Volume; + var newName = renamedVolumes.TryGetValue(volume.Source!, out var randomName) ? randomName : null; + var newMount = new ContainerMountAnnotation(newName, volume.Target, ContainerMountType.Volume, volume.IsReadOnly); + resource.Annotations.Remove(volume); + resource.Annotations.Add(newMount); + } + + return builder; + } + + /// + /// Waits for the specified resource to reach the specified state. + /// + public static Task WaitForResource(this DistributedApplication app, string resourceName, string? targetState = null, CancellationToken cancellationToken = default) + { + targetState ??= KnownResourceStates.Running; + var resourceNotificationService = app.Services.GetRequiredService(); + + return resourceNotificationService.WaitForResourceAsync(resourceName, targetState, cancellationToken); + } + + /// + /// Waits for all resources in the application to reach one of the specified states. + /// + /// + /// If is null, the default states are and . + /// + public static async Task WaitForResourcesAsync(this DistributedApplication app, IEnumerable? targetStates = null, CancellationToken cancellationToken = default) + { + var logger = app.Services.GetRequiredService().CreateLogger(nameof(WaitForResourcesAsync)); + + targetStates ??= [KnownResourceStates.Running, KnownResourceStates.Hidden, .. KnownResourceStates.TerminalStates]; + var applicationModel = app.Services.GetRequiredService(); + var resourceNotificationService = app.Services.GetRequiredService(); + + var resourceTasks = new Dictionary>(); + + foreach (var resource in applicationModel.Resources) + { + resourceTasks[resource.Name] = GetResourceWaitTask(resource.Name, targetStates, cancellationToken); + } + + logger.LogInformation("Waiting for resources [{Resources}] to reach one of target states [{TargetStates}].", + string.Join(',', resourceTasks.Keys), + string.Join(',', targetStates)); + + while (resourceTasks.Count > 0) + { + var completedTask = await Task.WhenAny(resourceTasks.Values); + var (completedResourceName, targetStateReached) = await completedTask; + + if (targetStateReached == KnownResourceStates.FailedToStart) + { + throw new DistributedApplicationException($"Resource '{completedResourceName}' failed to start."); + } + + resourceTasks.Remove(completedResourceName); + + logger.LogInformation("Wait for resource '{ResourceName}' completed with state '{ResourceState}'", completedResourceName, targetStateReached); + + // Ensure resources being waited on still exist + var remainingResources = resourceTasks.Keys.ToList(); + for (var i = remainingResources.Count - 1; i > 0; i--) + { + var name = remainingResources[i]; + if (!applicationModel.Resources.Any(r => r.Name == name)) + { + logger.LogInformation("Resource '{ResourceName}' was deleted while waiting for it.", name); + resourceTasks.Remove(name); + remainingResources.RemoveAt(i); + } + } + + if (resourceTasks.Count > 0) + { + logger.LogInformation("Still waiting for resources [{Resources}] to reach one of target states [{TargetStates}].", + string.Join(',', remainingResources), + string.Join(',', targetStates)); + } + } + + logger.LogInformation("Wait for all resources completed successfully!"); + + async Task<(string Name, string State)> GetResourceWaitTask(string resourceName, IEnumerable targetStates, CancellationToken cancellationToken) + { + var state = await resourceNotificationService.WaitForResourceAsync(resourceName, targetStates, cancellationToken); + return (resourceName, state); + } + } + + /// + /// Gets the app host and resource logs from the application. + /// + public static (IReadOnlyList AppHostLogs, IReadOnlyList ResourceLogs) GetLogs(this DistributedApplication app) + { + var environment = app.Services.GetRequiredService(); + var logCollector = app.Services.GetFakeLogCollector(); + var logs = logCollector.GetSnapshot(); + var appHostLogs = logs.Where(l => l.Category?.StartsWith($"{environment.ApplicationName}.Resources") == false).ToList(); + var resourceLogs = logs.Where(l => l.Category?.StartsWith($"{environment.ApplicationName}.Resources") == true).ToList(); + + return (appHostLogs, resourceLogs); + } + + /// + /// Get all logs from the whole test run. + /// + /// + /// List + public static IReadOnlyList GetAllLogs(this DistributedApplication app) + { + var logCollector = app.Services.GetFakeLogCollector(); + return logCollector.GetSnapshot(); + } + + /// + /// Asserts that no errors were logged by the application or any of its resources. + /// + /// + /// Some resource types are excluded from this check because they tend to write to stderr for various non-error reasons. + /// + /// + public static void EnsureNoErrorsLogged(this DistributedApplication app) + { + var environment = app.Services.GetRequiredService(); + var applicationModel = app.Services.GetRequiredService(); + var assertableResourceLogNames = applicationModel.Resources.Where(ShouldAssertErrorsForResource).Select(r => $"{environment.ApplicationName}.Resources.{r.Name}").ToList(); + + var (appHostlogs, resourceLogs) = app.GetLogs(); + + Assert.DoesNotContain(appHostlogs, log => log.Level >= LogLevel.Error); + Assert.DoesNotContain(resourceLogs, log => log.Category is { Length: > 0 } category && assertableResourceLogNames.Contains(category) && log.Level >= LogLevel.Error); + + static bool ShouldAssertErrorsForResource(IResource resource) + { +#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. + return resource + is + // Container resources tend to write to stderr for various reasons so only assert projects and executables + (ProjectResource or ExecutableResource) + // Node & Python resources tend to have modules that write to stderr so ignore them + and not (PythonAppResource) + // Dapr resources write to stderr about deprecated --components-path flag + && !resource.Name.EndsWith("-dapr-cli"); +#pragma warning restore ASPIREHOSTINGPYTHON001 + } + } + + /// + /// Asserts that the application and resource logs contain the specified message. + /// + /// + /// + public static void EnsureLogContains(this DistributedApplication app, string message) + { + var resourceLogs = app.GetAllLogs(); + Assert.Contains(resourceLogs, log => log.Message.Contains(message)); + } + + /// + /// Creates an configured to communicate with the specified resource. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, bool useHttpClientFactory) + => app.CreateHttpClient(resourceName, null, useHttpClientFactory); + + /// + /// Creates an configured to communicate with the specified resource. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, bool useHttpClientFactory) + { + if (useHttpClientFactory) + { + return app.CreateHttpClient(resourceName, endpointName); + } + + // Don't use the HttpClientFactory to create the HttpClient so, e.g., no resilience policies are applied + var httpClient = new HttpClient + { + BaseAddress = app.GetEndpoint(resourceName, endpointName) + }; + + return httpClient; + } + + /// + /// Creates an configured to communicate with the specified resource with custom configuration. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, Action configure) + { + var services = new ServiceCollection() + .AddHttpClient() + .ConfigureHttpClientDefaults(configure) + .BuildServiceProvider(); + var httpClientFactory = services.GetRequiredService(); + + var httpClient = httpClientFactory.CreateClient(); + httpClient.BaseAddress = app.GetEndpoint(resourceName, endpointName); + + return httpClient; + } + + private static bool DerivesFromDbContext(Type type) + { + var baseType = type.BaseType; + + while (baseType is not null) + { + if (baseType.FullName == "Microsoft.EntityFrameworkCore.DbContext" && baseType.Assembly.GetName().Name == "Microsoft.EntityFrameworkCore") + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Integration.Tests/Infrastructure/DistributedApplicationTestFactory.cs b/dotnet/test/Microsoft.AutoGen.Integration.Tests/Infrastructure/DistributedApplicationTestFactory.cs new file mode 100644 index 000000000..8cd09e5c8 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Integration.Tests/Infrastructure/DistributedApplicationTestFactory.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// DistributedApplicationTestFactory.cs + +using System.Reflection; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.AutoGen.Integration.Tests; + +internal static class DistributedApplicationTestFactory +{ + /// + /// Creates an for the specified app host assembly. + /// + public static async Task CreateAsync(string appHostAssemblyPath, ITestOutputHelper? testOutput) + { + var appHostProjectName = Path.GetFileNameWithoutExtension(appHostAssemblyPath) ?? throw new InvalidOperationException("AppHost assembly was not found."); + + var appHostAssembly = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, appHostAssemblyPath)); + + var appHostType = appHostAssembly.GetTypes().FirstOrDefault(t => t.Name.EndsWith("_AppHost")) + ?? throw new InvalidOperationException("Generated AppHost type not found."); + + var builder = await DistributedApplicationTestingBuilder.CreateAsync(appHostType); + + //builder.WithRandomParameterValues(); + builder.WithRandomVolumeNames(); + builder.WithContainersLifetime(ContainerLifetime.Session); + + builder.Services.AddLogging(logging => + { + logging.ClearProviders(); + logging.AddSimpleConsole(); + logging.AddFakeLogging(); + if (testOutput is not null) + { + logging.AddXUnit(testOutput); + } + logging.SetMinimumLevel(LogLevel.Trace); + logging.AddFilter("Aspire", LogLevel.Trace); + logging.AddFilter(builder.Environment.ApplicationName, LogLevel.Trace); + }); + + return builder; + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Integration.Tests/Microsoft.AutoGen.Integration.Tests.csproj b/dotnet/test/Microsoft.AutoGen.Integration.Tests/Microsoft.AutoGen.Integration.Tests.csproj new file mode 100644 index 000000000..0f3adc1c3 --- /dev/null +++ b/dotnet/test/Microsoft.AutoGen.Integration.Tests/Microsoft.AutoGen.Integration.Tests.csproj @@ -0,0 +1,52 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + .venv + $(RepoRoot)..\python + + + + + + $(PythonVenvRoot)\$(PythonVirtualEnvironmentName)\ + True + + + + + + + +