| File: TestServices\WorkloadTestExecutor.cs | Web Access |
| Project: src\tests\Aspire.Hosting.Native.Core.Tests\Aspire.Hosting.Native.Core.Tests.csproj (Aspire.Hosting.Native.Core.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Collections.Immutable; using Aspire.Hosting.Native.Runtime; namespace Aspire.Hosting.Native.Core.Tests.TestServices; /// <summary>Tracks execution and gates cleanup without substituting the native model or runtime services.</summary> internal sealed class WorkloadTestExecutor : IWorkloadExecutor, IWorkloadPreparationExecutor { private readonly TaskCompletionSource _cleanup = new(TaskCreationOptions.RunContinuationsAsynchronously); public ConcurrentQueue<WorkloadIdentity> Started { get; } = new(); public ConcurrentQueue<WorkloadPlan> Plans { get; } = new(); public ConcurrentQueue<WorkloadIdentity> Prepared { get; } = new(); public ConcurrentQueue<WorkloadIdentity> ReleasedPreparations { get; } = new(); public ConcurrentQueue<WorkloadIdentity> Removed { get; } = new(); public TaskCompletionSource CleanupStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); public Exception? StartFailure { get; init; } public Func<WorkloadIdentity, CancellationToken, Task>? BeforePrepare { get; init; } public Func<CancellationToken, Task>? BeforeStart { get; init; } public Func<WorkloadIdentity, CancellationToken, Task>? BeforeStartResource { get; init; } public Func<CancellationToken, Task>? BeforeReadStatus { get; init; } public Func<WorkloadIdentity, Task>? BeforeCleanup { get; init; } public WorkloadPhase Phase { get; set; } = WorkloadPhase.Running; public int? ExitCode { get; set; } public Func<WorkloadPlan, WorkloadStatus>? StatusForPlan { get; init; } public void ReleaseCleanup() => _cleanup.TrySetResult(); public void FailCleanup() => _cleanup.TrySetException(new InvalidOperationException("Test workload cleanup failed.")); public async Task<IWorkloadPreparation> PrepareAsync(WorkloadIdentity identity, WorkloadPlan template, CancellationToken cancellationToken) { if (BeforePrepare is { } before) { await before(identity, cancellationToken).ConfigureAwait(false); } cancellationToken.ThrowIfCancellationRequested(); Prepared.Enqueue(identity); return new Preparation(this, identity, template); } public async Task<IWorkloadLease> StartAsync(WorkloadIdentity identity, WorkloadPlan plan, CancellationToken cancellationToken) { if (BeforeStartResource is { } resourceCallback) { await resourceCallback(identity, cancellationToken).ConfigureAwait(false); } if (BeforeStart is { } before) { await before(cancellationToken).ConfigureAwait(false); } cancellationToken.ThrowIfCancellationRequested(); Started.Enqueue(identity); Plans.Enqueue(plan); if (StartFailure is not null) { throw StartFailure; } return new Lease(this, identity, plan); } private sealed class Lease(WorkloadTestExecutor executor, WorkloadIdentity identity, WorkloadPlan plan) : IWorkloadLease { public WorkloadEndpoint Endpoint { get; } = new("test-instance", plan.Endpoints.Select((endpoint, index) => new AllocatedWorkloadPort( endpoint.Name, endpoint.Protocol, endpoint.UriScheme, "127.0.0.1", 5050 + index)).ToImmutableArray()); public async Task<WorkloadStatus> ReadStatusAsync(CancellationToken cancellationToken) { if (executor.BeforeReadStatus is { } before) { await before(cancellationToken).ConfigureAwait(false); } return executor.StatusForPlan?.Invoke(plan) ?? new WorkloadStatus(executor.Phase, executor.ExitCode); } public Task<ImmutableArray<WorkloadLog>> ReadLogsAsync(CancellationToken cancellationToken) => Task.FromResult(ImmutableArray<WorkloadLog>.Empty); public async ValueTask DisposeAsync() { executor.CleanupStarted.TrySetResult(); await executor._cleanup.Task.ConfigureAwait(false); if (executor.BeforeCleanup is { } before) { await before(identity).ConfigureAwait(false); } executor.Removed.Enqueue(identity); } } private sealed class Preparation(WorkloadTestExecutor executor, WorkloadIdentity identity, WorkloadPlan template) : IWorkloadPreparation { private IWorkloadLease? _lease; private bool _disposed; public WorkloadEndpoint Endpoint => _lease?.Endpoint ?? new("", template.Endpoints.Select((endpoint, index) => new AllocatedWorkloadPort( endpoint.Name, endpoint.Protocol, endpoint.UriScheme, "127.0.0.1", 5050 + index)).ToImmutableArray()); public async Task StartAsync(WorkloadPlan plan, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); if (_lease is not null) { throw new InvalidOperationException("The prepared workload has already started."); } _lease = await executor.StartAsync(identity, plan, cancellationToken).ConfigureAwait(false); } public Task<WorkloadStatus> ReadStatusAsync(CancellationToken cancellationToken) => (_lease ?? throw new InvalidOperationException("The workload has not started.")).ReadStatusAsync(cancellationToken); public Task<ImmutableArray<WorkloadLog>> ReadLogsAsync(CancellationToken cancellationToken) => (_lease ?? throw new InvalidOperationException("The workload has not started.")).ReadLogsAsync(cancellationToken); public async ValueTask DisposeAsync() { if (_disposed) { return; } if (_lease is not null) { await _lease.DisposeAsync().ConfigureAwait(false); } executor.ReleasedPreparations.Enqueue(identity); _disposed = true; } } }