using System.Diagnostics; using System.Text.Json; using Xunit; namespace DevTool.Tests; public sealed class ScriptSmokeTests { [Fact] public async Task DiagProbe_JsonContract_IsValid() { var python = ResolvePython(); var result = await RunAsync( python, ["scripts/diag.py", "probe", "--tool", "python", "--json"]); Assert.Equal(0, result.ExitCode); using var doc = JsonDocument.Parse(result.StdOut); Assert.True(doc.RootElement.TryGetProperty("tool", out _)); Assert.True(doc.RootElement.TryGetProperty("available", out _)); } [Fact] public async Task BuildAction_InvalidRequirements_PropagatesNonZeroAndJson() { var python = ResolvePython(); var missingReq = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); var result = await RunAsync( python, [ "scripts/build.py", "python-pip-install", "--project-root", RepoRoot(), "--requirements", missingReq, "--json" ]); Assert.NotEqual(0, result.ExitCode); var jsonText = ExtractLastJsonObject(result.StdOut); using var doc = JsonDocument.Parse(jsonText); Assert.True(doc.RootElement.TryGetProperty("exit_code", out var code)); Assert.NotEqual(0, code.GetInt32()); Assert.True(doc.RootElement.TryGetProperty("failure_reason", out _)); } [Fact] public async Task BuildAction_DotnetRestore_CommandNotFoundStillReturnsJson() { var python = ResolvePython(); var result = await RunAsync( python, [ "scripts/build.py", "dotnet-restore", "--project-root", RepoRoot(), "--json" ]); var jsonText = ExtractLastJsonObject(result.StdOut); using var doc = JsonDocument.Parse(jsonText); Assert.True(doc.RootElement.TryGetProperty("exit_code", out _)); Assert.True(doc.RootElement.TryGetProperty("status", out _)); } [Fact] public async Task BuildAction_DotnetBuild_AutoSelectsSlnTarget_WhenSingleSlnFound() { var python = ResolvePython(); var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempRoot); var sln = Path.Combine(tempRoot, "sample.sln"); await File.WriteAllTextAsync(sln, "Microsoft Visual Studio Solution File, Format Version 12.00"); var result = await RunAsync( python, [ "scripts/build.py", "dotnet-build", "--project-root", tempRoot, "--working-dir", ".", "--json" ]); var jsonText = ExtractLastJsonObject(result.StdOut); using var doc = JsonDocument.Parse(jsonText); Assert.True(doc.RootElement.TryGetProperty("args", out var args)); Assert.Contains(args.EnumerateArray().Select(x => x.GetString()), x => string.Equals(x, sln, StringComparison.OrdinalIgnoreCase)); } private static string ResolvePython() { var candidates = OperatingSystem.IsWindows() ? new[] { "python", "py" } : new[] { "python3", "python" }; foreach (var candidate in candidates) { try { using var process = new Process { StartInfo = new ProcessStartInfo { FileName = candidate, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, } }; process.StartInfo.ArgumentList.Add("--version"); process.Start(); process.WaitForExit(2000); if (process.ExitCode == 0) return candidate; } catch { } } throw new InvalidOperationException("Python executable not found for script smoke tests."); } private static async Task<(int ExitCode, string StdOut, string StdErr)> RunAsync(string command, IReadOnlyList args) { var psi = new ProcessStartInfo { FileName = command, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = RepoRoot(), }; foreach (var arg in args) psi.ArgumentList.Add(arg); using var process = new Process { StartInfo = psi }; process.Start(); var stdout = await process.StandardOutput.ReadToEndAsync(); var stderr = await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); return (process.ExitCode, stdout, stderr); } private static string RepoRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null) { if (File.Exists(Path.Combine(dir.FullName, "devtool.json"))) return dir.FullName; dir = dir.Parent; } throw new InvalidOperationException("Could not locate repo root (devtool.json not found)."); } private static string ExtractLastJsonObject(string text) { var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); for (var i = lines.Length - 1; i >= 0; i--) { var line = lines[i]; if (line.StartsWith("{", StringComparison.Ordinal) && line.EndsWith("}", StringComparison.Ordinal)) return line; } throw new InvalidOperationException("No JSON object line found in script output."); } }