350 lines
13 KiB
C#
350 lines
13 KiB
C#
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Runtime.CompilerServices;
|
|
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));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BuildAction_DotnetBuild_AutoSelectsSlnxTarget_WhenSingleSlnxFound()
|
|
{
|
|
var python = ResolvePython();
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
var slnx = Path.Combine(tempRoot, "sample.slnx");
|
|
await File.WriteAllTextAsync(slnx, "<Solution />");
|
|
|
|
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, slnx, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BuildAction_DotnetBuild_FallsBackToCsproj_WhenNoSolutionFound()
|
|
{
|
|
var python = ResolvePython();
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
var csproj = Path.Combine(tempRoot, "sample.csproj");
|
|
await File.WriteAllTextAsync(csproj, "<Project Sdk=\"Microsoft.NET.Sdk\" />");
|
|
|
|
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, csproj, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BuildAction_DotnetBuild_Skips_WhenNoDotnetTargetFound()
|
|
{
|
|
var python = ResolvePython();
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-dotnet-target-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
|
|
var result = await RunAsync(
|
|
python,
|
|
[
|
|
"scripts/build.py",
|
|
"dotnet-build",
|
|
"--project-root",
|
|
tempRoot,
|
|
"--working-dir",
|
|
".",
|
|
"--json"
|
|
]);
|
|
|
|
Assert.Equal(0, result.ExitCode);
|
|
var jsonText = ExtractLastJsonObject(result.StdOut);
|
|
using var doc = JsonDocument.Parse(jsonText);
|
|
Assert.Equal("skipped", doc.RootElement.GetProperty("status").GetString());
|
|
Assert.Equal("not_applicable_no_dotnet_target", doc.RootElement.GetProperty("skip_reason").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BuildAction_NpmBuild_Skips_WhenNoPackageJsonInWorkingDir()
|
|
{
|
|
var python = ResolvePython();
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-node-target-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
|
|
var result = await RunAsync(
|
|
python,
|
|
[
|
|
"scripts/build.py",
|
|
"npm-build",
|
|
"--project-root",
|
|
tempRoot,
|
|
"--working-dir",
|
|
".",
|
|
"--json"
|
|
]);
|
|
|
|
Assert.Equal(0, result.ExitCode);
|
|
var jsonText = ExtractLastJsonObject(result.StdOut);
|
|
using var doc = JsonDocument.Parse(jsonText);
|
|
Assert.Equal("skipped", doc.RootElement.GetProperty("status").GetString());
|
|
Assert.Equal("not_applicable_no_package_json", doc.RootElement.GetProperty("skip_reason").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BuildAction_NpmBuild_Skips_WhenBuildScriptMissing()
|
|
{
|
|
var python = ResolvePython();
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-node-target-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
await File.WriteAllTextAsync(Path.Combine(tempRoot, "package.json"), """{ "name": "demo", "dependencies": { "x": "1.0.0" } }""");
|
|
|
|
var result = await RunAsync(
|
|
python,
|
|
[
|
|
"scripts/build.py",
|
|
"npm-build",
|
|
"--project-root",
|
|
tempRoot,
|
|
"--working-dir",
|
|
".",
|
|
"--json"
|
|
]);
|
|
|
|
Assert.Equal(0, result.ExitCode);
|
|
var jsonText = ExtractLastJsonObject(result.StdOut);
|
|
using var doc = JsonDocument.Parse(jsonText);
|
|
Assert.Equal("skipped", doc.RootElement.GetProperty("status").GetString());
|
|
Assert.Equal("not_applicable_missing_build_script", doc.RootElement.GetProperty("skip_reason").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishOutput_SkipsNonApplicableStacks_InGenericRepo()
|
|
{
|
|
var python = ResolvePython();
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-publish-output-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
|
|
var result = await RunAsync(
|
|
python,
|
|
[
|
|
"scripts/publish-output.py",
|
|
"--repo-root",
|
|
tempRoot
|
|
]);
|
|
|
|
Assert.Equal(0, result.ExitCode);
|
|
Assert.Contains("Publish output workflow complete.", result.StdOut, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains("Skipping sidecar", result.StdOut, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishOutput_SkipsWeb_WhenBuildScriptMissing()
|
|
{
|
|
var python = ResolvePython();
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), "sdt-publish-output-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(tempRoot);
|
|
await File.WriteAllTextAsync(Path.Combine(tempRoot, "package.json"), """{ "name": "demo", "scripts": { "test": "echo ok" } }""");
|
|
|
|
var result = await RunAsync(
|
|
python,
|
|
[
|
|
"scripts/publish-output.py",
|
|
"--repo-root",
|
|
tempRoot
|
|
]);
|
|
|
|
Assert.Equal(0, result.ExitCode);
|
|
Assert.Contains("Skipping web: package.json has no 'build' script.", result.StdOut, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains("Publish output workflow complete.", result.StdOut, 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<string> 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([CallerFilePath] string file = "")
|
|
{
|
|
var repoRoot = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", ".."));
|
|
if (!File.Exists(Path.Combine(repoRoot, "scripts", "diag.py")))
|
|
throw new InvalidOperationException("Could not locate repo root (scripts/diag.py not found).");
|
|
return repoRoot;
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|