191 lines
6.2 KiB
C#
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;
|
|
}
|
|
}
|