SDT/tests/DevTool.Tests/ScriptCommonTests.cs
stan44 2c5493f249
Some checks failed
reliability-matrix / ubuntu-latest / .NET tests (push) Failing after 2m46s
reliability-matrix / macos-latest / .NET tests (push) Has been cancelled
reliability-matrix / windows-latest / .NET tests (push) Has been cancelled
second first push?
2026-03-01 21:40:14 -06:00

191 lines
6.2 KiB
C#

using System.Diagnostics;
using System.Runtime.CompilerServices;
using Xunit;
namespace DevTool.Tests;
public sealed class ScriptCommonTests
{
[Fact]
public async Task ResolveRepoRoot_UsesGlobRootHints()
{
var root = CreateTempDir("sdt-script-root-");
var nested = Path.Combine(root, "src", "app");
Directory.CreateDirectory(nested);
await File.WriteAllTextAsync(Path.Combine(root, "sample.sln"), "");
await File.WriteAllTextAsync(Path.Combine(root, "devtool.json"), """
{
"name": "demo",
"version": "0.1.0",
"workflows": [],
"project": {
"rootHints": ["*.sln"]
}
}
""");
var output = await RunPythonAsync(nested, "import script_common; print(script_common.resolve_repo_root(r'" + Escape(nested) + "'))");
Assert.Equal(Path.GetFullPath(root), output.Trim());
}
[Fact]
public async Task ResolveRepoRoot_UsesDirectoryMarkerHints()
{
var root = CreateTempDir("sdt-script-root-");
var nested = Path.Combine(root, "child", "leaf");
Directory.CreateDirectory(nested);
Directory.CreateDirectory(Path.Combine(root, ".git"));
await File.WriteAllTextAsync(Path.Combine(root, "devtool.json"), """
{
"name": "demo",
"version": "0.1.0",
"workflows": [],
"project": {
"rootHints": [".git", "package.json"]
}
}
""");
var output = await RunPythonAsync(nested, "import script_common; print(script_common.resolve_repo_root(r'" + Escape(nested) + "'))");
Assert.Equal(Path.GetFullPath(root), output.Trim());
}
[Fact]
public async Task ResolveCommand_ExpandsWindowsPathTokens()
{
if (!OperatingSystem.IsWindows())
return;
var root = CreateTempDir("sdt-script-cmd-");
var shimDir = Path.Combine(root, "nodejs");
Directory.CreateDirectory(shimDir);
await File.WriteAllTextAsync(Path.Combine(shimDir, "npm.cmd"), "@echo off");
var output = await RunPythonAsync(
root,
"import script_common; print(script_common.resolve_command('npm'))",
new Dictionary<string, string?>
{
["NVM_HOME"] = root,
["NVM_SYMLINK"] = shimDir,
["PATH"] = "%NVM_HOME%;%NVM_SYMLINK%",
});
Assert.EndsWith("npm.cmd", output.Trim(), StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task FindNodeAppRoot_PrefersTauriAppOverRootPackageJson()
{
var root = CreateTempDir("sdt-script-node-root-");
await File.WriteAllTextAsync(Path.Combine(root, "package.json"), """
{
"dependencies": {
"left-pad": "1.3.0"
}
}
""");
var tauriRoot = Path.Combine(root, "src", "DevTool.Host.Gui", "TauriShell");
Directory.CreateDirectory(Path.Combine(tauriRoot, "src-tauri"));
await File.WriteAllTextAsync(Path.Combine(tauriRoot, "src-tauri", "tauri.conf.json"), "{}");
await File.WriteAllTextAsync(Path.Combine(tauriRoot, "package.json"), """
{
"scripts": {
"tauri": "tauri",
"build": "vite build"
}
}
""");
var output = await RunPythonAsync(
root,
"import pathlib, script_common; print(script_common.find_node_app_root(pathlib.Path(r'" + Escape(root) + "'), None))");
Assert.Equal(Path.GetFullPath(tauriRoot), output.Trim());
}
private static async Task<string> RunPythonAsync(
string workingDir,
string script,
IReadOnlyDictionary<string, string?>? env = null)
{
var python = ResolvePython();
var psi = new ProcessStartInfo
{
FileName = python,
WorkingDirectory = workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add("-c");
psi.ArgumentList.Add($"import sys; sys.path.insert(0, r'{Escape(Path.Combine(ProjectRepoRoot(), "scripts"))}'); {script}");
if (env is not null)
{
foreach (var pair in env)
psi.Environment[pair.Key] = pair.Value ?? string.Empty;
}
using var process = new Process { StartInfo = psi };
process.Start();
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
throw new InvalidOperationException($"Python exited {process.ExitCode}: {stderr}");
return stdout;
}
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.");
}
private static string CreateTempDir(string prefix)
{
var path = Path.Combine(Path.GetTempPath(), prefix + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("'", "\\'");
private static string ProjectRepoRoot([CallerFilePath] string file = "")
{
var repoRoot = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", ".."));
if (!File.Exists(Path.Combine(repoRoot, "scripts", "script_common.py")))
throw new InvalidOperationException("Could not locate project repo root.");
return repoRoot;
}
}