| File: Scripts\Common\ScriptToolCommand.cs | Web Access |
| Project: src\tests\Aspire.Acquisition.Tests\Aspire.Acquisition.Tests.csproj (Aspire.Acquisition.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.TestUtilities; using Xunit; using Aspire.Templates.Tests; namespace Aspire.Acquisition.Tests.Scripts; /// <summary> /// Extends ToolCommand to execute shell scripts (bash or PowerShell) with proper argument handling. /// </summary> public class ScriptToolCommand : ToolCommand { private readonly string _scriptPath; private readonly TestEnvironment _testEnvironment; /// <summary> /// Creates a new command to execute a script. /// </summary> /// <param name="scriptPath">Relative path to the script from repo root (e.g., "eng/scripts/get-aspire-cli.sh")</param> /// <param name="testEnvironment">Test environment providing isolated temp directories</param> /// <param name="testOutput">xUnit test output helper</param> public ScriptToolCommand( string scriptPath, TestEnvironment testEnvironment, ITestOutputHelper testOutput) : base(GetExecutable(scriptPath), testOutput, label: Path.GetFileName(scriptPath)) { _scriptPath = scriptPath; _testEnvironment = testEnvironment; // Set mock HOME to prevent any accidental user directory access WithEnvironmentVariable("HOME", _testEnvironment.MockHome); WithEnvironmentVariable("USERPROFILE", _testEnvironment.MockHome); // Override XDG_CONFIG_HOME to prevent scripts that consult // ${XDG_CONFIG_HOME:-$HOME/.config} from reading a real profile // outside the temp home when the developer has XDG_CONFIG_HOME set. WithEnvironmentVariable("XDG_CONFIG_HOME", Path.Combine(_testEnvironment.MockHome, ".config")); // Disable any real PATH modifications during tests WithEnvironmentVariable("ASPIRE_TEST_MODE", "true"); // Default timeout to prevent hanging tests — individual tests can override via WithTimeout() WithTimeout(TimeSpan.FromSeconds(60)); } /// <summary> /// Determines the executable (bash or pwsh) based on the script extension. /// </summary> private static string GetExecutable(string scriptPath) { return scriptPath.EndsWith(".sh", StringComparison.OrdinalIgnoreCase) ? "bash" : "pwsh"; } /// <summary> /// Builds the full command arguments including the script path and user-provided arguments. /// </summary> protected override string GetFullArgs(params string[] args) { // Find the repo root var repoRoot = TestUtils.FindRepoRoot()?.FullName ?? throw new InvalidOperationException("Could not find repository root"); // Resolve the full script path var fullScriptPath = Path.Combine(repoRoot, _scriptPath); if (!File.Exists(fullScriptPath)) { throw new FileNotFoundException($"Script not found: {fullScriptPath}"); } // For bash: bash script.sh args... // For PowerShell: pwsh -File script.ps1 args... if (_scriptPath.EndsWith(".sh", StringComparison.OrdinalIgnoreCase)) { // Bash: bash script.sh arg1 arg2 — quote args containing spaces var escapedArgs = args.Select(arg => { if (arg.Contains(' ') || arg.Contains('"')) { return $"\"{arg.Replace("\"", "\\\"")}\""; } return arg; }); return $"\"{fullScriptPath}\" {string.Join(" ", escapedArgs)}"; } else { // PowerShell: pwsh -File script.ps1 arg1 arg2 var escapedArgs = args.Select(arg => { // Escape PowerShell special characters if needed if (arg.Contains(' ') || arg.Contains('"')) { return $"\"{arg.Replace("\"", "`\"")}\""; } return arg; }); return $"-File \"{fullScriptPath}\" {string.Join(" ", escapedArgs)}"; } } }