// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using Aspire.Cli.Backchannel;
using Aspire.Cli.DotNet;
using Aspire.Cli.Utils;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;
namespace Aspire.Cli.Tests.TestServices;
internal sealed class TestDotNetCliRunner : IDotNetCliRunner
{
public Func<FileInfo, string, string, string?, bool, ProcessInvocationOptions, CancellationToken, int>? AddPackageAsyncCallback { get; set; }
public Func<FileInfo, FileInfo, ProcessInvocationOptions, CancellationToken, int>? AddProjectToSolutionAsyncCallback { get; set; }
public Func<FileInfo, bool, ProcessInvocationOptions, CancellationToken, int>? BuildAsyncCallback { get; set; }
public Func<FileInfo, ProcessInvocationOptions, CancellationToken, int>? RestoreAsyncCallback { get; set; }
public Func<FileInfo, ProcessInvocationOptions, CancellationToken, (int ExitCode, bool IsAspireHost, string? AspireHostingVersion)>? GetAppHostInformationAsyncCallback { get; set; }
public Func<DirectoryInfo, ProcessInvocationOptions, CancellationToken, (int ExitCode, string[] ConfigPaths)>? GetNuGetConfigPathsAsyncCallback { get; set; }
public Func<FileInfo, string[], string[], string[], ProcessInvocationOptions, CancellationToken, Task<(int ExitCode, JsonDocument? Output)>>? GetProjectItemsAndPropertiesAsyncCallbackWithTargetsAsync { get; set; }
public Func<FileInfo, string[], string[], string[], ProcessInvocationOptions, CancellationToken, (int ExitCode, JsonDocument? Output)>? GetProjectItemsAndPropertiesAsyncCallbackWithTargets { get; set; }
public Func<FileInfo, string[], string[], ProcessInvocationOptions, CancellationToken, Task<(int ExitCode, JsonDocument? Output)>>? GetProjectItemsAndPropertiesAsyncCallbackAsync { get; set; }
public Func<FileInfo, string[], string[], ProcessInvocationOptions, CancellationToken, (int ExitCode, JsonDocument? Output)>? GetProjectItemsAndPropertiesAsyncCallback { get; set; }
public Func<string, string, FileInfo?, string?, bool, ProcessInvocationOptions, CancellationToken, (int ExitCode, string? TemplateVersion)>? InstallTemplateAsyncCallback { get; set; }
public Func<string, string, string, ProcessInvocationOptions, CancellationToken, int>? NewProjectAsyncCallback { get; set; }
public string[]? LastNewProjectExtraArgs { get; private set; }
public Func<FileInfo, bool, bool, bool, string[], IDictionary<string, string>?, TaskCompletionSource<IAppHostCliBackchannel>?, ProcessInvocationOptions, CancellationToken, Task<int>>? RunAsyncCallback { get; set; }
public Func<FileInfo, string, DirectoryInfo, string[], IDictionary<string, string>?, TaskCompletionSource<IAppHostCliBackchannel>?, ProcessInvocationOptions, CancellationToken, Task<int>>? RunAppHostCommandAsyncCallback { get; set; }
public Func<DirectoryInfo, string, bool, bool, int, int, FileInfo?, bool, ProcessInvocationOptions, CancellationToken, (int ExitCode, NuGetPackage[]? Packages)>? SearchPackagesAsyncCallback { get; set; }
public Func<FileInfo, ProcessInvocationOptions, CancellationToken, (int ExitCode, IReadOnlyList<FileInfo> Projects)>? GetSolutionProjectsAsyncCallback { get; set; }
public Func<FileInfo, FileInfo, ProcessInvocationOptions, CancellationToken, int>? AddProjectReferenceAsyncCallback { get; set; }
public Task<int> AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return AddPackageAsyncCallback != null
? Task.FromResult(AddPackageAsyncCallback(projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken))
: throw new NotImplementedException();
}
public Task<int> AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return AddProjectToSolutionAsyncCallback != null
? Task.FromResult(AddProjectToSolutionAsyncCallback(solutionFile, projectFile, options, cancellationToken))
: Task.FromResult(0); // If not overridden, just return success.
}
public Task<int> BuildAsync(FileInfo projectFilePath, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return BuildAsyncCallback != null
? Task.FromResult(BuildAsyncCallback(projectFilePath, noRestore, options, cancellationToken))
: throw new NotImplementedException();
}
public Task<int> RestoreAsync(FileInfo projectFilePath, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return RestoreAsyncCallback != null
? Task.FromResult(RestoreAsyncCallback(projectFilePath, options, cancellationToken))
: Task.FromResult(0); // If not overridden, just return success.
}
public Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
var informationalVersion = VersionHelper.GetDefaultTemplateVersion();
return GetAppHostInformationAsyncCallback != null
? Task.FromResult(GetAppHostInformationAsyncCallback(projectFile, options, cancellationToken))
: Task.FromResult<(int, bool, string?)>((0, true, informationalVersion));
}
public Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return GetNuGetConfigPathsAsyncCallback != null
? Task.FromResult(GetNuGetConfigPathsAsyncCallback(workingDirectory, options, cancellationToken))
: Task.FromResult((0, GetGlobalNuGetPaths())); // If not overridden, return success with no config paths which will blow up.
}
private static string[] GetGlobalNuGetPaths()
{
return Environment.OSVersion.Platform switch
{
PlatformID.Win32NT => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "NuGet", "NuGet.Config")],
_ => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "NuGet.Config")],
};
}
public Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, string[] targets, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
// Prefer the targets-aware callbacks when tests need to assert on the targets argument.
if (GetProjectItemsAndPropertiesAsyncCallbackWithTargetsAsync != null)
{
return GetProjectItemsAndPropertiesAsyncCallbackWithTargetsAsync(projectFile, items, properties, targets, options, cancellationToken);
}
if (GetProjectItemsAndPropertiesAsyncCallbackWithTargets != null)
{
return Task.FromResult(GetProjectItemsAndPropertiesAsyncCallbackWithTargets(projectFile, items, properties, targets, options, cancellationToken));
}
if (GetProjectItemsAndPropertiesAsyncCallbackAsync != null)
{
return GetProjectItemsAndPropertiesAsyncCallbackAsync(projectFile, items, properties, options, cancellationToken);
}
if (GetProjectItemsAndPropertiesAsyncCallback != null)
{
return Task.FromResult(GetProjectItemsAndPropertiesAsyncCallback(projectFile, items, properties, options, cancellationToken));
}
// DotNetAppHostProject collapsed its three legacy MSBuild round-trips into a single
// GetProjectItemsAndPropertiesAsync call (issue #17197). Tests that pre-date that
// change still configure GetAppHostInformationAsyncCallback to control compatibility
// gating, so honor it here by synthesizing the JSON shape the production code expects.
if (GetAppHostInformationAsyncCallback != null)
{
var (exitCode, isAspireHost, aspireHostingVersion) = GetAppHostInformationAsyncCallback(projectFile, options, cancellationToken);
var versionJson = aspireHostingVersion is null ? "null" : $"\"{aspireHostingVersion}\"";
var json = $$"""
{
"Properties": {
"IsAspireHost": "{{(isAspireHost ? "true" : "false")}}",
"AspireHostingSDKVersion": {{versionJson}}
},
"Items": {}
}
""";
return Task.FromResult<(int, JsonDocument?)>((exitCode, JsonDocument.Parse(json)));
}
// Default response: shape it so DotNetAppHostProject treats the project as a valid
// Aspire host with the default template version. Most tests rely on the implicit
// "valid AppHost" default.
var defaultJson = $$"""
{
"Properties": {
"IsAspireHost": "true",
"AspireHostingSDKVersion": "{{VersionHelper.GetDefaultTemplateVersion()}}"
},
"Items": {}
}
""";
return Task.FromResult<(int, JsonDocument?)>((0, JsonDocument.Parse(defaultJson)));
}
public Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return InstallTemplateAsyncCallback != null
? Task.FromResult(InstallTemplateAsyncCallback(packageName, version, nugetConfigFile, nugetSource, force, options, cancellationToken))
: Task.FromResult<(int, string?)>((0, version)); // If not overridden, just return success for the version specified.
}
public Task<int> NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
LastNewProjectExtraArgs = extraArgs.ToArray();
return NewProjectAsyncCallback != null
? Task.FromResult(NewProjectAsyncCallback(templateName, name, outputPath, options, cancellationToken))
: Task.FromResult(0); // If not overridden, just return success.
}
public Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary<string, string>? env, TaskCompletionSource<IAppHostCliBackchannel>? backchannelCompletionSource, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return RunAsyncCallback != null
? RunAsyncCallback(projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken)
: throw new NotImplementedException();
}
public Task<int> RunAppHostCommandAsync(FileInfo projectFile, string command, DirectoryInfo workingDirectory, string[] args, IDictionary<string, string>? env, TaskCompletionSource<IAppHostCliBackchannel>? backchannelCompletionSource, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
if (RunAppHostCommandAsyncCallback is not null)
{
return RunAppHostCommandAsyncCallback(projectFile, command, workingDirectory, args, env, backchannelCompletionSource, options, cancellationToken);
}
return RunAsyncCallback is not null
? RunAsyncCallback(projectFile, false, true, false, args, env, backchannelCompletionSource, options, cancellationToken)
: throw new NotImplementedException();
}
public Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return SearchPackagesAsyncCallback != null
? Task.FromResult(SearchPackagesAsyncCallback(workingDirectory, query, exactMatch, prerelease, take, skip, nugetConfigFile, useCache, options, cancellationToken))
: throw new NotImplementedException();
}
public Task<(int ExitCode, IReadOnlyList<FileInfo> Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return GetSolutionProjectsAsyncCallback != null
? Task.FromResult(GetSolutionProjectsAsyncCallback(solutionFile, options, cancellationToken))
: Task.FromResult<(int, IReadOnlyList<FileInfo>)>((0, Array.Empty<FileInfo>()));
}
public Task<int> AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return AddProjectReferenceAsyncCallback != null
? Task.FromResult(AddProjectReferenceAsyncCallback(projectFile, referencedProject, options, cancellationToken))
: Task.FromResult(0);
}
public Task<int> InitUserSecretsAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> Task.FromResult(0);
}