SDT/tests/DevTool.Tests/ToolchainManagerServiceTests.cs
2026-03-01 20:52:56 -06:00

114 lines
4.6 KiB
C#

using Sdt.Config;
using Sdt.Core;
using Sdt.Runner;
using Xunit;
namespace DevTool.Tests;
public sealed class ToolchainManagerServiceTests
{
[Fact]
public async Task ProbeConfiguredTools_IncludesToolchainAndWorkflowRequirements()
{
var config = new DevToolConfig
{
Toolchains = new ToolchainConfig
{
Python = new PythonToolchain(),
Node = new NodeToolchain { PackageManager = "npm" }
},
Workflows =
[
new WorkflowDefinition
{
Id = "build",
Label = "Build",
Steps =
[
new WorkflowStep
{
Id = "s1",
Label = "S1",
Requires = [new ToolRequirement { Tool = "dotnet" }]
}
]
}
]
};
var service = new ToolchainManagerService(new AvailableProbe(), new NoOpInstaller());
var probes = await service.ProbeConfiguredToolsAsync(config, Directory.GetCurrentDirectory());
var tools = probes.Select(p => p.Tool).ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Contains("python", tools);
Assert.Contains("node", tools);
Assert.Contains("npm", tools);
Assert.Contains("dotnet", tools);
}
[Fact]
public async Task AutoFixMissingTools_AttemptsInstallAndVerifies()
{
var config = new DevToolConfig
{
Toolchains = new ToolchainConfig
{
Node = new NodeToolchain { PackageManager = "npm" }
}
};
var probe = new SequenceProbe();
var installer = new SuccessInstaller();
var service = new ToolchainManagerService(probe, installer);
var results = await service.AutoFixMissingToolsAsync(
config,
Directory.GetCurrentDirectory(),
(_, _) => Task.FromResult(true),
(_, _) => { });
Assert.Contains(results, r => r.Tool.Equals("npm", StringComparison.OrdinalIgnoreCase) && r.Success);
}
private sealed class AvailableProbe : IToolProbe
{
public Task<ProbeResult> ProbeAsync(string tool, string projectRoot, DevToolConfig? config = null, IReadOnlyDictionary<string, string>? envOverrides = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new ProbeResult(tool, true, Version: "1.0.0"));
}
private sealed class SequenceProbe : IToolProbe
{
private readonly Dictionary<string, int> _count = new(StringComparer.OrdinalIgnoreCase);
public Task<ProbeResult> ProbeAsync(string tool, string projectRoot, DevToolConfig? config = null, IReadOnlyDictionary<string, string>? envOverrides = null, CancellationToken cancellationToken = default)
{
_count.TryGetValue(tool, out var c);
_count[tool] = c + 1;
if (tool.Equals("node", StringComparison.OrdinalIgnoreCase))
return Task.FromResult(new ProbeResult(tool, true, Version: "1.0.0"));
// npm: first probe missing, later available (after install).
var available = c > 0;
return Task.FromResult(new ProbeResult(tool, available, Version: available ? "1.0.0" : null));
}
}
private sealed class NoOpInstaller : IPrereqInstaller
{
public Task<InstallPlan> GetInstallPlanAsync(string tool, string projectRoot, DevToolConfig? config = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new InstallPlan(tool, true, "noop", [new InstallCommand("echo", ["ok"])]));
public Task<RunResult> RunInstallAsync(InstallCommand command, string projectRoot, Action<string, bool> onOutput, IReadOnlyDictionary<string, string>? envOverrides = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new RunResult(0, TimeSpan.Zero));
}
private sealed class SuccessInstaller : IPrereqInstaller
{
public Task<InstallPlan> GetInstallPlanAsync(string tool, string projectRoot, DevToolConfig? config = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new InstallPlan(tool, true, "install", [new InstallCommand("echo", ["install"])]));
public Task<RunResult> RunInstallAsync(InstallCommand command, string projectRoot, Action<string, bool> onOutput, IReadOnlyDictionary<string, string>? envOverrides = null, CancellationToken cancellationToken = default)
=> Task.FromResult(new RunResult(0, TimeSpan.FromMilliseconds(5)));
}
}